diff --git a/.gitignore b/.gitignore index 22ab8329..846eca50 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ frontend-web/.vscode/ # Frontend local/runtime artifacts frontend-web/.env +frontend-web/.env.local +frontend-web/.env.*.local frontend-web/node_modules/ frontend-web/.pnpm-store/ frontend-web/tsconfig.tsbuildinfo @@ -24,8 +26,9 @@ frontend-web/src.zip frontend-web/src9.zip frontend-web/src-v1.zip frontend-web/src-*.rar - +frontend-web/dist # Frontend build artifacts +frontend-web/dist/ frontend-web/dist-scheduled-check/ # Offline backend bundles and local deployment artifacts @@ -76,6 +79,9 @@ skills/ hermes-agent-main/ .gitignore offline-backend-20260512/backend/tests/* +deploy +.history + .codegraph codegraph.mdc \ No newline at end of file diff --git a/frontend-web/.env.development b/frontend-web/.env.development new file mode 100644 index 00000000..0d15230d --- /dev/null +++ b/frontend-web/.env.development @@ -0,0 +1,12 @@ +# 本地开发(pnpm dev)— 与原先 .env 保持一致 +# 个人覆盖请新建 .env.development.local(已 gitignore,优先级更高) + +# Gateway / DeerFlow API +VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 +VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api + +# 轻应用「新建笔记」系列接口 +VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 + +# 可选: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 new file mode 100644 index 00000000..3f551f71 --- /dev/null +++ b/frontend-web/.env.development.local.example @@ -0,0 +1,7 @@ +# 复制为 .env.development.local,仅本机生效,不提交 Git +# 直连本机 Gateway(需后端 CORS_ORIGINS 包含 http://127.0.0.1:5173 等) + +VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 +VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api + +# VITE_NOTE_API_BASE_URL=http://127.0.0.1:8000 diff --git a/frontend-web/.env.example b/frontend-web/.env.example index 06579a79..30e82ad6 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -1,6 +1,30 @@ -# 复制为 .env 后按需修改(Vite 仅暴露以 VITE_ 开头的变量) -# 直连本机 Gateway 时使用;若浏览器报 CORS,请在网关环境设置 CORS_ORIGINS,包含前端源站,例如: -# CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173 +# 环境变量说明(Vite 仅暴露以 VITE_ 开头的变量) +# +# ┌─────────────────┬──────────────────────────────────────────────────┐ +# │ 命令 │ 生效文件 │ +# ├─────────────────┼──────────────────────────────────────────────────┤ +# │ pnpm dev │ .env.development + .env.development.local │ +# │ pnpm build │ .env.production + .env.production.local │ +# │ scripts/start-* │ 脚本注入的环境变量会覆盖上述文件(见下方说明) │ +# └─────────────────┴──────────────────────────────────────────────────┘ +# +# 推荐: +# - 日常本地开发:改 .env.development(已提交,团队默认 UAT 地址) +# - 本机连 127.0.0.1:8001:复制 .env.development.local.example → .env.development.local +# - 内网打包部署:改 .env.production 后再执行 pnpm build +# +# 若浏览器直连后端报 CORS,请在 Gateway 配置 CORS_ORIGINS,包含前端源站,例如: +# CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173,http://127.0.0.1:5174 -VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 -VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api +# --- 开发环境(见 .env.development)--- +# VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow +# VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api +# VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 + +# --- 生产打包(见 .env.production)--- +# 部署前修改 .env.production 中的地址,再执行 pnpm build + +# 可选 +# VITE_CANVAS_LANGGRAPH_BASE_URL= +# VITE_STATIC_WEBSITE_ONLY= +# VITE_GITHUB_OAUTH_TOKEN= diff --git a/frontend-web/.env.production b/frontend-web/.env.production new file mode 100644 index 00000000..1d13e37e --- /dev/null +++ b/frontend-web/.env.production @@ -0,0 +1,12 @@ +# 生产打包(pnpm build)— 部署前请改成实际内网/生产地址 +# 打包后变量会写入 dist,运行时不可再改 + +# 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 + +# 轻应用「新建笔记」系列接口 +VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api + +# 可选:Canvas 独立 LangGraph 地址 +# VITE_CANVAS_LANGGRAPH_BASE_URL= diff --git a/frontend-web/.env.production.local.example b/frontend-web/.env.production.local.example new file mode 100644 index 00000000..659844ac --- /dev/null +++ b/frontend-web/.env.production.local.example @@ -0,0 +1,5 @@ +# 复制为 .env.production.local,打包前临时覆盖生产地址(不提交 Git) + +# VITE_BACKEND_BASE_URL=https://your-intranet-host/deerflow +# VITE_LANGGRAPH_BASE_URL=https://your-intranet-host/deerflow/api +# VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api diff --git a/frontend-web/docs/ai-writing-assistant-backend-api.md b/frontend-web/docs/ai-writing-assistant-backend-api.md deleted file mode 100644 index 4f8e00c8..00000000 --- a/frontend-web/docs/ai-writing-assistant-backend-api.md +++ /dev/null @@ -1,726 +0,0 @@ -# AI 写作助手后端接口与 Prompt 协议文档 - -## 1. 文档目标 - -本文档用于指导 AI 写作助手第二阶段升级中的后端开发、接口设计、提示词设计和联调约束。 - -本轮后端目标不是重做 DeerFlow 的整体架构,而是在保留现有 Python + LangGraph + 线程体系的基础上,新增一套稳定的“选区改写型能力”,服务前端的 Tiptap 写作编辑器。 - ---- - -## 2. 本轮后端职责 - -后端负责: - -- 接收当前线程信息 -- 接收用户选区文本 -- 接收改写动作 -- 选取必要上下文 -- 调用模型生成候选改写结果 -- 返回纯净候选文本 - -后端本轮不负责: - -- 直接编辑 Tiptap JSON -- 直接操作前端编辑器选区 -- 直接替换前端文档内容 -- 做复杂富文本 patch -- 做 tracked changes - ---- - -## 3. 设计原则 - -### 3.1 纯文本协议优先 - -首版只让后端处理: - -- 选区文本 -- 前后文文本 -- 动作类型 -- 风格参数 - -不让后端直接理解完整富文本 JSON。 - -### 3.2 前端负责应用,后端负责生成 - -- 前端负责选区获取 -- 前端负责候选结果展示 -- 前端负责替换 / 插入 -- 后端只负责“给出候选文本” - -### 3.3 结果必须可直接落文档 - -后端输出必须尽量是可直接放回编辑器的正文,不应带解释性前缀,例如: - -- “当然可以,以下是润色版本:” -- “我帮你改写如下:” -- “下面是优化后的内容:” - -这类内容必须禁止。 - ---- - -## 4. 推荐接口设计 - -## 4.1 主接口 - -推荐新增统一接口: - -```text -POST /api/writing/rewrite -``` - -由 `action` 决定具体重写类型。 - -## 4.2 为什么推荐统一接口 - -因为前端交互是一套统一浮层和统一预览面板,拆成多个接口会导致: - -- 前端判断分支增多 -- 埋点与异常处理分散 -- 后续新增动作不方便扩展 - -统一接口更适合本轮需求。 - ---- - -## 5. 请求体定义 - -建议请求体如下: - -```json -{ - "threadId": "string", - "documentId": "string", - "selection": { - "from": 120, - "to": 260, - "text": "待改写文本" - }, - "action": "polish", - "tone": "professional", - "surroundingContext": { - "before": "选区前文", - "after": "选区后文" - } -} -``` - ---- - -## 6. 字段说明 - -### 6.1 `threadId` - -用途: - -- 关联当前 DeerFlow 线程 -- 必要时取线程上下文或会话元数据 - -### 6.2 `documentId` - -用途: - -- 标识当前右侧文档 -- 首版可选 -- 如果当前系统还没有独立文档 ID,可先允许为空 - -### 6.3 `selection.from` / `selection.to` - -用途: - -- 帮助前后端联调排查 -- 首版后端通常不需要真正使用偏移量 -- 建议保留,便于日志定位 - -### 6.4 `selection.text` - -用途: - -- 这是本轮最核心输入 -- 后端实际重写的主文本 - -### 6.5 `action` - -允许值: - -- `polish` -- `expand` -- `shorten` -- `tone` - -### 6.6 `tone` - -仅当 `action = tone` 时使用。 - -建议允许值: - -- `professional` -- `casual` -- `readable` -- `academic` -- `expressive` - -### 6.7 `surroundingContext.before` - -用途: - -- 提供选区前文 -- 帮助模型理解语气、主语、叙事连续性 - -建议: - -- 控制长度 -- 不要塞太多无关上下文 - -### 6.8 `surroundingContext.after` - -用途: - -- 提供选区后文 -- 帮助模型在扩写、改语气时与后续内容衔接 - ---- - -## 7. 返回体定义 - -建议返回体如下: - -```json -{ - "output": "AI 改写后的候选文本", - "originalSelection": { - "from": 120, - "to": 260, - "text": "待改写文本" - }, - "metadata": { - "action": "polish", - "tone": null, - "model": "your-model-name" - } -} -``` - ---- - -## 8. 返回字段要求 - -## 8.1 `output` - -必须: - -- 只包含候选正文 -- 尽量不夹带解释语 -- 能直接被前端替换或插入 - -## 8.2 `originalSelection` - -作用: - -- 前端可以做校验 -- 联调时方便确认是针对哪段文本返回的 - -## 8.3 `metadata` - -作用: - -- 便于埋点 -- 便于日志记录 -- 便于问题追踪 - ---- - -## 9. 错误响应规范 - -建议统一错误结构: - -```json -{ - "error": { - "code": "WRITING_REWRITE_FAILED", - "message": "rewrite failed" - } -} -``` - -建议错误码: - -- `WRITING_REWRITE_FAILED` -- `WRITING_INVALID_ACTION` -- `WRITING_INVALID_TONE` -- `WRITING_EMPTY_SELECTION` -- `WRITING_MODEL_UNAVAILABLE` -- `WRITING_CONTEXT_TOO_LARGE` - ---- - -## 10. 动作语义定义 - -## 10.1 `polish` - -目标: - -- 保持原意 -- 改善表达 -- 提升通顺度、自然度和可读性 - -不应: - -- 引入新事实 -- 改写成完全不同内容 - -## 10.2 `expand` - -目标: - -- 保持主旨 -- 扩充细节 -- 增强画面感、解释性或完整度 - -不应: - -- 无限制发散 -- 添加明显脱离主题的新内容 - -## 10.3 `shorten` - -目标: - -- 压缩篇幅 -- 保留关键信息 -- 减少重复表达 - -不应: - -- 丢失关键论点 -- 压缩到语义残缺 - -## 10.4 `tone` - -目标: - -- 不改变原意 -- 调整语言风格 - -不应: - -- 改写事实 -- 改写逻辑结构到完全不同 - ---- - -## 11. Prompt 设计原则 - -## 11.1 通用系统约束 - -所有改写类 prompt 都应包含以下约束: - -- 直接输出最终可用正文 -- 不输出标题,除非原文本身就是标题改写 -- 不输出解释 -- 不输出分析过程 -- 不输出“以下是结果”之类前缀 -- 保持与上下文语气一致 - -## 11.2 上下文使用原则 - -前后文只用于理解,不要求模型重写前后文。 - -明确告诉模型: - -- 只改写选中文本 -- 前后文仅用于参考衔接 - -## 11.3 长度控制原则 - -- `polish`:长度与原文接近 -- `expand`:长度可明显增加,但不要失控 -- `shorten`:长度明显缩短,但要保留关键信息 -- `tone`:长度尽量与原文接近 - ---- - -## 12. Prompt 模板建议 - -## 12.1 润色模板 - -```text -你是一名中文写作编辑助手。 - -请在不改变原意的前提下,对“选中文本”进行润色,使表达更自然、更流畅、更清晰。 - -要求: -1. 保留原文事实和核心意思。 -2. 不要添加原文没有的新事实。 -3. 不要输出解释,不要输出前缀,不要输出分析过程。 -4. 直接输出润色后的正文。 - -前文参考: -{before} - -选中文本: -{selection} - -后文参考: -{after} -``` - -## 12.2 扩写模板 - -```text -你是一名中文写作编辑助手。 - -请在不偏离主题的前提下,对“选中文本”进行扩写,增加必要细节、画面感或补充说明,使内容更充实。 - -要求: -1. 保持原文主旨不变。 -2. 增加内容时要自然,不要跑题。 -3. 不要输出解释,不要输出前缀,不要输出分析过程。 -4. 直接输出扩写后的正文。 - -前文参考: -{before} - -选中文本: -{selection} - -后文参考: -{after} -``` - -## 12.3 缩写模板 - -```text -你是一名中文写作编辑助手。 - -请在保留关键信息和原意的前提下,对“选中文本”进行缩写,使其更精炼。 - -要求: -1. 不要丢掉核心信息。 -2. 去掉重复、冗余和拖沓表达。 -3. 不要输出解释,不要输出前缀,不要输出分析过程。 -4. 直接输出缩写后的正文。 - -前文参考: -{before} - -选中文本: -{selection} - -后文参考: -{after} -``` - -## 12.4 改语气模板 - -```text -你是一名中文写作编辑助手。 - -请在不改变原意和事实的前提下,把“选中文本”改写成更{tone_label}的表达方式。 - -要求: -1. 保持原文信息不变。 -2. 只调整表达风格,不改变事实和逻辑。 -3. 不要输出解释,不要输出前缀,不要输出分析过程。 -4. 直接输出改写后的正文。 - -前文参考: -{before} - -选中文本: -{selection} - -后文参考: -{after} -``` - ---- - -## 13. Tone 映射建议 - -建议前端传标准值,后端映射中文标签: - -- `professional` -> 专业、正式 -- `casual` -> 口语化、轻松自然 -- `readable` -> 更易读、更清晰 -- `academic` -> 学术、严谨 -- `expressive` -> 更有文采、更有表现力 - ---- - -## 14. 上下文截取策略 - -## 14.1 推荐策略 - -前端传: - -- 选区前固定长度文本 -- 选区后固定长度文本 - -例如: - -- 前文 200 到 500 字 -- 后文 200 到 500 字 - -### 原因 - -- 足够帮助模型理解文气和衔接 -- 不会让请求太重 - -## 14.2 不建议 - -- 把整篇长文全量塞进去 -- 把整个线程消息都塞进改写接口 - -如果后端确实要参考线程上下文,也建议只取最近和写作相关的少量消息摘要。 - ---- - -## 15. LangGraph 接入建议 - -## 15.1 推荐接入方式 - -在现有 LangGraph / gateway 架构中新增一个写作改写专用入口。 - -建议二选一: - -### 方案 A - -新增专用 router: - -- `POST /api/writing/rewrite` - -内部调用现有模型层 / agent 层。 - -### 方案 B - -新增专用 graph node 或 tool: - -- `rewrite_selection` - -再由 router 调用该能力。 - -## 15.2 更推荐方案 - -推荐: - -- 对外暴露统一 router -- 内部走已有模型封装或 LangGraph 节点 - -这样前端接口更稳定,后续后端内部重构不会影响前端。 - ---- - -## 16. 后端模块拆解 - -## 16.1 模块 A:接口层 - -负责: - -- 请求校验 -- 错误响应 -- 参数标准化 - -需要完成: - -- 统一 rewrite API -- action / tone 校验 -- 空选区校验 - -## 16.2 模块 B:上下文拼装层 - -负责: - -- 拼装 before / selection / after -- 必要时提取线程相关信息 - -需要完成: - -- 上下文长度控制 -- 清洗空白和异常字符 - -## 16.3 模块 C:Prompt 层 - -负责: - -- 选择模板 -- 填充 action 和 tone - -需要完成: - -- 4 类动作模板 -- tone 映射 - -## 16.4 模块 D:模型调用层 - -负责: - -- 走现有模型配置体系 -- 执行大模型请求 - -需要完成: - -- 统一调用入口 -- 模型不可用错误处理 - -## 16.5 模块 E:结果清洗层 - -负责: - -- 去掉解释性前缀 -- 去掉多余包裹 -- 标准化返回 - -需要完成: - -- 输出清洗 -- metadata 填充 - ---- - -## 17. 返回结果清洗建议 - -即使 prompt 已经约束,模型仍可能输出: - -- “以下是润色后的内容:” -- “当然,这里是改写结果:” -- “优化如下:” - -建议后端加一层轻量清洗: - -- 去掉常见说明性前缀 -- 去掉包裹性 markdown 代码块 -- 去掉多余首尾空白 - -注意: - -- 清洗必须保守 -- 不要误删正文 - ---- - -## 18. 日志与观测建议 - -建议记录: - -- `threadId` -- `documentId` -- `action` -- `tone` -- `selection_length` -- `before_length` -- `after_length` -- `model_name` -- `latency_ms` -- `success / fail` - -不要直接把整段用户正文原文打到普通日志里,避免敏感信息风险。 - ---- - -## 19. 后端测试清单 - -## 19.1 单元测试 - -- action 校验 -- tone 校验 -- 空选区校验 -- prompt 选择逻辑 -- 输出清洗逻辑 - -## 19.2 集成测试 - -- `polish` -- `expand` -- `shorten` -- `tone-professional` -- `tone-casual` -- `tone-readable` -- `tone-academic` -- `tone-expressive` - -## 19.3 边界测试 - -- 选区过短 -- 选区过长 -- 前后文为空 -- 多段文本带换行 -- 模型超时 -- 模型不可用 - ---- - -## 20. 联调约束 - -## 20.1 前后端必须统一 - -- `action` 枚举值 -- `tone` 枚举值 -- 错误码 -- 返回体字段名 - -## 20.2 联调重点 - -- 多段落文本中换行保留是否正确 -- AI 返回内容是否适合直接落文档 -- 超时和失败时前端是否能优雅重试 - -## 20.3 联调建议顺序 - -1. 先 mock 返回固定文本 -2. 再接真实模型 -3. 先验证单段落 -4. 再验证多段落 -5. 最后验证大段文本 - ---- - -## 21. 优先级建议 - -### P0 - -- 统一 rewrite API -- 4 类动作支持 -- 输出纯净正文 -- 错误结构标准化 - -### P1 - -- 输出清洗增强 -- 埋点与日志 -- 上下文截断策略优化 - -### P2 - -- 更细粒度风格控制 -- 结果质量二次校验 - ---- - -## 22. 完成定义 - -后端满足以下条件即可视为本轮完成: - -- 前端能通过统一接口发起改写 -- 4 类动作均可稳定返回候选文本 -- 返回文本基本可直接用于替换或插入 -- 多段落文本能稳定处理 -- 错误结构规范统一 - ---- - -## 23. 结论 - -本轮后端最重要的,不是做复杂文档结构编辑,而是把“选区改写”做成稳定、可预测、可直接接入前端编辑器的能力。 - -最稳妥的路线是: - -- 统一接口 -- 统一动作枚举 -- 统一 prompt 模板 -- 统一错误结构 -- 统一输出约束 - -只要这层做稳,前端的 Tiptap 写作编辑器就能很顺畅地完成豆包式“选中即改、预览后应用”的核心体验。 diff --git a/frontend-web/docs/ai-writing-assistant-frontend-checklist.md b/frontend-web/docs/ai-writing-assistant-frontend-checklist.md deleted file mode 100644 index 3fd885ac..00000000 --- a/frontend-web/docs/ai-writing-assistant-frontend-checklist.md +++ /dev/null @@ -1,703 +0,0 @@ -# AI 写作助手前端开发任务清单 - -## 1. 文档目标 - -本文档用于把《AI 写作助手右侧编辑器升级需求文档》进一步拆成前端可直接执行的任务清单。 - -目标是让前端团队按模块并行推进,最终完成: - -- 保留左侧聊天区 -- 右侧升级为 `Tiptap` 写作编辑器 -- 支持选区浮层 -- 支持润色 / 扩写 / 缩写 / 改语气 -- 支持替换 / 插入 -- 支持标题 / 列表 / 引用 / 代码块工具条 -- UI 参考豆包 + Novel + open-canvas - ---- - -## 2. 本轮前端改造范围 - -### 2.1 保留不动 - -- 左侧聊天主链路 -- 现有线程消息流 -- 现有模型选择器 -- 现有输入框的 `writing_mode` 总入口 - -### 2.2 重点改造 - -- 右侧 Artifact 面板承载方式 -- 新增独立写作工作区 -- 富文本编辑器内核 -- 选区浮层 -- AI 改写结果预览 -- 文档级工具条 - ---- - -## 3. 推荐目录结构 - -建议新增独立模块目录: - -```text -frontend-web/src/open-canvas-writing/ - routes/ - WritingRoutes.tsx - pages/ - WritingWorkspacePage.tsx - components/ - WritingShell.tsx - WritingHeader.tsx - WritingToolbar.tsx - WritingEditorPane.tsx - WritingBubbleMenu.tsx - WritingAiMenu.tsx - WritingRewritePreview.tsx - WritingStatusBar.tsx - WritingEmptyState.tsx - editor/ - TiptapEditor.tsx - TiptapExtensions.ts - commands.ts - selection.ts - context/ - WritingEditorContext.tsx - hooks/ - useWritingEditor.ts - useWritingSelection.ts - useWritingRewrite.ts - useWritingToolbar.ts - api/ - rewrite.ts - documents.ts - types/ - document.ts - rewrite.ts - utils/ - apply-rewrite.ts - markdown.ts - editor-style.ts -``` - ---- - -## 4. 现有代码落点 - -## 4.1 需要接入的现有文件 - -- `frontend-web/src/pages/PageRoutes.tsx` - - 增加新写作路由入口 - -- `frontend-web/src/components/workspace/chats/chat-box.tsx` - - 当前双栏布局参考来源 - - 后续决定是否复用布局容器,或在新页面中重建双栏 - -- `frontend-web/src/components/workspace/input-box.tsx` - - 保留 `writing_mode` - - 后续可在切换写作模式时跳转到新写作页面 - -- `frontend-web/src/components/workspace/artifacts/context.tsx` - - 仅保留给旧 Artifact 逻辑 - - 不要继续承载写作编辑器状态 - -## 4.2 不建议继续沿用的方式 - -- 不要把编辑器主状态存成 `string[] artifacts` -- 不要把右侧主体验继续建在 `artifact-file-detail.tsx` 上 -- 不要让 AI 改写结果先回聊天,再由用户手动复制 - ---- - -## 5. 任务分阶段拆解 - -## 阶段 A:路由与页面壳 - -### 目标 - -先把新的写作工作区页面立起来,让后续编辑器与交互都有落点。 - -### 任务 - -- 新增 `WritingRoutes.tsx` -- 新增 `WritingWorkspacePage.tsx` -- 在 `PageRoutes.tsx` 中挂入 `page/writing/*` -- 确认新页面进入方式 -- 确定是: - - 通过左侧聊天上下文进入 - - 通过 `writing_mode` 入口进入 - - 或二者都支持 - -### 交付物 - -- 新写作页面能被路由访问 -- 页面有基础左右布局和空态 - -### 验收 - -- 打开 `page/writing` 能进入工作区 -- 页面不报错 - ---- - -## 阶段 B:编辑器底座 - -### 目标 - -把右侧正式替换成富文本编辑器。 - -### 任务 - -- 安装 Tiptap 依赖 -- 建立 `TiptapEditor.tsx` -- 建立 `TiptapExtensions.ts` -- 配置节点: - - paragraph - - heading - - bulletList - - orderedList - - listItem - - blockquote - - codeBlock - - hardBreak -- 配置 marks: - - bold - - italic - - underline - - strike - - code -- 建立编辑器初始化逻辑 -- 建立编辑器销毁逻辑 -- 定义最小文档状态 - -### 交付物 - -- 可编辑正文区域 -- 初始文档可加载 -- 内容可读可写 - -### 验收 - -- 可以直接输入正文 -- 可以换行、加标题、切换列表 -- 不出现内容错位和明显闪烁 - ---- - -## 阶段 C:主工具栏 - -### 目标 - -右侧具备正式写作工具栏。 - -### 任务 - -- 新增 `WritingToolbar.tsx` -- 新增 `useWritingToolbar.ts` -- 实现按钮: - - 正文 - - H1 - - H2 - - H3 - - 有序列表 - - 无序列表 - - 引用 - - 代码块 - - 加粗 - - 斜体 - - 下划线 - - 删除线 -- 处理 active 状态 -- 处理不可用状态 -- 处理光标块状态联动 - -### 交付物 - -- 主工具栏可用 - -### 验收 - -- 工具条按钮能正确反映当前块状态 -- 操作后编辑器内容正确变化 - ---- - -## 阶段 D:选区状态与浮层 - -### 目标 - -实现豆包式选区浮层基础能力。 - -### 任务 - -- 新增 `useWritingSelection.ts` -- 新增 `selection.ts` -- 新增 `WritingBubbleMenu.tsx` -- 基于 Tiptap 监听 selection 变化 -- 过滤掉空选区 -- 处理多段连续选区 -- 浮层按钮: - - AI 改写 - - 加粗 - - 斜体 - - 标题 - - 复制 -- 处理滚动定位 -- 处理窗口尺寸变化 - -### 交付物 - -- 选中内容后出现浮层 - -### 验收 - -- 单段可触发 -- 多段可触发 -- 浮层位置稳定 - ---- - -## 阶段 E:AI 改写菜单 - -### 目标 - -把选区浮层接到 AI 改写工作流。 - -### 任务 - -- 新增 `WritingAiMenu.tsx` -- 新增 `useWritingRewrite.ts` -- 新增 `api/rewrite.ts` -- 实现一级动作: - - 润色 - - 扩写 - - 缩写 - - 改语气 -- 实现改语气二级菜单: - - 更专业 - - 更口语化 - - 更易读 - - 更学术 - - 更有文采 -- 封装请求入参 -- 增加 loading 态 -- 增加 error 态 -- 增加 retry 入口 - -### 交付物 - -- AI 改写菜单和请求链路 - -### 验收 - -- 4 个动作都能发请求 -- 改语气的二级菜单可正常展开 -- 请求失败时有反馈 - ---- - -## 阶段 F:候选结果预览 - -### 目标 - -把 AI 结果变成“可预览、可决策、可回退”的候选内容。 - -### 任务 - -- 新增 `WritingRewritePreview.tsx` -- 新增 `apply-rewrite.ts` -- 显示原文与候选内容 -- 提供按钮: - - 替换 - - 插入 - - 重试 - - 取消 -- 用 transaction 应用结果 -- 处理应用成功后的焦点与滚动 - -### 交付物 - -- 改写结果预览卡片或面板 - -### 验收 - -- 替换准确 -- 插入准确 -- 取消后文档保持原样 - ---- - -## 阶段 G:文档状态与容器体验 - -### 目标 - -补齐正式产品的壳层体验。 - -### 任务 - -- 新增 `WritingShell.tsx` -- 新增 `WritingHeader.tsx` -- 新增 `WritingStatusBar.tsx` -- 新增 `WritingEmptyState.tsx` -- 补充文档标题区域 -- 补充文档状态提示 -- 补充空态和加载态 - -### 交付物 - -- 页面完整壳层 - -### 验收 - -- 页面不是裸编辑器 -- 有完整的文档工作区感 - ---- - -## 阶段 H:视觉打磨 - -### 目标 - -让整体观感接近成熟产品。 - -### 任务 - -- 调整右侧阅读宽度 -- 调整字体大小和行高 -- 调整段距和标题层级 -- 调整工具栏阴影、边框、圆角 -- 调整浮层动效 -- 调整 AI 预览卡片质感 -- 优化深浅色模式兼容 - -### 交付物 - -- 完整视觉优化 - -### 验收 - -- 明显优于当前版本 -- 有豆包 / Novel 风格的编辑器质感 - ---- - -## 6. 组件级开发清单 - -## 6.1 `WritingWorkspacePage.tsx` - -职责: - -- 页面级容器 -- 获取线程 / 文档上下文 -- 组织左右布局 - -必须完成: - -- 页面初始化 -- 与左侧聊天协同 -- 右侧编辑区挂载 - -## 6.2 `WritingShell.tsx` - -职责: - -- 写作工作区总壳层 -- 布局标题区、工具栏、正文区、状态区 - -必须完成: - -- 自适应布局 -- 桌面端优先 -- 窄屏降级 - -## 6.3 `TiptapEditor.tsx` - -职责: - -- 包装 Tiptap EditorContent -- 负责 editor 实例初始化 - -必须完成: - -- `onUpdate` -- `onSelectionUpdate` -- 内容同步 - -## 6.4 `WritingToolbar.tsx` - -职责: - -- 顶部主工具栏 - -必须完成: - -- 所有基础格式化命令 -- 当前状态高亮 - -## 6.5 `WritingBubbleMenu.tsx` - -职责: - -- 选区浮层 - -必须完成: - -- 跟随选区 -- 基础快捷操作 -- AI 改写入口 - -## 6.6 `WritingAiMenu.tsx` - -职责: - -- AI 改写动作菜单 - -必须完成: - -- 一级菜单 -- 改语气二级菜单 -- 动作触发 - -## 6.7 `WritingRewritePreview.tsx` - -职责: - -- 展示候选内容 - -必须完成: - -- 替换 -- 插入 -- 重试 -- 取消 - ---- - -## 7. Hook 级开发清单 - -## 7.1 `useWritingEditor.ts` - -负责: - -- editor 生命周期 -- 初始内容装载 -- 内容变更同步 - -## 7.2 `useWritingSelection.ts` - -负责: - -- 当前选区抽取 -- 选区文本缓存 -- 多段落纯文本拼接 - -## 7.3 `useWritingRewrite.ts` - -负责: - -- 改写请求 -- loading / success / error -- 结果缓存 -- 重试 - -## 7.4 `useWritingToolbar.ts` - -负责: - -- toolbar active 状态 -- editor command 映射 - ---- - -## 8. API 对接清单 - -前端需向后端约定以下接口能力: - -- `POST /api/writing/rewrite` - -前端请求内容至少包括: - -- `threadId` -- `documentId` -- `selection.from` -- `selection.to` -- `selection.text` -- `action` -- `tone` -- `surroundingContext.before` -- `surroundingContext.after` - -前端需处理的返回字段: - -- `output` -- `originalSelection` -- `metadata` - ---- - -## 9. 状态管理建议 - -建议新增独立 Context: - -- `WritingEditorContext.tsx` - -建议持有: - -- 当前文档内容 -- 当前 editor 实例 -- 当前选区 -- 当前改写请求状态 -- 当前候选结果 -- 当前 UI 面板状态 - -不建议继续复用旧 Artifact Context 的原因: - -- 旧 Context 关注文件列表和文件路径 -- 新编辑器关注选区、结构、候选结果、应用动作 -- 关注点完全不同 - ---- - -## 10. 前端联调检查点 - -## 10.1 与后端联调前 - -- 先用 mock 数据打通浮层和预览 -- 先验证替换 / 插入逻辑 - -## 10.2 与后端联调时 - -- 检查多段落选区拼接结果 -- 检查 AI 返回内容是否带说明性前缀 -- 检查替换后位置是否准确 - -## 10.3 联调后 - -- 验证大段文本性能 -- 验证撤销 / 重做 -- 验证快速连续触发是否会状态错乱 - ---- - -## 11. 前端测试清单 - -## 11.1 功能测试 - -- 输入正文 -- 插入标题 -- 切换列表 -- 插入引用 -- 插入代码块 -- 加粗 / 斜体 / 下划线 / 删除线 -- 选区浮层显示 -- 选区浮层隐藏 -- AI 改写菜单打开 -- 改语气二级菜单打开 -- 改写结果替换 -- 改写结果插入 -- 改写结果取消 - -## 11.2 场景测试 - -- 单段短文本润色 -- 多段文本润色 -- 多段文本扩写 -- 多段文本缩写 -- 多段文本改语气 -- 连续两次改写同一段 -- 快速切换线程后重新进入 - -## 11.3 回归测试 - -- 左侧聊天是否正常 -- 模型切换是否正常 -- `writing_mode` 是否正常 -- 原有 artifact 模块是否仍可使用 - ---- - -## 12. 任务优先级 - -### P0 - -- 路由与页面壳 -- Tiptap 接入 -- 主工具栏 -- 选区浮层 -- 改写菜单 -- 替换 / 插入 - -### P1 - -- 页面头部与状态 -- 优雅 loading / error / retry -- 多段落稳定性 -- UI 打磨 - -### P2 - -- 空态优化 -- 动效优化 -- 快捷键支持 - ---- - -## 13. 建议分工 - -### 前端 1 号 - -负责: - -- 路由与页面壳 -- Tiptap 接入 -- 主工具栏 - -### 前端 2 号 - -负责: - -- 选区状态 -- BubbleMenu -- AI 菜单 - -### 前端 3 号 - -负责: - -- 候选结果预览 -- 替换 / 插入 -- 状态栏与页面体验 - -### 联调负责人 - -负责: - -- 前后端请求协议 -- 错误态规范 -- 多段落测试和回归 - ---- - -## 14. 前端完成定义 - -满足以下条件可视为前端完成: - -- 右侧能作为正式编辑器独立使用 -- 选区后可直接发起 AI 改写 -- 改写后可替换或插入 -- 视觉上明显优于当前版本 -- 左侧聊天和现有系统不被破坏 - ---- - -## 15. 下一步建议 - -前端团队可以按以下顺序开工: - -1. 先起页面和路由 -2. 再接 Tiptap 与主工具栏 -3. 再做选区浮层 -4. 再接 AI 菜单和结果预览 -5. 最后做视觉打磨 - -这个顺序风险最小,也最容易在中途验收。 diff --git a/frontend-web/docs/ai-writing-assistant-source-map.md b/frontend-web/docs/ai-writing-assistant-source-map.md deleted file mode 100644 index 0dbbd7f6..00000000 --- a/frontend-web/docs/ai-writing-assistant-source-map.md +++ /dev/null @@ -1,457 +0,0 @@ -# AI 写作助手源码与实现映射表 - -## 1. 文档目标 - -本文档用于把以下几类信息对应起来: - -- 豆包 AI 写作助手里要借鉴的交互 -- Novel / open-canvas / Tiptap 中可参考的实现思路 -- DeerFlow 当前项目里已经存在的落点 -- 本次新建模块应该落到哪里 - -这份文档不是源码复制文档,而是开发时的“对照表”和“导航图”。 - ---- - -## 2. 使用方式 - -开发时建议按以下顺序看: - -1. 先看“目标能力” -2. 再看“参考来源” -3. 再看“DeerFlow 当前落点” -4. 最后看“本次目标文件” - -这样可以避免出现两种常见问题: - -- 直接照搬外部项目结构,结果和你们现有架构冲突 -- 只看自己项目,不知道成熟产品交互应该怎么落 - ---- - -## 3. 总体映射 - -| 目标能力 | 产品参考 | 外部参考实现 | DeerFlow 当前落点 | 本次目标文件 | -| --- | --- | --- | --- | --- | -| 写作工作区路由 | open-canvas 独立页面 | open-canvas 路由组织思路 | `frontend-web/src/pages/PageRoutes.tsx` | `frontend-web/src/open-canvas-writing/routes/WritingRoutes.tsx` | -| 左右双栏工作区 | 豆包写作页 / open-canvas | open-canvas 页面壳思路 | `frontend-web/src/components/workspace/chats/chat-box.tsx` | `frontend-web/src/open-canvas-writing/pages/WritingWorkspacePage.tsx`、`WritingShell.tsx` | -| 富文本正文编辑 | Novel | Tiptap 官方编辑器模式 | 当前无正式富文本正文编辑器 | `frontend-web/src/open-canvas-writing/editor/TiptapEditor.tsx` | -| 文档工具栏 | 豆包 / Novel | Tiptap Toolbar 组合思路 | 当前右侧无正式工具栏 | `frontend-web/src/open-canvas-writing/components/WritingToolbar.tsx` | -| 选区浮层 | 豆包 | Tiptap BubbleMenu | 当前右侧无选区浮层 | `frontend-web/src/open-canvas-writing/components/WritingBubbleMenu.tsx` | -| AI 改写菜单 | 豆包 | 自定义菜单 + BubbleMenu 挂接 | 当前无 | `frontend-web/src/open-canvas-writing/components/WritingAiMenu.tsx` | -| 候选结果预览 | 豆包 / open-canvas | open-canvas 候选应用思路 | 当前无 | `frontend-web/src/open-canvas-writing/components/WritingRewritePreview.tsx` | -| 替换 / 插入 | 豆包 | Tiptap transaction 应用 | 当前无 | `frontend-web/src/open-canvas-writing/utils/apply-rewrite.ts` | -| 写作模式总入口 | 当前 DeerFlow | 现有 `writing_mode` 开关 | `frontend-web/src/components/workspace/input-box.tsx` | 保留现有入口并接新页面 | -| 线程与模型协同 | 当前 DeerFlow | 现有线程 / 模型体系 | 现有 `core/threads`、`core/models` | 继续复用,新增 `api/rewrite.ts` | - ---- - -## 4. 豆包功能到 DeerFlow 的映射 - -## 4.1 滑词 / 选区浮层 - -### 豆包里的体验 - -- 选中文本后出现轻量浮层 -- 浮层里有: - - AI 改写 - - 文本样式 - - 复制 - -### DeerFlow 当前状态 - -- 右侧还没有正式的选区级工具层 - -### 本次实现方式 - -- 使用 Tiptap 的 `BubbleMenu` -- 再叠加自定义菜单逻辑 - -### 本次落点 - -- `frontend-web/src/open-canvas-writing/components/WritingBubbleMenu.tsx` -- `frontend-web/src/open-canvas-writing/hooks/useWritingSelection.ts` -- `frontend-web/src/open-canvas-writing/editor/selection.ts` - ---- - -## 4.2 多段落改写 - -### 豆包里的体验 - -- 不是只改单句 -- 能对连续几段一起润色、扩写、缩写、改语气 - -### DeerFlow 当前状态 - -- 现有右侧编辑区没有正式的多段落选区改写模型 - -### 本次实现方式 - -- 前端提取连续选区纯文本 -- 前端附带前后文 -- 后端返回候选纯文本 -- 前端决定替换或插入 - -### 本次落点 - -- `frontend-web/src/open-canvas-writing/hooks/useWritingSelection.ts` -- `frontend-web/src/open-canvas-writing/hooks/useWritingRewrite.ts` -- `frontend-web/src/open-canvas-writing/api/rewrite.ts` - ---- - -## 4.3 替换 / 插入 - -### 豆包里的体验 - -- 改写结果不是直接覆盖原文 -- 用户可选择: - - 替换 - - 插入 - -### DeerFlow 当前状态 - -- 当前没有候选文本应用层 - -### 本次实现方式 - -- 建立候选结果预览区 -- 基于 Tiptap transaction 应用文本 - -### 本次落点 - -- `frontend-web/src/open-canvas-writing/components/WritingRewritePreview.tsx` -- `frontend-web/src/open-canvas-writing/utils/apply-rewrite.ts` - ---- - -## 4.4 标题 / 列表 / 引用 / 代码块工具条 - -### 豆包里的体验 - -- 右侧本身就是正式文档编辑器 -- 可以快速切换文档结构 - -### DeerFlow 当前状态 - -- 当前右侧偏预览,不是正式写作编辑器 - -### 本次实现方式 - -- 使用 Tiptap 节点和命令 -- 在主工具栏中集中承载 - -### 本次落点 - -- `frontend-web/src/open-canvas-writing/components/WritingToolbar.tsx` -- `frontend-web/src/open-canvas-writing/hooks/useWritingToolbar.ts` -- `frontend-web/src/open-canvas-writing/editor/commands.ts` -- `frontend-web/src/open-canvas-writing/editor/TiptapExtensions.ts` - ---- - -## 5. Novel 参考映射 - -## 5.1 最值得借鉴的不是“代码整搬”,而是这三件事 - -- 编辑器主容器的产品感 -- 工具栏和空态的组织方式 -- 正文阅读宽度、排版、留白 - -## 5.2 对应到 DeerFlow 的实现位置 - -- 页面壳:`WritingShell.tsx` -- 顶部文档区:`WritingHeader.tsx` -- 编辑容器:`TiptapEditor.tsx` -- 工具栏:`WritingToolbar.tsx` -- 空态:`WritingEmptyState.tsx` - -## 5.3 不建议直接照搬的部分 - -- Next.js 页面结构 -- 依赖其服务端 AI 调用方式 -- 与 Vercel AI SDK 深绑定的部分 - ---- - -## 6. open-canvas 参考映射 - -## 6.1 主要借鉴对象 - -open-canvas 这次最值得借鉴的不是页面外观,而是: - -- AI 工作流组织方式 -- 改写结果应用节奏 -- 编辑器和聊天协作的模式 - -## 6.2 借鉴重点 - -### 借鉴 1:独立工作区思想 - -不要把写作体验塞进一个零散的弹窗或文件详情页,要有独立工作区。 - -对应 DeerFlow: - -- `frontend-web/src/open-canvas-writing/pages/WritingWorkspacePage.tsx` - -### 借鉴 2:画布状态独立维护 - -不要让写作状态完全依附聊天消息渲染结果,要独立维护文档和候选状态。 - -对应 DeerFlow: - -- `frontend-web/src/open-canvas-writing/context/WritingEditorContext.tsx` - -### 借鉴 3:AI 输出先预览,再应用 - -对应 DeerFlow: - -- `frontend-web/src/open-canvas-writing/components/WritingRewritePreview.tsx` - ---- - -## 7. Tiptap 参考映射 - -## 7.1 Tiptap 在本项目中的职责 - -- 作为右侧正式富文本编辑器内核 -- 提供结构节点与 marks -- 提供 BubbleMenu 能力 -- 提供 transaction 应用能力 - -## 7.2 建议直接参考的实现点 - -- `EditorContent` 的封装模式 -- `BubbleMenu` 的接入方式 -- heading / list / blockquote / codeBlock 扩展配置 -- `editor.chain().focus()` 命令调用模式 -- selection 变化监听 - -## 7.3 DeerFlow 本次落点 - -- `frontend-web/src/open-canvas-writing/editor/TiptapEditor.tsx` -- `frontend-web/src/open-canvas-writing/editor/TiptapExtensions.ts` -- `frontend-web/src/open-canvas-writing/editor/commands.ts` -- `frontend-web/src/open-canvas-writing/components/WritingBubbleMenu.tsx` - ---- - -## 8. DeerFlow 现有文件到新模块的映射 - -## 8.1 `frontend-web/src/pages/PageRoutes.tsx` - -### 当前职责 - -- 汇总页面路由 - -### 本次作用 - -- 挂入新的写作助手路由 - -### 后续动作 - -- 引入 `WritingRoutes` -- 增加 `page/writing/*` - ---- - -## 8.2 `frontend-web/src/components/workspace/chats/chat-box.tsx` - -### 当前职责 - -- 左聊天 + 右 Artifact 双栏布局 - -### 本次作用 - -- 提供双栏工作区组织思路 -- 可复用布局比例和 panel 交互经验 - -### 不建议直接复用的部分 - -- 不要继续让右侧核心能力挂在 ArtifactFileDetail 上 - -### 本次目标替代 - -- `WritingWorkspacePage.tsx` -- `WritingShell.tsx` - ---- - -## 8.3 `frontend-web/src/components/workspace/artifacts/context.tsx` - -### 当前职责 - -- 管理文件型 artifact 列表、选中项和开关状态 - -### 本次作用 - -- 仅作为旧 Artifact 模块保留 - -### 本次目标替代 - -- `WritingEditorContext.tsx` - -### 原因 - -- 写作编辑器需要的不是文件路径状态 -- 而是文档状态、选区状态、候选结果状态、工具层状态 - ---- - -## 8.4 `frontend-web/src/components/workspace/input-box.tsx` - -### 当前职责 - -- 聊天输入区 -- 已有 `writing_mode` 入口 - -### 本次作用 - -- 保留为用户进入写作模式的现有入口 - -### 本次改造建议 - -- 写作模式打开时,跳转或唤起新的写作工作区 -- 不再只停留在“生成 Markdown 文件”这个层面 - ---- - -## 9. 新模块间的依赖关系 - -## 9.1 页面层 - -- `WritingRoutes.tsx` -- `WritingWorkspacePage.tsx` -- `WritingShell.tsx` - -职责: - -- 页面路由 -- 双栏或工作区布局 -- 上下文装配 - -## 9.2 编辑器层 - -- `TiptapEditor.tsx` -- `TiptapExtensions.ts` -- `commands.ts` -- `selection.ts` - -职责: - -- 编辑器内核 -- 节点和 marks -- 基础命令 -- 选区抽取 - -## 9.3 交互层 - -- `WritingToolbar.tsx` -- `WritingBubbleMenu.tsx` -- `WritingAiMenu.tsx` -- `WritingRewritePreview.tsx` - -职责: - -- 工具栏 -- 浮层 -- AI 菜单 -- 候选内容应用 - -## 9.4 状态层 - -- `WritingEditorContext.tsx` -- `useWritingEditor.ts` -- `useWritingSelection.ts` -- `useWritingRewrite.ts` -- `useWritingToolbar.ts` - -职责: - -- 主状态 -- 选区状态 -- 改写状态 -- 工具栏状态 - -## 9.5 API 层 - -- `api/rewrite.ts` -- `api/documents.ts` - -职责: - -- 对接后端写作改写接口 -- 对接文档加载与保存接口 - ---- - -## 10. 能力到文件的精确映射 - -| 能力 | 新文件 | 说明 | -| --- | --- | --- | -| 新写作页面 | `frontend-web/src/open-canvas-writing/pages/WritingWorkspacePage.tsx` | 承载整个新模式 | -| 写作路由 | `frontend-web/src/open-canvas-writing/routes/WritingRoutes.tsx` | 统一路由入口 | -| 页面外壳 | `frontend-web/src/open-canvas-writing/components/WritingShell.tsx` | 页面布局与容器 | -| 文档头部 | `frontend-web/src/open-canvas-writing/components/WritingHeader.tsx` | 标题、状态 | -| 主工具栏 | `frontend-web/src/open-canvas-writing/components/WritingToolbar.tsx` | 文档结构与格式工具 | -| 正文编辑器 | `frontend-web/src/open-canvas-writing/editor/TiptapEditor.tsx` | Tiptap 核心封装 | -| 扩展配置 | `frontend-web/src/open-canvas-writing/editor/TiptapExtensions.ts` | 节点和 marks | -| 选区浮层 | `frontend-web/src/open-canvas-writing/components/WritingBubbleMenu.tsx` | 选区出现的悬浮条 | -| AI 菜单 | `frontend-web/src/open-canvas-writing/components/WritingAiMenu.tsx` | 润色、扩写等动作 | -| 改写预览 | `frontend-web/src/open-canvas-writing/components/WritingRewritePreview.tsx` | 候选内容展示与应用 | -| 替换 / 插入 | `frontend-web/src/open-canvas-writing/utils/apply-rewrite.ts` | 应用候选结果 | -| 编辑器状态 | `frontend-web/src/open-canvas-writing/context/WritingEditorContext.tsx` | 独立上下文 | -| 改写请求 | `frontend-web/src/open-canvas-writing/api/rewrite.ts` | 调后端接口 | - ---- - -## 11. 开发时的参考顺序 - -建议开发顺序如下: - -1. 先看 `PageRoutes.tsx`,确定路由接法 -2. 再看 `chat-box.tsx`,理解当前左右布局 -3. 然后起 `WritingWorkspacePage.tsx` 和 `WritingShell.tsx` -4. 再接 `TiptapEditor.tsx` -5. 再做 `WritingToolbar.tsx` -6. 再做 `WritingBubbleMenu.tsx` -7. 再做 `WritingAiMenu.tsx` -8. 最后做 `WritingRewritePreview.tsx` - -这样可以让每一层都有稳定承载,不会出现“先做了 AI 菜单,但页面还没落点”的混乱情况。 - ---- - -## 12. 明确哪些是“参考”,哪些是“目标” - -### 12.1 参考 - -- 豆包:交互体验参考 -- Novel:页面和编辑器质感参考 -- open-canvas:AI 工作流参考 -- Tiptap:编辑器技术实现参考 - -### 12.2 目标 - -目标不是复刻任何一个外部项目,而是: - -- 在 DeerFlow 现有架构中 -- 用 React + Vite + 现有线程体系 -- 做出一个更成熟的 AI 写作编辑器模式 - ---- - -## 13. 结论 - -这份映射表的核心作用,是避免团队在开发时迷路。 - -真正推荐的姿势是: - -- 用豆包定义目标体验 -- 用 Novel 定义页面质感 -- 用 open-canvas 定义 AI 工作流 -- 用 Tiptap 作为编辑器内核 -- 用 DeerFlow 现有路由、线程、模型体系做宿主 - -这样推进,既不会脱离你们现有工程,也能尽量快地把右侧编辑区拉到更接近成熟产品的水平。 diff --git a/frontend-web/docs/ai-writing-assistant-upgrade.md b/frontend-web/docs/ai-writing-assistant-upgrade.md deleted file mode 100644 index a8ffcf1d..00000000 --- a/frontend-web/docs/ai-writing-assistant-upgrade.md +++ /dev/null @@ -1,1191 +0,0 @@ -# AI 写作助手右侧编辑器升级需求文档 - -## 1. 文档目标 - -本文档用于指导 DeerFlow 当前 AI 写作助手的第二阶段升级。 - -本次升级不推翻现有整体架构,不重做左侧聊天,不替换 LangGraph 后端,而是在现有 DeerFlow 基础上,把右侧编辑区升级为更接近豆包 AI 写作助手、Novel、open-canvas 风格的 AI 原生写作编辑器。 - -本次需求聚焦四类核心能力: - -- 选区浮层 -- 润色 / 扩写 / 缩写 / 改语气 -- 替换 / 插入 -- 标题 / 列表 / 引用 / 代码块工具条 - -本次交付目标不是“补几个按钮”,而是把右侧从“结果预览面板”升级成“可直接写、可选区改、可连续编辑、可 AI 辅助重写”的正式写作工作区。 - ---- - -## 2. 总体结论 - -### 2.1 保留不动的部分 - -- 保留现有左侧聊天区 -- 保留现有线程体系 -- 保留现有 LangGraph 后端 -- 保留现有模型配置与模型选择体系 -- 保留现有消息流与线程上下文 -- 保留现有“写作模式”总入口 - -### 2.2 本次替换和新增的部分 - -- 将右侧简易 Artifact 文本查看 / 编辑区升级为 `Tiptap` 富文本编辑器 -- 在右侧新增面向写作场景的 AI 选区改写工作流 -- 为右侧补齐写作工具栏、选区浮层、结果预览、替换 / 插入动作 -- 将当前“生成后查看”的体验改造成“生成、选中、局部重写、继续编辑”的闭环 - -### 2.3 技术选型结论 - -本次默认采用 `Tiptap` 作为右侧编辑器内核。 - -原因: - -- 更适合做选区级 AI 操作 -- 更适合做浮动菜单和块结构格式工具栏 -- 更适合多段落选区与结构化文本编辑 -- 更适合后续扩展 slash menu、批注、修订记录、评论等能力 -- 与 Novel 和 open-canvas 的交互借鉴路径更贴近 - -`Plate` 作为 UI 参考和交互参考保留,不作为本轮主内核。 - ---- - -## 3. 参考产品与参考源码 - -### 3.1 交互参考 - -- 豆包 AI 写作助手 -- Novel -- open-canvas - -### 3.2 借鉴重点 - -- 豆包:选区浮层、AI 改写入口、替换 / 插入、轻量化写作工具条、页面留白和排版气质 -- Novel:文档编辑器产品感、工具条组织、编辑容器、页面视觉层级 -- open-canvas:AI 改写流程、候选内容应用流程、编辑器与聊天协同思路 - -### 3.3 明确不直接照搬的部分 - -- 不直接复制 open-canvas 的 Next.js 页面结构 -- 不直接复制 Novel 的服务端实现 -- 不直接引入整套 Plate 生态替代现有编辑区 - ---- - -## 4. 当前系统现状 - -结合当前代码结构,和本需求直接相关的现状如下: - -### 4.1 已有接入点 - -- `frontend-web/src/pages/PageRoutes.tsx` - - 当前已引入 `@/open-canvas/routes/OpenCanvasRoutes` - - 说明项目已经有独立 Canvas 路由接入能力 - -- `frontend-web/src/components/workspace/chats/chat-box.tsx` - - 当前左聊天 + 右 Artifact 面板是双栏布局 - - 当前右侧仍然偏 Artifact 文件查看 / 编辑 - -- `frontend-web/src/components/workspace/artifacts/context.tsx` - - 当前 Artifact 上下文仍以 `string[]` 文件路径为主 - - 不适合直接承载“选区级写作编辑器状态” - -- `frontend-web/src/components/workspace/input-box.tsx` - - 已经存在 `writing_mode` - - 可以作为本次写作助手模式切换的入口保留 - -### 4.2 当前不足 - -- 右侧编辑区更像文件预览器,不像完整写作编辑器 -- 缺少选区工具条 -- 缺少多段落改写能力 -- 缺少 AI 改写候选应用工作流 -- 缺少标题 / 列表 / 引用 / 代码块等正式文档工具 -- 页面视觉上仍偏“工程面板”,产品完成度不足 - ---- - -## 5. 本次需求范围 - -### 5.1 本次必须实现 - -- 基于 `Tiptap` 的右侧富文本编辑器 -- 选区浮层 -- AI 改写动作菜单 -- 润色 / 扩写 / 缩写 / 改语气 -- 改语气二级选项 -- 改写结果预览 -- 替换 / 插入 -- 标题 / 列表 / 引用 / 代码块工具条 -- UI 美化与交互统一 -- 多段落选区改写 - -### 5.2 本次不做 - -- 多人协作 -- 评论系统 -- 修订模式 -- 文档历史对比 -- 图片、表格、附件编辑器 -- 复杂导入导出 -- 全量 Notion 式块拖拽系统 -- slash command - -### 5.3 下阶段可扩展 - -- slash command -- 改写历史 -- 修订轨迹 -- 边写边建议 -- 文风模板 -- 批注与审阅 - ---- - -## 6. 产品目标 - -升级完成后,用户应获得以下体验: - -- 在左侧继续正常聊天 -- 在右侧像正式文档编辑器一样直接书写和改写 -- 选中一句、一段、多段内容后,能立即触发 AI 改写 -- AI 返回结果后,用户能决定“替换原文”还是“插入到后面” -- 页面视觉上明显从“开发态”升级为“产品态” -- 对长文写作、改稿、润色、扩写更友好 - ---- - -## 7. 页面与交互结构 - -### 7.1 整体布局 - -继续保留双栏布局: - -- 左侧:聊天区 -- 右侧:AI 写作编辑器 - -### 7.2 右侧编辑器结构 - -建议右侧拆分为以下区域: - -1. 文档头部区 - - 文档标题 - - 文档状态 - - 可能的保存提示 - -2. 主工具栏 - - 正文 - - 一级标题 - - 二级标题 - - 三级标题 - - 有序列表 - - 无序列表 - - 引用 - - 代码块 - - 加粗 - - 斜体 - - 下划线 - - 删除线 - -3. 编辑正文区 - - 基于 Tiptap 的可编辑正文 - -4. 选区浮层 - - AI 改写 - - 加粗 - - 斜体 - - 标题 - - 复制 - -5. AI 改写动作菜单 - - 润色 - - 扩写 - - 缩写 - - 改语气 - -6. 改写结果预览区 - - 预览文本 - - 替换 - - 插入 - - 重试 - - 取消 - -### 7.3 目标交互闭环 - -1. 用户在左侧发起写作任务 -2. 右侧生成初稿或打开写作画布 -3. 用户在右侧直接编辑 -4. 用户选中一句或多段 -5. 弹出浮层 -6. 点击 `AI 改写` -7. 选择 `润色 / 扩写 / 缩写 / 改语气` -8. 看到候选结果 -9. 选择 `替换` 或 `插入` -10. 继续写、继续改、继续和左侧聊天协同 - ---- - -## 8. 核心能力详细定义 - -## 8.1 能力一:选区浮层 - -### 目标 - -用户在右侧选中文本后,出现跟随选区的浮层工具条。 - -### 首版必须包含的按钮 - -- AI 改写 -- 加粗 -- 斜体 -- 标题 -- 复制 - -### 行为要求 - -- 只有在有效文本选区存在时显示 -- 选区取消后自动消失 -- 滚动时位置跟随更新 -- 只对右侧编辑器正文区域生效 -- 对跨多个段落的纯文本选区生效 - -### 首版限制 - -- 首版只处理文本选区 -- 不处理跨复杂嵌套节点的高级结构变更 - -### 验收标准 - -- 选中单段文本时能稳定出现 -- 选中连续多段文本时能稳定出现 -- 不遮挡正文主要阅读区域 - ---- - -## 8.2 能力二:AI 改写动作 - -### 目标 - -用户选中内容后,可快速对该内容发起 AI 重写。 - -### 一级动作 - -- 润色 -- 扩写 -- 缩写 -- 改语气 - -### 改语气二级选项 - -- 更专业 -- 更口语化 -- 更易读 -- 更学术 -- 更有文采 - -### 行为要求 - -- 改写动作基于当前选区触发 -- 支持单段落与多段落 -- 支持带少量前后文一起发给后端 -- 改写期间有明显 loading 态 -- 改写失败要有失败提示和重试入口 - -### 提示词策略 - -首版不开放自定义 prompt,只提供预设动作。 - -### 验收标准 - -- 四类一级动作可正常使用 -- 改语气二级菜单可正常展开和触发 -- 多段落改写不会破坏原文结构边界 - ---- - -## 8.3 能力三:替换 / 插入 - -### 目标 - -AI 返回候选内容后,不直接覆盖原文,而是先让用户决策如何应用。 - -### 两种应用方式 - -- 替换:用候选结果替换当前选区 -- 插入:把候选结果插入到选区后面,保留原文 - -### 行为要求 - -- AI 返回结果后先展示预览 -- 默认不自动落文档 -- 支持重试 -- 支持取消 -- 应用动作完成后,编辑器内容与光标位置要合理 - -### 首版简化 - -- 不做复杂 diff 对比 -- 先做“原文 + 候选文”的简单预览卡片 - -### 验收标准 - -- 替换位置准确 -- 插入位置准确 -- 应用后编辑器不丢焦点或出现异常闪动 - ---- - -## 8.4 能力四:主工具栏 - -### 目标 - -让右侧不再只是 AI 输出区,而是正式写作编辑器。 - -### 首版必须支持的结构 - -- 正文 -- 一级标题 -- 二级标题 -- 三级标题 -- 有序列表 -- 无序列表 -- 引用 -- 代码块 - -### 首版必须支持的行内样式 - -- 加粗 -- 斜体 -- 下划线 -- 删除线 - -### 行为要求 - -- 工具条按钮有选中态 -- 光标所在块能正确反映当前结构 -- 对选区和当前块的作用逻辑明确 -- 工具栏与选区浮层不打架 - -### 验收标准 - -- 标题和正文可互相切换 -- 列表、引用、代码块可稳定切换 -- 行内样式不影响 AI 选区改写链路 - ---- - -## 9. 编辑器数据模型 - -### 9.1 前端主状态 - -建议前端编辑器使用 Tiptap JSON 作为内部主状态。 - -建议定义: - -```ts -type WritingDocumentState = { - documentId?: string; - title: string; - markdown: string; - editorJson?: Record; - updatedAt?: string; -}; -``` - -### 9.2 选区状态 - -```ts -type WritingSelectionState = { - from: number; - to: number; - text: string; - isCollapsed: boolean; - blockTypes?: string[]; -}; -``` - -### 9.3 改写动作 - -```ts -type RewriteAction = - | "polish" - | "expand" - | "shorten" - | "tone"; -``` - -### 9.4 改语气风格 - -```ts -type RewriteTone = - | "professional" - | "casual" - | "readable" - | "academic" - | "expressive"; -``` - -### 9.5 改写请求 - -```ts -type RewriteRequest = { - threadId: string; - documentId?: string; - selection: { - from: number; - to: number; - text: string; - }; - action: RewriteAction; - tone?: RewriteTone; - mode?: "preview"; - surroundingContext?: { - before?: string; - after?: string; - }; -}; -``` - -### 9.6 改写响应 - -```ts -type RewriteResponse = { - output: string; - originalSelection: { - from: number; - to: number; - text: string; - }; - metadata?: { - action: RewriteAction; - tone?: RewriteTone; - model?: string; - }; -}; -``` - ---- - -## 10. 与现有 DeerFlow 代码结构的对应关系 - -## 10.1 需要保留并对接的现有模块 - -- `frontend-web/src/pages/PageRoutes.tsx` - - 保留现有路由接入方式 - -- `frontend-web/src/components/workspace/chats/chat-box.tsx` - - 保留左右布局思路 - - 需要改造右侧内容承载能力 - -- `frontend-web/src/components/workspace/input-box.tsx` - - 保留 `writing_mode` - - 后续可作为进入写作编辑区的重要入口 - -- `frontend-web/src/components/workspace/artifacts/context.tsx` - - 现有结构不够承载写作编辑器状态 - - 可保留给原文件型 artifact - - 新写作画布建议单独维护自己的 context - -## 10.2 不建议继续复用的方式 - -- 不建议把 Tiptap 状态直接塞进现有 `string[] artifacts` -- 不建议继续把右侧主体验建立在“文件详情页”上 -- 不建议把 AI 改写做成单纯聊天消息回传后人工复制 - ---- - -## 11. 前端目录建议 - -建议在 `frontend-web/src` 下新增独立模块目录: - -```text -src/open-canvas-writing/ - routes/ - WritingRoutes.tsx - pages/ - WritingWorkspacePage.tsx - components/ - WritingShell.tsx - WritingHeader.tsx - WritingToolbar.tsx - WritingEditorPane.tsx - WritingBubbleMenu.tsx - WritingAiMenu.tsx - WritingRewritePreview.tsx - WritingStatusBar.tsx - editor/ - TiptapEditor.tsx - TiptapExtensions.ts - commands.ts - selection.ts - context/ - WritingEditorContext.tsx - hooks/ - useWritingEditor.ts - useWritingSelection.ts - useWritingRewrite.ts - useWritingToolbar.ts - api/ - rewrite.ts - documents.ts - types/ - document.ts - rewrite.ts - utils/ - markdown.ts - apply-rewrite.ts - editor-style.ts -``` - ---- - -## 12. 路由与页面接入要求 - -### 12.1 路由策略 - -建议在现有 `PageRoutes` 体系下增加独立写作工作区页面,而不是塞进已有普通 artifact 面板。 - -### 12.2 接入方式 - -建议新增: - -- `frontend-web/src/open-canvas-writing/routes/WritingRoutes.tsx` -- `frontend-web/src/open-canvas-writing/pages/WritingWorkspacePage.tsx` - -然后在: - -- `frontend-web/src/pages/PageRoutes.tsx` - -中引入新的写作助手路由,例如: - -- `page/writing/*` -- 或合并到现有 `page/canvas/*` 下的子路由 - -### 12.3 路由设计建议 - -推荐: - -- `page/writing` - - 新建写作工作区 - -- `page/writing/:threadId` - - 进入某个线程对应写作工作区 - -如果你们已经有 `open-canvas` 路由,则建议不要把“豆包式写作编辑器”塞进原实验性画布页面里,而是作为独立子模式接入,避免后续页面职责混乱。 - ---- - -## 13. 前端实施拆解 - -## 13.1 模块 A:编辑器内核接入 - -### 目标 - -让右侧拥有正式可编辑文档能力。 - -### 任务 - -- 安装 Tiptap 基础依赖 -- 接入 `StarterKit` -- 接入 `Underline` -- 配置支持: - - paragraph - - heading - - bulletList - - orderedList - - blockquote - - codeBlock - - bold - - italic - - underline - - strike - - code -- 建立受控 / 半受控编辑器状态 -- 实现 Markdown 与 Tiptap JSON 的最小同步策略 - -### 输出物 - -- `TiptapEditor.tsx` -- `TiptapExtensions.ts` -- 基础编辑器容器样式 - -### 依赖关系 - -- 无前置依赖,可先开工 - -### 验收标准 - -- 右侧可以像普通文档编辑器一样输入和编辑内容 - -## 13.2 模块 B:主工具栏 - -### 目标 - -补齐写作编辑基础能力。 - -### 任务 - -- 实现标题切换 -- 实现列表切换 -- 实现引用切换 -- 实现代码块切换 -- 实现加粗 / 斜体 / 下划线 / 删除线 -- 工具按钮高亮与禁用态 -- 光标所在块状态识别 - -### 输出物 - -- `WritingToolbar.tsx` -- `useWritingToolbar.ts` - -### 依赖关系 - -- 依赖模块 A - -### 验收标准 - -- 常用写作结构和行内样式均可稳定切换 - -## 13.3 模块 C:选区浮层 - -### 目标 - -选中文本后出现轻量化操作浮层。 - -### 任务 - -- 监听 Tiptap selection -- 接入 BubbleMenu -- 处理显示 / 隐藏逻辑 -- 布局 AI 改写、加粗、斜体、标题、复制 -- 处理滚动与定位稳定性 - -### 输出物 - -- `WritingBubbleMenu.tsx` -- `useWritingSelection.ts` -- `selection.ts` - -### 依赖关系 - -- 依赖模块 A - -### 验收标准 - -- 选区浮层在单段和多段文本上都稳定可用 - -## 13.4 模块 D:AI 改写菜单 - -### 目标 - -把 AI 改写入口真正挂进选区工作流。 - -### 任务 - -- 实现 `AI 改写` 一级入口 -- 实现动作菜单 -- 实现改语气二级菜单 -- 封装改写请求 API -- 建立 loading / success / error 状态管理 - -### 输出物 - -- `WritingAiMenu.tsx` -- `useWritingRewrite.ts` -- `api/rewrite.ts` - -### 依赖关系 - -- 依赖模块 C -- 与后端接口联调 - -### 验收标准 - -- 四类改写动作都能从选区直接触发 - -## 13.5 模块 E:候选结果预览与应用 - -### 目标 - -将 AI 改写从“直接灌入文本”升级为“预览后应用”。 - -### 任务 - -- 设计候选结果展示卡 -- 展示原文选区与候选结果 -- 提供: - - 替换 - - 插入 - - 重试 - - 取消 -- 将候选内容通过 Tiptap transaction 应用到文档 - -### 输出物 - -- `WritingRewritePreview.tsx` -- `apply-rewrite.ts` - -### 依赖关系 - -- 依赖模块 D - -### 验收标准 - -- 替换和插入都能准确写回编辑器 - -## 13.6 模块 F:页面美化 - -### 目标 - -把页面从“工程面板”升级为“成熟产品页面”。 - -### 任务 - -- 调整双栏比例和留白 -- 优化右侧容器背景、阴影、边框、圆角 -- 优化标题层级、正文字号、行高、段距 -- 优化浮层和工具条视觉层级 -- 优化 loading、hover、active 动效 -- 对移动端和窄屏做降级策略 - -### 输出物 - -- `WritingShell.tsx` -- `WritingHeader.tsx` -- `WritingStatusBar.tsx` -- 相关样式变量 - -### 依赖关系 - -- 可与其他模块并行推进 - -### 验收标准 - -- 整体视觉明显接近正式写作产品 - ---- - -## 14. 后端实施拆解 - -## 14.1 总原则 - -后端保留现有 LangGraph 和线程架构,本轮只增加“选区改写型接口 / 节点 / 工具”,不重构整套后端。 - -## 14.2 后端最小职责 - -后端负责: - -- 接收选区文本 -- 接收动作类型 -- 补足必要上下文 -- 调用模型完成改写 -- 返回候选文本 - -后端首版不负责: - -- 直接操作 Tiptap JSON -- 直接做文档 patch -- 直接做复杂结构变换 - -## 14.3 推荐新增能力 - -- `rewrite_selection` -- `expand_selection` -- `shorten_selection` -- `change_tone` - -### 统一接口建议 - -更推荐做成一个统一接口: - -```text -POST /api/writing/rewrite -``` - -由请求体中的 `action` 和 `tone` 控制行为。 - -### 请求建议 - -- `threadId` -- `documentId` -- `selection.text` -- `selection.from` -- `selection.to` -- `action` -- `tone` -- `surroundingContext.before` -- `surroundingContext.after` - -### 返回建议 - -- `output` -- `metadata.action` -- `metadata.tone` -- `metadata.model` - -## 14.4 后端提示词策略 - -首版建议固定模板: - -- 润色:保持原意,提高表达质量 -- 扩写:在不偏离主题的前提下扩展细节 -- 缩写:压缩表达但保留关键信息 -- 改语气:保持事实不变,调整语言风格 - -统一要求: - -- 尽量不要引入原文没有的新事实 -- 不输出解释性前缀 -- 直接返回可落文档的正文 - -## 14.5 后端验收标准 - -- 单段落改写稳定 -- 多段落改写稳定 -- 返回结果不夹带解释性废话 -- 失败时能返回可识别错误 - ---- - -## 15. 前后端协议约束 - -### 15.1 首版协议原则 - -- 前端负责编辑器结构和最终写回 -- 后端负责文本重写 -- 前后端暂时通过“纯文本选区 + 上下文”交互 - -### 15.2 为什么不直接传富文本 JSON - -因为首版最大的目标是稳定闭环,而不是复杂结构编辑。 - -如果一开始就让后端理解完整 Tiptap JSON,会显著增加: - -- 协议复杂度 -- 结构错位风险 -- 测试成本 -- 联调成本 - -因此首版推荐: - -- 编辑器内部状态:Tiptap JSON -- 改写协议:纯文本 + 前后文 -- 最终应用:前端 transaction - ---- - -## 16. 分阶段实施计划 - -## 阶段 1:编辑器底座 - -### 目标 - -让右侧具备正式写作编辑能力。 - -### 包含内容 - -- Tiptap 接入 -- 标题 / 列表 / 引用 / 代码块 -- 行内样式 -- 初步页面容器 - -### 验收 - -- 右侧能独立承担写作编辑 - -## 阶段 2:选区浮层 - -### 目标 - -把“选中文本后操作”打通。 - -### 包含内容 - -- BubbleMenu -- AI 改写入口 -- 基础样式按钮 - -### 验收 - -- 单段和多段选区都可稳定触发 - -## 阶段 3:AI 改写 - -### 目标 - -把改写请求与后端串起来。 - -### 包含内容 - -- 润色 -- 扩写 -- 缩写 -- 改语气 - -### 验收 - -- 四类动作都能返回结果 - -## 阶段 4:替换 / 插入 - -### 目标 - -把候选结果稳定应用到文档。 - -### 包含内容 - -- 结果预览 -- 替换 -- 插入 -- 重试 -- 取消 - -### 验收 - -- 改写结果可控落文档 - -## 阶段 5:UI 打磨 - -### 目标 - -让右侧从“能用”升级到“成熟产品感”。 - -### 包含内容 - -- 容器和留白 -- 工具栏设计 -- 浮层风格 -- 字体和排版 -- 动效 - -### 验收 - -- 视觉上明显接近豆包 / Novel 风格 - ---- - -## 17. 可并行分工建议 - -## 17.1 前端 A 组:编辑器底座 - -负责: - -- Tiptap 接入 -- 主工具栏 -- 结构节点与 marks - -对应阶段: - -- 阶段 1 - -## 17.2 前端 B 组:选区交互 - -负责: - -- 选区监听 -- BubbleMenu -- AI 改写入口 - -对应阶段: - -- 阶段 2 - -## 17.3 前端 C 组:候选结果与体验层 - -负责: - -- 改写结果预览 -- 替换 / 插入 -- loading / error / retry -- UI 打磨 - -对应阶段: - -- 阶段 4 -- 阶段 5 - -## 17.4 后端 D 组:改写服务 - -负责: - -- 统一改写接口 -- LangGraph 节点或工具 -- 提示词 -- 上下文拼接 - -对应阶段: - -- 阶段 3 - -## 17.5 联调负责人 - -负责: - -- 协议定稿 -- 错误码和异常约定 -- 多段落选区联调 -- 光标位置 / 替换位置问题兜底 - ---- - -## 18. UI 设计要求 - -### 18.1 风格关键词 - -- 轻 -- 干净 -- 编辑器优先 -- AI 能力明显但不喧宾夺主 -- 有留白 -- 有产品感 - -### 18.2 页面视觉要求 - -- 右侧阅读宽度适中,不可过宽 -- 主文内容器有明确层级 -- 正文行高偏舒展 -- 标题有清晰层级 -- 工具栏不应过厚重 -- 浮层必须轻、准、快 - -### 18.3 动效要求 - -- 浮层出现和消失有轻微过渡 -- AI 改写请求有反馈 -- 替换 / 插入确认有轻量状态切换 - -### 18.4 参考视觉方向 - -- 布局参考 Novel -- 选区和改写交互参考豆包 -- AI 工作流节奏参考 open-canvas - ---- - -## 19. 风险与规避方案 - -## 19.1 风险一:现有 Artifact 架构不适合承载写作编辑器 - -### 规避 - -- 不强塞到 `string[] artifacts` -- 新建独立写作模块和上下文 - -## 19.2 风险二:富文本与 Markdown 同步复杂 - -### 规避 - -- 首版以 Tiptap 内部状态为准 -- 对外只做最小 Markdown 输出 - -## 19.3 风险三:多段落改写后替换位置错乱 - -### 规避 - -- 前端记录稳定选区范围 -- 候选结果应用时使用 Tiptap transaction -- 首版优先支持连续纯文本段落 - -## 19.4 风险四:AI 返回啰嗦解释文本 - -### 规避 - -- 后端提示词严格约束只返回正文 -- 前端必要时做轻量清洗 - -## 19.5 风险五:UI 做成“功能堆砌” - -### 规避 - -- 首版只上最核心 4 类能力 -- 不把太多低频按钮塞进第一层工具栏 - ---- - -## 20. 验收标准 - -## 20.1 功能验收 - -- 右侧可直接编辑文档 -- 支持标题 / 列表 / 引用 / 代码块 -- 选中内容后有浮层 -- 支持润色 / 扩写 / 缩写 / 改语气 -- 支持替换 / 插入 -- 支持多段落选区改写 - -## 20.2 交互验收 - -- 浮层位置稳定 -- AI 改写链路顺畅 -- 替换 / 插入后位置正确 -- 不影响左侧聊天流 - -## 20.3 体验验收 - -- 右侧明显比当前版本更成熟 -- 页面更适合长文阅读和改稿 -- 整体交互接近豆包 AI 写作助手 - ---- - -## 21. 首版最小可交付范围 - -如果需要控制风险,建议首版只交付: - -- Tiptap 接入 -- 主工具栏 -- 选区浮层 -- 润色 / 扩写 / 缩写 / 改语气 -- 替换 / 插入 -- 基础 UI 美化 - -以下能力不要强行塞进首版: - -- 自定义提示词 -- slash command -- 修订模式 -- 评论系统 -- 复杂 diff 对比 -- 文档历史版本 - ---- - -## 22. 实施优先级 - -### P0 - -- Tiptap 接入 -- 主工具栏 -- 选区浮层 -- AI 改写接口 -- 替换 / 插入 - -### P1 - -- 页面整体美化 -- 改语气二级菜单 -- 多段落改写稳定性 -- 错误态 / 重试态 - -### P2 - -- 文档状态提示 -- 轻量空态 -- 细节动画 -- 交互易用性优化 - ---- - -## 23. 结论 - -这次升级最合理的路线,不是继续在现有右侧 Artifact 详情页上小修小补,而是正式引入一层写作编辑器能力。 - -最稳妥的做法是: - -- 保留左侧聊天和现有 LangGraph 后端 -- 在右侧引入 `Tiptap` -- 先把四个核心能力做深做稳 -- UI 借鉴豆包、Novel、open-canvas 的成熟交互 - -只要按照本文档分阶段推进,这个需求虽然大,但完全可以拆成多组并行、逐步验收、持续落地的工程任务,而不是一次性大爆炸改造。 diff --git a/frontend-web/docs/ai-writing-backend-orchestrator-blueprint.md b/frontend-web/docs/ai-writing-backend-orchestrator-blueprint.md deleted file mode 100644 index d44e3f78..00000000 --- a/frontend-web/docs/ai-writing-backend-orchestrator-blueprint.md +++ /dev/null @@ -1,645 +0,0 @@ -# AI 写作后端新路线设计草案 - -## 1. 目标 - -为“弱模型多 Agent 写作工作流”建设一条独立后端路线,满足以下要求: - -- 不影响现有同事正在开发的功能 -- 不强耦合现有 DeerFlow 通用对话主链路 -- 可独立部署、独立调试、独立回滚 -- 支持多阶段写作编排、人工介入、失败重试、流式事件输出 - ---- - -## 2. 推荐建设方式 - -优先建议: - -- 在 `offline-backend-20260512` 下新建独立应用或独立模块 - -建议目录: - -```text -offline-backend-20260512/ - apps/ - ai_writing_orchestrator/ - api/ - application/ - domain/ - infrastructure/ - prompts/ - tests/ -``` - -如果当前后端没有 `apps/` 结构,也可以采用: - -```text -offline-backend-20260512/ - ai-writing-orchestrator/ - api/ - application/ - domain/ - infrastructure/ - prompts/ - tests/ -``` - -重点不是目录名字,而是一定要把“多 Agent 写作编排”与现有通用聊天能力隔离开。 - ---- - -## 3. 技术路线建议 - -推荐组合: - -- 编排框架:`LangGraph` -- 模型调用:接你们内网模型网关 -- 检索层:接内网知识库 / 文档库 / 搜索服务 -- 状态存储:先用数据库或 Redis + DB,后续再演进 -- 事件输出:SSE 或流式 HTTP - -推荐原因: - -- `LangGraph` 适合状态机和多节点回退 -- 对弱模型最关键的是“强流程控制”,不是“自由 Agent 对话” -- 后续加人工确认节点和 checkpoint 会更自然 - ---- - -## 4. 分层设计 - -建议按四层拆分。 - -## 4.1 API 层 - -职责: - -- 接收前端请求 -- 鉴权 -- 参数校验 -- 返回 workflow 信息 -- 提供事件流出口 - -建议文件: - -```text -api/ - routes/ - workflows.py - workflow_events.py - schemas/ - workflow_request.py - workflow_response.py -``` - -## 4.2 Application 层 - -职责: - -- 组织用例 -- 调度 workflow service -- 管理人工确认动作 -- 处理重试和终止逻辑 - -建议文件: - -```text -application/ - services/ - workflow_service.py - workflow_event_service.py - human_action_service.py - use_cases/ - create_workflow.py - start_workflow.py - confirm_outline.py - rewrite_section.py - approve_final.py -``` - -## 4.3 Domain 层 - -职责: - -- 定义工作流状态机 -- 定义 Agent 输入输出协议 -- 定义 artifact 与评分模型 - -建议文件: - -```text -domain/ - models/ - workflow.py - workflow_stage.py - workflow_event.py - artifact.py - review_result.py - agents/ - researcher.py - outliner.py - writer.py - editor.py - supervisor.py - graphs/ - writing_graph.py - policies/ - retry_policy.py - quality_policy.py -``` - -## 4.4 Infrastructure 层 - -职责: - -- 访问模型网关 -- 访问检索服务 -- 访问数据库 -- 发送事件 -- 缓存与日志 - -建议文件: - -```text -infrastructure/ - llm/ - model_client.py - retrieval/ - intranet_search_client.py - knowledge_base_client.py - persistence/ - workflow_repository.py - artifact_repository.py - streaming/ - sse_publisher.py - logging/ - workflow_logger.py -``` - ---- - -## 5. 工作流最小闭环 - -第一版建议只做如下状态机: - -1. `collect_materials` -2. `generate_outline` -3. `await_outline_confirmation` -4. `write_draft` -5. `review_draft` -6. `rewrite_if_needed` -7. `complete` - -终止规则: - -- 编辑评分达标则完成 -- 否则进入重写 -- 最多重写 2 次 -- 超过次数则标记 `human_intervention_required` - ---- - -## 6. Agent 模块设计 - -## 6.1 Researcher - -输入: - -- 主题 -- 用户要求 -- 检索范围 -- 可用资料源 - -输出: - -- `material_cards` -- `facts` -- `quotes` -- `examples` -- `risks` -- `source_refs` - -程序职责: - -- 先做检索 -- 再把检索结果裁剪后送给模型 -- 输出必须结构化 - -## 6.2 Outliner - -输入: - -- 用户要求 -- 研究结果 - -输出: - -- `title_candidates` -- `outline_sections` -- `key_points_per_section` - -约束: - -- 提纲优先,不写正文 - -## 6.3 Writer - -输入: - -- 已确认提纲 -- 素材卡片 -- 风格要求 - -输出: - -- `draft_sections` -- `section_source_refs` -- `full_draft_markdown` - -建议: - -- 按章节分段生成,不要一次长文直出 - -## 6.4 Editor - -输入: - -- 初稿 -- 提纲 -- 素材 -- 用户要求 - -输出: - -- `issues` -- `severity` -- `score` -- `rewrite_instructions` -- `decision` - -规则: - -- 先查事实,再查结构,再查文风 - -## 6.5 Supervisor - -输入: - -- 各阶段结果 -- 重试计数 -- 评分结果 - -输出: - -- 下一跳节点 -- 是否需要人工确认 -- 是否结束 - ---- - -## 7. Prompt 组织建议 - -建议为每个 Agent 分开管理 prompt,不要写成一个超级 prompt。 - -建议目录: - -```text -prompts/ - researcher/ - system.txt - output_schema.json - outliner/ - system.txt - output_schema.json - writer/ - system.txt - output_schema.json - editor/ - system.txt - output_schema.json - supervisor/ - system.txt -``` - -原则: - -- 每个 Agent 一套职责 -- 每个 Agent 一套输出 schema -- Prompt 版本可管理 -- 后续支持 A/B 调参 - ---- - -## 8. 弱模型适配策略 - -## 8.1 先检索后生成 - -不要让模型自己“想资料”,程序先检索、裁剪、去重后再喂给模型。 - -## 8.2 短上下文任务化 - -每个节点只给弱模型一小段明确任务: - -- 研究归纳 -- 提纲整理 -- 单章节写作 -- 单章节审稿 - -## 8.3 结构化输出 - -优先 JSON 或 Pydantic schema,不要放任自然语言自由返回。 - -## 8.4 章节级重写 - -不要整篇回炉。编辑指出第 2 节有问题,就只重写第 2 节。 - -## 8.5 程序接管质量门槛 - -例如: - -- 资料引用数不足,不进入下一步 -- 提纲章节数过少,直接重生 -- 编辑评分低于阈值,自动重写 - ---- - -## 9. 数据模型建议 - -## 9.1 Workflow - -建议字段: - -- `id` -- `status` -- `current_stage` -- `user_id` -- `thread_id` -- `input_payload` -- `current_artifact_id` -- `retry_count` -- `created_at` -- `updated_at` - -## 9.2 Workflow Artifact - -建议字段: - -- `id` -- `workflow_id` -- `artifact_type` -- `stage` -- `version` -- `title` -- `content` -- `metadata` - -## 9.3 Workflow Event - -建议字段: - -- `id` -- `workflow_id` -- `event_type` -- `stage` -- `agent_name` -- `payload` -- `created_at` - -## 9.4 Human Action - -建议字段: - -- `workflow_id` -- `action_type` -- `payload` -- `operator` -- `created_at` - ---- - -## 10. API 草案 - -## 10.1 创建任务 - -`POST /api/ai-writing/workflows` - -请求: - -```json -{ - "topic": "主题", - "articleType": "行业分析", - "audience": "管理层", - "tone": "专业克制", - "targetLength": 3000, - "constraints": ["避免口语化"], - "referenceScope": ["内网知识库", "项目复盘文档"] -} -``` - -返回: - -```json -{ - "workflowId": "wf_xxx", - "status": "idle" -} -``` - -## 10.2 启动任务 - -`POST /api/ai-writing/workflows/{id}/start` - -## 10.3 查询任务 - -`GET /api/ai-writing/workflows/{id}` - -## 10.4 订阅事件 - -`GET /api/ai-writing/workflows/{id}/events` - -返回事件流示例: - -```json -{ - "type": "stage_changed", - "stage": "generating_outline", - "message": "正在生成提纲" -} -``` - -## 10.5 确认提纲 - -`POST /api/ai-writing/workflows/{id}/confirm-outline` - -## 10.6 指定章节重写 - -`POST /api/ai-writing/workflows/{id}/rewrite-section` - -## 10.7 确认终稿 - -`POST /api/ai-writing/workflows/{id}/approve-final` - ---- - -## 11. LangGraph 实现建议 - -## 11.1 推荐 graph 节点 - -- `load_input` -- `retrieve_materials` -- `normalize_materials` -- `generate_outline` -- `wait_for_outline_confirmation` -- `write_sections` -- `merge_sections` -- `review_draft` -- `rewrite_sections` -- `publish_final_artifact` - -## 11.2 推荐 state - -建议 state 字段: - -- `workflow_id` -- `input_payload` -- `materials` -- `outline` -- `outline_confirmed` -- `draft_sections` -- `merged_draft` -- `review_result` -- `rewrite_targets` -- `final_article` -- `retry_count` -- `events` - -## 11.3 条件边 - -建议条件边逻辑: - -- 提纲未确认 -> 等待用户 -- 编辑通过 -> 完成 -- 编辑不通过且可重试 -> 重写 -- 编辑不通过且超限 -> 人工介入 - ---- - -## 12. 可观测性要求 - -第一版就建议带上以下能力: - -- 每个 workflow 的事件日志 -- 每个 Agent 的输入输出摘要 -- 每次重试原因 -- 每个阶段耗时 -- 每次模型调用 token 与错误日志 - -后续排问题会非常依赖这些数据。 - ---- - -## 13. 测试建议 - -## 13.1 单元测试 - -覆盖: - -- stage 流转 -- retry policy -- quality policy -- 事件构造 - -## 13.2 集成测试 - -覆盖: - -- 创建任务到完成的主链路 -- 提纲确认链路 -- 审稿失败后重写链路 -- 人工介入链路 - -## 13.3 Prompt 回归集 - -建议建立 10-20 个固定写作样例,做回归: - -- 短文 -- 长文 -- 事实密集型文章 -- 宣传型文章 -- 分析型文章 - ---- - -## 14. 开发顺序建议 - -## Step 1:先做 domain 与 API 契约 - -先定: - -- stage 枚举 -- event 枚举 -- artifact 结构 -- human action payload - -## Step 2:先做 fake orchestrator - -先不接真实模型,只模拟事件流,把前后端联调跑通。 - -## Step 3:接 Researcher + Outliner - -优先把“研究 + 提纲”做出来,因为这是弱模型提升最大的环节。 - -## Step 4:接 Writer + Editor - -再接初稿和审稿重写。 - -## Step 5:接可观测性和缓存 - -最后再补生产化能力。 - ---- - -## 15. 风险控制建议 - -### 风险 1:直接复用现有线程流,导致协议污染 - -建议: - -- workflow 用独立 route 和独立 event schema - -### 风险 2:写作结果与右侧 artifact 模型不兼容 - -建议: - -- 后端统一输出标准 markdown artifact - -### 风险 3:弱模型频繁输出不合法 JSON - -建议: - -- 每个节点加 schema repair -- 加轻量重试 -- 尽量减少复杂嵌套输出 - -### 风险 4:研究资料太多导致上下文爆炸 - -建议: - -- 检索后先裁剪、摘要、分桶 - ---- - -## 16. 最终建议 - -后端第一版最重要的是把“状态机、事件流、人工确认、章节重写”这四件事做对,而不是一开始就追求很多花哨 Agent。 - -建议 MVP 只保留: - -1. Researcher -2. Outliner -3. Writer -4. Editor -5. Supervisor - -当这条链路稳定后,再继续扩展 fact-checker、style editor、compliance reviewer。 diff --git a/frontend-web/docs/ai-writing-final-execution-plan.md b/frontend-web/docs/ai-writing-final-execution-plan.md deleted file mode 100644 index cd8805cd..00000000 --- a/frontend-web/docs/ai-writing-final-execution-plan.md +++ /dev/null @@ -1,806 +0,0 @@ -# AI 写作执行方案 - -## 1. 总体原则 - -本项目的最终落地原则: - -1. 前端继续基于当前 `open-canvas` 页面开发,不复制第二套页面 -2. 后端单独开 `ai-writing-orchestrator` 新路线,不侵入现有主链路 -3. 编排框架采用 `LangGraph` -4. 写作工作流采用 `Researcher -> Outliner -> Writer -> Editor -> Supervisor` -5. Agent 间输出统一采用 `JSON Schema / Pydantic` -6. 前后端通过 `SSE` 传递工作流事件 -7. 第一版只做最小闭环,不做过多花哨扩展 - -一句话说明: - -用独立的新后端路线承载多 Agent 写作工作流,前端继续复用 `open-canvas`,实现上采用 `LangGraph + A/B/C Agent + SSE` 的最小闭环。 - ---- - -## 2. 项目边界 - -## 2.1 前端边界 - -继续使用当前页面: - -- [CanvasResizableLayout.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/layout/CanvasResizableLayout.tsx) -- [CanvasChatPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/chat/CanvasChatPanel.tsx) -- [CanvasArtifactPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/artifact/CanvasArtifactPanel.tsx) - -前端做的事情: - -- 左侧增加“写作工作流模式” -- 展示多 Agent 执行进度与中间产物 -- 提供提纲确认和指定章节重写 -- 将初稿、终稿同步到右侧 artifact - -前端不做的事情: - -- 不重写右侧编辑器 -- 不重写现有普通 chat 流程 -- 不把 workflow 状态散落在多个旧 context 里 - -## 2.2 后端边界 - -后端采用独立新路线: - -推荐目录: - -```text -offline-backend-20260512/ - apps/ - ai_writing_orchestrator/ - api/ - application/ - domain/ - infrastructure/ - prompts/ - tests/ -``` - -后端做的事情: - -- 实现写作 workflow 状态机 -- 实现 A/B/C Agent 与 Supervisor -- 接内网模型与内网检索 -- 输出标准事件流和文章 artifact - -后端不做的事情: - -- 不直接改现有 DeerFlow 主流程 -- 不把工作流逻辑塞到现有通用 thread 协议里 - -## 2.3 资料搜集边界 - -第一版处于互联网开发阶段,因此研究员的资料搜集先走互联网检索,不接内网检索服务。 - -第一版要求: - -- 研究员可以调用互联网搜索能力收集公开资料 -- 研究结果必须保留来源 URL、标题、摘要、抓取时间 -- 检索层必须抽象成统一接口,不能把互联网搜索逻辑写死在 Agent prompt 或 graph 节点里 - -第二版预留: - -- 后续替换或并行接入内网搜索接口 -- 后续接入内部知识库、文档库、FAQ、项目复盘库 -- 前端和工作流层不因检索源变化而改协议 - -统一设计要求: - -- `Researcher` 只依赖“检索接口”,不依赖具体是互联网还是内网 -- `writing_graph` 只读取标准化后的素材结果 -- API、事件流、artifact 结构不区分互联网资料和内网资料 - -推荐检索抽象: - -```text -infrastructure/ - retrieval/ - base.py - internet_search_client.py - intranet_search_client.py - retriever_router.py -``` - -建议接口: - -```python -class RetrievalProvider(Protocol): - def search(self, query: str, top_k: int) -> list[RetrievedDocument]: ... - -class RetrievedDocument(BaseModel): - title: str - url: str | None - source_type: str - snippet: str - published_at: str | None = None - fetched_at: str - reliability: str | None = None -``` - -第一版路由策略: - -- 默认 `internet` -- 配置项预留 `intranet`、`hybrid` - -例如: - -```yaml -retrieval: - provider: internet - top_k: 8 - timeout_seconds: 15 -``` - -这样第一版先跑公网资料,后续切换内网时,只替换 `retriever_router` 和对应 provider,不改 workflow 主逻辑。 - ---- - -## 3. 第一版产品目标 - -用户在 `open-canvas` 中选择“写作工作流模式”后,可以完成以下流程: - -1. 输入主题、体裁、读者、风格、篇幅 -2. 研究员搜集资料并输出素材卡片 -3. 提纲 Agent 生成提纲 -4. 用户确认提纲 -5. Writer 生成初稿 -6. Editor 给出问题清单与评分 -7. 不达标则重写一次或两次 -8. 终稿写入右侧 artifact - -第一版成功标准: - -- 不是“生成得像最强模型一样好” -- 而是“明显优于单轮直出,并且流程可控、可回退、可人工介入” - -## 3.1 第一版研究员目标 - -第一版 `Researcher` 的核心职责,不是“写文章”,而是“给 Writer 准备可信、可引用、可筛选的素材”。 - -第一版输入: - -- 用户主题 -- 用户写作要求 -- 检索源配置,第一版默认 `internet` - -第一版输出至少包含: - -- `material_cards` -- `facts` -- `quotes` -- `examples` -- `risks` -- `source_refs` - -每条素材最少要带: - -- 来源标题 -- 来源 URL -- 摘要 -- 可信度标签 -- 被引用的原始片段或整理后的事实点 - -第一版 Researcher 的程序流程建议: - -1. 根据主题自动生成 3-5 个检索 query -2. 调用互联网检索接口拿回候选文档 -3. 做去重、裁剪、初筛 -4. 让模型将结果整理成结构化素材卡片 -5. 将结构化结果返回给 Outliner 和 Writer - -第一版先不做: - -- 深度网页 crawling -- 多轮自动追问式深研究 -- 内网知识库接入 -- 复杂引用格式生成 - ---- - -## 4. 最终工作流 - -## 4.1 Agent 角色 - -### `Researcher` - -职责: - -- 从配置的检索源检索信息 -- 产出结构化素材卡片 -- 标注来源、可信度、风险点 - -第一版说明: - -- 默认走互联网公开资料检索 -- 必须预留后续切换到内网检索源的接口 -- 检索实现放在 `infrastructure/retrieval/`,不要写死在 prompt 中 - -第一版检索源字段建议: - -- `source_type = "internet"` -- 后续预留 `source_type = "intranet" | "hybrid"` - -输出: - -- `material_cards` -- `facts` -- `quotes` -- `examples` -- `risks` -- `source_refs` - -### `Outliner` - -职责: - -- 根据用户要求和研究结果生成提纲 - -输出: - -- `title_candidates` -- `outline_sections` -- `key_points_per_section` - -### `Writer` - -职责: - -- 基于提纲和素材写初稿 -- 按章节写,不一次生成整篇长文 - -输出: - -- `draft_sections` -- `section_source_refs` -- `full_draft_markdown` - -### `Editor` - -职责: - -- 检查事实、结构、文风 -- 给出评分与重写建议 - -输出: - -- `issues` -- `severity` -- `score` -- `rewrite_instructions` -- `decision` - -### `Supervisor` - -职责: - -- 负责流程调度 -- 负责通过 / 重写 / 人工介入判断 - -## 4.2 状态机 - -第一版统一采用: - -- `idle` -- `collecting_materials` -- `generating_outline` -- `await_outline_confirmation` -- `writing_draft` -- `reviewing_draft` -- `rewriting_draft` -- `completed` -- `failed` -- `human_intervention_required` - -## 4.3 流程闭环 - -最小闭环固定为: - -1. `Research` -2. `Outline` -3. `HumanConfirmOutline` -4. `Draft` -5. `Review` -6. `RewriteIfNeeded` -7. `Complete` - -重试规则: - -- 最多重写 2 次 -- 达标则完成 -- 超限则进入人工介入 - -## 4.4 LangGraph 推荐节点 - -第一版推荐节点: - -- `load_input` -- `build_search_queries` -- `retrieve_materials` -- `normalize_materials` -- `generate_outline` -- `wait_for_outline_confirmation` -- `write_sections` -- `merge_sections` -- `review_draft` -- `rewrite_sections` -- `publish_final_artifact` - -其中和资料搜集直接相关的节点说明: - -### `build_search_queries` - -职责: - -- 根据用户主题和目标文章类型,生成 3-5 个检索 query - -### `retrieve_materials` - -职责: - -- 调用检索 provider 获取候选文档 -- 第一版默认走 `internet_search_client` -- 后续可切到 `intranet_search_client` - -### `normalize_materials` - -职责: - -- 做去重 -- 做摘要裁剪 -- 做来源字段统一 -- 输出标准 `RetrievedDocument[]` - ---- - -## 5. 前端最终做法 - -## 5.1 目录建议 - -在现有 `open-canvas` 下新增: - -```text -frontend-web/src/open-canvas/ - workflow/ - api/ - writing-workflow.ts - components/ - WritingWorkflowEntryForm.tsx - WritingWorkflowTimeline.tsx - WritingWorkflowEventList.tsx - WritingWorkflowArtifactDrawer.tsx - WritingWorkflowConfirmCard.tsx - WritingWorkflowHeader.tsx - CanvasWorkflowPanel.tsx - contexts/ - WritingWorkflowContext.tsx - hooks/ - useWritingWorkflow.ts - useWritingWorkflowEvents.ts - types/ - workflow.ts -``` - -## 5.2 左侧改造策略 - -`CanvasChatPanel` 只承担模式切换与容器职责: - -- `chat` 模式:沿用现有普通聊天 -- `writing_pipeline` 模式:渲染 `CanvasWorkflowPanel` - -新面板负责: - -- 写作任务表单 -- 进度时间线 -- 事件列表 -- 中间产物抽屉 -- 提纲确认卡片 - -## 5.3 右侧衔接策略 - -继续复用现有 `CanvasArtifactPanel`。 - -同步规则: - -- 提纲:可选同步到右侧预览 -- 初稿:同步到右侧 -- 重写稿:覆盖当前草稿 -- 终稿:作为最终 artifact - -## 5.4 前端状态设计 - -新增: - -- `workflowMode` -- `workflowRunId` -- `workflowStage` -- `workflowStatus` -- `workflowEvents` -- `workflowArtifacts` -- `workflowPendingAction` - -注意: - -- workflow 状态放进新的 `WritingWorkflowContext` -- 不塞进现有 `CanvasGraphContext` - -## 5.5 前端显示的资料信息 - -第一版左侧至少要能展示研究员产出的资料摘要: - -- 来源标题 -- 来源域名或 URL -- 资料摘要 -- 可信度标签 -- 是否被提纲或正文引用 - -建议在 `WritingWorkflowArtifactDrawer.tsx` 中增加“素材卡片”视图。 - -注意: - -- 第一版显示公开互联网来源即可 -- 后续接内网时,这个展示层不变,只是来源字段从 URL 变成内网文档标识或内部链接 - ---- - -## 6. 后端最终做法 - -## 6.1 分层 - -推荐分层: - -```text -api/ -application/ -domain/ -infrastructure/ -prompts/ -tests/ -``` - -## 6.2 核心模块 - -建议核心文件: - -```text -domain/ - models/ - workflow.py - workflow_stage.py - workflow_event.py - artifact.py - review_result.py - agents/ - researcher.py - outliner.py - writer.py - editor.py - supervisor.py - graphs/ - writing_graph.py - policies/ - retry_policy.py - quality_policy.py -``` - -## 6.3 检索层实现要求 - -第一版检索层必须独立成基础设施模块,不允许: - -- 在 Agent prompt 中直接拼接“去网上找资料” -- 在 graph 节点中散落请求第三方搜索的代码 -- 把互联网检索和内网检索混在一个无抽象的函数里 - -建议结构: - -```text -infrastructure/ - retrieval/ - base.py - internet_search_client.py - intranet_search_client.py - retriever_router.py - material_normalizer.py -``` - -建议职责: - -- `internet_search_client.py` - - 第一版实际使用 - - 对接互联网搜索 API 或爬取网关 - -- `intranet_search_client.py` - - 第一版只保留接口和空实现或 mock - - 后续接企业内部接口 - -- `retriever_router.py` - - 根据配置选择 provider - -- `material_normalizer.py` - - 统一清洗和标准化结果 - -## 6.4 Prompt 管理 - -统一独立存放: - -```text -prompts/ - researcher/ - system.txt - output_schema.json - outliner/ - system.txt - output_schema.json - writer/ - system.txt - output_schema.json - editor/ - system.txt - output_schema.json - supervisor/ - system.txt -``` - -## 6.5 输出约束 - -每个 Agent 都必须: - -- 优先输出 JSON / Pydantic schema -- 失败时允许轻量 repair 和一次重试 -- 避免自由散文式输出 - -## 6.6 Researcher 的实现细则 - -Researcher 推荐由两部分组成: - -1. 程序化检索 -2. 模型化整理 - -推荐伪流程: - -```python -def run_research(topic: str, article_type: str, provider: RetrievalProvider): - queries = build_queries(topic, article_type) - raw_docs = [] - for query in queries: - raw_docs.extend(provider.search(query=query, top_k=5)) - docs = normalize_materials(raw_docs) - docs = dedupe_materials(docs) - docs = rank_materials(docs) - return llm_extract_material_cards(docs) -``` - -第一版建议: - -- 先把 query 生成逻辑做简单 -- `top_k` 先控制在 5-10 -- 先做轻量去重和摘要裁剪 -- 不做很重的网页全文抓取 - ---- - -## 7. 前后端协议 - -## 7.1 API - -第一版统一提供: - -- `POST /api/ai-writing/workflows` -- `POST /api/ai-writing/workflows/{id}/start` -- `GET /api/ai-writing/workflows/{id}` -- `GET /api/ai-writing/workflows/{id}/events` -- `POST /api/ai-writing/workflows/{id}/confirm-outline` -- `POST /api/ai-writing/workflows/{id}/rewrite-section` -- `POST /api/ai-writing/workflows/{id}/approve-final` - -## 7.2 SSE 事件 - -统一事件类型: - -- `workflow_started` -- `stage_changed` -- `agent_started` -- `agent_output_chunk` -- `artifact_generated` -- `human_confirmation_required` -- `retry_scheduled` -- `workflow_completed` -- `workflow_failed` - -第一版前端不需要消费复杂嵌套事件,只要先把这 9 类打通。 - -## 7.3 配置项补充 - -建议增加以下配置项: - -```yaml -retrieval: - provider: internet - top_k: 8 - timeout_seconds: 15 - enable_content_fetch: false - -workflow: - max_revisions: 2 - -models: - researcher: your-default-model - outliner: your-default-model - writer: your-default-model - editor: your-default-model -``` - -说明: - -- 第一版 `provider=internet` -- 第二版可切成 `intranet` 或 `hybrid` - ---- - -## 8. 必须做 / 延后做 / 不建议现在做 - -## 8.1 必须做 - -### 架构 - -- 独立后端路线 `ai-writing-orchestrator` -- 前端独立 `workflow/` 目录 -- LangGraph 状态机 -- `Researcher / Outliner / Writer / Editor / Supervisor` -- 独立检索抽象层,第一版先落互联网 provider - -### 协议 - -- 标准 workflow 状态枚举 -- 标准 SSE 事件枚举 -- 标准 artifact 结构 -- JSON / Pydantic 输出约束 - -### 功能 - -- 启动写作任务 -- 研究阶段 -- 提纲阶段 -- 提纲确认 -- 初稿生成 -- 审稿与最多 2 次重写 -- 终稿同步到右侧 -- 素材卡片展示 -- 来源 URL、标题、摘要、可信度落库或挂在 artifact metadata - -### UI - -- 模式切换 -- 时间线 -- 事件列表 -- 提纲确认卡片 -- 右侧终稿显示 -- 左侧素材卡片视图 - -## 8.2 延后做 - -- 章节级精准重写 -- 初稿多版本树 -- FactChecker -- StyleEditor -- ComplianceReviewer -- 自动评分看板 -- 素材引用可视化高亮 -- 更复杂的人机协作节点 -- 内网知识库正式接入 -- 混合检索 `hybrid` -- 网页全文抓取与深摘要 - -## 8.3 不建议现在做 - -- 直接接入现有 DeerFlow 主 agent 包 -- 同时试 MetaGPT、CrewAI、AutoGen 多套底座 -- 一开始就做 5 个以上 Agent -- 一开始就做很复杂的文章版本管理 -- 一开始就把评分阈值和 prompt 写死得非常细 - -## 9. 最终开发顺序 - -## Step 1:先定契约 - -先一起拍板: - -- stage 枚举 -- event 枚举 -- artifact 结构 -- confirm / rewrite 的 payload -- retrieval provider 接口 -- RetrievedDocument 结构 - -## Step 2:先做 fake workflow - -后端先不接真实模型,只吐 mock SSE 事件,前端把左侧工作台跑通。 - -## Step 3:先接互联网版 Researcher + Outliner - -优先打磨“研究 + 提纲”,这是弱模型提升最大的部分。 - -这一阶段要求: - -- `Researcher` 先走互联网检索 -- `retriever_router` 可配置 -- `intranet_search_client` 先留接口空壳 - -## Step 4:接 Writer + Editor - -再补初稿与审稿重写闭环。 - -## Step 5:接右侧 artifact - -把初稿和终稿稳定写入右侧。 - -## Step 6:补生产化 - -- 缓存 -- 日志 -- 错误回放 -- 配置化 prompt - -## 10. 推荐任务拆分 - -## 前端 - -- `CanvasChatPanel` 模式切换 -- `workflow/` 目录与类型 -- 时间线 / 事件列表 / 确认卡片 -- SSE 订阅 hook -- 右侧 artifact 映射 -- 素材卡片列表展示 -- 来源字段展示规范 - -## 后端 - -- workflow API -- LangGraph graph -- 5 个 Agent 模块 -- Prompt 目录 -- SSE 输出 -- 仓储与日志 -- `RetrievalProvider` 抽象 -- `internet_search_client` -- `intranet_search_client` 预留接口 -- `material_normalizer` - -## Prompt / 算法 - -- Researcher prompt -- Outliner prompt -- Writer prompt -- Editor prompt -- JSON schema -- 失败样本回归 - -## 联调 - -- mock SSE -- 真模型接入 -- artifact 同步校验 -- 提纲确认链路验证 -- 互联网资料来源字段联调 -- 素材卡片到提纲引用链路联调 - ---- - -## 11. 开发过程中的约束 - -开发过程中统一采用下面这套口径: - -- 产品形态:当前 `open-canvas` 页面上的“写作工作流模式” -- 技术底座:`LangGraph` -- 核心模式:`Researcher -> Outliner -> Writer -> Editor -> Supervisor` -- 后端方式:独立 `ai-writing-orchestrator` -- 检索方式:第一版 `internet`,接口预留 `intranet` -- 通信方式:`SSE` -- 输出方式:`JSON Schema / Pydantic` -- 第一版目标:先做最小闭环,不追求角色数量和复杂度 - -给开发团队的统一口径: - -“我们在 `open-canvas` 上开发写作工作流模式,后端单开 `ai-writing-orchestrator`,第一版先用互联网资料检索,保留内网检索接入口,用 LangGraph 编排 Researcher、Outliner、Writer、Editor、Supervisor,把研究、提纲、初稿、审稿重写这条最小闭环做稳。” diff --git a/frontend-web/docs/ai-writing-frontend-refactor-checklist.md b/frontend-web/docs/ai-writing-frontend-refactor-checklist.md deleted file mode 100644 index b7119431..00000000 --- a/frontend-web/docs/ai-writing-frontend-refactor-checklist.md +++ /dev/null @@ -1,472 +0,0 @@ -# AI 写作前端改造清单 - -## 1. 目标 - -在不新增第二套页面的前提下,基于现有 `open-canvas` 页面,把左侧从“单轮聊天输入框”升级为“多 Agent 写作工作台”,右侧继续沿用当前 `artifact` 编辑与流式预览能力。 - -当前核心文件: - -- [CanvasResizableLayout.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/layout/CanvasResizableLayout.tsx) -- [CanvasChatPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/chat/CanvasChatPanel.tsx) -- [CanvasArtifactPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/artifact/CanvasArtifactPanel.tsx) -- [CanvasGraphContext.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/contexts/CanvasGraphContext.tsx) -- [CanvasStreamContext.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/contexts/CanvasStreamContext.tsx) -- [useCanvasThreadStream.ts](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/hooks/useCanvasThreadStream.ts) -- [runs.ts](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/api/runs.ts) - ---- - -## 2. 改造原则 - -- 不改右侧主编辑器交互心智 -- 不复制新页面 -- 不破坏现有普通 chat 模式 -- 新增“写作工作流模式”时尽量以增量状态字段和新组件完成 -- 所有 workflow 能力都应允许逐步灰度 - ---- - -## 3. 推荐前端结构 - -建议在 `frontend-web/src/open-canvas` 下新增以下目录: - -```text -frontend-web/src/open-canvas/ - workflow/ - api/ - writing-workflow.ts - components/ - WritingWorkflowEntryForm.tsx - WritingWorkflowTimeline.tsx - WritingWorkflowEventList.tsx - WritingWorkflowArtifactDrawer.tsx - WritingWorkflowConfirmCard.tsx - WritingWorkflowHeader.tsx - contexts/ - WritingWorkflowContext.tsx - hooks/ - useWritingWorkflow.ts - useWritingWorkflowEvents.ts - types/ - workflow.ts - utils/ - workflow-event.ts -``` - -说明: - -- `open-canvas` 原有聊天、artifact、editor 目录不重构 -- 以 `workflow/` 作为新能力边界,减少对既有文件的侵入 - ---- - -## 4. 状态设计 - -## 4.1 新增的核心状态 - -建议新增: - -- `workflowMode: "chat" | "writing_pipeline"` -- `workflowRunId: string | null` -- `workflowStage` -- `workflowStatus: "idle" | "running" | "waiting_user" | "completed" | "failed"` -- `workflowEvents: WritingWorkflowEvent[]` -- `workflowArtifacts: WritingWorkflowArtifact[]` -- `workflowPendingAction: WritingPendingAction | null` - -## 4.2 推荐类型定义 - -建议在 `workflow/types/workflow.ts` 中先定义: - -```ts -export type WritingWorkflowStage = - | "idle" - | "collecting_materials" - | "generating_outline" - | "await_outline_confirmation" - | "writing_draft" - | "reviewing_draft" - | "rewriting_draft" - | "completed" - | "failed"; - -export type WritingWorkflowEventType = - | "workflow_started" - | "stage_changed" - | "agent_started" - | "agent_output_chunk" - | "artifact_generated" - | "human_confirmation_required" - | "retry_scheduled" - | "workflow_completed" - | "workflow_failed"; -``` - -第一版把类型先收紧,后续再扩展。 - ---- - -## 5. 组件拆分建议 - -## 5.1 左侧主面板改造 - -当前 `CanvasChatPanel` 建议只承担“模式路由”和布局容器职责,不继续无限堆逻辑。 - -建议改造为: - -- `chat` 模式继续渲染现有消息列表 + 输入框 -- `writing_pipeline` 模式渲染新的工作流视图 - -建议拆分出: - -- `CanvasChatConversation.tsx` -- `CanvasChatInputBox.tsx` -- `CanvasWorkflowPanel.tsx` - -这样后续不会把 `CanvasChatPanel.tsx` 变成一个过大的组件。 - -## 5.2 新增组件职责 - -### `WritingWorkflowEntryForm.tsx` - -负责采集: - -- 主题 -- 文章类型 -- 目标读者 -- 语气风格 -- 目标篇幅 -- 是否需要引用资料 -- 禁写项 -- 补充说明 - -### `WritingWorkflowTimeline.tsx` - -负责显示阶段: - -- 研究中 -- 提纲生成中 -- 等待确认提纲 -- 正在写初稿 -- 正在审稿 -- 正在重写 -- 已完成 - -### `WritingWorkflowEventList.tsx` - -负责显示细粒度事件: - -- 哪个 Agent 开始执行 -- 当前在处理哪一步 -- 当前产生了什么中间结果 -- 是否重试 - -### `WritingWorkflowArtifactDrawer.tsx` - -负责展示中间产物: - -- 素材卡片 -- 提纲 -- 初稿 -- 编辑意见 -- 重写稿 - -### `WritingWorkflowConfirmCard.tsx` - -负责展示用户确认点: - -- 确认提纲 -- 驳回提纲并附带修改意见 -- 继续写稿 -- 指定重写章节 - ---- - -## 6. Context 与 Hook 改造建议 - -## 6.1 `CanvasGraphContext` - -当前它只维护: - -- `messages` -- `artifact` -- `streamingArtifact` -- `isStreaming` - -建议不要把 workflow 状态直接塞进现有 `CanvasGraphContext`,否则会让语义过重。 - -推荐做法: - -- 保留 `CanvasGraphContext` 继续服务右侧 artifact 和通用流式状态 -- 新增 `WritingWorkflowContext` - -## 6.2 `WritingWorkflowContext` - -建议承载: - -- 当前 workflow 运行状态 -- 中间产物列表 -- 待确认操作 -- workflow 事件缓存 -- 启动 / 取消 / 确认 / 重写等 action - -## 6.3 `useCanvasThreadStream` - -当前这个 hook 已经负责: - -- 创建 thread -- 提交消息 -- 接收 thread stream -- 将 `update_artifact` 同步到右侧 - -建议不要把“多 Agent workflow 状态机”也硬塞进去。 - -推荐方案: - -- 保留 `useCanvasThreadStream` 给普通 chat 或兼容旧能力使用 -- 新增 `useWritingWorkflow` -- 新增 `useWritingWorkflowEvents` - -分工如下: - -- `useCanvasThreadStream`:普通对话流 -- `useWritingWorkflow`:创建 workflow、提交表单、触发确认动作 -- `useWritingWorkflowEvents`:订阅 workflow 事件流 - ---- - -## 7. API 接入建议 - -## 7.1 前端 API 文件 - -新增: - -- `workflow/api/writing-workflow.ts` - -建议暴露方法: - -- `createWorkflow(payload)` -- `startWorkflow(id)` -- `getWorkflow(id)` -- `subscribeWorkflowEvents(id, handlers)` -- `confirmOutline(id, payload)` -- `rewriteSection(id, payload)` -- `approveFinal(id)` - -## 7.2 事件流形式 - -优先级建议: - -1. `SSE` -2. 流式 HTTP chunk -3. 轮询兜底 - -理由: - -- 事件驱动比整段消息流更适合 workflow UI -- 可以更方便映射到时间线和事件列表 - ---- - -## 8. 与右侧 Artifact 的衔接 - -## 8.1 现有优势 - -当前 `CanvasArtifactPanel` 已具备: - -- 流式展示内容 -- 最终 artifact 渲染 -- 右侧编辑体验成熟 - -这意味着多 Agent 工作流不需要再做第二套文章渲染,只要把最终阶段结果推成 artifact 即可。 - -## 8.2 建议接法 - -工作流各阶段结果按两类处理: - -- 中间产物:进入左侧 `workflowArtifacts` -- 可阅读正文:同步一份到右侧 `artifact` - -推荐映射: - -- 提纲:可选同步到右侧,供用户预览 -- 初稿:同步到右侧 -- 重写稿:覆盖或新增版本到右侧 -- 终稿:作为最终右侧主版本 - -## 8.3 版本策略 - -建议右侧版本保留: - -- `outline` -- `draft_v1` -- `draft_v2` -- `final` - -如果现有 artifact 数据结构不方便承载完整版本树,第一版可先保留在 workflowArtifacts 中,右侧只展示当前激活版本。 - ---- - -## 9. 页面交互流程 - -## 9.1 用户首次进入 - -- 默认仍是现有 chat 模式 -- 在左侧增加一个显眼切换入口:`普通对话` / `写作工作流` - -## 9.2 启动写作工作流 - -- 用户填写写作任务表单 -- 点击“开始生成” -- 左侧切换到时间线 + 事件流视图 -- 右侧进入 loading 或素材 / 提纲预览态 - -## 9.3 等待提纲确认 - -- 时间线停在 `await_outline_confirmation` -- 左侧出现确认卡片 -- 用户可执行: - - 通过提纲 - - 要求调整提纲 - - 追加要求 - -## 9.4 生成初稿与审稿 - -- 左侧显示 Agent 事件 -- 右侧展示初稿内容 -- 审稿意见在左侧展示,同时高亮需要重写的章节 - -## 9.5 完成 - -- 左侧显示“终稿已完成” -- 右侧进入可继续局部 AI 编辑状态 - ---- - -## 10. 分步开发清单 - -## Step 1:类型与假数据 - -目标: - -- 先把 workflow 的类型、mock 数据、组件骨架搭好 - -任务: - -- 新建 `workflow/types/workflow.ts` -- 新建 `workflow/components/*` -- 用本地 mock 事件驱动时间线和事件流 - -验收: - -- 不接后端也能在页面中演示完整流程 - -## Step 2:模式切换 - -目标: - -- 在 `CanvasChatPanel` 中接入 `chat / writing_pipeline` 双模式 - -任务: - -- 拆出 `CanvasWorkflowPanel` -- 增加模式切换 UI -- 保证旧 chat 不回归 - -验收: - -- 现有功能不受影响 -- 新模式可进入表单页 - -## Step 3:接入 workflow API - -目标: - -- 从 mock 切到真实 API - -任务: - -- 接 `create/start/get/events` -- 实现基础错误态和重试态 - -验收: - -- 前后端可联调跑一条主链路 - -## Step 4:接入确认节点 - -目标: - -- 支持提纲确认和指定章节重写 - -任务: - -- 做确认卡片 -- 做动作回传 - -验收: - -- 用户能在流程中介入 - -## Step 5:打通右侧版本 - -目标: - -- 让 workflow 产物稳定进入右侧编辑器 - -任务: - -- 约定 artifact 映射 -- 接入版本切换或当前版本覆盖 - -验收: - -- 终稿稳定显示在右侧 - ---- - -## 11. 建议修改点位 - -优先涉及文件: - -- [CanvasChatPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/chat/CanvasChatPanel.tsx) -- [CanvasGraphContext.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/contexts/CanvasGraphContext.tsx) -- [useCanvasThreadStream.ts](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/hooks/useCanvasThreadStream.ts) -- [CanvasArtifactPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/artifact/CanvasArtifactPanel.tsx) - -建议新增,不建议重写原有: - -- `open-canvas/workflow/api/writing-workflow.ts` -- `open-canvas/workflow/contexts/WritingWorkflowContext.tsx` -- `open-canvas/workflow/hooks/useWritingWorkflow.ts` -- `open-canvas/workflow/hooks/useWritingWorkflowEvents.ts` -- `open-canvas/workflow/components/*` - ---- - -## 12. 联调约定 - -前后端需要尽早统一: - -- workflow id 命名 -- stage 枚举 -- event type 枚举 -- artifact type 枚举 -- user action payload -- 错误码与可恢复动作 - -建议先由前端与后端共同确认一份 `workflow.ts` 契约文档,再开工。 - ---- - -## 13. 最终建议 - -前端最重要的不是“把 Agent 结果展示出来”,而是把“工作流过程可视化并允许人工介入”做清楚。 - -第一版建议先保证三件事: - -1. 左侧能看见阶段进展 -2. 用户能确认提纲 -3. 右侧能稳定收到初稿和终稿 - -只要这三件事稳定,后续再继续加章节重写、评分展示、素材卡片增强,风险会小很多。 diff --git a/frontend-web/docs/ai-writing-weak-model-multi-agent-prd.md b/frontend-web/docs/ai-writing-weak-model-multi-agent-prd.md deleted file mode 100644 index e2293119..00000000 --- a/frontend-web/docs/ai-writing-weak-model-multi-agent-prd.md +++ /dev/null @@ -1,718 +0,0 @@ -# AI 写作弱模型多 Agent 编排需求文档 - -## 1. 背景与目标 - -当前 `open-canvas` 页面右侧的成品展示与编辑体验已经比较成熟,但左侧模型直接生成文章的质量还不稳定。对于内网部署的弱性能大模型,单轮直出通常会出现以下问题: - -- 结构松散,长文容易跑题 -- 素材不足,内容空泛 -- 事实依据弱,容易幻觉 -- 润色能力有限,语言风格不统一 -- 一次生成失败后缺少可控的纠偏机制 - -因此本次需求的核心目标不是继续优化“单个模型一次写完”,而是在内网用代码编排一个“小型杂志社工作流”,让弱模型通过多角色分工协作,产出明显优于单模型直出的文章质量。 - -建议的角色分工: - -- `Agent A 研究员`:负责搜集内网素材、归纳事实、提取观点 -- `Agent B 作家`:基于选题、素材和结构写初稿 -- `Agent C 严苛编辑`:负责挑错、质检、润色、打回重写 -- `Supervisor / Orchestrator`:负责流程调度、状态记录、重试与终止判断 - ---- - -## 2. 本次建设边界 - -### 2.1 前端边界 - -前端继续使用现有 `open-canvas` 页面,不新开第二套编辑器页面。 - -当前相关入口: - -- [CanvasResizableLayout.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/layout/CanvasResizableLayout.tsx) -- [CanvasChatPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/chat/CanvasChatPanel.tsx) -- [CanvasArtifactPanel.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/components/artifact/CanvasArtifactPanel.tsx) -- [CanvasStreamContext.tsx](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/contexts/CanvasStreamContext.tsx) -- [runs.ts](/F:/react/deeflow-code/deerflow-server-master/frontend-web/src/open-canvas/api/runs.ts) - -前端本期只做以下增强: - -- 在左侧聊天区增加“写作工作流模式” -- 让用户能看到多 Agent 阶段进度,而不是只看到最终回答 -- 支持结构化输入:主题、体裁、目标读者、篇幅、语气、参考资料、禁写项 -- 支持查看中间产物:提纲、素材卡片、初稿、编辑意见、终稿 -- 支持人工介入:确认提纲、确认初稿、接受编辑建议、手动重跑某一步 - -### 2.2 后端边界 - -后端必须单开新路线,不影响其他同事当前功能开发。 - -建议原则: - -- 不直接改现有 DeerFlow 主流程 -- 不把多 Agent 逻辑硬塞进当前通用对话链路 -- 以独立服务或独立模块形式建设 -- 通过清晰 API 与当前前端页面对接 - -推荐落地位置: - -- 基于仓库中的 `offline-backend-20260512` 单独开一条 `ai-writing-orchestrator` 路线 -- 或在后端新建独立目录,例如 `offline-backend-20260512/apps/ai_writing_orchestrator` - ---- - -## 3. 为什么多 Agent 对弱模型更合适 - -弱模型最怕“一个 prompt 做完所有事”,因为这要求模型同时完成: - -- 选题理解 -- 信息检索 -- 信息筛选 -- 结构规划 -- 长文写作 -- 自我校验 -- 风格润色 - -把这些任务拆开后,每个 Agent 只处理一个更短、更清晰、更容易被弱模型完成的子任务,通常会带来以下收益: - -- 降低单次上下文复杂度 -- 降低一次性长输出失败率 -- 把“写得不好”变成“某一步做得不好”,便于重试 -- 可以在关键节点插入规则校验与人工确认 -- 可以用程序补足模型短板,而不是完全依赖模型智力 - -这套方案的本质不是“让模型变聪明”,而是“让系统更会组织模型工作”。 - ---- - -## 4. 可借鉴的开源项目 - -以下项目适合借鉴,但不建议整套照搬。建议以“借鉴状态机、角色协作、阶段拆分、重试与审稿机制”为主。 - -### 4.1 第一优先级:LangGraph - -推荐原因: - -- 天然适合把写作流程建模成状态图 -- 支持多步骤、条件分支、重试、人工介入 -- 便于把“研究 -> 提纲 -> 初稿 -> 评审 -> 重写”固化成可控流程 -- 对“弱模型 + 强流程控制”很友好 - -可借鉴点: - -- `StateGraph` 的状态驱动编排 -- 每一步单独定义输入、输出、失败处理 -- 可视化地表达“通过 / 打回 / 重试 / 终止” - -参考: - -- [langchain-ai/langgraph](https://github.com/langchain-ai/langgraph) -- [langgraph supervisor pattern](https://github.com/langchain-ai/langgraph-supervisor-py) - -### 4.2 第二优先级:STORM - -推荐原因: - -- 它本身就是“研究 + 提纲 + 成文 + 打磨”的长文系统 -- 对“先研究后写作”的思路非常契合 -- 很适合借鉴其“问题驱动研究”和“分阶段生成”的思想 - -根据其 GitHub README,STORM 将长文生成拆成两个大阶段:先研究并产出提纲,再基于提纲与资料写长文;其引擎进一步拆成知识整理、提纲生成、正文生成、文章润色四个模块。 - -这和我们的目标高度一致,尤其适合借鉴: - -- 提纲先行 -- 素材引用先行 -- 分模块写作 -- 后置润色 - -参考: - -- [stanford-oval/storm](https://github.com/stanford-oval/storm) - -### 4.3 第三优先级:Deep Research Agent - -推荐原因: - -- 明确支持本地模型与云模型混用 -- 已经包含研究、写作、质量校验、缓存、重试等能力 -- 很适合借鉴“研究阶段如何约束弱模型” - -根据其 README,这个项目采用四个专职 Agent,并且带有可信度评分、质量验证、重试逻辑、缓存和 checkpoint。 - -特别适合借鉴: - -- 素材缓存 -- 分阶段质量评分 -- 节点失败重试 -- 研究结果结构化存储 - -参考: - -- [tarun7r/deep-research-agent](https://github.com/tarun7r/deep-research-agent) - -### 4.4 可选借鉴:CrewAI - -推荐原因: - -- 角色扮演表达自然 -- 写“研究员 / 作家 / 编辑”这种协作关系很直观 -- Flow + Crew 的组织方式适合做 demo 和快速验证 - -不建议作为首选底座的原因: - -- 更偏“Agent 角色编排体验” -- 若要做企业内网稳定落地,状态管理和流程可控性通常不如 LangGraph 路线清晰 - -适合借鉴: - -- 角色定义写法 -- Task 模板 -- 多角色协作 prompt 结构 - -参考: - -- [crewAIInc/crewAI](https://github.com/crewAIInc/crewAI) - -### 4.5 不建议新项目优先采用:AutoGen - -原因: - -- 截至最近公开信息,`microsoft/autogen` 已进入 maintenance mode -- 官方建议新项目优先采用 `Microsoft Agent Framework` - -如果要借鉴,只建议借鉴早期多 Agent 对话模式,不建议作为本项目主框架。 - -参考: - -- [microsoft/autogen](https://github.com/microsoft/autogen) -- [microsoft/agent-framework](https://github.com/microsoft/agent-framework) - -### 4.6 结论 - -综合本项目需求,建议技术选型优先级如下: - -1. `LangGraph` 作为主编排框架 -2. `STORM` 借鉴研究-提纲-成文-润色的模块划分 -3. `Deep Research Agent` 借鉴缓存、评分、重试、可观测性 -4. `CrewAI` 借鉴角色 prompt 设计与任务模板 - ---- - -## 5. 目标产品形态 - -用户在当前 `open-canvas` 页面中,不再只是“向模型提问写文章”,而是启动一个可视化的写作流水线。 - -理想交互流程: - -1. 用户输入主题、用途、风格、读者、篇幅要求 -2. 系统先进入“研究阶段” -3. 系统给出资料卡片与候选提纲 -4. 用户确认提纲或调整 -5. 系统生成初稿 -6. 严苛编辑给出问题清单 -7. 系统自动重写问题段落 -8. 系统输出终稿到右侧编辑区 -9. 用户可继续局部改写、扩写、精简 - -核心体验目标: - -- 左侧像“编辑部工作台” -- 右侧像“成文编辑器” -- 用户知道系统现在进行到哪一步 -- 用户可以在关键节点接管,而不是只能等待最终结果 - ---- - -## 6. 多 Agent 流程设计 - -## 6.1 建议的最小可用流程 MVP - -第一版不要一上来做很复杂,建议先做 4 步闭环: - -1. `Research` - - 输入:主题、用户要求、内网资料源 - - 输出:素材卡片列表、可引用事实列表、风险点列表 - -2. `Outline` - - 输入:用户要求 + 素材卡片 - - 输出:结构化提纲 - -3. `Draft` - - 输入:提纲 + 素材卡片 - - 输出:初稿 - -4. `ReviewAndRewrite` - - 输入:初稿 + 用户要求 + 素材卡片 - - 输出:问题清单、改写稿、终稿 - -MVP 终止条件: - -- 编辑评分达标则结束 -- 不达标最多重写 2 次 -- 超过次数则返回“需要人工介入” - -## 6.2 第二阶段增强流程 - -在 MVP 跑通后,再增加: - -- `Planner`:根据文章类型动态规划流程 -- `SectionWriter`:按章节分别写作,避免长文崩坏 -- `FactChecker`:逐段核对素材来源 -- `StyleEditor`:统一语气、标题风格、句式节奏 -- `ComplianceReviewer`:内网场景下做敏感词、保密、术语规范检查 - -## 6.3 建议的状态机 - -建议将流程状态固定为: - -- `idle` -- `collecting_materials` -- `generating_outline` -- `await_outline_confirmation` -- `writing_draft` -- `reviewing_draft` -- `rewriting_draft` -- `completed` -- `failed` -- `human_intervention_required` - -这样前端和后端都可以围绕统一状态通信。 - ---- - -## 7. Agent 角色定义建议 - -## 7.1 Agent A 研究员 - -职责: - -- 搜集内网知识库、文档库、FAQ、历史稿件中的相关信息 -- 输出结构化素材,而不是长篇自然语言 -- 对每条素材标注来源、可信度、适用角度 - -输出格式建议: - -- `facts[]` -- `quotes[]` -- `examples[]` -- `risks[]` -- `source_refs[]` - -约束建议: - -- 禁止直接生成整篇文章 -- 不做风格化表达 -- 每条结论必须可回溯到来源 - -## 7.2 Agent B 作家 - -职责: - -- 严格基于提纲和素材写稿 -- 按章节写,而不是一次写完整篇 -- 对每段附带素材引用 ID,便于后续审核 - -约束建议: - -- 无素材不发挥 -- 每段只服务一个小节目标 -- 段落长度控制 -- 输出结构化草稿数据,而不仅是纯文本 - -## 7.3 Agent C 严苛编辑 - -职责: - -- 检查逻辑跳跃、素材不足、事实不稳、表述空泛、风格不统一 -- 输出明确的“问题清单”和“改写指令” -- 必要时打回指定章节,而不是整篇推翻 - -输出格式建议: - -- `issues[]` -- `severity` -- `rewrite_instructions[]` -- `accept/reject` - -编辑原则建议: - -- 优先挑事实问题,再挑结构问题,最后才是文风问题 -- 问题描述必须可执行,不能只说“写得不够好” - -## 7.4 Supervisor - -职责: - -- 负责流程启动、状态流转、失败重试、记录日志 -- 判断是否需要回到上一步 -- 控制最大重试次数 -- 对外提供统一 API 与流式事件 - ---- - -## 8. 前端需求 - -## 8.1 保持当前页面结构不变 - -继续沿用当前左右布局: - -- 左侧:聊天 + 流程进度 + 中间产物 -- 右侧:最终文章编辑与局部 AI 改写 - -不建议再复制一套新页面,以免造成维护分叉。 - -## 8.2 左侧新增 UI 模块 - -建议在 `CanvasChatPanel` 基础上新增: - -- 写作任务表单 -- 工作流阶段时间线 -- Agent 执行日志面板 -- 中间产物抽屉 -- 人工确认卡片 - -建议新增的前端概念: - -- `workflowMode`: `chat | writing_pipeline` -- `workflowStage` -- `workflowRunId` -- `workflowEvents[]` -- `workflowArtifacts[]` - -## 8.3 需要展示的中间产物 - -至少支持展示: - -- 素材卡片 -- 提纲 -- 初稿 -- 编辑问题单 -- 重写结果 -- 最终定稿 - -这些内容建议不仅显示在消息区,也保存到右侧可选版本中,便于回看。 - -## 8.4 人工介入节点 - -至少提供两个确认点: - -- 提纲确认 -- 初稿确认或编辑后重跑 - -后续可扩展: - -- 指定重写某个章节 -- 锁定某段不允许再改 -- 替换某条素材后重新生成 - ---- - -## 9. 后端需求 - -## 9.1 架构原则 - -后端新路线必须满足: - -- 与现有通用对话链路解耦 -- 与现有 Agent 运行时解耦 -- 独立维护 Prompt、状态机、数据结构 -- 可以单独部署、单独回滚、单独压测 - -## 9.2 推荐模块划分 - -建议后端拆分为以下模块: - -- `workflow_orchestrator` -- `agent_registry` -- `material_retriever` -- `outline_generator` -- `draft_writer` -- `editor_reviewer` -- `rewrite_engine` -- `workflow_store` -- `stream_event_adapter` - -## 9.3 API 建议 - -建议最小 API: - -- `POST /api/ai-writing/workflows` - - 创建写作任务 - -- `POST /api/ai-writing/workflows/{id}/start` - - 启动流程 - -- `GET /api/ai-writing/workflows/{id}` - - 查询当前状态 - -- `GET /api/ai-writing/workflows/{id}/events` - - 获取事件流或轮询事件 - -- `POST /api/ai-writing/workflows/{id}/confirm-outline` - - 用户确认提纲 - -- `POST /api/ai-writing/workflows/{id}/rewrite-section` - - 指定章节重写 - -- `POST /api/ai-writing/workflows/{id}/approve-final` - - 确认终稿 - -## 9.4 流式事件建议 - -为了与当前 `CanvasStreamContext` 风格兼容,建议后端输出结构化事件流,例如: - -- `workflow_started` -- `stage_changed` -- `agent_started` -- `agent_output_chunk` -- `artifact_generated` -- `human_confirmation_required` -- `retry_scheduled` -- `workflow_completed` -- `workflow_failed` - -前端只要订阅事件,就能把左侧工作台做出来。 - ---- - -## 10. 弱模型优化策略 - -这是本项目成功的关键,不能只靠“多 Agent”这四个字。 - -## 10.1 强约束输出格式 - -每个 Agent 尽量输出 JSON 或结构化对象,不要自由发挥。 - -原因: - -- 便于程序消费 -- 便于后续 Agent 接续 -- 便于失败重试 -- 便于质量评估 - -## 10.2 长文拆成章节生成 - -不要让弱模型一次生成整篇长文,建议: - -- 先出提纲 -- 再按章节逐段写 -- 最后统一润色 - -## 10.3 把检索和写作分离 - -研究员只负责找资料,作家只负责组织表达。不要让写作 Agent 同时去“边搜边编”。 - -## 10.4 程序兜底而不是模型兜底 - -程序需要承担: - -- 去重 -- 引用格式化 -- 章节拼接 -- 长度控制 -- 评分阈值判断 -- 重试路由 - -## 10.5 小步快跑、失败可回退 - -每步都要能独立失败、独立重试、独立回放。 - -## 10.6 引入可量化评分 - -编辑 Agent 输出不能只是一段文字,建议给出评分维度: - -- 相关性 -- 事实性 -- 结构完整度 -- 语言流畅度 -- 风格一致性 - -系统据此决定是否进入下一步。 - ---- - -## 11. 分阶段实施计划 - -## Phase 1:需求冻结与技术验证 - -目标: - -- 确认文章类型范围 -- 确认内网资料来源 -- 选定主框架 -- 跑通最小多 Agent demo - -交付物: - -- 技术选型说明 -- 角色与状态机设计 -- 1 个主题的端到端 demo - -验收标准: - -- 弱模型可完成“研究 -> 提纲 -> 初稿 -> 编辑 -> 重写”闭环 - -## Phase 2:后端新路线搭建 - -目标: - -- 建立独立 orchestrator -- 建立 workflow state store -- 建立事件流输出 -- 建立可配置 prompt 模板 - -交付物: - -- 独立服务目录 -- 基础 API -- 状态机运行骨架 - -验收标准: - -- 不依赖现有主流程改造即可独立跑通 - -## Phase 3:前端接入当前页面 - -目标: - -- 在 `CanvasChatPanel` 接入工作流模式 -- 增加阶段展示与人工确认 -- 中间产物可视化 - -交付物: - -- 左侧工作流时间线 -- 中间产物展示面板 -- 流程控制按钮 - -验收标准: - -- 用户可以在当前页面完成一次全流程写作 - -## Phase 4:质量优化 - -目标: - -- 强化素材检索质量 -- 提高提纲质量 -- 引入章节级重写 -- 引入评分与回退机制 - -交付物: - -- 质量评估规则 -- 重试策略 -- Prompt 调优包 - -验收标准: - -- 弱模型效果明显优于单轮直出 - -## Phase 5:生产化 - -目标: - -- 加缓存 -- 加日志 -- 加监控 -- 加失败告警 -- 加审计记录 - -交付物: - -- 可观测性方案 -- 运行日志与回放 -- 配置中心或 prompt 管理机制 - -验收标准: - -- 可长期维护并可定位问题 - ---- - -## 12. 团队协作与隔离方案 - -为了不影响其他同事,建议采用以下协作原则: - -- 新后端目录独立开发 -- 新接口使用独立路由前缀 -- 前端只在 `open-canvas` 内增量开发 -- 不修改现有公共线程协议,必要时增加新事件类型 -- Prompt、Agent 配置、评分规则独立存放 - -推荐分工: - -- 前端 1 人:负责左侧工作流 UI 与右侧版本接入 -- 后端 1-2 人:负责 orchestrator、Agent 执行、事件流 -- 算法/Prompt 1 人:负责角色 prompt、评分规则、失败样本调优 - ---- - -## 13. 风险与规避 - -### 风险 1:Agent 太多,成本和耗时暴涨 - -规避: - -- MVP 只保留 3 个业务 Agent + 1 个 Supervisor -- 长文按章节写,失败只重写局部 - -### 风险 2:弱模型在自由对话场景中失控 - -规避: - -- 全流程结构化 -- 每步只做一件事 -- 强约束输入输出格式 - -### 风险 3:内网检索质量不足,导致垃圾进垃圾出 - -规避: - -- 先把资料源治理好 -- 对素材做来源、可信度、去重处理 - -### 风险 4:前后端协议频繁变化影响联调 - -规避: - -- 先定义事件模型和 artifact 模型 -- 先 mock 数据,再并行开发 - -### 风险 5:影响现有功能稳定性 - -规避: - -- 新路线、新接口、新状态机 -- 不侵入现有主链路 - ---- - -## 14. 推荐结论 - -本项目推荐的总体策略: - -1. 前端继续基于当前 `open-canvas` 页面开发,不复制新页面 -2. 后端单独开 `ai-writing-orchestrator` 新路线,避免影响现有同事 -3. 主框架优先采用 `LangGraph` -4. 流程设计借鉴 `STORM` -5. 质量控制借鉴 `Deep Research Agent` -6. 角色设计与任务模板可参考 `CrewAI` -7. 第一版只做 4 步 MVP:研究、提纲、初稿、编辑重写 - -一句话总结: - -不是继续“调一个更强的 prompt”,而是把弱模型放进一个可控的编辑部流水线,让系统帮它把复杂写作拆开完成。 - ---- - -## 15. 参考链接 - -- [LangGraph](https://github.com/langchain-ai/langgraph) -- [LangGraph Supervisor](https://github.com/langchain-ai/langgraph-supervisor-py) -- [STORM](https://github.com/stanford-oval/storm) -- [Deep Research Agent](https://github.com/tarun7r/deep-research-agent) -- [CrewAI](https://github.com/crewAIInc/crewAI) -- [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) -- [AutoGen](https://github.com/microsoft/autogen) diff --git a/frontend-web/docs/aixiezuo.md b/frontend-web/docs/aixiezuo.md deleted file mode 100644 index 52048f12..00000000 --- a/frontend-web/docs/aixiezuo.md +++ /dev/null @@ -1,1896 +0,0 @@ -# AI 智能写作 —— 杂志社多 Agent 协作系统(含用户干预 & 详细进度) - -> **目标**:通过角色扮演 + 流程编排,让性能较弱的本地大模型产出高质量文章; -> 同时在每个关键节点暂停等待用户确认,确保最终结果符合预期。 - ---- - -## 目录 - -1. [整体架构](#1-整体架构) -2. [用户干预节点设计(核心)](#2-用户干预节点设计核心) -3. [开源参考项目](#3-开源参考项目) -4. [弱模型增强策略](#4-弱模型增强策略) -5. [三个 Agent 的角色设计](#5-三个-agent-的角色设计) -6. [数据结构定义](#6-数据结构定义) -7. [后端实现(分步)](#7-后端实现分步) -8. [前端实现(分步)](#8-前端实现分步) -9. [内网接口预留设计](#9-内网接口预留设计) -10. [LangGraph 状态机流转图](#10-langgraph-状态机流转图) -11. [提示词模板(弱模型专用)](#11-提示词模板弱模型专用) -12. [SSE 协议 & 干预协议设计](#12-sse-协议--干预协议设计) -13. [前端 UI 组件详细设计](#13-前端-ui-组件详细设计) -14. [文件目录结构](#14-文件目录结构) -15. [分步开发检查清单](#15-分步开发检查清单) - ---- - -## 1. 整体架构 - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ 前端(现有页面) │ -│ frontend-web/src/open-canvas/ │ -│ │ -│ ┌───────────────────────────┐ ┌──────────────────────────────────┐ │ -│ │ AI写作左侧面板 │ │ CanvasArtifactPanel(右侧) │ │ -│ │ AIWritingPanel.tsx │ │ (右侧保持不变,实时展示文章草稿) │ │ -│ │ ┌─────────────────────┐ │ │ │ │ -│ │ │ 1. 写作意图输入区 │ │ │ ┌────────────────────────────┐ │ │ -│ │ ├─────────────────────┤ │ │ │ 实时文章草稿(Markdown) │ │ │ -│ │ │ 2. 进度时间线 │ │ │ │ 每次修改都保留版本历史 │ │ │ -│ │ │ ┌───────────────┐ │ │ │ └────────────────────────────┘ │ │ -│ │ │ │ ✅ 研究院完成 │ │ │ │ │ │ -│ │ │ │ 素材卡片列表▼ │ │ │ │ │ │ -│ │ │ │ [可收起展开] │ │ │ │ │ │ -│ │ │ ├───────────────┤ │ │ │ │ │ -│ │ │ │ ⏸ 等待确认 │ │ │ │ │ │ -│ │ │ │ [用户干预卡片]│ │ │ │ │ │ -│ │ │ ├───────────────┤ │ │ │ │ │ -│ │ │ │ ✅ 大纲已生成 │ │ │ │ │ │ -│ │ │ │ 大纲预览▼ │ │ │ │ │ │ -│ │ │ │ [可收起展开] │ │ │ │ │ │ -│ │ │ ├───────────────┤ │ │ │ │ │ -│ │ │ │ ⏸ 等待确认 │ │ │ │ │ │ -│ │ │ └───────────────┘ │ │ │ │ │ -│ └───────────────────────────┘ └──────────────────────────────────┘ │ -└───────────────────────────────┬──────────────────────────────────────┘ - │ SSE 流式推送(进度/干预/草稿) - │ REST 干预响应(用户决策提交) - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ 后端新路由(独立,URL 前缀 /api/ai-writing/) │ -│ app/gateway/routers/ai_writing.py │ -│ POST /api/ai-writing/start ← 启动写作流程,返回 session_id │ -│ GET /api/ai-writing/{id}/stream ← SSE 流(进度/干预/草稿事件) │ -│ POST /api/ai-writing/{id}/resume ← 用户干预响应,恢复流程 │ -│ GET /api/ai-writing/{id}/state ← 查询当前流程状态 │ -└───────────────────────────────┬──────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ AI Writing LangGraph Graph(独立于原有 Graph) │ -│ packages/harness/deerflow/agents/ai_writing/graph.py │ -│ │ -│ intent_parser → researcher → [PAUSE:材料确认] → writer(outline) │ -│ → [PAUSE:大纲确认] → writer(draft) → editor │ -│ → [PAUSE:终稿确认] → END │ -│ ↑__________________________|(打回重写,最多3次) │ -└──────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 用户干预节点设计(核心) - -整个流程共有 **4 个用户干预点**,每个干预点都会暂停流程,向前端推送 `await_user` 事件,等待用户操作后再恢复。 - -### 干预点总览 - -``` -流程步骤 干预点 用户可执行的操作 -───────────────────────── ────── ─────────────────────────────────────── -① 研究院搜集素材完成 PAUSE-1 确认素材 / 补充关键词重搜 / 手动删除素材 -② 作家生成大纲完成 PAUSE-2 确认大纲 / 编辑大纲内容 / 要求重新规划 -③ 作家完成初稿(每次修改) PAUSE-3 确认定稿 / 给出修改意见 / 直接编辑文本 -④ 编辑审核完成(打回时) PAUSE-4 采纳编辑意见 / 忽略编辑意见强制定稿 -``` - -### PAUSE-1:素材确认 - -**触发**:研究院搜集完素材后暂停 - -前端展示: -- 素材卡片列表(默认收起,点击展开) -- 每张卡片:标题、来源、时间、相关度色条、正文摘要(可展开) -- 每张卡片有勾选框,支持删除不相关素材 -- "补充关键词"输入框,可追加搜索 - -用户操作选项: -``` -[确认素材,开始写大纲] ← 主操作,使用当前勾选素材继续 -[重新搜索] ← 清空素材重新来 -[补充搜索: ___________] ← 追加关键词,合并到现有素材 -``` - -### PAUSE-2:大纲确认 - -**触发**:作家生成大纲后暂停 - -前端展示: -- 大纲树形视图(默认展开) -- 每个章节可直接点击编辑标题和要点 -- 可拖拽调整章节顺序(可选) - -用户操作选项: -``` -[确认大纲,开始写作] ← 使用当前大纲(含用户修改)继续 -[重新规划大纲] ← 清空大纲重新生成,可附加意见: ___ -``` - -### PAUSE-3:草稿确认 - -**触发**:每次作家完成草稿后暂停(包括修改稿) - -前端展示: -- 右侧实时显示完整草稿 -- 左侧显示"编辑打分 + 问题清单"(如已经过编辑) -- 版本切换导航(第1稿 / 第2稿 …) - -用户操作选项: -``` -[确认定稿] ← 以此为最终文章 -[给出修改意见: ____________] ← 用户自己写修改意见,绕过编辑流程直接指导重写 -[让编辑继续审核] ← 走编辑流程(默认) -``` - -### PAUSE-4:编辑审核结果确认(仅在打回时) - -**触发**:编辑判断"打回"且未达到最大循环次数 - -前端展示: -- 编辑评分 + 分项得分(事实/逻辑/语言) -- 问题清单(按严重程度排序) -- 每个问题条目:位置 + 问题描述 + 修改建议 - -用户操作选项: -``` -[采纳编辑意见,让作家重写] ← 走编辑反馈重写 -[强制定稿(忽略编辑意见)] ← 用当前草稿结束 -[补充修改指令: ____________] ← 用户自己的意见覆盖编辑意见 -``` - ---- - -## 3. 开源参考项目 - -### Stanford STORM(最核心参考) -- **项目**:https://github.com/stanford-oval/storm -- **借鉴**:Researcher + Writer + Editor 完整协作流程;素材 → 大纲 → 分段写作的两步写法;短 prompt + JSON 输出模式(弱模型友好) - -### GPT-Researcher -- **项目**:https://github.com/assafelovic/gpt-researcher -- **借鉴**:统一搜索接口抽象(`context/retriever.py`);素材字段定义(标题/来源/时间/相关度) - -### CrewAI -- **项目**:https://github.com/crewAIInc/crewAI -- **借鉴**:`role + goal + backstory` 三段式角色锁定 Prompt;`expected_output` 强制 JSON 输出;任务链传参方式 - -### LangGraph Human-in-the-loop(项目已有,重点参考) -- **文档**:https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/ -- **借鉴**:`interrupt()` 实现流程暂停等待用户输入;`Command(resume=...)` 恢复流程;`MemorySaver` checkpointing 持久化状态 - ---- - -## 4. 弱模型增强策略 - -| 策略 | 实现方式 | 效果 | -|------|----------|------| -| **任务最小化** | 每次 LLM 调用只做一件事,禁止混合任务 | 减少跑偏概率 | -| **角色锁定** | System Prompt 开头:「你是X,你只做Y,不做其他」 | 防止角色混淆 | -| **强制 JSON** | `response_format=json_object` + Prompt 末尾提供 JSON 模板 | 输出可解析,有 fallback | -| **上下文压缩** | 传给下游的素材截断到 200 字摘要,不传原文 | 防止 context 溢出 | -| **两步写作** | 先写大纲(JSON),确认后再逐段写正文 | 防止弱模型一次生成长文失控 | -| **分项审查** | 编辑三项独立打分(事实/逻辑/语言),不混在一起 | 每次调用任务极简 | -| **Few-Shot 示例** | 每个 Agent 的 Prompt 内置 1 个期望输出的完整 JSON 示例 | 模型直接模仿,输出更稳定 | -| **迭代上限保护** | 编辑打回最多 3 次,超出强制定稿 | 防死循环 | -| **用户干预兜底** | 每个关键节点人工确认 | 彻底避免方向跑偏 | - ---- - -## 5. 三个 Agent 的角色设计 - -### Agent A —— 研究院(Researcher) - -``` -角色:专业情报研究员 -单次调用任务:① 只拆关键词 ② 只排序素材(两次独立调用) -输入:用户写作意图 -输出:MaterialPackage(结构化素材包) -工具:web_search(当前) / intranet_search(预留) -``` - -**两步拆分(弱模型关键)**: -- Step 1:`extract_keywords(intent)` → `{"keywords": [...]}`(仅此一件事) -- Step 2:对每个关键词独立搜索(并发),合并去重,截断到 500 字 - -**输出字段**: -```json -{ - "keywords": ["关键词1", "关键词2"], - "materials": [ - { - "id": "m001", - "title": "文章标题", - "content": "正文摘要(最多500字)", - "source": "来源域名或系统名称", - "published_at": "2024-01-15", - "relevance_score": 0.92, - "source_type": "internet", - "url": "https://...", - "recUuid": "", - } - ], - "summary": "共搜索到X条素材,精选Y条" -} -``` - -### Agent B —— 作家(Writer) - -``` -角色:专业内容创作者 -Phase 1:write_outline → 输出大纲 JSON(暂停等用户确认) -Phase 2:write_draft → 按段落逐段写正文(逐段 SSE 推送) -输入:MaterialPackage + 用户意图 + 确认后的大纲 - (修改稿时额外附加:编辑意见 or 用户修改意见) -输出:DraftArticle(含 Markdown 全文) -``` - -### Agent C —— 编辑(Editor) - -``` -角色:资深主编,标准极高 -任务:三项独立审查 → 综合评分 → 通过/打回决定 - 每项审查是独立的 LLM 调用(弱模型关键) -输入:DraftArticle + MaterialPackage(用于核实事实) -输出:ReviewResult(评分 + 分项问题清单) -``` - -**三次独立 LLM 调用**: -1. `review_facts(draft, materials)` → `{"score": 85, "issues": [...]}` -2. `review_logic(draft)` → `{"score": 80, "issues": [...]}` -3. `review_language(draft)` → `{"score": 90, "issues": [...]}` - ---- - -## 6. 数据结构定义 - -### 6.1 后端 LangGraph State(Python) - -**文件**:`packages/harness/deerflow/agents/ai_writing/state.py` - -```python -from typing import TypedDict, List, Optional, Annotated, Literal -import operator - -# ── 素材 ────────────────────────────────────────────────────────────── -class Material(TypedDict): - id: str - title: str - content: str # 截断到 500 字 - source: str - published_at: str - relevance_score: float - source_type: str # "internet" | "intranet" - url: str - recUuid: str # 知识库ID - -class MaterialPackage(TypedDict): - keywords: List[str] - materials: List[Material] - summary: str - -# ── 大纲 ────────────────────────────────────────────────────────────── -class OutlineSection(TypedDict): - section_title: str - key_points: List[str] - material_ids: List[str] - -class Outline(TypedDict): - title: str - sections: List[OutlineSection] - -# ── 草稿 ────────────────────────────────────────────────────────────── -class DraftArticle(TypedDict): - title: str - outline: Outline - full_markdown: str - word_count: int - revision_count: int - -# ── 编辑审核 ────────────────────────────────────────────────────────── -class ReviewIssue(TypedDict): - category: str # "fact" | "logic" | "language" - severity: str # "major" | "minor" - location: str - issue: str - suggestion: str - -class ReviewResult(TypedDict): - verdict: str # "pass" | "reject" - fact_score: int - logic_score: int - language_score: int - overall_score: int - pass_threshold: int - revision_notes: List[ReviewIssue] - -# ── 用户干预 ────────────────────────────────────────────────────────── -class UserInterventionPayload(TypedDict): - """用户在干预点提交的数据""" - action: str # 见各干预点定义 - # PAUSE-1 素材确认 - approved_material_ids: List[str] # 用户勾选保留的素材 ID - extra_keywords: List[str] # 补充搜索关键词 - # PAUSE-2 大纲确认 - edited_outline: Optional[Outline] # 用户编辑后的大纲(None 表示未修改) - outline_feedback: str # 要求重新规划的意见 - # PAUSE-3 草稿确认 - user_revision_notes: str # 用户自己的修改意见 - # PAUSE-4 编辑审核确认 - override_verdict: bool # True = 强制定稿,False = 采纳编辑意见 - -# ── 主状态 ──────────────────────────────────────────────────────────── -WritingStatus = Literal[ - "researching", "awaiting_material_confirm", - "writing_outline", "awaiting_outline_confirm", - "writing_draft", "awaiting_draft_confirm", - "reviewing", "awaiting_review_confirm", - "revising", "done", "error" -] - -class AIWritingState(TypedDict): - # 写作配置 - user_intent: str - article_type: str - target_audience: str - word_count_target: int - - # Agent A 输出 - material_package: Optional[MaterialPackage] - - # Agent B 输出 - current_outline: Optional[Outline] - drafts: Annotated[List[DraftArticle], operator.add] - current_draft: Optional[DraftArticle] - - # Agent C 输出 - review_result: Optional[ReviewResult] - - # 用户干预数据(每个干预点会覆盖) - last_user_intervention: Optional[UserInterventionPayload] - - # 流程控制 - revision_count: int - max_revisions: int # 默认 3 - status: WritingStatus - - # SSE 进度推送队列(每个节点追加) - progress_events: Annotated[List[dict], operator.add] -``` - -### 6.2 前端类型(TypeScript) - -**文件**:`frontend-web/src/open-canvas/types/ai-writing.ts` - -```typescript -// ── 素材 ───────────────────────────────────────────────────────────── -export interface AIMaterial { - id: string; - title: string; - content: string; - source: string; - publishedAt: string; - relevanceScore: number; - sourceType: 'internet' | 'intranet'; - url?: string; - docId?: string; - department?: string; - classification?: string; -} - -// ── 大纲 ───────────────────────────────────────────────────────────── -export interface OutlineSection { - sectionTitle: string; - keyPoints: string[]; - materialIds: string[]; -} - -export interface AIOutline { - title: string; - sections: OutlineSection[]; -} - -// ── 编辑审核 ────────────────────────────────────────────────────────── -export interface ReviewIssue { - category: 'fact' | 'logic' | 'language'; - severity: 'major' | 'minor'; - location: string; - issue: string; - suggestion: string; -} - -export interface ReviewResult { - verdict: 'pass' | 'reject'; - factScore: number; - logicScore: number; - languageScore: number; - overallScore: number; - passThreshold: number; - revisionNotes: ReviewIssue[]; -} - -// ── 写作状态 ────────────────────────────────────────────────────────── -export type WritingStatus = - | 'idle' - | 'researching' - | 'awaiting_material_confirm' - | 'writing_outline' - | 'awaiting_outline_confirm' - | 'writing_draft' - | 'awaiting_draft_confirm' - | 'reviewing' - | 'awaiting_review_confirm' - | 'revising' - | 'done' - | 'error'; - -// ── SSE 进度事件 ────────────────────────────────────────────────────── -export type ProgressEventType = - | 'step_start' // Agent 开始执行某步骤 - | 'step_done' // Agent 完成某步骤 - | 'materials_ready' // 素材搜集完毕 - | 'outline_ready' // 大纲生成完毕 - | 'draft_chunk' // 草稿文字流式片段(逐段) - | 'draft_ready' // 草稿完整版本 - | 'review_ready' // 编辑审核完毕 - | 'await_user' // 暂停,等待用户干预 - | 'resumed' // 用户干预已提交,流程恢复 - | 'done' // 全部完成 - | 'error'; // 出错 - -export interface ProgressEvent { - type: ProgressEventType; - sessionId: string; - agentName?: string; // '研究院' | '作家' | '编辑' | '系统' - message: string; - detail?: string; - // type===materials_ready 时 - materials?: AIMaterial[]; - keywords?: string[]; - materialSummary?: string; - // type===outline_ready 时 - outline?: AIOutline; - // type===draft_chunk / draft_ready 时 - draftMarkdown?: string; - draftTitle?: string; - revisionCount?: number; - // type===review_ready 时 - reviewResult?: ReviewResult; - // type===await_user 时 - pausePoint?: 'material_confirm' | 'outline_confirm' | 'draft_confirm' | 'review_confirm'; - pauseContext?: Record; // 干预点所需的上下文数据 -} - -// ── 用户干预提交体 ──────────────────────────────────────────────────── -export interface UserInterventionPayload { - action: string; - // PAUSE-1 - approvedMaterialIds?: string[]; - extraKeywords?: string[]; - // PAUSE-2 - editedOutline?: AIOutline; - outlineFeedback?: string; - // PAUSE-3 - userRevisionNotes?: string; - // PAUSE-4 - overrideVerdict?: boolean; -} - -// ── 写作请求 ────────────────────────────────────────────────────────── -export interface AIWritingRequest { - userIntent: string; - articleType: 'news' | 'analysis' | 'feature' | 'report'; - targetAudience: string; - wordCountTarget: number; - extraContext?: string; -} - -// ── Hook 返回值 ─────────────────────────────────────────────────────── -export interface AIWritingState { - sessionId: string | null; - status: WritingStatus; - progressEvents: ProgressEvent[]; - materials: AIMaterial[]; - outline: AIOutline | null; - currentDraftMarkdown: string; - currentDraftTitle: string; - reviewResult: ReviewResult | null; - revisionCount: number; - currentPause: ProgressEvent | null; // 当前干预点事件(null 表示不在暂停状态) - error: string | null; -} -``` - ---- - -## 7. 后端实现(分步) - -### 步骤 7.1:目录结构 - -``` -packages/harness/deerflow/agents/ai_writing/ -├── __init__.py -├── state.py -├── graph.py -├── nodes/ -│ ├── __init__.py -│ ├── intent_parser.py -│ ├── researcher.py -│ ├── writer_outline.py # 只写大纲(独立节点) -│ ├── writer_draft.py # 只写正文(独立节点) -│ └── editor.py -├── search/ -│ ├── __init__.py -│ ├── base.py # 抽象接口(内网预留) -│ ├── internet_search.py -│ └── intranet_stub.py -└── prompts/ - ├── researcher_prompts.py - ├── writer_prompts.py - └── editor_prompts.py - -app/gateway/routers/ -└── ai_writing.py # 新建,不修改其他路由文件 -``` - -### 步骤 7.2:搜索接口抽象(内网预留) - -**文件**:`packages/harness/deerflow/agents/ai_writing/search/base.py` - -```python -from abc import ABC, abstractmethod -from typing import List -from dataclasses import dataclass, field - -@dataclass -class SearchResult: - """统一搜索结果格式 —— 内外网共用,对接内网只需映射到此结构""" - title: str - content: str - source: str - published_at: str - url: str = "" - relevance_score: float = 0.0 - source_type: str = "internet" # "internet" | "intranet" - # ── 内网扩展字段(按需填充)────────────────────────────────────── - recUuid: str = "" # 知识库 ID - extra_metadata: dict = field(default_factory=dict) - -class BaseSearchProvider(ABC): - """ - 搜索提供者抽象基类 - - 对接内网步骤: - 1. 继承此类,创建 IntranetSearchProvider - 2. 实现 search() —— 调用内网 API,将响应映射到 SearchResult - 3. 在 config.yaml 中设置 ai_writing.search_provider: intranet - 4. 无需修改任何 Agent 代码 - """ - - @abstractmethod - async def search( - self, - query: str, - max_results: int = 5, - date_from: str = "", - date_to: str = "", - ) -> List[SearchResult]: ... - - @abstractmethod - async def health_check(self) -> bool: ... -``` - -**内网接口桩**:`packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py` - -```python -from typing import List -from .base import BaseSearchProvider, SearchResult - -class IntranetSearchProvider(BaseSearchProvider): - """ - 内网搜索接口适配器 - ═══════════════════════════════════════════════════════════ - 内网系统需提供 HTTP 接口,推荐格式: - - POST /api/search - Content-Type: application/json - Authorization: Bearer {api_key} (若需认证) - - 请求体: - { - "query": "搜索关键词", - "page_size": 5, - "date_from": "2024-01-01", (可选) - "date_to": "2024-12-31" (可选) - } - - 响应体: - { - "total": 100, - "results": [ - { - "title": "文档标题", - "content": "摘要(建议500字以内)", - "source": "来源系统名称", - "published_at": "2024-01-15", - "recUuid": "DOC-2024-001", - "url": "intranet://...", (可选) - "relevance_score": 0.92 (可选,无则传0) - } - ] - } - - 字段名称不同时,在 config.yaml 的 intranet_field_map 中配置映射。 - ═══════════════════════════════════════════════════════════ - """ - - def __init__(self, base_url: str, api_key: str = "", timeout: int = 10, - field_map: dict = None): - self.base_url = base_url - self.api_key = api_key - self.timeout = timeout - # 字段映射:内网字段名 → 标准字段名 - self.field_map = field_map or {} - - def _map_field(self, item: dict, standard_field: str) -> str: - """将内网字段名映射到标准字段名""" - intranet_field = self.field_map.get(standard_field, standard_field) - return item.get(intranet_field, item.get(standard_field, "")) - - async def search(self, query: str, max_results: int = 5, **kwargs) -> List[SearchResult]: - # ── TODO:对接内网时,取消以下注释并调整 ────────────────────── - # import httpx - # headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {} - # async with httpx.AsyncClient(timeout=self.timeout) as client: - # resp = await client.post( - # f"{self.base_url}/api/search", - # json={"query": query, "page_size": max_results}, - # headers=headers, - # ) - # resp.raise_for_status() - # items = resp.json().get("results", []) - # return [ - # SearchResult( - # title=self._map_field(it, "title"), - # content=self._map_field(it, "content")[:500], - # source=self._map_field(it, "source"), - # published_at=self._map_field(it, "published_at"), - # relevance_score=float(it.get( - # self.field_map.get("relevance_score", "relevance_score"), 0)), - # recUuid=self._map_field(it, "recUuid"), - # source_type="intranet", - # ) - # for it in items - # ] - # ───────────────────────────────────────────────────────────── - return [] # 未配置时返回空,不影响互联网模式 - - async def health_check(self) -> bool: - return False -``` - -**搜索工厂**:`packages/harness/deerflow/agents/ai_writing/search/__init__.py` - -```python -from .base import BaseSearchProvider, SearchResult -from .internet_search import InternetSearchProvider -from .intranet_stub import IntranetSearchProvider - -def get_search_provider(ai_writing_config: dict) -> BaseSearchProvider: - """ - config.yaml 中配置: - ai_writing: - search_provider: internet # 或 intranet - intranet_search_url: http://... - intranet_search_api_key: "" - intranet_field_map: - content: "abstract" - source: "system_name" - """ - provider = ai_writing_config.get("search_provider", "internet") - if provider == "intranet": - return IntranetSearchProvider( - base_url=ai_writing_config.get("intranet_search_url", ""), - api_key=ai_writing_config.get("intranet_search_api_key", ""), - field_map=ai_writing_config.get("intranet_field_map", {}), - ) - return InternetSearchProvider() -``` - -### 步骤 7.3:LangGraph Graph 与 Interrupt 机制 - -**文件**:`packages/harness/deerflow/agents/ai_writing/graph.py` - -```python -import asyncio -from langgraph.graph import StateGraph, END -from langgraph.checkpoint.memory import MemorySaver -from langgraph.types import interrupt, Command -from .state import AIWritingState -from .nodes.intent_parser import intent_parser_node -from .nodes.researcher import researcher_node -from .nodes.writer_outline import writer_outline_node -from .nodes.writer_draft import writer_draft_node -from .nodes.editor import editor_node - -# ── 干预节点:暂停并等待用户输入 ───────────────────────────────────────── - -async def pause_material_confirm(state: AIWritingState) -> dict: - """PAUSE-1:素材确认干预点""" - user_input = interrupt({ - "pause_point": "material_confirm", - "message": "研究院已完成素材搜集,请确认素材后继续。", - "materials": state["material_package"]["materials"], - "keywords": state["material_package"]["keywords"], - "summary": state["material_package"]["summary"], - }) - # 用户提交后,user_input 是 UserInterventionPayload - # 根据用户操作过滤素材 - action = user_input.get("action", "confirm") - if action == "confirm": - approved_ids = set(user_input.get("approvedMaterialIds", [ - m["id"] for m in state["material_package"]["materials"] - ])) - filtered_materials = [ - m for m in state["material_package"]["materials"] - if m["id"] in approved_ids - ] - updated_pkg = {**state["material_package"], "materials": filtered_materials} - return { - "material_package": updated_pkg, - "last_user_intervention": user_input, - "status": "writing_outline", - "progress_events": [{"type": "resumed", "message": "用户已确认素材,开始规划大纲"}] - } - # action == "re_search":重新搜索(返回 researcher 节点,通过路由控制) - extra_keywords = user_input.get("extraKeywords", []) - return { - "last_user_intervention": {**user_input, "_extra_keywords": extra_keywords}, - "status": "researching", - "progress_events": [{"type": "resumed", "message": "重新搜索中..."}] - } - -async def pause_outline_confirm(state: AIWritingState) -> dict: - """PAUSE-2:大纲确认干预点""" - user_input = interrupt({ - "pause_point": "outline_confirm", - "message": "作家已完成大纲规划,请确认或编辑大纲后继续。", - "outline": state["current_outline"], - }) - action = user_input.get("action", "confirm") - if action == "confirm": - edited_outline = user_input.get("editedOutline") or state["current_outline"] - return { - "current_outline": edited_outline, - "last_user_intervention": user_input, - "status": "writing_draft", - "progress_events": [{"type": "resumed", "message": "用户已确认大纲,开始写作"}] - } - # action == "re_outline" - feedback = user_input.get("outlineFeedback", "") - return { - "last_user_intervention": {**user_input, "_feedback": feedback}, - "status": "writing_outline", - "progress_events": [{"type": "resumed", "message": f"重新规划大纲(意见:{feedback[:50]})"}] - } - -async def pause_draft_confirm(state: AIWritingState) -> dict: - """PAUSE-3:草稿确认干预点""" - user_input = interrupt({ - "pause_point": "draft_confirm", - "message": "草稿已完成,请确认或给出修改意见。", - "draft_title": state["current_draft"]["title"], - "revision_count": state["current_draft"]["revision_count"], - "review_result": state.get("review_result"), - }) - action = user_input.get("action", "to_editor") - if action == "finalize": - return { - "last_user_intervention": user_input, - "status": "done", - "progress_events": [{"type": "resumed", "message": "用户确认定稿"}] - } - if action == "user_revise": - notes = user_input.get("userRevisionNotes", "") - return { - "last_user_intervention": {**user_input, "_user_notes": notes}, - "revision_count": state.get("revision_count", 0) + 1, - "status": "revising", - "progress_events": [{"type": "resumed", "message": f"按用户意见重写({notes[:50]})"}] - } - # action == "to_editor":走编辑流程 - return { - "last_user_intervention": user_input, - "status": "reviewing", - "progress_events": [{"type": "resumed", "message": "提交编辑审核"}] - } - -async def pause_review_confirm(state: AIWritingState) -> dict: - """PAUSE-4:编辑打回确认(仅 verdict==reject 时触发)""" - user_input = interrupt({ - "pause_point": "review_confirm", - "message": "编辑认为文章需要修改,请决定下一步操作。", - "review_result": state["review_result"], - "revision_count": state.get("revision_count", 0), - "max_revisions": state.get("max_revisions", 3), - }) - if user_input.get("overrideVerdict"): - return { - "last_user_intervention": user_input, - "status": "done", - "progress_events": [{"type": "resumed", "message": "用户强制定稿"}] - } - # 采纳编辑意见 or 附加用户意见 - extra_notes = user_input.get("userRevisionNotes", "") - return { - "last_user_intervention": {**user_input, "_extra_notes": extra_notes}, - "revision_count": state.get("revision_count", 0) + 1, - "status": "revising", - "progress_events": [{"type": "resumed", "message": "按编辑意见重写"}] - } - -# ── 条件路由 ────────────────────────────────────────────────────────── - -def after_material_pause(state: AIWritingState) -> str: - if state["status"] == "researching": - return "researcher" # 重新搜索 - return "writer_outline" - -def after_outline_pause(state: AIWritingState) -> str: - if state["status"] == "writing_outline": - return "writer_outline" # 重新规划 - return "writer_draft" - -def after_draft_pause(state: AIWritingState) -> str: - status = state["status"] - if status == "done": - return "end" - if status == "revising": - return "writer_draft" # 按用户意见重写 - return "editor" # 走编辑流程 - -def after_review(state: AIWritingState) -> str: - review = state.get("review_result", {}) - if review.get("verdict") == "pass": - return "pause_draft_confirm" # 编辑通过 → 草稿确认 - rev_count = state.get("revision_count", 0) - max_rev = state.get("max_revisions", 3) - if rev_count >= max_rev: - return "pause_draft_confirm" # 超出次数强制到草稿确认 - return "pause_review_confirm" # 编辑打回 → 审核确认 - -def after_review_pause(state: AIWritingState) -> str: - if state["status"] == "done": - return "end" - return "writer_draft" # 重写 - -# ── 组装 Graph ──────────────────────────────────────────────────────── - -def build_ai_writing_graph(): - graph = StateGraph(AIWritingState) - - # 节点 - graph.add_node("intent_parser", intent_parser_node) - graph.add_node("researcher", researcher_node) - graph.add_node("pause_material", pause_material_confirm) # PAUSE-1 - graph.add_node("writer_outline", writer_outline_node) - graph.add_node("pause_outline", pause_outline_confirm) # PAUSE-2 - graph.add_node("writer_draft", writer_draft_node) - graph.add_node("pause_draft", pause_draft_confirm) # PAUSE-3 - graph.add_node("editor", editor_node) - graph.add_node("pause_review", pause_review_confirm) # PAUSE-4 - - # 边 - graph.set_entry_point("intent_parser") - graph.add_edge("intent_parser", "researcher") - graph.add_edge("researcher", "pause_material") - - graph.add_conditional_edges("pause_material", after_material_pause, - {"researcher": "researcher", "writer_outline": "writer_outline"}) - - graph.add_edge("writer_outline", "pause_outline") - graph.add_conditional_edges("pause_outline", after_outline_pause, - {"writer_outline": "writer_outline", "writer_draft": "writer_draft"}) - - graph.add_edge("writer_draft", "pause_draft") - graph.add_conditional_edges("pause_draft", after_draft_pause, - {"end": END, "writer_draft": "writer_draft", "editor": "editor"}) - - graph.add_conditional_edges("editor", after_review, - {"pause_draft_confirm": "pause_draft", - "pause_review_confirm": "pause_review"}) - - graph.add_conditional_edges("pause_review", after_review_pause, - {"end": END, "writer_draft": "writer_draft"}) - - # 使用 MemorySaver 支持 interrupt/resume(生产环境换成 PostgresSaver) - checkpointer = MemorySaver() - return graph.compile(checkpointer=checkpointer, interrupt_before=None) - -AI_WRITING_GRAPH = build_ai_writing_graph() -``` - -### 步骤 7.4:后端 HTTP 路由 - -**文件**:`app/gateway/routers/ai_writing.py` - -```python -import asyncio -import json -import uuid -from typing import AsyncGenerator, Optional -from fastapi import APIRouter, HTTPException -from fastapi.responses import StreamingResponse -from pydantic import BaseModel, Field -from langgraph.types import Command -from deerflow.agents.ai_writing.graph import AI_WRITING_GRAPH - -router = APIRouter() - -# ── 请求/响应模型 ───────────────────────────────────────────────────── - -class StartWritingRequest(BaseModel): - user_intent: str - article_type: str = "analysis" - target_audience: str = "普通读者" - word_count_target: int = 800 - model_name: str = "" - -class StartWritingResponse(BaseModel): - session_id: str - message: str = "写作流程已启动" - -class ResumeRequest(BaseModel): - action: str - approved_material_ids: list[str] = Field(default_factory=list) - extra_keywords: list[str] = Field(default_factory=list) - edited_outline: Optional[dict] = None - outline_feedback: str = "" - user_revision_notes: str = "" - override_verdict: bool = False - -# ── 工具函数 ────────────────────────────────────────────────────────── - -def sse(event: str, data: dict) -> str: - return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" - -def _make_thread_config(session_id: str, model_name: str = ""): - from deerflow.models.factory import get_default_llm - llm = get_default_llm(model_name=model_name or None) - return { - "configurable": { - "thread_id": session_id, - "llm": llm, - "ai_writing_config": {}, # 从全局 config 读取 search_provider 等 - } - } - -# ── 路由 ────────────────────────────────────────────────────────────── - -@router.post("/start", response_model=StartWritingResponse) -async def start_writing(body: StartWritingRequest): - """启动写作流程,返回 session_id(后续用此 ID 监听 SSE 和提交干预)""" - session_id = str(uuid.uuid4()) - initial_state = { - "user_intent": body.user_intent, - "article_type": body.article_type, - "target_audience": body.target_audience, - "word_count_target": body.word_count_target, - "revision_count": 0, - "max_revisions": 3, - "status": "researching", - "drafts": [], - "progress_events": [], - } - config = _make_thread_config(session_id, body.model_name) - - # 异步启动 Graph(不等待完成,通过 SSE 推送进度) - asyncio.create_task(_run_graph(session_id, initial_state, config)) - return StartWritingResponse(session_id=session_id) - - -@router.get("/{session_id}/stream") -async def stream_progress(session_id: str): - """SSE 流:推送进度事件、干预请求、草稿内容""" - return StreamingResponse( - _sse_generator(session_id), - media_type="text/event-stream", - headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, - ) - - -@router.post("/{session_id}/resume") -async def resume_writing(session_id: str, body: ResumeRequest): - """用户提交干预决策,恢复暂停的流程""" - config = _make_thread_config(session_id) - intervention = { - "action": body.action, - "approvedMaterialIds": body.approved_material_ids, - "extraKeywords": body.extra_keywords, - "editedOutline": body.edited_outline, - "outlineFeedback": body.outline_feedback, - "userRevisionNotes": body.user_revision_notes, - "overrideVerdict": body.override_verdict, - } - # LangGraph Command(resume=...) 恢复被 interrupt() 暂停的流程 - asyncio.create_task( - AI_WRITING_GRAPH.ainvoke(Command(resume=intervention), config=config) - ) - return {"message": "已提交,流程恢复中"} - - -@router.get("/{session_id}/state") -async def get_state(session_id: str): - """查询当前流程状态快照""" - config = _make_thread_config(session_id) - state = await AI_WRITING_GRAPH.aget_state(config) - if not state: - raise HTTPException(status_code=404, detail="Session not found") - return state.values - - -# ── 内部辅助 ────────────────────────────────────────────────────────── - -# 每个 session 的事件队列 -_event_queues: dict[str, asyncio.Queue] = {} - -async def _run_graph(session_id: str, initial_state: dict, config: dict): - """后台运行 Graph,将节点输出推入事件队列""" - queue = asyncio.Queue() - _event_queues[session_id] = queue - - try: - async for chunk in AI_WRITING_GRAPH.astream( - initial_state, config=config, stream_mode="updates" - ): - node_name = list(chunk.keys())[0] - node_output = chunk[node_name] - - # 推送 progress_events - for evt in node_output.get("progress_events", []): - await queue.put(("progress", evt)) - - # 素材就绪 - if pkg := node_output.get("material_package"): - await queue.put(("materials_ready", { - "keywords": pkg["keywords"], - "materials": pkg["materials"], - "summary": pkg["summary"], - })) - - # 大纲就绪 - if outline := node_output.get("current_outline"): - await queue.put(("outline_ready", {"outline": outline})) - - # 草稿就绪 - if draft := node_output.get("current_draft"): - await queue.put(("draft_ready", { - "title": draft["title"], - "markdown": draft["full_markdown"], - "revision_count": draft["revision_count"], - })) - - # 编辑审核就绪 - if review := node_output.get("review_result"): - await queue.put(("review_ready", {"review_result": review})) - - # interrupt() 触发 → 推送 await_user 事件 - # LangGraph 在 interrupt 时会抛出特殊异常, - # 通过 stream_mode="updates" 时以 "__interrupt__" 节点出现 - if node_name == "__interrupt__": - interrupt_data = node_output.get("value", {}) - await queue.put(("await_user", interrupt_data)) - - await queue.put(("done", {"message": "写作完成"})) - - except Exception as e: - await queue.put(("error", {"message": str(e)})) - finally: - await queue.put(None) # 哨兵,关闭流 - - -async def _sse_generator(session_id: str) -> AsyncGenerator[str, None]: - """从队列消费事件,推送为 SSE""" - # 等待队列创建(最多 5 秒) - for _ in range(50): - if session_id in _event_queues: - break - await asyncio.sleep(0.1) - else: - yield sse("error", {"message": "Session not found"}) - return - - queue = _event_queues[session_id] - yield sse("connected", {"session_id": session_id}) - - while True: - item = await queue.get() - if item is None: - break - event_type, data = item - yield sse(event_type, {"session_id": session_id, **data}) - await asyncio.sleep(0) - - _event_queues.pop(session_id, None) -``` - -**注册路由**(在 `app/gateway/app.py` 末尾追加,不修改已有代码): - -```python -from app.gateway.routers import ai_writing -app.include_router(ai_writing.router, prefix="/api/ai-writing", tags=["ai-writing"]) -``` - ---- - -## 8. 前端实现(分步) - -### 步骤 8.1:API 客户端 - -**文件**:`frontend-web/src/open-canvas/api/ai-writing.ts` - -```typescript -import type { AIWritingRequest, ProgressEvent, UserInterventionPayload } from '../types/ai-writing'; - -const BASE = '/api/ai-writing'; - -/** 启动写作流程,返回 session_id */ -export async function startWritingSession(req: AIWritingRequest): Promise { - const resp = await fetch(`${BASE}/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - user_intent: req.userIntent, - article_type: req.articleType, - target_audience: req.targetAudience, - word_count_target: req.wordCountTarget, - }), - }); - if (!resp.ok) throw new Error(`启动失败: ${resp.status}`); - const data = await resp.json(); - return data.session_id; -} - -/** 监听 SSE 进度流 */ -export function subscribeToProgress( - sessionId: string, - onEvent: (event: ProgressEvent) => void, - signal?: AbortSignal, -): () => void { - const es = new EventSource(`${BASE}/${sessionId}/stream`); - - const eventTypes = [ - 'progress', 'materials_ready', 'outline_ready', - 'draft_ready', 'review_ready', 'await_user', - 'resumed', 'done', 'error', - ] as const; - - eventTypes.forEach((type) => { - es.addEventListener(type, (e: MessageEvent) => { - try { - const data = JSON.parse(e.data); - onEvent({ type, sessionId, ...data } as ProgressEvent); - } catch { - // 忽略 - } - }); - }); - - signal?.addEventListener('abort', () => es.close()); - return () => es.close(); -} - -/** 提交用户干预决策 */ -export async function resumeWriting( - sessionId: string, - payload: UserInterventionPayload, -): Promise { - const resp = await fetch(`${BASE}/${sessionId}/resume`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: payload.action, - approved_material_ids: payload.approvedMaterialIds ?? [], - extra_keywords: payload.extraKeywords ?? [], - edited_outline: payload.editedOutline ?? null, - outline_feedback: payload.outlineFeedback ?? '', - user_revision_notes: payload.userRevisionNotes ?? '', - override_verdict: payload.overrideVerdict ?? false, - }), - }); - if (!resp.ok) throw new Error(`提交干预失败: ${resp.status}`); -} -``` - -### 步骤 8.2:核心 Hook - -**文件**:`frontend-web/src/open-canvas/hooks/useAIWriting.ts` - -```typescript -import { useState, useCallback, useRef, useEffect } from 'react'; -import { startWritingSession, subscribeToProgress, resumeWriting } from '../api/ai-writing'; -import type { - AIWritingRequest, AIWritingState, ProgressEvent, - UserInterventionPayload, AIMaterial, AIOutline, ReviewResult, -} from '../types/ai-writing'; -import { useCanvasGraphContext } from '../contexts/CanvasGraphContext'; - -const INITIAL_STATE: AIWritingState = { - sessionId: null, - status: 'idle', - progressEvents: [], - materials: [], - outline: null, - currentDraftMarkdown: '', - currentDraftTitle: '', - reviewResult: null, - revisionCount: 0, - currentPause: null, - error: null, -}; - -export function useAIWriting() { - const { setArtifact } = useCanvasGraphContext(); - const [state, setState] = useState(INITIAL_STATE); - const abortRef = useRef(null); - const unsubscribeRef = useRef<(() => void) | null>(null); - - const handleEvent = useCallback((event: ProgressEvent) => { - setState((prev) => { - const base = { ...prev, progressEvents: [...prev.progressEvents, event] }; - - switch (event.type) { - case 'materials_ready': - return { ...base, materials: event.materials ?? [], status: 'awaiting_material_confirm', currentPause: event }; - - case 'outline_ready': - return { ...base, outline: event.outline ?? null, status: 'awaiting_outline_confirm', currentPause: event }; - - case 'draft_ready': - return { - ...base, - currentDraftMarkdown: event.draftMarkdown ?? '', - currentDraftTitle: event.draftTitle ?? '', - revisionCount: event.revisionCount ?? 0, - status: 'awaiting_draft_confirm', - currentPause: event, - }; - - case 'review_ready': - return { ...base, reviewResult: event.reviewResult ?? null }; - - case 'await_user': - return { ...base, status: `awaiting_${event.pausePoint ?? 'unknown'}` as AIWritingState['status'], currentPause: event }; - - case 'resumed': - return { ...base, currentPause: null }; - - case 'done': - return { ...base, status: 'done', currentPause: null }; - - case 'error': - return { ...base, status: 'error', error: event.message ?? '未知错误' }; - - default: - return base; - } - }); - - // 草稿实时同步到右侧 Artifact 面板 - if (event.type === 'draft_ready' && event.draftMarkdown) { - setArtifact({ - currentIndex: 0, - contents: [{ - index: 0, - type: 'text', - title: event.draftTitle ?? '草稿', - fullMarkdown: event.draftMarkdown, - }], - }); - } - }, [setArtifact]); - - const startWriting = useCallback(async (request: AIWritingRequest) => { - if (state.status !== 'idle' && state.status !== 'done' && state.status !== 'error') return; - - abortRef.current?.abort(); - abortRef.current = new AbortController(); - setState({ ...INITIAL_STATE, status: 'researching' }); - - try { - const sessionId = await startWritingSession(request); - setState((prev) => ({ ...prev, sessionId })); - const unsubscribe = subscribeToProgress(sessionId, handleEvent, abortRef.current.signal); - unsubscribeRef.current = unsubscribe; - } catch (err) { - setState((prev) => ({ ...prev, status: 'error', error: String(err) })); - } - }, [state.status, handleEvent]); - - const submitIntervention = useCallback(async (payload: UserInterventionPayload) => { - if (!state.sessionId) return; - try { - await resumeWriting(state.sessionId, payload); - setState((prev) => ({ ...prev, currentPause: null })); - } catch (err) { - setState((prev) => ({ ...prev, error: String(err) })); - } - }, [state.sessionId]); - - const stopWriting = useCallback(() => { - abortRef.current?.abort(); - unsubscribeRef.current?.(); - setState((prev) => ({ ...prev, status: 'idle' })); - }, []); - - const reset = useCallback(() => { - abortRef.current?.abort(); - unsubscribeRef.current?.(); - setState(INITIAL_STATE); - }, []); - - useEffect(() => () => { abortRef.current?.abort(); }, []); - - return { state, startWriting, submitIntervention, stopWriting, reset }; -} -``` - ---- - -## 9. 内网接口预留设计 - -### 9.1 配置切换(config.yaml) - -```yaml -ai_writing: - search_provider: internet # ← 切换为 intranet 即启用内网模式 - intranet_search_url: "" # 内网搜索服务地址 - intranet_search_api_key: "" # 认证 Token(若需要) - intranet_search_timeout: 10 - intranet_field_map: # 内网字段名映射(与标准字段不同时填写) - title: "title" - content: "abstract" # 示例:内网叫 abstract - source: "system_name" # 示例:内网叫 system_name - published_at: "create_time" - relevance_score: "score" - recUuid: "id" -``` - -### 9.2 内网素材标准字段 - -| 标准字段 | 类型 | 必须 | 说明 | -|----------|------|:----:|------| -| `title` | string | ✅ | 文章/文档标题 | -| `content` | string | ✅ | 正文摘要,500 字以内 | -| `source` | string | ✅ | 来源系统名(如"研发知识库") | -| `published_at` | string | ✅ | 发布日期 YYYY-MM-DD | -| `relevance_score` | float 0-1 | ⬜ | 无则传 0 | -| `recUuid` | string | ⬜ | 知识库文档 ID | - -### 9.3 对接内网步骤清单 - -- [ ] 确认内网搜索 API 地址和认证方式 -- [ ] 在 `config.yaml` 的 `intranet_field_map` 中配置字段映射 -- [ ] 取消 `intranet_stub.py` 中的注释代码,调整字段映射 -- [ ] 将 `config.yaml` 中 `search_provider` 改为 `intranet` -- [ ] 调用 `GET /api/ai-writing/health` 验证连通性 -- [ ] 验证内网素材卡片在前端正常展示(包括 `department`、`classification` 字段) - ---- - -## 10. LangGraph 状态机流转图 - -``` -START - │ - ▼ -intent_parser ──── 意图解析(结构化写作配置) - │ - ▼ -researcher ──────── Agent A:关键词拆解 → 并发搜索 → 去重排序 - │ - ▼ -pause_material ──── ⏸ PAUSE-1:推送 await_user 事件,等待用户确认素材 - │ (interrupt) - ├── action=confirm ──────────────────────────▶ writer_outline - └── action=re_search ──────────────────────────▶ researcher(重搜) - -writer_outline ─── Agent B Phase1:写大纲(JSON) - │ - ▼ -pause_outline ───── ⏸ PAUSE-2:推送 await_user 事件,等待用户确认大纲 - │ (interrupt) - ├── action=confirm(含用户可能的编辑)─────────▶ writer_draft - └── action=re_outline ─────────────────────────▶ writer_outline(重规划) - -writer_draft ────── Agent B Phase2:按大纲逐段写正文 - │ - ▼ -pause_draft ─────── ⏸ PAUSE-3:推送 await_user 事件,等待用户确认草稿 - │ (interrupt) - ├── action=finalize ────────────────────────────▶ END(用户直接定稿) - ├── action=user_revise ─────────────────────────▶ writer_draft(按用户意见重写) - └── action=to_editor ────────────────────────────▶ editor - -editor ─────────── Agent C:三项独立审查,综合评分 - │ - ├── verdict=pass ────────────────────────────────▶ pause_draft(草稿确认) - ├── verdict=reject & count < max ───────────────▶ pause_review(PAUSE-4) - └── verdict=reject & count >= max ──────────────▶ pause_draft(强制到草稿确认) - -pause_review ────── ⏸ PAUSE-4:推送编辑意见,等待用户决策 - │ (interrupt) - ├── override_verdict=true ──────────────────────▶ END(强制定稿) - └── override_verdict=false ─────────────────────▶ writer_draft(按编辑意见重写) - -END -``` - ---- - -## 11. 提示词模板(弱模型专用) - -以下是针对弱模型优化的三段式提示词框架(借鉴 CrewAI 的 role+goal+backstory 结构): - -### 研究院 System Prompt - -``` -你是一名专业情报研究员(研究院)。 -你的目标:为写作任务搜集高质量素材。 -你的工作背景:你在一家权威媒体机构任职,熟悉各类信息源的可信度评估。 - -你只能做以下事情: -1. 从写作意图中提取搜索关键词 -2. 对搜索结果按相关度排序 -你不能写文章,不能评价内容好坏,只做信息收集。 - -所有输出必须是合法的 JSON 格式,参考示例: -{"keywords": ["关键词1", "关键词2", "关键词3"]} -``` - -### 作家 System Prompt(大纲阶段) - -``` -你是一名专业内容创作者(作家)。 -你的目标:根据素材和意图,规划清晰的文章大纲。 -你的工作背景:你有十年内容创作经验,擅长将零散素材组织成有逻辑的文章结构。 - -你只能做以下事情: -1. 分析素材,提取核心观点 -2. 规划文章章节结构和每节要点 -你不能写正文,只写大纲。 - -所有输出必须是合法的 JSON,严格参考以下示例格式: -{ - "title": "文章标题", - "sections": [ - { - "section_title": "第一章标题", - "key_points": ["要点一", "要点二"], - "material_ids": ["m001"] - } - ] -} -``` - -### 作家 System Prompt(写作阶段) - -``` -你是一名专业内容创作者(作家)。 -你的目标:根据大纲和素材,写出一段高质量的文章内容。 -你的工作背景:你有十年内容创作经验,文字流畅,善于用事实说话。 - -你只能做以下事情: -1. 根据给定的章节要点和素材写这一段内容 -2. 观点必须来自素材,不编造数据 -你不能修改大纲,不能写其他章节,只写当前这一段。 - -直接返回段落文字,不要 JSON,不要标题,不要解释。 -``` - -### 编辑 System Prompt - -``` -你是一名资深主编(编辑),标准极高,要求严格。 -你的目标:审查文章质量,给出客观评分和具体修改意见。 -你的工作背景:你在一家权威媒体担任主编十五年,见过各种质量的文章。 - -你只能做以下事情: -1. 从 [fact/logic/language] 某一个维度审查文章 -2. 给出该维度的评分(0-100)和具体问题列表 -你不能改写文章,不能评价其他维度,只做当前指定维度的审查。 - -所有输出必须是合法的 JSON,严格参考以下示例: -{ - "score": 82, - "issues": [ - { - "location": "第二段", - "issue": "数据来源不明确", - "suggestion": "引用素材 m001 中的具体数据,并注明来源" - } - ] -} -``` - ---- - -## 12. SSE 协议 & 干预协议设计 - -### SSE 事件类型 - -| event | 触发时机 | data 关键字段 | -|-------|----------|---------------| -| `connected` | 连接建立 | `session_id` | -| `step_start` | Agent 开始某步骤 | `agent_name`, `message` | -| `step_done` | Agent 完成某步骤 | `agent_name`, `message` | -| `materials_ready` | 研究院搜索完成 | `keywords`, `materials[]`, `summary` | -| `outline_ready` | 大纲生成完成 | `outline{title, sections[]}` | -| `draft_ready` | 草稿完成 | `title`, `markdown`, `revision_count` | -| `review_ready` | 编辑审核完成 | `review_result{verdict, scores, notes[]}` | -| `await_user` | 流程暂停,等待干预 | `pause_point`, 干预所需上下文数据 | -| `resumed` | 用户干预已提交,流程恢复 | `message` | -| `done` | 全部完成 | `message` | -| `error` | 出错 | `message` | - -### 干预 API - -``` -POST /api/ai-writing/{session_id}/resume -Content-Type: application/json - -{ - "action": "confirm", // 操作类型(见各干预点定义) - "approved_material_ids": [...], // PAUSE-1 - "extra_keywords": [...], // PAUSE-1 - "edited_outline": {...}, // PAUSE-2(null=不修改) - "outline_feedback": "", // PAUSE-2(重规划时的意见) - "user_revision_notes": "", // PAUSE-3 或 PAUSE-4 - "override_verdict": false // PAUSE-4 -} -``` - -**各干预点的 action 值**: - -| 干预点 | action 值 | 说明 | -|--------|-----------|------| -| PAUSE-1 | `confirm` | 确认当前素材继续 | -| PAUSE-1 | `re_search` | 重新搜索(可附 extra_keywords) | -| PAUSE-2 | `confirm` | 确认大纲(可附 edited_outline) | -| PAUSE-2 | `re_outline` | 重新规划(可附 outline_feedback) | -| PAUSE-3 | `finalize` | 直接定稿 | -| PAUSE-3 | `user_revise` | 按用户意见重写(附 user_revision_notes) | -| PAUSE-3 | `to_editor` | 提交编辑审核 | -| PAUSE-4 | `accept_review` | 采纳编辑意见重写 | -| PAUSE-4 | `force_finalize` | 强制定稿(override_verdict=true) | -| PAUSE-4 | `custom_revise` | 附加用户意见重写(user_revision_notes) | - ---- - -## 13. 前端 UI 组件详细设计 - -### 13.1 组件树 - -``` -AIWritingPanel.tsx (左侧面板根组件) -├── AIWritingForm.tsx (写作意图输入表单) -├── AIWritingTimeline.tsx (进度时间线,纵向排列) -│ ├── TimelineStep.tsx (单个步骤卡片) -│ │ ├── StepHeader.tsx (Agent名 + 状态图标 + 时间) -│ │ └── StepContent.tsx (步骤详情,可收起展开) -│ │ -│ ├── MaterialsCard.tsx (素材卡片区,PAUSE-1 触发展示) -│ │ ├── MaterialItem.tsx (单条素材卡片,默认收起) -│ │ │ ├── 标题 + 来源 + 时间 -│ │ │ ├── 相关度色条 -│ │ │ └── 正文摘要(可展开) -│ │ └── MaterialsToolbar.tsx (全选/全取消/补充搜索输入) -│ │ -│ ├── OutlineCard.tsx (大纲展示卡,PAUSE-2 触发展示) -│ │ ├── OutlineSection.tsx (可编辑的章节行) -│ │ └── OutlineToolbar.tsx (确认/重规划按钮) -│ │ -│ ├── ReviewCard.tsx (编辑审核结果,PAUSE-4 触发展示) -│ │ ├── ScoreBar.tsx (三项评分进度条) -│ │ └── IssueList.tsx (问题清单,按严重程度分组) -│ │ -│ └── InterventionCard.tsx (用户干预操作区,每个 PAUSE 点渲染) -│ ├── 操作按钮组 -│ └── 补充意见输入框 -│ -└── AIWritingStatus.tsx (底部状态栏:当前 Agent + 进度) -``` - -### 13.2 MaterialItem 组件规格 - -``` -┌─────────────────────────────────────────────────────────┐ -│ ☑ [标题(最多两行截断)] ▶ │ ← 点击展开 -│ 来源网站 · 2024-01-15 · ████████░░ 82% │ -└─────────────────────────────────────────────────────────┘ - -展开后: -┌─────────────────────────────────────────────────────────┐ -│ ☑ [标题] ▼ │ -│ 来源网站 · 2024-01-15 · ████████░░ 82% │ -│ ───────────────────────────────────────────────── │ -│ [正文摘要,最多 200 字,超出显示"...展开"] │ -│ │ -│ [查看原文 →](若有 url) │ -└─────────────────────────────────────────────────────────┘ -``` - -**相关度色条颜色规则**: -- `>= 0.8`:绿色(高相关) -- `0.5 ~ 0.8`:橙色(中相关) -- `< 0.5`:红色(低相关,建议取消勾选) - -### 13.3 OutlineCard 组件规格 - -``` -┌──────────────────────────────────────────────────────────┐ -│ 大纲(可编辑) [全部展开▼] │ -├──────────────────────────────────────────────────────────┤ -│ 文章标题:[可内联编辑] │ -│ │ -│ ▶ 第一部分:引言与背景(点击展开要点) │ -│ ▶ 第二部分:核心分析 │ -│ ▶ 第三部分:结论与建议 │ -│ │ -│ (展开后) │ -│ ▼ 第二部分:核心分析 │ -│ • 要点一(可编辑) │ -│ • 要点二(可编辑) │ -│ + 添加要点 │ -│ 引用素材:m001 m002(Tag 样式) │ -├──────────────────────────────────────────────────────────┤ -│ [确认大纲,开始写作] [重新规划大纲...] │ -└──────────────────────────────────────────────────────────┘ -``` - -### 13.4 InterventionCard 组件规格 - -``` -┌──────────────────────────────────────────────────────────┐ -│ ⏸ 需要您的确认才能继续 │ -│ ─────────────────────────────────────────────────────── │ -│ [PAUSE-1 素材确认时] │ -│ 已搜集到 8 条素材(已为您默认全选) │ -│ 您可以取消勾选不相关的素材,或补充关键词 │ -│ │ -│ 补充关键词:[___________________] [+追加搜索] │ -│ │ -│ [✓ 确认素材,开始规划大纲] [↺ 重新搜索] │ -└──────────────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────────────┐ -│ ⏸ 需要您的确认才能继续 │ -│ ─────────────────────────────────────────────────────── │ -│ [PAUSE-3 草稿确认时] │ -│ 初稿已完成(共 856 字) │ -│ 编辑评分:81分(通过线 80 分) │ -│ │ -│ 您的修改意见(可选): │ -│ [___________________________________] │ -│ │ -│ [✓ 确认定稿] [✎ 按我的意见修改] [→ 让编辑审核] │ -└──────────────────────────────────────────────────────────┘ -``` - -### 13.5 ReviewCard 组件规格 - -``` -┌──────────────────────────────────────────────────────────┐ -│ 编辑审核结果(第 1 次修改) 综合评分:78 / 100 │ -│ │ -│ 事实准确性 ████████░░ 82 (通过) │ -│ 逻辑结构 ██████░░░░ 72 (需改进) │ -│ 语言文字 █████████░ 88 (优秀) │ -│ │ -│ ── 修改意见 ────────────────────────────────────────── │ -│ │ -│ [major] 逻辑 · 第三段:论点跳跃,缺少过渡 │ -│ → 建议:在第三段开头增加连接上下文的过渡句 │ -│ │ -│ [minor] 事实 · 第二段:引用数据未标注来源 │ -│ → 建议:补充"据素材 m002 显示" │ -│ │ -│ 您的补充意见(可选): │ -│ [___________________________________] │ -│ │ -│ [↺ 采纳编辑意见,让作家重写] [✓ 忽略编辑,强制定稿] │ -└──────────────────────────────────────────────────────────┘ -``` - -### 13.6 进度时间线整体效果 - -``` -左侧面板时间线(纵向) - -○ ── 研究院 ← 完成(绿色圆点) -│ 搜集到 8 条素材 -│ ┌───────────────────────────┐ -│ │ 素材列表(默认收起) ▶ │ ← 点击展开素材卡片 -│ └───────────────────────────┘ -│ ┌──────────────────────────────────────────────┐ -│ │ ⏸ 需要您确认素材,才能继续 │ -│ │ [✓ 确认] [↺ 重新搜索] │ -│ └──────────────────────────────────────────────┘ - -○ ── 作家(大纲) ← 完成(绿色圆点) -│ 已规划 3 个章节 -│ ┌───────────────────────────┐ -│ │ 大纲预览(默认收起) ▶ │ -│ └───────────────────────────┘ -│ ┌──────────────────────────────────────────────┐ -│ │ ⏸ 请确认大纲后继续写作 │ -│ │ [✓ 确认] [✎ 编辑大纲] [↺ 重规划] │ -│ └──────────────────────────────────────────────┘ - -◌ ── 作家(写作) ← 进行中(动画圆点) -│ 正在写作第二章... - -◌ ── 编辑 ← 等待中(灰色圆点) - -◌ ── 定稿 ← 等待中 -``` - ---- - -## 14. 文件目录结构 - -``` -# 后端新增(不修改任何已有文件) -offline-backend-20260512/backend/ -├── app/gateway/routers/ -│ └── ai_writing.py ← 新建 -└── packages/harness/deerflow/agents/ - └── ai_writing/ ← 新建目录 - ├── __init__.py - ├── state.py - ├── graph.py - ├── nodes/ - │ ├── __init__.py - │ ├── intent_parser.py - │ ├── researcher.py - │ ├── writer_outline.py - │ ├── writer_draft.py - │ └── editor.py - ├── search/ - │ ├── __init__.py - │ ├── base.py ← 抽象接口(内网预留) - │ ├── internet_search.py - │ └── intranet_stub.py ← 内网桩 - └── prompts/ - ├── researcher_prompts.py - ├── writer_prompts.py - └── editor_prompts.py - -# 前端新增(不修改已有组件) -frontend-web/src/open-canvas/ -├── api/ -│ └── ai-writing.ts ← 新建 -├── types/ -│ └── ai-writing.ts ← 新建 -├── hooks/ -│ └── useAIWriting.ts ← 新建 -└── components/ - └── ai-writing/ ← 新建目录 - ├── AIWritingPanel.tsx ← 左侧面板根组件 - ├── AIWritingForm.tsx ← 写作意图输入表单 - ├── AIWritingTimeline.tsx ← 进度时间线 - ├── TimelineStep.tsx ← 单步骤卡片 - ├── MaterialsCard.tsx ← 素材卡片区 - ├── MaterialItem.tsx ← 单条素材 - ├── MaterialsToolbar.tsx ← 素材操作工具栏 - ├── OutlineCard.tsx ← 大纲展示与编辑 - ├── OutlineSection.tsx ← 单章节行 - ├── ReviewCard.tsx ← 编辑审核结果 - ├── ScoreBar.tsx ← 评分进度条 - ├── IssueList.tsx ← 问题清单 - └── InterventionCard.tsx ← 用户干预操作区 -``` - ---- - -## 15. 分步开发检查清单 - -### 阶段一:后端骨架(2 天) - -- [ ] 创建 `ai_writing/` 目录结构(所有 `__init__.py`) -- [ ] 实现 `state.py`(完整 AIWritingState 定义) -- [ ] 实现 `search/base.py`(抽象接口) -- [ ] 实现 `search/internet_search.py`(接入项目已有搜索工具) -- [ ] 实现 `search/intranet_stub.py`(桩,返回空列表) -- [ ] 实现 `nodes/intent_parser.py`(意图解析 → 结构化配置) -- [ ] 实现 `nodes/researcher.py`(Agent A,两步:关键词+搜索) -- [ ] 实现 `nodes/writer_outline.py`(Agent B Phase1,只写大纲) -- [ ] 实现 `nodes/writer_draft.py`(Agent B Phase2,逐段写正文) -- [ ] 实现 `nodes/editor.py`(Agent C,三次独立调用) -- [ ] 实现 `graph.py`(含 4 个 interrupt 干预节点) -- [ ] 实现 `app/gateway/routers/ai_writing.py`(start + stream + resume + state) -- [ ] 在 `app.py` 注册路由(追加一行) -- [ ] `curl` 测试 `/api/ai-writing/start` 和 `/api/ai-writing/{id}/stream` - -### 阶段二:后端干预验证(1 天) - -- [ ] 测试 PAUSE-1(素材确认):验证 `interrupt()` 暂停 + `resume` 恢复 -- [ ] 测试 PAUSE-2(大纲确认):验证编辑大纲后继续写作 -- [ ] 测试 PAUSE-3(草稿确认):验证三种 action(定稿/重写/编辑) -- [ ] 测试 PAUSE-4(审核确认):验证打回循环和强制定稿 -- [ ] 测试最大循环上限(revision_count >= max_revisions) - -### 阶段三:前端进度展示(2 天) - -- [ ] 创建 `types/ai-writing.ts`(完整类型定义) -- [ ] 创建 `api/ai-writing.ts`(start + SSE subscribe + resume) -- [ ] 创建 `hooks/useAIWriting.ts`(状态管理 + 事件处理) -- [ ] 创建 `AIWritingTimeline.tsx` + `TimelineStep.tsx`(时间线骨架) -- [ ] 创建 `ScoreBar.tsx`(评分进度条) -- [ ] 在 `CanvasResizableLayout.tsx` 引入 `AIWritingPanel`(新增 import,不修改现有逻辑) - -### 阶段四:素材卡片(1 天) - -- [ ] 创建 `MaterialItem.tsx`(收起/展开,相关度色条,勾选框) -- [ ] 创建 `MaterialsCard.tsx`(素材列表,默认收起,显示素材数量) -- [ ] 创建 `MaterialsToolbar.tsx`(全选/取消,补充关键词输入) -- [ ] 创建 `InterventionCard.tsx` for PAUSE-1(确认/重搜按钮) -- [ ] 联调测试:素材 SSE 事件 → 素材列表渲染 → 用户确认 → resume 请求 - -### 阶段五:大纲交互(1 天) - -- [ ] 创建 `OutlineSection.tsx`(可内联编辑章节标题和要点) -- [ ] 创建 `OutlineCard.tsx`(大纲树形展示,默认收起) -- [ ] 创建 `InterventionCard.tsx` for PAUSE-2(确认/重规划按钮 + 反馈输入) -- [ ] 测试:编辑大纲后提交 → 后端收到 `edited_outline` → 按修改后大纲写作 - -### 阶段六:草稿确认与编辑审核(1 天) - -- [ ] 创建 `IssueList.tsx`(问题清单,按 major/minor 分组) -- [ ] 创建 `ReviewCard.tsx`(评分 + 问题清单) -- [ ] 创建 `InterventionCard.tsx` for PAUSE-3 和 PAUSE-4 -- [ ] 测试完整流程(搜索 → 大纲确认 → 写作 → 编辑 → 打回 → 重写 → 定稿) - -### 阶段七:弱模型调优(2 天) - -- [ ] 为每个 Agent 添加 Few-Shot JSON 示例 -- [ ] 添加完善的 JSON 解析容错(try/except + fallback 策略) -- [ ] 调整各 Agent 的 prompt,确保弱模型返回合法 JSON -- [ ] 调整 `pass_threshold`(从 80 开始,根据实际测试调整) -- [ ] 测试素材截断逻辑(确保不超过 token 限制) - -### 阶段八:内网对接(未来) - -- [ ] 确认内网搜索 API 文档和认证方式 -- [ ] 在 `config.yaml` 配置 `intranet_field_map` 字段映射 -- [ ] 取消 `intranet_stub.py` 中的注释代码并调整 -- [ ] 切换 `search_provider: intranet` 测试连通性 -- [ ] 验证内网素材卡片(包含 `department`、`classification` 字段)正常展示 - ---- - -> **架构原则**:后端新路由用 `/api/ai-writing/` 前缀完全隔离;前端新建独立组件目录, -> 通过在 `CanvasResizableLayout.tsx` 中新增导入接入,不修改任何已有组件。 -> 参考:Stanford STORM 多 Agent 写作流程、LangGraph HITL interrupt 机制、 -> CrewAI 角色锁定 Prompt 设计、GPT-Researcher 搜索接口抽象。 diff --git a/frontend-web/docs/hebing-1.md b/frontend-web/docs/hebing-1.md deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend-web/docs/hebing.md b/frontend-web/docs/hebing.md deleted file mode 100644 index 4bbe4153..00000000 --- a/frontend-web/docs/hebing.md +++ /dev/null @@ -1,140 +0,0 @@ -# App.tsx 路由与页面结构重构需求(首页与业务页分离) - -## 一、需求背景 - -当前 `frontend-web/src/App.tsx` 同时承载了: - -1. 登录/首页相关路由(如 `/login`、`/landing`) -2. 工作台业务相关路由(如 `/workspace/chats`、`/workspace/agents`、`/workspace/scheduled/*`) -3. 业务页所需布局与运行时包装(`WorkspaceLayout`、`ChatRuntime`) - -这会导致 `App.tsx` 过重,职责不够单一,不利于后续维护和扩展。 - ---- - -## 二、目标(你提出的核心诉求) - -你希望: - -1. **除了登录与首页入口内容外**,将 `App.tsx` 中其余业务页面路由整体迁移到 `pages` 下一个新组件中。 -2. 再创建一个“承接导航/入口”的页面(或路由容器),用于放置这些导航逻辑。 -3. `App.tsx` 最终只保留顶层路由分发和基础 Provider,变得更薄。 - -> 你提到“`App.tsx 74-75 login`”,可理解为:保留登录/首页相关入口,其他工作台内容抽离。 - ---- - -## 三、重构范围 - -### 1)保留在 `App.tsx` 的内容 - -- 全局 Provider:`HashRouter`、`AppProviders` -- 登录/首页路由: - - `/` - - `/login` - - `/login/:username` - - `/landing` -- 404 路由(可留在 `App.tsx` 或交给新路由容器统一处理,二选一) - -### 2)迁移出 `App.tsx` 的内容 - -将以下与工作台有关的路由从 `App.tsx` 抽离: - -- `/workspace`(重定向) -- `/workspace/chats` -- `/workspace/chats/:thread_id` -- `/workspace/agents` -- `/workspace/agents/new` -- `/workspace/agents/:agent_id/chats`(重定向) -- `/workspace/agents/:agent_id/chats/:thread_id` -- `/workspace/scheduled` -- `/workspace/scheduled/tasks` -- `/workspace/scheduled/runs/:run_id` - -以及配套的包装组件职责: - -- `WorkspaceLayout` -- `ChatRuntime` - ---- - -## 四、建议的新结构(方案) - -## 方案 A(推荐):新增“业务路由容器组件” - -在 `pages` 下新增一个路由容器组件,例如: - -- `src/pages/WorkspaceRoutes.tsx`(或 `src/pages/AppWorkspaceRoutes.tsx`) - -职责: -- 专门维护 `/workspace/*` 相关路由 -- 内部统一使用 `WorkspaceLayout` 与 `ChatRuntime` -- 输出一个可被 `App.tsx` 挂载的路由节点 - -同时在 `App.tsx` 里只保留: -- 登录/首页/404 -- 一个 `Route path="/workspace/*" element={} />` - ---- - -## 五、“再建立一个页面放置导航”的落地解释 - -你说的“建立一个页面放置这些导航”,可以有两种理解: - -1. **路由容器页(推荐)**: - - 不强调视觉 UI,重点是集中管理导航与路由跳转规则。 - - 即上面的 `WorkspaceRoutes.tsx`。 - -2. **可见的导航页(可选)**: - - 例如新建 `WorkspaceHomePage.tsx`,展示聊天/Agents/定时任务入口按钮。 - - 路由上把 `/workspace` 指向该页,而不是直接重定向到 `/workspace/chats/new`。 - -当前从你的描述看,优先建议第 1 种(先做结构整理),之后若有产品需求再做第 2 种可见导航页。 - ---- - -## 六、实施步骤(开发任务拆解) - -1. 新建 `WorkspaceRoutes` 组件(`pages` 目录下)。 -2. 把所有 `/workspace/*` 路由从 `App.tsx` 搬入该组件。 -3. 把 `WorkspaceLayout` 与 `ChatRuntime` 移入该组件(或抽到共享 layout/runtime 文件)。 -4. 在 `App.tsx` 中保留登录/首页路由,并挂载 `/workspace/*` 到新组件。 -5. 校验以下访问路径行为一致: - - `/workspace` 重定向 - - chats/agents/scheduled 各页面可正常进入 - - 动态参数路由(`thread_id`、`agent_id`、`run_id`)正常 - - 404 不受影响 -6. 运行前端并做手工回归。 - ---- - -## 七、验收标准 - -1. `App.tsx` 明显瘦身,主要负责顶层入口路由。 -2. `/workspace/*` 全部由新建组件统一管理。 -3. 功能行为与改造前一致(或符合新定义)。 -4. 代码可读性提升:登录入口与业务入口职责分离。 - ---- - -## 八、注意事项 - -1. 当前项目使用 `HashRouter`,路由写法需保持兼容。 -2. `Navigate` 重定向逻辑要完整迁移,避免出现死循环或空白页。 -3. `WorkspaceLayout` 与 `ChatRuntime` 的包裹层级不能错,否则可能影响侧边栏、输入区、子任务/Artifacts 状态。 -4. 若后续再加模块(例如 `/workspace/settings`),优先在 `WorkspaceRoutes` 中扩展。 - ---- - -## 九、建议的命名(可直接采用) - -- 新增组件文件:`src/pages/WorkspaceRoutes.tsx` -- (可选)可见导航页:`src/pages/WorkspaceHomePage.tsx` - ---- - -## 十、后续可选优化 - -1. 将 `WorkspaceLayout` 抽到 `src/layouts/WorkspaceLayout.tsx`。 -2. 将 `ChatRuntime` 抽到 `src/runtime/ChatRuntime.tsx`。 -3. 把路由常量集中到 `src/core/router/paths.ts`,减少硬编码路径。 diff --git a/frontend-web/docs/multi-agent-api.md b/frontend-web/docs/multi-agent-api.md new file mode 100644 index 00000000..a493801e --- /dev/null +++ b/frontend-web/docs/multi-agent-api.md @@ -0,0 +1,265 @@ +# 多智能体协调 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-backend-dev.md b/frontend-web/docs/multi-agent-backend-dev.md new file mode 100644 index 00000000..e759d19d --- /dev/null +++ b/frontend-web/docs/multi-agent-backend-dev.md @@ -0,0 +1,1180 @@ +# 多智能体圆桌研讨 · 后端开发文档 + +本文档面向需要维护或扩展「多智能体圆桌研讨」功能后端的开发者。前端 API 速查见 `multi-agent-api.md`;前端实现见 `multi-agent-frontend-dev.md`;推荐弹窗专项进度见 `recommend-agents-progress.md`(若未提供则当作待补文件)。 + +> ⚠️ 历史版本曾引用 `multi-agent-step1-api.md`、`multi-agent-step3-api.md`,**当前仓库未提供**这两份文件;Step 1(intent)与 Step 3(artifacts)的接口签名直接参考本页第 4 / 7 节即可。 + +源码内已有中文模块级注释(`app/gateway/routers/intent.py`、`recommend.py`、`multi_agent.py`、`artifacts.py` 及 `clarification_utils.py`),与本文互为补充。 + +--- + +## 0. 速读 + +- 三个圆桌 Router(`intent / recommend / multi_agent`)**通过 httpx ASGITransport 进程内直调本机 ASGI app**,复用既有 `/api/threads`、`/api/agents`、`/api/threads/{id}/runs/stream` 路由——**不走 TCP / 不经过网络栈**。所有 client 都由 `multi_agent._make_loopback_client(request)` 工厂统一构造。 +- 历史包袱:早期实现走 `httpx.AsyncClient(trust_env=False)` + `127.0.0.1:`,遇到开发机 `HTTP_PROXY` / `HTTPS_PROXY` 时仍可能把回环走代理(症状 `headers_in=42.160s`)。**改用 ASGITransport 后此问题不再存在**,相关 `trust_env=False` workaround 全部移除。 +- **席位 agent 持久注册**(磁盘 `.deer-flow/agents/` + `_sync_legacy_agents`),**协调智能体每次新建**(命名 `roundtable-coordinator-{timestamp36}`,DB 累积,需后续 GC)。 +- **`.deer-flow/` 整个目录被 git ignore**(仓库根 `.gitignore` 第 64 / 74 行),`git clone` 后内置 agent 目录在部署机上**不会存在**——任何环境都必须把 `roundtable-intent` + `roundtable-recommender` 两个**功能型 agent** 持久化进发布物,否则 `/api/intent/stream` 报 500。**互联网 demo 还需要 6 个示例研讨席位**(`roundtable-intelligence` 等),**内网部署用客户自有 agent 替代**这些席位,无需打包。详见 §11.7。 +- **`init` 是 SSE 而非一次性 JSON**:用 `asyncio.as_completed` 按真实完成顺序推送 `seat_ready`,前端因此能逐个点亮席位。 +- **`init_done.thread_ids` 包含协调智能体自身**(`agent_threads[main_agent_name] = coord_thread`),前端 Step 3 列文件时需自行过滤。 +- **澄清协议统一**:Step 1 与 Step 2 leader 都借 `ask_clarification` + 终态 `clarification` 帧驱动同一套选项卡片组件;`allow_multiple` 默认 false(单选、点击即发送)。 +- **leader 共识 = `status: []` 空数组**;澄清 = `status: "clarification"`(字符串);派活 = `status: [[name, task], ...]`。这三者**互斥不混用**。 +- **后端 picks clamp 2–6,前端用户可手动加到 8**。 +- **不写死任何具体模型名**:`intent / recommend / multi_agent` 三个 router 的 `model_name = str(payload.get("model") or "")`,空串会被下游 `lead_agent._resolve_model_name()` 自动回退到 `config.yaml` 的 `models[0]`(与 `/api/ai-writing` 行为一致)。内网部署只需配 `config.yaml`,前后端均无需改硬编码。 + +--- + +## 1. 功能定位与三步数据流 + +对应前端页面:`/page/workspace/roundtable/planning`(`RoundtablePlanningPage.tsx`)的三个步骤。 + +``` +Step 1 任务意图理解 + POST /api/intent/init → 建 thread,绑定内置 roundtable-intent + POST /api/intent/stream (多轮) → 澄清对话,终态 [INTENT_READY] + summary + + ↓ intentReady { objective, constraints, assumptions } + +推荐弹窗(Step 1 → Step 2 之间,可选) + POST /api/recommend/stream → 从候选池选 2–6 个席位,[RECOMMEND_READY] + picks + + ↓ selectedAgents[] // 前端可再手动调整到 2-8 + +Step 2 多智能体圆桌研讨 + POST /api/multi-agent/init (SSE) → 协调智能体 + 各席位 thread(asyncio.as_completed 流式推 seat_ready) + POST /api/multi-agent/run/stream → leader 派活 / special 执行 / 澄清 + + ↓ lastLeaderContent、各席位交付、hasConsensus + +Step 3 最终方案展示 + GET /api/threads/{tid}/artifacts → 列出 outputs/ 产出文件 + GET /api/threads/{tid}/artifacts/{path} → 预览或下载 + PATCH /api/threads/{tid}/artifacts/{path} → 文本类产物原地编辑(可选) + (总控结论、席位文本主要来自 Step 2 前端内存缓存,一般不再调 run/stream) +``` + +| 步骤 | 前端 `currentStep` | 专属 Router | 内置 Agent | +|------|-------------------|-------------|------------| +| 1 | 1 | `intent.py` | `roundtable-intent`(固定 id,复用) | +| 1→2 | 推荐弹窗 | `recommend.py` | `roundtable-recommender`(每请求新建 thread) | +| 2 | 2 | `multi_agent.py` | 协调智能体(每次新建)+ 用户选定的 `roundtable-*` 席位 | +| 3 | 3 | `artifacts.py`(通用) | 无新 agent | + +所有接口需 `Authorization: Bearer `。Loopback 调用一律由 `_make_loopback_client(request)` 构造(`httpx.ASGITransport(app=request.app)`,base_url 占位 `http://loopback`),**不经 TCP**,因此不再受本机 `HTTP_PROXY` 干扰。 + +--- + +## 2. 涉及的后端文件 + +``` +offline-backend-20260512/backend/ +├── app/gateway/ +│ ├── app.py # include_router: multi_agent, intent, recommend (artifacts 已存在) +│ └── routers/ +│ ├── multi_agent.py # Step 2:init(SSE) + run/stream + 共享的 loopback 工具 +│ ├── intent.py # Step 1:init + stream(import multi_agent 工具) +│ ├── recommend.py # Step 1→2:推荐 stream(import multi_agent 工具) +│ └── artifacts.py # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用) +├── packages/harness/deerflow/tools/builtins/ +│ └── clarification_utils.py # resolve_allow_multiple + coerce_bool(Step1/Step2 共享) +└── .deer-flow/agents/ + ├── roundtable-intent/ # Step 1 任务理解(SOUL 含完整工作流,可不挂 Skill) + ├── roundtable-recommender/ # 推荐智能体 + ├── roundtable-intelligence/ # 默认可选席位(6 个,前端可替换) + ├── roundtable-environment/ + ├── roundtable-solution-design/ + ├── roundtable-risk-review/ + ├── roundtable-execution-plan/ + ├── roundtable-summary/ + └── roundtable-coordinator-/ # 协调智能体壳,每次会话新建,长期累积 +``` + +**内置 agent 注册**:各 `roundtable-*` 目录通过 `_sync_legacy_agents()`(`app/gateway/routers/agents.py`)在下次 `/api/agents` 请求时 upsert 进 `agents` 表,`user_id IS NULL` 表示内置。`recommend.py` 不依赖此表,前端会过滤 `roundtable-coordinator-*` 出候选池。 + +**Router 间共享导出**:`intent.py` 与 `recommend.py` 从 `multi_agent.py` import `_auth_headers / _create_thread / _flatten_content / _loopback_base / _parse_last_messages / _run_payload / _stream_upstream` 等底层工具,**不要**在 intent / recommend 重复实现 loopback 逻辑。 + +**共享工具逻辑**:`clarification_utils.py` 的 `resolve_allow_multiple(args)` 与 `coerce_bool(value, default)` 被 `intent.py` / `multi_agent.py` / `ClarificationMiddleware` 共用: +- `coerce_bool` 把 LLM tool args 里可能出现的 bool / 数字 / 字符串(`"true"`、`"1"`、`"yes"`、`"y"`、`"on"` / `"false"`、`"0"`、`"no"`、`"n"`、`"off"`)统一成 bool; +- `resolve_allow_multiple` 仅 `args["allow_multiple"]` 显式为真时返回 `True`,缺省 / `null` 都视为 `False`(单选)。 + +--- + +## 3. 接口总览 + +| 方法 | 路径 | 步骤 | 用途 | +|------|------|------|------| +| POST | `/api/intent/init` | 1 | 创建意图澄清 thread | +| POST | `/api/intent/stream` | 1 | SSE,一轮用户对话 | +| POST | `/api/recommend/stream` | 1→2 | SSE,一次性推荐席位 | +| POST | `/api/multi-agent/init` | 2 | SSE,**按完成顺序**推 `seat_ready`,最后 `init_done` | +| POST | `/api/multi-agent/run/stream` | 2 | SSE,leader 或 special 一轮 | +| GET | `/api/threads/{thread_id}/artifacts` | 3 | 列出该 thread 的 outputs 文件 | +| GET | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 预览/下载单个产出(含 `.skill/` 内文件提取) | +| PATCH | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 编辑文本类产物(白名单后缀) | + +字段级请求/响应见各 step 的 API 文档;下文只写**后端实现语义**。 + +--- + +## 4. Step 1:任务理解(`intent.py`) + +### 4.1 定位 + +轻量包装 Lead Agent 运行时,固定跑内置 `roundtable-intent`(`INTENT_AGENT_ID` 常量)。不做 broadcast、不派活;每轮用户消息对应一次 `/stream`。 + +### 4.2 `POST /api/intent/init` + +1. `_create_thread()` → LangGraph thread +2. 返回固定 `agent_id: "roundtable-intent"`(**不**每次新建 agent) + +请求体:`{"model": "deepseek-chat"}` — `model` 可选,**此处不写入 thread**,每轮 `/stream` 自己带 model。 + +与 Step 2 `/init` 的区别:无协调智能体、无 `agent_names` 列表。 + +### 4.3 `POST /api/intent/stream` + +**入参**:`{thread_id, message, model?}`,缺 `thread_id` 或 `message` → 400。 + +**上游 run_payload**:见 §6.6;`excluded_tools` 屏蔽**除 `ask_clarification` 外的几乎全部工具**,硬清单: + +```python +[ + "web_search", + "present_files", + "view_image", + "agent_orchestration", + "bash", "ls", "read_file", "write_file", "str_replace", + "memory", "hindsight_recall", "hindsight_reflect", "hindsight_retain", + "task", "write_todos", +] +``` + +写死黑名单而不是「只允许 ask_clarification」的原因:SOUL 文字约束不可靠,硬排除更稳;防止模型偏离去写方案文档或派活。 + +**流式**:透传 LangGraph SSE → `_parse_last_messages(captured)` 从倒数 `values` 快照拿 `messages`。 + +**终态帧判定顺序(重要,勿调整)**: + +1. **`[INTENT_READY]` 优先**:从最新消息向前扫描每条 AIMessage,正则 `_INTENT_READY_PATTERN = /\[INTENT_READY\]\s*```json\s*(\{.*?\})\s*```/s` 命中后 `json.loads` 解析 + - 命中 → `status: "done"`,`summary` 为 JSON、`content` 为含标记的整段 + - **优先于** `ask_clarification`:模型可能在较早的 AIMessage 里已 emit `[INTENT_READY]`,但本轮残留 `ask_clarification` 的 tool_call;不先扫 READY 前端会卡在澄清 UI +2. 否则用 `_find_dispatch_message(messages)` 找最后一条带 `tool_calls` 的 AIMessage,检查是否含 `ask_clarification` + - 命中 → `status: "clarification"`,附结构化字段: + ```json + { + "status": "clarification", + "content": "", + "question": "...", + "clarification_type": "missing_info", + "clarification_context": "...", + "options": ["..."], + "allow_custom": true, + "allow_multiple": false, + "summary": null + } + ``` + - `content` 取 `_find_trailing_ai_text(messages, exclude_text=preamble)`:ClarificationMiddleware 常在 tool 后还有一条纯文本 AIMessage,比 tool_call 旁的短 preamble 更适合作为气泡正文 + - `allow_multiple` 由 `resolve_allow_multiple(cargs)` 决定(缺省 false) +3. 否则 → `status: "asking"`,`content` 为 `_flatten_content(dispatch_msg.content)`,前端继续 chat + +**异常**:`HTTPException` / 其它异常都返回 `{"status": "error", "content": "...", "summary": null}`,前端展示红色错误气泡。 + +**`summary` 形状**(`done` 时): + +```json +{ + "objective": "一句话目标", + "constraints": ["硬约束 1", "..."], + "assumptions": ["可选假设"] +} +``` + +### 4.4 内置 Agent:`roundtable-intent` + +- 配置:`.deer-flow/agents/roundtable-intent/config.yaml` + `SOUL.md` +- SOUL 内写清:工作流、`ask_clarification` 用法、`[INTENT_READY]` 格式、禁止派活/写文件 +- **可不配置 `skills`**;若配置 Skill,SOUL 要求不得偏离澄清与结束协议 + +--- + +## 5. 推荐智能体(`recommend.py`,Step 1 → 2) + +### 5.1 定位 + +无状态单次调用:每次请求内部 `_create_thread()`,**不暴露 `/init`**。前端传入 Step 1 的 `intent` + 过滤后的 `candidates`(剔除 `roundtable-intent`、`roundtable-recommender`、历史 `roundtable-coordinator-*`)。 + +### 5.2 `POST /api/recommend/stream` + +**入参校验**: + +- `intent.objective` 必填(trim 后非空) +- `candidates` 必须是非空数组 +- 至少一个有效 `agent_id`(trim 后非空) +- 三者缺一 → 400 + +**Prompt 拼装**(`_build_prompt`): + +``` +任务目标: +约束:; ; ... +关键假设:; ... + +候选智能体池: +- (): +- ... + +请按 SOUL 中规定的格式输出推荐结果(自然语言思路 + [RECOMMEND_READY] + json 块)。 +``` + +格式刻意贴近 SOUL 示例,让模型有清晰的复刻模板。 + +**上游 run_payload**:`agent_name=RECOMMENDER_AGENT_ID`、`excluded_tools=["web_search", "present_files", "view_image", "agent_orchestration", "ask_clarification"]` — 纯判断,不调任何工具。 + +**终态分支**: + +- 取**最后一条非空 AIMessage** 的文本(`last_ai_content`) +- 正则 `_RECOMMEND_READY_PATTERN = /\[RECOMMEND_READY\]\s*```json\s*(\{.*?\})\s*```/s` 匹配 picks JSON +- `_validate_picks(parsed.picks, candidate_ids)`: + - 跳过非 dict / 缺 `agent_id` / 不在候选池 / 已 seen 的项 + - 至多保留 `_MAX_PICKS = 6` 个 + - 不足 `_MIN_PICKS = 2` → 返回 `[]`,触发 fallback +- 有效 picks ≥ 2 → `status: "done"`,`picks` 有值 +- 否则 → `status: "asking"`(**不是模型在追问用户**,而是「网关未能产出合法 picks」,前端走 FALLBACK 默认 6 席) + +```json +{ + "status": "done", + "content": "<自然语言推荐理由>", + "picks": [{"agent_id": "roundtable-risk-review", "reason": "..."}] +} +``` + +**picks 数量上下限的不一致**(需要心里有数): + +| 边界 | 后端 | 前端弹窗 | +|------|------|----------| +| 下限 | `_MIN_PICKS = 2`(不足返 asking) | `MIN_SELECTED = 2`(确认按钮校验) | +| 上限 | `_MAX_PICKS = 6`(截断模型输出) | `MAX_SELECTED = 8`(允许用户手加 2 个) | + +后端只对**模型输出**做 clamp,用户可手动追加候选池里其它 agent 到 8 个。 + +**前端流式展示**:自然语言理由与 JSON 由 `[RECOMMEND_READY]` 分界;`TitleMiddleware` 产生的标题增量会在客户端过滤(`langgraph_node` 含 `TitleMiddleware` / `tags` 含 `middleware:title`),避免污染推荐理由区。 + +### 5.3 与 SOUL 的契约 + +模型须在自然语言思路后输出: + +``` +[RECOMMEND_READY] +```json +{"picks": [{"agent_id": "...", "reason": "..."}, ...]} +``` +``` + +`agent_id` 必须是候选池内已有的 id,`_validate_picks` 会做白名单校验。 + +--- + +## 6. Step 2:多智能体研讨(`multi_agent.py`) + +### 6.1 `POST /api/multi-agent/init` — SSE,非一次性 JSON + +``` +请求体: +{ + "agent_names": ["roundtable-intelligence", ...], // 用户选的席位 id + "main_agent_name": "roundtable-coordinator-xxx", // 当次总控 id,前端生成 + "model": "deepseek-chat" // 可选,写入 coordinator agent +} +``` + +**早期校验**(同步抛 HTTP 4xx,**不**进 SSE): + +- `agent_names` 必须非空 +- 每个 `agent_names` 元素 + `main_agent_name` 都需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好) +- 所有 id 都 `.lower()` 后比较 / 存储 + +**SSE 内部流程**: + +``` +asyncio.as_completed(各席位 _prepare_sub_agent) + 每完成一个 → data: {"event":"seat_ready","agent_name","thread_id","display_name","description"} + (按真实完成顺序,先完成先发,前端因此能逐个点亮) + +异常时:取消所有未完成的 task,避免泄露到 AsyncClient close 之后 + +协调智能体: + POST /api/threads → coord_thread + POST /api/agents + skills=["agent_orchestration"] + soul=动态拼接(本次 init 的席位列表 + 派活规则) + id=main_agent_name(前端生成的 roundtable-coordinator-) + agent_threads[main_agent_name] = coord_thread // ⚠️ thread_ids 含 coordinator 自身 + → data: {"event":"coordinator_ready","main_agent_name","thread_id"} + +→ data: {"event":"init_done","main_agent_name","thread_ids":{...}} + +失败 → data: {"event":"error","detail":"..."} +``` + +**协调智能体创建幂等**:若 `_create_agent` 返回 (False, "exists ...")(同名 agent 已存在)则继续往下走,不报错。注意这里的「已存在」会复用历史 agent 的 SOUL(与本次动态拼接的不同),通常只在前端用同一个时间戳跑过两遍时触发。 + +**协调智能体 SOUL**(动态拼接,与席位列表一起写入): + +``` +#核心定位: +用途:根据用户的任务目标,调用其他智能体干活,只负责协调汇总, +当你觉得已有信息支持回答用户问题时候,停止调度,并输出最后的结果 + +#调度方法: +通过使用 agent_orchestration 技能调用其他智能体干活,使用技能时传入 +agent_name 和需要做的活。 +⚠️ agent_name 必须严格使用下方列出的英文 id(例如 roundtable-intelligence), + 不要把括号里的中文显示名拼进 agent_name 里。 + +#可调度智能体: +- agent_name: roundtable-intelligence(情报收集): +- agent_name: roundtable-environment(环境评估): +... +``` + +中文显示名通过 `GET /api/agents/{id}` 取 `name`、描述取 `description`。**强调英文 id** 是因为部分模型会把整段 `roundtable-x(中文)` 当 `agent_name` 塞回来,靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。 + +### 6.2 `/run/stream` — `agent_type = "leader"`(`_leader_run`) + +``` +1. 并行启动 pre_broadcast_task(不 await):向除自己外所有 thread 追加 + "总控智能体收到用户消息:" human 消息 +2. _run_payload + _stream_upstream(POST /api/threads/{coordinator}/runs/stream) + - excluded_tools: ["web_search"] // 只禁联网,其余工具(含 agent_orchestration)保留 + - skill_stop_names: {"agent_orchestration", *callers_extra} + // 网关自动并入 agent_orchestration,让 SkillStopMiddleware 在工具调用后停图, + // 便于本路由从 AIMessage.tool_calls 解析派活列表 +3. 透传上游 SSE,_parse_last_messages 拿 messages +4. await pre_broadcast_task(LLM 通常已跑完,这里 await 接近 0 成本) +5. _find_dispatch_message(messages) 找带 tool_calls 的 AIMessage +6. 分三支: + + 分支 A — ask_clarification: + {"status": "clarification", "content": follow_up 或 question 或 leader_text, + "question", "clarification_type", "clarification_context", + "options", "allow_custom", "allow_multiple", "agent_name"} + ⚠️ 空 dispatched 不等于共识,前端展示澄清卡片并暂停自动派活循环 + + 分支 B — 有 tool_calls(派活): + _extract_dispatch(tool_call)解析每个 tool_call → (sub_name, task_text) + 并行 _broadcast("总控智能体给 X 派活,内容为:Y", skip=[leader, sub_name]) + 等所有派活广播完成(asyncio.gather) + {"status": [[sub_name, task_text], ...], "content": leader_text, "agent_name": agent_name} + + 分支 C — 无 tool_calls: + {"status": [], "content": leader_text, "agent_name": agent_name} + // 共识达成,前端 hasConsensus = true +``` + +**`_extract_dispatch` 双格式支持**: + +1. **结构化**:`{"name": "agent_orchestration", "args": {"agent_name": "...", "task": "..."}}` + — 大多数 LLM 在 SOUL 引导下走这条 +2. **Legacy bash argv**:`{"name": "bash", "args": {"command": '... .py "X" "Y" ...'}}` + — 正则 `_TOOL_CALL_PATTERN = /\.py\s+"([^"]*)"\s+"([^"]*)"/`,兼容旧 skill;不可移除 + +两条路径都会过 `_strip_display_suffix`:用 `re.split(r"[^A-Za-z0-9_-]", agent_name, maxsplit=1)[0]` 截断第一个非合法 id 字符之后的内容,把 `roundtable-x(中文)` 还原为 `roundtable-x`;剥离后若仍违规,原样返回让上游 422。 + +**时序日志**(grep `[leader-timing]`): + +``` +payload_built — 构造请求体耗时(broadcast 已经在后台跑) +first_upstream_line — 上游首行(LangGraph 启动 + LLM 首字节) +upstream_stream_complete — 整条 SSE 透传完成 +pre_broadcast_await — 必须 await 才能 yield 终态;>0 表示广播比 LLM 还慢 +dispatch_broadcasts_await — 派活广播并行 gather 耗时 +``` + +### 6.3 `/run/stream` — `agent_type = "special"`(`_special_run`) + +``` +1. _run_payload + _stream_upstream(POST /api/threads/{sub}/runs/stream) + - excluded_tools: ["web_search", "ask_clarification", "present_files", + "view_image", "agent_orchestration"] + // 子智能体禁派活/澄清/文件呈现/图片;只做交付 +2. 透传 SSE,_parse_last_messages 拿最后一条 AIMessage 的 content +3. 启动 post_broadcast_task("子智能体 X 完成 Y 工作,交付内容为:..."),不 await +4. yield {"status": "子智能体X完成X工作", "content": "<交付正文>", "agent_name": "..."} + // ⚠️ 在 yield 之后再 await broadcast,让前端先收到终态、关 loading +5. await post_broadcast_task(SSE 连接稍微多保持一会;前端早就拿到结果了) +``` + +**时序日志**(grep `[special-timing]`): + +``` +payload_built +first_upstream_line +upstream_stream_complete +post_broadcast_total — yield 之后 await broadcast 的总耗时 +``` + +### 6.4 `_broadcast` 与 `_append_thread_message` 机制 + +```python +async def _append_thread_message(client, base, headers, thread_id, content): + # GET /api/threads/{tid}/state → state + # state["values"]["messages"].append({"type": "human", "content": content}) + # POST /api/threads/{tid}/state + # 失败仅打 warning,不阻断主流程 + +async def _broadcast(client, base, headers, message, agent_threads, skip): + targets = [tid for name, tid in agent_threads.items() if name not in skip] + await asyncio.gather(*(_append_thread_message(...) for tid in targets), + return_exceptions=True) +``` + +**目的**:各 thread 对话历史里保留「谁派活、谁交付」的群聊上下文;**`skip` 通常含发起方与接收方**,避免回声。 + +**三种触发**: + +| 触发点 | 内容 | skip | +|--------|------|------| +| leader pre_broadcast | `总控智能体收到用户消息:` | `[leader]` | +| leader 派活 | `总控智能体给 X 派活,内容为:` | `[leader, sub_name]` | +| special 后广播 | `子智能体 X 完成 Y 工作,交付内容为:` | `[sub]` | + +**注意**:广播是 best-effort,对方 LLM 真正干活仍需前端显式 `/run/stream` 调到对应席位。 + +### 6.5 终态帧协议(leader / special / clarification) + +前端 `streamMultiAgent` 用 `isFinalStatusFrame()` 识别:**同时存在 `status` 与 `content`**。 + +| `status` 类型 | 含义 | +|---------------|------| +| `[["agent","task"],...]` | 派活列表(数组);`[]` = 共识 | +| `"clarification"` | 总控追问,需用户回复后再 leader | +| `"子智能体 X 完成..."` | special 完成(字符串) | +| `"error"` | 异常 | + +与 Step 1 的 `clarification` 帧字段对齐(`question`、`options`、`allow_multiple` 等),便于复用选项卡片组件。 + +### 6.6 `_run_payload` 完整字段 + +```python +{ + "input": {"messages": [{"type": "human", + "content": [{"type": "text", "text": new_message}], + "additional_kwargs": {}}]}, + "config": {"recursion_limit": 1000}, + "context": { + "agent_name": , + "model_name": "deepseek-chat", + "mode": "pro", + "reasoning_effort": "medium", + "thinking_enabled": True, + "is_plan_mode": False, + "subagent_enabled": False, + "thread_id": , + }, + "stream_mode": ["messages-tuple", "values"], + "stream_subgraphs": True, + "stream_resumable": True, + "assistant_id": "lead_agent", + "on_disconnect": "continue", + "excluded_tools": <按 router 不同>, + "skill_stop_names": , +} +``` + +字段含义: + +- `recursion_limit: 1000` — 防止 LangGraph 循环深度兜底(圆桌实际很难达到) +- `thinking_enabled: True` — 开启模型的扩展思考(CoT),需 model 支持 +- `is_plan_mode: False`、`subagent_enabled: False` — 圆桌内禁用 TodoList / subagent 中间件,避免与多智能体协调冲突 +- `stream_mode: ["messages-tuple", "values"]` — 同时需要逐 token 增量与最后一次完整 values 快照 +- `assistant_id: "lead_agent"` — 走 Lead Agent 工厂 +- `on_disconnect: "continue"` — 客户端断连后上游继续跑完(不丢 token 但客户端拿不到) + +### 6.7 上游 SSE 透传与 `_parse_last_messages` + +`_stream_upstream` 逐行 yield 上游 `/api/threads/{tid}/runs/stream` 的 SSE。返回元组: + +``` +(line, None) — 原样转发给浏览器(含 event: / data: 行) +(None, captured) — 流结束,captured 是所有 data: 行的字符串列表 +``` + +日志 `[upstream-timing] headers_in=<>` / `first_line=<>`:排查上游首字节延迟;改 ASGITransport 后通常 `headers_in` < 10ms,长尾大概率出在路由 handler 内部(不再是网络/代理问题)。 + +`_parse_last_messages(captured)` 从倒数往前扫,**跳过 trailing `data: null` end 帧**,找第一条解析为 dict 且含 `messages` key 的 frame;取 `messages` 列表。无可解析帧抛 `502`。 + +### 6.8 三个 AIMessage 搜索辅助 + +| 函数 | 用途 | +|------|------| +| `_find_dispatch_message(messages)` | 从后向前找带 `tool_calls` 的 AIMessage(跳过 SkillStop / Clarification 注入的 ToolMessage);都没有则返回最后一条 AIMessage | +| `_find_trailing_ai_text(messages, exclude_text)` | 从后向前找**无 tool_calls** 的 AIMessage 的纯文本(用于澄清后的 follow-up,排除等于 preamble 的项) | +| `_flatten_content(content)` | 把 `str` / 结构化 content blocks `[{"text": ...}]` 拍平成单串 | + +--- + +## 7. Step 3:产出文件(`artifacts.py`,通用 Thread API) + +Step 3 **没有** 单独的 `multi_agent` 路由;使用既有 artifacts 能力,圆桌只是消费者之一。 + +### 7.1 `GET /api/threads/{thread_id}/artifacts` + +- 鉴权:`@require_permission("threads", "read", owner_check=True)` +- 扫描:`get_paths().sandbox_outputs_dir(thread_id, user_id=get_effective_user_id())` + - 解析为 `.deer-flow/users/{user_id}/threads/{thread_id}/user-data/outputs/` +- `collect_outputs_listing(outputs_dir)`: + - 递归 `rglob("*")`,过滤非文件 + - 软链接安全检查(`resolved.relative_to(outputs_root)`,越界丢弃) + - 按 `modified_at` 倒序 +- 目录不存在 → `files: []`(**非 404**) + +```json +{ + "thread_id": "...", + "files": [ + { + "path": "mnt/user-data/outputs/report.md", + "name": "report.md", + "size_bytes": 12480, + "mime_type": "text/markdown", + "modified_at": "2026-05-23T14:32:30.123Z" + } + ] +} +``` + +前端 `listSessionArtifacts(threadId)` 封装此接口;Step 3 对协调 thread 与各席位 thread 分别拉取,合并为下载清单(实际只列子席位)。 + +### 7.2 `GET /api/threads/{thread_id}/artifacts/{path}` + +- 鉴权同上 +- 虚拟路径前缀 `mnt/user-data/outputs/...`,经 `resolve_thread_virtual_path` 转物理路径并防穿越 +- **特殊:`.skill/` 压缩包透视**:若 `path` 含 `.skill/`,从 ZIP 提取内部文件(如 `xxx.skill/SKILL.md`)。`_extract_file_from_skill_archive` 同时支持「直接路径命中」与「带顶层目录前缀」两种压缩包结构。结果带 `Cache-Control: private, max-age=300` 头避免重复解压 +- `ACTIVE_CONTENT_MIME_TYPES = {"text/html", "application/xhtml+xml", "image/svg+xml"}` 始终强制 `Content-Disposition: attachment`(防止 XSS 与脚本执行) +- 文本类(mime 以 `text/` 开头)→ `PlainTextResponse` +- 后缀判定不到、但 8KB 内无 null 字节 → 当文本返回(`is_text_file_by_content`) +- 其它 → `FileResponse` 或 `Response` 内嵌 +- `?download=true` 对所有类型强制附件下载 + +### 7.3 `PATCH /api/threads/{thread_id}/artifacts/{path}` + +- 鉴权:`@require_permission("threads", "write", owner_check=True)` +- 请求体:`{"content": "<新内容>"}` +- 限制: + - `.skill/` ZIP 内文件只读(400) + - `ACTIVE_CONTENT_MIME_TYPES` 拒绝(400 active artifacts are read-only) + - 文件后缀需在 `EDITABLE_TEXT_SUFFIXES = {.md, .markdown, .txt, .json, .yaml, .yml, .csv, .xml, .log}` 之内,**或** mime 以 `text/` 开头 +- 用 UTF-8 覆写原文件,返回 `PlainTextResponse(body.content)` + +圆桌 Step 3 当前主要消费 GET,PATCH 用于「在线修订交付稿」类场景,前端默认未启用。 + +### 7.4 与 Step 2 内存的关系 + +**总控结论、各席位最终段落**:主要来自 Step 2 前端缓存的 `lastLeaderContent`、`step2RoundtableDialogues`(filter `confidence === "子智能体交付"`),**不**为 Step 3 新增聚合接口;如后续需要后端聚合,可加 `sessions/summary` 草案路由。 + +--- + +## 8. 内置席位 SOUL 设计要点 + +每个 `roundtable-*`(除 intent / recommender)遵循统一结构:角色定位、核心职责、输出风格(emoji + 简短结论)、边界(只产出本席位、等总控派活)。 + +| 席位 | agent_id | emoji | 核心职责 | +|------|----------|-------|----------| +| 情报收集 | `roundtable-intelligence` | 📢 | 同类案例 + 历史风险 | +| 环境评估 | `roundtable-environment` | 🌍 | 硬件容量 + 承载力 | +| 方案设计 | `roundtable-solution-design` | 📐 | 主备方案 + 关键路径 | +| 风险审查 | `roundtable-risk-review` | ⚠️ | 合规红线 + 收敛建议 | +| 执行规划 | `roundtable-execution-plan` | 📅 | 里程碑 + 资源 | +| 总结陈述 | `roundtable-summary` | 🎤 | 归纳 + 冲突标记 | + +子智能体**不**安装 `agent_orchestration`;仅协调智能体安装,保证单调度源。 + +--- + +## 9. 关键设计决策 + +### 9.1 ASGITransport 进程内 loopback(替代旧的 HTTP loopback) + +三个 router 通过 `httpx.ASGITransport(app=request.app)` 直调本机 ASGI app,**不走 TCP / 不开 socket / 不经网络栈**,但仍然完整复用 FastAPI 路由分发、鉴权 middleware、Pydantic 校验、SSE 流式响应。代价仅是路由层的少量 dispatch 开销(< 1ms),相对 LLM run 本身的数百 ms~数十秒可忽略。 + +统一入口:`multi_agent._make_loopback_client(request)`,返回: + +```python +httpx.AsyncClient( + transport=httpx.ASGITransport(app=request.app), + base_url="http://loopback", # 占位 host,ASGITransport 不在意 + timeout=None, # SSE 长连接仍需 None +) +``` + +**已废止的 `trust_env=False` workaround**:旧版本走 `127.0.0.1:` 真实 HTTP,需要 `trust_env=False` 阻止 httpx 读 `HTTP_PROXY` 把回环也走代理(症状日志 `headers_in=42.160s`)。改 ASGITransport 后这个问题在物理上已不存在——根本没走网络栈。 + +### 9.2 协调智能体每次新建 + +`main_agent_name` 形如 `roundtable-coordinator-{timestamp36}`,会话隔离;长期会累积 agent 目录(实际仓库已有数十个 `roundtable-coordinator-mp*`),需后续 GC 策略: + +- 选项 A:定时任务清理 N 天前无活跃 thread 的 coordinator agent +- 选项 B:每次创建前 LRU 清理(注意:跨服务实例难协调) +- 选项 C:把 coordinator 实例化为「内置 + 动态 SOUL」复用同一个 id + +### 9.3 澄清协议统一 + +Step 1 与 Step 2 leader 均通过 `ask_clarification` + 终态 `clarification` 帧驱动选项卡片;`[INTENT_READY]` / 派活列表 / `[]` 各自表示阶段结束语义,互不混用。 + +### 9.4 选项单选默认 + +`resolve_allow_multiple()`:**未传则 false**(单选、点击发送);仅显式 `allow_multiple: true` 时多选。降低模型把 `missing_info` 全标成多选导致 UX 混乱的概率。`coerce_bool` 容忍 LLM 返回的字符串/数字 bool。 + +### 9.5 SSE 末尾状态帧 + +LangGraph 原生流不含「派活结果 / 意图 summary / 推荐 picks」;各 router 在流末追加 JSON `data:` 帧,前端用 `isFinalStatusFrame()` / `isFinalIntentFrame()` / `isFinalRecommendFrame()` 与 LangGraph 帧区分解析。 + +### 9.6 SkillStopMiddleware 配合 leader 派活 + +leader run 自动并入 `skill_stop_names = {"agent_orchestration", ...}`,让 `SkillStopMiddleware` 在 `agent_orchestration` 工具调用后立刻停图。这样网关能从 `AIMessage.tool_calls` 直接读出派活列表,不必等 LLM 真把任务执行完。 + +### 9.7 init `agent_threads` 含 coordinator 自身 + +`init_done.thread_ids` 是 `{agent_name: thread_id}` 形式,其中 `main_agent_name` 自己的 thread 也在里面。前端 Step 3 列文件时需自行过滤掉 coordinator thread,否则会多调一次 `listSessionArtifacts`(虽然结果会是空,但有冗余 HTTP)。 + +--- + +## 10. 性能特征 + +| 阶段 | 已做优化 | 典型耗时 | +|------|----------|----------| +| Step 2 `/init` | 席位 `asyncio.as_completed` + 流式 `seat_ready` + 去掉 intro 广播 | 约 0.5–1.2s(视席位数) | +| LLM `/run/stream` | — | 数百 ms~数十秒(主因) | +| leader pre_broadcast | 与上游 LLM run 并发启动 | 通常已在 LLM 完成前结束 | +| leader 派活广播 | `asyncio.gather` 并行 | < 200ms(同 thread state R/W) | +| special post_broadcast | yield 之后才 await,前端先关 loading | 不阻塞 UX | +| Step 1/推荐 | 单 thread,无 broadcast | 与模型相关 | + +**时序日志关键字**(开发自带): + +```bash +grep "init-timing" .runtime/logs/*.log # 仅前端打,作参考 +grep "upstream-timing" .runtime/logs/*.log # _stream_upstream 内 headers_in / first_line +grep "leader-timing" .runtime/logs/*.log # payload_built / first_upstream_line / dispatch_broadcasts_await ... +grep "special-timing" .runtime/logs/*.log # payload_built / first_upstream_line / post_broadcast_total +``` + +仍存在的开销:`GET /api/agents/{id}` 触发 legacy sync 扫目录、`GET/POST /state` 广播等。未来可考虑 loopback 改内部函数调用。 + +--- + +## 11. 依赖与扩展点 + +### 11.1 `agent_orchestration` 技能 + +协调智能体派活依赖此技能;正则 `_TOOL_CALL_PATTERN` 从 tool 输出提取 `agent_name` 与 task。技能缺失时 leader 易变成「无 tool_calls → 空 status → 前端误判共识」。 + +### 11.2 模型 + +`model` 写入 run 的 `context.model_name`;前端可按席位覆盖 special 调用的模型(见前端文档 §5.5 Persona 抽屉)。 + +**绝不在路由里硬编码具体模型名**。三个 router 一律 `str(payload.get("model") or "")`: + +- 前端不传 / 传空串 → 下游 `lead_agent._resolve_model_name("")` 命中 falsy 分支,返回 `app_config.models[0].name`(`config.yaml` 配置顺序里的第一项) +- 前端传了一个不在 `config.yaml` 里的 model 名 → 同样回退到首项,并 `logger.warning("Model 'x' not found ... fallback to ...")` +- 前端传了合法 model 名 → 直接使用 + +这是 `/api/ai-writing` 与圆桌三路由的统一行为,让本地 / 内网部署只需要改 `config.yaml`,不需要触前端常量或路由代码。详见 `deerflow/agents/lead_agent/agent.py:_resolve_model_name`。 + +### 11.3 新增席位 + +1. `.deer-flow/agents/roundtable-xxx/` 增加 `config.yaml` + `SOUL.md` +2. 前端候选池 / 推荐逻辑纳入该 id +3. Step 2 `agent_names` 传入即可 — **multi_agent 路由无需改** + +### 11.7 内置 agent 部署持久化(必读,关系到内网部署能否启动) + +#### 11.7.1 问题背景 + +圆桌功能涉及的 agent 都靠**磁盘文件**激活(`.deer-flow/agents//{config.yaml,SOUL.md}`),但仓库根 `.gitignore` 第 64 / 74 行有: + +``` +offline-backend-20260512/backend/.deer-flow/* +``` + +整个 `.deer-flow/` 被 git 排除。新机器 `git clone` 后 `.deer-flow/agents/` 不存在 → 圆桌相关 agent 缺失 → 进入 `/api/intent/init` 后 `_create_thread` 本身能成功,但下一步 `/api/intent/stream` 真正调用 LangGraph 时会因为 agent 解析失败抛 500(典型表现:前端 Step 1 init 一次成功,下一句话发出去就 500)。 + +#### 11.7.1.1 哪些 agent **必须**打包,哪些**不需要** + +圆桌涉及的 agent 分**两类**,部署侧需求完全不同: + +| Agent | 角色 | 是否必须打包发布 | +|-------|------|------------------| +| `roundtable-intent` | **功能型**:Step 1 任务理解,所有部署都用它 | ✅ **必须**(任何环境) | +| `roundtable-recommender` | **功能型**:Step 1→2 之间从候选池里挑推荐,所有部署都用它 | ✅ **必须**(任何环境) | +| `roundtable-intelligence` | 示例研讨席位(情报收集) | ⚠️ **互联网 demo 用**,内网不需要 | +| `roundtable-environment` | 示例研讨席位(环境评估) | ⚠️ 同上 | +| `roundtable-solution-design` | 示例研讨席位(方案设计) | ⚠️ 同上 | +| `roundtable-risk-review` | 示例研讨席位(风险审查) | ⚠️ 同上 | +| `roundtable-execution-plan` | 示例研讨席位(执行规划) | ⚠️ 同上 | +| `roundtable-summary` | 示例研讨席位(总结陈述) | ⚠️ 同上 | +| `roundtable-coordinator-*` | 协调智能体壳,每次会话动态新建 | ❌ **不打包**(运行时生成) | + +**关键区分**: +- `roundtable-intent` / `roundtable-recommender` 是**圆桌功能本身的一部分**——没有它们,整个 Step 1 + 推荐流程跑不起来,跟客户业务无关 +- 6 个 `roundtable-<领域>` 席位只是**互联网 demo 用的示例研讨者**,让访客打开页面就能看到完整三步流程。内网部署面对的是客户自己的业务场景,会把客户自有的领域 agent(行业专家、合规审查员、业务规则引擎……)注册成候选 agent;这时 6 个示例席位反而成噪声 + +**对应的部署组合**: + +| 部署场景 | 打包内容 | +|----------|----------| +| **互联网 demo** | `roundtable-intent` + `roundtable-recommender` + **6 个示例席位** | +| **内网客户部署** | `roundtable-intent` + `roundtable-recommender`(仅此 2 个,**不带** 6 个示例席位) | +| **本地开发** | 全要(方便快速跑通) | + +**内网部署的额外注意**(去掉 6 个示例席位时): + +1. **客户自有的候选 agent 必须先存在**:内网部署落地时,客户的业务 agent 应该已经注册进 `.deer-flow/agents/`(手工建目录或通过 `POST /api/agents` 创建)。否则候选池为空,推荐拿不到任何 picks。 +2. **前端 `FALLBACK_AGENT_IDS` 需要适配**:前端 `RecommendAgentsDialog.tsx` 里硬编码了 6 个示例席位 id 作为 fallback;若推荐失败回退路径会拿这 6 个去 `candidates` 里交集,内网部署该交集恒为空,**fallback 会失败**。建议要么: + - **方案 1**(最稳):内网客户的候选池足够丰富 + recommender SOUL 调好,让推荐成功率 ~100%,不依赖 fallback + - **方案 2**:把 `FALLBACK_AGENT_IDS` 改成从后端 `/api/agents` 取前 6 个 `published=true` 的候选 agent(动态 fallback)。改动点见前端文档 §6.2 `RecommendAgentsDialog` +3. **推荐 SOUL(`roundtable-recommender/SOUL.md`)通常无需改**:SOUL 本身没写死候选 id,只描述「从给的候选池里挑」;候选池由前端传入,模型读到客户自有 agent 后会照样工作。 + +下文给出三种修复方案,**推荐方案 A**(最简单,无需写代码)。 + +#### 11.7.2 方案 A:`.gitignore` 白名单(推荐) + +让 git 只跟踪 **`roundtable-intent` + `roundtable-recommender` 两个功能型 agent** 的 `config.yaml` 与 `SOUL.md`,其它一律忽略。6 个示例席位**不进白名单**——互联网 demo 部署机如果需要它们,按 §11.7.2.A 在该分支单独追加白名单;内网部署机直接享受「干净的 `.deer-flow/`」。 + +**步骤 1:编辑仓库根 `.gitignore`** + +在最后追加白名单段(位置很重要,**必须在 `offline-backend-20260512/backend/.deer-flow/*` 之后**,否则被前面的通配符吃掉): + +```gitignore +# === 圆桌功能型 agent 跟随仓库发布(任何部署都需要) === +# 父目录先 unignore,git 才能往下递归 +!offline-backend-20260512/backend/.deer-flow/ +!offline-backend-20260512/backend/.deer-flow/agents/ +# 仅放行 2 个功能型 agent;6 个示例席位由互联网分支自行放行 +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/SOUL.md +``` + +⚠️ **不要**直接写 `!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/**`——`git` 不会进入还在 ignore 的父目录,必须逐层 un-ignore,再精确放行文件。 + +**步骤 2:强制 add 并提交** + +```bash +# 验证白名单生效 +git check-ignore -v offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/config.yaml +# 期望输出无任何匹配,或最后一条匹配是上面的 ! 行 + +# 加入索引(只加 2 个功能型 agent) +git add offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent +git add offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender + +git status # 应只看到 2 × 2 = 4 个新文件,绝不出现 users/、threads/、data/、6 个示例席位 + +git commit -m "chore: 把圆桌功能型 agent(intent + recommender)纳入版本管理" +``` + +**步骤 3:部署机验证** + +```bash +# clone 之后立刻 +ls offline-backend-20260512/backend/.deer-flow/agents/ +# 内网部署:应看到 roundtable-intent/ + roundtable-recommender/(+ 客户自有 agent) +# 互联网 demo:还应看到 6 个 roundtable-<领域>/ 示例席位 +``` + +启动后端,前端打开圆桌规划页,Step 1 第一句话发出去能正常拿到 streaming 响应而不是 500,就说明 OK。 + +##### 11.7.2.A 互联网 demo 分支:追加 6 个示例席位 + +互联网 demo 维护一条独立分支(或独立 `.gitignore` 段,用 build flag 切),在白名单后追加: + +```gitignore +# === 仅互联网 demo 需要的 6 个示例研讨席位 === +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/SOUL.md +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/ +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/config.yaml +!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/SOUL.md +``` + +`git add` 对应 6 个目录、提交,互联网分支即拿到全部 8 个内置 agent。两条分支共享同一份 `roundtable-intent / roundtable-recommender`,SOUL 调整对两边都生效,避免双维护。 + +**优点**: + +- 零代码改动;新机器 `git clone` 即可,部署文档不用单独说「记得拷一下 `.deer-flow/agents/`」 +- 修改 SOUL 直接走 git 走 review 流程 +- 多人协作时 SOUL 改动可见 + +**注意事项**: + +- `git status` 检查时确保**只**有 16 个内置文件进了索引,**绝对不能**包括 `users/`、`threads/`、`data/*.db`、`roundtable-coordinator-*/` 这些运行时数据 +- `roundtable-coordinator-mp*/` 这类历史会话残留目录会被 ignore 规则自动忽略(白名单没放行它们),无需手动清理 +- 部署机如果之前手动建过 `.deer-flow/agents/roundtable-intent/`,`git pull` 会把仓库版本覆盖上去——这是期望行为 + +#### 11.7.3 方案 B:源码内置 + 启动时种子化 + +如果不想让 `.deer-flow/` 这种「运行时目录」出现在 git 里,可以把 SOUL/config 放进 `packages/harness/` 里作为只读资源,启动时检查 `.deer-flow/agents//` 不存在就从源拷过去。 + +**步骤大纲**: + +1. 在 `packages/harness/deerflow/agents/builtin_personas/` 下建 8 个目录(`roundtable-intent/` 等),把 SOUL.md + config.yaml 搬过去 +2. 在 `app/gateway/app.py` 的 `lifespan` 启动阶段新增 `_seed_builtin_roundtable_agents()`: + ```python + def _seed_builtin_roundtable_agents() -> None: + from importlib.resources import files + src_root = files("deerflow.agents.builtin_personas") + dst_root = get_paths().agents_dir() # .deer-flow/agents/ + for agent_id in BUILTIN_ROUNDTABLE_AGENT_IDS: # 8 个常量 + dst = dst_root / agent_id + if dst.exists(): + continue # 已有(可能是用户改过的),不覆盖 + dst.mkdir(parents=True) + for fname in ("config.yaml", "SOUL.md"): + (dst / fname).write_bytes((src_root / agent_id / fname).read_bytes()) + ``` +3. 紧接着调一次 `_sync_legacy_agents(store)`,把这些刚落地的目录 upsert 进 `agents` 表 +4. 把 `BUILTIN_ROUNDTABLE_AGENT_IDS` 常量也导出给 `recommend.py` / `multi_agent.py` 共享 + +**优点**: + +- `.deer-flow/` 仍然作为「运行时数据目录」语义干净 +- 运维可以手动改 `.deer-flow/agents/roundtable-intent/SOUL.md` 临时调试,重启不被覆盖(`if dst.exists(): continue`) +- 升级时如果想强制刷新,加个 `--force-reseed-builtins` 启动参数即可 + +**缺点**: + +- 多一段 bootstrap 代码 + 一份 `pyproject.toml` 的 `package-data` 声明(确保 SOUL.md 跟着 wheel 一起发布) +- 「现场改 SOUL 不被覆盖」既是优点也是坑——A 改了 SOUL 但忘了同步源码,下次新机器部署又回到旧版本 + +#### 11.7.4 方案 C:纯运维流程(最弱,不推荐) + +不动代码、不动 .gitignore,发布说明里写一行: + +> 部署前请将 `<开发机>:offline-backend-20260512/backend/.deer-flow/agents/` 整个目录 `scp` 到部署机同位置。 + +只在「不允许改仓库结构」或「初期试跑」时用。长期维护几乎一定会有人忘记拷而部署失败。 + +#### 11.7.5 方案选择决策 + +| 场景 | 推荐方案 | +|------|----------| +| 单仓库多人协作、SOUL 经常调整 | **A**(.gitignore 白名单) | +| SOUL 极少改,希望严格区分代码与运行时数据 | **B**(源码 + lifespan 种子化) | +| 一次性 POC / 即将切换到 B | C | + +无论选哪种,部署机第一次启动后都建议跑这条 smoke test 验证: + +```bash +curl -X POST http://127.0.0.1:8001/api/intent/init \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +# 期望: 200 + {"status":"success","agent_id":"roundtable-intent","thread_id":"..."} + +curl -X POST http://127.0.0.1:8001/api/intent/stream \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"thread_id":"<上一步返回的>","message":"测试一下"}' +# 期望: 200 + SSE 长连接,最后一帧 status 为 asking 或 clarification +# 若 500 → 看后端日志,大概率是 SOUL.md 找不到或 LLM 外网不通(看上一条排查项) +``` + +#### 11.7.6 推荐智能体(`roundtable-recommender`)与研讨席位的关系(注:完整定制流程见 §11.8) + +推荐路由 `recommend.py` 用到的 `roundtable-recommender` 与「研讨席位」是**两件事**: + +- **`roundtable-recommender`**:负责从候选池里挑 picks 的「推荐者」本身,是个 LLM agent,**功能型**,所有部署必须有 +- **研讨席位**(无论是 6 个示例还是客户自有 agent):是「被挑选的候选」,前端 `filterRecommendCandidates` 过滤后送进 `/api/recommend/stream` 的 `candidates` 数组;recommender 在 SOUL 里看到候选池后才能输出 `[RECOMMEND_READY]` JSON + +「推荐改成动态」改的是**从池子里挑哪几个**,不是「池子可以没有」。**池子的来源在两类部署里不同**: + +| 部署 | 候选池来源 | +|------|------------| +| 互联网 demo | 仓库自带的 6 个 `roundtable-<领域>` 示例席位(+ 用户自建的) | +| 内网客户 | 客户自有的业务 agent(通过 `POST /api/agents` 或 `.deer-flow/agents//` 注册)| + +`roundtable-recommender/SOUL.md` 本身没写死任何候选 id(只描述「从给的候选池里挑」),所以内网部署即使候选池从「6 个示例」换成「客户自有 agent」,**recommender 的 SOUL 无需改动**,候选池由前端传入。 + +**前端 fallback 行为差异**: + +互联网 demo 因为有这 6 个内置示例席位垫底,推荐失败时 `FALLBACK_AGENT_IDS ∩ candidates` 还能拿到回退选项;**内网部署该交集恒为空**,回退路径失效——所以内网部署应通过「候选池足够丰富 + recommender SOUL 调好」让推荐成功率接近 100%,或改造前端 `FALLBACK_AGENT_IDS` 为动态从 `/api/agents` 取前 N 个(见前端文档 §6.2)。 + +### 11.4 新增 Step 1 工具 + +若需新工具,须同步修改 `intent.py` 的 `excluded_tools` 黑名单(当前默认禁 15 个工具,仅 `ask_clarification` 实际可用)。 + +### 11.5 改回旧 bash 派活协议 + +后端已**双格式兼容**,前端无需变化;只要 SOUL 改成生成 `bash` + `.py "X" "Y"` argv,`_extract_dispatch` 仍会解析正确。但建议保持新的结构化 `agent_orchestration`,prompt 更稳定。 + +### 11.6 把广播改为推送 + +`_append_thread_message` 通过 `GET state → mutate → POST state` 实现,是为了与 LangGraph checkpoint 一致;如果将来 Gateway 提供 `POST /api/threads/{id}/messages/append` 这类原子接口,可换上,降低并发竞争(虽然圆桌当前不会并发广播同一 thread)。 + +### 11.8 添加 / 替换任务推荐智能体(`roundtable-recommender`) + +推荐智能体 id 在后端是**硬编码**常量(`recommend.py:63` → `RECOMMENDER_AGENT_ID = "roundtable-recommender"`),路由不接收 `agent_id` 参数。因此「添加推荐智能体」**不是新增一个 id**,而是落地这个固定 id 对应的文件(首次部署)或**就地改 SOUL**(定制行为)。 + +#### 11.8.1 何时需要动它 + +| 场景 | 是否需要动 SOUL | +|------|-----------------| +| 全新部署,目录还不存在 | ✅ 必须先把目录搭起来(§11.8.2) | +| 候选池从 6 个互联网示例换成客户自有 agent | ❌ **不用改**——SOUL 没写死候选 id,候选池由前端传入 | +| 想改变推荐风格(更激进 / 更保守 / 偏好某领域) | ✅ 改 SOUL(§11.8.4) | +| 想让模型偏好你新加的领域 agent | ⚠️ **优先**改新 agent 的 `description`(候选池 prompt 用的就是它),其次才改 recommender SOUL | +| 想改输出 JSON 格式 | ❌ **不要**——后端正则 `_RECOMMEND_READY_PATTERN` 写死了 `[RECOMMEND_READY]` + ```json ``` 块;改 SOUL 输出会让后端解析失败走 fallback | + +#### 11.8.2 标准目录结构 + +``` +.deer-flow/agents/roundtable-recommender/ +├── config.yaml # id / name / description(只是 DB 元数据,不进 prompt) +└── SOUL.md # 行为契约,详见 11.8.4 +``` + +**`config.yaml` 模板**(**id 必须严格等于 `roundtable-recommender`**,否则 `recommend.py` 找不到): + +```yaml +description: 智能体推荐专员,基于用户的任务目标与约束,从候选智能体池中挑选最相关的 2-6 个智能体并给出推荐理由,为多智能体圆桌研讨准备最终参会名单 +id: roundtable-recommender +name: 智能体推荐 +``` + +字段说明: +- `id`:**不可改**,与 `RECOMMENDER_AGENT_ID` 常量绑死 +- `name`:DB 显示名,候选池过滤时不参与(`filterRecommendCandidates` 按 id 排除) +- `description`:DB 元数据,不进 prompt;改了不会影响推荐效果 + +**SOUL.md** 内容见 §11.8.4 模板。新增之后按 §11.7.2 把这两个文件加入 git 白名单 + commit 即可。 + +#### 11.8.3 后端给推荐智能体喂的 prompt 长什么样 + +`recommend.py:_build_prompt()` 把前端传的 intent + candidates 拼成下面这段,作为**唯一一条 user message** 投递: + +``` +任务目标:对生产系统进行安全改造,满足等保 2.0 合规要求 +约束:预算 ≤ 500 万元; 工期 ≤ 6 个月; 必须满足等保 2.0 +关键假设:现有系统具备基础数字化能力; 允许预定窗口停机 + +候选智能体池: +- roundtable-intelligence (情报收集): 检索同类历史案例、行业最佳实践与外部情报 +- roundtable-solution-design (方案设计): 产出主备方案与关键路径 +- my-cost-calc (成本测算): 基于预算和资源单价计算总成本 +- ... + +请按 SOUL 中规定的格式输出推荐结果(自然语言思路 + [RECOMMEND_READY] + json 块)。 +``` + +**关键事实**: +- **候选 agent 的 `name` 与 `description` 在 prompt 里就是它的「介绍」**——这是模型判断「这个 agent 适不适合本任务」的全部信息。客户自有 agent 想被推荐准确,`description` 必须写**任务导向**的能力描述,不要写 "由 XX 团队维护" 这种元信息 +- **候选池由前端过滤后传入**,后端不读 DB;前端 `filterRecommendCandidates` 已剔除 `roundtable-intent / roundtable-recommender / roundtable-coordinator-*` +- SOUL 看不到「intent agent 上一轮说了什么 / 用户原始 message」,只看到结构化的 `objective / constraints / assumptions`——这是设计选择,让推荐稳定 + +#### 11.8.4 SOUL 设计要点(模板 + 注解) + +SOUL **必须**满足三件事,其它都可定制: + +1. **输出格式严格**:自然语言思路 → `[RECOMMEND_READY]` 单独成行 → ```json {"picks": [{"agent_id": "...", "reason": "..."}, ...]} ``` +2. **`agent_id` 严格来自候选池**:模型造词出来会被 `_validate_picks` 过滤掉 +3. **2 ≤ picks ≤ 6**:少于 2 → 整轮判 `asking` 走 fallback;多于 6 → 后端截断 + +下面是一份可直接用的最小 SOUL,重点行带注释(**真正落盘时去掉 ``**): + +```markdown +# 智能体推荐专员 + +## 角色定位 +你是多智能体圆桌研讨的"智能体推荐专员"。任务理解专员已经把用户需求整理成 +「任务目标 + 约束 + 关键假设」交给你,你的工作是从一个**候选智能体池**里, +挑出**最适合**参加这次圆桌研讨的 2-6 个智能体,并为每一位写一句推荐理由。 + +## 核心职责 +- 仔细阅读用户的任务目标与约束 +- 浏览候选池中每个智能体的 `name` 与 `description` +- 选出与本次任务**最相关**的 2-6 个智能体(数量动态,不必凑齐 6 个) +- 为每个推荐的智能体写一句**具体、可验证**的推荐理由 +- 不推荐重复职能 / 不推荐与任务明显无关的智能体(宁缺毋滥) + +## 数量约束 +- **下限**:至少 2 个 / **上限**:最多 6 个 +- 候选池不足 2 个时,有几个推几个 + +## 输出协议(后端用正则识别,严格执行) +1. 先用自然语言简短说明推荐思路(1-3 句话) +2. 输出标记 `[RECOMMEND_READY]` 单独成行 +3. 紧接一个 ```json 代码块: + + ```json + { + "picks": [ + {"agent_id": "<候选池中的 id>", "reason": "<一句推荐理由>"}, + ... + ] + } + ``` + +格式注意: +- `[RECOMMEND_READY]` **单独成行**,前后无其他字符 +- `agent_id` **严格**来自候选池,不要造词 +- `reason` **具体**,不要"很合适"这种空话 + +## 边界 +- 不调用任何工具(已被后端 excluded_tools 屏蔽) +- 不评价候选 agent 优劣,只判断"是否适合本次任务" +- 不发起对话、不反问用户 + +## 示例 + +``` + +##### 定制方向 + +- **偏好新加的 agent**:在「示例」里把它列进候选池并出现在推荐结果中,模型会学到这种 id 也可推荐。比单纯在「核心职责」里加文字约束有效 +- **行业偏向**:在「角色定位」加一句「优先考虑能直接对接 <行业> 合规要求的智能体」 +- **激进 vs 保守**:调「数量约束」——例如「能挑出强相关的 3 个就不要凑 6 个」会让 picks 缩到 3-4 个 +- **不要做的事**: + - ❌ 改 `[RECOMMEND_READY]` 为别的标记 → 解析失败 + - ❌ 把 JSON 结构改成 `{"recommendations": [...]}` 之类 → 解析失败 + - ❌ 让模型只输出 JSON 不带前面的自然语言 → 前端 `parseRecommendStreamContent` 把整段当 `rationale`,弹窗顶部为空白 + +#### 11.8.5 验证流程 + +部署完成后跑 smoke test 确认推荐路径通: + +```bash +# 1. 确认 agent 文件落地 +ls .deer-flow/agents/roundtable-recommender/ +# 期望: SOUL.md config.yaml + +# 2. 确认 DB 已 upsert(任意 /api/agents 请求会触发 _sync_legacy_agents) +curl -H "Authorization: Bearer " http://127.0.0.1:8001/api/agents | \ + python -c "import sys,json; ids=[a['id'] for a in json.load(sys.stdin).get('agents',[])]; print('roundtable-recommender' in ids)" +# 期望: True + +# 3. 触发一次推荐(用最小 intent + 候选池) +curl -X POST http://127.0.0.1:8001/api/recommend/stream \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "intent": { + "objective": "测试推荐链路", + "constraints": ["响应时间 < 30s"], + "assumptions": [] + }, + "candidates": [ + {"agent_id": "agent-a", "name": "A", "description": "做 X"}, + {"agent_id": "agent-b", "name": "B", "description": "做 Y"}, + {"agent_id": "agent-c", "name": "C", "description": "做 Z"} + ] + }' +# 期望(SSE 流):最后一帧 data: {"status":"done","content":"...","picks":[...]} +# 若 picks 是空 → SOUL 没产生合法 [RECOMMEND_READY] / agent_id 不在候选池 +# 若整轮 status=error → 看后端 _diagnose_exception 输出的 hint +``` + +#### 11.8.6 故障排查清单 + +| 现象 | 排查方向 | +|------|----------| +| 推荐永远走 fallback(status=asking) | 后端日志 grep `recommend stream`,看 `last_ai_content` 是不是不含 `[RECOMMEND_READY]`——SOUL 没遵守协议 | +| `picks` 数量被截断到 6 | 后端 `_MAX_PICKS=6` 故意限制,前端用户可再手加到 8 | +| 模型推荐了候选池里没有的 id | `_validate_picks` 会丢掉,但若有效项不足 2 个会整轮走 fallback;改 SOUL 强调「严格来自候选池」 | +| 推荐结果不偏向客户自有 agent | 先检查该 agent 的 `description` 是不是写得「任务导向」;再考虑在 SOUL 示例里加它 | +| 推荐稳定性差(同 intent 重跑结果飘) | 推荐 model 用低 temperature(在 `roundtable-recommender/config.yaml` 加 `model: <某个低 temp 模型>` 或直接前端选 deterministic 模型) | +| 前端弹窗顶部 rationale 区空白 | SOUL 让模型只输出 JSON 没有自然语言;恢复成「先思路 + 后 JSON」 | + +--- + +## 12. 故障排查 + +| 现象 | 可能原因 | 排查 | +|------|----------|------| +| Step 1 一直有澄清、无法进入 Step 2 | 未产出 `[INTENT_READY]` 或终态被 `clarification` 覆盖 | 看 SOUL、最后一帧 `status`;确认 intent 先扫 READY(§4.3 顺序) | +| 选项全是多选 | 旧前端默认 `allow_multiple !== false` | 确认 `allow_multiple` 字段与 `resolve_allow_multiple`(缺省 false) | +| Step 2 init SSE `error` | 协调 agent 创建失败 / agent_orchestration 缺失 / `_validate_agent_id` 命中 | Network + 后端 `multi_agent` 日志 | +| Step 2 init 完全卡住(历史症状,ASGITransport 改造后已不应再出现) | 旧版 `httpx` 走 `HTTP_PROXY` 拦回环 → 40s 超时 | 自改 ASGITransport 起此现象物理上不可能复现;若仍卡,看后端日志栈、检查路由 handler 是否阻塞 | +| leader 立即 `status: []` | 未调用 orchestration 技能 / SkillStopMiddleware 没拦到 / SOUL 没声明派活 | 换模型或加强 SOUL;检查 `skill_stop_names` 是否包含 `agent_orchestration` | +| dispatch 失败但 leader 文本看着正常 | 模型把 `roundtable-x(中文)` 整段塞进 `agent_name` | `_strip_display_suffix` 会兜底;若仍报 400,强化 SOUL 的「严格英文 id」 | +| 推荐 picks 为空 | 未输出 `[RECOMMEND_READY]` 或 picks 不在候选池 | 看 recommend 终态帧;检查 `candidate_ids` 集合 | +| Step 3 无文件 | 子 agent 未写 outputs / 错路径 | `GET .../artifacts` 对子 thread_id 逐个查;确认 sandbox outputs 路径 | +| `seat_ready` 顺序不固定 | `asyncio.as_completed` 按真实完成顺序推送 | 这是设计行为,不是 bug;前端不应按顺序依赖 agent_name | + +### 日志关键字 + +```bash +grep "intent stream" .runtime/logs/*.log +grep "multi_agent\|multi-agent" .runtime/logs/*.log +grep "recommend stream" .runtime/logs/*.log +grep "dispatch failed" .runtime/logs/*.log +grep "upstream-timing" .runtime/logs/*.log +grep "leader-timing" .runtime/logs/*.log +grep "special-timing" .runtime/logs/*.log +grep "headers_in=" .runtime/logs/*.log # 上游首字节延迟;ASGITransport 改造后通常 < 10ms +``` + +--- + +## 13. 测试建议 + +``` +backend/tests/test_intent.py # init、INTENT_READY 优先级、clarification 字段 +backend/tests/test_recommend.py # picks 校验、候选池过滤、clamp 2-6 +backend/tests/test_multi_agent.py # init SSE 事件顺序、leader/special 终态、clarification +``` + +可用 `httpx.MockTransport` 模拟 loopback,不依赖真实 Gateway: + +```python +def mock_handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/api/threads": + return httpx.Response(200, json={"thread_id": "t-1"}) + ... + +transport = httpx.MockTransport(mock_handler) +client = httpx.AsyncClient(transport=transport, base_url=...) +``` + +针对 SSE 上游可用 `respx` 或直接 mock `_stream_upstream` 的字符串列表。 + +--- + +## 14. 路径速查 + +| 资源 | 路径 | +|------|------| +| Step 1 路由 | `offline-backend-20260512/backend/app/gateway/routers/intent.py` | +| 推荐路由 | `offline-backend-20260512/backend/app/gateway/routers/recommend.py` | +| Step 2 路由 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` | +| Step 3 列出/下载/编辑 | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | +| 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py` | +| 澄清单选/多选 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py` | +| 任务理解 Agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/` | +| 推荐 Agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/` | +| 圆桌席位 | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-*/` | +| 协调智能体壳(累积) | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-coordinator-*/` | +| 前端 Step 1 API | `frontend-web/src/roundtable-planning/api/intent.ts` | +| 前端推荐 API | `frontend-web/src/roundtable-planning/api/recommend.ts` | +| 前端 Step 2 API | `frontend-web/src/roundtable-planning/api/multi-agent.ts` | +| 前端页面 | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | +| 前端开发文档 | `frontend-web/docs/multi-agent-frontend-dev.md` | +| API 速查 | `frontend-web/docs/multi-agent-api.md` | diff --git a/frontend-web/docs/multi-agent-frontend-dev.md b/frontend-web/docs/multi-agent-frontend-dev.md new file mode 100644 index 00000000..fe1c781d --- /dev/null +++ b/frontend-web/docs/multi-agent-frontend-dev.md @@ -0,0 +1,667 @@ +# 多智能体圆桌研讨 · 前端开发文档 + +本文档面向维护 `frontend-web` 圆桌规划功能的开发者。后端实现见 `multi-agent-backend-dev.md`;HTTP 字段速查见 `multi-agent-api.md`;推荐弹窗专项进度见 `recommend-agents-progress.md`(若不存在则为待补文件)。 + +> ⚠️ 历史版本曾引用 `multi-agent-step1-api.md`、`multi-agent-step3-api.md`,当前仓库未提供这两份文件;Step 1(intent)/ Step 3(artifacts)的接口签名直接参考本页第 4 节即可。 + +--- + +## 0. 速读:你需要先知道的事 + +- **整页只有一个文件**:`RoundtablePlanningPage.tsx`(≈ 3 621 行)。三步 UI 通过 `currentStep ∈ {1,2,3}` 切换,不使用 React Router 子路由。 +- **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。 +- **不写死模型名**:与 `/page/canvas/ai-writing` 同样的策略 —— 全局选中模型由 `selectedModel` state 维护,首次 `useModels()` 完成后初始化为 `availableModels[0].name`,顶栏「模型」下拉可切换。所有 API 调用统一传 `model: selectedModel || undefined`;后端缺省由 `lead_agent._resolve_model_name()` 回退到 `config.yaml` 的 `models[0]`。**内网部署只需在 `config.yaml` 配自家模型即可,前后端均无需改动**。 +- **席位区间硬约束**:推荐弹窗要求 **2 ≤ 选中数 ≤ 8**(`MIN_SELECTED` / `MAX_SELECTED`)。 +- **总控编排循环上限**:`MAX_CYCLES = 8`,超出后强制停止并 toast。 +- **席位派活策略**:**顺序**(不是并行)。旧实现并行触发多路 SSE 会让浏览器卡顿,现已改为顺序。 +- **调试开关**:`window.__MULTI_AGENT_INIT_TIMING__` 与 `window.__MULTI_AGENT_DEBUG__`(**不是** `__MULTI_AGENT_STREAM_DEBUG__`,文档历史版本有误)。 + +--- + +## 1. 功能定位 + +**页面路由**:`/page/workspace/roundtable/planning`(侧栏「圆桌规划」) + +**入口组件**:`src/roundtable-planning/pages/RoundtablePlanningPage.tsx` + +**核心能力**: + +| 步骤 | 用户目标 | 真实后端对接 | +|------|----------|--------------| +| 1 | 与「任务理解智能体」多轮澄清,得到结构化意图 | `intent.ts` | +| 1→2 | 从候选池选 2–8 个研讨席位 | `recommend.ts` + `RecommendAgentsDialog` | +| 2 | 总控派活、子智能体顺序交付、可澄清/挂起 | `multi-agent.ts` + 编排循环 | +| 3 | 查看总控结论、席位交付、产出文件下载 | `HighFidelityReport` + `listSessionArtifacts` | + +**步骤门禁**: + +- **Step 2**:`intentReady` 非空才可点击(顶栏按钮禁用 + toast 提示)。 +- **Step 3**:`hasConsensus === true` 才可点击。Leader 派活返回空数组才会自然收敛触发 `hasConsensus`,手动跳转的「达成共识生成全案成果」按钮也会校验。 + +--- + +## 2. 目录结构 + +``` +frontend-web/src/roundtable-planning/ +├── index.ts # 导出 RoundtablePlanningPage、HighFidelityReport +├── pages/ +│ └── RoundtablePlanningPage.tsx # 三步主页面 + 状态机 + 编排循环(≈3600 行) +├── api/ +│ ├── intent.ts # Step 1:initIntent、streamIntent +│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent +│ └── multi-agent.ts # Step 2/3:initMultiAgent、streamMultiAgent、listSessionArtifacts、buildArtifactDownloadUrl +├── components/ +│ ├── IntentClarificationCard.tsx # Step 1 选项卡片(单选/多选)+ formatIntentClarificationAnswer +│ ├── RecommendAgentsDialog.tsx # Step 1→2 推荐弹窗(含流式 rationale + 候选筛选) +│ ├── HighFidelityReport.tsx # Step 3 报告 + 文件清单 +│ ├── ArtifactPreviewModal.tsx # Markdown/纯文本/JSON/CSV/YAML 等文本类文件 in-app 预览 +│ └── ScrollToBottomButton.tsx # 对话区「回到底部」浮动按钮 +├── hooks/ +│ └── useAutoScroll.ts # 滚动跟随 + 用户上滑暂停跟随 +├── lib/ +│ └── clarification.ts # resolveAllowMultiple(单选/多选解析),与主聊天 ClarificationCard 对齐 +├── utils/ +│ └── drafts.ts # localStorage 草稿存档(list / upsert / get / delete / rename / deriveName) +└── styles/ + └── roundtable-planning.css # 页面主题变量(浅红品牌色 + 深色覆盖) +``` + +**路由注册**(`src/pages/WorkspaceRoutes.tsx`): + +```tsx +} /> +``` + +侧栏入口:`src/components/page-sidebar.tsx` → `/page/roundtable/planning`。 + +**环境变量**(由 `scripts/start-frontend.sh` 注入): + +- `VITE_BACKEND_BASE_URL` → `getBackendBaseURL()`,所有 API 前缀 +- `VITE_LANGGRAPH_BASE_URL` → 本功能**不**直接用 LangGraph SDK,全部走 Gateway 封装 + +--- + +## 3. 三步数据流(前端视角) + +``` +[进入 Step 1] + useEffect → runStep1Init() + initIntent(model) → { agent_id, thread_id } + setIntentThreadId、setTimelineEntries + interactiveMessages seed 欢迎语(本地 seed,非 API) + +[用户发送 / 选项卡片提交] + sendIntentMessage(text) + 追加 user 气泡 + agent 占位(streaming + phase) + streamIntent({ onTextDelta, onPhaseChange, signal }) + 终态: + done + summary → setIntentReady,解锁 Step 2 / 推荐弹窗 + clarification → IntentClarificationCard + pendingIntentClarificationMsgId + asking → 继续对话 + error → toast + 红色 (错误) 文案 + +[点击「进入多智能体研讨」且 intentReady] + setIsRecommendDialogOpen(true) + RecommendAgentsDialog + listAgents() → filterRecommendCandidates() + streamRecommend({ onRationaleUpdate, onPicksUpdate, onPhaseChange }) + 用户勾选 2–8 个 → onConfirm(agentIds) + handleConfirmAgents + setSelectedAgents(agentIds.map(...)) + step2InitRef.current = false // 复位 Step 2 init 闸门 + setCurrentStep(2) + +[进入 Step 2] + useEffect(step2InitRef 闸门)→ initMultiAgent(SSE) + onSeatReady:右侧席位面板逐个「在线」 + onCoordinatorReady / init_done → threadIds + coordinatorName + runOrchestration(buildInitialIntent(), threadIds, coordinatorName) + while !cancelled && cycles<8: + leader stream → 若 clarification:暂停循环,展示 Step 2 底部澄清条 + 否则 dispatched[] → **顺序** streamMultiAgent(special) → 再 leader 综合… + dispatched.length === 0 → hasConsensus = true,可进 Step 3 + +[进入 Step 3] + HighFidelityReport + consensusText ← lastLeaderContent + seatDeliveries ← step2RoundtableDialogues.filter(d => d.confidence === "子智能体交付") + Object.entries(threadIds).filter(name !== coordinatorName).map → listSessionArtifacts(threadId) + 文本类文件 → ArtifactPreviewModal;其它 → window.open(downloadUrl) + ``` + + --- + + ## 4. API 层 + + ### 4.1 `api/intent.ts` — Step 1 + +| 函数 | 说明 | +|------|------| +| `initIntent(model?)` | `POST /api/intent/init` → `{ status, agent_id, thread_id }` | +| `streamIntent(req)` | `POST /api/intent/stream`,读 SSE 至终态帧 | + +**`StreamIntentRequest` 重要字段**: + +- `threadId`:必填,跨多轮对话保持 +- `signal`:必填(页面通过 `intentAborterRef` 控制暂停 / 切步取消) +- `onTextDelta(text, messageId)`:每个 AI 文本增量 +- `onPhaseChange(phase, toolName?)`:`connecting → connected → streaming|tool_calling` + +**`StreamIntentResult`**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | `"asking" \| "clarification" \| "done" \| "error"` | 终态分支 | +| `content` | `string` | 完整 AI 文本(含 `[INTENT_READY]` 块) | +| `summary` | `IntentSummary \| null` | 仅 `done` 有;前端 fallback 也会从 `content` 里用 `splitIntentReadyBlock` 解 | +| `clarification` | `ClarificationPayload \| null` | 仅 `clarification` 有 | + +**SSE 解析要点**: + +- `event: metadata` → phase `connected` +- `event: messages` + `extractAiTextDelta` → `onTextDelta` +- tool call 帧(`tool_calls` / `tool_call_chunks` 非空)→ phase `tool_calling`(含 `ask_clarification` 工具名) +- 终态帧识别:`isFinalIntentFrame`(必须有 `status` + `content`) + +**Abort 行为**: + +- `sendIntentMessage` 内建 `AbortController`,保存到 `intentAborterRef` +- `stopIntentStream()` 用于「暂停」按钮、`currentStep !== 1` 自动调用、`handleStartNewTask`、`handleLoadDraft` +- 触发 AbortError 时不当作错误:在气泡尾部追加「(已暂停)」并 toast「⏸️ 已暂停任务理解回复」 + +### 4.2 `api/recommend.ts` — 推荐弹窗 + +| 函数 | 说明 | +|------|------| +| `filterRecommendCandidates(agents)` | 从 `GET /api/agents` 结果剔除 intent / recommender / `roundtable-coordinator-*` | +| `streamRecommend(req)` | `POST /api/recommend/stream`,无 init(一次性、无状态) | +| `parseRecommendStreamContent(raw)` | 将累计文本拆成 `{ rationale, picks[] }`,供边流边渲染 | + +**`EXCLUDED_AGENT_IDS`** 内置常量(仅 `recommend.ts` 内可见): + +- `roundtable-intent` +- `roundtable-recommender` +- 任何以 `roundtable-coordinator-` 开头的 agent_id(动态创建的协调器壳) + +**流式 UI 拆分**(与主聊天的 `[RECOMMEND_READY]` 分界类似): + +- 标记常量:`RECOMMEND_READY_MARKER = "[RECOMMEND_READY]"` +- `parseRecommendStreamContent(raw)`:用 `PICK_OBJECT_PATTERN` 正则增量匹配出已完成的 `{agent_id, reason}` 对象 +- `onRationaleUpdate`:仅 marker 之前的自然语言(弹窗顶部展示) +- `onPicksUpdate`:从部分 JSON 增量解析 `picks[]`(卡片边流边出,过滤已见 id) +- 过滤 `TitleMiddleware` 产生的标题增量(`langgraph_node` 含 `TitleMiddleware` 或 `tags` 含 `middleware:title`) + +**终态语义**: + +- `status === "done"` 且 `picks.length >= 2` → 弹窗用 picks 作为默认勾选 +- 否则 → `phase: "fallback"`,使用 `FALLBACK_AGENT_IDS`(6 个内置 roundtable-* 席位)与候选池交集 + +### 4.3 `api/multi-agent.ts` — Step 2 & Step 3 文件 + +| 函数 | 说明 | +|------|------| +| `initMultiAgent(payload)` | SSE:`seat_ready`(N 次) → `coordinator_ready` → `init_done` | +| `streamMultiAgent(req)` | leader / special 一轮 | +| `listSessionArtifacts(threadId)` | `GET /api/threads/{id}/artifacts` | +| `buildArtifactDownloadUrl(threadId, virtualPath, { download? })` | 预览或附件下载 URL | + +**`initMultiAgent` 事件**(`data:` JSON 的 `event` 字段): + +```ts +{ event: "seat_ready", agent_name, thread_id, display_name, description } +{ event: "coordinator_ready", main_agent_name, thread_id } +{ event: "init_done", main_agent_name, thread_ids } // thread_ids: Record +{ event: "error", detail } +``` + +**`streamMultiAgent` 终态**(`isFinalStatusFrame`): + +| `status` | 含义 | 前端处理 | +|----------|------|----------| +| `[["agent","task"], ...]` | 派活列表(数组形态) | **顺序**触发 special;最终用 `nextLeaderTask` 综合 | +| `[]` | 共识达成(空数组) | `hasConsensus = true`,跳出循环 | +| `"clarification"` | 总控追问 | `setPendingClarification`,暂停循环 | +| `"子智能体 X 完成..."` | special 完成 | 更新对应对话气泡 confidence | +| `"error"` | 失败 | toast + 红色气泡 | + +**为什么 special 是顺序而不是并行**:旧版并行触发多路 SSE,每路都在 token 级别对 `step2RoundtableDialogues` 整表 setState,N 个并行 stream 让浏览器卡顿明显。改为顺序后同一时刻只有一个 SSE 在跑,UX 平滑很多(详见 `RoundtablePlanningPage.tsx` 编排循环里的注释)。 + +**`buildArtifactDownloadUrl` 路径编码**:将 `virtualPath` 按 `/` 切段后逐段 `encodeURIComponent` 再拼回,避免文件名里的中文 / 空格触发后端 404;`download=true` 加在 query string 上而非 path。 + +**调试开关**: + +```js +window.__MULTI_AGENT_INIT_TIMING__ = true; // 默认开启;init 各阶段耗时(fetch.start / first-byte / seat_ready#n / coordinator_ready / init_done / total) +window.__MULTI_AGENT_DEBUG__ = true; // 默认开启;stream 帧计数 + lastEvent + bad JSON 提示,前缀 [stream::] +``` + +> ⚠️ 文档历史版本曾写作 `__MULTI_AGENT_STREAM_DEBUG__`,**实际代码并不识别**这个名字。 + +--- + +## 5. 主页面状态(`RoundtablePlanningPage`) + +### 5.1 步骤与门禁 + +- `currentStep`: `1 | 2 | 3` +- **Step 2 锁定**:`intentReady == null` 时顶栏 Step 2 按钮禁用(visual + `aria-disabled`),同时点击「进入多智能体研讨」会 toast 「⚠️ 请先与任务理解智能体完成澄清…」 +- **Step 3**:`hasConsensus === false` 时顶栏 Step 3 与底部「达成共识生成全案成果」按钮均禁用 + +### 5.2 Step 1 关键 state + +| 状态 | 用途 | +|------|------| +| `intentThreadId` | `/api/intent/init` 返回的线程 id;未就绪前发送按钮置灰 | +| `intentReady` | `IntentSummary`,驱动 Step 2 初始 prompt + 右侧摘要面板 + 顶栏 step2 解锁 | +| `interactiveMessages` | 问答气泡列表(user / agent,含 streaming/phase/clarification 元数据) | +| `pendingIntentClarificationMsgId` | 当前可操作的 agent 消息 id(保证只有最新一条 clarification 展示可点卡片) | +| `intentClarificationSelections` | `Map`,多选 / 多消息时各自维护 | +| `isIntentSending` | 发送中(屏蔽双发 / 显示「暂停」按钮) | +| `intentAborterRef` | 当前 stream 的 AbortController | +| `intentInitError` | init 失败信息;驱动右侧角色面板「异常」状态与红色提示 | +| `step1InitRef` | 闸门:true 即认为已 init 过 | +| `step1InitGenRef` | **单调递增**的代数标记,详见下文 | +| `timelineEntries` | 右侧「澄清记录」时间线(init / user 反馈 / 任务意图已明确) | + +**`step1InitGenRef` 的存在原因**: + +React 18 StrictMode 开发环境下,effect 会 mount → cleanup → mount 两次。早期实现用闭包 `cancelled` 标志做防抖,结果是:第二次 mount 因 `step1InitRef.current === true` 早返回,但第一次 mount 的 async 仍未完成;当它最终 resolve 时,`cancelled` 已被 cleanup 置 true,因此 `setIntentThreadId` 被跳过,右栏卡在「正在上线」。改为单调代数后:每次 `runStep1Init` 自增 `step1InitGenRef`;async 完成时检查 `myGen === step1InitGenRef.current`,等价于「我是当前最新一次 init」,否则丢弃结果。这也覆盖了「新建任务 / 加载草稿期间旧 init 晚归」的场景。 + +**`splitIntentReadyBlock(text)`**(页面内工具函数,使用 `INTENT_READY_RE` 正则): + +- 从气泡正文解析 `[INTENT_READY]\n```json\n{...}\n``` ` 块 +- 返回 `{ before, summary, after }`,前后文与结构化卡片分别渲染 +- 单独的 `useEffect` 监听 `interactiveMessages`:若已存在含 `[INTENT_READY]` 的 agent 消息但 `intentReady` 仍为 null(极少数终态帧丢 summary 的场景),主动 `setIntentReady`,与后端 "READY 优先" 对齐 + +### 5.3 推荐与 Step 2 席位 + +| 状态 | 用途 | +|------|------| +| `selectedAgents` | `{ agent_id, name }[]`,默认 `DEFAULT_SELECTED_AGENTS`(6 内置) | +| `roleTemplates` / `roles` | 右侧席位面板(`buildRoleTemplates`,commander 占 id=1,agents 从 id=2 开始) | +| `agentNameToRole` | 流式输出时根据 `agent_name`(小写)反查 speakerId、avatarType、displayName | +| `roundtableAgentIds` | `selectedAgents.map(a => a.agent_id)`,传给 `/api/multi-agent/init` 的 `agent_names` | +| `threadIds` / `coordinatorName` | `/api/multi-agent/init` 结果;离开 Step 2 **不**清空,允许回来继续看 | +| `step2RoundtableDialogues` | Step 2 对话流(含 `streaming` / `confidence` / `time` / `avatarType`) | +| `lastLeaderContent` / `hasConsensus` | Step 3 总控结论 + 共识标记 | +| `pendingClarification` | Step 2 总控澄清(复用 `ClarificationPayload`) | +| `clarificationDraft` | 底部自定义回答输入框(受 `allowCustom` 控制) | +| `isRoundTableRunning` | 用户挂起 / 恢复协商的 UI 开关 | +| `isOrchestrating` | 编排循环正在 run(屏蔽并发触发) | +| `orchestrationCancelRef` | 与 `abortAllInFlight` 配合中断当前循环 | +| `activeAbortersRef` | 当前 Step 2 所有 in-flight stream 的 AbortController 集合 | +| `activeSpeakerId` / `activeSpeakerSet` | 圆桌可视化高亮:单 leader 时用单值,多席位并行时用 Set | +| `selectedAgentId` | 当前 Persona 抽屉所在的席位 id(点击圆桌头像打开) | +| `agentConfigs` | `Map` Persona 覆盖;下一轮该席位的派活会用此 model | +| `isUpperPanelCollapsed` | 是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间 | + +**协调智能体命名**:`roundtable-coordinator-${Date.now().toString(36)}`,每次 Step 2 会话新建。后端把这类壳 agent 留在 DB 中,因此 `filterRecommendCandidates` 默认过滤掉。 + +### 5.4 编排循环 `runOrchestration(seedMessage, ids, coord)` + +伪代码: + +``` +isOrchestrating = true +orchestrationCancelRef = false +nextLeaderTask = seedMessage +cycles = 0 +MAX_CYCLES = 8 + +while !cancelled && cycles < MAX_CYCLES: + cycles += 1 + highlight(speakerId=2) // 总控 + leaderBubble = appendStreamingDialogue("总控协调", confidence: "调度中") + res = streamMultiAgent(leader, ids, coord, nextLeaderTask, onTextDelta, onPhaseChange) + + if res.clarification: + finalize(leaderBubble, content, "等待用户澄清") + setPendingClarification(res.clarification) + break // 等用户回复后由 submitClarification 重启循环 + + finalize(leaderBubble, content, dispatched.length === 0 ? "全案盖印" : "派活说明") + if res.content: setLastLeaderContent(res.content) + + if res.dispatched.length === 0: + setHasConsensus(true); setConsensusPercentage(100); break + + for [subName, task] of res.dispatched: // **顺序**,不是并行 + meta = agentNameToRole[subName.toLowerCase()] + markSpeakerStreaming(meta.speakerId) + perSeatModel = agentConfigs[meta.speakerId]?.model || DEFAULT_MODEL + subBubble = appendStreamingDialogue(meta.displayName, confidence: "调度中") + specRes = streamMultiAgent(special, ..., perSeatModel, signal=subAborter) + finalize(subBubble, specRes.content, "子智能体交付") + markSpeakerIdle(meta.speakerId) + if cancelled: break + + setConsensusPercentage(prev => min(95, prev + 8)) + nextLeaderTask = "各子智能体已就上一轮派活完成交付,请你结合最新结论..." + +if cycles >= MAX_CYCLES: + toast("⏱️ 已达到最大研讨轮次,自动停止以避免循环") + +clearAllStreamingSpeakers() +isOrchestrating = false +``` + +要点: + +- **`buildInitialIntent()`**: + - 有 `intentReady` → 拼装 `objective / 约束条件 / 关键假设` + 「不要再反问用户」终结语 + - 无 `intentReady`(用户跳过澄清 / 顶栏直接切到 Step 2)→ 拼接 Step 1 内 `interactiveMessages` 里 sender==='user' 的内容,逐行 `用户#N: ...`,**绝不**注入 mock 的「预算 500 万 / 72 小时」数字 + - 用户输入完全为空 → 让总控先调用 `ask_clarification` 而不是凭空派活 +- **用户「干预研审」**:`handleInjectToRoundtable` 追加一个「💡 (人工指令介入) ...」气泡,然后构造 `runOrchestration("用户人工干预新指令:...", ...)` +- **挂起 / 恢复**: + - 挂起 = `setIsRoundTableRunning(false)` → `stopOrchestration()` → `orchestrationCancelRef = true` + `abortAllInFlight()` + - 恢复 = `setIsRoundTableRunning(true)`,effect 触发 `runOrchestration("用户已恢复研讨,请继续上一轮的协调工作。", ...)` +- **澄清恢复**:`submitClarification` 把用户回答拼到 prompt 里再 `runOrchestration`,不会重建 thread + +### 5.5 Persona 抽屉(Step 2 圆桌可视化点击) + +- 点击圆桌头像 → `setSelectedAgentId(seatId)` → 在上半部分下方展开抽屉 +- 抽屉控件: + - **System Prompt**:仅前端记录(后端覆写未开放) + - **Temp**:仅前端记录 + - **推理模型**:选项来自 `useModels()`;显示值优先级 `agentConfigs[seat]?.model > selectedModel` +- 「参数覆盖并重启动算研讨」按钮 → `handleSaveAgentAndRecalculate`: + - 把新的 `(seatId, model)` 写入 `agentConfigs` + - 追加一条「⚡ [模型偏好已保存] ...」对话气泡 + - **不**立即重启编排;下一轮该席位被派活时 `runOrchestration` 内会读 `agentConfigs[meta.speakerId]?.model || selectedModel || undefined` 作为 special 调用的 `model` 参数 + +### 5.6 全局模型选择器(右侧 sidebar 顶部) + +- **位置**:右侧 sidebar `currentStep !== 3` 时显示的第一个面板 —— 在「当前意图摘要」**正上方**。Step 3 时整个 sidebar 切换成「最终共识结论 / 风险与预算摘要 / 导出与交付」,所以模型选择器仅在 Step 1 / Step 2 可见(此时也是唯一会发起新模型调用的阶段) +- **state**:`const [selectedModel, setSelectedModel] = useState("")` +- **初始化**:`useEffect(() => ...)` 在 `availableModels` 首次返回后把 `selectedModel` 设为 `availableModels[0].name`;后端配置变化导致当前选中 model 不在列表里时也回退到首项 +- **传参规则**:所有 5 个 API 调用点(`initIntent` / `streamIntent` / `streamRecommend` / `initMultiAgent` / `streamMultiAgent` leader / `streamMultiAgent` special)都传 `selectedModel || undefined`;special 还会先看 `agentConfigs[seat]?.model` per-seat 覆盖 +- **禁用态**:`availableModels.length === 0` 或 `modelsLoading` → `disabled`,placeholder 显示「加载中…」或「未配置模型」 +- **可改换为 Radix Select**:当前用原生 `