diff --git a/.gitignore b/.gitignore index 1b033763..532043d8 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 @@ -75,4 +78,6 @@ offline-backend-20260512/backend/skill_evolution.json skills/ hermes-agent-main/ .gitignore -offline-backend-20260512/backend/tests/* \ No newline at end of file +offline-backend-20260512/backend/tests/* +deploy +.history 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/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..c04bce2e --- /dev/null +++ b/frontend-web/docs/multi-agent-backend-dev.md @@ -0,0 +1,359 @@ +# 多智能体圆桌研讨 · 后端开发文档 + +本文档面向需要维护或扩展"多智能体圆桌研讨"功能后端的开发者。前端 API 速查请看 `multi-agent-api.md`,此文档关注**实现细节、设计取舍、性能特征**。 + +--- + +## 1. 功能定位 + +把"任务目标 + 用户约束"丢给一个**协调智能体**,由它通过 `agent_orchestration` 技能调度 6 个固定职能的子智能体(情报收集 / 环境评估 / 方案设计 / 风险审查 / 执行规划 / 总结陈述)协同产出方案,直到协调智能体认为信息足够、停止派活并输出最终结论为止。 + +对应前端页面:`/page/roundtable/planning` 的 **Step 2 多智能体研讨**。 + +--- + +## 2. 涉及的后端文件 + +``` +offline-backend-20260512/backend/ +├── app/gateway/ +│ ├── app.py # 注册 multi_agent.router +│ └── routers/ +│ └── multi_agent.py # 本功能唯一新增的 router +└── .deer-flow/agents/ + ├── roundtable-intelligence/ # 6 个内置子智能体目录 + │ ├── config.yaml # ├── id / name / description + │ └── SOUL.md # └── 角色 prompt + ├── roundtable-environment/ + ├── roundtable-solution-design/ + ├── roundtable-risk-review/ + ├── roundtable-execution-plan/ + └── roundtable-summary/ +``` + +**零代码改动构造内置 agent**:6 个目录通过既有的 `_sync_legacy_agents()`(`app/gateway/routers/agents.py`)在下次任意 `/api/agents` 请求时自动 upsert 进 `agents` 表,`user_id IS NULL` 标记为内置。 + +--- + +## 3. 接口总览 + +路由前缀 `/api/multi-agent`,所有接口需要 `Authorization: Bearer `。 + +| 方法 | 路径 | 用途 | +|---|---|---| +| POST | `/api/multi-agent/init` | 一次性创建协调智能体 + 6 个子席位线程,返回 `thread_ids` 映射 | +| POST | `/api/multi-agent/run/stream` | SSE 流式驱动一次 leader 或 special 调用 | + +字段细节见 `multi-agent-api.md`,这里不重复。 + +--- + +## 4. 内部实现详解 + +### 4.1 `/init` 内部流程 + +``` +[阶段1] 并行准备 6 个子智能体席位 (asyncio.gather) + ├─ GET /api/agents/{id} × 6 → 取 name + description + └─ POST /api/threads × 6 → 建新 thread + +[阶段2] 创建协调智能体 + ├─ POST /api/threads → 协调智能体的 thread + └─ POST /api/agents (with model + skills=[agent_orchestration] + soul) + └─ soul 里包含可调度的 6 个 agent_name + description 列表 +``` + +**协调智能体 SOUL 是动态拼出来的**(`multi_agent.py:216-224`): + +```python +soul = ( + "#核心定位:\n" + "用途:根据用户的任务目标,调用其他智能体干活,只负责协调汇总,...\n" + "#调度方法:\n" + "通过使用agent_orchestration技能调用其他智能体干活,...\n" + "#可调度智能体:\n" + f"{descriptions}" # ← 6 个子席位的 name+description +) +``` + +由于 `main_agent_name` 由前端每次新建(`roundtable-coordinator-{timestamp36}`),所以协调智能体本身是一次性的、用完即弃。 + +### 4.2 `/run/stream` 内部流程 + +接收 `agent_type` 后走两条分支: + +#### `agent_type = "leader"` (协调智能体) + +``` +1. broadcast「总控收到用户消息」到所有非自己线程 (asyncio.gather) +2. 拼 RunCreateRequest,转发到 POST /api/threads/{coordinator}/runs/stream + - excluded_tools: ["web_search"] + - context.model_name = 用户传的 model +3. 流式透传 LangGraph 事件给前端 +4. 流结束后从最后一帧 values 取最末一条 message +5. 解析 message.tool_calls (agent_orchestration 技能调用): + - 用 _extract_name_task() 提取 [agent_name, task_text] + - 对每个 [agent_name, task]: + broadcast「总控给 X 派活,内容为: ...」到所有非派活双方 +6. 末尾追加状态帧: + { + "status": [["agent_name", "task"], ...], // 空数组 = 共识达成 + "content": "", + "agent_name": "" + } +``` + +#### `agent_type = "special"` (子智能体) + +``` +1. 拼 RunCreateRequest,转发到 POST /api/threads/{sub_agent}/runs/stream + - excluded_tools: ["web_search","ask_clarification","present_files","view_image"] +2. 流式透传 LangGraph 事件 +3. 流结束后从最后一帧 values 取交付内容 (content) +4. broadcast「子智能体 X 完成 Y 工作,交付内容为: ...」到所有非自己线程 +5. 末尾追加状态帧: + { + "status": "子智能体 X 完成 Y 工作", + "content": "<交付内容>", + "agent_name": "" + } +``` + +### 4.3 _broadcast 机制 + +```python +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)) +``` + +`_append_thread_message` 对每个目标 thread: + +``` +GET /api/threads/{id}/state # 拿当前 messages 列表 +state.values.messages.append({"type":"human","content":...}) +POST /api/threads/{id}/state # 写回完整 state +``` + +这是**为了让每个子线程的对话历史里都能看到全局上下文**(其他席位说了什么、协调智能体怎么派活的)。子智能体下次被调用时,模型自然能基于这份"群消息回放"做判断。 + +**关键不变量**:`skip` 列表通常包含发起方和接收方,避免发起方收到回声、接收方收到重复消息。 + +--- + +## 5. 6 个内置 agent SOUL 设计要点 + +每个 `roundtable-*` 的 SOUL.md 遵循统一结构: + +``` +# {席位名} + +## 角色定位 +你是"多智能体圆桌研讨"会议的 **{席位}** 席位。会议由"总控协调智能体"统一调度, +你只在被派到任务时发言,不需要主动推动议程。 + +## 核心职责 +- ...具体职责... + +## 输出风格 +- 每次发言以 {emoji} 开头 +- 第一句直接给结论 +- 不超过 4 句 + +## 边界 +- 只产出 {本席位输出} +- 不替其他席位做 {其他席位输出} +- 任务结束后停下,等待总控派下一个任务 + +## 示例输出 +> ... +``` + +| 席位 | agent_id | emoji | 核心职责 | +|---|---|---|---| +| 情报收集 | `roundtable-intelligence` | 📢 | 同类案例 + 历史风险预警 | +| 环境评估 | `roundtable-environment` | 🌍 | 硬件容量 + 量化承载力 | +| 方案设计 | `roundtable-solution-design` | 📐 | 主备方案 + 关键路径 | +| 风险审查 | `roundtable-risk-review` | ⚠️ | 合规红线 + 收敛建议 | +| 执行规划 | `roundtable-execution-plan` | 📅 | 里程碑 + 资源排布 | +| 总结陈述 | `roundtable-summary` | 🎤 | 归纳 + 冲突标记 + 盖印 | + +**为什么不让子智能体自己调度**:所有 SOUL 都明文写了"只在被点到时发言,由总控协调智能体派活"。子智能体不装 `agent_orchestration` 技能。只有协调智能体装。这是单调度源,避免多智能体相互抢调度权造成的死循环。 + +--- + +## 6. 关键设计决策 + +### 6.1 为什么用 HTTP loopback 而不是内部函数调用 + +`multi_agent.py` 调用 `/api/threads`、`/api/agents`、`/api/threads/.../runs/stream` 都是用 `httpx` 走 `http://localhost:8001/...` 自身循环回来,而不是直接 `from app.gateway.services import ...`。 + +**好处**: +- 自动复用 Gateway 的鉴权 + CSRF + permission 中间件,不用再手写 +- 复用既有路由的请求验证(Pydantic)和错误响应 +- `Authorization` 头透传,被调端自然知道是哪个用户在调 + +**代价**: 每次调用都有一次额外的 TCP+JWT 解析开销(~10-50ms)。但因为 init 已经做了并行优化(A)、broadcast 已经省掉(C),目前总体耗时是可接受的。 + +如未来需要进一步优化,可改为直接调用 `app/gateway/services.py` 里的内部函数,绕过 HTTP 一层。 + +### 6.2 为什么协调智能体每次新建 + +前端在每次进入 step 2 时生成 `roundtable-coordinator-{Date.now().toString(36)}`,作为 `main_agent_name`。 + +**好处**: +- 不同会话/不同用户/同一用户不同任务之间完全隔离 +- 协调智能体的对话历史就是本次研讨的全部上下文,不会被上次任务污染 +- SOUL 不会被"上次的可调度列表"覆盖(虽然现在 6 个席位是固定的) + +**代价**: 每次都要在 `agents` 表插一行,`.deer-flow/agents//` 下生成新目录。长期会累积大量协调智能体目录,**未来需要清理脚本**(标记 description 以 "你是总控控制协调智能体" 开头的 agent 自动 GC)。 + +### 6.3 子智能体席位为什么固定 6 个 + +- 圆桌的 6 个角色定位是产品级强约束(情报/环境/方案/风险/执行/总结) +- 写在后端 SOUL.md 一处维护,跟其他内置 agent 同等地位 +- 前端只需要把 6 个 hyphen-case 名作为 `agent_names` 传过来即可 + +如果后续要支持"用户自选席位",在前端加 mapping 面板让用户从 `GET /api/agents` 里挑,不动后端。 + +### 6.4 为什么 SSE 末尾还要追加一个 `{status:...}` 帧 + +LangGraph 原生 SSE 里没有"调度结果"这种语义,前端只能从最后一帧 values 里去翻 tool_calls。如果由前端解析,前后端协议会绑死 LangGraph 内部数据形状,迁移成本高。所以后端在流末尾追加一个**协议层的总结帧**: + +```json +{ + "status": [["agent_name", "task"], ...] | "<完成提示>", + "content": "<最终 AI 文本>", + "agent_name": "<本轮 agent>" +} +``` + +前端 `streamMultiAgent` helper 通过 `isFinalStatusFrame()` 识别该帧(必须同时有 `status` + `content` 键),与其他 LangGraph 帧区分。 + +--- + +## 7. 性能特征 + +### 7.1 已应用的优化 + +| ID | 优化 | 收益 | +|---|---|---| +| **A** | `/init` 阶段 1 并行化(`asyncio.gather`) | 阶段 1 从 2-4s 降到 0.3-0.7s | +| **C** | 删除 `/init` 末尾的 intro 广播(SOUL 已含定位) | 阶段 3 整段消失,省 1-2s | + +应用前 `/init` 耗时 **3-8s**,应用后 **0.5-1.2s**。 + +### 7.2 仍然存在的开销点 + +| 调用 | 单次耗时 | 主因 | +|---|---|---| +| `GET /api/agents/{id}` | 50-300ms | 每次触发 `_sync_legacy_agents()` 扫整个 `.deer-flow/agents/` 目录(目前 30+ 项) | +| `POST /api/threads` | 100-200ms | LangGraph thread 表插入 + 默认 state 初始化 | +| `POST /api/agents` | 100-300ms | 文件系统写入 SOUL.md + config.yaml + agents 表 | +| `GET/POST /state` | 50-150ms × 2 | checkpointer 读写 | +| `POST /runs/stream` 启动 | 50-100ms | RunManager + StreamBridge 初始化 | +| LLM 推理 | **数百毫秒~数十秒** | 取决于 model + 上下文长度 | + +**LLM 推理才是绝对大头**,基础设施开销在十几个百分点以内。 + +### 7.3 未来可探索 + +- **B**(已识别未做): broadcast 跳过 GET-state,直接 POST 新建态。但 POST 需要构造完整的 state shape,改造成本不小。 +- **D**(已识别未做): loopback HTTP → 内部函数调用。代码改动大,但能省掉 ~20-30 次 HTTP 解析开销。 + +--- + +## 8. 依赖与扩展点 + +### 8.1 `agent_orchestration` 技能 + +协调智能体强依赖此技能,SOUL 里硬编码: + +``` +通过使用agent_orchestration技能调用其他智能体干活,使用技能时传入agent_name和需要做的活 +``` + +技能的实际派活产物是一个形如 `python3 scripts/agent_orchestration.py "agent_name" "task"` 的 shell 调用文本,后端用正则提取: + +```python +_TOOL_CALL_PATTERN = re.compile(r'\.py\s+"([^"]*)"\s+"([^"]*)"') +``` + +**若该技能被删除/改名**,leader 流程会拿不到 `tool_calls`,直接走"无派活=共识达成"分支。注意 `skills/` 下必须保留此技能。 + +### 8.2 模型选择 + +前端通过 `GET /api/models` 拿到列表,在 `model` 字段传 `Model.name` (与 `config.yaml.models[].name` 对应)。 + +- `/init` 的 `model` 字段 → 协调智能体的默认模型 (写进 agents 配置) +- `/run/stream` 的 `model` 字段 → 该次具体运行用什么模型 (写进 `context.model_name`) + +前端允许"逐席位覆盖模型",由前端在 special 调用时按 `agentConfigs[seat].model` 传入。后端不感知"席位"概念,只看到 `model` 字段。 + +### 8.3 自定义新席位 + +如果要加第 7 个席位(比如"成本测算"): + +1. `.deer-flow/agents/roundtable-cost/` 加 `config.yaml` + `SOUL.md` +2. 前端 `RoundtablePlanningPage.tsx` 的 `ROUNDTABLE_ROLE_TEMPLATES` + `AGENT_NAME_TO_ROLE` 加映射 +3. 圆桌可视化(圆圈环形图)加一个气泡 + 头像 + +**后端代码完全不需要改**。 + +--- + +## 9. 故障排查 + +| 现象 | 可能原因 | 排查 | +|---|---|---| +| `/init` 500 "create coordinator agent failed" | `agent_orchestration` 技能不存在 / config.yaml 写入失败 | 看后端日志 `multi_agent` logger;检查 `skills/` 目录 | +| `/run/stream` 502 "upstream returned HTTP 4xx" | JWT 过期 / 转发到上游时鉴权挂了 | 前端检查 Authorization 头是否带上 | +| leader 永远 `status: []` 立即结束 | 协调智能体没产生 `tool_calls`,直接给文本回答 | 协调智能体 SOUL 不够强、或模型不支持工具调用,换 model | +| special 内容为空 | 子智能体被 `excluded_tools` 限制到无可用工具 / 模型无文本输出 | 看上游 `runs/stream` 的 values 帧 messages.last.content | +| 进入 step 2 卡住转圈 | `/init` 超时或失败 | F12 Network 看 `/api/multi-agent/init` 状态码与响应体 | + +### 服务端日志关键字 + +```bash +# multi_agent 路由的日志 +grep "multi_agent" .runtime/logs/*.log + +# 派活解析失败 +grep "dispatch failed" .runtime/logs/*.log + +# 广播失败(通常无害) +grep "update state .* failed" .runtime/logs/*.log +``` + +--- + +## 10. 测试入口 + +后端目前没有针对本功能的单测。建议补: + +``` +backend/tests/test_multi_agent.py + - test_init_creates_threads_for_all_subagents + - test_init_creates_coordinator_with_orchestration_skill + - test_init_returns_400_when_agent_names_empty + - test_stream_leader_emits_status_frame_with_content + - test_stream_special_emits_status_frame_with_content + - test_extract_name_task_parses_orchestration_skill_output + - test_broadcast_skips_listed_agents +``` + +可用 `httpx.MockTransport` 拦截 loopback 请求,不依赖真实 Gateway。 + +--- + +## 11. 路径速查 + +| 资源 | 路径 | +|---|---| +| 路由实现 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` | +| 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py`(`include_router(multi_agent.router)`) | +| 6 个内置 agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-*/` | +| `agent_orchestration` 技能定义 | `skills/public/agent_orchestration/`(若存在) | +| 前端 API helper | `frontend-web/src/roundtable-planning/api/multi-agent.ts` | +| 前端页面 | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | +| 前端 API 速查 | `frontend-web/docs/multi-agent-api.md` | diff --git a/frontend-web/docs/multi-agent-step1-api.md b/frontend-web/docs/multi-agent-step1-api.md new file mode 100644 index 00000000..62f36b0c --- /dev/null +++ b/frontend-web/docs/multi-agent-step1-api.md @@ -0,0 +1,492 @@ +# 多智能体圆桌 · Step 1 · 任务理解智能体接口与实现 + +> Step 1(`/page/roundtable/planning` 的"任务意图与澄清"页)对应 `RoundtablePlanningPage.tsx` 里 `currentStep === 1` 的渲染分支。当前它是纯前端 mock —— 用户发消息后 `setTimeout(1500)` 拼一段固定话术回。本文档定义一个**内置任务理解智能体 + 2 个轻量接口**,让 step 1 跑起来真正的对话,为 step 2 输入更高质量的"任务目标 + 约束"。 +> +> 整体风格直接参照 `multi-agent-api.md` / `multi-agent-backend-dev.md`。 + +--- + +## 1. 目标与边界 + +### ✅ 要做的事 +- 内置一个智能体:**任务理解智能体**(`roundtable-intent`),跟 6 个 roundtable 席位同等地位 +- 提供 2 个接口:`/api/intent/init` 和 `/api/intent/stream` +- 复用 step 2 的 SSE 协议风格(前端只需 1 个新 helper) +- 让用户跟智能体多轮澄清后,把最终"目标 + 约束"传给 step 2 的 `runOrchestration` + +### ❌ 不做的事 +- 不重新设计澄清协议(直接用 `ask_clarification` 工具 + ClarificationMiddleware 现成机制,跟 step 2 leader 一样) +- 不做"自动跳过"按钮的真实业务逻辑(保留前端按钮形态即可) +- 不做时间线(timelineEntries)的后端持久化(前端临时状态够用) +- 不做"评分/置信度"等假指标 +- 不做单独的"任务理解专员模型选择"——直接用全局 `DEFAULT_MODEL` + +--- + +## 2. 数据流概览 + +``` +[Step 1 入口] + ↓ +前端:POST /api/intent/init + ← {agent_id: "roundtable-intent", thread_id: "..."} + +用户输入第一句话:"帮我规划一个生产系统安全改造方案" + ↓ +前端:POST /api/intent/stream (SSE) + ← (流式 AI 文本) "好的,预算大致是多少?" + ← {status: "asking", content: "..."} ← 智能体反问中,需要用户继续 + +用户回:"500 万左右" + ↓ +前端再次 POST /api/intent/stream + ← (流式) "明白,还有什么时间约束吗?" + ← {status: "asking", ...} + +...对话进行几轮 ... + +用户:可以了 + ↓ +前端再 POST /api/intent/stream + ← (流式) "好,我理解的任务是: <汇总>" + ← {status: "done", content: "<最终汇总文本>", summary: {...}} ← 信息充分,可进入 step 2 + +[Step 1 完成 → Step 2 init] + 前端把 summary.objective + summary.constraints 拼成 step 2 的 buildInitialIntent() +``` + +--- + +## 3. 接口设计 + +跟 step 2 完全同构。`/api/intent/init` 简化版(只建 1 个 thread + 1 个 agent,没有"协调"概念);`/api/intent/stream` 跟 `/api/multi-agent/run/stream` 的 special 分支几乎一样。 + +### 3.1 `POST /api/intent/init` + +**请求体** +```json +{ + "model": "deepseek-chat" // 可选,默认全局 DEFAULT_MODEL +} +``` + +**响应 200** +```json +{ + "status": "success", + "agent_id": "roundtable-intent", + "thread_id": "" +} +``` + +**行为**: +1. 鉴权(透传 `Authorization`) +2. 调上游 `POST /api/threads` 建 1 个 thread +3. 直接返回 `agent_id = "roundtable-intent"`(**这是内置 agent,不需要每次新建**) + +**与 step 2 `/init` 的区别**: +- 没有"协调智能体"概念,只有 1 个 agent +- 没有 6 个 sub 席位,只有 1 个 thread +- 不需要传 `agent_names` 数组 +- `agent_id` 是固定字符串 `"roundtable-intent"`,不带 timestamp 后缀(智能体本身是内置、长期、复用的) + +### 3.2 `POST /api/intent/stream` + +**请求体** +```json +{ + "thread_id": "", + "message": "用户这一轮要说的话", + "model": "deepseek-chat" // 可选 +} +``` + +**响应**:`text/event-stream`,SSE 流式。 + +**SSE 帧组成**: +1. 透传上游 LangGraph 的所有帧(`event: metadata` / `event: messages` / `event: values`)—— 前端用现有的 `extractAiTextDelta` 解析 AI 文本增量 +2. 流末追加一个**状态帧**(跟 step 2 同样模式): + + ```json + { + "status": "asking" | "done" | "error", + "content": "<本轮 AI 文本>", + "summary": { + "objective": "...", // 仅 status=done 时有: 任务目标 + "constraints": ["...", ...], // 仅 status=done 时有: 约束清单 + "assumptions": ["...", ...] // 仅 status=done 时有: 关键假设 + } + } + ``` + +**`status` 取值**: +- `"asking"` —— 智能体还在追问,前端应该把焦点放回输入框 +- `"done"` —— 智能体宣布"信息已充分",前端就可以亮起"进入下一步"按钮,并拿 `summary` 拼 step 2 的初始 intent +- `"error"` —— 上游异常,`content` 里是错误信息 + +**判定 done/asking 的简单策略**(后端实现): +- 智能体在 SOUL 里被告知:信息已充分时**必须**调 `present_intent_summary` 工具(或直接在 AIMessage 里用一个固定标记 `[INTENT_READY]`),后端识别后发 `done` 帧 +- 否则一律是 `asking` + +> 上面"summary 工具"是新增的内置工具吗?不一定。最省事的做法是**让模型用 JSON 块输出**,后端用正则提取: +> +> ``` +> [INTENT_READY] +> ```json +> {"objective": "...", "constraints": ["..."], "assumptions": ["..."]} +> ``` +> ``` +> +> 后端流末扫一下最末 AIMessage 里有没有这个标记,有就 parse JSON 出来填到 summary,发 `done`;没有就发 `asking`。 + +--- + +## 4. 内置智能体配置 + +**位置**:`offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/` + +### 4.1 `config.yaml` +```yaml +id: roundtable-intent +name: 任务理解智能体 +description: 帮助用户明确任务目标、约束、关键假设,为后续多智能体圆桌研讨准备结构化输入。 +``` + +### 4.2 `SOUL.md` + +```markdown +# 任务理解智能体 + +## 角色定位 +你是多智能体圆桌研讨的"任务理解专员"。你的工作是在用户和 6 个圆桌席位之间架一座桥: +通过 2-5 轮简短追问,把用户模糊的需求转化成结构化的「任务目标 + 约束 + 关键假设」。 + +## 核心职责 +- 用清晰、简短的问题引导用户补全关键信息(优先级: 目标 > 硬约束 > 软偏好) +- 不要追问已经清楚的事 +- 不要做方案,不要给具体技术建议——那是后面 6 个席位的工作 +- 信息足够后,立即停止追问,产出结构化汇总 + +## 信息充分性判定 +当满足以下条件时,认为信息已充分: +1. 明确的任务目标(至少能写出一句 "用户希望实现 X") +2. 至少 1 条硬约束(预算/时间/合规/范围之一) +3. 没有明显的歧义点 + +## 信息汇总产出 +信息充分时,你必须在回复末尾输出**精确的标记 + JSON 块**(给后端识别用): + +``` +[INTENT_READY] +```json +{ + "objective": "用一句话写出用户最终要做的事", + "constraints": ["预算 ≤ XXX 万", "时间 ≤ XXX 天", ...], + "assumptions": ["假设 1", "假设 2", ...] +} +``` +``` + +输出前可以先用自然语言总结一遍给用户看(让用户能 review 内容),再附上述标记 + JSON。 + +## 输出风格 +- 一次回复不超过 3 句话 +- 提问要具体,不要"还有什么补充吗"这种空问 +- 用 emoji 简短点缀(❓ 表示问题, ✅ 表示明确, 📋 表示汇总) + +## 边界 +- 不调用 agent_orchestration(这是 step 2 的协调智能体的事) +- 不写文件、不做计算、不联网 +- 用户消息歧义大时,优先调 ask_clarification 工具引导 +``` + +### 4.3 注册方式 + +跟现有 6 个 `roundtable-*` 一样,通过 `_sync_legacy_agents()`(`app/gateway/routers/agents.py`)在下次任意 `/api/agents` 请求时自动 upsert 进 `agents` 表,`user_id IS NULL` 标记为内置。**零代码改动**。 + +--- + +## 5. 后端实现要点 + +### 5.1 新建 router 文件 +**位置**:`offline-backend-20260512/backend/app/gateway/routers/intent.py` + +```python +"""任务理解智能体接口 - Step 1 的极简包装。 + +跟 multi_agent.py 同构但简化: +- 没有 broadcast(单 thread,无需广播) +- 没有派活逻辑 +- 没有 leader/special 分支,只有一种调用方式 +- 流末状态帧解析 [INTENT_READY] 标记决定 status=done/asking +""" + +from fastapi import APIRouter, Body, HTTPException, Request +from fastapi.responses import StreamingResponse +import httpx, json, re, asyncio, logging + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/api/intent", tags=["intent"]) + +# 复用 multi_agent.py 里已有的工具函数 +from app.gateway.routers.multi_agent import ( + _loopback_base, _auth_headers, + _create_thread, _stream_upstream, _parse_last_messages, + _flatten_content, _run_payload, +) + +INTENT_AGENT_ID = "roundtable-intent" + +_INTENT_READY_PATTERN = re.compile( + r"\[INTENT_READY\]\s*```json\s*(\{.*?\})\s*```", + re.DOTALL, +) + + +@router.post("/init") +async def init_intent(request: Request, payload: dict = Body(...)) -> dict: + base = _loopback_base(request) + headers = _auth_headers(request) + async with httpx.AsyncClient(timeout=None) as client: + thread_id = await _create_thread(client, base, headers) + return { + "status": "success", + "agent_id": INTENT_AGENT_ID, + "thread_id": thread_id, + } + + +@router.post("/stream") +async def stream_intent(request: Request, payload: dict = Body(...)) -> StreamingResponse: + thread_id = str(payload.get("thread_id") or "").strip() + message = str(payload.get("message") or "").strip() + model_name = str(payload.get("model") or "deepseek-chat") + if not thread_id or not message: + raise HTTPException(status_code=400, detail="thread_id and message required") + + base = _loopback_base(request) + headers = _auth_headers(request) + + async def gen(): + async with httpx.AsyncClient(timeout=None) as client: + try: + body = _run_payload( + agent_name=INTENT_AGENT_ID, + model_name=model_name, + thread_id=thread_id, + new_message=message, + excluded_tools=["web_search", "present_files", + "view_image", "agent_orchestration"], + skill_stop_names=[], + ) + captured = [] + async for line, done in _stream_upstream(client, base, headers, thread_id, body): + if line is not None: + yield (line if line.endswith("\n") else line + "\n") + elif done is not None: + captured = done + + messages = _parse_last_messages(captured) + last_ai = {} + for m in reversed(messages): + if isinstance(m, dict) and (m.get("type") or "").lower() in ("ai", "aimessage", "aimessagechunk"): + last_ai = m + break + content = _flatten_content(last_ai.get("content", "")) + + # 检测 [INTENT_READY] 标记 + match = _INTENT_READY_PATTERN.search(content) + if match: + try: + summary = json.loads(match.group(1)) + except json.JSONDecodeError: + summary = None + frame = {"status": "done", "content": content, "summary": summary} + else: + frame = {"status": "asking", "content": content} + yield f"data: {json.dumps(frame, ensure_ascii=False)}\n\n" + except HTTPException as exc: + yield f"data: {json.dumps({'status':'error','content':f'[{exc.status_code}] {exc.detail}'}, ensure_ascii=False)}\n\n" + except Exception as exc: + logger.exception("intent stream failure") + yield f"data: {json.dumps({'status':'error','content':f'unexpected: {exc!r}'}, ensure_ascii=False)}\n\n" + + return StreamingResponse( + gen(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive"}, + ) +``` + +### 5.2 在 `app/gateway/app.py` 注册 +```python +from app.gateway.routers import intent +app.include_router(intent.router) +``` + +--- + +## 6. 前端实现要点 + +### 6.1 新增 API helper +**位置**:`frontend-web/src/roundtable-planning/api/intent.ts` + +```ts +import { apiFetch } from "@/core/api/fetch-client"; +import { getBackendBaseURL } from "@/core/config"; + +export interface IntentInitResponse { + status: "success"; + agent_id: string; + thread_id: string; +} + +export async function initIntent(model?: string): Promise { + const res = await apiFetch(`${getBackendBaseURL()}/api/intent/init`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model }), + }); + if (!res.ok) throw new Error(`init intent failed (${res.status}): ${await res.text()}`); + return res.json(); +} + +export interface IntentSummary { + objective: string; + constraints: string[]; + assumptions?: string[]; +} + +export interface StreamIntentRequest { + threadId: string; + message: string; + model?: string; + signal?: AbortSignal; + onTextDelta?: (text: string, id: string) => void; +} + +export interface StreamIntentResult { + status: "asking" | "done" | "error"; + content: string; + summary: IntentSummary | null; +} + +export async function streamIntent(req: StreamIntentRequest): Promise { + // 实现:reader 循环跟 streamMultiAgent 几乎一样,只是状态帧解析改成 + // {status, content, summary?} 格式。 + // ...(参考 multi-agent.ts 的 streamMultiAgent 改写,简化 80%) +} +``` + +### 6.2 改造 `RoundtablePlanningPage.tsx` 的 step1 渲染 + +替换 `handleSendMessage`: + +```ts +const [intentThreadId, setIntentThreadId] = useState(null); +const [intentReady, setIntentReady] = useState(null); + +useEffect(() => { + if (currentStep === 1 && !intentThreadId) { + initIntent(DEFAULT_MODEL).then(r => setIntentThreadId(r.thread_id)); + } +}, [currentStep]); + +const handleSendMessage = async () => { + if (!inputText.trim() || !intentThreadId) return; + const userText = inputText.trim(); + // (push user msg to interactiveMessages 略) + setInputText(""); + + // 占位 agent 气泡 + const agentMsgId = pushAgentPlaceholder("任务理解智能体"); + + const res = await streamIntent({ + threadId: intentThreadId, + message: userText, + model: DEFAULT_MODEL, + onTextDelta: (delta) => appendToAgentMsg(agentMsgId, delta), + }); + + finalizeAgentMsg(agentMsgId, res.content); + if (res.status === "done" && res.summary) { + setIntentReady(res.summary); + triggerToast("✅ 任务理解完成,可以进入圆桌研讨"); + } +}; +``` + +### 6.3 "进入 step 2"按钮的联动 + +- 当前页面已有"完成澄清/进入下一步"按钮(应该是路由到 `currentStep === 2`) +- 改造:只有 `intentReady !== null` 时该按钮才 enabled +- 点击后:把 `intentReady.objective + intentReady.constraints` 写入 `buildInitialIntent()` 的源(替换现在按 `interactiveMessages` 找最后一条 user msg + budgetLimit 拼接的逻辑) + +```ts +const buildInitialIntent = useCallback(() => { + if (intentReady) { + return `${intentReady.objective}\n\n约束条件:\n${intentReady.constraints.map(c => `- ${c}`).join("\n")}\n\n请基于上述目标与约束,在圆桌内调度合适的子智能体完成研讨。`; + } + // fallback: 旧逻辑保留兜底 + const lastUserMessage = [...interactiveMessages].reverse().find(m => m.sender === "user"); + const userIntent = lastUserMessage?.content ?? "请围绕给定任务展开圆桌研讨"; + const constraints = `预算硬约束:${budgetLimit} 万元`; + return `${userIntent}\n\n约束条件:${constraints}\n\n请基于上述目标与约束,在圆桌内调度合适的子智能体完成研讨。`; +}, [interactiveMessages, budgetLimit, intentReady]); +``` + +--- + +## 7. 实施清单 + +| 优先级 | 项 | 文件 | +|---|---|---| +| P0 | 内置 agent 目录 | `backend/.deer-flow/agents/roundtable-intent/{config.yaml,SOUL.md}` | +| P0 | 新 router | `backend/app/gateway/routers/intent.py` | +| P0 | 注册 router | `backend/app/gateway/app.py`(一行 `include_router`) | +| P0 | 前端 helper | `frontend-web/src/roundtable-planning/api/intent.ts` | +| P0 | 前端页面改造 | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx`(handleSendMessage + buildInitialIntent + 入口 useEffect) | +| P1 | step 1 进入下一步按钮 disabled 状态联动 `intentReady` | 同上 | +| P2 | 单测 | `backend/tests/test_intent.py`(init 返回 agent_id 固定值, stream 识别 INTENT_READY 标记) | +| P3 | 失败兜底(INTENT_READY 标记被模型遗漏时的"用户手动确认进入下一步"按钮) | 前端 | + +--- + +## 8. 与 step 2 的衔接 + +``` +step 1 输出: intentReady.objective + intentReady.constraints + ↓ +step 2 入口: buildInitialIntent() 拼成 leader 的 new_message + ↓ +step 2 leader 不再需要主动反问澄清(因为 step1 已经清完了), + 除非模型遇到完全意料之外的歧义 + ↓ +研讨更聚焦,减少 step2 阶段的 clarification 轮次 +``` + +副产物:协调智能体的 SOUL 可以稍微调整(参考 `multi_agent.py:soul = "#核心定位..."` 那段),加一句"用户已经经过任务理解智能体澄清,task 描述里包含目标+约束,请直接开始派活而不是再反问",进一步减少 step2 的反问。 + +--- + +## 9. 路径速查 + +| 资源 | 位置 | +|---|---| +| Step 1 渲染 | `RoundtablePlanningPage.tsx` 内 `currentStep === 1` 分支(约 line 1418+) | +| 新增前端 helper | `frontend-web/src/roundtable-planning/api/intent.ts` | +| 新增后端 router | `offline-backend-20260512/backend/app/gateway/routers/intent.py` | +| 内置 agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/` | +| 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py` | +| 参照实现 | `multi_agent.py`(接口模式、SSE 包装、状态帧) | + +--- + +## 10. 验收清单 + +- [ ] 进入 step 1 自动调 `/api/intent/init`,拿到 thread_id +- [ ] 用户每次发消息后,任务理解智能体真实流式输出(不是 setTimeout) +- [ ] 智能体能在 2-5 轮对话后自动停止追问,发 `status: done` 帧并附 summary +- [ ] step 2 的初始 intent 用 step1 的 summary 拼接,不再依赖 `budgetLimit` 这种页面状态 +- [ ] 离开/重新进入 step 1 时,thread_id 状态干净(不会带着上次的对话) +- [ ] 任何接口失败都不让 step 1 卡死(容错的"用旧 mock 兜底"或者明确错误提示) diff --git a/frontend-web/docs/multi-agent-step3-api.md b/frontend-web/docs/multi-agent-step3-api.md new file mode 100644 index 00000000..b0b76023 --- /dev/null +++ b/frontend-web/docs/multi-agent-step3-api.md @@ -0,0 +1,264 @@ +# 多智能体圆桌 · Step 3 · 接口与对接说明 + +> Step 3(`/page/roundtable/planning` 的"最终方案展示"页)对应 `frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx`。当前页面是产品早期的"展厅效果",绝大多数内容是硬编码 mock。本文档梳理**真正需要的数据**、**用哪些接口**、**前后端怎么对接**。 + +--- + +## 1. 改造方向(在做什么,不做什么) + +### ✅ 保留并对接真实数据 + +| 区块 | 现状 | 改造后 | +|---|---|---| +| 标题栏 + 生成时间 | 硬编码"2026-05-21 11:01:12" | 用本地 `Date.now()` 或 init/最后一次 leader 完成时刻 | +| "已达成共识"徽章 | 静态显示 | step2 自然收敛(`leaderRes.dispatched.length === 0`)后才显示 | +| **总控最终结论** | 完全是 mock 文本 | 取最后一次 `leaderRes.content`(已经在 step2 拿到了) | +| **各席位最终交付(折叠卡片)** | 当前 step3 没渲染 | 取每个 sub 线程的最后一条 AIMessage 文本 | +| **生成的报告文件清单** | 5 张假"档案" | 真实列出 `outputs/` 目录里的所有产出(markdown / pdf / csv 等) | +| **文件下载** | 假 toast | 用现有 `GET /api/threads/{tid}/artifacts/{path}` | +| "重新起草新规划" | 调用 `setCurrentStep(1)` 重置 | **保留**,含义就是回到 step1 重新开始 | + +### ❌ 不做(直接删 / 不接口) + +| 区块 | 原因 | +|---|---| +| 4 张 metric 卡片(设计全要素通过率 99.4% / Tier III+ / 终审共识度 42 轮研商 / 90.3分)| 全是按 `budgetLimit` 编出来的"展厅指标",没有真实数据源 | +| 三阶段推进路线图(0-24h / 24-60h / 60-72h 三张卡)| 假设业务必然是 72 小时交付,写死场景,不通用 | +| "采纳理由与最终裁定判词"段 | 整段都是模板话术,没接口能算出"90.3 分总评" | +| "12 轮极限对抗研商" 数据 | 当前没记录"轮次"语义,临时就用 leader 实际轮数代替即可,不值得为它出接口 | +| "签发起草成果并投产运行" 按钮 | 无对应业务动作(不是要部署、不是要审批),删掉 | +| "执行摘要" 段写死的 3 个 bullet | 模板话术,跟实际任务无关 | + +最终 step3 页面只保留四块:**头部状态 + 总控结论 + 席位交付列表 + 文件下载清单**,再加底部"重新起草"。 + +--- + +## 2. 数据来源全景 + +``` +┌───────────────────────────────────────────────────────────┐ +│ step2 全程结束时,前端手里已经有的本地状态 │ +├───────────────────────────────────────────────────────────┤ +│ coordinatorName: "roundtable-coordinator-xxx" │ +│ threadIds: { [agent_name]: thread_id, ... } │ ← /init 返回 +│ step2RoundtableDialogues: [...所有气泡, 含每个 sub 最终 │ +│ 交付的 content...] │ +│ leaderRes.content (最后一次 leader 的 final text) │ +└───────────────────────────────────────────────────────────┘ + +后端权威数据(需要时从后端拉): +- 每个 sub 线程的完整 state → GET /api/threads/{tid}/state +- 单个 outputs 文件内容/下载 → GET /api/threads/{tid}/artifacts/{path} +- outputs 目录文件清单 ❗暂无 → 新增接口(见 §3) +``` + +**关键判断**:席位最终交付文本 + 总控结论文本 **已经在 step2 内存里**,无需重新调接口。文件清单是**目前唯一缺失**的数据源。 + +--- + +## 3. 接口需求 + +### 3.1 新增(**必需**):列出某 thread 的产出文件 + +``` +GET /api/multi-agent/sessions/artifacts?thread_id= +``` + +或者更通用、放进 artifacts 路由下: + +``` +GET /api/threads/{thread_id}/artifacts +``` + +**为什么需要**:现有 artifacts endpoint 只能"按已知路径下载文件",没有"列出目录"功能。Step 3 需要知道 sub-agent 写到 `/mnt/user-data/outputs/` 下哪些文件,才能在前端列出可下载清单。 + +**响应**: +```json +{ + "thread_id": "", + "files": [ + { + "path": "mnt/user-data/outputs/方案设计报告_生产系统安全改造技术方案.md", + "name": "方案设计报告_生产系统安全改造技术方案.md", + "size_bytes": 12480, + "mime_type": "text/markdown", + "modified_at": "2026-05-23T14:32:30.123Z" + }, + ... + ] +} +``` + +**实现建议**: +- 列 `backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/outputs/` 下的文件 +- 鉴权复用 `@require_permission("threads", "read", owner_check=True)` +- 路径要做 traversal 保护(虚拟路径仅限 `mnt/user-data/outputs/`) + +### 3.2 新增(**可选,体感优化**):一次性聚合 step3 数据 + +``` +POST /api/multi-agent/sessions/summary +Body: { + "main_agent_name": "roundtable-coordinator-xxx", + "thread_ids": { "": "", ... } +} +``` + +**为什么可选**:不加这个接口的话,前端需要为每个席位发一次 `GET /state` + 一次 `GET /artifacts`(共 7-13 次 round-trip)。加了这个接口,**一次返回所有数据**。 + +**响应**: +```json +{ + "main_agent_name": "roundtable-coordinator-xxx", + "coordinator": { + "thread_id": "...", + "final_text": "...", // 最后一条 AIMessage content + "dispatch_rounds": 3 // leader 派活的轮数(可选展示) + }, + "seats": [ + { + "agent_name": "roundtable-intelligence", + "thread_id": "...", + "display_name": "情报收集", + "final_text": "...", // 最后一条 AIMessage content + "files": [ // 该席位写到 outputs/ 的文件 + { "path": "...", "name": "...", "size_bytes": ..., "mime_type": "..." } + ] + }, + ... + ] +} +``` + +**取舍**: +- 优点:1 次请求 vs 13 次;体验更顺 +- 缺点:等于把 3.1 + N 次 state 读放到一个 endpoint,实现稍复杂 +- 推荐:**先做 3.1,3.2 暂缓**;前端用现有 endpoint 串起来够用 + +### 3.3 复用(**无需改动**) + +| Endpoint | 用途 | +|---|---| +| `GET /api/threads/{tid}/state` | 取席位 / 总控线程的完整状态,提取最后一条 AIMessage content | +| `GET /api/threads/{tid}/artifacts/{path}` | 点击文件名预览或下载(已有 `?download=true` 支持) | + +> Note: 这两个 endpoint 已经在 `app.gateway.routers.threads` / `app.gateway.routers.artifacts` 里实现,前端 `fetch-client` 也有现成调用方式。 + +--- + +## 4. 前后端对接流程 + +### 4.1 进入 step3 时 + +```ts +// 触发条件:runOrchestration 自然结束(leaderRes.dispatched.length === 0 +// 且无 clarification),用户点"达成共识生成全案成果"按钮 / 自动跳转。 + +// 前端手里已有:threadIds, coordinatorName, 最后的 leaderRes +// 1. 总控结论文本 — 已在内存,直接用 +const consensusText = lastLeaderRes.content; + +// 2. 各席位交付 — 已在 step2RoundtableDialogues 里,过滤"子智能体交付"的卡片 +const seatDeliveries = step2RoundtableDialogues + .filter(d => d.confidence === "子智能体交付") + .map(d => ({ sender: d.sender, content: d.content })); + +// 3. 文件清单 — 调新接口 +const filesPerSeat = await Promise.all( + Object.entries(threadIds) + .filter(([name]) => name !== coordinatorName) + .map(async ([name, tid]) => ({ + agentName: name, + files: (await fetch(`/api/threads/${tid}/artifacts`).then(r => r.json())).files, + })), +); +``` + +### 4.2 用户点击文件时 + +```ts +// 在线预览(mime 是 markdown/text/pdf 自动 inline) +window.open(`/api/threads/${threadId}/artifacts/${encodeURIComponent(filePath)}`); + +// 强制下载 +window.open(`/api/threads/${threadId}/artifacts/${encodeURIComponent(filePath)}?download=true`); +``` + +### 4.3 "重新起草新规划" 按钮 + +```ts +// 跟现在一样,纯前端:清掉 dialogues + 跳 step1 +setCurrentStep(1); +setInteractiveMessages(...); // 已有逻辑 +setStep2RoundtableDialogues([]); +// 不需要调任何接口 +``` + +--- + +## 5. 改造前端的具体文件清单 + +| 文件 | 改动 | +|---|---| +| `frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx` | **大改**。删除四张 metric 卡 / 三阶段路线图 / 论证要件 / 裁定判词;新增结论卡 + 席位折叠列表 + 文件清单 | +| `frontend-web/src/roundtable-planning/api/multi-agent.ts` | 新增 `listSessionArtifacts(threadId)` helper(封装 §3.1 接口) | +| `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 把 `threadIds` / `coordinatorName` / `lastLeaderContent` 传进 `HighFidelityReport` 的 props(目前没传) | + +--- + +## 6. 改造后端的具体文件清单 + +| 文件 | 改动 | +|---|---| +| `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | 新增 `GET /api/threads/{tid}/artifacts`(不带 path,列目录) | +| `offline-backend-20260512/backend/CLAUDE.md` | 在 artifacts 路由表里增加新条目说明 | +| `offline-backend-20260512/backend/tests/test_artifacts_list.py` | 列目录新接口的单测(覆盖:空目录返回 [];traversal 路径返回 403;不存在 thread 返回 404)| + +--- + +## 7. 实施优先级 + +| 优先级 | 项 | 备注 | +|---|---|---| +| P0 | 新增 `GET /api/threads/{tid}/artifacts` 列文件接口 | 必需,没它前端无法列文件 | +| P0 | step3 大改:删 mock 区块、加结论/席位/文件三块 | 主要价值改造 | +| P1 | 前端 helper `listSessionArtifacts` | 封装 fetch + 类型 | +| P2 | 单测 `test_artifacts_list.py` | 上线后跟进 | +| P3 | `POST /api/multi-agent/sessions/summary` 聚合接口 | 13 次→1 次,体感优化,**非必需** | + +--- + +## 8. 显式不做的事 ❌ + +1. **不做"PDF 导出"**:后端没有现成 PDF 渲染管线,要做就是单独的项目。当前页面那个 `导出方案 PDF` 按钮直接删掉,或者保留按钮但点击只触发 `window.print()`(让浏览器自己生成 PDF)。 +2. **不做"安全分享"**:项目里已有 `/api/thread-shares`(per-thread 分享)和 `/api/public/shares`(公开页面),但那是针对单个 thread 的,不是多 thread 会话的。要做"多 thread 整合分享"是新功能,远超 step3 的范围。 +3. **不做"指标雷达图"**(什么 90.3 分总评):没有真实数据源,强行加只会变成下一个 mock。 +4. **不做"签发投产"**:纯展示按钮,不接业务系统,删。 +5. **不做"轮次/对抗次数"显示**:除非前端记一下 `dispatch_count`,否则后端没现成统计。如果真要展示,用现成的"派活了几次"代替"对抗 N 轮"。 + +--- + +## 9. 验收清单 + +step3 改造完成后应该能: + +- [ ] 看到当前 session 的总控最终结论,markdown 渲染 +- [ ] 看到 6 个席位各自的最终交付内容(默认折叠,点击展开) +- [ ] 看到所有 sub-agent 写到 outputs 的文件列表,含文件名、大小、类型 icon +- [ ] 点击文件名能在线预览(markdown / pdf / 图片) +- [ ] 点击下载按钮能下载(带正确文件名) +- [ ] 点"重新起草新规划"能回到 step1 且 step2 内存清空 +- [ ] 页面不再包含任何来自 `budgetLimit` 的"假指标" + +--- + +## 10. 路径速查 + +| 资源 | 位置 | +|---|---| +| Step 3 组件 | `frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx` | +| 父页面 (state 持有者) | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | +| 前端 API helper | `frontend-web/src/roundtable-planning/api/multi-agent.ts` | +| 后端 artifacts 路由 | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | +| 后端 multi-agent 路由 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` | +| 物理 outputs 目录 | `backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/outputs/` | diff --git a/frontend-web/docs/浅红色ui修改.md b/frontend-web/docs/浅红色ui修改.md new file mode 100644 index 00000000..f2cdb310 --- /dev/null +++ b/frontend-web/docs/浅红色ui修改.md @@ -0,0 +1,310 @@ +# 浅色红色 UI 规范对齐 — 实施计划 + +> 依据:`H:\页面设计\UI设计规范\浅色红色设计规范-前端用.pdf`(共 104 页) +> 范围:**仅对齐 Light 模式(红色主题)**,**不改动图表部分** + +--- + +## ⚠️ 重要约束(必读) + +### 1. 主题切换必须保留 +项目使用 `next-themes` 实现 Light/Dark 切换: + +- `:root` → Light 模式(红色品牌,对应本规范) +- `.dark` → Dark 模式(蓝色品牌,**不在本次范围内**) +- 切换组件入口:`src/components/theme-provider.tsx` + +**所有改动只能动 `:root` 块和 light-only 选择器,禁止动 `.dark` 块和 `dark:` 前缀样式。** + +### 2. 不动图表 +本次跳过 `chart-1` ~ `chart-5` 颜色变量和所有图表组件。 + +### 3. 同一 token 双模式都用时如何改 +若变量同时被 light/dark 使用(如 `--radius`、按钮 `h-*`、`--primary-foreground`),改动后需在 Dark 模式下回归检查,确保没有破坏。 + +--- + +## 改动总目标对照 + +| 类别 | UI 规范 | 现状 | 差异 | +|------|--------|------|------| +| 主品牌红 | `#F5222D` | `#d92121` (`oklch(0.52 0.19 27)`) | 偏差 | +| 深色品牌 | `#CF1322` / `#A8071A` | 未使用 | 缺失 | +| 圆角-按钮 | 2px | 12px (`--radius: 0.75rem`) | 严重偏大 | +| 圆角-卡片 | 4px | 12px | 严重偏大 | +| 按钮高度(正常)| 32px | 36px (`h-9`) | 偏大 4px | +| 按钮高度(小)| 24px | 32px (`h-8`) | 偏大 8px | +| 输入框高度 | 32px | 36px (`h-9`) | 偏大 4px | +| 中文字体 | NotoSansSChinese | 系统字体 | 未引入 | +| 数字字体 | D-DIN | 系统字体 | 未引入 | +| 间距体系 | 8px 基准(4/8/12/16/24/32/40/56)| Tailwind 默认 | 基本一致 | + +--- + +## 分步实施计划 + +### Step 1 — 颜色变量对齐(Light 模式) + +**文件**:`src/styles/globals.css` +**只动 `:root` 块(第 242-277 行),保持 `.dark` 块原样** + +#### 1.1 主色映射 + +| 角色 | 规范值 | 用途 | +|------|--------|------| +| 主色 | `#F5222D` | `--primary`、`--ring`、`--sidebar-primary` | +| 主色 hover | `#CF1322` | 按钮 hover 状态(新增辅助变量) | +| 主色 active | `#A8071A` | 按钮按下状态 | +| 主色淡 1 | `#FEEDED` | `--accent`(极淡红,背景) | +| 主色淡 2 | `#FDDADC` | hover bg、tag 背景 | +| 主色淡 3 | `#FCB5B9` | 浅边框、selected 态 | +| 危险色 | `#F5222D` | `--destructive` | + +#### 1.2 新增工程变量(建议加在 `:root` 内) + +```css +/* 红色主题阶梯色(规范色板)*/ +--brand-50: #FEFAFA; +--brand-100: #FEF6F6; +--brand-200: #FEEDED; +--brand-300: #FDDADC; +--brand-400: #FCB5B9; +--brand-500: #FA9196; +--brand-600: #F86C73; +--brand-700: #F74750; +--brand-800: #F5222D; /* ← 主色 */ +--brand-900: #CF1322; /* ← hover */ +--brand-950: #A8071A; /* ← active */ + +/* 辅色(规范)*/ +--accent-orange: #FF9900; +--accent-green: #2FD69A; +--accent-cyan: #45C3F3; +``` + +#### 1.3 `--workspace-brand` 同步 + +`:root` 内:`--workspace-brand: #F5222D;`(从 `#d92121` 改) +`.dark` 内:**不动**(保持 `#3b82f6` 蓝色) + +#### 1.4 验收 +- 切到 Light 模式:所有按钮、链接、选中态使用规范红 +- 切到 Dark 模式:依然是原蓝色品牌(无变化) + +--- + +### Step 2 — 圆角规范化 + +**文件**:`src/styles/globals.css`(`--radius` 是全局变量,light/dark 共用) + +#### 2.1 改动方案(推荐) + +按规范:按钮 2px、卡片/页面框 4px。但当前许多组件依赖 `rounded-md` / `rounded-lg`,全局把 `--radius` 改成 4px 会引起视觉跳变。 + +**采用细粒度策略:** + +```css +:root { + --radius: 0.25rem; /* 4px → 卡片/页面框 */ +} + +@theme inline { + --radius-sm: 2px; /* 按钮、tag */ + --radius-md: calc(var(--radius) - 1px); /* 3px → 输入框 */ + --radius-lg: var(--radius); /* 4px → 卡片 */ + --radius-xl: calc(var(--radius) + 4px); /* 8px → 弹窗 */ + --radius-2xl: calc(var(--radius) + 8px); /* 12px → 大卡片 */ +} +``` + +#### 2.2 例外保留(设计感样式) +以下组件**圆角不改**,保持设计稿原貌: +- `.workspace-user-bubble`(聊天气泡,圆角 `1.6rem 1.6rem 0.5rem 1.6rem`) +- `.workspace-assistant-card`(圆角 `1.75rem`) +- `.gemini-gradient-border`(圆角 `1.5rem`) + +#### 2.3 验收 +- 普通按钮变为近直角(2px) +- 卡片维持温和圆角(4px) +- 聊天气泡、AI 卡片不变形 + +--- + +### Step 3 — 按钮组件尺寸对齐 + +**文件**:`src/components/ui/button.tsx` + +#### 3.1 改 size 变体(规范:小 24px / 正常 32px) + +| size | 规范 | 当前 | 改后 | +|------|------|------|------| +| `sm` | 24px | h-8 (32px) | `h-6 px-2 text-xs` | +| `default` | 32px | h-9 (36px) | `h-8 px-3 text-sm` | +| `lg` | — | h-10 (40px) | 保留 `h-10`(用于 Banner/登录场景)| + +#### 3.2 圆角 +- 默认按钮:`rounded-sm`(2px) + +#### 3.3 主按钮配色 +- bg:`bg-primary` +- hover:`hover:bg-[var(--brand-900)]`(深红 `#CF1322`) +- active:`active:bg-[var(--brand-950)]`(更深 `#A8071A`) +- disabled:`disabled:opacity-50`(保留) + +#### 3.4 次要按钮 / 文本按钮 +- 次要按钮(`outline` variant):边框 `border-primary`,文字 `text-primary` +- 文本按钮(`ghost` variant):仅文字色 `text-primary`,hover 背景 `--brand-200` + +#### 3.5 验收 +检查所有调用 ` + + + setNotificationOpen(true)}> + + {t.notifications.title} + {unreadCount > 0 && ( + + {unreadCount > 99 ? "99+" : unreadCount} + + )} + + openSettings("tools")}> + + {t.settings.sections.tools} + + openSettings("account")}> + + 个人信息 + + + + ) : ( + + )} + + +
+
+ ); } diff --git a/frontend-web/src/components/ui/sidebar.tsx b/frontend-web/src/components/ui/sidebar.tsx index f6c5e3e4..f544abf3 100644 --- a/frontend-web/src/components/ui/sidebar.tsx +++ b/frontend-web/src/components/ui/sidebar.tsx @@ -397,53 +397,53 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { ); } -const SidebarGroupLabel = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "div"; - - return ( - svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", - className, - )} - {...props} - /> - ); -}); - -SidebarGroupLabel.displayName = "SidebarGroupLabel"; +const SidebarGroupLabel = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div"; -const SidebarGroupAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "group-data-[collapsible=icon]:hidden", - className, - )} - {...props} - /> - ); -}); - -SidebarGroupAction.displayName = "SidebarGroupAction"; + return ( + svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className, + )} + {...props} + /> + ); +}); + +SidebarGroupLabel.displayName = "SidebarGroupLabel"; + +const SidebarGroupAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +}); + +SidebarGroupAction.displayName = "SidebarGroupAction"; function SidebarGroupContent({ className, @@ -503,99 +503,99 @@ const sidebarMenuButtonVariants = cva( }, ); -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string | React.ComponentProps; - } & VariantProps ->( - ( - { - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props - }, - ref, - ) => { - const Comp = asChild ? Slot : "button"; - const { isMobile, state } = useSidebar(); - - const button = ( - - ); - - if (!tooltip) { - return button; - } - - if (typeof tooltip === "string") { - tooltip = { - children: tooltip, - }; - } - - return ( - - {button} - - ); - }, -); - -SidebarMenuButton.displayName = "SidebarMenuButton"; +const SidebarMenuButton = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean; + isActive?: boolean; + tooltip?: string | React.ComponentProps; + } & VariantProps +>( + ( + { + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props + }, + ref, + ) => { + const Comp = asChild ? Slot : "button"; + const { isMobile, state } = useSidebar(); -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", - className, - )} - {...props} - /> - ); -}); - -SidebarMenuAction.displayName = "SidebarMenuAction"; + const button = ( + + ); + + if (!tooltip) { + return button; + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + }; + } + + return ( + + {button} + + ); + }, +); + +SidebarMenuButton.displayName = "SidebarMenuButton"; + +const SidebarMenuAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean; + showOnHover?: boolean; + } +>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + className, + )} + {...props} + /> + ); +}); + +SidebarMenuAction.displayName = "SidebarMenuAction"; function SidebarMenuBadge({ className, @@ -686,37 +686,37 @@ function SidebarMenuSubItem({ ); } -const SidebarMenuSubButton = React.forwardRef< - HTMLAnchorElement, - React.ComponentProps<"a"> & { - asChild?: boolean; - size?: "sm" | "md"; - isActive?: boolean; - } ->(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => { - const Comp = asChild ? Slot : "a"; - - return ( - svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", - size === "sm" && "text-xs", - size === "md" && "text-sm", - "group-data-[collapsible=icon]:hidden", - className, - )} - {...props} - /> - ); -}); - -SidebarMenuSubButton.displayName = "SidebarMenuSubButton"; +const SidebarMenuSubButton = React.forwardRef< + HTMLAnchorElement, + React.ComponentProps<"a"> & { + asChild?: boolean; + size?: "sm" | "md"; + isActive?: boolean; + } +>(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a"; + + return ( + svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +}); + +SidebarMenuSubButton.displayName = "SidebarMenuSubButton"; export { Sidebar, diff --git a/frontend-web/src/components/workspace/admin/admin-memory-page.tsx b/frontend-web/src/components/workspace/admin/admin-memory-page.tsx new file mode 100644 index 00000000..7469cd81 --- /dev/null +++ b/frontend-web/src/components/workspace/admin/admin-memory-page.tsx @@ -0,0 +1,716 @@ +"use client"; + +import { useState } from "react"; +import { Check, Loader2, Pencil, Plus, Trash2, X } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { + useAdminAddMemoryEntry, + useAdminAddV1Fact, + useAdminDeleteMemoryEntry, + useAdminDeleteV1Fact, + useAdminReplaceMemoryEntry, + useAdminUpdateV1Fact, + useAdminUpdateV1Section, + useUserMemory, +} from "@/core/admin/hooks"; +import { useAllUsers } from "@/core/auth/hooks"; + +export function getEntries(data: Record): string[] { + const raw = (data as { entries?: unknown }).entries; + if (!Array.isArray(raw)) return []; + return raw.filter((e): e is string => typeof e === "string" && e.trim().length > 0); +} + +// ── V1 只读视图 ──────────────────────────────────────────────────────────── + +const CTX_LABELS: Record = { + workContext: "工作背景", + personalContext: "个人信息", + topOfMind: "当前重点", +}; +const HISTORY_LABELS: Record = { + recentMonths: "近期", + earlierContext: "早期背景", + longTermBackground: "长期背景", +}; +const CATEGORY_COLORS: Record = { + preference: "bg-blue-100 text-blue-700", + knowledge: "bg-green-100 text-green-700", + context: "bg-yellow-100 text-yellow-700", + behavior: "bg-purple-100 text-purple-700", + goal: "bg-orange-100 text-orange-700", + correction: "bg-red-100 text-red-700", +}; + +const V1_CATEGORIES = ["preference", "knowledge", "context", "behavior", "goal", "correction"]; + +function extractSummary(val: unknown): string { + if (typeof val === "string") return val; + if (val && typeof val === "object" && "summary" in val) + return String((val as Record).summary ?? ""); + return ""; +} + +// 单个摘要字段的行内编辑 +function V1SectionField({ + label, + value, + onSave, +}: { + label: string; + value: string; + onSave: (v: string) => Promise; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + const [saving, setSaving] = useState(false); + + async function handleSave() { + if (draft.trim() === value.trim()) { setEditing(false); return; } + setSaving(true); + try { await onSave(draft.trim()); setEditing(false); } + catch (err) { toast.error(err instanceof Error ? err.message : "保存失败"); } + finally { setSaving(false); } + } + + return ( +
+
+ {label} + {!editing && ( + + )} +
+ {editing ? ( +
+