diff --git a/.vscode/launch.json b/.vscode/launch.json index cdd14238..fb342a87 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -47,7 +47,7 @@ "name": "DeerFlow: Open Frontend in Chrome", "type": "node-terminal", "request": "launch", - "preLaunchTask": "DeerFlow: Start Full Stack and Wait", + "preLaunchTask": "DeerFlow: VS Code Start", "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Start-Process 'http://127.0.0.1:5174/'\"", "cwd": "${workspaceFolder}" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b8a70d8f..0886ef70 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,29 @@ { "version": "2.0.0", "tasks": [ + { + "label": "DeerFlow: VS Code Start", + "type": "process", + "command": "powershell.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "${workspaceFolder}/scripts/vscode-start.ps1", + "-OpenBrowser" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [] + }, { "label": "DeerFlow: Start Full Stack and Wait", "type": "process", diff --git a/frontend-web/src/components/workspace/skill-display-sources.tsx b/frontend-web/src/components/workspace/skill-display-sources.tsx index 8e9950e2..b3db288a 100644 --- a/frontend-web/src/components/workspace/skill-display-sources.tsx +++ b/frontend-web/src/components/workspace/skill-display-sources.tsx @@ -1,4 +1,4 @@ -import { ExternalLinkIcon, FileTextIcon, LoaderCircle } from "lucide-react"; +import { ExternalLinkIcon, FileTextIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -9,12 +9,12 @@ import { DialogTitle, } from "@/components/ui/dialog"; import type { RuntimeDisplaySource } from "@/core/skill-display/runtime"; -import { apiFetch } from "@/core/api/fetch-client"; -import { getBackendBaseURL } from "@/core/config"; +import { resolveWeKnoraSourceTarget } from "@/core/llmwiki/source-links"; import { useLocalSettings } from "@/core/settings"; import { useCitationDisplaySettings } from "@/core/system-settings/hooks"; import { cn } from "@/lib/utils"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { WeKnoraSourceDrawer } from "@/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer"; export function SkillDisplaySources({ highlightedReferenceId, @@ -52,12 +52,14 @@ export function SourceCard({ source: RuntimeDisplaySource; referenceTarget?: boolean; }) { - const [open, setOpen] = useState(false); - const [context, setContext] = useState(null); - const [contextLoading, setContextLoading] = useState(false); - const [contextError, setContextError] = useState(null); + const [relatedOpen, setRelatedOpen] = useState(false); + const [detailOpen, setDetailOpen] = useState(false); const [settings] = useLocalSettings(); const { data: citationDisplay } = useCitationDisplaySettings(); + const previewTarget = resolveWeKnoraSourceTarget({ + ...(source.raw || {}), + url: source.url, + }); const cleanupWords = citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; const content = cleanReferenceText(source.content || source.snippet, cleanupWords); @@ -78,57 +80,27 @@ export function SourceCard({ ].filter(Boolean); const isCitation = source.mode === "citation"; const showReferenceNumber = isCitation; - const previewTarget = parseWeKnoraPreviewUrl(source.url); - - useEffect(() => { - if (!open || !previewTarget) { - return; - } - let cancelled = false; - setContext(null); - setContextError(null); - setContextLoading(true); - const params = new URLSearchParams({ - knowledge_base_id: previewTarget.knowledgeBaseId, - chunk_id: previewTarget.chunkId, - }); - 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)) { - throw new Error(readPreviewError(payload, response.status)); - } - if (!cancelled) { - setContext(payload.data); - } - }) - .catch((error: unknown) => { - if (!cancelled) { - setContextError(error instanceof Error ? error.message : "无法加载原文上下文"); - } - }) - .finally(() => { - if (!cancelled) { - setContextLoading(false); - } - }); - return () => { - cancelled = true; - }; - }, [open, previewTarget?.chunkId, previewTarget?.knowledgeBaseId]); return ( <> - - - + {previewTarget && ( +
+ +
+ )} + + {previewTarget && ( + + )} + +
@@ -205,13 +202,7 @@ export function SourceCard({ )}
- {previewTarget ? ( - - ) : paragraphs.length > 0 ? ( + {paragraphs.length > 0 ? (
{paragraphs.map((paragraph, index) => (

@@ -231,156 +222,6 @@ export function SourceCard({ ); } -type WeKnoraPreviewTarget = { - knowledgeBaseId: string; - chunkId: string; -}; - -type WeKnoraSourceContext = { - knowledge_base_name?: string; - document_title?: string; - document_file_name?: string; - display_mode?: "wiki" | "chunks"; - focus_chunk_id: string; - wiki_page?: { - id?: string; - slug?: string; - title?: string; - page_type?: string; - status?: string; - summary?: string; - content?: string; - source_refs?: string[]; - } | null; - chunks: Array<{ - id: string; - chunk_index?: number; - content: string; - is_focus: boolean; - }>; -}; - -function parseWeKnoraPreviewUrl(url?: string): WeKnoraPreviewTarget | null { - if (!url?.startsWith("/weknora-source-preview?")) { - return null; - } - const queryIndex = url.indexOf("?"); - const params = new URLSearchParams(url.slice(queryIndex + 1)); - const knowledgeBaseId = params.get("knowledge_base_id")?.trim(); - const chunkId = params.get("chunk_id")?.trim(); - return knowledgeBaseId && chunkId ? { knowledgeBaseId, chunkId } : null; -} - -function isWeKnoraSourceContextResponse( - value: unknown, -): value is { success: true; data: WeKnoraSourceContext } { - if (!value || typeof value !== "object") { - return false; - } - const data = (value as { data?: unknown }).data; - return ( - (value as { success?: unknown }).success === true && - !!data && - typeof data === "object" && - Array.isArray((data as WeKnoraSourceContext).chunks) - ); -} - -function readPreviewError(payload: unknown, status: number): string { - if (payload && typeof payload === "object") { - const detail = (payload as { detail?: unknown }).detail; - if (typeof detail === "string" && detail) { - return detail; - } - } - return status === 404 ? "原文已不可用" : "无法加载原文上下文"; -} - -function WeKnoraSourceContextPanel({ - context, - loading, - error, -}: { - context: WeKnoraSourceContext | null; - loading: boolean; - error: string | null; -}) { - if (loading) { - return ( -

- - 正在加载原文上下文… -
- ); - } - if (error) { - return ( -
- {error} -
- ); - } - if (!context) { - return null; - } - const wikiPage = context.wiki_page; - const wikiContent = cleanReferenceText( - [wikiPage?.summary, wikiPage?.content].filter(Boolean).join("\n\n"), - [], - ); - if (wikiPage && wikiContent) { - return ( -
-
- {context.knowledge_base_name || "WeKnora 知识库"} - · LLMWiki - {wikiPage.page_type && · {wikiPage.page_type}} -
-
-
- - {wikiPage.title || wikiPage.slug || "LLMWiki 页面"} - - - 清洗页 - -
-
{wikiContent}
-
-
- ); - } - return ( -
-
- {context.knowledge_base_name || "WeKnora 知识库"} - {context.document_title && · {context.document_title}} -
- {context.chunks.map((chunk) => ( -
-
- 分段 {typeof chunk.chunk_index === "number" ? chunk.chunk_index + 1 : "-"} - {chunk.is_focus && ( - - 检索命中 - - )} -
-

{chunk.content}

-
- ))} -
- ); -} - function cleanReferenceText( raw: string | undefined, cleanupWords: string[], diff --git a/frontend-web/src/core/llmwiki/source-links.ts b/frontend-web/src/core/llmwiki/source-links.ts new file mode 100644 index 00000000..d91c0845 --- /dev/null +++ b/frontend-web/src/core/llmwiki/source-links.ts @@ -0,0 +1,125 @@ +import { readRuntimeEnv } from "@/env"; + +export type WeKnoraSourceTarget = { + knowledgeBaseId: string; + chunkId?: string; + documentId?: string; + wikiSlug?: string; + tab?: "documents" | "wiki" | "graph"; +}; + +export type KnowledgeSourceLike = { + url?: string | null; + recUuid?: string | null; + rec_uuid?: string | null; + knowledge_base_id?: string | null; + knowledgeBaseId?: string | null; + mapping_id?: string | null; + mappingId?: string | null; + chunk_id?: string | null; + chunkId?: string | null; + knowledge_id?: string | null; + knowledgeId?: string | null; + document_id?: string | null; + documentId?: string | null; + wiki_slug?: string | null; + wikiSlug?: string | null; + slug?: string | null; +}; + +export function parseWeKnoraPreviewUrl(url?: string | null): WeKnoraSourceTarget | null { + if (!url?.startsWith("/weknora-source-preview?")) { + return null; + } + const queryIndex = url.indexOf("?"); + const params = new URLSearchParams(url.slice(queryIndex + 1)); + const knowledgeBaseId = params.get("knowledge_base_id")?.trim(); + const chunkId = params.get("chunk_id")?.trim(); + const documentId = + params.get("document_id")?.trim() || + params.get("knowledge_id")?.trim() || + ""; + const wikiSlug = + params.get("wiki_slug")?.trim() || + params.get("wikiSlug")?.trim() || + params.get("slug")?.trim() || + ""; + return knowledgeBaseId && chunkId + ? { + knowledgeBaseId, + chunkId, + documentId: documentId || undefined, + wikiSlug: wikiSlug || undefined, + tab: wikiSlug ? "wiki" : "documents", + } + : null; +} + +export function resolveWeKnoraSourceTarget(source: KnowledgeSourceLike): WeKnoraSourceTarget | null { + const fromPreview = parseWeKnoraPreviewUrl(source.url); + const knowledgeBaseId = + source.knowledge_base_id || + source.knowledgeBaseId || + source.mapping_id || + source.mappingId || + fromPreview?.knowledgeBaseId || + ""; + if (!knowledgeBaseId) return null; + + const wikiSlug = source.wiki_slug || source.wikiSlug || source.slug || fromPreview?.wikiSlug || ""; + const documentId = + source.document_id || + source.documentId || + source.knowledge_id || + source.knowledgeId || + fromPreview?.documentId || + ""; + const chunkId = source.chunk_id || source.chunkId || fromPreview?.chunkId || ""; + return { + knowledgeBaseId, + chunkId: chunkId || undefined, + documentId: documentId || undefined, + wikiSlug: wikiSlug || undefined, + tab: wikiSlug ? "wiki" : "documents", + }; +} + +export function buildWeKnoraSourceIframeUrl(target: WeKnoraSourceTarget): string { + const params = new URLSearchParams(); + if (target.tab) params.set("tab", target.tab); + if (target.chunkId) params.set("sourceChunkId", target.chunkId); + 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}` : ""}`; +} + +export function buildLegacyKnowledgeSourceUrl(source: KnowledgeSourceLike): string { + const recUuid = source.recUuid || source.rec_uuid || source.url || ""; + if (!recUuid) return ""; + if (/^https?:\/\//i.test(recUuid)) return recUuid; + const base = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL"); + if (base) { + return `${base.replace(/\/+$/, "")}/#/knowledge/mid-detail?recUuid=${encodeURIComponent(recUuid)}`; + } + return ""; +} + +export function openKnowledgeSource(source: KnowledgeSourceLike): boolean { + const weknoraTarget = resolveWeKnoraSourceTarget(source); + const href = weknoraTarget + ? buildWeKnoraSourceIframeUrl(weknoraTarget) + : buildLegacyKnowledgeSourceUrl(source); + if (!href) return false; + window.open(href, "_blank", "noopener,noreferrer"); + return true; +} + +export function normalizeMalformedWeKnoraHashRoute(): boolean { + if (typeof window === "undefined") return false; + 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}`); + return true; +} diff --git a/frontend-web/src/main.tsx b/frontend-web/src/main.tsx index 263da283..35280c2e 100644 --- a/frontend-web/src/main.tsx +++ b/frontend-web/src/main.tsx @@ -10,15 +10,18 @@ import "katex/dist/katex.min.css"; import React from "react"; import ReactDOM from "react-dom/client"; -import { App } from "./App"; -import { syncEmbedSessionFromCurrentUrl } from "@/core/embed/embed-session"; - -// 渲染前同步启用「iframe 嵌入会话」(按标签页 sessionStorage):必须早于任何鉴权写入, -// 使 iframe 嵌入态的登录走 sessionStorage、与同浏览器正常登录(localStorage)完全隔离。 -syncEmbedSessionFromCurrentUrl(); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +import { App } from "./App"; +import { syncEmbedSessionFromCurrentUrl } from "@/core/embed/embed-session"; +import { normalizeMalformedWeKnoraHashRoute } from "@/core/llmwiki/source-links"; + +if (!normalizeMalformedWeKnoraHashRoute()) { + // 渲染前同步启用「iframe 嵌入会话」(按标签页 sessionStorage):必须早于任何鉴权写入, + // 使 iframe 嵌入态的登录走 sessionStorage、与同浏览器正常登录(localStorage)完全隔离。 + syncEmbedSessionFromCurrentUrl(); + + ReactDOM.createRoot(document.getElementById("root")!).render( + + + , + ); +} diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index d070d594..786ae401 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -39,6 +39,7 @@ import AdminSkillsPage from './AdminSkillsPage'; import ScheduledTaskRunDetailPage from './ScheduledTaskRunDetailPage'; import ScheduledTasksPage from './ScheduledTasksPage'; import LlmWikiKnowledgePage from './LlmWikiKnowledgePage'; +import WeKnoraWikiManagementPage from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage'; import LlmMetricsPage from './LlmMetricsPage'; import CheckpointMigrationPage from './CheckpointMigrationPage'; import MemoryPage from './MemoryPage'; @@ -656,6 +657,14 @@ export default function WorkspaceRoutes({ } /> + + + + } + /> ; +} + export interface LlmWikiSearchResult { chunk_id: string; content: string; @@ -159,6 +176,33 @@ export interface LlmWikiWeKnoraEmbedSession { expires_in: number; } +export interface LlmWikiWikiPageCreate { + slug: string; + title: string; + content: string; + summary?: string; + page_type?: string; + status?: string; + aliases?: string[]; + parent_slug?: string; + category_path?: string[]; + folder_id?: string; + wiki_path?: string; +} + +export interface LlmWikiWikiPageUpdate { + title?: string; + content?: string; + summary?: string; + page_type?: string; + status?: string; + aliases?: string[]; + parent_slug?: string; + category_path?: string[]; + folder_id?: string; + wiki_path?: string; +} + async function readJson(response: Response, context: string): Promise { if (!response.ok) { const payload = await response.json().catch(() => null as unknown); @@ -226,6 +270,21 @@ export async function createLlmWikiWeKnoraEmbedSession( ); } +export async function getLlmWikiSourceContext( + knowledgeBaseId: string, + chunkId: string, +): Promise { + const params = new URLSearchParams({ + knowledge_base_id: knowledgeBaseId, + chunk_id: chunkId, + }); + const payload = await readJson<{ success: boolean; data: LlmWikiSourceContext }>( + await apiFetch(`${API()}/sources/context?${params}`), + "加载参考文献原文", + ); + return payload.data; +} + export async function createLlmWikiKnowledgeBase(input: { name: string; description: string; @@ -353,6 +412,56 @@ export async function getLlmWikiWikiPage( ); } +export async function createLlmWikiWikiPage( + knowledgeBaseId: string, + input: LlmWikiWikiPageCreate, +): Promise { + return readJson( + await apiFetch( + `${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/wiki/pages`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }, + ), + "创建 Wiki 页面", + ); +} + +export async function updateLlmWikiWikiPage( + knowledgeBaseId: string, + slug: string, + input: LlmWikiWikiPageUpdate, +): Promise { + const safeSlug = slug.split("/").map(encodeURIComponent).join("/"); + return readJson( + await apiFetch( + `${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/wiki/pages/${safeSlug}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }, + ), + "鏇存柊 Wiki 椤甸潰", + ); +} + +export async function deleteLlmWikiWikiPage( + knowledgeBaseId: string, + slug: string, +): Promise { + const safeSlug = slug.split("/").map(encodeURIComponent).join("/"); + await readJson( + await apiFetch( + `${API()}/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/wiki/pages/${safeSlug}`, + { method: "DELETE" }, + ), + "鍒犻櫎 Wiki 椤甸潰", + ); +} + export async function uploadLlmWikiDocument( knowledgeBaseId: string, file: File, @@ -698,6 +807,21 @@ export function useLlmWikiActions() { reprocessLlmWikiDocument(id, documentId), onSuccess: invalidateKnowledge, }), + createWikiPage: useMutation({ + mutationFn: ({ id, input }: { id: string; input: LlmWikiWikiPageCreate }) => + createLlmWikiWikiPage(id, input), + onSuccess: invalidateKnowledge, + }), + updateWikiPage: useMutation({ + mutationFn: ({ id, slug, input }: { id: string; slug: string; input: LlmWikiWikiPageUpdate }) => + updateLlmWikiWikiPage(id, slug, input), + onSuccess: invalidateKnowledge, + }), + removeWikiPage: useMutation({ + mutationFn: ({ id, slug }: { id: string; slug: string }) => + deleteLlmWikiWikiPage(id, slug), + onSuccess: invalidateKnowledge, + }), publication: useMutation({ mutationFn: ({ id, publish }: { id: string; publish: boolean }) => setLlmWikiPublication(id, publish), diff --git a/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.mock.ts b/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.mock.ts index d6f235bf..62bc6cb4 100644 --- a/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.mock.ts +++ b/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.mock.ts @@ -310,11 +310,29 @@ export const createMockSingleRagMessages = (): Message[] => ([ /** * 将多轮检索接口返回的 references 项映射为通用引用来源。 */ -export const mapReferenceToSource = (item: { title?: string; content?: string; snippet?: string }): SourceItem => ({ +export const mapReferenceToSource = (item: { + title?: string; + content?: string; + snippet?: string; + url?: string; + knowledge_base_id?: string; + knowledge_id?: string; + document_id?: string; + chunk_id?: string; + wiki_slug?: string; + slug?: string; +}): SourceItem => ({ title: item.title || '未命名来源', type: '参考资料', snippet: item.snippet || item.content || '', content: item.content || item.snippet || '', + url: item.url, + knowledge_base_id: item.knowledge_base_id, + knowledge_id: item.knowledge_id, + document_id: item.document_id, + chunk_id: item.chunk_id, + wiki_slug: item.wiki_slug, + slug: item.slug, }); /** @@ -343,6 +361,12 @@ export const mapDocsToSources = (docs: KnowledgeBaseChunk['docs']): SourceItem[] source: item.source || item.title || '', time: '', url: item.url, + knowledge_base_id: (item as any).knowledge_base_id, + knowledge_id: (item as any).knowledge_id, + document_id: (item as any).document_id, + chunk_id: (item as any).chunk_id, + wiki_slug: (item as any).wiki_slug, + slug: (item as any).slug, content: item.content || item.snippet || '', }; }); @@ -447,4 +471,4 @@ export const platformTypeChange = (platform: string) => { platformType = 'facebook'; } return platformType; - } \ No newline at end of file + } diff --git a/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.tsx b/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.tsx index 674a4ef6..95403f81 100644 --- a/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.tsx +++ b/frontend-web/src/strategy-components/components/chat-qa/GeneralQA.tsx @@ -30,7 +30,9 @@ import { import { requestCrudService } from '@/service/request-axios-service'; import { authStorageGet } from '@/core/auth/scoped-storage'; import { copyTextToClipboard } from '@/core/clipboard'; +import { openKnowledgeSource, resolveWeKnoraSourceTarget, type WeKnoraSourceTarget } from '@/core/llmwiki/source-links'; import { readRuntimeEnv } from '@/env'; +import { WeKnoraSourceDrawer } from '@/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer'; /** * 通用问答组件 @@ -43,7 +45,6 @@ const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || ''; const chatUrl = readRuntimeEnv('VITE_AIPI_CHAT_URL') || ''; const generateUrl = readRuntimeEnv('VITE_AIPI_GENERATE_CHAT_URL') || ''; const taskChatUrl = readRuntimeEnv('VITE_AIPI_TASK_CHAT_URL') || ''; -const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL'); const service = requestCrudService; const GeneralQA: React.FC<{ onNavigate?: (page: string) => void; @@ -110,6 +111,10 @@ const GeneralQA: React.FC<{ const [showScrollToBottom, setShowScrollToBottom] = useState(false); // 当前选中的引用来源,用于显示引用详情弹窗 const [selectedCitation, setSelectedCitation] = useState(null); + const [weknoraDrawer, setWeknoraDrawer] = useState<{ + target: WeKnoraSourceTarget; + title: string; + } | null>(null); const [editingMessageId, setEditingMessageId] = useState(null); const [editingThinking, setEditingThinking] = useState(''); const [editingAnswer, setEditingAnswer] = useState(''); @@ -384,6 +389,13 @@ const GeneralQA: React.FC<{ source: item.source1, time: item.m_publish, url: item.recUuid, + recUuid: item.recUuid, + knowledge_base_id: item.knowledge_base_id, + knowledge_id: item.knowledge_id, + document_id: item.document_id, + chunk_id: item.chunk_id, + wiki_slug: item.wiki_slug, + slug: item.slug, content: item.m_content || '', })); // 更新检索统计信息 @@ -2380,12 +2392,22 @@ const GeneralQA: React.FC<{
- {selectedCitation.url && ( + {(selectedCitation.url || resolveWeKnoraSourceTarget(selectedCitation)) && ( )} @@ -2406,6 +2428,15 @@ const GeneralQA: React.FC<{ )} + { + if (!nextOpen) setWeknoraDrawer(null); + }} + /> + ({ const isDark = theme === 'dark'; const isUser = message.role === 'user'; const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace); - const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL'); const [visible, setVisible] = useState(false); // 打开详情对话框 const [listData, setListData] = useState({ title: '', @@ -252,10 +253,17 @@ export function ChatMessage({ source: '', content: '', }); + const [weknoraDrawer, setWeknoraDrawer] = useState<{ + target: WeKnoraSourceTarget; + title: string; + } | null>(null); const showDialog = (item: SourceItem) => { console.log('item', item) - if (item.url) { - window.open(`${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${item.url}`); + const weknoraTarget = resolveWeKnoraSourceTarget(item); + if (weknoraTarget) { + setWeknoraDrawer({ target: weknoraTarget, title: item.title || '参考文献原文' }); + } else if (item.url) { + openKnowledgeSource(item); } else { setListData(item); setVisible(true); @@ -361,12 +369,19 @@ export function ChatMessage({
时间:{listData.time || '未知'}
- {listData.url && ( + {(listData.url || resolveWeKnoraSourceTarget(listData)) && ( )}
({
+ { + if (!nextOpen) setWeknoraDrawer(null); + }} + /> {!isUser && renderAssistantExtra?.(message)} diff --git a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx index e733ae3a..40fe6e83 100644 --- a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx +++ b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx @@ -1,14 +1,24 @@ -import { AlertCircle, Loader2, RefreshCw, Upload } from "lucide-react"; +import { AlertCircle, Edit3, FileText, Loader2, RefreshCw, Trash2, Upload } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { Navigate, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { getBackendBaseURL } from "@/core/config"; import { createLlmWikiWeKnoraEmbedSession, + getLlmWikiSourceContext, useLlmWikiActions, useLlmWikiKnowledgeBase, + useLlmWikiWikiPage, useLlmWikiRuntime, } from "@/strategy-components/api/llmwiki"; @@ -86,10 +96,12 @@ export function WeKnoraKnowledgeDetailPage() { noteId: workspaceKnowledgeBaseId, } = useParams(); const knowledgeBaseId = strategyKnowledgeBaseId ?? workspaceKnowledgeBaseId ?? ""; + const [searchParams] = useSearchParams(); const navigate = useNavigate(); const frameRef = useRef(null); const fileInputRef = useRef(null); const [frameSrc, setFrameSrc] = useState(""); + const [frameVersion, setFrameVersion] = useState(0); const [frameError, setFrameError] = useState(null); const [uploading, setUploading] = useState(false); const runtime = useLlmWikiRuntime(); @@ -97,7 +109,16 @@ export function WeKnoraKnowledgeDetailPage() { const actions = useLlmWikiActions(); const detailsReady = !!details.data; - const preferredTab = details.data?.indexing_strategy?.wiki_enabled ? "wiki" : "documents"; + const requestedTab = searchParams.get("tab"); + const preferredTab = + requestedTab === "documents" || requestedTab === "wiki" || requestedTab === "graph" + ? requestedTab + : details.data?.indexing_strategy?.wiki_enabled + ? "wiki" + : "documents"; + const sourceChunkId = searchParams.get("sourceChunkId") || searchParams.get("chunkId") || ""; + const documentId = searchParams.get("documentId") || ""; + const wikiSlug = searchParams.get("wikiSlug") || ""; useEffect(() => { let cancelled = false; @@ -109,14 +130,24 @@ export function WeKnoraKnowledgeDetailPage() { }; } - void createLlmWikiWeKnoraEmbedSession(knowledgeBaseId, preferredTab) - .then((session) => { + void (async () => { + const resolvedWikiSlug = + wikiSlug || + (sourceChunkId + ? (await getLlmWikiSourceContext(knowledgeBaseId, sourceChunkId).catch( + () => null, + ))?.wiki_page?.slug + : ""); + const session = await createLlmWikiWeKnoraEmbedSession(knowledgeBaseId, preferredTab); if (cancelled) return; const framePath = session.frame_path || ""; const nextUrl = buildFrameUrl(framePath); nextUrl.searchParams.set("theme", collectThemePayload().theme); + if (sourceChunkId) nextUrl.searchParams.set("sourceChunkId", sourceChunkId); + if (documentId) nextUrl.searchParams.set("documentId", documentId); + if (resolvedWikiSlug) nextUrl.searchParams.set("wikiSlug", resolvedWikiSlug); setFrameSrc(nextUrl.toString()); - }) + })() .catch((error: unknown) => { if (cancelled) return; const message = error instanceof Error ? error.message : "创建 WeKnora 详情会话失败"; @@ -126,7 +157,7 @@ export function WeKnoraKnowledgeDetailPage() { return () => { cancelled = true; }; - }, [detailsReady, knowledgeBaseId, preferredTab, runtime.data?.provider]); + }, [detailsReady, documentId, knowledgeBaseId, preferredTab, runtime.data?.provider, sourceChunkId, wikiSlug]); useEffect(() => { if (!frameSrc) return; @@ -194,11 +225,14 @@ export function WeKnoraKnowledgeDetailPage() { } const canUpload = details.data.can_write && !details.data.is_conversation_deposit; + const canEditCurrentWikiArticle = + canUpload && !!details.data.indexing_strategy?.wiki_enabled && !!wikiSlug; + const canManageWiki = canUpload && !!details.data.indexing_strategy?.wiki_enabled; return (
- {canUpload && ( -
+ {(canUpload || canEditCurrentWikiArticle) && ( +
0) { await details.refetch(); - if (frameRef.current) frameRef.current.src = frameSrc; + setFrameVersion((value) => value + 1); } if (failed.length === 0) { toast.success(`已上传 ${succeeded} 个文件,正在处理`); @@ -237,23 +271,53 @@ export function WeKnoraKnowledgeDetailPage() { })(); }} /> - +
+
+ {canManageWiki && ( + + )} + {canUpload && ( + + )} +
)} {frameSrc && (