diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..e02ee52c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "PowerShell(uv --version)", + "PowerShell(netstat -ano)", + "PowerShell(Get-NetTCPConnection -LocalPort 8001 -ErrorAction SilentlyContinue)", + "PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })" + ] + } +} diff --git a/.gitignore b/.gitignore index 532043d8..ba42257b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ frontend-web/.vscode/ # Local runtime logs and pid files .runtime/ +logs/ +offline-backend-20260512/backend/logs/ # Frontend local/runtime artifacts frontend-web/.env @@ -81,3 +83,6 @@ hermes-agent-main/ offline-backend-20260512/backend/tests/* deploy .history + +# Frontend skills components must be tracked (excluded from the broad skills/ ignore above) +!frontend-web/src/components/workspace/skills/ diff --git a/frontend-web/.env.development b/frontend-web/.env.development index 0d15230d..41decf09 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -7,6 +7,20 @@ VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 - +# CMZS后端接口 +VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm +# 佳来模型对话接口 +VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt +# 多轮检索接口 +VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat +VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat +VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary +VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web +VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope +VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn +VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer +VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn +VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn # 可选:Canvas 独立 LangGraph 地址(不设则回退到 VITE_LANGGRAPH_BASE_URL) # VITE_CANVAS_LANGGRAPH_BASE_URL= diff --git a/frontend-web/.env.development.local.example b/frontend-web/.env.development.local.example index 3f551f71..471b2649 100644 --- a/frontend-web/.env.development.local.example +++ b/frontend-web/.env.development.local.example @@ -3,5 +3,18 @@ VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api +# 佳来模型对话接口 +VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt +# 多轮检索接口 +VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat +VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat +VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary +VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web +VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope +VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn +VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer +VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn +VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn # VITE_NOTE_API_BASE_URL=http://127.0.0.1:8000 diff --git a/frontend-web/.env.production b/frontend-web/.env.production index 1d13e37e..0e94e082 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -7,6 +7,19 @@ VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api +# 佳来模型对话接口 +VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt +# 多轮检索接口 +VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat +VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat +VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary +VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web +VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope +VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn +VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer +VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn +VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn # 可选:Canvas 独立 LangGraph 地址 # VITE_CANVAS_LANGGRAPH_BASE_URL= diff --git a/frontend-web/docs/roundtable-step2-sandbox-progress.md b/frontend-web/docs/roundtable-step2-sandbox-progress.md new file mode 100644 index 00000000..d3fd7fed --- /dev/null +++ b/frontend-web/docs/roundtable-step2-sandbox-progress.md @@ -0,0 +1,207 @@ +# 圆桌 Step 2 沙箱预览 · 开发进度 + +> 最后更新:2026-05-27 +> 关联文档:`multi-agent-frontend-dev.md`(总览)、`multi-agent-backend-dev.md`(后端)、`multi-agent-api.md`(接口) +> 对照参考:主聊天 `/page/workspace/chats/:threadId`(`ChatBox` + `ArtifactsProvider` + `message-group.tsx`) + +--- + +## 1. 目标 + +在 Step 2「多智能体圆桌研讨」中,当子智能体调用 `write_file` / `str_replace` / `present_files` 时,**行为与主聊天一致**: + +| 能力 | 主聊天 | Step 2 目标 | +|------|--------|-------------| +| `write_file` 流式期间自动打开右侧沙箱 | ✅ | ✅ 虚拟 URL | +| 步骤卡里点击文件路径打开沙箱 | ✅ | ✅ | +| 消息下方文件卡片(`present_files`)点击打开 | ✅ | ✅ `PresentFilesRow` | +| 沙箱打开时收起 workspace 左侧栏 | ✅(`ArtifactsProvider.select`) | ✅ Step 2 左栏联动 | +| 60/40 对话 \| 产物预览分栏 | ✅ `ChatBox` | ✅ `Step2SandboxLayout` | +| 流式 MD 从 tool_call.args.content 预览 | ✅ 虚拟 URL + stub messages | ✅ `seatStubMessages`(双通道:`onPhaseChange` + `onUpstreamFrame`) | +| 落盘后 HTTP 拉取真实文件 | ✅ artifacts API | ✅ `present_files` 卡片走 HTTP | +| 草稿 reload 后历史文件可预览 | ✅ | ✅ `seatStubMessages` 一并入草稿 | + +--- + +## 2. 已完成 + +### 2.1 Step 2 布局调整 + +| 项 | 状态 | 说明 | +|----|------|------| +| 推理模型下拉 | ✅ | 从右侧栏迁至 Step 2 顶栏「多智能体圆桌会商中心」右侧 | +| 意图理解 | ✅ | 移至左侧栏顶部,支持展开/收起;收起时仅显示「目标」 | +| 任务概览 | ✅ | 放在「人工参与」下方 | +| 右侧栏 Step 2/3 隐藏 | ✅ | `RightSidebar` 仅在 `currentStep === 1` 渲染 | +| 左侧栏整体收起 | ✅ | 宽度 56px;展开按钮在顶部;研讨中角色 ring + ping 动画 | +| 沙箱打开 → 左栏收起 | ✅ | `Step2SandboxSidebarSync` 监听 `useArtifacts().open` | + +**主要文件**:`RoundtablePlanningPage.tsx`(`step2SidebarExpanded`、`LeftSidebar`、`Step2SandboxSidebarSync`) + +### 2.2 沙箱基础设施(Phase 1) + +| 项 | 状态 | 说明 | +|----|------|------| +| 路由包 `ChatRuntime` | ✅ | `WorkspaceRoutes.tsx` → `roundtable/planning` 外包 `ArtifactsProvider` | +| 60/40 分栏壳 | ✅ | `Step2SandboxLayout.tsx`,对齐 `chat-box.tsx` | +| `ArtifactFileDetail` / `ArtifactFileList` | ✅ | 嵌在右栏 ResizablePanel | +| `ThreadContext` stub | ✅ | 最小 `BaseStream` stub,供 `useArtifactContent` 工作 | + +### 2.3 沙箱打开逻辑(Phase 2) + +| 项 | 状态 | 说明 | +|----|------|------| +| `write_file` 自动打开 | ✅ | `handleArtifactToolPhase` 在 `tool_calling` + 完整 `args.path` 时触发 | +| 步骤卡点击打开 | ✅ | `MessageStepsCard` → `openArtifactFromStep` | +| 虚拟 URL 机制 | ✅ | `write-file:?message_id=...&tool_call_id=...`(对齐主聊天) | +| Stub messages 注入 | ✅ | `seatStubMessages` + `upsertSeatToolCall` → `loadArtifactContentFromToolCall` | +| 重置清理 | ✅ | `resetStep2State` 清空 artifacts / stub / deselect | +| `present_files` 步骤点击 | ✅ | 走真实路径 `openArtifactPreview`(HTTP API) | + +### 2.4 上游帧兜底注入(Phase 3) + +| 项 | 状态 | 说明 | +|----|------|------| +| `onPhaseChange` 主通道 | ✅ | `tool_calling` 阶段每帧把完整 `args` 喂进 stub;首次出现 `path` 即触发自动打开 | +| `onUpstreamFrame` 兜底通道 | ✅ | `handleUpstreamFrame` 扫描 messages-tuple 帧的 `tool_calls[]`,对 `write_file`/`str_replace` 持续 upsert `args.content`;覆盖 phase 已切走但 tool_calls 又回包的边界 | +| 双通道幂等 | ✅ | `upsertSeatToolCall` 按 `(toolName, path)` 去重,两个通道并发写入安全 | + +### 2.5 `present_files` 文件卡(Phase 4) + +| 项 | 状态 | 说明 | +|----|------|------| +| 气泡下方文件卡行 | ✅ | `Step2Message` → `collectPresentedFiles` → `PresentFilesRow` | +| 卡片样式 | ✅ | 文件名 + 扩展名徽章(FILE_EXT_LABELS),最小宽度 30/最大 50(hover 边框亮色) | +| 点击 → HTTP 预览 | ✅ | `openArtifactPreview(agentThreadId, path)`,与步骤卡 `present_files` 走同一通道 | +| 席位 thread 缺失保护 | ✅ | 没有 `agentThreadId` 时 disabled + tooltip | +| 同气泡多次 present_files | ✅ | `filepaths` 顺序去重,全部聚合到一行卡片 | + +### 2.6 草稿持久化(Phase 5) + +| 项 | 状态 | 说明 | +|----|------|------| +| `seatStubMessages` 进 `Step2Snapshot` | ✅ | `useStep2Orchestration.ts` Step2Snapshot 新增字段 | +| `seatStubMessages` 进 `DraftStep2Snapshot` | ✅ | `drafts.ts` 加 optional 字段,老草稿 reload 时回退到 `{}` | +| `hydrateFromDraft` 还原 stub | ✅ | 同步写 `seatStubMessagesRef` + state,加载草稿后点击历史 write_file 步骤可直接预览 | +| `getStep2Snapshot` 透出 stub | ✅ | `RoundtablePlanningPage.tsx` 给 `useDraftPersistence` 的 getter 加上该字段 | + +**主要文件**: + +``` +frontend-web/src/pages/WorkspaceRoutes.tsx +frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts +frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +frontend-web/src/roundtable-planning/components/Step2Panel.tsx +frontend-web/src/roundtable-planning/components/MessageStepsCard.tsx +frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +frontend-web/src/roundtable-planning/utils/drafts.ts +``` + +### 2.7 与主聊天对齐的核心数据流 + +``` +special seat SSE (streamMultiAgent) + ├── onPhaseChange("tool_calling", "write_file", args) + │ ↓ + │ upsertSeatToolCall(seatThreadId, "write_file", args) + │ → seatStubMessages[seatThreadId] = [{ type:"ai", tool_calls:[{ id, name, args }] }] + │ ↓ + │ buildWriteFileVirtualUrl(path, messageId, toolCallId) + │ → "write-file:/mnt/user-data/outputs/foo.md?message_id=...&tool_call_id=..." + │ ↓ + │ openSeatVirtualArtifact(seatThreadId, virtualUrl, auto=true) + │ → selectArtifact(virtualUrl) + setArtifactsOpen(true) + │ + └── onUpstreamFrame(frame) ← 兜底通道 + ↓ + handleUpstreamFrame —— 持续 upsert tool_calls 里完整的 args(不打开沙箱) + +Step2SandboxLayout + ThreadContext.thread.messages = seatStubMessages[activeThreadId] + ArtifactFileDetail(filepath=virtualUrl, threadId=seatThreadId) + ↓ + useArtifactContent → loadArtifactContentFromToolCall + → 从 thread.messages 读 tool_call.args.content(不发 HTTP) +``` + +**为何不用真实 path 直接预览**:流式期间文件可能尚未落盘,直接调 `/api/threads/:id/artifacts/...` 会返回 `404 {"detail":"Not Found"}`。主聊天同样用虚拟 URL 绕过此问题。 + +--- + +## 3. 未完成 / 待验证 + +### 3.1 功能缺口(按优先级) + +| 优先级 | 项 | 状态 | 说明 | +|--------|----|------|------| +| P0 | 端到端回归 | ⚠️ 待验证 | 虚拟 URL + 双通道 + 文件卡 + 草稿持久化全链路尚未在浏览器里跑一遍 | +| P2 | `str_replace` 流式编辑预览 | ❌ | 主聊天 `applyPendingTextEdit` 依赖完整 message 链;stub 未模拟 tool result / 多轮 str_replace 累加 | +| P2 | 沙箱 `artifacts[]` 列表与虚拟 URL 混排 | ❌ | 虚拟 URL 不写入 `setArtifacts([])`;多文件切换、文件名下拉体验未完全对齐主聊天 | +| P3 | Step 3 与沙箱衔接 | ❌ | Step 3 仍用 `HighFidelityReport` + `listSessionArtifacts`;与 Step 2 沙箱选中态无联动 | +| P3 | 文档同步 | ❌ | `multi-agent-frontend-dev.md` 仍描述旧布局(右侧栏模型选择器等),需更新 § 布局 + 沙箱章节 | + +### 3.2 已知风险 / 边界 + +1. **打开时机**:仅在 SSE 进入 `tool_calling` 且 `tool_calls[0].args` 为完整对象时触发自动打开(`multi-agent.ts` 刻意忽略 `tool_call_chunks` 的拼接 args)。若模型只发 chunks、迟迟不发完整 tool_calls,自动打开会延迟到 args 完整那一帧——与主聊天一致。 +2. **多 write_file 同气泡**:`upsertSeatToolCall` 按 `(toolName, path)` 幂等;同 path 多次 write 会更新 args,不会新建多条 stub tool_call。 +3. **leader 席位**:当前仅 special 流绑定 `agentThreadId` 并接 `onUpstreamFrame`;总控若直接 write_file,需单独接 thread id 与帧回调。 +4. **`present_files` 文件大小**:流式期间 backend 尚未给出真实大小;卡片只显示扩展名徽章,不显示 size。落盘后用户点开走 HTTP API 自然能看到完整内容。 +5. **调试日志**:控制台 `[roundtable-sandbox]` 前缀;生产环境可考虑加 `window.__ROUNDTABLE_SANDBOX_DEBUG__` 门控。 + +--- + +## 4. 分阶段计划(原始规划 vs 现状) + +| 阶段 | 内容 | 状态 | +|------|------|------| +| Phase 1 | `ChatRuntime` + 分栏壳 + 侧栏联动 | ✅ 完成 | +| Phase 2 | `write_file` 自动/点击打开 + 虚拟 URL 预览 | ✅ 完成 | +| Phase 3 | `onUpstreamFrame` 注入完整 LangGraph messages | ✅ 完成(双通道兜底) | +| Phase 4 | `present_files` 卡片 + Step 3 文件区衔接 | ⚠️ 卡片完成;Step 3 衔接遗留 | +| Phase 5 | 草稿 stub 持久化 + 文档更新 | ✅ stub 完成;本文档同步更新 | + +--- + +## 5. 测试清单 + +在 `/page/workspace/roundtable/planning` Step 2 下逐项勾选: + +- [ ] 子智能体 `write_file` 进入 `tool_calling` 后,右侧沙箱 **自动** 打开(约 100ms 延迟,对齐主聊天) +- [ ] 沙箱标题为文件名(如 `sample-paper.md`),**不是** `{"detail":"Not Found"}` +- [ ] 流式期间 MD 内容随 `args.content` 更新(双通道:phase 切换帧 + 兜底帧) +- [ ] 点击 ChainOfThought 步骤「写入文件」/ 路径 chip → 手动打开同一文件 +- [ ] `present_files` 后气泡下方出现文件卡(带扩展名徽章),点击 → HTTP 预览 +- [ ] 沙箱打开时左侧「意图理解」栏 **自动收起** +- [ ] 关闭沙箱(X)后对话区恢复全宽 +- [ ] 「新建任务」后 artifacts / 沙箱 / `seatStubMessages` 全部清空 +- [ ] 「保存草稿」→「加载草稿」后,点击历史 `write_file` 步骤可立即预览(stub 已还原) +- [ ] 与主聊天 `/page/workspace/chats/:threadId` 对比:同一 write_file 行为一致 + +**调试**:DevTools Console 过滤 `[roundtable-sandbox]` 或 `[stream:special:...]`。 + +--- + +## 6. 关键代码索引 + +| 用途 | 路径 | +|------|------| +| 主聊天 write_file 虚拟 URL + 自动打开 | `src/components/workspace/messages/message-group.tsx` | +| 主聊天 `RichFileCard`(present_files) | `src/components/workspace/messages/message-list-item.tsx` | +| 主聊天 60/40 分栏 | `src/components/workspace/chats/chat-box.tsx` | +| 虚拟 URL 内容加载 | `src/core/artifacts/loader.ts` → `loadArtifactContentFromToolCall` | +| Artifacts 上下文 | `src/components/workspace/artifacts/context.tsx` | +| Step 2 编排 + 沙箱状态 + 草稿快照 | `src/roundtable-planning/hooks/useStep2Orchestration.ts` | +| Step 2 沙箱布局 | `src/roundtable-planning/components/Step2SandboxLayout.tsx` | +| Step 2 文件卡 + 步骤渲染 | `src/roundtable-planning/components/Step2Panel.tsx` | +| SSE phase 推断 + `onUpstreamFrame` | `src/roundtable-planning/api/multi-agent.ts` → `setPhase` | +| 主页面侧栏 + 联动 | `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | +| 草稿持久化 | `src/roundtable-planning/utils/drafts.ts`、`hooks/useDraftPersistence.ts` | + +--- + +## 7. 下一步建议(给接续开发者) + +1. **先跑通 P0 测试清单**,确认 Phase 3/4/5 改造后无回归;重点观察双通道并发写入 stub 时 args 是否被旧帧覆盖(理论上 LangGraph 完整 tool_calls 是单调累积的,应该没问题)。 +2. **Phase 4 收尾**:把 Step 3 的 `listSessionArtifacts` / `HighFidelityReport` 接进沙箱选中态——Step 3 进入时复用 Step 2 的 `ArtifactsProvider`,让用户在 Step 3 也能预览 Step 2 留下的产物。 +3. **Phase 2 边界**:`str_replace` 多轮编辑的 stub 模拟(追加 tool result 消息让 `applyPendingTextEdit` 能 replay edit 序列),优先级低。 +4. **更新** `multi-agent-frontend-dev.md`:Step 2 布局、沙箱架构、`seatStubMessages` 数据流,避免与本文档重复——可在总览文档加一节「见 `roundtable-step2-sandbox-progress.md`」。 diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 155c3f64..ea03c986 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { ThemeProvider as NextThemesProvider } from "next-themes"; import { QueryClientProvider } from "@/components/query-client-provider"; +import { AuthSyncWatcher } from "@/core/auth/sync"; import { I18nProvider } from "@/core/i18n/context"; import { detectLocale } from "@/core/i18n/locale"; import { ChatProvider } from "@/contexts/ChatContext"; @@ -31,13 +32,18 @@ function AppProviders({ children }: { children: React.ReactNode }) { disableTransitionOnChange forcedTheme={forcedTheme} > - - - - {children} - - - + {/* QueryClientProvider must wrap ThemeProvider because the custom + ThemeProvider reads the admin-configured default via useQuery. */} + + + + + + {children} + + + + ); } diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css new file mode 100644 index 00000000..61890fb3 --- /dev/null +++ b/frontend-web/src/components/page-sdebar.css @@ -0,0 +1,24 @@ +.sidebar-contents{ + width:98px; + height:986px; + background:rgba(3, 38, 80, 0.8); + border:1px solid; + border-color:rgba(43, 143, 254, 0.6); + border-radius:4px; + backdrop-filter:blur(2px); +} + +.item-nav{ + width:78px; + height:70px; + background:rgba(0, 12, 28, 0.15); + border:1px solid; + border-color:rgba(85, 166, 254, 0.2); + line-height:8px; + font-family:Noto Sans S Chinese; + font-weight:500; + color:rgba(255, 255, 255, 0.8); + font-size:20px; + padding-bottom:18px; + text-align:center; +} \ No newline at end of file diff --git a/frontend-web/src/components/page-sidebar-dark.tsx b/frontend-web/src/components/page-sidebar-dark.tsx new file mode 100644 index 00000000..251e1d82 --- /dev/null +++ b/frontend-web/src/components/page-sidebar-dark.tsx @@ -0,0 +1,311 @@ +import { useEffect, useState } from "react"; +import type { ElementType } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { + Bell, + Bot, + Brain, + CalendarClock, + ChevronLeft, + ChevronRight, + ChevronsUpDown, + CircleHelp, + Database, + FileBox, + FileText, + Folder, + FolderOpen, + Gauge, + Grid3X3, + HelpCircle, + History, + Layout, + Library, + List, + ListChecks, + ListTodo, + MessageSquareCode, + MessageSquarePlus, + Network, + NotebookPen, + Plus, + Settings, + Sparkles, + Target, + User, + Users, + Wrench, +} from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { NotificationDialog } from "@/components/workspace/notification-dialog"; +import { SettingsDialog } from "@/components/workspace/settings"; +import { useTheme } from "@/contexts/ThemeContext"; +import { getStoredAuth } from "@/core/auth"; +import { useI18n } from "@/core/i18n/hooks"; +import { useUnreadCount } from "@/core/notifications"; +import './page-sdebar.css' + +type MenuItem = { + id: string; + label: string; + icon: ElementType; + path?: string; + adminOnly?: boolean; + children?: MenuItem[]; +}; + +type PageSidebarDarkProps = { + collapsed: boolean; + onToggleCollapsed: () => void; +}; + +type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; + +const menuItems: MenuItem[] = [ + { + id: "qa-center", + label: "问答中心", + icon: HelpCircle, + children: [ + { id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" }, + { id: "qa-zzjc", label: "zzjc支持", icon: Bot }, + { id: "qa-zzxd", label: "zzXD应用", icon: Layout }, + { id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" }, + ], + }, + { + id: "light-app-center", + label: "轻应用", + icon: Grid3X3, + children: [ + { + id: "light-app-note-management", + label: "笔记管理", + icon: NotebookPen, + children: [ + { + id: "light-app-new-note", + label: "新建笔记", + icon: Plus, + path: "/page/strategy/light-app/new-note", + }, + { + id: "light-app-note-list", + label: "笔记列表", + icon: List, + path: "/page/strategy/light-app/notes", + }, + { + id: "light-app-note-qa", + label: "笔记问答", + icon: MessageSquareCode, + path: "/page/strategy/light-app/note-qa", + }, + ], + }, + { id: "light-app-square", label: "应用广场", icon: Layout }, + { id: "light-app-records", label: "运行记录", icon: History }, + ], + }, + { + id: "task-management", + label: "任务管理", + icon: Target, + children: [ + { + id: "xk-center-task", + label: "XK中任务", + icon: Folder, + children: [ + { id: "xk-task-jc", label: "任务JC", icon: ListTodo, path: "/page/strategy/task-management/xk-task-jc" }, + { id: "xk-task-dispatch", label: "任务调度", icon: ListTodo, path: "/page/strategy/task-management/xk-task-dispatch" }, + ], + }, + { + id: "coh-center-task", + label: "COH中任务", + icon: Folder, + children: [ + { id: "coh-task-3qfx", label: "3QFX", icon: ListTodo }, + { id: "coh-task-fx", label: "任务FX", icon: ListTodo }, + { id: "coh-task-xdfx", label: "XDFX", icon: ListTodo }, + ], + }, + { + id: "pg-center-task", + label: "PG中任务", + icon: Folder, + children: [ + { id: "pg-task-pg", label: "任务PG", icon: ListTodo }, + { id: "pg-task-xdpg", label: "XDPG", icon: ListTodo }, + { id: "pg-task-info-pg", label: "信息PG", icon: ListTodo }, + ], + }, + { + id: "ty-center-task", + label: "TY中任务", + icon: Folder, + children: [ + { id: "ty-task-xdty", label: "XDTY任务", icon: ListTodo }, + { id: "ty-task-plan", label: "方案TY任务", icon: ListTodo }, + { id: "ty-task-xxty", label: "XXTY任务", icon: ListTodo }, + ], + }, + ], + }, + { + id: "comprehensive-management", + label: "综合管理", + icon: Settings, + children: [ + { id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" }, + { id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" }, + { id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" }, + { + id: "mgmt-tasks", + label: "任务管理", + icon: ListChecks, + path: "/page/strategy/scheduled/tasks", + }, + { + id: "mgmt-recommended-questions", + label: "推荐问题配置", + icon: MessageSquarePlus, + path: "/page/strategy/settings/recommended-questions", + adminOnly: true, + }, + { + id: "mgmt-prompt-prefix", + label: "问答前缀配置", + icon: MessageSquareCode, + path: "/page/strategy/settings/prompt-prefix", + adminOnly: true, + }, + { + id: "mgmt-llm-metrics", + label: "大模型调用统计", + icon: Gauge, + path: "/page/strategy/settings/llm-metrics", + adminOnly: true, + }, + { + id: "mgmt-users", + label: "用户管理", + icon: Users, + path: "/page/strategy/admin/users", + adminOnly: true, + }, + { + id: "mgmt-admin-scheduled-tasks", + label: "定时任务管理", + icon: CalendarClock, + path: "/page/strategy/admin/scheduled-tasks", + adminOnly: true, + }, + { + id: "mgmt-user-activity", + label: "用户活动审计", + icon: History, + path: "/page/strategy/admin/user-activity", + adminOnly: true, + }, + ], + }, + { + id: "resource-management", + label: "资源管理", + icon: Database, + children: [ + { id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network }, + { id: "resource-knowledge-base", label: "知识库资源管理", icon: Library }, + { id: "resource-zz", label: "ZZ资源管理", icon: FileBox }, + ], + }, +]; + +const defaultExpandedIds = new Set([ + "qa-center", + "light-app-center", + "light-app-note-management", + "task-management", + "xk-center-task", +]); + +function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] { + return items + .filter((item) => !item.adminOnly || isAdmin) + .map((item) => + item.children + ? { ...item, children: filterMenuItemsByRole(item.children, isAdmin) } + : item, + ); +} + +export function PageSidebarDark({ + collapsed, + onToggleCollapsed, +}: PageSidebarDarkProps) { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const isAdmin = getStoredAuth()?.system_role === "admin"; + const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin); + const { t } = useI18n(); + const { data: unreadCount = 0 } = useUnreadCount(); + const [expandedIds, setExpandedIds] = useState>(defaultExpandedIds); + const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsDefaultSection, setSettingsDefaultSection] = + useState("appearance"); + const [notificationOpen, setNotificationOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + useEffect(() => { + setMounted(true); + }, []); + + const openSettings = (section: SettingsSection) => { + setSettingsDefaultSection(section); + setSettingsOpen(true); + }; + + const toggleExpanded = (id: string) => { + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + return ( + <> + + + {/* TODO: 在这里编写深色主题的侧边栏 HTML 结构 */} +
+
+
+ 问答管理 +
+
+
+ + ); +} diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index b4e32a8f..810e9e74 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -26,6 +26,7 @@ import { ListTodo, MessageSquareCode, MessageSquarePlus, + ShieldCheck, Network, NotebookPen, Plus, @@ -175,7 +176,7 @@ const menuItems: MenuItem[] = [ { id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" }, { id: "mgmt-tasks", - label: "任务管理", + label: "定时任务", icon: ListChecks, path: "/page/strategy/scheduled/tasks", }, @@ -193,6 +194,13 @@ const menuItems: MenuItem[] = [ path: "/page/strategy/settings/prompt-prefix", adminOnly: true, }, + { + id: "mgmt-private-square", + label: "私密广场配置", + icon: ShieldCheck, + path: "/page/strategy/settings/private-square", + adminOnly: true, + }, { id: "mgmt-llm-metrics", label: "大模型调用统计", diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 99d44e12..a7e2b79b 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -6,8 +6,10 @@ import { InfoIcon, LockIcon, MessageSquareIcon, + ShieldIcon, SparklesIcon, SquarePenIcon, + StarIcon, TagsIcon, Trash2Icon, } from "lucide-react"; @@ -40,8 +42,11 @@ import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-di import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; import { + useAgent, useDeleteAgent, useSetAgentFeatured, + useSetAgentPrivate, + useToggleAgentFavorite, useUpdateAgent, } from "@/core/agents"; import type { Agent } from "@/core/agents"; @@ -61,6 +66,8 @@ export function AgentCard({ agent }: AgentCardProps) { const deleteAgent = useDeleteAgent(); const updateAgent = useUpdateAgent(); const setFeatured = useSetAgentFeatured(); + const setPrivate = useSetAgentPrivate(); + const toggleFavorite = useToggleAgentFavorite(); const { skills, isLoading: isSkillsLoading } = useSkills(); const [deleteOpen, setDeleteOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); @@ -69,11 +76,19 @@ export function AgentCard({ agent }: AgentCardProps) { const [editDescription, setEditDescription] = useState(agent.description ?? ""); const [editSoul, setEditSoul] = useState(agent.soul ?? ""); const [editPublished, setEditPublished] = useState(agent.published); + const [editIsPrivate, setEditIsPrivate] = useState(agent.is_private); const [selectedSkills, setSelectedSkills] = useState( agent.skills ?? [], ); const [detailSkillName, setDetailSkillName] = useState(null); + // The /api/agents list response intentionally omits `soul` (perf optimization + // — SOUL.md is only read on demand). Fetch the detail row when the edit + // dialog opens so the SOUL textarea can be hydrated. + const { agent: agentDetail, isLoading: isAgentDetailLoading } = useAgent( + editOpen ? agent.id : null, + ); + const storedAuth = useMemo(() => getStoredAuth(), []); const currentUserId = storedAuth?.user_id ?? null; const isAdmin = storedAuth?.system_role === "admin"; @@ -81,11 +96,16 @@ export function AgentCard({ agent }: AgentCardProps) { const isOwned = !isBuiltin && agent.user_id !== null && agent.user_id === currentUserId; const isFromOthers = !isBuiltin && !isOwned; - // System admins are allowed to edit and delete built-in agents. + // System admins are allowed to edit and delete any agent (built-in or owned + // by another user) so the admin console can manage the full catalog. // Publish-toggle stays owner-only — built-ins are inherently visible to // everyone so toggling published is moot. - const canEdit = isOwned || (isBuiltin && isAdmin); - const canDelete = isOwned || (isBuiltin && isAdmin); + const canEdit = isOwned || isAdmin; + const canDelete = isOwned || isAdmin; + // Favoriting collects a square agent into "我的"; pointless for one you + // already own, so only offer it on built-in / others' agents. + const canFavorite = !isOwned; + const isFavorite = !!agent.is_favorite; useEffect(() => { if (!editOpen) { @@ -93,18 +113,32 @@ export function AgentCard({ agent }: AgentCardProps) { } setEditName(agent.name); setEditDescription(agent.description ?? ""); - setEditSoul(agent.soul ?? ""); setEditPublished(agent.published); + setEditIsPrivate(agent.is_private); setSelectedSkills(agent.skills ?? []); }, [ agent.description, + agent.is_private, agent.name, agent.published, agent.skills, - agent.soul, editOpen, ]); + useEffect(() => { + if (!editOpen) { + return; + } + // SOUL.md only ships on the detail endpoint; fall back to whatever the list + // happened to carry so older deployments (or cached objects that still + // include `soul`) keep working. + if (agentDetail) { + setEditSoul(agentDetail.soul ?? ""); + } else { + setEditSoul(agent.soul ?? ""); + } + }, [agent.soul, agentDetail, editOpen]); + function handleChat() { router.push(`/page/workspace/agents/${agent.id}/chats/new`); } @@ -125,6 +159,10 @@ export function AgentCard({ agent }: AgentCardProps) { toast.error(t.agents.nameStepInvalidError ?? "名称不能为空"); return; } + if (editIsPrivate && editPublished) { + toast.warning("私密广场与公开发布互斥,请只勾选其一"); + return; + } try { await updateAgent.mutateAsync({ id: agent.id, @@ -132,7 +170,10 @@ export function AgentCard({ agent }: AgentCardProps) { name: trimmedName, description: editDescription, soul: editSoul, - published: editPublished, + // Private agents cannot also be published — force the value the + // backend will see, regardless of any stale state in editPublished. + published: editIsPrivate ? false : editPublished, + is_private: editIsPrivate, skills: selectedSkills, }, }); @@ -144,6 +185,13 @@ export function AgentCard({ agent }: AgentCardProps) { } async function handleTogglePublished() { + // Private and public squares are mutually exclusive — a private agent + // can't simultaneously be published to everyone. Block the toggle + // instead of silently moving the agent between squares. + if (!agent.published && agent.is_private) { + toast.warning("该智能体已加入私密广场,不可发布"); + return; + } try { await updateAgent.mutateAsync({ id: agent.id, @@ -155,6 +203,37 @@ export function AgentCard({ agent }: AgentCardProps) { } } + async function handleTogglePrivate() { + const turningOn = !agent.is_private; + try { + if (isOwned) { + // Owners can re-categorize their own agents through the normal update + // endpoint — keeps publish/private edits in one transaction. + await updateAgent.mutateAsync({ + id: agent.id, + request: { is_private: turningOn }, + }); + } else { + // Admins can move any agent (built-in or other users') via the + // dedicated admin endpoint. + await setPrivate.mutateAsync({ id: agent.id, isPrivate: turningOn }); + } + toast.success(turningOn ? "已加入私密广场" : "已移出私密广场"); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } + + async function handleToggleFavorite() { + const next = !isFavorite; + try { + await toggleFavorite.mutateAsync({ id: agent.id, favorite: next }); + toast.success(next ? "已收藏到「我的」" : "已取消收藏"); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } + async function handleToggleFeatured() { // One-click on/off — ordering is handled by the dedicated 管理 Agent 列表 // dialog. Featured agents get appended to the end on add (Date.now() is a @@ -232,7 +311,7 @@ export function AgentCard({ agent }: AgentCardProps) { 来自他人 )} - {isOwned && agent.published && ( + {isOwned && agent.published && !agent.is_private && ( )} + {agent.is_private && ( + + + 私密 + + )} {agent.featured_order != null && ( )} - - - {(canEdit || canDelete || isOwned || isAdmin) && ( -
+ + {(canEdit || canDelete || isOwned || isAdmin || canFavorite) && ( +
+ {canFavorite && ( + + )} {isAdmin && ( + )} {canEdit && ( @@ -462,7 +586,11 @@ export function AgentCard({ agent }: AgentCardProps) {