From 60e2c72514cf82229399e8a07ca77fb2bcd01ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Thu, 3 Sep 2026 19:21:10 +0800 Subject: [PATCH] feat: preserve Wiki vectors across knowledge transfers and add local BGE-M3 Unify Wiki reading, preserve metadata and graph links, add asynchronous file-backed transfers, and reuse compatible vectors. Includes regression coverage and live acceptance notes. --- .gitignore | 7 +- docs/WIKI_ROUNDTRIP_ACCEPTANCE_20260903.md | 117 +++ .../workspace/skills/SkillKnowledgePanel.tsx | 6 +- .../src/pages/AssistantKnowledgePage.tsx | 726 +++++++++--------- .../api/assistant-knowledge.ts | 257 +++++-- .../src/strategy-components/api/llmwiki.ts | 1 + .../llmwiki/WeKnoraKnowledgeDetailPage.tsx | 2 + .../llmwiki/WeKnoraKnowledgeWorkspace.tsx | 108 ++- .../llmwiki/WeKnoraWikiPageView.tsx | 11 +- .../llmwiki/WikiLibraryReader.tsx | 449 +++++++++++ .../llmwiki/WikiPackageTransfers.tsx | 126 +++ offline-backend-20260512/backend/CLAUDE.md | 32 + offline-backend-20260512/backend/README.md | 51 ++ .../backend/app/gateway/app.py | 3 + .../backend/app/gateway/deps.py | 14 +- .../backend/app/gateway/knowledge_transfer.py | 28 + .../gateway/routers/assistant_knowledge.py | 93 ++- .../backend/app/gateway/routers/llmwiki.py | 18 +- .../app/gateway/routers/llmwiki_index.py | 2 +- .../app/gateway/routers/skill_knowledge.py | 14 +- .../app/gateway/routers/wiki_packages.py | 193 +++++ .../gateway/skill_knowledge_job_executor.py | 65 +- offline-backend-20260512/backend/config.yaml | 8 +- .../deerflow/assistant_knowledge/archive.py | 79 ++ .../harness/deerflow/config/llmwiki_config.py | 9 +- .../deerflow/integrations/weknora/client.py | 15 + .../weknora/local_index/chunking.py | 8 + .../weknora/local_index/embedding.py | 47 ++ .../integrations/weknora/local_index/sync.py | 9 +- .../persistence/assistant_knowledge/model.py | 1 + .../persistence/assistant_knowledge/store.py | 197 +++-- .../persistence/llmwiki_index/memory.py | 2 +- .../deerflow/persistence/llmwiki_index/sql.py | 7 +- .../20260903_01_assistant_wiki_metadata.py | 21 + .../backend/pyproject.toml | 1 + .../test_assistant_knowledge_repository.py | 3 + .../test_llmwiki_knowledge_management.py | 180 +++++ .../tests/test_wiki_package_roundtrip.py | 207 +++++ 38 files changed, 2536 insertions(+), 581 deletions(-) create mode 100644 docs/WIKI_ROUNDTRIP_ACCEPTANCE_20260903.md create mode 100644 frontend-web/src/strategy-components/components/resource-management/llmwiki/WikiLibraryReader.tsx create mode 100644 frontend-web/src/strategy-components/components/resource-management/llmwiki/WikiPackageTransfers.tsx create mode 100644 offline-backend-20260512/backend/app/gateway/knowledge_transfer.py create mode 100644 offline-backend-20260512/backend/app/gateway/routers/wiki_packages.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/assistant_knowledge/archive.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260903_01_assistant_wiki_metadata.py create mode 100644 offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py create mode 100644 offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py diff --git a/.gitignore b/.gitignore index d7f7d34a..8debb7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ offline-backend-20260512/backend/tests/* # Keep the openGauss URL/dialect/schema compatibility regression coverage versioned. !offline-backend-20260512/backend/tests/test_gauss_database.py !offline-backend-20260512/backend/tests/test_llmwiki_weknora.py +!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py !offline-backend-20260512/backend/tests/test_health_auth_bypass.py # Keep formal Word OOXML and embedded-font regression coverage versioned. !offline-backend-20260512/backend/tests/test_word_export.py @@ -172,8 +173,10 @@ docs/backups # Frontend skills components must be tracked (excluded from the broad skills/ ignore above) !frontend-web/src/components/workspace/skills/ -# uv lockfile (not tracked per project preference) -offline-backend-20260512/backend/uv.lock +# uv lockfile (not tracked per project preference) +offline-backend-20260512/backend/uv.lock + +!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py # Local-only scratch directory (not committed) 0622/ diff --git a/docs/WIKI_ROUNDTRIP_ACCEPTANCE_20260903.md b/docs/WIKI_ROUNDTRIP_ACCEPTANCE_20260903.md new file mode 100644 index 00000000..69d8a887 --- /dev/null +++ b/docs/WIKI_ROUNDTRIP_ACCEPTANCE_20260903.md @@ -0,0 +1,117 @@ +# Wiki 数据流转与本地编码验收(2026-09-03) + +## 结论 + +本地 BGE-M3 小批量编码已安装、配置并实际运行。已有 Wiki 的向量化、自动沉淀、 +普通库文件导出、助手库异步导入、助手库文件导出、空普通库回流及两端向量检索已实测通过。 +**技能自动生成 Wiki 尚未完成验收**:WeKnora 的两个生成模型都返回 HTTP 402 +`Insufficient Balance`。不能把后半段通过等同于全链路通过。 + +本次修改运行在本地 DeerFlow 开发服务(前端 5175、后端 8001),连接用户提供的远端 +WeKnora;没有把业务代码发布到服务器,没有提交 Git,也没有直接写 WeKnora 图数据库。 + +## 本地编码配置 + +- 实现:FastEmbed / ONNX Runtime CPU,官方 `BAAI/bge-m3` 权重。 +- 配置名:`bge-embedding-m3`,1024 维,每批 2 条,2 个 CPU 推理线程。 +- 运行依赖:后端 `local-embedding` extra;当前本地虚拟环境已安装。 +- 权重:约 2.3 GB,持久缓存 `C:\Users\ji\.cache\deerflow\embeddings`,不进入业务仓库。 +- 配置:`llmwiki.local_wiki_index.embedding.provider: local`;可用 + `local_model_path` 指定离线权重目录,详见后端 README。 +- 查询向量在本地实时编码。已有 Wiki 在内容哈希、模型指纹匹配且向量非空时直接复用。 + 不同模型或维度不能混用;导入不静默重新编码。 +- 原始文档分段向量与模型生成后的 Wiki 文本不同,不能相互冒用。本次新建的是 Wiki + 正文的 BGE-M3 索引,不是把既有 512 维文档向量改名为 1024 维。 + +## 实际数据验收 + +来源是已有普通知识库“图片测试111”,没有改写其 Wiki 内容。 + +| 阶段 | Wiki | Wiki 向量 | 实体 | 引用关系 | +| --- | ---: | ---: | ---: | ---: | +| 普通知识库导出 | 19 | 101 | 19 | 108 | +| 助手知识库导入后导出 | 19 | 101 | 19 | 108 | +| 新建普通知识库回流后导出 | 19 | 101 | 19 | 108 | +| 知识梳理总库导出(7 个来源记录) | 19 | 101 | 19 | 108 | + +逐项断言通过: + +1. 三份包的 Wiki slug、标题、正文、摘要、页面类型、别名、分类路径、出链一致。 +2. 101 条向量的原始 float32 字节、分段编号、维度和模型指纹完全一致。 +3. 将导入前后实体 ID 映射回原 Wiki slug 后,108 条关系的端点及谓词集合完全一致。 +4. 总库导出没有因多个来源重复同步而重复导出同一当前页面的分段向量。 +5. 元数据回归测试另覆盖 tags、目录恢复、原始文档片段不计入 Wiki 数量、原生技能无 + ownership 记录时可被选中、短 Wiki 不漏向量、模型指纹不兼容时不假装向量检索。 + +普通包约 1.01 MB,助手包约 0.79 MB,回流包约 0.83 MB。普通包还带 2 个原始文档及 +108 个原始片段;助手导出提供 Wiki、Wiki 向量和关系,不重新上传原始文档触发二次生成。 + +### 检索 + +实际查询:“军方用什么工具评估极端天气带来的风险?” + +- 回流普通库 `/api/llmwiki/search` 返回 `mode=vector`,首条“国防气候评估工具”。 +- 助手库 `/api/assistant-knowledge/search` 返回 `retrieval_mode=vector`,首条同上; + 首个命中分段余弦相似度约 0.6343。 +- 浏览器实际提交总库检索,显示“向量检索 · 余弦相似度”和相同命中;不是只检查返回字段。 +- 普通库采用已有页面级聚合评分,因此页面最终分数与单分段余弦值不必相等。 +- 回流的向量写入 **DeerFlow 管理的本地 Wiki 索引**。未写入 WeKnora 原生向量数据库, + 本次结论适用于本系统问答检索,不能声称原生 WeKnora 自身的检索也已完成该向量导入。 + +### 页面 + +普通与助手库复用同一 Wiki 阅读组件:目录树、类型/标签筛选、分类面包屑、别名标签、 +正文内链、出链、反向链接及邻接关系视图。窄窗口可收起目录。 + +浏览器验证了“索引 → 国防气候评估工具”、展开关系视图、助手 Wiki 跳回普通库来源, +以及普通 Wiki 的“在知识梳理中查看”反向跳转。 + +### 验收数据位置 + +- 原有普通库:`48f669dc-433e-4aab-b5ea-897b6f7a3d2f` +- 知识梳理总库:`c3fd9841-0718-44dc-860e-aae3020fd485` +- 最终助手测试库:`489be905-08c8-40fe-8d60-42ac21dd945a` +- 最终普通回流测试库:`428162aa-03f6-4c61-8994-360f2f39e0c9` + +新建测试库名称均以“Wiki链路验收-0903-1826”开头,保留用于复核;未删除用户已有库。 +本地 `.runtime/wiki-live-result.json` 保存验收结果,`.runtime/wiki_live_acceptance.py` +保存本次验收流程,均为忽略的运行产物,不包含认证口令。 + +## 这次实际修复的问题 + +- 内置技能只有磁盘目录、没有归属记录时,被错误判定为不存在。 +- 技能应上传去代码后的知识正文,让 WeKnora 生成 Wiki,而不是伪造一个 `skill` 类型页面。 +- Wiki 生成等待使用实际 stats 接口;零页结果明确失败,不无期限等待。 +- 向量化完成后自动导出沉淀到总库,并阻断导出回调重复触发全量同步。 +- 短摘要和短正文同时存在时,旧分段阈值会将整页过滤为零向量。 +- 分类路径必须先创建 WeKnora folder 并绑定 folder_id;只传 category_path 会被服务端清空。 +- Wiki 的标签/分类/别名/原始引用保留;图节点绑定真实 Wiki,避免生成重复的空实体页面。 +- 保留归一化 float32 原始字节;导入中不能用 `section_index or index`,因为合法编号 0 会被改写。 +- 总库多个来源保留审计,但检索与导出只使用当前 Wiki 版本对应的向量,避免旧贡献污染检索。 +- 包解析改为同盘临时 SQLite,上传先落盘再异步导入,导出完成后才提供文件下载。 + +## 阻塞与未通过的范围 + +1. 技能 `knowledge-base-ingest` 已送入专门创建的普通测试库,但远端 + `deepseek-v4-flash` 和 `qwen3.6-flash` 均因余额不足失败。WeKnora 即使失败也可能把 + 文档 parse_status 标为 completed。需恢复可用生成模型后重新运行技能全链路验收。 +2. 已采用磁盘暂存和有界批处理,但本次只有约 1 MB 实测,**未做数 GB 压测、断点续传、 + 进程中断恢复验收**。大包部署仍需磁盘容量、代理 body size 和超时配置。 +3. 当前实体去重主要按类型及规范化名称;这次没有完成跨别名、歧义实体的模型对齐及 + 多来源事实冲突合并验收,不能视为这一历史需求已全面完成。 +4. 重复上传仍会保留独立来源记录。向量导出已消除旧版本重复,但上传身份幂等、部分远端 + 写入失败后的无损重试、跨实例图片资源打包仍需独立验证。 +5. 自动沉淀挂在本系统成功向量化完成后;未新增文件监听或定时远程写入。直接在外部 + WeKnora 修改的内容仍需通过本系统同步/向量化入口接入。 +6. 回流当前只允许新建空 Wiki 普通库,避免覆盖已有页面。普通库现有页面列表的超大规模 + 分页与助手问答的最终生成回答不属于这轮通过结论。 + +## 自动化检查 + +- 后端相关测试:46 passed(assistant repository、Wiki sync、package roundtrip、技能扫描、 + 技能关系仓储、知识库管理)。 +- 修改的 Python 功能文件 Ruff 检查通过;app.py 既有启动顺序导致的 E402 不在此次改动中处理。 +- `git diff --check` 通过(只有仓库换行提示)。 +- 前端完整 `pnpm typecheck` 未通过:57 个既有/本轮范围外错误,涉及 nextra 缺失、Markdown + 插件类型及其他模块;本轮新建/修改的 Wiki 组件、助手 API、技能面板没有出现在错误清单中。 + 浏览器实际页面与跳转已验证,但不能声称全项目构建通过。 diff --git a/frontend-web/src/components/workspace/skills/SkillKnowledgePanel.tsx b/frontend-web/src/components/workspace/skills/SkillKnowledgePanel.tsx index c93a520c..947dfe84 100644 --- a/frontend-web/src/components/workspace/skills/SkillKnowledgePanel.tsx +++ b/frontend-web/src/components/workspace/skills/SkillKnowledgePanel.tsx @@ -25,6 +25,10 @@ const PHASE_LABELS: Record = { awaiting_review: "等待审核", merging: "合并实体关系", writing_wiki: "写入 Wiki", + writing_sources: "上传知识资料", + generating_wiki: "分析并生成分类 Wiki", + vectorizing_wiki: "Wiki 向量化", + syncing_assistant: "同步知识梳理总库", activating: "激活新版本", completed: "完成", }; @@ -35,7 +39,7 @@ export function SkillKnowledgePanel() { const [selectedSkills, setSelectedSkills] = useState([]); const [skillQuery, setSkillQuery] = useState(""); const [selectedTargetKey, setSelectedTargetKey] = useState(null); - const [reviewMode, setReviewMode] = useState<"auto_high_confidence" | "off">("auto_high_confidence"); + const [reviewMode, setReviewMode] = useState<"auto_high_confidence" | "off">("off"); const runtime = useLlmWikiRuntime(); const knowledgeBases = useLlmWikiKnowledgeBases("all", runtime.data?.provider === "weknora"); const bindings = useSkillKnowledgeBindings(); diff --git a/frontend-web/src/pages/AssistantKnowledgePage.tsx b/frontend-web/src/pages/AssistantKnowledgePage.tsx index 444eac15..55a6d2ea 100644 --- a/frontend-web/src/pages/AssistantKnowledgePage.tsx +++ b/frontend-web/src/pages/AssistantKnowledgePage.tsx @@ -1,443 +1,425 @@ -import { BookOpen, Database, Download, Loader2, Plus, RefreshCw, Search, ShieldCheck, Trash2, Upload } from "lucide-react"; +import { + ArrowLeft, + BookOpenText, + Database, + Loader2, + Plus, + RefreshCw, + Search, + Upload, +} from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; - import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, - DialogDescription, - DialogFooter, DialogHeader, DialogTitle, + DialogFooter, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; import { KnowledgeProductNav } from "@/components/workspace/knowledge-product-nav"; -import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; import { getStoredAuth } from "@/core/auth"; +import { buildAppHashUrl } from "@/core/llmwiki/source-links"; import { - type AssistantKnowledgeBase, - type AssistantWikiPage, + searchAssistantWiki, useAssistantImportJobs, useAssistantKnowledgeActions, useAssistantKnowledgeBases, useAssistantKnowledgeSources, useAssistantWikiPages, } from "@/strategy-components/api/assistant-knowledge"; - -function baseSubtitle(base: AssistantKnowledgeBase) { - if (base.is_global) return "全库沉淀总库 · 所有 WeKnora 导入数据都会同步进入这里并做实体对齐"; - return base.description || "自建助手知识库 · 可上传普通知识库导出的 Wiki 向量包"; -} - -function EmptyInitializeCard({ - isAdmin, - onInitialize, - pending, -}: { - isAdmin: boolean; - onInitialize: () => void; - pending: boolean; -}) { - return ( -
-
- -

初始化助手知识库

-

- 未配置 WeKnora 时会默认展示助手知识库。首次使用需要创建“知识梳理总库”,后续从 WeKnora 导入的数据都会沉淀到这里。 -

- {isAdmin ? ( - - ) : ( -

请联系管理员初始化。

- )} -
-
- ); -} +import { + WikiLibraryReader, + type LibraryWikiPage, +} from "@/strategy-components/components/resource-management/llmwiki/WikiLibraryReader"; +import { WikiPackageTransfers } from "@/strategy-components/components/resource-management/llmwiki/WikiPackageTransfers"; export default function AssistantKnowledgePage() { const isAdmin = getStoredAuth()?.system_role === "admin"; - const [query, setQuery] = useState(""); - const [selectedBaseId, setSelectedBaseId] = useState(null); - const [selected, setSelected] = useState(null); - const [createOpen, setCreateOpen] = useState(false); - const [createDraft, setCreateDraft] = useState({ name: "", description: "" }); - const packageInputRef = useRef(null); + const [params, setParams] = useSearchParams(); + const baseId = params.get("base_id"); + const slug = params.get("wikiSlug"); const bases = useAssistantKnowledgeBases(); const actions = useAssistantKnowledgeActions(); - - useEffect(() => { - const values = bases.data ?? []; - if (!values.length) { - setSelectedBaseId(null); - return; - } - if (selectedBaseId && values.some((base) => base.id === selectedBaseId)) return; - setSelectedBaseId((values.find((base) => base.is_global) ?? values[0]).id); - }, [bases.data, selectedBaseId]); - - useEffect(() => { - setSelected(null); - }, [selectedBaseId]); - - const selectedBase = useMemo( - () => (bases.data ?? []).find((base) => base.id === selectedBaseId) ?? null, - [bases.data, selectedBaseId], + const pages = useAssistantWikiPages("", baseId); + const sources = useAssistantKnowledgeSources(baseId); + const jobs = useAssistantImportJobs(baseId); + const client = useQueryClient(); + const [tab, setTab] = useState("wiki"); + const [createOpen, setCreateOpen] = useState(false); + const [name, setName] = useState(""); + const [query, setQuery] = useState(""); + const [searching, setSearching] = useState(false); + const [searchResult, setSearchResult] = useState + > | null>(null); + const upload = useRef(null); + const base = bases.data?.find((b) => b.id === baseId); + const libraryPages = useMemo( + () => + (pages.data || []).map((p) => ({ + ...p, + content: p.content_markdown, + status: "published", + aliases: p.aliases || [], + source_refs: p.source_refs || [], + in_links: p.in_links || [], + out_links: p.out_links || [], + version: 1, + })), + [pages.data], ); - const pages = useAssistantWikiPages(query, selectedBaseId); - const sources = useAssistantKnowledgeSources(selectedBaseId); - const jobs = useAssistantImportJobs(selectedBaseId); - const activePage = selected ?? pages.data?.[0] ?? null; - const counts = useMemo( - () => ({ - pages: pages.data?.length ?? selectedBase?.page_count ?? 0, - sources: sources.data?.length ?? selectedBase?.source_count ?? 0, - jobs: jobs.data?.length ?? 0, - }), - [jobs.data, pages.data, selectedBase?.page_count, selectedBase?.source_count, sources.data], - ); - - const initializeGlobal = () => { - actions.initialize.mutate("知识梳理总库", { - onSuccess: (base) => { - setSelectedBaseId(base.id); - toast.success("知识梳理总库已创建"); - }, - onError: (error) => toast.error(error.message), + const active = + libraryPages.find((p) => p.slug === slug) || + libraryPages.find((p) => p.page_type === "index") || + libraryPages[0] || + null; + const navigatePage = (nextSlug: string) => { + setParams({ base_id: baseId!, wikiSlug: nextSlug }); + setTab("wiki"); + }; + const finishedJobs = (jobs.data || []) + .filter((j) => j.status === "completed") + .map((j) => j.id) + .join(","); + useEffect(() => { + void client.invalidateQueries({ + queryKey: ["assistant-knowledge", "pages"], }); - }; - - const createBase = async () => { - const name = createDraft.name.trim(); - if (!name) { - toast.error("请输入助手知识库名称"); - return; - } + void client.invalidateQueries({ + queryKey: ["assistant-knowledge", "bases"], + }); + }, [finishedJobs, client]); + useEffect(() => { + setSearchResult(null); + setQuery(""); + }, [baseId]); + const create = async () => { + if (!name.trim()) return; try { - const base = await actions.createBase.mutateAsync({ - name, - description: createDraft.description.trim(), - }); - setSelectedBaseId(base.id); - setCreateDraft({ name: "", description: "" }); + const b = await actions.createBase.mutateAsync({ name: name.trim() }); setCreateOpen(false); - toast.success("助手知识库已创建"); - } catch (error) { - toast.error(error instanceof Error ? error.message : String(error)); + setName(""); + setParams({ base_id: b.id }); + } catch (e) { + toast.error((e as Error).message); } }; - - const uploadPackage = async (file: File | null | undefined) => { - if (!file) return; + const search = async () => { + if (!query.trim() || !baseId) return; + setSearching(true); try { - await actions.uploadWeKnoraPackage.mutateAsync({ - file, - assistantBaseId: selectedBaseId, - }); - toast.success("导出包已上传,后台正在异步导入"); - } catch (error) { - toast.error(error instanceof Error ? error.message : String(error)); + setSearchResult(await searchAssistantWiki(query, baseId)); + } catch (e) { + toast.error((e as Error).message); } finally { - if (packageInputRef.current) packageInputRef.current.value = ""; + setSearching(false); } }; - - if (bases.isLoading) { - return ( -
- -
- -
-
- ); - } - - if (!bases.data?.length) { - return ( -
- - -
- ); - } - + const refresh = () => + void client.invalidateQueries({ queryKey: ["assistant-knowledge"] }); return (
-
- - -
-
-
-

{selectedBase?.name}

-

- {selectedBase ? baseSubtitle(selectedBase) : "请选择助手知识库"} -

-
- - - 实体对齐 - - {isAdmin && ( -
- void uploadPackage(event.target.files?.[0])} + + + buildAppHashUrl( + `/page/workspace/knowledge/assistant?${new URLSearchParams({ base_id: baseId, wikiSlug: s })}`, + ) + } + mappingId={ + active?.origins?.find((o) => o.mapping_id)?.mapping_id + } + /> + + +
+ setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void search(); + }} /> -
- )} -
- - -
- - Wiki({counts.pages}) - 来源({counts.sources}) - 导入记录({counts.jobs}) - -
- - setQuery(event.target.value)} - placeholder="搜索标题、实体别名和正文" - className="pl-8" - /> -
-
- - - -
- {pages.isLoading && } - {pages.data?.map((page) => ( - - ))} - {!pages.isLoading && !pages.data?.length && ( -
- 暂无 Wiki 页面。请先到普通知识库详情页下载向量化数据包,再在当前助手知识库上传。 -
- )} -
-
- -
- {activePage ? ( - <> -

- - {activePage.title} -

-

{activePage.slug}

- - - ) : ( -

暂无 Wiki 页面。

- )} -
-
+ {searchResult && ( + <> + + {searchResult.retrieval_mode === "vector" + ? "向量检索 · 余弦相似度" + : searchResult.retrieval_mode === "empty" + ? "没有匹配结果" + : "关键词检索(暂无匹配模型的向量)"} + +
+ {searchResult.results.map((p) => ( + + ))} +
+ + )}
- - -
- {sources.data?.map((source) => ( -
-
- - {source.source_name} - {source.source_type} -
-

业务键:{source.source_key}

+ +
+ {sources.data?.map((s) => ( +
+

{s.source_name}

+

+ 上次同步:{new Date(s.updated_at).toLocaleString()} +

{isAdmin && ( -
- - -
+ )}
))} - {!sources.data?.length &&

暂无来源。

}
- - -
- {jobs.data?.map((job) => ( -
+ {isAdmin && ( + + +

导入与同步记录

+ {jobs.data?.map((j) => ( +
- {job.status} - {job.trigger} - 阶段:{job.phase} - {new Date(job.created_at).toLocaleString()} - {isAdmin && job.package_download_url && ( - - )} + + {{ + completed: "已完成", + failed: "失败", + running: "处理中", + queued: "排队中", + }[j.status] || j.status} + + + {new Date(j.created_at).toLocaleString()} +
-
- {Object.entries(job.counts || {}).map(([key, value]) => `${key} ${value}`).join(" · ") || "暂无统计"} -
- {job.last_error_message && ( -
- {job.last_error_message} -
+

+ {j.counts?.wiki_pages || 0} Wiki ·{" "} + {j.counts?.vectors || 0} 向量 · {j.counts?.entities || 0}{" "} + 实体 · {j.counts?.relations || 0} 关系 +

+ {j.last_error_message && ( +

+ {j.last_error_message} +

)}
))} - {!jobs.data?.length &&

暂无导入记录。

} -
- + + )} -
-
- + + )} 新建助手知识库 - - 自建助手知识库用于承接从 WeKnora 普通知识库下载的 Wiki 向量包;导入时同一份数据也会沉淀到“知识梳理总库”。 - -
-
- - setCreateDraft((prev) => ({ ...prev, name: event.target.value }))} - placeholder="例如:政策资料库" - /> -
-
- -