From e0c09f6704ce5a9cf3e0c1505bd843ab24e592a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Tue, 11 Aug 2026 18:13:30 +0800 Subject: [PATCH 1/2] feat: integrate WeKnora llmwiki knowledge flow --- .gitignore | 32 +- .vscode/extensions.json | 6 + .vscode/launch.json | 66 + .vscode/tasks.json | 52 + .../src/components/wiki/WikiHeader.tsx | 6 +- .../workspace/agents/agent-detail-panel.tsx | 24 + .../src/components/workspace/input-box.tsx | 33 +- .../workspace/llmwiki-knowledge-selector.tsx | 353 ++++ .../workspace/skill-display-sources.tsx | 2 +- frontend-web/src/core/agents/types.ts | 12 +- .../src/core/llmwiki/conversation-deposit.ts | 66 + frontend-web/src/core/threads/hooks.ts | 26 +- frontend-web/src/core/threads/types.ts | 4 +- frontend-web/src/pages/AgentChatPage.tsx | 143 +- frontend-web/src/pages/ChatPage.tsx | 113 +- frontend-web/src/pages/KnowledgeBasePage.tsx | 9 +- .../src/pages/LlmWikiKnowledgePage.tsx | 91 + frontend-web/src/pages/NewAgentPage.tsx | 12 + frontend-web/src/pages/WorkspaceRoutes.tsx | 9 +- .../src/strategy-components/api/llmwiki.ts | 689 +++++++ .../llmwiki/WeKnoraKnowledgeDetailPage.tsx | 152 ++ .../llmwiki/WeKnoraKnowledgeWorkspace.tsx | 318 ++++ offline-backend-20260512/README.md | 13 +- offline-backend-20260512/backend/CLAUDE.md | 11 +- offline-backend-20260512/backend/README.md | 17 +- .../backend/app/gateway/app.py | 57 +- .../backend/app/gateway/auth_middleware.py | 27 +- .../backend/app/gateway/deps.py | 10 +- .../backend/app/gateway/llmwiki_deposit.py | 241 +++ .../backend/app/gateway/llmwiki_rag.py | 290 +++ .../backend/app/gateway/routers/agents.py | 109 +- .../backend/app/gateway/routers/llmwiki.py | 1582 +++++++++++++++++ .../backend/app/gateway/routers/threads.py | 24 +- .../backend/app/gateway/services.py | 31 + offline-backend-20260512/backend/config.yaml | 29 +- .../docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md | 105 ++ .../backend/extensions_config.json | 24 +- .../community/configurable_search/__init__.py | 3 + .../community/configurable_search/tools.py | 202 +++ .../harness/deerflow/config/agents_config.py | 19 +- .../harness/deerflow/config/app_config.py | 15 +- .../harness/deerflow/config/llmwiki_config.py | 117 ++ .../deerflow/config/system_settings.py | 2 + .../harness/deerflow/config/tool_config.py | 41 +- .../harness/deerflow/integrations/__init__.py | 2 + .../deerflow/integrations/weknora/__init__.py | 5 + .../deerflow/integrations/weknora/client.py | 443 +++++ .../deerflow/integrations/weknora/runtime.py | 43 + .../deerflow/persistence/llmwiki/__init__.py | 22 + .../deerflow/persistence/llmwiki/base.py | 72 + .../deerflow/persistence/llmwiki/memory.py | 190 ++ .../deerflow/persistence/llmwiki/model.py | 63 + .../deerflow/persistence/llmwiki/sql.py | 244 +++ .../deerflow/persistence/models/__init__.py | 3 + .../harness/deerflow/runtime/references.py | 53 +- .../tools/builtins/llmwiki_search_tool.py | 132 ++ .../packages/harness/deerflow/tools/tools.py | 24 +- .../tests/test_configurable_web_search.py | 229 +++ .../backend/tests/test_health_auth_bypass.py | 100 ++ .../backend/tests/test_llmwiki_weknora.py | 418 +++++ .../offline-runtime-requirements.txt | 4 +- .../prepare_wheelhouse.sh | 2 + offline-backend-20260512/start_backend.sh | 6 + 63 files changed, 7045 insertions(+), 197 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 frontend-web/src/components/workspace/llmwiki-knowledge-selector.tsx create mode 100644 frontend-web/src/core/llmwiki/conversation-deposit.ts create mode 100644 frontend-web/src/pages/LlmWikiKnowledgePage.tsx create mode 100644 frontend-web/src/strategy-components/api/llmwiki.ts create mode 100644 frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx create mode 100644 frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeWorkspace.tsx create mode 100644 offline-backend-20260512/backend/app/gateway/llmwiki_deposit.py create mode 100644 offline-backend-20260512/backend/app/gateway/llmwiki_rag.py create mode 100644 offline-backend-20260512/backend/app/gateway/routers/llmwiki.py create mode 100644 offline-backend-20260512/backend/docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/community/configurable_search/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/community/configurable_search/tools.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/config/llmwiki_config.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/integrations/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/integrations/weknora/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/integrations/weknora/client.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/integrations/weknora/runtime.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/llmwiki/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/llmwiki/base.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/llmwiki/memory.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/llmwiki/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/llmwiki/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/llmwiki_search_tool.py create mode 100644 offline-backend-20260512/backend/packages/harness/tests/test_configurable_web_search.py create mode 100644 offline-backend-20260512/backend/tests/test_health_auth_bypass.py create mode 100644 offline-backend-20260512/backend/tests/test_llmwiki_weknora.py diff --git a/.gitignore b/.gitignore index 7f1164a1..444a95df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Workspace/editor -.vscode/ -frontend-web/.vscode/ +.vscode/* +!.vscode/launch.json +!.vscode/tasks.json +!.vscode/extensions.json +frontend-web/.vscode/ node_modules/ # Claude Code local-only settings (per-user permissions, not shared) .claude/settings.local.json @@ -52,8 +55,9 @@ offline-backend-20260505/bundle/start_backend.sh offline-backend-20260505/bundle/stop_backend.sh # offline-backend-20260512 runtime artifacts (large files, local config, data) -offline-backend-20260512/.env.example -offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md +offline-backend-20260512/.env.example +offline-backend-20260512/.env +offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md offline-backend-20260512/SHA256SUMS offline-backend-20260512/config.yaml.example offline-backend-20260512/data/ @@ -61,12 +65,14 @@ offline-backend-20260512/deerflow-backend-offline-image-20260505.tar offline-backend-20260512/extensions_config.json offline-backend-20260512/skills/ offline-backend-20260512/wheelhouse/ -offline-backend-20260512/backend/.deer-flow/data/*.db -offline-backend-20260512/backend/.deer-flow/data/*.db-* -offline-backend-20260512/backend/.deer-flow/* -offline-backend-20260512/start_backend.sh -offline-backend-20260512/stop_backend.sh -offline-backend-20260512/check_backend.sh +offline-backend-20260512/backend/.deer-flow/data/*.db +offline-backend-20260512/backend/.deer-flow/data/*.db-* +offline-backend-20260512/backend/.deer-flow/* +offline-backend-20260512/backend/.env +offline-backend-20260512/backend/.env.local +offline-backend-20260512/start_backend.sh +offline-backend-20260512/stop_backend.sh +offline-backend-20260512/check_backend.sh hermes-agent-main/ @@ -80,8 +86,10 @@ offline-backend-20260512/backend/skill_evolution.json skills/ hermes-agent-main/ .gitignore -offline-backend-20260512/backend/tests/* -deploy +offline-backend-20260512/backend/tests/* +!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py +!offline-backend-20260512/backend/tests/test_health_auth_bypass.py +deploy .history # Cloned third-party repos (local only) diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..596ef561 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.debugpy" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..f2ad5591 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,66 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "DeerFlow Backend FastAPI (8001)", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "app.gateway.app:app", + "--host", + "0.0.0.0", + "--port", + "8001", + "--reload" + ], + "cwd": "${workspaceFolder}/offline-backend-20260512/backend", + "python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe", + "envFile": "${workspaceFolder}/offline-backend-20260512/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}/offline-backend-20260512/backend;${workspaceFolder}/offline-backend-20260512/backend/.deer-flow/python-vendor", + "GATEWAY_CORS_ORIGINS": "http://localhost:5174,http://127.0.0.1:5174,http://localhost:8080,http://127.0.0.1:8080" + }, + "console": "integratedTerminal", + "justMyCode": false, + "subProcess": true + }, + { + "name": "DeerFlow Frontend Vite (5174)", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "runtimeArgs": [ + "exec", + "vite", + "--host", + "0.0.0.0", + "--port", + "5174" + ], + "cwd": "${workspaceFolder}/frontend-web", + "env": { + "VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001", + "VITE_LANGGRAPH_BASE_URL": "http://127.0.0.1:8001/api" + }, + "console": "integratedTerminal", + "outputCapture": "std", + "serverReadyAction": { + "pattern": "Local:\\s+(https?://[^\\s]+)", + "uriFormat": "%s", + "action": "debugWithChrome", + "webRoot": "${workspaceFolder}/frontend-web" + } + } + ], + "compounds": [ + { + "name": "DeerFlow Full Stack (Backend + Frontend)", + "configurations": [ + "DeerFlow Backend FastAPI (8001)", + "DeerFlow Frontend Vite (5174)" + ], + "stopAll": true + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..c0f966c6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,52 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "DeerFlow:检查本地开发环境", + "type": "shell", + "command": "$backendPython = '${workspaceFolder}\\offline-backend-20260512\\backend\\.venv\\Scripts\\python.exe'; if (-not (Test-Path -LiteralPath $backendPython)) { throw '后端虚拟环境不存在,请先在 backend 目录执行 uv sync' }; & $backendPython -c \"import uvicorn; print('Backend Python OK')\"; pnpm --dir '${workspaceFolder}\\frontend-web' exec vite --version", + "options": { + "shell": { + "executable": "powershell.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] + } + }, + "problemMatcher": [] + }, + { + "label": "DeerFlow:前端生产构建", + "type": "process", + "command": "pnpm", + "args": [ + "build" + ], + "options": { + "cwd": "${workspaceFolder}/frontend-web" + }, + "problemMatcher": [] + }, + { + "label": "DeerFlow:WeKnora 专项测试", + "type": "process", + "command": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe", + "args": [ + "-m", + "pytest", + "tests/test_llmwiki_weknora.py", + "-v" + ], + "options": { + "cwd": "${workspaceFolder}/offline-backend-20260512/backend", + "env": { + "PYTHONPATH": "." + } + }, + "problemMatcher": [] + } + ] +} diff --git a/frontend-web/src/components/wiki/WikiHeader.tsx b/frontend-web/src/components/wiki/WikiHeader.tsx index 9c5e6429..fe8b0210 100644 --- a/frontend-web/src/components/wiki/WikiHeader.tsx +++ b/frontend-web/src/components/wiki/WikiHeader.tsx @@ -8,6 +8,7 @@ import { SlidersHorizontal, Upload, } from "lucide-react"; +import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; @@ -18,7 +19,7 @@ function HeaderButton({ variant = "outline", className, }: { - icon: React.ReactNode; + icon: ReactNode; label: string; onClick: () => void; variant?: "outline" | "primary"; @@ -52,6 +53,7 @@ export function WikiHeader({ onCreate, onManageFolders, onManageTemplates, + extraActions, }: { onToggleSidebar: () => void; onImportFile: () => void; @@ -61,6 +63,7 @@ export function WikiHeader({ onCreate: () => void; onManageFolders: () => void; onManageTemplates: () => void; + extraActions?: ReactNode; }) { return (
@@ -79,6 +82,7 @@ export function WikiHeader({
+ {extraActions} } label="导入文件" diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index ca90f080..dd2f8212 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -3,6 +3,7 @@ import { BotIcon, CheckIcon, + DatabaseIcon, GlobeIcon, InfoIcon, Loader2Icon, @@ -43,6 +44,7 @@ import { import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor"; +import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector"; import { AgentRecentChats } from "@/components/workspace/agents/agent-recent-chats"; import { useArtifactsOptional } from "@/components/workspace/artifacts"; import { EnabledSkillList } from "@/components/workspace/agents/enabled-skill-list"; @@ -80,6 +82,7 @@ interface AgentDraft { templateName: string; /** 会商席位「技能识别强化」开关。 */ seatSkillDirective: boolean; + knowledgeBaseIds: string[]; } function draftFromAgent(agent: Agent): AgentDraft { @@ -93,6 +96,7 @@ function draftFromAgent(agent: Agent): AgentDraft { featured: agent.featured_order != null, templateName: agent.template_name ?? "", seatSkillDirective: agent.seat_skill_directive ?? false, + knowledgeBaseIds: agent.llmwiki_knowledge_base_ids ?? [], }; } @@ -310,6 +314,11 @@ export function AgentDetailPanel({ request.template_name = draft.templateName.trim(); if (draft.seatSkillDirective !== (agent.seat_skill_directive ?? false)) request.seat_skill_directive = draft.seatSkillDirective; + if ( + JSON.stringify(draft.knowledgeBaseIds) !== + JSON.stringify(agent.llmwiki_knowledge_base_ids ?? []) + ) + request.llmwiki_knowledge_base_ids = draft.knowledgeBaseIds; if (nextPublished !== agent.published) request.published = nextPublished; if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? [])) request.skills = draft.skills; @@ -579,6 +588,13 @@ export function AgentDetailPanel({
)} + {!editing && (agent.llmwiki_knowledge_base_ids?.length ?? 0) > 0 && ( +
+ + LLMWiki 知识库:{agent.llmwiki_knowledge_base_ids?.length ?? 0} 个 +
+ )} + {/* Publish / featured toggles — only meaningful in edit mode. */} {editing && draft && (
@@ -596,6 +612,14 @@ export function AgentDetailPanel({ 配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。

+ + setDraft({ ...draft, knowledgeBaseIds }) + } + variant="panel" + />

会商技能识别强化

diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index b1e1d7e7..4e35ac23 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -22,9 +22,10 @@ import { useEffect, useMemo, useRef, - useState, - type ComponentProps, -} from "react"; + useState, + type ComponentProps, + type ReactNode, +} from "react"; import { PromptInput, @@ -132,9 +133,10 @@ export function InputBox({ onFollowupsVisibilityChange, onSubmit, onStop, - promptPrefix, - agentSelector, - assistantId, + promptPrefix, + agentSelector, + knowledgeSelector, + assistantId, hideAdminRecommendedQuestions, compact, ...props @@ -190,11 +192,13 @@ export function InputBox({ * homepage (``ChatPage``) passes this; ``AgentChatPage`` omits it because the * thread is already bound to one agent. */ - agentSelector?: { - selectedAgentId: string | null; - onAgentChange: (agentId: string | null) => void; - }; -}) { + agentSelector?: { + selectedAgentId: string | null; + onAgentChange: (agentId: string | null) => void; + }; + /** WeKnora-style knowledge-space picker rendered inside the composer toolbar. */ + knowledgeSelector?: ReactNode; +}) { const { t } = useI18n(); const [localSettings, setLocalSettings] = useLocalSettings(); const searchParams = useSearchParams(); @@ -1081,9 +1085,10 @@ export function InputBox({ )} - - - {agentSelector && ( + + + {knowledgeSelector} + {agentSelector && ( item.publication_status !== "published" || item.is_owner), + }, + { + key: "public", + label: "公共知识空间", + rows: rows.filter((item) => item.publication_status === "published" && !item.is_owner), + }, + ].filter((group) => group.rows.length > 0); +} + +function SpaceIcon({ item }: { item: LlmWikiKnowledgeBase }) { + return ( + + {item.publication_status === "published" ? ( + + ) : ( + + )} + + ); +} + +function SelectionContent({ + rows, + value, + onChange, + search, + onSearchChange, + loading, + error, + source, + variant, +}: { + rows: LlmWikiKnowledgeBase[]; + value: string[]; + onChange: (value: string[]) => void; + search: string; + onSearchChange: (value: string) => void; + loading: boolean; + error: unknown; + source: SelectorSource; + variant: SelectorVariant; +}) { + const selected = useMemo(() => new Set(value), [value]); + const filtered = useMemo(() => { + const query = search.trim().toLocaleLowerCase(); + if (!query) return rows; + return rows.filter((item) => + `${item.name} ${item.description}`.toLocaleLowerCase().includes(query), + ); + }, [rows, search]); + const groups = useMemo(() => groupRows(filtered), [filtered]); + const toggle = (id: string) => { + onChange(selected.has(id) ? value.filter((item) => item !== id) : [...value, id]); + }; + + return ( + <> +
+
+ + onSearchChange(event.target.value)} + onKeyDown={(event) => event.stopPropagation()} + placeholder="搜索知识空间" + className="h-8 bg-muted/40 pl-8 text-xs" + /> +
+
+ +
+ {loading ? ( +
+ 正在读取知识空间 +
+ ) : error ? ( +
+ {error instanceof Error ? error.message : "知识空间读取失败"} +
+ ) : groups.length === 0 ? ( +
+ {search + ? "没有匹配的知识空间" + : source === "agent" + ? "该智能体尚未绑定可用知识空间" + : "当前账号暂无可用知识空间"} +
+ ) : ( + groups.map((group, groupIndex) => ( +
+ {groupIndex > 0 && ( + variant === "popover" + ? + :
+ )} + {variant === "popover" ? ( + + {group.key === "personal" ? : }{group.label} + + ) : ( +
+ {group.key === "personal" ? : }{group.label} +
+ )} + {group.rows.map((item) => { + const rowContent = ( + <> + +
+
{item.name}
+
+ {item.type === "faq" ? "FAQ" : "文档库"} · {item.knowledge_count ?? 0} 项知识 +
+
+ {item.publication_status === "published" && ( + 公共 + )} + + ); + return variant === "popover" ? ( + toggle(item.id)} + onSelect={(event) => event.preventDefault()} + className={cn( + "my-0.5 min-h-10 rounded-md py-1.5 pr-2 pl-7", + selected.has(item.id) && "bg-primary/8 focus:bg-primary/12", + )} + > + {rowContent} + + ) : ( + + ); + })} +
+ )) + )} +
+ +
+ 已选择 {value.length} 个 +
+ + +
+
+ + ); +} + +export function LlmWikiKnowledgeSelector({ + value, + onChange, + source = "all", + agentId, + autoSelectAll = false, + disabled = false, + className, + label = "知识空间", + variant = "popover", +}: { + value: string[]; + onChange: (value: string[]) => void; + source?: SelectorSource; + agentId?: string | null; + autoSelectAll?: boolean; + disabled?: boolean; + className?: string; + label?: string; + variant?: SelectorVariant; +}) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const runtime = useLlmWikiRuntime(); + const all = useLlmWikiKnowledgeBases( + "all", + runtime.data?.provider === "weknora", + ); + const agent = useAgentLlmWikiKnowledgeBases( + agentId, + runtime.data?.provider === "weknora" && source === "agent" && !!agentId, + ); + const rows = all.data ?? []; + const defaultIds = useMemo(() => { + if (source !== "agent") { + return rows.map((item) => item.id); + } + return (agent.data ?? []).map((item) => item.id); + }, [agent.data, rows, source]); + const loading = all.isLoading || (source === "agent" && !!agentId && agent.isLoading); + const error = all.error ?? (source === "agent" ? agent.error : null); + const availableIds = useMemo(() => new Set(rows.map((item) => item.id)), [rows]); + const initializationKey = `${source}:${agentId ?? "all"}:${defaultIds.join(",")}`; + const initialized = useRef(null); + + useEffect(() => { + if (!all.data || (source === "agent" && agentId && !agent.data) || initialized.current === initializationKey) return; + initialized.current = initializationKey; + const valid = value.filter((id) => availableIds.has(id)); + const next = autoSelectAll && valid.length === 0 && defaultIds.length > 0 + ? defaultIds.filter((id) => availableIds.has(id)) + : valid; + if (next.length !== value.length || next.some((id, index) => id !== value[index])) { + onChange(next); + } + }, [agent.data, agentId, all.data, autoSelectAll, availableIds, defaultIds, initializationKey, onChange, source, value]); + + useEffect(() => { + if (!open) setSearch(""); + }, [open]); + + if (runtime.isError || runtime.data?.provider !== "weknora") return null; + + const content = ( + + ); + + if (variant === "panel") { + return ( +
+
+
+
+ 绑定知识空间 +
+

+ 新建该智能体对话时,只能从已绑定空间中选择本次检索范围。 +

+
+ {value.length} 个 +
+ {content} +
+ ); + } + + const selectedName = value.length === 1 + ? rows.find((item) => item.id === value[0])?.name + : null; + + return ( + + + + + + {content} + + + ); +} + +export default LlmWikiKnowledgeSelector; diff --git a/frontend-web/src/components/workspace/skill-display-sources.tsx b/frontend-web/src/components/workspace/skill-display-sources.tsx index 3993f022..0e60488c 100644 --- a/frontend-web/src/components/workspace/skill-display-sources.tsx +++ b/frontend-web/src/components/workspace/skill-display-sources.tsx @@ -92,7 +92,7 @@ export function SourceCard({ knowledge_base_id: previewTarget.knowledgeBaseId, chunk_id: previewTarget.chunkId, }); - void apiFetch(`${getBackendBaseURL()}/api/weknora/source-context?${params}`) + void apiFetch(`${getBackendBaseURL()}/api/llmwiki/sources/context?${params}`) .then(async (response) => { const payload = (await response.json().catch(() => null)) as unknown; if (!response.ok || !isWeKnoraSourceContextResponse(payload)) { diff --git a/frontend-web/src/core/agents/types.ts b/frontend-web/src/core/agents/types.ts index 283993f8..26bd7e33 100644 --- a/frontend-web/src/core/agents/types.ts +++ b/frontend-web/src/core/agents/types.ts @@ -61,7 +61,9 @@ export interface Agent { /** 会商席位「技能识别强化」开关。作为会商席位运行时,是否把本智能体配置的技能 * (名称 + SKILL.md 路径 + 必须先 read_file 的硬指令) 拼进本轮任务以补偿弱模型。 * 与业务链条同名开关取 OR。null/false = 关 (默认)。 */ - seat_skill_directive?: boolean | null; + seat_skill_directive?: boolean | null; + /** DeerFlow LLMWiki mapping ids this agent allows conversations to select. */ + llmwiki_knowledge_base_ids?: string[] | null; tags?: Tag[]; featured_order?: number | null; /** Whether the current user has favorited (收藏) this agent. */ @@ -96,7 +98,8 @@ export interface CreateAgentRequest { common_questions?: AgentQuestion[] | null; template_name?: string | null; /** 会商席位「技能识别强化」开关;缺省/false = 关。 */ - seat_skill_directive?: boolean | null; + seat_skill_directive?: boolean | null; + llmwiki_knowledge_base_ids?: string[] | null; } export interface UpdateAgentRequest { @@ -113,5 +116,6 @@ export interface UpdateAgentRequest { /** 入库模板名;传空字符串表示清除。 */ template_name?: string | null; /** 会商席位「技能识别强化」开关;缺省/false = 关。 */ - seat_skill_directive?: boolean | null; -} + seat_skill_directive?: boolean | null; + llmwiki_knowledge_base_ids?: string[] | null; +} diff --git a/frontend-web/src/core/llmwiki/conversation-deposit.ts b/frontend-web/src/core/llmwiki/conversation-deposit.ts new file mode 100644 index 00000000..545431e2 --- /dev/null +++ b/frontend-web/src/core/llmwiki/conversation-deposit.ts @@ -0,0 +1,66 @@ +import type { Message } from "@langchain/langgraph-sdk"; + +import { textOfMessage } from "@/core/threads/utils"; +import { createLlmWikiConversationDeposit } from "@/strategy-components/api/llmwiki"; + +function isHidden(message: Message): boolean { + const extra = (message as { additional_kwargs?: Record }) + .additional_kwargs; + return extra?.hide_from_ui === true; +} + +function messageText(message: Message | undefined): string { + return (message ? textOfMessage(message) : "")?.trim() ?? ""; +} + +function hasText(message: Message | undefined): message is Message { + return !!message && messageText(message).length > 0; +} + +export function queueLlmWikiConversationDeposit({ + threadId, + messages, + assistantId, + knowledgeBaseIds, +}: { + threadId: string | null | undefined; + messages: Message[] | undefined; + assistantId?: string | null; + knowledgeBaseIds?: string[]; +}) { + if (!threadId || !messages?.length) { + return; + } + const visible = messages.filter((message) => !isHidden(message)); + const assistantIndex = [...visible] + .map((message, index) => ({ message, index })) + .reverse() + .find(({ message }) => message.type === "ai" && hasText(message))?.index; + if (assistantIndex === undefined) { + return; + } + const assistantMessage = visible[assistantIndex]; + const humanMessage = [...visible.slice(0, assistantIndex)] + .reverse() + .find((message) => message.type === "human" && hasText(message)); + if (!assistantMessage?.id || !humanMessage) { + return; + } + const question = messageText(humanMessage); + const answer = messageText(assistantMessage); + if (!question || !answer) { + return; + } + + void createLlmWikiConversationDeposit({ + thread_id: threadId, + question, + answer, + human_message_id: humanMessage.id ?? null, + assistant_message_id: assistantMessage.id, + assistant_id: assistantId ?? null, + knowledge_base_ids: knowledgeBaseIds ?? [], + }).catch((error) => { + console.warn("Failed to queue LLMWiki conversation deposit", error); + }); +} diff --git a/frontend-web/src/core/threads/hooks.ts b/frontend-web/src/core/threads/hooks.ts index 088cf4a9..e9e19a2a 100644 --- a/frontend-web/src/core/threads/hooks.ts +++ b/frontend-web/src/core/threads/hooks.ts @@ -1122,20 +1122,26 @@ export function useThreadStream({ }), ); - const ragModeEnabled = - localSettings.display.ragModeEnabled || isThreadRagModeLocked(threadId); - if (ragModeEnabled) { - lockThreadRagMode(threadId); - } + const mergedContext = { + ...context, + ...extraContext, + }; + const selectedLlmWikiIds = Array.isArray( + mergedContext.llmwiki_knowledge_base_ids, + ) + ? mergedContext.llmwiki_knowledge_base_ids + : []; + const ragModeEnabled = + localSettings.display.ragModeEnabled || + isThreadRagModeLocked(threadId) || + selectedLlmWikiIds.length > 0; + if (ragModeEnabled) { + lockThreadRagMode(threadId); + } const contextCompressionEnabled = localSettings.display.contextCompressionEnabled ?? false; - const mergedContext = { - ...context, - ...extraContext, - }; - const hiddenContextMessages = normalizeHiddenContextMessages(options); const submittedMessages: Message[] = [ ...hiddenContextMessages.map((item) => ({ diff --git a/frontend-web/src/core/threads/types.ts b/frontend-web/src/core/threads/types.ts index 56cfc515..9577501b 100644 --- a/frontend-web/src/core/threads/types.ts +++ b/frontend-web/src/core/threads/types.ts @@ -24,7 +24,9 @@ export interface AgentThreadContext extends Record { /** Per-request switch for context compression (上下文压缩). Default off. */ summarization_enabled?: boolean; /** Per-conversation switch for knowledge-base RAG injection (phase 2). */ - knowledge_rag_enabled?: boolean; + knowledge_rag_enabled?: boolean; + /** DeerFlow LLMWiki mapping ids selected for this conversation. */ + llmwiki_knowledge_base_ids?: string[]; agent_id?: string; /** @deprecated kept for reading legacy thread metadata; prefer agent_id */ agent_name?: string; diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index a67bfce8..0d9d57a3 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -16,6 +16,7 @@ import { ArtifactTrigger } from "@/components/workspace/artifacts"; import { ChatBox, useThreadChat } from "@/components/workspace/chats"; import { ExportTrigger } from "@/components/workspace/export-trigger"; import { InputBox } from "@/components/workspace/input-box"; +import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import { QuestionNavigator } from "@/components/workspace/question-navigator"; @@ -24,7 +25,9 @@ import { TodoList } from "@/components/workspace/todo-list"; import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator"; import { Tooltip } from "@/components/workspace/tooltip"; import { normalizeAgentQuestions, useAgent } from "@/core/agents"; +import { getAPIClient } from "@/core/api"; import { useI18n } from "@/core/i18n/hooks"; +import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit"; import { useModels } from "@/core/models/hooks"; import { useNotification } from "@/core/notification/hooks"; import { saveLastAgentId, useThreadSettings } from "@/core/settings"; @@ -69,6 +72,26 @@ const TEMPLATE_BUILDER_AGENT_ID = "template-json-builder"; const RWFX_SQ_REPORT_CONTEXT_MAX_CHARS = 12000; const RWFX_SQ_REPORT_CONTEXT_TAIL_CHARS = 2000; +function readPendingKnowledgeSelection(agentId: string | undefined): string[] | null { + if (typeof window === "undefined" || !agentId) return null; + try { + const raw = sessionStorage.getItem("pendingAgentMessage"); + if (!raw) return null; + const parsed = JSON.parse(raw) as { + agentId?: string; + knowledgeBaseIds?: unknown; + }; + if (parsed.agentId !== agentId || !Array.isArray(parsed.knowledgeBaseIds)) { + return null; + } + return parsed.knowledgeBaseIds + .map((item) => String(item).trim()) + .filter(Boolean); + } catch { + return null; + } +} + function stringifyForHiddenContext(value: unknown): string { if (typeof value === "string") return value; try { @@ -244,6 +267,79 @@ export default function AgentChatPage({ useThreadChat(); const [settings, setSettings] = useThreadSettings(threadId); const { tokenUsageEnabled } = useModels(); + const pendingKnowledgeSelection = useRef( + readPendingKnowledgeSelection(agent_id), + ); + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState( + () => pendingKnowledgeSelection.current ?? [], + ); + const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState( + () => pendingKnowledgeSelection.current !== null, + ); + const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false); + const knowledgeSelectionThread = useRef(null); + const knowledgeSelectionAgent = useRef(agent_id); + const activeThreadIdRef = useRef(threadId); + + const changeKnowledgeSelection = useCallback((ids: string[]) => { + setSelectedKnowledgeBaseIds(ids); + setKnowledgeSelectionReady(true); + }, []); + + useEffect(() => { + activeThreadIdRef.current = threadId; + }, [threadId]); + + useEffect(() => { + if (knowledgeSelectionAgent.current === agent_id) return; + knowledgeSelectionAgent.current = agent_id; + const pending = readPendingKnowledgeSelection(agent_id); + pendingKnowledgeSelection.current = pending; + knowledgeSelectionThread.current = null; + setSelectedKnowledgeBaseIds(pending ?? []); + setKnowledgeSelectionReady(pending !== null); + }, [agent_id]); + + useEffect(() => { + if (isNewThread) { + if (knowledgeSelectionThread.current !== "__new__") { + setSelectedKnowledgeBaseIds(pendingKnowledgeSelection.current ?? []); + setKnowledgeSelectionReady(pendingKnowledgeSelection.current !== null); + } + knowledgeSelectionThread.current = "__new__"; + setKnowledgeSelectionLoading(false); + return; + } + if (!threadId || knowledgeSelectionThread.current === threadId) return; + knowledgeSelectionThread.current = threadId; + setKnowledgeSelectionLoading(true); + void getAPIClient() + .threads.get(threadId) + .then((record) => { + if (knowledgeSelectionThread.current !== threadId) return; + const metadata = record.metadata as Record | undefined; + const raw = metadata?.llmwiki_knowledge_base_ids; + if (Array.isArray(raw)) { + setSelectedKnowledgeBaseIds( + raw.map((item) => String(item).trim()).filter(Boolean), + ); + setKnowledgeSelectionReady(true); + } else { + setSelectedKnowledgeBaseIds([]); + setKnowledgeSelectionReady(false); + } + }) + .catch(() => { + if (knowledgeSelectionThread.current === threadId) { + setKnowledgeSelectionReady(false); + } + }) + .finally(() => { + if (knowledgeSelectionThread.current === threadId) { + setKnowledgeSelectionLoading(false); + } + }); + }, [isNewThread, threadId]); useEffect(() => { if (isNewThread) { @@ -253,14 +349,34 @@ export default function AgentChatPage({ }, [isNewThread, threadId, agent_id]); const { showNotification } = useNotification(); + const conversationMetadata = useMemo( + () => ({ + ...(taskId ? taskThreadMetadata(taskContext) : {}), + ...(knowledgeSelectionReady + ? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds } + : {}), + }), + [knowledgeSelectionReady, selectedKnowledgeBaseIds, taskContext, taskId], + ); const [thread, sendMessage] = useThreadStream({ threadId: isNewThread ? undefined : threadId, assistantId: agent_id, - context: { ...settings.context, agent_id }, + context: { + ...settings.context, + agent_id, + ...(knowledgeSelectionReady + ? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds } + : {}), + }, // 任务闸门:新建 thread 打 metadata.taskId 标签,供任务对话列表(1:N)按任务过滤; // xdfx 额外打 metadata.actionId 标签,按「行动id」划分聊天记录(见 taskThreadMetadata)。 - threadMetadata: taskId ? taskThreadMetadata(taskContext) : undefined, + threadMetadata: + Object.keys(conversationMetadata).length > 0 + ? conversationMetadata + : undefined, onStart: (createdThreadId) => { + knowledgeSelectionThread.current = createdThreadId; + activeThreadIdRef.current = createdThreadId; setThreadId(createdThreadId); setIsNewThread(false); // Keep the current component mounted while updating the hash route. @@ -286,6 +402,12 @@ export default function AgentChatPage({ } catch { /* 上报绝不影响问答 */ } + queueLlmWikiConversationDeposit({ + threadId: activeThreadIdRef.current, + messages: state.messages, + assistantId: agent_id, + knowledgeBaseIds: selectedKnowledgeBaseIds, + }); if (document.hidden || !document.hasFocus()) { let body = "Conversation finished"; const lastMessage = state.messages[state.messages.length - 1]; @@ -581,10 +703,23 @@ export default function AgentChatPage({ autoFocus status={status} context={settings.context} - disabled={isDemoMode} + disabled={isDemoMode || knowledgeSelectionLoading} onContextChange={(context) => setSettings("context", context) } + knowledgeSelector={ + + } onFollowupsVisibilityChange={setShowFollowups} onSubmit={handleSubmit} onStop={handleStop} @@ -636,7 +771,7 @@ export default function AgentChatPage({ assistantId={agent_id} status={status} context={settings.context} - disabled={isDemoMode} + disabled={isDemoMode || knowledgeSelectionLoading} onContextChange={(context) => setSettings("context", context) } diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index 2bec4781..8a92a701 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -16,6 +16,7 @@ import { } from "@/components/workspace/chats"; import { ExportTrigger } from "@/components/workspace/export-trigger"; import { KnowledgeSaveTrigger } from "@/components/workspace/knowledge-save-trigger"; +import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList, @@ -52,6 +53,7 @@ import { useThreadSettings, } from "@/core/settings"; import { useThreadStream } from "@/core/threads/hooks"; +import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit"; import { useLayoutMode } from "@/core/system-settings/hooks"; import { useRouteBase } from "@/core/route-base"; import { textOfMessage } from "@/core/threads/utils"; @@ -121,12 +123,29 @@ export default function ChatPage() { // In-memory only — explicitly overrides any value persisted by the old // per-thread header toggle so stale localStorage can't re-enable it. const [knowledgeRagEnabled] = useKnowledgeRagEnabled(); + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]); + const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState(false); + const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false); + const knowledgeSelectionThread = useRef(null); + const activeThreadIdRef = useRef(threadId); + const changeKnowledgeSelection = useCallback((ids: string[]) => { + setSelectedKnowledgeBaseIds(ids); + setKnowledgeSelectionReady(true); + }, []); const streamContext = useMemo( () => ({ ...settings.context, knowledge_rag_enabled: knowledgeRagEnabled, + ...(knowledgeSelectionReady + ? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds } + : {}), }), - [settings.context, knowledgeRagEnabled], + [ + settings.context, + knowledgeRagEnabled, + knowledgeSelectionReady, + selectedKnowledgeBaseIds, + ], ); const [mounted, setMounted] = useState(false); const { tokenUsageEnabled } = useModels(); @@ -150,6 +169,10 @@ export default function ChatPage() { const { selectedArtifact } = useArtifacts(); useSpecificChatMode(); + useEffect(() => { + activeThreadIdRef.current = threadId; + }, [threadId]); + useEffect(() => { setMounted(true); }, []); @@ -158,12 +181,54 @@ export default function ChatPage() { if (isNewThread) { setHasStartedConversation(false); setPromptPrefixOn(false); + setSelectedKnowledgeBaseIds([]); + setKnowledgeSelectionReady(false); + setKnowledgeSelectionLoading(false); + knowledgeSelectionThread.current = "__new__"; // Pre-select the agent the user last started a conversation with so a // fresh chat defaults to it. Validated against the known agent list below. setSelectedAgentId(getLastAgentId()); } }, [isNewThread, threadId]); + // The knowledge-base choice belongs to the conversation, not to a browser + // component instance. Restore it from LangGraph thread metadata whenever an + // existing conversation is opened. An explicit empty array means the user + // deliberately disabled LLMWiki retrieval for that conversation. + useEffect(() => { + if (isNewThread || !threadId || knowledgeSelectionThread.current === threadId) { + return; + } + knowledgeSelectionThread.current = threadId; + setKnowledgeSelectionLoading(true); + void getAPIClient(isMock) + .threads.get(threadId) + .then((record) => { + if (knowledgeSelectionThread.current !== threadId) return; + const metadata = record.metadata as Record | undefined; + const raw = metadata?.llmwiki_knowledge_base_ids; + if (Array.isArray(raw)) { + setSelectedKnowledgeBaseIds( + raw.map((item) => String(item).trim()).filter(Boolean), + ); + setKnowledgeSelectionReady(true); + } else { + setSelectedKnowledgeBaseIds([]); + setKnowledgeSelectionReady(false); + } + }) + .catch(() => { + if (knowledgeSelectionThread.current === threadId) { + setKnowledgeSelectionReady(false); + } + }) + .finally(() => { + if (knowledgeSelectionThread.current === threadId) { + setKnowledgeSelectionLoading(false); + } + }); + }, [isMock, isNewThread, threadId]); + // Drop a restored last-used agent that is no longer in the selector list // (deleted / unpublished / no longer curated), so we never hand off to a // dead agent. Wait for the list to load before deciding. @@ -175,6 +240,8 @@ export default function ChatPage() { if (known.length === 0) return; if (!known.some((a) => a.id === selectedAgentId)) { setSelectedAgentId(null); + setSelectedKnowledgeBaseIds([]); + setKnowledgeSelectionReady(false); } }, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]); @@ -183,8 +250,13 @@ export default function ChatPage() { const [thread, sendMessage, isUploading] = useThreadStream({ threadId: isNewThread ? undefined : threadId, context: streamContext, + threadMetadata: knowledgeSelectionReady + ? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds } + : undefined, isMock, onStart: (createdThreadId) => { + knowledgeSelectionThread.current = createdThreadId; + activeThreadIdRef.current = createdThreadId; setThreadId(createdThreadId); setIsNewThread(false); // Keep the current component mounted while updating the hash route. @@ -210,6 +282,12 @@ export default function ChatPage() { } catch { /* 上报绝不影响问答 */ } + queueLlmWikiConversationDeposit({ + threadId: activeThreadIdRef.current, + messages: state.messages, + assistantId: selectedAgentId, + knowledgeBaseIds: selectedKnowledgeBaseIds, + }); if (document.hidden || !document.hasFocus()) { let body = "Conversation finished"; const lastMessage = state.messages.at(-1); @@ -262,7 +340,12 @@ export default function ChatPage() { void (async () => { try { const created = await getAPIClient(isMock).threads.create({ - metadata: { agent_id: selectedAgentId }, + metadata: { + agent_id: selectedAgentId, + ...(knowledgeSelectionReady + ? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds } + : {}), + }, }); sessionStorage.setItem( "pendingAgentMessage", @@ -270,6 +353,9 @@ export default function ChatPage() { agentId: selectedAgentId, threadId: created.thread_id, text, + ...(knowledgeSelectionReady + ? { knowledgeBaseIds: selectedKnowledgeBaseIds } + : {}), }), ); saveLastAgentId(selectedAgentId); @@ -328,6 +414,8 @@ export default function ChatPage() { hideAdminPrompt, preferencesLoading, selectedAgentId, + selectedKnowledgeBaseIds, + knowledgeSelectionReady, agentHandoffPending, isMock, router, @@ -418,6 +506,8 @@ export default function ChatPage() { // Passing null (the ✕ clear) wipes the stored preference. const handleAgentChange = useCallback((agentId: string | null) => { setSelectedAgentId(agentId); + setSelectedKnowledgeBaseIds([]); + setKnowledgeSelectionReady(false); saveLastAgentId(agentId); }, []); const handleStop = useCallback(async () => { @@ -620,7 +710,8 @@ export default function ChatPage() { disabled={ env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading || - agentHandoffPending + agentHandoffPending || + knowledgeSelectionLoading } onContextChange={(context) => setSettings("context", context) @@ -634,6 +725,22 @@ export default function ChatPage() { } : undefined } + knowledgeSelector={ + isLandingState ? ( + + ) : undefined + } onFollowupsVisibilityChange={setShowFollowups} onSubmit={handleSubmit} onStop={handleStop} diff --git a/frontend-web/src/pages/KnowledgeBasePage.tsx b/frontend-web/src/pages/KnowledgeBasePage.tsx index 5380610c..f85fcd40 100644 --- a/frontend-web/src/pages/KnowledgeBasePage.tsx +++ b/frontend-web/src/pages/KnowledgeBasePage.tsx @@ -1,5 +1,5 @@ import { Loader2 } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "sonner"; @@ -530,7 +530,11 @@ function VaultPageDialog({ ); } -export default function KnowledgeBasePage() { +export default function KnowledgeBasePage({ + headerExtraActions, +}: { + headerExtraActions?: ReactNode; +} = {}) { const navigate = useNavigate(); const { noteId } = useParams<{ noteId: string }>(); @@ -653,6 +657,7 @@ export default function KnowledgeBasePage() { onCreate={() => setCreateOpen(true)} onManageFolders={() => setFoldersOpen(true)} onManageTemplates={() => setTemplatesOpen(true)} + extraActions={headerExtraActions} />
+ + 正在读取 LLMWiki 运行模式 +
+ ); +} + +function AdminWeKnoraSettingsButton() { + const [open, setOpen] = useState(false); + + return ( + <> + + + + + LLMWiki 运行模式 + + + + + + ); +} + +function LegacyKnowledgePage() { + const isAdmin = getStoredAuth()?.system_role === "admin"; + + return ( + : undefined} + /> + ); +} + +export default function LlmWikiKnowledgePage({ + mode = "list", +}: { + mode?: LlmWikiKnowledgePageMode; +}) { + const runtime = useLlmWikiRuntime(); + + if (runtime.isLoading) { + return ; + } + + if (runtime.isError || runtime.data?.provider !== "weknora") { + if (mode === "graph") { + return ; + } + return ; + } + + if (mode === "graph") { + return ; + } + + if (mode === "detail") { + return ; + } + + return ; +} diff --git a/frontend-web/src/pages/NewAgentPage.tsx b/frontend-web/src/pages/NewAgentPage.tsx index 4a4b3c43..82c42559 100644 --- a/frontend-web/src/pages/NewAgentPage.tsx +++ b/frontend-web/src/pages/NewAgentPage.tsx @@ -31,6 +31,7 @@ import { } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { ArtifactsProvider } from "@/components/workspace/artifacts"; +import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector"; import { MessageList, type SetupAgentApprovalPayload, @@ -95,6 +96,7 @@ export default function NewAgentPage() { const [isCreatingAgent, setIsCreatingAgent] = useState(false); const [isSavingDraft, setIsSavingDraft] = useState(false); const [selectedSkills, setSelectedSkills] = useState([]); + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]); const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false); const [detailSkillName, setDetailSkillName] = useState(null); // 恢复已有 bootstrap 会话时该 thread 已存在,无需再 create(置为 true,跳过创建)。 @@ -230,6 +232,7 @@ export default function NewAgentPage() { model: modelOverride, template_name: templateNameInput.trim(), seat_skill_directive: seatSkillDirectiveInput, + llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds, }); setAgent(updated); return updated; @@ -252,6 +255,7 @@ export default function NewAgentPage() { model: modelOverride, template_name: templateNameInput.trim(), seat_skill_directive: seatSkillDirectiveInput, + llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds, }); setAgentId(created.id); setAgent(created); @@ -280,6 +284,7 @@ export default function NewAgentPage() { descriptionInput, modelInput, selectedSkills, + selectedKnowledgeBaseIds, soulInput, templateNameInput, seatSkillDirectiveInput, @@ -590,6 +595,13 @@ export default function NewAgentPage() {

+ +