diff --git a/frontend-web/src/core/llmwiki/source-links.ts b/frontend-web/src/core/llmwiki/source-links.ts index 60f7f677..10bc99ee 100644 --- a/frontend-web/src/core/llmwiki/source-links.ts +++ b/frontend-web/src/core/llmwiki/source-links.ts @@ -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; } diff --git a/frontend-web/src/core/llmwiki/wiki-page-markdown.ts b/frontend-web/src/core/llmwiki/wiki-page-markdown.ts index 4ee93185..17a2a60f 100644 --- a/frontend-web/src/core/llmwiki/wiki-page-markdown.ts +++ b/frontend-web/src/core/llmwiki/wiki-page-markdown.ts @@ -37,6 +37,34 @@ const WIKI_INDEX_TYPE_LABELS: Record = { const WIKI_INDEX_TYPE_ORDER = ["summary", "entity", "concept"]; +export const WIKI_PROTECTED_IMAGE_MARKER = "/__llmwiki-protected-image__"; + +const PROTECTED_WIKI_IMAGE_SOURCE = + /(?:storage:\/\/[0-9A-Za-z_-]+\/)?(?:resource|local|minio|cos|tos|s3|oss|ks3|obs):\/\/[^\s"'<>\])]+/gi; + +/** + * Browsers cannot load WeKnora's protected resource:// and object-storage + * schemes directly. Replace them with a safe marker before Markdown + * sanitization; the image component resolves that marker through DeerFlow. + */ +export function rewriteProtectedWikiImageSources(markdown: string): string { + return markdown.replace( + PROTECTED_WIKI_IMAGE_SOURCE, + (filePath) => `${WIKI_PROTECTED_IMAGE_MARKER}?file_path=${encodeURIComponent(filePath)}`, + ); +} + +export function parseProtectedWikiImageSource(source: string | undefined): string | null { + if (!source) return null; + try { + const url = new URL(source, window.location.origin); + if (url.pathname !== WIKI_PROTECTED_IMAGE_MARKER) return null; + return url.searchParams.get("file_path")?.trim() || null; + } catch { + return null; + } +} + export function stripWikiFrontmatter(value: string) { return value.replace(/^\s*---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, "").trim(); } diff --git a/frontend-web/src/strategy-components/api/llmwiki.ts b/frontend-web/src/strategy-components/api/llmwiki.ts index d7df1d9f..40f0fd38 100644 --- a/frontend-web/src/strategy-components/api/llmwiki.ts +++ b/frontend-web/src/strategy-components/api/llmwiki.ts @@ -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,6 +466,24 @@ export async function getLlmWikiDocumentPreview( return response.blob(); } +export async function getLlmWikiProtectedFileUrl( + knowledgeBaseId: string, + filePath: string, + publicAccess = false, +): Promise { + const params = new URLSearchParams({ file_path: filePath }); + if (publicAccess) { + return `${PUBLIC_API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/files?${params.toString()}`; + } + 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 { return readJson( await apiFetch(`${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/graph?limit=200`), diff --git a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx index 272457d2..d013fa9c 100644 --- a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx +++ b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx @@ -33,6 +33,7 @@ import { reasoningPlugins } from "@/core/streamdown"; import { exportableWikiMarkdown, renderableWikiMarkdown, + rewriteProtectedWikiImageSources, safeWikiFileName, stripWikiFrontmatter, wikiLinkMarkdown, @@ -49,6 +50,7 @@ import { type LlmWikiDocument, type LlmWikiWikiPage, } from "@/strategy-components/api/llmwiki"; +import { WikiProtectedImage } from "./WikiProtectedImage"; type SourceContextResponse = { success?: boolean; @@ -926,10 +928,18 @@ export function WeKnoraSourceDrawer({ ( + + ), + }} /> @@ -964,10 +974,18 @@ export function WeKnoraSourceDrawer({
( + + ), + }} linkClassName={(href) => parseWeKnoraPreviewUrl(href)?.wikiSlug ? "wiki-content-link font-medium text-[#be2323] decoration-[#be2323]/55 decoration-dashed hover:decoration-solid" diff --git a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage.tsx b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage.tsx index 3eddeb72..ec06241b 100644 --- a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage.tsx +++ b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage.tsx @@ -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 失败; + } + if (item?.vectorized) { + return 已向量化 · {item.vector_count} 段; + } + return 未向量化; + }; + 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() { >
- {page.title || page.slug} + {page.title || page.slug} + {renderVectorBadge(page)}
{page.slug}
@@ -460,6 +477,7 @@ export function WeKnoraWikiManagementPage() {
{page.title || page.slug} + {renderVectorBadge(page)}
{page.slug}
{page.aliases?.length ? ( @@ -634,6 +652,26 @@ export function WeKnoraWikiManagementPage() {