From 6b49741e690cda3df6b749faa6e7bb692bfdd6e4 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 10:05:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/docs/multi-agent-api.md | 265 ------------------ ...agent-background-run-遗留问题与下次继续.md | 125 --------- frontend-web/docs/xvqiu-0616.md | 161 +++++++++++ .../workspace/frequent-agents-list.tsx | 127 +++++++++ .../components/workspace/recent-chat-list.tsx | 33 ++- .../workspace/workspace-sidebar.tsx | 13 +- offline-backend-20260512/backend/config.yaml | 2 +- 7 files changed, 332 insertions(+), 394 deletions(-) delete mode 100644 frontend-web/docs/multi-agent-api.md delete mode 100644 frontend-web/docs/multi-agent-background-run-遗留问题与下次继续.md create mode 100644 frontend-web/docs/xvqiu-0616.md create mode 100644 frontend-web/src/components/workspace/frequent-agents-list.tsx diff --git a/frontend-web/docs/multi-agent-api.md b/frontend-web/docs/multi-agent-api.md deleted file mode 100644 index a493801e..00000000 --- a/frontend-web/docs/multi-agent-api.md +++ /dev/null @@ -1,265 +0,0 @@ -# 多智能体协调 API 接口文档 - -后端路由:`offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` -路由前缀:`/api/multi-agent` - -## 概览 - -提供一个"总控 + 多个子智能体"的协调编排接口。 - -- **总控(leader)** 智能体:通过 `agent_orchestration` 技能给子智能体派活 -- **子智能体(special)**:执行被派到的具体任务,完成后把交付内容广播给其他线程 - -调用方按下面顺序使用: - -1. 调 `POST /api/multi-agent/init` 一次性创建协调智能体 + 给每个子智能体建 `thread`,拿到 `thread_ids` 映射 -2. 客户端把 `thread_ids` 保管好,后续每次调 `POST /api/multi-agent/run/stream` 时回传 -3. 先调用 `agent_type="leader"` 拿派活清单,再针对清单里每个子智能体调用 `agent_type="special"` - - -## 1. 初始化多智能体集群 - -### `POST /api/multi-agent/init` - -创建协调智能体并为列表中的每个子智能体建立独立的 `thread`,同时把"职责说明"广播到所有子智能体线程作为初始上下文。 - -#### 请求体 - -```json -{ - "agent_names": ["taiwan-intelligence-gathering-agent", "us-intelligence-gathering-agent"], - "main_agent_name": "main-controller", - "model": "deepseek-chat" -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `agent_names` | `string[]` | 是 | 已有子智能体的 id 列表(kebab-case,区分大小写不敏感,自动转小写) | -| `main_agent_name` | `string` | 否 | 要创建的协调智能体名,默认 `main-agent`。重名时会复用已存在的同名智能体 | -| `model` | `string` | 否 | 协调智能体使用的模型名。不传则用后端默认模型 | - -#### 响应(200) - -```json -{ - "status": "success", - "main_agent_name": "main-controller", - "thread_ids": { - "main-controller": "thread-uuid-xxx", - "taiwan-intelligence-gathering-agent": "thread-uuid-yyy", - "us-intelligence-gathering-agent": "thread-uuid-zzz" - } -} -``` - -`thread_ids` 是一个 `{agent_name: thread_id}` 字典。**前端必须把这个对象完整保管下来**,后续调用 stream 接口需要原样回传。 - -#### 错误 - -| 状态码 | 触发条件 | -|---|---| -| `400` | `agent_names` 为空 | -| `401` | 未携带 `Authorization` 头 | -| `500` | 协调智能体创建失败且不是"已存在"错误 | - ---- - -## 2. 流式调度 - -### `POST /api/multi-agent/run/stream` - -通过 SSE(Server-Sent Events)流式驱动一个 leader 或 special 智能体跑一轮对话。 - -#### 请求体 - -```json -{ - "agent_type": "leader", - "d_agent_thread_id": { - "main-controller": "thread-uuid-xxx", - "taiwan-intelligence-gathering-agent": "thread-uuid-yyy" - }, - "agent_name": "main-controller", - "new_message": "请分析特朗普的支持率", - "skill_stop_names": [], - "model": "deepseek-chat" -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| -| `agent_type` | `"leader"` \| `"special"` | 否 | 默认 `leader`。`leader` 调用协调智能体,`special` 调用子智能体 | -| `d_agent_thread_id` | `object` | 是 | 由 `/init` 接口返回的 `thread_ids` 映射,原样回传 | -| `agent_name` | `string` | 是 | 本轮要驱动的智能体名(必须存在于 `d_agent_thread_id` 中) | -| `new_message` | `string` | 是 | 这轮发给智能体的用户消息 | -| `skill_stop_names` | `string[]` | 否 | 触发后中断执行的技能名列表(默认空) | -| `model` | `string` | 否 | 本轮 LLM 模型名,默认 `deepseek-chat` | - -#### 响应 - -`Content-Type: text/event-stream` 的 SSE 长连接,按到达顺序产出多种数据: - -##### a. 透传上游事件 - -原样转发底层 `POST /api/threads/{thread_id}/runs/stream` 的事件帧(LangGraph 协议),包含 `event:`、`data:` 等行。前端按 LangGraph SDK 既有方式渲染即可。 - -##### b. 最后一帧:调度状态 - -流结束前会额外产出一帧 `data:` 行,结构因 `agent_type` 而异: - -**`agent_type == "leader"`:** - -``` -data: {"status": [["us-intelligence-gathering-agent", "收集美国民众对特朗普的看法"], ["taiwan-intelligence-gathering-agent", "..."]]} -``` - -`status` 是一个 `[agent_name, task_text]` 二元组数组,代表协调智能体本轮派出去的任务清单。**前端拿到后应当对每个元素再调用一次本接口(`agent_type="special"`)来执行子智能体**。如果数组为空,说明 leader 觉得已有信息足够,不再派活,可以直接展示 leader 的最终文本回答。 - -**`agent_type == "special"`:** - -``` -data: {"status": "子智能体xxx完成yyy工作"} -``` - -代表子智能体完成本轮工作并已把交付内容广播给其他线程。 - -#### 副作用(重要) - -- **leader 调用**:自动把"总控智能体收到用户消息: …"广播到所有非自己的线程;派活后再把"总控智能体给 X 派活, 内容为: …"广播到所有非派活双方的线程 -- **special 调用**:完成后把"子智能体 X 完成 Y 工作, 交付内容为: …"广播到所有非自己的线程 - -这意味着每个线程的对话历史里会逐步累积出全局上下文,无需前端再做合并。 - -#### 错误 - -| 状态码 | 触发条件 | -|---|---| -| `400` | `agent_name` 缺失或未在 `d_agent_thread_id` 中 | -| `401` | 未携带 `Authorization` 头 | -| `502` | 上游 `runs/stream` 返回非 200 / SSE 解析失败 | - ---- - -## 3. 前端调用流程示例 - -```ts -// 1. 拿 token -const loginResp = await fetch("/api/v1/auth/login/username", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username: "yourname" }), -}); -const { access_token } = await loginResp.json(); -const authHeaders = { - Authorization: `Bearer ${access_token}`, - "Content-Type": "application/json", -}; - -// 2. 初始化 -const initResp = await fetch("/api/multi-agent/init", { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ - agent_names: [ - "taiwan-intelligence-gathering-agent", - "us-intelligence-gathering-agent", - ], - main_agent_name: "main-controller", - model: "deepseek-chat", - }), -}); -const { thread_ids } = await initResp.json(); - -// 3. 流式调用 leader -async function streamLeader(message: string, model = "deepseek-chat") { - const resp = await fetch("/api/multi-agent/run/stream", { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ - agent_type: "leader", - d_agent_thread_id: thread_ids, - agent_name: "main-controller", - new_message: message, - skill_stop_names: [], - model, - }), - }); - const reader = resp.body!.getReader(); - const decoder = new TextDecoder(); - let buf = ""; - let dispatched: [string, string][] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buf += decoder.decode(value, { stream: true }); - const lines = buf.split("\n"); - buf = lines.pop() ?? ""; - for (const line of lines) { - if (!line.startsWith("data:")) continue; - try { - const payload = JSON.parse(line.slice(5).trim()); - if (Array.isArray(payload.status)) { - dispatched = payload.status; - } - // 其余事件按 LangGraph SDK 协议自行渲染 - } catch { - /* 非 JSON 帧,可忽略或交给现有渲染器 */ - } - } - } - return dispatched; -} - -// 4. 对派活清单逐个调用 special -async function streamSpecial(agentName: string, task: string, model = "deepseek-chat") { - return fetch("/api/multi-agent/run/stream", { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ - agent_type: "special", - d_agent_thread_id: thread_ids, - agent_name: agentName, - new_message: task, - skill_stop_names: [], - model, - }), - }); - // 同样按 SSE 解析 -} - -// 5. 编排:先 leader 派活 → 依次执行 special → 再让 leader 汇总 -const dispatched = await streamLeader("请分析特朗普的支持率"); -for (const [agentName, task] of dispatched) { - await streamSpecial(agentName, task); -} -const next = await streamLeader("根据上面收集到的信息出最终结论"); -// 当 leader 返回 status: [] 时,意味着汇报完毕,可结束循环 -``` - ---- - -## 4. 注意事项 - -- **`model` 字段的取值**:必须是后端 `config.yaml` 中 `models[]` 已配置的模型名(如 `deepseek-chat`、`glm-4`、`gpt-4o` 等)。传错会导致上游 502 -- **`thread_ids` 必须每次回传**:后端不存储这个映射,是无状态接口 -- **协调智能体必须装有 `agent_orchestration` 技能**:`/init` 创建时会自动指定 `skills=["agent_orchestration"]`。后端 `skills/` 目录里必须存在该技能定义,否则 leader 无法派活 -- **同名协调智能体冲突**:`main_agent_name` 重名时后端不会报错("already exists" 走兼容分支),会复用既有智能体——所以同一个用户多次 init 用同一个名字是安全的,但 SOUL 不会被覆盖 -- **SSE 断连**:本接口底层 `on_disconnect: "continue"`,断连后上游 run 会继续跑到结束(不会取消) -- **并发广播**:广播消息使用 `asyncio.gather` 并发更新所有线程 state,性能上不会随子智能体数线性变慢 - ---- - -## 5. 相关接口(底层依赖) - -本接口内部转发以下 Gateway 原生接口,了解细节可查: - -| 接口 | 路由 | 用途 | -|---|---|---| -| 创建 thread | `POST /api/threads` | 为每个智能体建立独立会话 | -| 读 thread state | `GET /api/threads/{id}/state` | 广播前取当前消息列表 | -| 写 thread state | `POST /api/threads/{id}/state` | 把广播消息追加到 messages | -| 取 agent 详情 | `GET /api/agents/{id}` | init 时读取子智能体的 name/description | -| 建 agent | `POST /api/agents` | init 时创建协调智能体 | -| 流式 run | `POST /api/threads/{id}/runs/stream` | leader/special 的实际执行 | diff --git a/frontend-web/docs/multi-agent-background-run-遗留问题与下次继续.md b/frontend-web/docs/multi-agent-background-run-遗留问题与下次继续.md deleted file mode 100644 index 4bdb11f6..00000000 --- a/frontend-web/docs/multi-agent-background-run-遗留问题与下次继续.md +++ /dev/null @@ -1,125 +0,0 @@ -# 多智能体「后台挂起」—— 当前状态、遗留问题与下次继续指南 - -> 配套设计文档:`multi-agent-background-run-dev.md`(分 Phase 0-9 的总方案)。 -> 本文记录**截至本次会话的实际进展 + 还没解决的遗留问题**,方便下次直接续接。 - ---- - -## 一、整体状态:跑通到哪了 - -「后台挂起」= Step2 把研讨交给后端**进程内驱动真模型**(leader→各席位→共识→报告), -survive 刷新/关页,对话与报告作为一个 **run 写进草稿**(和手动跑同一存取),可在历史回看。 - -**真网关已在本地验证可跑通**(2026-06-04 日志确认:roundtable-coordinator + 各席位 + -roundtable-report 真调 deepseek、真搜索、跑到 report run success)。但还有几个**内容/产物层面的遗留问题**(见第三节)。 - -### 已完成(可用) -- ✅ 后端持久化:`roundtable_jobs` 表(迁移 `20260604_01` / `20260605_01` dialogues / `20260605_02` step3)。 -- ✅ 编排引擎 `packages/harness/deerflow/agents/roundtable_orchestrator/`(recommend + chain + 报告,纯逻辑可单测)。 -- ✅ 控制面 `app/gateway/routers/roundtable_jobs.py`:start/get/by-draft/stream(SSE)/resume/cancel/admin-cleanup。 -- ✅ 执行器 `app/gateway/roundtable_job_executor.py`:后台任务 + `set_current_user` + 生命周期 + **每轮把对话作为 `job-` run 写进 `draft.step2.runs[]`(同手动存取)** + done 写 step3。 -- ✅ 真网关 `app/gateway/roundtable_inprocess_gateway.py`:`SimpleNamespace(app)` + `start_run` 进程内驱动 agent;复用 `multi_agent._extract_dispatch` 解析派活;checkpointer 读最终 messages。 -- ✅ 模拟网关 `roundtable_orchestrator/simulated_gateway.py`(不调真模型,前端联调用)。 -- ✅ 前端:`api/roundtable-jobs.ts`、`lib/job-types.ts`、Step2「后台挂起」按钮、历史下拉 loading、`DispatchChainModal`/`DispatchChainJobModal`(流程图 + 对话面板 markdown + 加宽 + 澄清作答 + 取消 + 查看)。 -- ✅ 上下文「广播」(inline):席位/总控 prompt 带上「任务背景 + 前序各席位交付 + 别提问」约束(修「席位拿不到关键信息、反问用户、总控瞎编」)。 -- ✅ 后端测试:`tests/test_roundtable_jobs.py` / `test_roundtable_orchestrator.py` / `test_roundtable_job_executor.py` / `test_roundtable_jobs_api.py` 全过。 - ---- - -## 二、怎么启动 / 验证(每次开工先做) - -1. **开真网关**(默认是模拟网关):已写进 `offline-backend-20260512/.env`: - ``` - ROUNDTABLE_JOB_GATEWAY=inprocess - ``` - 启动日志应出现 `[roundtable-jobs] gateway = inprocess (real models)`。 - -2. **必须用 `make gateway`(不带 --reload)跑后端!** - `make dev` 的 `--reload` 监视 `backend/` 目录,agent 运行时往 `.deer-flow/` 写文件 - (memory.json / checkpoint / 沙箱输出)会触发热重载,**把长时后台任务杀掉**。 - (改进项:给 uvicorn 加 `--reload-exclude '.deer-flow/*'`,或始终用 `make gateway`。) - -3. **直接查库验证**(最有效,PowerShell 控制台是 GBK,导出到文件看中文): - ```bash - cd offline-backend-20260512/backend - # 查最新作业的 dialogues / step3 - PYTHONPATH=. uv run python -c "import sqlite3,json; con=sqlite3.connect('.deer-flow/data/deerflow.db'); ..." - ``` - - 作业表:`.deer-flow/data/deerflow.db` → 表 `roundtable_jobs`(`dialogues`/`step3` 列)。 - - 草稿表:`roundtable_drafts` → `step2` 列里应有 `runs[ {id:"job-", jobId, jobStatus, step2RoundtableDialogues:[...], step3:{...}} ]`。 - -4. **前端验证**:后台挂起 → 等跑完 → 从历史加载该草稿 → Step2 应显示后台研讨完整对话(走现有 `normalizeStep2Runs`/`hydrateStep2Run`)。 - ---- - -## 三、遗留问题(下次重点,已按优先级排) - -### P0 —— 报告 agent 不写 HTML 文件(结果绘制拿不到产物)✅ 已于 2026-06-12 修复并恢复 -- **根因(2026-06-12 定位)**:`dc8fca6`(2026-06-11 智能体可见性对齐)给 `services._enforce_agent_access` - 加 admin 判断时**裸访问 `request.state`** —— 后台进程内虚拟 request(`SimpleNamespace(app=app)`) - 没有 `state` 属性 → `AttributeError` 杀死整个编排(前端真 HTTP 请求有 auth 中间件写的 `state.user` - 所以手动路径一直正常)。修复:getattr 全链防御 + 虚拟 request 补空 `state`(含 scheduler 同款), - 回归测试 `tests/test_roundtable_inprocess_request.py`。 -- **已恢复**:`ROUNDTABLE_BACKGROUND_REPORT_ENABLED = True`。后台收口后自动跑 Step3 双产物: - ① `roundtable-summary` 写「方案总结报告.md」(新增 `gateway.run_summary` + `engine._run_step3`, - prompt 对齐 `useStep3Summary.buildSummaryPrompt`)→ ② `roundtable-report` 画「方案总览.html」。 - 两段故障隔离(一段失败保留另一段产物);step3 形状对齐前端 `DraftStep3Snapshot` - (`summaryReport` 嵌套),写草稿 `draft.step3` 与手动调用同款。测试 - `tests/test_roundtable_step3_pipeline.py`。 -- **待真机验证**:早期(06-05,回归引入前)曾观察到 deepseek-chat 在后台不调 `write_file`, - 当时根因可能就是更早版本的同类访问崩溃,也可能另有 prompt 层问题——恢复后跑一单真模型确认 - outputs 下两份产物都落盘;若报告仍不写 HTML,按旧排查清单对比前端 run 参数(run policy 现已 - 由 `roundtable_run_policy.py` 单源统一,差异面只剩 prompt/上下文)。 - -### P1 —— 内容质量(本次刚加 inline 广播,待真机验证) -- **已做**:席位/总控 prompt 注入「任务背景 + 前序交付 + 别提问」。 -- **待验证**:真机跑一次,看席位是否基于真实上下文产出(不再反问/凭记忆瞎编)。 -- **若 inline 不够**:考虑实现**真·thread 广播**(像 `multi_agent._append_thread_message` 那样把 - 「用户意图 / 派活 / 各席位交付」追加进各 thread 历史)。进程内要避开 HTTP loopback 鉴权, - 得用 checkpointer 直接 append human 消息(参考 `threads` 路由 POST `/state` 的内部逻辑)。这块较重,先看 inline 够不够。 - -### P2 —— 对话「没有步骤(steps)」 -- **现象**:手动跑的气泡带工具调用步骤卡(`onPhaseChange` → `appendStepIfNew` → `Step2Dialogue.steps`); - 后台对话只存了最终正文,没有 steps。 -- **下次怎么做**:后台 `_run_agent_turn` 跑完后,从 checkpoint 的 messages 里抽出 tool_calls, - 映射成 `Step2Dialogue.steps`(`{id,key,label,ts,args}`),在执行器 `_to_step2_dialogue` 里带上。 - 纯展示增强,不影响内容/报告。 - -### P3 —— 双跑浪费 + --reload 坑(工程健壮性) -- 进 Step2 时前端会自动跑一遍本地研讨(真模型、真线程);点后台挂起后前端那条 run 在后端还会跑完 - (`stopOrchestration` 只断客户端、停不了后端 run)→ 和后台作业短暂双跑、双倍 token。 - - 优化方向:进 Step2 改成「显式开始」而非自动跑;或后台挂起时真正 cancel 前端那条 run。 -- `make dev` 的 `--reload` 会杀后台任务(见第二节);改进项:`--reload-exclude '.deer-flow/*'`。 - ---- - -## 四、关键文件清单(下次定位用) - -**后端** -| 文件 | 作用 | -|------|------| -| `app/gateway/roundtable_inprocess_gateway.py` | 真网关:进程内驱动 leader/seat/re 编排循环 + `build_report_prompt` + inline 广播(`_seat_message`/`_deliveries_block`) | -| `.../roundtable_orchestrator/progress.py` | 进度落库 + `on_dialogue` 每轮回调 | -| `.../persistence/roundtable_jobs/` | job 表 model/sql | -| `app/gateway/deps.py` | `ROUNDTABLE_JOB_GATEWAY` 选网关 + 注入 executor(draft_store) | - -**前端** -| 文件 | 作用 | -|------|------| -| `src/roundtable-planning/api/roundtable-jobs.ts` | 作业 API + SSE | -| `src/roundtable-planning/lib/job-types.ts` | 类型(RoundtableJob/JobDialogue/...) | -| `src/roundtable-planning/components/DispatchChainModal.tsx` / `DispatchChainJobModal.tsx` | 流程图弹窗 + 对话面板 | -| `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 后台挂起按钮、历史下拉、**runs[] 多运行模型**(`hydrateStep2Run`/`getStep2SnapshotWithRuns`/`normalizeStep2Runs`) | -| `src/roundtable-planning/api/drafts.ts` | `DraftStep2RunSnapshot`(带 jobId/jobStatus/step3) | - ---- - -## 五、关键架构事实(避免重复踩坑) - -1. **真网关进程内驱动**:`start_run(body, thread_id, SimpleNamespace(app=app))` + `set_current_user`, - 不走 HTTP(loopback 调 `/api/multi-agent/*` 不可行:`_auth_headers` 只转发用户 token + owner check)。 -2. **后台作业首启自建全新线程**(`StartParams.is_resume`),绝不复用前端本地研讨的线程(会撞 409)。 -3. **存取统一**:后台作业 = `draft.step2.runs[]` 里一个带 jobId 的 run,和手动跑同一存取/hydrate 路径, - **不要再开 observe/回显分支**(之前试过,被否)。 -4. **聊天记录确实存上了**(job.dialogues + draft 的 run);之前"没存上"是测了旧代码 / 看的是前端本地残缺数据。 -5. 多 worker / SSE:状态进 DB、SSE 走 DB 轮询;真网关进程内无 loopback。 diff --git a/frontend-web/docs/xvqiu-0616.md b/frontend-web/docs/xvqiu-0616.md new file mode 100644 index 00000000..ed3544d1 --- /dev/null +++ b/frontend-web/docs/xvqiu-0616.md @@ -0,0 +1,161 @@ +# 需求文档 · 问答中心左侧栏改造(0616) + +> 整理自 2026-06-16 需求草稿。本批需求统一围绕「问答中心」菜单下 5 个页面入口的**左侧侧边栏**进行改造,目标是让每个业务页面的左栏从「通用导航」变为「贴合该业务场景的快捷入口 + 历史记录」。 +> +> 菜单结构参考:`frontend-web/src/core/page-layout/sidebar-menu.ts` 中 `qa-center`(问答中心)分组,含 5 个子项,正好对应下文 5 条需求。 + +## 0. 背景与共性约定 + +| 需求 | 菜单项 | id | 现有路由 | 对应页面/组件 | +|------|--------|----|----------|--------------| +| 1 | 通用问答 | `general-qa` | `/page/workspace/chats/new` | Workspace Chat + 左栏(`recent-chat-list.tsx` 等) | +| 2 | 多智能体会商 | `multi-agent-roundtable` | `/page/strategy/qa/multi-agent-roundtable` | `roundtable-planning/pages/RoundtablePlanningPage.tsx` | +| 3 | zzJC支持 | `qa-zzjc` | `/page/strategy/agents/6bf/chats/new` | `pages/AgentChatPage.tsx` | +| 4 | zzXD应用 | `qa-zzxd` | `/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new` | `pages/AgentChatPage.tsx` | +| 5 | 课题研究 | `qa-research` | `/page/canvas/ai-writing` | `open-canvas/pages/AIWritingPage.tsx` | + +**共性约定(适用于全部 5 条,除非单条另有说明):** + +- 「更多」一律为可点击入口;点击后跳转到对应的完整列表/管理页(具体目标见各条「待确认」)。 +- 列表为空时的兜底:参考各条「现状」中的级联/空态规则;无内容时该区块整体隐藏或显示占位文案(待 UI 设计确认,默认隐藏,与现有 `recent-chat-list.tsx` 一致)。 +- 左栏宽度、折叠交互沿用现有侧边栏体系(`SidebarGroup` 系列组件)。 +- 所有「智能体」相关数据复用 `core/agents`(`Agent` 类型已含 `is_pinned` / `is_favorite` / `featured_order`,scope 支持 `mine` / `square` / `builtin`)。 + +--- + +## 1. 通用问答 — 左侧栏改造 + +**页面**:`/page/workspace/chats/new` + +### 1.1 现状 +当前左栏主要展示「最近会话」列表(`recent-chat-list.tsx`,已过滤 `scheduler` / `bootstrap` / `roundtable` 类型线程,置顶会话优先)。 + +### 1.2 目标布局(自上而下) +左栏从上到下依次为三块:**新建回答按钮 → 常用智能体 → 聊天记录**。 + +#### ① 顶部:新建回答 +- 左栏最顶部放置「**新建回答**」按钮(主操作)。 +- 点击 = 开启一次全新的通用问答会话(等价于跳转 `/page/workspace/chats/new`)。 + +#### ② 常用智能体(最多 3 个 + 更多) +- 展示「常用智能体」卡片/列表项,**最多 3 个**。 +- 数据来源采用**级联兜底**逻辑(取到即用,不足再降级): + 1. 若用户有**置顶智能体**(`is_pinned = true`)→ 展示置顶的; + 2. 否则展示**用户自己的智能体**(`scope = "mine"`); + 3. 仍没有 → 展示**默认广场智能体**(`scope = "square"` / `builtin`,即广场默认推荐)。 +- 三者按上述优先级取,最终最多取 3 个。 +- 下方显示「**更多**」入口。 +- 点击某个常用智能体 → 进入该智能体的对话(待确认:是否为该智能体新建会话)。 + +#### ③ 聊天记录(最多 10 条 + 更多) +- 展示用户的问答聊天记录,**最多 10 条**(沿用现有 `useThreads` + 过滤规则)。 +- 下方显示「**更多**」入口。 +- 点击「更多」→ **跳转到问答记录页面**(完整聊天记录列表页)。 + +### 1.3 +- 「常用智能体」点击后「打开该智能体主页」 +- 「常用智能体」的「更多」跳转目标页 智能体管理 `/page/strategy/agents`。 +- 「聊天记录」的「问答记录页面」具体路由http://localhost:5173/#/page/workspace/chats。 +- 级联兜底是(置顶组够 3 个就完全不展示我的/广场)如果不够「补足到 3 个 + +--- + +## 2. 多智能体会商 — 左侧栏重做 + +**页面**:`/page/strategy/qa/multi-agent-roundtable`(`RoundtablePlanningPage`) + +### 2.1 现状 +圆桌会商页已有业务链选择、分析草稿等能力(`roundtable-planning/api/chains.ts`、`hooks/useRoundtableDrafts.ts`)。需求要求**左栏整体重做**。 + +### 2.2 目标布局(自上而下) + +#### ① 顶部:业务链(公共,前 3 个) +- 顶部展示**业务链**,仅取**公共**链条(`listChains("public")`,即所有已发布 `is_public = true` 的链条)。 +- 仅展示**前 3 个**。 +- 点击某条业务链 → 以该业务链启动一次会商(沿用现有"会商自动串联圆桌"流程,见记忆 `business-chain-mapping`)。 + +#### ② 下方:分析记录 +- 业务链下方展示「**分析记录**」列表。 +- 数据来源:圆桌分析草稿/历史(`useRoundtableDrafts` / `listDrafts`)。 +- 点击某条分析记录 → 打开对应的历史会商分析。 + +### 2.3 待确认 +- 「业务链」是否需要「更多」入口跳转到业务链条配置页(`/page/strategy/business-chains`)?需求未提及,默认仅展示前 3 个、无更多。 需要更多入口 +- 「分析记录」是否需要分页/「更多」?需求未提及,默认展示全部或合理上限 分析记录默认全部吧。 +- 公共业务链「前 3 个」的排序口径(创建时间倒序?已有 `listChains` 默认 newest first) 按照现在默认的显示来吧,然后要支持用户单选,如果用户选择后,点击进入多智能体研讨按钮时,直接按照选择的业务链进去执行,如果用户取消勾选,或没有勾选就还是打开弹窗。 + +--- + +## 3. zzJC支持 — 左侧栏改造(按类型聚合智能体) + +**页面**:`/page/strategy/agents/6bf/chats/new`(当前为单一智能体 `6bf` 的对话页) + +### 3.1 现状 +当前 zzJC支持直接进入某个固定智能体(id=`6bf`)的对话页(`AgentChatPage`)。 + +### 3.2 目标布局 +- **左栏按「类型」分组**,展示该类型下的智能体列表("类型"待确认,推测为智能体 `tags` / 分类)。 +- 智能体分组**下方**展示**聊天记录**。 +- 左栏**宽度与通用问答(需求 1)保持一致**("左侧是问答宽")。 + +### 3.3 Admin 设置入口 +- 在 **admin 账号权限下**,页面**右侧顶部显示「设置」图标按钮**(齿轮图标)。 +- 点击设置按钮 → 打开设置面板,可对该模块进行配置。 +- 非 admin 账号不显示设置按钮(沿用 `sidebar-menu.ts` 中 `adminOnly` 同款角色判断)。 + +### 3.4 待确认 +- 「类型」的确切含义:是智能体的 `tags`,还是 zzJC 业务自定义的一组分类? +- 现 zzJC 入口是单个智能体 `6bf`;改为「按类型展示多个智能体」后,**进入该菜单时默认落地页是什么**(智能体列表?还是仍默认进 `6bf` 对话)?是否需要新建一个 zzJC 聚合页。 +- Admin「设置」具体可配置项:配置「该模块纳入哪些类型/哪些智能体」?还是其他(如默认智能体、欢迎语)?需明确设置项清单。 +- 「聊天记录」过滤范围:仅 zzJC 相关会话,还是全部会话? + +--- + +## 4. zzXD应用 — 左侧栏改造 + +**页面**:`/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new` + +### 4.1 需求 +**与需求 3(zzJC支持)完全一致**: +- 左栏按类型展示该类型下的智能体 + 下方聊天记录; +- 左栏宽度同通用问答; +- admin 账号下右侧顶部显示设置图标按钮,可设置。 + +### 4.2 待确认 +- 同需求 3 全部待确认项。 +- zzXD 与 zzJC 是否共用同一套「按类型 + 设置」组件(仅数据源/默认智能体不同),还是各自独立配置?建议**抽象为同一可复用组件**,通过参数区分。 + +--- + +## 5. 课题研究 — 左侧栏改造(研究模板 + 笔记本) + +**页面**:`/page/canvas/ai-writing`(`AIWritingPage`) + +### 5.1 目标布局(自上而下) + +#### ① 上半部分:研究模板 +- 左栏上半部分展示「**研究模板**」。 +- 研究模板 = **原来的「文章类型」**(`open-canvas/hooks/useArticleTypes.ts` 的 `articleTypes`,即 AI 写作表单里的「文章类型」选择项)。 +- 点击某个研究模板 → 以该模板/文章类型发起课题研究(沿用现有 `articleType` 设置逻辑)。 + +#### ② 下半部分:笔记本 +- 左栏下半部分展示「**笔记本**」。 +- 笔记本内容 = **「简洁模式」左侧菜单下方展示的笔记本内容**(`useStudioNotebooks`,即 `workspace-nav-studio.tsx` 中渲染的 studio notebooks 列表)。 +- 点击某个笔记本 → 打开该笔记本(沿用现有 `/page/workspace/studio/notebooks/{id}` 行为,或作为课题研究的素材来源 `materialSource = 'notebook'`,待确认)。 + +> 备注:草稿原文写作「简介模式」,结合代码应为「**简洁模式**」(`appearance-settings-page.tsx` 中的全局界面模式:全量 / 简洁;简洁模式下笔记本在左栏菜单展示)。 + +### 5.2 待确认 +- 「研究模板」点击行为:仅预填表单 `articleType`,还是直接开始生成? +- 「笔记本」点击行为:跳转笔记本详情页,还是将笔记本设为当前课题研究的素材来源(`materialSource='notebook'`)? +- 上/下两部分是否各自可折叠、是否需要「更多」入口与数量上限?需求未提及。 + +--- + +## 6. 整体待确认与建议 + +1. **复用性**:需求 3 / 4(zzJC / zzXD)布局完全相同,建议实现为同一套「按类型分组 + 聊天记录 + admin 设置」可复用组件,通过配置区分模块。 +2. **左栏宽度统一**:需求 1 / 3 / 4 都要求"问答宽",建议统一左栏宽度常量,避免各页面各写一套。 +3. **「更多」目标页**:需求 1 涉及「智能体更多」「聊天记录更多→问答记录页」两个跳转目标需先确定路由是否已存在。 +4. **Admin 设置项清单**:需求 3 / 4 的设置面板具体能配置什么,需要产品给出字段清单后才能落地。 +5. **入口语义变更**:需求 3 / 4 把原「单一智能体对话」入口改为「按类型聚合」,菜单 `path` 与默认落地页可能需要调整,需同步确认。 diff --git a/frontend-web/src/components/workspace/frequent-agents-list.tsx b/frontend-web/src/components/workspace/frequent-agents-list.tsx new file mode 100644 index 00000000..e84213aa --- /dev/null +++ b/frontend-web/src/components/workspace/frequent-agents-list.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { Bot, ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useMemo } from "react"; + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { useAgentsPaginated } from "@/core/agents"; +import type { Agent } from "@/core/agents"; +import { desensitize } from "@/lib/desensitize"; + +/** 常用智能体最多展示个数(需求 1.2 ②)。 */ +const MAX_FREQUENT = 3; + +/** 「更多」跳转目标:智能体管理页(需求 1.3)。 */ +const MORE_HREF = "/page/strategy/agents"; + +/** 打开某个智能体主页(其对话落地页,thread_id=new = 新会话)。 */ +function agentHomeHref(agentId: string): string { + return `/page/strategy/agents/${agentId}/chats/new`; +} + +/** + * 「常用智能体」侧栏区块(通用问答左栏,全量模式)。 + * + * 级联兜底取数(需求 1.2 ② / 1.3)——按优先级补足到最多 3 个、去重: + * 1. 置顶智能体(``is_pinned``,可来自「我的」或「广场」); + * 2. 不足 3 个 → 用「我的」智能体(``scope="mine"``)补足; + * 3. 仍不足 → 用「广场」默认推荐(``scope="square"``)补足。 + * + * 取不到任何智能体时整块隐藏(与 ``RecentChatList`` 的空态约定一致)。 + */ +export function FrequentAgentsList() { + const pathname = usePathname(); + const { agents: mine } = useAgentsPaginated({ + scope: "mine", + page: 1, + pageSize: 20, + }); + const { agents: square } = useAgentsPaginated({ + scope: "square", + page: 1, + pageSize: 20, + }); + + const frequent = useMemo(() => { + const out: Agent[] = []; + const seen = new Set(); + const add = (agent: Agent | undefined) => { + if (!agent || out.length >= MAX_FREQUENT || seen.has(agent.id)) return; + seen.add(agent.id); + out.push(agent); + }; + // 1. 置顶组优先(够 3 个则下面我的/广场都不再补)。 + [...mine, ...square].filter((a) => a.is_pinned).forEach(add); + // 2. 我的智能体补足。 + mine.forEach(add); + // 3. 广场默认推荐补足。 + square.forEach(add); + return out; + }, [mine, square]); + + if (frequent.length === 0) { + return null; + } + + return ( + + + 常用智能体 + + + +
+ {frequent.map((agent) => { + const href = agentHomeHref(agent.id); + const isActive = pathname === href; + return ( + + + + + {desensitize(agent.name)} + + + + ); + })} + + + + 更多 + + + + +
+
+
+
+ ); +} diff --git a/frontend-web/src/components/workspace/recent-chat-list.tsx b/frontend-web/src/components/workspace/recent-chat-list.tsx index 457c8806..702bcaa7 100644 --- a/frontend-web/src/components/workspace/recent-chat-list.tsx +++ b/frontend-web/src/components/workspace/recent-chat-list.tsx @@ -2,6 +2,7 @@ import { ChevronDown, + ChevronRight, Download, FileJson, FileText, @@ -74,7 +75,17 @@ import { desensitize } from "@/lib/desensitize"; import { isIMEComposing } from "@/lib/ime"; import { cn } from "@/lib/utils"; -export function RecentChatList({ compact = false }: { compact?: boolean }) { +export function RecentChatList({ + compact = false, + limit, + moreHref, +}: { + compact?: boolean; + /** 最多展示的会话条数;省略则不限制(需求 1.3:通用问答最多 10 条)。 */ + limit?: number; + /** 「更多」入口跳转目标;省略则不展示「更多」行。 */ + moreHref?: string; +}) { const { t } = useI18n(); const router = useRouter(); const pathname = usePathname(); @@ -190,6 +201,8 @@ export function RecentChatList({ compact = false }: { compact?: boolean }) { if (visibleThreads.length === 0) { return null; } + const shownThreads = + limit != null ? visibleThreads.slice(0, limit) : visibleThreads; return ( <> @@ -225,7 +238,7 @@ export function RecentChatList({ compact = false }: { compact?: boolean }) { >
- {visibleThreads.map((thread) => { + {shownThreads.map((thread) => { const isActive = pathOfThread(thread) === pathname; const pinned = isThreadPinned(thread); const favorited = isThreadFavorite(thread); @@ -368,6 +381,22 @@ export function RecentChatList({ compact = false }: { compact?: boolean }) { ); })} + {moreHref && ( + + + + {t.common.more} + + + + + )}
diff --git a/frontend-web/src/components/workspace/workspace-sidebar.tsx b/frontend-web/src/components/workspace/workspace-sidebar.tsx index a721a9c9..8712f9d0 100644 --- a/frontend-web/src/components/workspace/workspace-sidebar.tsx +++ b/frontend-web/src/components/workspace/workspace-sidebar.tsx @@ -10,7 +10,9 @@ import { } from "@/components/ui/sidebar"; import { useTheme } from "@/contexts/ThemeContext"; import { cn } from "@/lib/utils"; +import { usePathname } from "next/navigation"; +import { FrequentAgentsList } from "./frequent-agents-list"; import { RecentChatList } from "./recent-chat-list"; import { RecentRoundtableList } from "./recent-roundtable-list"; import { RecentWritingList } from "./recent-writing-list"; @@ -25,7 +27,11 @@ export function WorkspaceSidebar({ }: React.ComponentProps & { compactChrome?: boolean }) { const { open: isSidebarOpen } = useSidebar(); const { theme } = useTheme(); + const pathname = usePathname(); const useWhiteSidebar = compactChrome && theme === "light"; + // 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。 + // 仅在通用问答(/page/workspace/chats*)下启用三块式布局;其余 workspace 页保持原样。 + const isGeneralQa = pathname.startsWith("/page/workspace/chats"); // 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。 // collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对 @@ -53,7 +59,12 @@ export function WorkspaceSidebar({ > - + {isGeneralQa && } + diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index 4f1c6569..f2971f53 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -14,7 +14,7 @@ models: display_name: DeepSeek Chat use: langchain_openai:ChatOpenAI model: deepseek-chat - api_key: sk-3fee562f834a4c08bb0d471ef44cb445 + api_key: sk-35345154983047aba9e12faa86c5f62c base_url: https://api.deepseek.com request_timeout: 600.0 max_retries: 2