fix: harden wiki delivery and leaderboard performance

This commit is contained in:
吉鑫升 committed 2026-08-25 16:57:42 +08:00
1 parent 0c5e2e8cb0
commit 9f29ecae9e
28 files changed
+616 -68

No files matched your search

+15 -4
View File
@@ -27,6 +27,16 @@ export type KnowledgeSourceLike = {
slug?: string | null;
};
/** Build a HashRouter URL without dropping a reverse-proxy path such as /web/. */
export function buildAppHashUrl(path: string): string {
const route = path.startsWith("/") ? path : `/${path}`;
if (typeof window !== "undefined") {
return `${window.location.pathname || "/"}#${route}`;
}
const base = import.meta.env.BASE_URL || "/";
return `${base}#${route}`;
}
export function parseWeKnoraPreviewUrl(url?: string | null): WeKnoraSourceTarget | null {
if (!url?.startsWith("/weknora-source-preview?")) {
return null;
@@ -92,7 +102,7 @@ export function buildWeKnoraSourceIframeUrl(target: WeKnoraSourceTarget): string
if (target.documentId) params.set("documentId", target.documentId);
if (target.wikiSlug) params.set("wikiSlug", target.wikiSlug);
const query = params.toString();
return `/#/page/workspace/knowledge/${encodeURIComponent(target.knowledgeBaseId)}${query ? `?${query}` : ""}`;
return buildAppHashUrl(`/page/workspace/knowledge/${encodeURIComponent(target.knowledgeBaseId)}${query ? `?${query}` : ""}`);
}
/**
@@ -103,7 +113,7 @@ export function buildWeKnoraWikiPageUrl(
knowledgeBaseId: string,
wikiSlug: string,
): string {
return `/#${buildWeKnoraWikiPagePath(knowledgeBaseId, wikiSlug)}`;
return buildAppHashUrl(buildWeKnoraWikiPagePath(knowledgeBaseId, wikiSlug));
}
export function buildWeKnoraWikiPagePath(
@@ -129,7 +139,7 @@ export function buildPublicKnowledgeWikiUrl(
knowledgeBaseId: string,
wikiSlug?: string,
): string {
return `/#${buildPublicKnowledgeWikiPath(knowledgeBaseId, wikiSlug)}`;
return buildAppHashUrl(buildPublicKnowledgeWikiPath(knowledgeBaseId, wikiSlug));
}
export function buildLegacyKnowledgeSourceUrl(source: KnowledgeSourceLike): string {
@@ -160,6 +170,7 @@ export function normalizeMalformedWeKnoraHashRoute(): boolean {
const { pathname, search, hash } = window.location;
if (!pathname.startsWith("/page/workspace/knowledge/")) return false;
if (hash && !hash.startsWith("#/")) return false;
window.location.replace(`/#${pathname}${search}`);
const base = import.meta.env.BASE_URL || "/";
window.location.replace(`${base}#${pathname}${search}`);
return true;
}
@@ -179,6 +179,28 @@ export interface LlmWikiWikiIndexStatus {
last_error?: string | null;
last_started_at?: string | null;
last_completed_at?: string | null;
progress?: {
total_pages: number;
processed_pages: number;
vectorized_pages: number;
failed_pages: number;
vector_count: number;
completion_percent: number;
};
pages?: Array<{
id: string;
slug: string;
title: string;
page_type: string;
status: string;
index_status: string;
vector_count: number;
vectorized: boolean;
processed_in_current_sync: boolean;
indexed_at?: string | null;
updated_at?: string | null;
last_error?: string | null;
}>;
}
export interface LlmWikiSourceContext {
@@ -444,21 +466,22 @@ export async function getLlmWikiDocumentPreview(
return response.blob();
}
export async function getLlmWikiProtectedFile(
export async function getLlmWikiProtectedFileUrl(
knowledgeBaseId: string,
filePath: string,
publicAccess = false,
): Promise<Blob> {
const base = publicAccess ? PUBLIC_API() : API();
): Promise<string> {
const params = new URLSearchParams({ file_path: filePath });
const response = await apiFetch(
`${base}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/files?${params.toString()}`,
);
if (!response.ok) {
await readJson<void>(response, "读取 Wiki 图片");
throw new Error("读取 Wiki 图片失败");
if (publicAccess) {
return `${PUBLIC_API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/files?${params.toString()}`;
}
return response.blob();
const access = await readJson<{ token: string; expires_in: number }>(
await apiFetch(
`${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/files/access-url?${params.toString()}`,
),
"读取 Wiki 图片",
);
return `${PUBLIC_API()}/files/${encodeURIComponent(access.token)}`;
}
export async function getLlmWikiGraph(knowledgeBaseId: string): Promise<LlmWikiGraph> {
@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Textarea } from "@/components/ui/textarea";
import { MarkdownCodeEditor } from "@/components/wiki/MarkdownCodeEditor";
import {
@@ -331,6 +332,10 @@ export function WeKnoraWikiManagementPage() {
() => moveTargetOptions(pagesList),
[pagesList],
);
const pageIndexBySlug = useMemo(
() => new Map((indexStatus.data?.pages ?? []).map((item) => [item.slug, item])),
[indexStatus.data?.pages],
);
const currentMoveTarget = moveTargetValue(draft.parentSlug, draft.categoryPathText, draft.folderId);
useEffect(() => {
@@ -387,6 +392,17 @@ export function WeKnoraWikiManagementPage() {
});
};
const renderVectorBadge = (page: WikiPageListItem) => {
const item = pageIndexBySlug.get(page.slug);
if (item?.last_error || item?.index_status === "failed" || item?.index_status === "stale") {
return <Badge variant="destructive" className="ml-auto shrink-0 text-[10px]" title={item?.last_error || "向量化失败"}>失败</Badge>;
}
if (item?.vectorized) {
return <Badge variant="secondary" className="ml-auto shrink-0 text-[10px]" title={`最近向量化:${item.indexed_at || "未知"}`}>已向量化 · {item.vector_count} 段</Badge>;
}
return <Badge variant="outline" className="ml-auto shrink-0 text-[10px]">未向量化</Badge>;
};
const renderWikiNode = (node: WikiTreeNode, depth = 0) => {
const hasChildren = node.children.length > 0;
const isExpanded = query.trim() ? true : expandedDirectories.has(node.key);
@@ -432,7 +448,8 @@ export function WeKnoraWikiManagementPage() {
>
<div className="flex min-w-0 items-center gap-2">
<FileText className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate font-medium">{page.title || page.slug}</span>
<span className="truncate font-medium">{page.title || page.slug}</span>
{renderVectorBadge(page)}
</div>
<div className="mt-1 truncate pl-5 text-xs text-muted-foreground">{page.slug}</div>
</button>
@@ -460,6 +477,7 @@ export function WeKnoraWikiManagementPage() {
<div className="flex min-w-0 items-center gap-2">
<FileText className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate font-medium">{page.title || page.slug}</span>
{renderVectorBadge(page)}
</div>
<div className="mt-1 truncate pl-5 text-xs text-muted-foreground">{page.slug}</div>
{page.aliases?.length ? (
@@ -634,6 +652,26 @@ export function WeKnoraWikiManagementPage() {
<main className="grid min-h-0 flex-1 grid-cols-[320px_1fr] overflow-hidden">
<aside className="flex min-h-0 flex-col border-r bg-muted/20">
<div className="border-b p-3">
{indexStatus.data && (
<div className="mb-3 rounded-lg border bg-background p-3">
<div className="flex items-center justify-between gap-2 text-xs">
<span className="flex items-center gap-2 font-medium">
向量化进度
{!indexStatus.data.local_index_enabled && <Badge variant="outline" className="text-[10px]">当前未启用</Badge>}
</span>
<span className="tabular-nums text-muted-foreground">
{indexStatus.data.progress?.processed_pages ?? 0}/{indexStatus.data.progress?.total_pages ?? 0} 篇 · {indexStatus.data.progress?.completion_percent ?? 0}%
</span>
</div>
<Progress value={indexStatus.data.progress?.completion_percent ?? 0} className="mt-2 h-2" />
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
<span>已向量化 {indexStatus.data.progress?.vectorized_pages ?? 0} 篇</span>
<span>向量章节 {indexStatus.data.progress?.vector_count ?? 0}</span>
<span className={(indexStatus.data.progress?.failed_pages ?? 0) > 0 ? "text-destructive" : ""}>失败 {indexStatus.data.progress?.failed_pages ?? 0} 篇</span>
</div>
{indexStatus.data.last_error && <div className="mt-2 break-words text-[11px] text-destructive">{indexStatus.data.last_error}</div>}
</div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
import {
buildAppHashUrl,
buildPublicKnowledgeWikiPath,
buildWeKnoraWikiPagePath,
} from "@/core/llmwiki/source-links";
@@ -98,9 +99,11 @@ function downloadBlob(blob: Blob, fileName: string) {
}
function publicWikiTargetFromHref(href: string, knowledgeBaseId: string) {
const routePrefix = `/#${buildPublicKnowledgeWikiPath(knowledgeBaseId)}`;
if (!href.startsWith(`${routePrefix}?`)) return null;
const route = href.slice(2);
const hashIndex = href.indexOf("#");
if (hashIndex < 0) return null;
const route = href.slice(hashIndex + 1);
const routePrefix = buildPublicKnowledgeWikiPath(knowledgeBaseId);
if (!route.startsWith(`${routePrefix}?`)) return null;
const queryIndex = route.indexOf("?");
const slug = new URLSearchParams(route.slice(queryIndex + 1)).get("wikiSlug")?.trim();
return slug ? { route, slug } : null;
@@ -219,7 +222,7 @@ export function WeKnoraWikiPageView({
? (knowledgeBaseId, slug) => {
const next = new URLSearchParams(searchParams);
next.set("wikiSlug", slug);
return `/#${buildPublicKnowledgeWikiPath(knowledgeBaseId)}?${next.toString()}`;
return buildAppHashUrl(`${buildPublicKnowledgeWikiPath(knowledgeBaseId)}?${next.toString()}`);
}
: undefined,
publicEmbed ? pagesList : [],
@@ -2,7 +2,7 @@ import { ImageOff, Loader2 } from "lucide-react";
import { type ImgHTMLAttributes, useEffect, useState } from "react";
import { parseProtectedWikiImageSource } from "@/core/llmwiki/wiki-page-markdown";
import { getLlmWikiProtectedFile } from "@/strategy-components/api/llmwiki";
import { getLlmWikiProtectedFileUrl } from "@/strategy-components/api/llmwiki";
type WikiProtectedImageProps = ImgHTMLAttributes<HTMLImageElement> & {
knowledgeBaseId: string;
@@ -18,24 +18,22 @@ export function WikiProtectedImage({
...props
}: WikiProtectedImageProps) {
const filePath = parseProtectedWikiImageSource(src);
const [objectUrl, setObjectUrl] = useState("");
const [httpUrl, setHttpUrl] = useState("");
const [error, setError] = useState("");
useEffect(() => {
if (!filePath || !knowledgeBaseId) {
setObjectUrl("");
setHttpUrl("");
setError("");
return;
}
let cancelled = false;
let nextObjectUrl = "";
setObjectUrl("");
setHttpUrl("");
setError("");
void getLlmWikiProtectedFile(knowledgeBaseId, filePath, publicAccess)
.then((blob) => {
void getLlmWikiProtectedFileUrl(knowledgeBaseId, filePath, publicAccess)
.then((url) => {
if (cancelled) return;
nextObjectUrl = URL.createObjectURL(blob);
setObjectUrl(nextObjectUrl);
setHttpUrl(url);
})
.catch((reason: unknown) => {
if (!cancelled) {
@@ -44,7 +42,6 @@ export function WikiProtectedImage({
});
return () => {
cancelled = true;
if (nextObjectUrl) URL.revokeObjectURL(nextObjectUrl);
};
}, [filePath, knowledgeBaseId, publicAccess]);
@@ -64,12 +61,12 @@ export function WikiProtectedImage({
</span>
);
}
if (!objectUrl) {
if (!httpUrl) {
return (
<span className="my-3 flex min-h-24 items-center justify-center rounded-lg bg-muted/30 text-muted-foreground">
<Loader2 className="size-5 animate-spin" aria-label="正在加载 Wiki 图片" />
</span>
);
}
return <img {...props} src={objectUrl} alt={alt} className={className} />;
return <img {...props} src={httpUrl} alt={alt} className={className} />;
}