fix: harden wiki delivery and leaderboard performance
This commit is contained in:
1 parent
0c5e2e8cb0
commit
9f29ecae9e
28 files changed
+616
-68
No files matched your search
@@ -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> {
|
||||
|
||||
+39
-1
@@ -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
|
||||
|
||||
+7
-4
@@ -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 : [],
|
||||
|
||||
+9
-12
@@ -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} />;
|
||||
}
|
||||
Reference in new issue
Block a user