diff --git a/CLAUDE.md b/CLAUDE.md index 7dba5283..54243962 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/` - **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js) +- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. The agent chat is rendered via `TaskEmbeddedAgentChat`, which passes `AgentChatPage`'s `chatBasePath` prop so the post-send URL rewrite + new-chat button stay in the task-management context (instead of the standalone `/agents/...` route). Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`. ### Configuration diff --git a/frontend-web/.env.development b/frontend-web/.env.development index d5dfe0f9..22563825 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(consumer 外网不可达时调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api @@ -27,7 +30,7 @@ VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope -VITE_AIPI_BACKENP_URL=http://localhost:8003 +VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000 # 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL) # 工作室/空间前端地址(用于 auth/callback 等跳转) # VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn diff --git a/frontend-web/.env.example b/frontend-web/.env.example index e254e6c8..1b3d80bb 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -33,6 +33,11 @@ # 仅本机 .env 默认 false(假数据)。改后需重启 dev / 重新打包生效。 # VITE_USE_REAL_DATA=true +# --- 任务详情(登录深链 cop-task-detail)数据源开关 --- +# 默认真实接口(未设/false);改成 true 时该接口走本地假数据(consumer 外网不可达时调试用)。 +# 与 VITE_USE_REAL_DATA 独立。改后需重启 dev / 重新打包生效。 +# VITE_MOCK_COP_TASK_DETAIL=false + # MaxKey 鉴权根地址(无界 onlineTicket 换 token) # VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn # VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer diff --git a/frontend-web/.env.network b/frontend-web/.env.network index 569b8422..471b6289 100644 --- a/frontend-web/.env.network +++ b/frontend-web/.env.network @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://47.88.25.99:7001 VITE_LANGGRAPH_BASE_URL=http://47.88.25.99:7001/api diff --git a/frontend-web/.env.production b/frontend-web/.env.production index 08bf2a70..97415742 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api diff --git a/frontend-web/.env.sit b/frontend-web/.env.sit index d4a4af7b..91ba650f 100644 --- a/frontend-web/.env.sit +++ b/frontend-web/.env.sit @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 0c191a09..88ee59df 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -86,12 +86,22 @@ function AppProviders({ children }: { children: React.ReactNode }) { ); } +/** + * 根路由 `/` → `/login` 重定向,**保留 query string**。 + * 外部系统的任务深链落在 `#/?taskId=...&goPath=...`,裸 `` 会丢掉 + * query,导致 LoginPage 读不到参数;这里把 search 一并带过去。 + */ +function RootRedirect() { + const { search } = useLocation(); + return ; +} + export function App() { return ( - } /> + } /> } /> } /> } /> diff --git a/frontend-web/src/components/wiki/KnowledgeManage.tsx b/frontend-web/src/components/wiki/KnowledgeManage.tsx new file mode 100644 index 00000000..de42b5ea --- /dev/null +++ b/frontend-web/src/components/wiki/KnowledgeManage.tsx @@ -0,0 +1,429 @@ +import { Plus, Trash2 } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + useExtractTemplateActions, + useExtractTemplates, + useFolderActions, + useKnowledgeFolders, +} from "@/core/knowledge/hooks"; +import type { + ExtractTemplateScope, + KnowledgeExtractTemplate, +} from "@/core/knowledge/types"; +import { cn } from "@/lib/utils"; + +const NONE = "__none__"; + +/** A dropdown of managed directories (+ an "unfiled" option). */ +export function FolderSelect({ + value, + onChange, + disabled, + triggerClassName, + allowNone = true, +}: { + value: string | null; + onChange: (v: string | null) => void; + disabled?: boolean; + triggerClassName?: string; + allowNone?: boolean; +}) { + const { data: folders = [] } = useKnowledgeFolders(); + return ( + + ); +} + +/** A dropdown of enabled extraction templates applicable to a scope. */ +export function TemplateSelect({ + value, + onChange, + scope, + disabled, + triggerClassName, +}: { + value: string | null; + onChange: (v: string | null) => void; + scope: ExtractTemplateScope; + disabled?: boolean; + triggerClassName?: string; +}) { + const { data: templates = [] } = useExtractTemplates(scope, true); + return ( + + ); +} + +/** Create / rename / delete knowledge directories. */ +export function FolderManageDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const { data: folders = [], isLoading } = useKnowledgeFolders(); + const { create, rename, remove } = useFolderActions(); + const [newPath, setNewPath] = useState(""); + + const handleCreate = async () => { + const path = newPath.trim(); + if (!path) { + toast.error("请输入目录路径"); + return; + } + try { + await create.mutateAsync(path); + toast.success("目录已创建"); + setNewPath(""); + } catch (error) { + toast.error(error instanceof Error ? error.message : "创建失败"); + } + }; + + const handleRename = async (folderId: string, current: string) => { + const next = window.prompt("重命名 / 移动目录(输入新的完整路径,斜杠分隔)", current); + if (next === null) return; + try { + await rename.mutateAsync({ folderId, path: next }); + toast.success("已更新"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "更新失败"); + } + }; + + const handleDelete = async (folderId: string, path: string) => { + if (!window.confirm(`删除目录「${path}」?其中的知识将变为未归类(不会被删除)。`)) return; + try { + await remove.mutateAsync({ folderId }); + toast.success("目录已删除"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "删除失败"); + } + }; + + return ( + + + + 目录管理 + + 自建知识目录,用斜杠分隔多级(如「投研/行业」)。删除目录只会让其中的知识变为未归类。 + + +
+ setNewPath(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleCreate(); + }} + /> + +
+ + {isLoading ? ( +
加载中...
+ ) : folders.length === 0 ? ( +
+ 还没有目录,先在上方新建一个。 +
+ ) : ( +
    + {folders.map((f) => ( +
  • + {f.path} +
    + + +
    +
  • + ))} +
+ )} +
+ + + +
+
+ ); +} + +const SCOPE_LABELS: Record = { + thread: "对话", + document: "文档", + both: "通用", +}; + +const EMPTY_DRAFT = { + name: "", + scope: "both" as ExtractTemplateScope, + description: "", + system_prompt: "", + is_default: false, + enabled: true, +}; + +/** Create / edit / delete the editable extraction-prompt templates. */ +export function ExtractTemplateManageDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const { data: templates = [], isLoading } = useExtractTemplates(); + const { create, update, remove } = useExtractTemplateActions(); + const [selectedId, setSelectedId] = useState(null); + const [draft, setDraft] = useState({ ...EMPTY_DRAFT }); + + const selected = useMemo( + () => templates.find((t) => t.id === selectedId) ?? null, + [templates, selectedId], + ); + + // Load the selected template into the editable draft (or reset for "new"). + useEffect(() => { + if (selected) { + setDraft({ + name: selected.name, + scope: selected.scope, + description: selected.description ?? "", + system_prompt: selected.system_prompt, + is_default: selected.is_default, + enabled: selected.enabled, + }); + } else { + setDraft({ ...EMPTY_DRAFT }); + } + }, [selected]); + + const startNew = () => setSelectedId(null); + + const handleSave = async () => { + if (!draft.name.trim() || !draft.system_prompt.trim()) { + toast.error("请填写名称与提示词正文"); + return; + } + try { + if (selectedId) { + await update.mutateAsync({ templateId: selectedId, data: draft }); + toast.success("已保存"); + } else { + const created = await create.mutateAsync(draft); + setSelectedId(created.id); + toast.success("模板已创建"); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存失败"); + } + }; + + const handleDelete = async (t: KnowledgeExtractTemplate) => { + if (!window.confirm(`删除模板「${t.name}」?`)) return; + try { + await remove.mutateAsync(t.id); + if (selectedId === t.id) setSelectedId(null); + toast.success("已删除"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "删除失败"); + } + }; + + return ( + + + + 抽取提示词模板 + + 编辑驱动「知识沉淀」的提炼提示词,可新建多套抽取逻辑。沉淀对话/导入文件时可选用对应模板;未选则用该类型的默认模板。 + + +
+ {/* template list */} +
+ + + {isLoading ? ( +
加载中...
+ ) : ( +
    + {templates.map((t) => ( +
  • + +
  • + ))} +
+ )} +
+
+ + {/* editor */} +
+
+ setDraft((d) => ({ ...d, name: e.target.value }))} + /> + +
+ setDraft((d) => ({ ...d, description: e.target.value }))} + /> +