` Persona 覆盖;下一轮该席位的派活会用此 model |
+| `isUpperPanelCollapsed` | 是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间 |
+
+**协调智能体命名**:`roundtable-coordinator-${Date.now().toString(36)}`,每次 Step 2 会话新建。后端把这类壳 agent 留在 DB 中,因此 `filterRecommendCandidates` 默认过滤掉。
+
+### 5.4 编排循环 `runOrchestration(seedMessage, ids, coord)`
+
+伪代码:
+
+```
+isOrchestrating = true
+orchestrationCancelRef = false
+nextLeaderTask = seedMessage
+cycles = 0
+MAX_CYCLES = 8
+
+while !cancelled && cycles < MAX_CYCLES:
+ cycles += 1
+ highlight(speakerId=2) // 总控
+ leaderBubble = appendStreamingDialogue("总控协调", confidence: "调度中")
+ res = streamMultiAgent(leader, ids, coord, nextLeaderTask, onTextDelta, onPhaseChange)
+
+ if res.clarification:
+ finalize(leaderBubble, content, "等待用户澄清")
+ setPendingClarification(res.clarification)
+ break // 等用户回复后由 submitClarification 重启循环
+
+ finalize(leaderBubble, content, dispatched.length === 0 ? "全案盖印" : "派活说明")
+ if res.content: setLastLeaderContent(res.content)
+
+ if res.dispatched.length === 0:
+ setHasConsensus(true); setConsensusPercentage(100); break
+
+ for [subName, task] of res.dispatched: // **顺序**,不是并行
+ meta = agentNameToRole[subName.toLowerCase()]
+ markSpeakerStreaming(meta.speakerId)
+ perSeatModel = agentConfigs[meta.speakerId]?.model || DEFAULT_MODEL
+ subBubble = appendStreamingDialogue(meta.displayName, confidence: "调度中")
+ specRes = streamMultiAgent(special, ..., perSeatModel, signal=subAborter)
+ finalize(subBubble, specRes.content, "子智能体交付")
+ markSpeakerIdle(meta.speakerId)
+ if cancelled: break
+
+ setConsensusPercentage(prev => min(95, prev + 8))
+ nextLeaderTask = "各子智能体已就上一轮派活完成交付,请你结合最新结论..."
+
+if cycles >= MAX_CYCLES:
+ toast("⏱️ 已达到最大研讨轮次,自动停止以避免循环")
+
+clearAllStreamingSpeakers()
+isOrchestrating = false
+```
+
+要点:
+
+- **`buildInitialIntent()`**:
+ - 有 `intentReady` → 拼装 `objective / 约束条件 / 关键假设` + 「不要再反问用户」终结语
+ - 无 `intentReady`(用户跳过澄清 / 顶栏直接切到 Step 2)→ 拼接 Step 1 内 `interactiveMessages` 里 sender==='user' 的内容,逐行 `用户#N: ...`,**绝不**注入 mock 的「预算 500 万 / 72 小时」数字
+ - 用户输入完全为空 → 让总控先调用 `ask_clarification` 而不是凭空派活
+- **用户「干预研审」**:`handleInjectToRoundtable` 追加一个「💡 (人工指令介入) ...」气泡,然后构造 `runOrchestration("用户人工干预新指令:...", ...)`
+- **挂起 / 恢复**:
+ - 挂起 = `setIsRoundTableRunning(false)` → `stopOrchestration()` → `orchestrationCancelRef = true` + `abortAllInFlight()`
+ - 恢复 = `setIsRoundTableRunning(true)`,effect 触发 `runOrchestration("用户已恢复研讨,请继续上一轮的协调工作。", ...)`
+- **澄清恢复**:`submitClarification` 把用户回答拼到 prompt 里再 `runOrchestration`,不会重建 thread
+
+### 5.5 Persona 抽屉(Step 2 圆桌可视化点击)
+
+- 点击圆桌头像 → `setSelectedAgentId(seatId)` → 在上半部分下方展开抽屉
+- 抽屉控件:
+ - **System Prompt**:仅前端记录(后端覆写未开放)
+ - **Temp**:仅前端记录
+ - **推理模型**:选项来自 `useModels()`,默认 `DEFAULT_MODEL`
+- 「参数覆盖并重启动算研讨」按钮 → `handleSaveAgentAndRecalculate`:
+ - 把新的 `(seatId, model)` 写入 `agentConfigs`
+ - 追加一条「⚡ [模型偏好已保存] ...」对话气泡
+ - **不**立即重启编排;下一轮该席位被派活时 `runOrchestration` 内会读 `agentConfigs[meta.speakerId]?.model` 作为 special 调用的 `model` 参数
+
+### 5.6 草稿持久化(`utils/drafts.ts`)
+
+- Key:`roundtable-planning:drafts:v1`(`localStorage`)
+- 草稿快照含:`step1.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`step2.{threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit}`、`furthestStep`
+- `deriveDraftName({intentReady, interactiveMessages})`:优先用 `intentReady.objective`,否则取第一个 user 消息,否则用 `未命名草稿 <时间>`,统一截断到 40 字
+- **自动保存策略**:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发,避免覆盖 `furthestStep`
+- **手动保存**:顶栏 + Step 1 底部「保存草稿」按钮均调用 `handleManualSave`
+- **加载草稿**:`handleLoadDraft`
+ - 中断所有 in-flight(`abortAllInFlight + stopIntentStream`)
+ - **先**置 `loadedFromDraftRef.current = true`,再 `setCurrentStep(draft.furthestStep)`,避免 step1/step2 init effect 重新调用 init 把恢复的状态打掉
+ - `step1InitGenRef` 自增,淘汰可能晚归的旧 init
+ - `step2InitRef.current = draft.furthestStep >= 2`:曾经走到 Step 2 才标记已 init
+ - 角色面板基线状态:被恢复时全部置「在线」,否则 init 流程逐个置「在线」
+
+---
+
+## 6. 组件说明
+
+### 6.1 `IntentClarificationCard`
+
+- Props:`payload`(`ClarificationPayload`)、`selection`、`onSubmit(overrideSelection?)`、`disabled`、`submitting`、`fallbackContent`
+- **单选**(默认 `allowMultiple === false`):`resolveAllowMultiple` 返回 false → 点击选项即 `onSubmit({selectedIds:[id], custom: ""})`,UI 不显示「发送回答」按钮(除非 `allowCustom`)
+- **多选**:勾选多个 + 输入补充,再点「发送回答」;答案格式见 `formatIntentClarificationAnswer`:
+ - 仅选项 → `我选择:A、B`
+ - 仅自定义 → 直接发自定义文本
+ - 同时存在 → `我选择:A、B;补充:…`
+- 展示条件:`isPendingClarification && !intentReady && !parts.summary && pendingIntentClarificationMsgId === msg.id`(已出现 `[INTENT_READY]` 则隐藏可操作卡片)
+
+### 6.2 `RecommendAgentsDialog`
+
+- 打开时管线:`listAgents() → filterRecommendCandidates() → streamRecommend()`
+- 阶段 UI(`InternalState.phase`):
+ - `idle` → `loading_agents` → `recommending`(顶部 rationale 流;下方推荐卡 streaming=true)→ `ready` / `fallback` / `error`
+- 选择约束:`2 ≤ selected ≤ 8`;底部实时显示「至少再选 N 个」「已选 X / 8」
+- 失败兜底:候选为空 → error;status==="asking" 或 picks 不足 2 → `fallback`,自动用 `FALLBACK_AGENT_IDS` 与候选池交集勾选
+- 反竞态:内部 `generationRef`,每次 `runRecommendation` 自增,所有 setState 都校验 `myGen === generationRef.current`,避免「重新推荐」时旧 stream 写脏 state
+- `onConfirm(agentIds)` → 父组件 `handleConfirmAgents`:复位 `step2InitRef` 并 `setCurrentStep(2)`,触发 Step 2 init
+
+### 6.3 `HighFidelityReport`(Step 3)
+
+- 输入:`threadIds`、`coordinatorName`、`consensusText`、`hasConsensus`、`seatDeliveries`、`seatNameMap`、`onReset`
+- `seatThreadEntries`:过滤掉 `coordinatorName`,给每个 sub-thread 拉一次 `listSessionArtifacts`(`Promise.all` 并行)
+- `reloadKey`:「刷新文件清单」按钮自增,触发 effect 重新拉
+- **文件预览策略**(`shouldPreviewInModal`):
+ - 命中 → `ArtifactPreviewModal`(Markdown 用 react-markdown 渲染,其它当 plain text)
+ - 不命中(pdf / png / html / 二进制)→ `window.open(buildArtifactDownloadUrl)`,交给浏览器原生 viewer
+ - 命中规则:
+ - mimeType 是 `text/markdown` / `text/plain` / `text/csv` / `application/json` / `text/*`
+ - 或文件名后缀属于 `md / markdown / txt / json / csv / log / yml / yaml / xml`
+- 各席位最终交付(中段折叠区)数据源:父组件传入的 `seatDeliveries`,按发送者去重(后到的覆盖前者)
+- 「重新起草」→ 父组件 `handleStartNewTask`(清空并 `runStep1Init`)
+- 「打印 / 另存 PDF」→ `window.print()`,浏览器原生流程
+
+### 6.4 `ArtifactPreviewModal`
+
+- 通过 `buildArtifactDownloadUrl(threadId, path)`(不带 `download=true`)GET 文件正文
+- Markdown 文件(mime===`text/markdown` 或后缀 `.md/.markdown`)→ react-markdown + remark-gfm
+- 其它文本类 → `` 直出
+- 头部「下载」按钮单独走 `download=true` URL,触发浏览器附件下载
+- 关闭:Esc / 点击外层遮罩(精确匹配 `e.target === e.currentTarget`,避免内部冒泡误关)
+
+### 6.5 共享 UI 模式与辅助
+
+- **三段式流式气泡**:`appendStreamingDialogue` → `appendToDialogue`(仅追加增量)→ `finalizeDialogue`(设最终 content + confidence + `streaming=false`)。三函数都通过 `id` 匹配,避免数组下标错位
+- **Phase 到中文文案**:`phaseLabel(phase, agentType, toolName)`:
+ - `connecting` → 「调度中」
+ - `connected` → 「已连接,等待响应」
+ - `tool_calling` 按 toolName:`agent_orchestration → 正在派活`、`ask_clarification → 正在提问`、`present_files → 正在交付文件`,其它 → 「调用工具:」
+ - `tool_result` → 「处理工具结果」
+ - `streaming` → leader 时「正在派活说明」、special 时「正在输出」
+ - Step 1 的 `intentPhaseLabel`:`ask_clarification → 正在生成追问`、`streaming → 正在思考问题`
+- **Markdown 渲染**:Step 1 / 2 / 3 都用 `react-markdown` + `remark-gfm`,自定义紧凑间距的 `components`(`p` 用 `my-1 whitespace-pre-wrap` 等)
+- **自动滚动**:`useAutoScroll(ref, [list])` 跟随;用户上滑后停止跟随;`ScrollToBottomButton` 一键回底
+- **`activeSpeakerSet`**:圆桌可视化里多席位同时高亮需要 Set,单 leader 时仍可用 `activeSpeakerId`;两者均触发 ring 动画
+- **状态条颜色**:`研讨中` 红 / `在线` 绿 / `已完成` 绿勾 / `正在上线` 黄旋 / 其它灰
+
+---
+
+## 7. 澄清交互(与主聊天对齐)
+
+| 能力 | 实现位置 |
+|------|----------|
+| 结构化 payload 类型 | `multi-agent.ts` → `ClarificationPayload`(intent.ts 复用) |
+| 单选/多选 | `lib/clarification.ts` → `resolveAllowMultiple`(缺省单选) |
+| 答案拼接 | `formatIntentClarificationAnswer`(`IntentClarificationCard`) |
+| Step 1 卡片 | `IntentClarificationCard` + `submitIntentClarification` |
+| Step 2 卡片 | 页面底部 `pendingClarification` 区块 + `submitClarification` → 再 `runOrchestration` |
+| 主聊天参考 | `src/components/workspace/messages/message-list.tsx` → `ClarificationCard` |
+
+后端字段 `allow_multiple` 缺省为 **false**(单选、点击即发送)。
+
+**关键差异**:
+
+- Step 1 用 `IntentClarificationCard` 组件(嵌在气泡里),Step 2 直接在页面底部渲染 amber 色带(**没有**抽象成独立组件,紧贴对话区)。
+- Step 2 澄清回复后**不**重建 thread;新 prompt 自带「用户已回复你刚才的澄清问题。问题:... 用户回答:...」上下文。
+
+---
+
+## 8. 与主聊天的差异
+
+| 维度 | 主聊天 `/page/workspace/chats/new` | 圆桌规划 |
+|------|-----------------------------------|----------|
+| SDK | `@langchain/langgraph-sdk` 直连 | 自研 SSE 解析(`apiFetch` + ReadableStream) |
+| 澄清展示 | `message-list` ToolMessage `additional_kwargs` | 终态帧 `status: "clarification"` |
+| Agent | 用户可选 assistant | 内置 intent + 动态 coordinator + 可选席位 |
+| 线程 | 单 thread | Step 1 一线程;Step 2 每席位 + 协调各一线程 |
+| 终态语义 | LangGraph state | 自定义 `{status, content, ...}` 终态帧 |
+| 暂停 | LangGraph interrupt | `AbortController.abort()` |
+
+复用思路:澄清卡片交互、答案文案格式;**不要**假设能直接复用 LangGraph React hooks。
+
+---
+
+## 9. 关键交互清单
+
+| 交互 | 行为 |
+|------|------|
+| Step 1 发送 | Enter 发送(Shift+Enter 换行);发送中显示「暂停」 |
+| Step 1 暂停 | `stopIntentStream()`,气泡尾部追加 `(已暂停)` 并 toast |
+| Step 1 进入 Step 2 | 需 `intentReady`;打开推荐弹窗而非直接切步 |
+| Step 1 跳过此项 | 仅本地追加「系统」气泡,不发请求;适合演示 |
+| Step 2 init | 首次进入自动 SSE init;离开 Step 2 **不**销毁 `threadIds`(可返回继续看,不会二次 init) |
+| Step 2 挂起/恢复 | `isRoundTableRunning` + `stopOrchestration` / 恢复时再 leader 一轮 |
+| Step 2 干预 | 底部输入框 → `runOrchestration` 新 seed,wrap 进「用户人工干预新指令:...」 prompt |
+| Step 2 Persona | 点圆桌头像打开抽屉;保存后下一轮该席位使用所选模型 |
+| 新建任务 | `handleStartNewTask`:abort 全部、清 state、`runStep1Init`,与 Step 3「重新起草」共用 |
+| 历史草稿 | 顶栏下拉:加载 / 重命名 / 删除(删除带 `window.confirm`) |
+| Step 3 预览 | 文本类 → 模态预览;其它 → 新窗口下载 |
+
+---
+
+## 10. 类型与常量速查
+
+```ts
+// Step 1 输出 → Step 2 输入
+interface IntentSummary {
+ objective: string;
+ constraints: string[];
+ assumptions?: string[];
+}
+
+// Step 2 线程映射(init_done.thread_ids)
+type ThreadIdMap = Record;
+
+// 派活
+type LeaderDispatch = [string, string]; // [agent_name, task]
+
+// 当前选中的研讨席位
+type SelectedAgent = { agent_id: string; name: string };
+
+// Step 1 单条澄清问题
+interface ClarificationPayload {
+ question: string;
+ clarificationType: string;
+ context: string;
+ options: unknown[];
+ allowCustom: boolean;
+ allowMultiple?: boolean;
+}
+
+// Step 2 stream 终态
+interface StreamMultiAgentResult {
+ dispatched: LeaderDispatch[];
+ message: string | null;
+ content: string;
+ agentName: string | null;
+ clarification: ClarificationPayload | null;
+}
+```
+
+**核心常量**:
+
+| 常量 | 值 | 位置 |
+|------|----|------|
+| `DEFAULT_MODEL` | `"deepseek-chat"` | RoundtablePlanningPage |
+| `COMMANDER_SEAT_ID` | `1` | RoundtablePlanningPage |
+| `MAX_CYCLES` | `8` | RoundtablePlanningPage(`runOrchestration` 内) |
+| `MIN_SELECTED` / `MAX_SELECTED` | `2` / `8` | RecommendAgentsDialog |
+| `FALLBACK_AGENT_IDS` | 6 内置 roundtable-* | RecommendAgentsDialog |
+| `BUILTIN_AGENT_META` | 6 内置头像/中文名 | RoundtablePlanningPage |
+| `FALLBACK_AVATAR_TYPES` | `[blue, purple, emerald, amber, pink]` | RoundtablePlanningPage |
+| `STORAGE_KEY` | `"roundtable-planning:drafts:v1"` | drafts.ts |
+| `INTENT_READY_RE` | `/\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/` | RoundtablePlanningPage |
+| `RECOMMEND_READY_MARKER` | `"[RECOMMEND_READY]"` | recommend.ts |
+| `EXCLUDED_AGENT_IDS` / 前缀 | `roundtable-intent`、`roundtable-recommender`、`roundtable-coordinator-*` | recommend.ts |
+
+---
+
+## 11. 故障排查
+
+| 现象 | 可能原因 | 排查 |
+|------|----------|------|
+| Step 1 一直「等待澄清」但气泡里出现绿色 [INTENT_READY] 摘要 | 终态帧未带 `summary` | 看 `splitIntentReadyBlock` + 同步 `useEffect`,必要时手动刷新 |
+| Step 1 角色面板卡在「正在上线」 | StrictMode 双 mount 导致 init 异步被丢弃 | 已用 `step1InitGenRef` 修复;检查是否被改坏 |
+| 选项全是多选 | `allow_multiple` 未传或为 true | Network 终态帧;`resolveAllowMultiple` |
+| Step 2 init 卡住 | init SSE error / 代理 | Network `init`;控制台 `[init-timing]` |
+| Step 2 stream 看不到任何文字 | TitleMiddleware 过滤错杀 / phase 卡在 connecting | 控制台搜 `[stream:` 前缀(需 `__MULTI_AGENT_DEBUG__`) |
+| 总控不派活 | `dispatched` 一直 `[]` 且非 clarification | 看 leader 终态帧;模型 SOUL 配置 |
+| 推荐弹窗为空 | `filterRecommendCandidates` 把所有候选都过滤了 | `GET /api/agents` 列表;检查 `EXCLUDED_AGENT_IDS` |
+| 推荐弹窗一直「等待推荐结果」 | recommender 模型死循环 | 控制台看 `[stream:` 日志,或点「重新推荐」 |
+| Step 3 各席位文件为空 | 子 agent 未写 outputs 或 thread id 错 | 对应 thread 调 `listSessionArtifacts` |
+| Step 3 预览空白 | 文件不是文本类,但被错误命中 `shouldPreviewInModal` | 检查 mime + 后缀;不在白名单的应该跳 `window.open` |
+| Step 3 文件名乱码 / 404 | `buildArtifactDownloadUrl` 编码错 | 看 path 是否被分段 encode;不能整体 encode(会把 `/` 也编了) |
+| 草稿加载后重复 init | `loadedFromDraftRef` 未置位 | `handleLoadDraft` 流程,注意 ref 必须先于 `setCurrentStep` |
+| 顶栏 Step 2 / Step 3 一直灰 | `intentReady` / `hasConsensus` 没设上 | 看终态帧;leader.dispatched 是否真为 `[]` |
+| Persona 抽屉改模型不生效 | 下一轮才生效 / 抽屉 model 未传给 streamMultiAgent | 看 `agentConfigs[meta.speakerId]?.model` 的传递路径 |
+| 并行 SSE 卡顿 | 误改为 parallel 派活 | 保持顺序 dispatch,详见 §5.4 注释 |
+
+**本地调试开关**:
+
+```js
+window.__MULTI_AGENT_INIT_TIMING__ = true; // init 各阶段耗时(默认开)
+window.__MULTI_AGENT_DEBUG__ = true; // stream 帧计数(默认开)
+```
+
+---
+
+## 12. 扩展指南
+
+### 12.1 新增默认研讨席位
+
+1. 后端 `.deer-flow/agents/roundtable-xxx/`
+2. `BUILTIN_AGENT_META` + `DEFAULT_SELECTED_AGENTS` + `RecommendAgentsDialog` 的 `FALLBACK_AGENT_IDS`
+3. 无需改 API 层(`agent_names` 由推荐结果传入)
+
+### 12.2 调整 Step 1 澄清轮次上限
+
+逻辑在 agent SOUL,不在前端;前端只需处理任意轮 `asking` / `clarification` / `done`。
+
+### 12.3 调整 Step 2 编排上限
+
+`RoundtablePlanningPage.tsx` 内 `MAX_CYCLES = 8`,超出会 toast「⏱️ 已达到最大研讨轮次,自动停止以避免循环」。若放宽:注意 `step2RoundtableDialogues` 数组会线性增长,超长讨论需要考虑虚拟滚动。
+
+### 12.4 把 special 改回并行
+
+不推荐;若一定要做:
+
+- 把 `for ... of leaderRes.dispatched` 改成 `await Promise.all(leaderRes.dispatched.map(...))`
+- 评估浏览器性能:每 token 一次 `setStep2RoundtableDialogues(prev => prev.map(...))`,N=6 时实测明显掉帧
+- 用更高效的派发结构(如按 id 分桶 `Map` + ref 缓存)改善之后再启用
+
+### 12.5 接入真实 Persona 覆盖
+
+当前 prompt / temp 只在前端记录;要落地需:
+
+- 后端开放 per-agent override 接口或在 `streamMultiAgent` 新增 `prompt_override` / `temperature` 字段
+- 前端:`agentConfigs[meta.speakerId]` 一并传入
+
+### 12.6 拆分页面
+
+当前刻意单文件维护三步。若拆分建议:
+
+- `useRoundtableStep1.ts` / `useRoundtableStep2.ts` — 状态与副作用
+- 保持 `api/*` 不变
+- 保持 `roundtable-planning.css` 主题变量
+
+### 12.7 文档协同
+
+`@/roundtable-planning/` 内一切公共 API 都应当:
+
+1. 在 `api/*.ts` 文件级注释里说明 endpoint
+2. 在本页第 4 节 / 第 10 节同步对照
+3. 后端字段变化(如 `init_done` 字段名)需要同步改 `initMultiAgent` 解析逻辑 + `multi-agent-api.md`
+
+---
+
+## 13. 路径速查
+
+| 资源 | 路径 |
+|------|------|
+| 主页面 | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` |
+| Step 1 API | `frontend-web/src/roundtable-planning/api/intent.ts` |
+| 推荐 API | `frontend-web/src/roundtable-planning/api/recommend.ts` |
+| Step 2/3 API | `frontend-web/src/roundtable-planning/api/multi-agent.ts` |
+| 澄清卡片 | `frontend-web/src/roundtable-planning/components/IntentClarificationCard.tsx` |
+| 推荐弹窗 | `frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx` |
+| Step 3 报告 | `frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx` |
+| 文件预览 | `frontend-web/src/roundtable-planning/components/ArtifactPreviewModal.tsx` |
+| 回底按钮 | `frontend-web/src/roundtable-planning/components/ScrollToBottomButton.tsx` |
+| 滚动跟随 | `frontend-web/src/roundtable-planning/hooks/useAutoScroll.ts` |
+| 澄清单选逻辑 | `frontend-web/src/roundtable-planning/lib/clarification.ts` |
+| 草稿存档 | `frontend-web/src/roundtable-planning/utils/drafts.ts` |
+| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
+| 样式 | `frontend-web/src/roundtable-planning/styles/roundtable-planning.css` |
+| 主聊天澄清参考 | `frontend-web/src/components/workspace/messages/message-list.tsx` |
+| 后端开发文档 | `frontend-web/docs/multi-agent-backend-dev.md` |
+| 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
deleted file mode 100644
index 62f36b0c..00000000
--- a/frontend-web/docs/multi-agent-step1-api.md
+++ /dev/null
@@ -1,492 +0,0 @@
-# 多智能体圆桌 · 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
deleted file mode 100644
index b0b76023..00000000
--- a/frontend-web/docs/multi-agent-step3-api.md
+++ /dev/null
@@ -1,264 +0,0 @@
-# 多智能体圆桌 · 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/openCanvas-backend-checklist.md b/frontend-web/docs/openCanvas-backend-checklist.md
deleted file mode 100644
index 31152fbe..00000000
--- a/frontend-web/docs/openCanvas-backend-checklist.md
+++ /dev/null
@@ -1,1031 +0,0 @@
-# Open Canvas 后端实施清单
-
-## 1. 文档目标
-
-本文档是 [openCanvas.md](./openCanvas.md) 的后端落地版清单。
-
-目标是把 Canvas 模式在当前 DeerFlow 后端中的实施工作拆成可以直接排期和执行的任务,覆盖:
-
-- 线程状态扩展
-- `canvas_agent` 设计
-- LangGraph 节点拆分
-- Gateway API 扩展
-- 流式输出契约
-- 模型配置与本地模型支持
-- 结构化 artifact 的持久化
-- 测试与验收顺序
-
-本文档默认前端会新增独立的 Canvas 子系统,并通过现有 LangGraph 线程和 Gateway API 与后端通信。
-
----
-
-## 2. 实施原则
-
-### 2.1 总原则
-
-- 不破坏现有 `lead_agent`
-- 不破坏现有 `artifacts: list[str]` 文件产物体系
-- 优先增量扩展,不做推倒重构
-- 先打通状态和协议,再实现复杂 AI 行为
-- 先支持文本/代码双类型,再逐步扩展快捷动作
-
-### 2.2 推荐路线
-
-推荐采用“新增独立 `canvas_agent`”方案,而不是把所有 Canvas 行为塞进现有 `lead_agent`。
-
-推荐原因:
-
-- 风险更低
-- 图逻辑更清晰
-- 更容易对齐 open-canvas 的状态结构
-- 不影响现有 DeerFlow 普通聊天与文件产物模式
-
-### 2.3 后端目标边界
-
-本次后端实施重点是:
-
-- 新增 Canvas 专用线程状态字段
-- 新增 Canvas Agent 图
-- 新增前端需要的模型能力元数据
-- 支持结构化 artifact 在线程状态中读写
-
-本次不要求:
-
-- 完整复刻 open-canvas 的所有 TS 图结构
-- 完整复刻 Supabase 相关逻辑
-- 把所有模型 provider 完全抽象成统一能力中台
-
----
-
-## 3. 现有后端基础能力盘点
-
-当前 DeerFlow 后端已经具备非常好的基础设施:
-
-- LangGraph 线程与流式交互
-- FastAPI Gateway
-- 模型动态配置
-- 本地线程状态管理
-- Sandbox/工具体系
-- 上传与文件 artifact 访问
-- Memory 与标题生成
-
-因此 Canvas 模式并不是“从 0 开始做 Agent 平台”,而是“在现有平台上新增一类结构化创作模式”。
-
----
-
-## 4. 目标代码落点
-
-建议优先关注以下目录:
-
-```text
-offline-backend-20260512/backend/
- app/gateway/
- routers/
- deps.py
- app.py
- packages/harness/deerflow/
- agents/
- factory.py
- thread_state.py
- lead_agent/
- middlewares/
- runtime/
- config/
- tools/
-```
-
-建议为 Canvas 新增或扩展以下区域:
-
-```text
-packages/harness/deerflow/
- agents/
- canvas_agent/
- __init__.py
- agent.py
- prompt.py
- state.py
- nodes/
- route_user_intent.py
- generate_artifact.py
- rewrite_artifact.py
- update_highlighted_text.py
- update_highlighted_code.py
- generate_followup.py
- quick_actions.py
- web_search.py
-```
-
-如果不想把节点拆得太细,首版也可以把部分逻辑先收在 `agent.py` 里,但建议至少把 `state.py` 和 `prompt.py` 独立出来。
-
----
-
-## 5. 开发阶段总览
-
-后端建议分为 11 个阶段:
-
-1. 线程状态扩展
-2. 结构化 artifact 协议定义
-3. `canvas_agent` 接入到 Agent 工厂
-4. Canvas 图的最小节点实现
-5. Gateway/线程接口兼容
-6. 流式状态输出适配
-7. 模型能力元数据扩展
-8. 本地模型兼容与配置统一
-9. 快捷动作与局部改写
-10. Web 搜索增强
-11. 测试、回归、验收
-
----
-
-## 6. 阶段一:线程状态扩展
-
-### 6.1 任务目标
-
-让 DeerFlow 的线程状态能原生保存 Canvas 所需的结构化信息。
-
-### 6.2 任务清单
-
-- 扩展 [thread_state.py](../../offline-backend-20260512/backend/packages/harness/deerflow/agents/thread_state.py)
-- 定义 Canvas 相关 TypedDict 或 dataclass 结构
-- 确保状态字段可序列化
-- 不影响现有 `messages/title/artifacts/todos` 逻辑
-
-### 6.3 建议新增字段
-
-在现有 `ThreadState` 上新增:
-
-- `artifact`
-- `highlighted_text`
-- `highlighted_code`
-- `canvas_mode`
-- `quick_action_id`
-- `web_search_enabled`
-- `web_search_results`
-
-### 6.4 建议数据形态
-
-可先采用 JSON-safe dict 结构,而不是在状态层引入过重的复杂对象。
-
-建议:
-
-```python
-class CanvasArtifactText(TypedDict):
- index: int
- type: Literal["text"]
- title: str
- fullMarkdown: str
-
-class CanvasArtifactCode(TypedDict):
- index: int
- type: Literal["code"]
- title: str
- language: str
- code: str
-
-class CanvasArtifact(TypedDict):
- currentIndex: int
- contents: list[CanvasArtifactText | CanvasArtifactCode]
-```
-
-### 6.5 与现有 `artifacts` 的关系
-
-必须保留现有:
-
-- `artifacts: list[str]`
-
-新增:
-
-- `artifact: dict | None`
-
-含义区分:
-
-- `artifacts` 表示文件型产物列表
-- `artifact` 表示 Canvas 当前主文档
-
-### 6.6 验收标准
-
-- 旧线程无 `artifact` 时不报错
-- 新线程可写入 `artifact`
-- 线程状态能被 FastAPI 和 LangGraph 正确序列化
-
----
-
-## 7. 阶段二:结构化 Artifact 协议定义
-
-### 7.1 任务目标
-
-统一前后端对 Canvas artifact 的字段约定。
-
-### 7.2 任务清单
-
-- 在后端建立一份明确的 artifact 协议定义
-- 明确字段命名、类型、兼容策略
-- 明确版本管理规则
-
-### 7.3 协议要求
-
-必须支持:
-
-- 文本 artifact
-- 代码 artifact
-- 标题
-- 当前版本索引
-- 历史版本列表
-- 后续支持局部改写
-
-### 7.4 版本规则
-
-- 每次 AI 整体生成,生成 `contents[0]`
-- 每次 AI 重写,追加一个新版本
-- `currentIndex` 指向当前版本
-- 历史版本默认只保留在线程状态中,不额外落库
-
-### 7.5 建议约束
-
-- `index` 从 1 开始
-- `currentIndex` 必须落在 `contents` 范围内
-- `type` 只允许 `"text"` 或 `"code"`
-
-### 7.6 验收标准
-
-- 前后端字段完全一致
-- 状态协议有明确默认值和边界规则
-
----
-
-## 8. 阶段三:接入 `canvas_agent`
-
-### 8.1 任务目标
-
-在现有 Agent 体系中注册一个独立的 Canvas Agent。
-
-### 8.2 任务清单
-
-- 新建 `packages/harness/deerflow/agents/canvas_agent/`
-- 新建 `agent.py`
-- 新建 `prompt.py`
-- 新建 `state.py`
-- 修改 [factory.py](../../offline-backend-20260512/backend/packages/harness/deerflow/agents/factory.py)
-
-### 8.3 注册方式建议
-
-建议将 `canvas_agent` 作为一个显式 assistant id 暴露给 LangGraph 运行时。
-
-前端调用时:
-
-- `assistant_id=canvas_agent`
-
-### 8.4 Agent 设计原则
-
-- Canvas Agent 不负责 DeerFlow 全部工具生态
-- 首版专注于:
- - 生成文档
- - 重写文档
- - 局部改写
- - 跟进回复
-
-### 8.5 工具策略
-
-首版建议:
-
-- 不强依赖 shell/sandbox
-- 不强依赖 present_files
-- 先把生成与改写链路做好
-
-后续可以根据需要接入文件导出能力。
-
-### 8.6 验收标准
-
-- `canvas_agent` 可被识别
-- 前端能以 assistant id 调用它
-- 不影响 `lead_agent`
-
----
-
-## 9. 阶段四:Canvas 状态与图定义
-
-### 9.1 任务目标
-
-为 Canvas Agent 定义清晰、最小但可扩展的状态结构。
-
-### 9.2 任务清单
-
-- 新建 `canvas_agent/state.py`
-- 定义图状态
-- 约束与 `ThreadState` 的映射方式
-
-### 9.3 建议状态字段
-
-- `messages`
-- `_messages` 或等价内部消息
-- `artifact`
-- `highlighted_text`
-- `highlighted_code`
-- `quick_action_id`
-- `web_search_enabled`
-- `web_search_results`
-- `next`
-
-### 9.4 状态设计建议
-
-优先复用 LangGraph 原生消息 reducer。
-
-对结构化字段,尽量采用:
-
-- 可整体替换
-- 易序列化
-- 无 Python 特殊对象依赖
-
-### 9.5 验收标准
-
-- 状态能驱动 Canvas 节点运行
-- 状态可被序列化回线程
-
----
-
-## 10. 阶段五:Canvas 图最小节点闭环
-
-### 10.1 任务目标
-
-先实现最小闭环,不要一开始就追求 open-canvas 全量功能。
-
-### 10.2 首版最小节点
-
-建议至少实现:
-
-- `route_user_intent`
-- `reply_general_input`
-- `generate_artifact`
-- `rewrite_artifact`
-- `generate_followup`
-
-### 10.3 节点职责
-
-#### `route_user_intent`
-
-判断用户当前意图:
-
-- 普通聊天
-- 新建 artifact
-- 重写当前 artifact
-
-#### `reply_general_input`
-
-仅返回普通消息,不更新 artifact。
-
-#### `generate_artifact`
-
-根据用户需求生成第一版 artifact。
-
-#### `rewrite_artifact`
-
-根据当前 artifact 和用户请求生成新版本。
-
-#### `generate_followup`
-
-在 artifact 更新后补一条自然语言消息。
-
-### 10.4 输出要求
-
-必须至少更新:
-
-- `messages`
-- `artifact`
-
-### 10.5 验收标准
-
-- 无 artifact 时能生成新 artifact
-- 有 artifact 时能整体重写
-- 重写后版本数增加
-
----
-
-## 11. 阶段六:Prompt 设计与模型调用封装
-
-### 11.1 任务目标
-
-把 Canvas 模式的 prompt 和模型调用逻辑集中管理。
-
-### 11.2 任务清单
-
-- 在 `canvas_agent/prompt.py` 中集中定义 prompt 常量
-- 在 `canvas_agent/agent.py` 或节点工具模块中封装模型调用
-
-### 11.3 最少需要的 prompt
-
-- 新建 artifact prompt
-- 整体重写 artifact prompt
-- 路由判断 prompt
-- followup prompt
-
-### 11.4 设计建议
-
-提示词必须显式约束:
-
-- 输出完整文档还是局部片段
-- 是否允许修改类型
-- 是否允许修改标题
-- 对文本和代码的输出格式要求
-
-### 11.5 模型调用封装建议
-
-不要在每个节点里重复拼模型配置。
-
-建议提供统一函数:
-
-- `get_canvas_model(...)`
-- `invoke_canvas_model(...)`
-
-### 11.6 验收标准
-
-- prompt 集中管理
-- 节点中模型调用逻辑可复用
-
----
-
-## 12. 阶段七:Gateway 线程接口兼容
-
-### 12.1 任务目标
-
-让前端读取线程状态时能拿到结构化 `artifact`。
-
-### 12.2 任务清单
-
-- 检查 [threads.py](../../offline-backend-20260512/backend/app/gateway/routers/threads.py)
-- 扩展线程 state 响应的 `values`
-- 确保 `artifact` 字段会被安全返回
-
-### 12.3 要求
-
-- `GET /api/threads/{id}`
-- 线程历史
-- 状态读取接口
-
-都要兼容 `artifact`
-
-### 12.4 注意事项
-
-- 历史 checkpoint 可能没有 `artifact`
-- 旧线程必须兼容
-- 结构化字段不能被错误裁剪
-
-### 12.5 验收标准
-
-- 前端取线程时能拿到 `artifact`
-- 刷新页面后 Canvas 内容能恢复
-
----
-
-## 13. 阶段八:流式输出适配
-
-### 13.1 任务目标
-
-让 Canvas 模式的 artifact 能通过 LangGraph 流实时更新。
-
-### 13.2 任务清单
-
-- 检查运行时序列化逻辑
-- 确保 `artifact` 在流事件中可见
-- 明确前端消费 `values` 还是 `updates`
-
-### 13.3 输出建议
-
-首版允许采用较简单策略:
-
-- 每次节点完成后输出完整 `artifact`
-
-不强制首版做精细的 token 级 artifact 局部流。
-
-### 13.4 原因
-
-完整对象覆盖更新:
-
-- 容易实现
-- 容易调试
-- 前后端一致性更好
-
-### 13.5 二期方向
-
-后续再考虑:
-
-- 局部 chunk 更新
-- 文本与代码差异化流式拼接
-
-### 13.6 验收标准
-
-- 前端发送请求时,artifact 能跟随 run 更新
-- 流结束后状态与线程最终值一致
-
----
-
-## 14. 阶段九:模型接口扩展
-
-### 14.1 任务目标
-
-让前端能拿到适合 Canvas 模式的模型能力信息。
-
-### 14.2 任务清单
-
-- 扩展 [models.py](../../offline-backend-20260512/backend/app/gateway/routers/models.py)
-- 扩展模型响应结构
-
-### 14.3 现有字段之外建议新增
-
-- `provider`
-- `supports_vision`
-- `supports_tool_calling`
-- `supports_canvas_generation`
-- `supports_canvas_rewrite`
-- `supports_canvas_code_edit`
-- `supports_streaming_text`
-- `supports_streaming_tool_calls`
-- `is_local`
-- `base_url`
-
-### 14.4 注意事项
-
-不是所有字段都必须从配置文件显式写出。
-
-可以采用:
-
-- 配置直出
-- provider 推断
-- 默认值补齐
-
-### 14.5 验收标准
-
-- 前端模型选择器能获取足够元数据
-- 本地模型和云模型能区分
-
----
-
-## 15. 阶段十:本地模型支持与配置统一
-
-### 15.1 任务目标
-
-支持像 open-canvas 一样自由配置本地模型,但配置真源在 DeerFlow。
-
-### 15.2 原则
-
-- 以 `config.yaml` 为唯一真源
-- 前端不依赖 open-canvas 的 TS 模型表
-- 后端负责把配置转换成前端可消费的能力描述
-
-### 15.3 首批支持范围
-
-推荐先支持:
-
-- Ollama
-- OpenAI-compatible 本地服务
-
-例如:
-
-- LM Studio
-- vLLM
-- LocalAI
-- One API/OpenAI 兼容代理
-
-### 15.4 配置要求
-
-建议模型配置支持以下字段:
-
-- `name`
-- `display_name`
-- `use`
-- `model`
-- `api_key`
-- `base_url`
-- `supports_vision`
-- `supports_thinking`
-- `supports_reasoning_effort`
-- `supports_tool_calling`
-- `is_local`
-
-### 15.5 Canvas 额外能力字段
-
-建议再增加:
-
-- `supports_canvas_generation`
-- `supports_canvas_rewrite`
-- `supports_canvas_code_edit`
-
-### 15.6 验收标准
-
-- 配置本地模型后 `/api/models` 可见
-- `canvas_agent` 可调用本地模型
-- 至少文本生成和整体改写可用
-
----
-
-## 16. 阶段十一:整体重写节点完善
-
-### 16.1 任务目标
-
-让已有 artifact 能被稳定整体重写并产生新版本。
-
-### 16.2 任务清单
-
-- 实现 `rewrite_artifact`
-- 明确文本与代码两种重写路径
-- 维护标题和类型变更规则
-
-### 16.3 规则建议
-
-- 默认保持当前类型
-- 用户明确要求时才允许改类型
-- 默认保持标题
-- 用户明确换主题时可更新标题
-
-### 16.4 结果写入规则
-
-- 新版本追加到 `contents`
-- `currentIndex` 指向新版本
-
-### 16.5 验收标准
-
-- 文本 artifact 整体重写正常
-- 代码 artifact 整体重写正常
-- 历史版本保留
-
----
-
-## 17. 阶段十二:局部改写节点
-
-### 17.1 任务目标
-
-支持文本和代码的局部选区修改。
-
-### 17.2 任务清单
-
-- 新增 `update_highlighted_text.py`
-- 新增 `update_highlighted_code.py`
-- 在状态中消费选区信息
-
-### 17.3 文本局部改写要求
-
-输入:
-
-- 当前 `fullMarkdown`
-- 选区起止
-- 选中原文
-- 用户修改指令
-
-输出:
-
-- 只返回替换片段或返回完整合并后文本
-
-### 17.4 首版建议
-
-首版优先采用“后端返回完整合并后文本”策略。
-
-原因:
-
-- 前端逻辑更轻
-- 状态更一致
-
-### 17.5 代码局部改写要求
-
-输入:
-
-- 当前代码全文
-- 选区代码片段
-- 选区边界
-- 用户修改指令
-
-输出:
-
-- 新的完整代码文本
-
-### 17.6 验收标准
-
-- 文本局部改写成功
-- 代码局部改写成功
-- 未选中区域不被误改
-
----
-
-## 18. 阶段十三:快捷动作节点
-
-### 18.1 任务目标
-
-支持前端动作栏触发不同的重写行为。
-
-### 18.2 任务清单
-
-- 新增 `quick_actions.py`
-- 定义动作 id 到 prompt 的映射
-- 在路由节点中识别 `quick_action_id`
-
-### 18.3 首版动作建议
-
-文本:
-
-- `expand`
-- `shorten`
-- `formalize`
-- `casualize`
-
-代码:
-
-- `add_comments`
-- `add_logs`
-- `fix_bugs`
-
-### 18.4 实现建议
-
-动作不要散落在各节点里硬编码。
-
-建议集中成映射表:
-
-- `action_id -> prompt builder`
-
-### 18.5 验收标准
-
-- 指定动作可触发不同结果
-- 动作执行后生成新版本
-
----
-
-## 19. 阶段十四:Web 搜索增强
-
-### 19.1 任务目标
-
-支持 Canvas 模式在需要时引入搜索结果。
-
-### 19.2 任务清单
-
-- 新增 `web_search.py`
-- 消费 `web_search_enabled`
-- 将搜索结果写入 `web_search_results`
-
-### 19.3 首版策略
-
-首版只做:
-
-- 打开开关后,生成/重写前先搜索
-- 把结果摘要注入 prompt
-
-先不做复杂页面抓取多轮链路。
-
-### 19.4 验收标准
-
-- 开关打开时能引入搜索结果
-- 结果可回传前端展示
-
----
-
-## 20. 阶段十五:线程创建与 metadata 策略
-
-### 20.1 任务目标
-
-让 Canvas 线程在创建和恢复时保留必要上下文。
-
-### 20.2 任务清单
-
-- 约定前端创建 Canvas thread 时写入 metadata
-- 后端在 thread search / get 中保留这些 metadata
-
-### 20.3 推荐 metadata
-
-- `ui_mode: "canvas"`
-- `agent_id: "canvas_agent"`
-- `model_name`
-- `canvas_type` 可选
-
-### 20.4 验收标准
-
-- Canvas thread 与普通 thread 可区分
-- 恢复线程时前端可据 metadata 恢复 UI
-
----
-
-## 21. 阶段十六:Gateway 扩展接口评估
-
-### 21.1 任务目标
-
-判断是否需要新增 Canvas 专用 REST API。
-
-### 21.2 原则
-
-能复用现有线程状态与 LangGraph 流,就不要新增太多新接口。
-
-### 21.3 可以先不新增的接口
-
-- Canvas artifact 专用 CRUD
-- Canvas 快捷动作专用接口
-
-这些都可以先通过:
-
-- 线程 state
-- runs.stream
-
-完成。
-
-### 21.4 可能需要新增的接口
-
-二期如果需要,可增加:
-
-- `GET /api/canvas/models`
-- `POST /api/canvas/export`
-- `POST /api/canvas/convert-to-file`
-
-### 21.5 验收标准
-
-- 首版尽量少增接口
-- 协议尽量复用现有线程与 run 模型
-
----
-
-## 22. 阶段十七:持久化与导出联动
-
-### 22.1 任务目标
-
-让 Canvas 文档能和现有文件 artifact 体系联动。
-
-### 22.2 任务清单
-
-- 规划导出接口
-- 规划 artifact 转文件逻辑
-- 规划从 Canvas 生成 outputs 文件
-
-### 22.3 推荐能力
-
-- 保存为 `.md`
-- 保存为代码文件
-- 将导出文件加入现有 `artifacts: list[str]`
-
-### 22.4 首版建议
-
-这部分不是最早阶段必须做的。
-
-建议等核心 Canvas 生成与重写稳定后再接。
-
-### 22.5 验收标准
-
-- 导出文件能出现在现有 artifact 文件体系中
-
----
-
-## 23. 测试清单
-
-### 23.1 单元测试建议
-
-优先覆盖:
-
-- artifact 协议校验
-- 版本追加逻辑
-- 标题/类型变更规则
-- 文本局部改写拼接逻辑
-- 代码局部改写拼接逻辑
-
-### 23.2 集成测试建议
-
-应覆盖:
-
-- 创建 Canvas thread
-- 首次生成 artifact
-- 整体重写
-- 局部改写
-- 模型切换
-- 本地模型调用
-
-### 23.3 回归测试重点
-
-重点检查现有功能未被破坏:
-
-- 普通 `lead_agent` 对话
-- 文件型 `artifacts`
-- uploads
-- memory
-- scheduled tasks
-
-### 23.4 验收标准
-
-- Canvas 新功能通过
-- 现有 DeerFlow 核心路径无回归
-
----
-
-## 24. 推荐开发顺序
-
-强烈建议按以下顺序推进:
-
-1. 线程状态扩展
-2. artifact 协议定稿
-3. `canvas_agent` 注册
-4. 最小图闭环
-5. 线程读取与流式返回
-6. 模型接口扩展
-7. 整体生成与整体改写
-8. 局部改写
-9. 快捷动作
-10. 本地模型增强
-11. Web 搜索增强
-12. 导出联动
-
-不要一开始就做:
-
-- 快捷动作全集
-- 局部 token 级流式 artifact 更新
-- 复杂导出体系
-
----
-
-## 25. 多人协作拆分建议
-
-### A 组:状态与线程
-
-- `thread_state.py`
-- 线程协议
-- Gateway 线程兼容
-
-### B 组:Canvas Agent 图
-
-- `canvas_agent`
-- prompt
-- route/generate/rewrite/followup
-
-### C 组:模型与配置
-
-- `/api/models`
-- 本地模型配置
-- provider 兼容
-
-### D 组:增强能力
-
-- 局部改写
-- 快捷动作
-- Web 搜索
-
----
-
-## 26. 首版最小可交付范围
-
-后端首版建议只交付这些:
-
-- 扩展线程状态支持 `artifact`
-- 新增 `canvas_agent`
-- 支持文本/代码 artifact 首次生成
-- 支持整体重写
-- 支持版本历史
-- 扩展 `/api/models` 返回 Canvas 能力元数据
-- 支持至少一种本地模型接入
-
-首版先不强制:
-
-- 局部高亮改写
-- 全量快捷动作
-- 搜索增强
-- 导出文件联动
-
----
-
-## 27. 总验收标准
-
-### 27.1 功能层
-
-- 无 artifact 时可生成新文档
-- 有 artifact 时可整体重写
-- 版本历史可保留
-- 前端刷新后可恢复
-
-### 27.2 协议层
-
-- `artifact` 字段稳定
-- 流式与最终线程状态一致
-- 模型能力字段可用于前端判断
-
-### 27.3 兼容层
-
-- 不破坏 `lead_agent`
-- 不破坏现有 `artifacts: list[str]`
-- 不破坏现有 Gateway 核心接口
-
----
-
-## 28. 下一步建议
-
-完成本文档后,建议继续补两份工程文档:
-
-### 28.1 Canvas 状态协议文档
-
-明确:
-
-- `artifact`
-- `highlighted_text`
-- `highlighted_code`
-- `quick_action_id`
-- `web_search_results`
-
-的最终 JSON 格式。
-
-### 28.2 Canvas Agent 节点接口文档
-
-逐节点定义:
-
-- 输入字段
-- 输出字段
-- prompt 责任
-- 错误处理
-
-这样进入实际编码时会更顺畅。
diff --git a/frontend-web/docs/openCanvas-frontend-checklist.md b/frontend-web/docs/openCanvas-frontend-checklist.md
deleted file mode 100644
index 26a47ae8..00000000
--- a/frontend-web/docs/openCanvas-frontend-checklist.md
+++ /dev/null
@@ -1,1373 +0,0 @@
-# Open Canvas 前端实施清单
-
-## 1. 文档目标
-
-本文档是 [openCanvas.md](./openCanvas.md) 的前端落地版清单。
-
-目标是把 Canvas 模式拆成可以直接开发的前端任务,覆盖:
-
-- 路由接入
-- 目录结构
-- 页面布局
-- 状态管理
-- 组件拆分
-- 接口适配
-- 流式更新
-- 版本管理
-- 局部改写
-- 模型选择
-- 验收标准
-
-本文档默认后端会配合新增结构化 `artifact` 字段与 `canvas_agent`。
-
----
-
-## 2. 实施原则
-
-### 2.1 总原则
-
-- 不破坏现有 `workspace` 聊天页面
-- 尽量复用现有基础设施
-- 先做骨架,再接数据,再接智能行为
-- 每一步都能跑、能看、能验收
-
-### 2.2 可复用的现有能力
-
-当前项目前端已有以下基础设施,可尽量复用:
-
-- `react-router-dom` 路由体系
-- `WorkspaceLayout` 工作区布局
-- `@langchain/langgraph-sdk` 客户端
-- `useThreadStream` 的流式线程模式
-- 模型列表接口 `/api/models`
-- 现有认证头注入机制
-- `sonner` 提示
-- `react-resizable-panels` 双栏/拖拽布局
-- `CodeMirror` 代码编辑器
-
-### 2.3 新增能力的边界
-
-Canvas 模式前端是一个“新子系统”,但不建议做成完全独立的第二套应用。
-
-建议方式:
-
-- 复用现有壳层和 API 客户端
-- 新建 `src/open-canvas` 目录承载新功能
-- 将 Canvas 模式接到现有 Page 层路由里
-
----
-
-## 3. 目标目录结构
-
-建议新增如下目录:
-
-```text
-frontend-web/src/open-canvas/
- api/
- client.ts
- models.ts
- threads.ts
- runs.ts
- components/
- layout/
- CanvasShell.tsx
- CanvasHeader.tsx
- CanvasResizableLayout.tsx
- chat/
- CanvasChatPanel.tsx
- CanvasMessageList.tsx
- CanvasInputBox.tsx
- CanvasWelcome.tsx
- artifact/
- CanvasArtifactPanel.tsx
- CanvasArtifactRenderer.tsx
- CanvasArtifactHeader.tsx
- CanvasArtifactVersionNav.tsx
- CanvasEmptyState.tsx
- TextArtifactRenderer.tsx
- CodeArtifactRenderer.tsx
- selectors/
- CanvasModelSelector.tsx
- CanvasModeSelector.tsx
- quick-actions/
- QuickActionBar.tsx
- QuickActionButton.tsx
- web-search/
- CanvasWebSearchPanel.tsx
- contexts/
- CanvasThreadContext.tsx
- CanvasGraphContext.tsx
- CanvasUIContext.tsx
- hooks/
- useCanvasThread.ts
- useCanvasStream.ts
- useCanvasArtifact.ts
- useCanvasSelection.ts
- useCanvasQuickActions.ts
- pages/
- CanvasHomePage.tsx
- CanvasThreadPage.tsx
- routes/
- OpenCanvasRoutes.tsx
- types/
- artifact.ts
- thread.ts
- model.ts
- selection.ts
- quick-action.ts
- utils/
- artifact.ts
- selection.ts
- thread.ts
- stream.ts
- quick-actions.ts
- constants/
- index.ts
-```
-
----
-
-## 4. 开发阶段总览
-
-前端建议分为 10 个实施阶段:
-
-1. 路由与目录骨架
-2. Canvas 页面布局
-3. 类型与状态协议
-4. API 适配层
-5. 线程和流式状态接入
-6. Artifact 渲染与版本管理
-7. 快速开始与空状态
-8. 局部选区改写交互
-9. 快捷动作系统
-10. 模型选择与增强能力接入
-
-建议每个阶段完成后都做一次可视化验收。
-
----
-
-## 5. 阶段一:路由与目录骨架
-
-### 5.1 任务目标
-
-把 Canvas 模式先挂到系统里,确保页面可进入。
-
-### 5.2 任务清单
-
-- 新建 `src/open-canvas/` 目录
-- 新建 `src/open-canvas/routes/OpenCanvasRoutes.tsx`
-- 新建 `src/open-canvas/pages/CanvasHomePage.tsx`
-- 新建 `src/open-canvas/pages/CanvasThreadPage.tsx`
-- 修改 [PageRoutes.tsx](../src/pages/PageRoutes.tsx)
-
-### 5.3 路由建议
-
-建议新增:
-
-- `/page/canvas`
-- `/page/canvas/:thread_id`
-
-建议在 `PageLayout` 内部新增:
-
-- `path="canvas/*"`
-
-子路由建议:
-
-- `index -> CanvasHomePage`
-- `:thread_id -> CanvasThreadPage`
-- `new -> 跳转到 CanvasHomePage 或创建临时线程态`
-
-### 5.4 页面行为要求
-
-`CanvasHomePage`:
-
-- 展示 Canvas 欢迎页
-- 展示快速开始入口
-- 可进入新建文本或代码模式
-
-`CanvasThreadPage`:
-
-- 只负责线程级页面装配
-- 挂载所有 Canvas 上下文提供器
-- 渲染主布局组件
-
-### 5.5 验收标准
-
-- 访问 `/page/canvas` 正常
-- 访问 `/page/canvas/xxx` 正常
-- 不影响 `/page/workspace/*`
-- 页面为独立功能入口
-
----
-
-## 6. 阶段二:Canvas 页面布局
-
-### 6.1 任务目标
-
-先搭出稳定的“左聊天右画布”骨架。
-
-### 6.2 任务清单
-
-- 新建 `layout/CanvasShell.tsx`
-- 新建 `layout/CanvasHeader.tsx`
-- 新建 `layout/CanvasResizableLayout.tsx`
-- 新建 `chat/CanvasChatPanel.tsx`
-- 新建 `artifact/CanvasArtifactPanel.tsx`
-- 新建 `artifact/CanvasEmptyState.tsx`
-
-### 6.3 布局要求
-
-推荐布局:
-
-- 顶部:Canvas 标题栏
-- 左侧:聊天面板
-- 右侧:artifact 面板
-- 可选右抽屉:Web 搜索结果
-
-### 6.4 建议复用
-
-可以优先复用:
-
-- `react-resizable-panels`
-- 现有按钮、分隔线、Tabs、Tooltip 等 UI 组件
-
-### 6.5 初始状态要求
-
-无 thread 或 thread 中无 artifact 时:
-
-- 右侧显示空状态
-- 左侧显示欢迎语和快速开始
-
-### 6.6 验收标准
-
-- 双栏布局稳定
-- 可拖拽调整宽度
-- 空状态显示正常
-- 适配桌面优先,移动端先做降级布局
-
----
-
-## 7. 阶段三:类型与状态协议
-
-### 7.1 任务目标
-
-先把前端需要消费和管理的数据类型定下来。
-
-### 7.2 任务清单
-
-- 新建 `types/artifact.ts`
-- 新建 `types/thread.ts`
-- 新建 `types/model.ts`
-- 新建 `types/selection.ts`
-- 新建 `types/quick-action.ts`
-
-### 7.3 必要类型
-
-#### artifact.ts
-
-- `CanvasArtifact`
-- `CanvasArtifactText`
-- `CanvasArtifactCode`
-- `CanvasArtifactContent`
-
-#### selection.ts
-
-- `CanvasTextSelection`
-- `CanvasCodeSelection`
-- `CanvasSelectionState`
-
-#### thread.ts
-
-- `CanvasThreadValues`
-- `CanvasThreadMetadata`
-- `CanvasThread`
-
-### 7.4 建议结构
-
-```ts
-export type CanvasArtifact = {
- currentIndex: number;
- contents: CanvasArtifactContent[];
-};
-
-export type CanvasArtifactText = {
- index: number;
- type: "text";
- title: string;
- fullMarkdown: string;
-};
-
-export type CanvasArtifactCode = {
- index: number;
- type: "code";
- title: string;
- language: string;
- code: string;
-};
-```
-
-### 7.5 线程值类型建议
-
-```ts
-export type CanvasThreadValues = {
- title?: string;
- messages?: Message[];
- artifact?: CanvasArtifact;
- web_search_results?: unknown[];
- todos?: unknown[];
-};
-```
-
-### 7.6 验收标准
-
-- 所有 Canvas 组件只依赖 `open-canvas/types`
-- 不把类型散落到页面或组件内部
-- 前后端协议字段统一命名
-
----
-
-## 8. 阶段四:前端 Context 设计
-
-### 8.1 任务目标
-
-把 Canvas 的状态拆成职责清晰的三层上下文。
-
-### 8.2 任务清单
-
-- 新建 `CanvasThreadContext.tsx`
-- 新建 `CanvasGraphContext.tsx`
-- 新建 `CanvasUIContext.tsx`
-
-### 8.3 职责划分
-
-#### CanvasThreadContext
-
-负责:
-
-- 当前 thread id
-- thread 基础信息
-- thread 创建/切换
-- 模型名称
-- 模型配置
-
-#### CanvasGraphContext
-
-负责:
-
-- messages
-- artifact
-- runId
-- streaming 状态
-- error 状态
-- 发送消息
-- 停止生成
-- 切换版本
-- 更新 artifact 内容
-
-#### CanvasUIContext
-
-负责:
-
-- 面板折叠状态
-- 是否正在编辑
-- 当前选区
-- Web 搜索面板开关
-- 快捷动作面板开关
-
-### 8.4 验收标准
-
-- 页面组件不直接持有大量跨层 useState
-- 聊天状态和 UI 状态分离
-- 线程切换时可安全重置状态
-
----
-
-## 9. 阶段五:API 适配层
-
-### 9.1 任务目标
-
-为 Canvas 模式建立专用的前端 API 包装层。
-
-### 9.2 任务清单
-
-- 新建 `api/client.ts`
-- 新建 `api/models.ts`
-- 新建 `api/threads.ts`
-- 新建 `api/runs.ts`
-
-### 9.3 设计要求
-
-不要让页面组件直接到处调用现有 `getAPIClient()` 或裸 `fetch`。
-
-建议统一在 `src/open-canvas/api/` 中封装。
-
-### 9.4 需要封装的能力
-
-#### models.ts
-
-- `loadCanvasModels()`
-- 对现有 `/api/models` 结果做标准化
-
-#### threads.ts
-
-- `createCanvasThread()`
-- `getCanvasThread(threadId)`
-- `updateCanvasThreadMetadata(threadId, metadata)`
-- `updateCanvasThreadState(threadId, values)`
-
-#### runs.ts
-
-- `streamCanvasRun(...)`
-- `stopCanvasRun(...)`
-
-### 9.5 与现有 API 的关系
-
-底层可以复用:
-
-- `src/core/api/api-client.ts`
-- `src/core/api/fetch-client.ts`
-
-但上层调用应走 Canvas 专用 API。
-
-### 9.6 验收标准
-
-- 页面层不直接拼接 URL
-- Canvas API 调用集中管理
-- 后续改协议时只动一层
-
----
-
-## 10. 阶段六:线程与流式状态接入
-
-### 10.1 任务目标
-
-让 Canvas 页面能像现有 ChatPage 一样收发消息并接流。
-
-### 10.2 任务清单
-
-- 新建 `hooks/useCanvasThread.ts`
-- 新建 `hooks/useCanvasStream.ts`
-- 新建 `utils/stream.ts`
-
-### 10.3 设计建议
-
-可以参考现有:
-
-- [ChatPage.tsx](../src/pages/ChatPage.tsx)
-- [threads/hooks.ts](../src/core/threads/hooks.ts)
-
-但不要直接把现有 `ChatPage` 复制一份。
-
-### 10.4 `useCanvasThread` 负责
-
-- 当前 thread id 管理
-- 新建/切换 thread
-- 从 URL 参数恢复 thread
-
-### 10.5 `useCanvasStream` 负责
-
-- 发送消息
-- 停止生成
-- 处理流式事件
-- 更新 `messages`
-- 更新 `artifact`
-- 更新 `runId`
-
-### 10.6 流式更新要求
-
-首版可以假设后端在 `values` 或 `updates` 中返回:
-
-- `messages`
-- `artifact`
-
-前端需要:
-
-- 新消息追加
-- artifact 覆盖或增量更新
-- 流结束后保持最终状态
-
-### 10.7 验收标准
-
-- 能发消息
-- 能收到消息
-- 能实时更新 artifact
-- 停止按钮可用
-
----
-
-## 11. 阶段七:Artifact 渲染器
-
-### 11.1 任务目标
-
-实现文本/代码两类 artifact 的基础渲染与切换。
-
-### 11.2 任务清单
-
-- 新建 `artifact/CanvasArtifactRenderer.tsx`
-- 新建 `artifact/CanvasArtifactHeader.tsx`
-- 新建 `artifact/CanvasArtifactVersionNav.tsx`
-- 新建 `artifact/TextArtifactRenderer.tsx`
-- 新建 `artifact/CodeArtifactRenderer.tsx`
-
-### 11.3 组件职责
-
-#### CanvasArtifactRenderer
-
-- 判断当前 artifact 是否存在
-- 判断当前内容是文本还是代码
-- 分发到具体渲染器
-
-#### CanvasArtifactHeader
-
-- 显示标题
-- 显示保存状态
-- 显示版本导航
-
-#### CanvasArtifactVersionNav
-
-- 上一个版本
-- 下一个版本
-- 当前版本号
-
-#### TextArtifactRenderer
-
-- 渲染 markdown 内容
-- 支持编辑模式
-- 支持文本选区
-
-#### CodeArtifactRenderer
-
-- 使用 CodeMirror
-- 支持语言模式切换
-- 支持代码选区
-
-### 11.4 首版策略
-
-文本首版建议:
-
-- 编辑态用 textarea 或现有 markdown 编辑器
-- 展示态用现有 markdown renderer
-
-代码首版建议:
-
-- 直接复用现有 CodeMirror 基础能力
-
-### 11.5 验收标准
-
-- 文本 artifact 可看可编辑
-- 代码 artifact 可看可编辑
-- 版本切换正确
-- 标题与当前版本一致
-
----
-
-## 12. 阶段八:快速开始与空状态
-
-### 12.1 任务目标
-
-让用户在没有任何 artifact 时也能顺畅开始。
-
-### 12.2 任务清单
-
-- 新建 `chat/CanvasWelcome.tsx`
-- 在 `CanvasHomePage` 中增加快速开始面板
-- 在 `CanvasThreadPage` 空状态时提供快速开始
-
-### 12.3 快速开始项建议
-
-- 空白文本
-- 空白代码
-- React 组件模板
-- HTML 页面模板
-- Python 脚本模板
-
-### 12.4 行为要求
-
-点击后:
-
-- 创建本地前端 artifact 草稿
-- 必要时自动创建 thread
-- 自动进入编辑态
-
-### 12.5 验收标准
-
-- 不发消息也能先建立 artifact
-- 新建文本/代码入口可用
-- 初始编辑体验完整
-
----
-
-## 13. 阶段九:Artifact 版本管理
-
-### 13.1 任务目标
-
-让每次 AI 生成或修改都成为一个新版本。
-
-### 13.2 任务清单
-
-- 新建 `hooks/useCanvasArtifact.ts`
-- 新建 `utils/artifact.ts`
-
-### 13.3 必要能力
-
-- 获取当前版本内容
-- 切换版本
-- 更新当前版本内容
-- 追加新版本
-- 判断 artifact 是否为空
-
-### 13.4 推荐工具函数
-
-- `getCurrentArtifactContent(artifact)`
-- `setCurrentArtifactIndex(artifact, index)`
-- `appendArtifactVersion(artifact, content)`
-- `replaceCurrentArtifactContent(artifact, content)`
-- `isTextArtifact(content)`
-- `isCodeArtifact(content)`
-
-### 13.5 注意事项
-
-- 前端切版本不能直接修改原对象引用
-- 每次更新要创建新对象,避免渲染异常
-
-### 13.6 验收标准
-
-- AI 更新后版本数增加
-- 手动切换历史版本稳定
-- 当前版本高亮正确
-
----
-
-## 14. 阶段十:局部选区交互
-
-### 14.1 任务目标
-
-实现局部改写所需的前端选区采集与展示。
-
-### 14.2 任务清单
-
-- 新建 `hooks/useCanvasSelection.ts`
-- 新建 `utils/selection.ts`
-
-### 14.3 文本选区需求
-
-- 获取选中起止位置
-- 获取选中文本
-- 在 UI 上显示“已选中某段内容”
-- 支持清除选区
-
-### 14.4 代码选区需求
-
-- 获取 CodeMirror 选区
-- 保存选中代码内容
-- 保存 start/end 或行列信息
-
-### 14.5 交互建议
-
-可以在选区出现后展示浮层或工具条:
-
-- 改写
-- 精简
-- 扩写
-- 解释
-
-首版先只保留:
-
-- “对所选内容进行修改” 的统一入口
-
-### 14.6 验收标准
-
-- 文本选区可读
-- 代码选区可读
-- 选区状态可清空
-- 切换版本或 thread 时选区自动重置
-
----
-
-## 15. 阶段十一:聊天面板适配
-
-### 15.1 任务目标
-
-实现 Canvas 模式专用聊天面板,不直接塞回现有 ChatBox。
-
-### 15.2 任务清单
-
-- 新建 `chat/CanvasMessageList.tsx`
-- 新建 `chat/CanvasInputBox.tsx`
-- 新建 `chat/CanvasChatPanel.tsx`
-
-### 15.3 设计建议
-
-可以参考现有 ChatPage 的结构,但建议把 Canvas 模式聊天区做成更轻量版本。
-
-### 15.4 必要功能
-
-- 展示消息列表
-- 展示生成中状态
-- 提交输入
-- 停止生成
-- 显示模型选择器
-- 显示 Web 搜索开关
-
-### 15.5 可以复用
-
-- 现有消息渲染组件中的部分能力
-- 现有 InputBox 的一部分交互逻辑
-
-### 15.6 不建议直接复用
-
-- 当前工作区的文件 artifact 面板耦合
-- 当前以文件产物为中心的 ChatBox 结构
-
-### 15.7 验收标准
-
-- Canvas 聊天区独立可用
-- 能与 artifact 面板联动
-- 布局不受现有文件 artifact 影响
-
----
-
-## 16. 阶段十二:模型选择器
-
-### 16.1 任务目标
-
-让 Canvas 模式可以使用 DeerFlow 的动态模型列表。
-
-### 16.2 任务清单
-
-- 新建 `selectors/CanvasModelSelector.tsx`
-- 在 `api/models.ts` 中做标准化
-
-### 16.3 UI 要求
-
-- 显示模型名
-- 显示是否本地模型
-- 显示是否支持视觉/推理/Canvas
-
-### 16.4 状态要求
-
-- 当前 thread 记录 `model_name`
-- 必要时记录 `model_config`
-- 切换 thread 时能恢复模型
-
-### 16.5 首版能力
-
-首版可以只做:
-
-- 下拉选择模型
-- 将模型信息写入 thread metadata 或 run configurable
-
-### 16.6 验收标准
-
-- 模型列表正确展示
-- 切换模型后发消息使用新模型
-- 本地模型与云模型能在 UI 中区分
-
----
-
-## 17. 阶段十三:快捷动作栏
-
-### 17.1 任务目标
-
-实现 artifact 上方或下方的快捷动作入口。
-
-### 17.2 任务清单
-
-- 新建 `quick-actions/QuickActionBar.tsx`
-- 新建 `quick-actions/QuickActionButton.tsx`
-- 新建 `hooks/useCanvasQuickActions.ts`
-- 新建 `utils/quick-actions.ts`
-
-### 17.3 首版动作建议
-
-文本类:
-
-- 扩写
-- 精简
-- 改正式
-- 改口语
-
-代码类:
-
-- 加注释
-- 加日志
-- 修 bug
-
-### 17.4 行为要求
-
-点击动作后:
-
-- 写入 `quick_action_id`
-- 触发一次 Canvas run
-- 生成新的 artifact 版本
-
-### 17.5 验收标准
-
-- 动作栏根据 artifact 类型动态变化
-- 动作点击后有效果
-- 新版本正确生成
-
----
-
-## 18. 阶段十四:Web 搜索面板
-
-### 18.1 任务目标
-
-为后续搜索增强预留 UI 和交互位置。
-
-### 18.2 任务清单
-
-- 新建 `web-search/CanvasWebSearchPanel.tsx`
-- 在 `CanvasUIContext` 中增加开关状态
-
-### 18.3 首版要求
-
-- 只做抽屉面板
-- 可展示结构化搜索结果列表
-- 可开关显示
-
-### 18.4 二期能力
-
-- 支持点击来源预览
-- 支持从结果插入上下文
-
-### 18.5 验收标准
-
-- 面板开关正常
-- 搜索结果区域可承载数据
-
----
-
-## 19. 阶段十五:页面装配
-
-### 19.1 任务目标
-
-将所有子模块整合到页面中。
-
-### 19.2 任务清单
-
-`CanvasHomePage.tsx`:
-
-- 欢迎页
-- 快速开始
-- 最近 Canvas 线程入口
-
-`CanvasThreadPage.tsx`:
-
-- 解析 route params
-- 挂载 `CanvasThreadContext`
-- 挂载 `CanvasGraphContext`
-- 挂载 `CanvasUIContext`
-- 渲染 `CanvasShell`
-
-### 19.3 结构建议
-
-```tsx
-
-
-
-
-
-
-
-```
-
-### 19.4 验收标准
-
-- 页面层只负责装配,不承载复杂业务逻辑
-- 子组件依赖上下文而不是层层 props drilling
-
----
-
-## 20. 推荐逐文件实施顺序
-
-建议按这个顺序建文件:
-
-1. `routes/OpenCanvasRoutes.tsx`
-2. `pages/CanvasHomePage.tsx`
-3. `pages/CanvasThreadPage.tsx`
-4. `types/*`
-5. `contexts/CanvasThreadContext.tsx`
-6. `contexts/CanvasUIContext.tsx`
-7. `contexts/CanvasGraphContext.tsx`
-8. `layout/CanvasShell.tsx`
-9. `layout/CanvasResizableLayout.tsx`
-10. `artifact/CanvasEmptyState.tsx`
-11. `artifact/CanvasArtifactRenderer.tsx`
-12. `artifact/TextArtifactRenderer.tsx`
-13. `artifact/CodeArtifactRenderer.tsx`
-14. `chat/CanvasChatPanel.tsx`
-15. `chat/CanvasInputBox.tsx`
-16. `api/*`
-17. `hooks/useCanvasStream.ts`
-18. `hooks/useCanvasSelection.ts`
-19. `quick-actions/*`
-20. `web-search/*`
-
----
-
-## 21. 开发任务拆分建议
-
-如果多人协作,建议这样拆:
-
-### A 组:页面与路由
-
-- 路由接入
-- 页面骨架
-- 主布局
-
-### B 组:artifact 视图
-
-- artifact 类型
-- 渲染器
-- 版本导航
-- 代码/文本编辑器
-
-### C 组:聊天与流式
-
-- thread 管理
-- stream 接入
-- 消息面板
-- 模型切换
-
-### D 组:增强交互
-
-- 选区
-- 快捷动作
-- 搜索面板
-
----
-
-## 22. 每阶段验收清单
-
-### 22.1 阶段一到三
-
-- 路由正常
-- 空页面正常
-- 类型编译通过
-- 目录结构清晰
-
-### 22.2 阶段四到六
-
-- thread 可创建/读取
-- 消息可发送
-- artifact 可展示
-- 流式结果可更新
-
-### 22.3 阶段七到九
-
-- 文本/代码 artifact 渲染正确
-- 版本导航正确
-- 快速开始可用
-- 模型选择可用
-
-### 22.4 阶段十到十四
-
-- 局部选区可采集
-- 快捷动作可触发
-- 搜索面板可显示
-- 各功能互不冲突
-
----
-
-## 23. 风险点与规避方式
-
-### 23.1 不要直接复制现有 ChatPage
-
-原因:
-
-- 现有页面与文件 artifact 耦合较重
-- Canvas 需要结构化 artifact 而不是文件列表
-
-### 23.2 不要把所有状态都放一个 Context
-
-原因:
-
-- 更新频率高
-- 容易导致全树重渲染
-
-### 23.3 不要先做复杂富文本
-
-原因:
-
-- 选区映射复杂
-- 首版容易卡在编辑器实现而不是业务链路
-
-建议先以:
-
-- `fullMarkdown`
-- `textarea/markdown renderer`
-
-作为最小闭环。
-
-### 23.4 不要前端硬编码模型表
-
-原因:
-
-- 后端才是真源
-- 后期本地模型和自定义 provider 会频繁变化
-
----
-
-## 24. 首版最小可交付范围
-
-前端首版建议只交付这些:
-
-- `canvas/*` 路由
-- Canvas 双栏布局
-- 文本/代码 artifact 渲染
-- 模型选择器
-- 流式聊天
-- artifact 版本切换
-- 快速开始
-
-首版先不强制包含:
-
-- 复杂文本局部高亮浮层
-- 复杂快捷动作配置中心
-- 高级 Web 搜索结果联动
-
----
-
-## 25. 下一步建议
-
-完成本文档后,建议继续产出两份更细的执行文件:
-
-### 25.1 前端文件创建顺序清单
-
-按天或按 PR 颗粒度,列出:
-
-- 每一批次新建哪些文件
-- 每一批次修改哪些已有文件
-
-### 25.2 前端组件接口文档
-
-列出关键组件 props:
-
-- `CanvasShell`
-- `CanvasChatPanel`
-- `CanvasArtifactRenderer`
-- `CanvasModelSelector`
-
-这样正式开工时会更顺畅。
----
-
-## 26. 参考源码映射
-
-前端实施清单建议补充对 `langchain-ai/open-canvas` 源码的“映射式参考”,但不建议把源码大段粘贴进本文档。
-
-推荐做法是:
-
-- 记录参考文件路径
-- 说明它解决了什么问题
-- 说明在 DeerFlow 前端里应该落到哪个模块
-- 只摘能力点,不摘整段实现
-
-这样做的原因:
-
-- 文档更清晰
-- 不会把 Next.js 运行时细节误当成可直接迁移代码
-- 后续维护更轻
-
-### 26.1 推荐参考文件总览
-
-以下文件最值得作为前端实现参考:
-
-- `apps/web/src/contexts/GraphContext.tsx`
-- `apps/web/src/components/canvas/canvas.tsx`
-- `apps/web/src/components/artifacts/ArtifactRenderer.tsx`
-- `apps/web/src/components/artifacts/TextRenderer.tsx`
-- `apps/web/src/components/artifacts/CodeRenderer.tsx`
-- `apps/web/src/contexts/ThreadProvider.tsx`
-- `apps/web/src/contexts/AssistantContext.tsx`
-- `apps/web/src/hooks/useRuns.tsx`
-- `apps/web/src/types.ts`
-- `packages/shared/src/types.ts`
-- `packages/shared/src/models.ts`
-- `packages/shared/src/utils/artifacts.ts`
-
-### 26.2 逐文件映射建议
-
-#### `apps/web/src/contexts/GraphContext.tsx`
-
-参考价值:
-
-- 这是 open-canvas 前端最核心的状态管理文件之一
-- 管理消息、artifact、流式状态、保存状态、版本切换、局部更新
-- 处理流中 artifact 更新和 thread 切换
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/contexts/CanvasGraphContext.tsx`
-- `src/open-canvas/hooks/useCanvasStream.ts`
-- `src/open-canvas/utils/stream.ts`
-- `src/open-canvas/utils/artifact.ts`
-
-重点借鉴内容:
-
-- artifact 如何作为线程主状态存在
-- 流式过程中如何同步 messages 与 artifact
-- 切换 thread 时如何恢复状态
-- 如何维护“当前版本”和“已保存状态”
-
-不建议直接照搬的部分:
-
-- 与 Next.js、`nuqs`、open-canvas 内部 hook 耦合的代码
-- 与其后端返回格式强绑定的处理逻辑
-
-#### `apps/web/src/components/canvas/canvas.tsx`
-
-参考价值:
-
-- Open Canvas 的主页面装配组件
-- 管理左聊天右画布布局
-- 定义快速开始逻辑
-- 管理 chat collapsed、artifact 初始创建
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/components/layout/CanvasShell.tsx`
-- `src/open-canvas/components/layout/CanvasResizableLayout.tsx`
-- `src/open-canvas/pages/CanvasThreadPage.tsx`
-
-重点借鉴内容:
-
-- 快速开始时如何创建文本或代码 artifact
-- 左右双栏如何组织
-- 页面级状态和上下文边界怎么划分
-
-不建议直接照搬的部分:
-
-- Next 路由参数逻辑
-- 与 open-canvas ThreadProvider 强耦合的状态恢复代码
-
-#### `apps/web/src/components/artifacts/ArtifactRenderer.tsx`
-
-参考价值:
-
-- 负责 artifact 总渲染分发
-- 根据当前版本内容选择文本或代码渲染器
-- 负责版本导航和选区交互挂钩
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/components/artifact/CanvasArtifactRenderer.tsx`
-- `src/open-canvas/components/artifact/CanvasArtifactHeader.tsx`
-- `src/open-canvas/components/artifact/CanvasArtifactVersionNav.tsx`
-
-重点借鉴内容:
-
-- 当前 artifact 的统一入口组件结构
-- 版本导航和正文渲染如何组合
-- 文本/代码视图如何统一被包装
-
-#### `apps/web/src/components/artifacts/TextRenderer.tsx`
-
-参考价值:
-
-- 负责文本 artifact 的渲染与编辑
-- 处理 markdown 内容、更新、局部交互
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/components/artifact/TextArtifactRenderer.tsx`
-- `src/open-canvas/hooks/useCanvasSelection.ts`
-
-重点借鉴内容:
-
-- 文本 artifact 的编辑态与展示态切换
-- 当前版本内容如何回写到状态
-- 局部选区的挂接点
-
-实现上的取舍建议:
-
-- DeerFlow 首版不要追求和 open-canvas 一样复杂的富文本块级编辑
-- 建议先用 `fullMarkdown` + markdown renderer + 简单编辑态完成闭环
-
-#### `apps/web/src/components/artifacts/CodeRenderer.tsx`
-
-参考价值:
-
-- 负责代码 artifact 的渲染与编辑
-- 与 CodeMirror 集成
-- 管理代码内容更新
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/components/artifact/CodeArtifactRenderer.tsx`
-
-重点借鉴内容:
-
-- 代码 artifact 如何绑定语言
-- 代码编辑结果如何同步回 artifact 状态
-- 局部代码选区的接入位置
-
-#### `apps/web/src/contexts/ThreadProvider.tsx`
-
-参考价值:
-
-- 负责 thread 与模型相关状态
-- 管理 thread 切换和 thread 级配置
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/contexts/CanvasThreadContext.tsx`
-- `src/open-canvas/hooks/useCanvasThread.ts`
-
-重点借鉴内容:
-
-- 当前 thread id 的管理方式
-- thread metadata 与 model state 的关系
-- 页面恢复 thread 时的上下文同步
-
-#### `apps/web/src/contexts/AssistantContext.tsx`
-
-参考价值:
-
-- 管理 assistant 选择
-- 管理与 assistant 相关的 thread 行为
-
-DeerFlow 对应落点:
-
-- DeerFlow 首版可以弱化这部分
-- 如果首版只有 `canvas_agent` 一个固定 assistant,可只保留最小 assistant 绑定能力
-
-重点借鉴内容:
-
-- thread 与 assistant 的耦合方式
-
-#### `apps/web/src/hooks/useRuns.tsx`
-
-参考价值:
-
-- 管理 run 相关行为
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/api/runs.ts`
-- `src/open-canvas/hooks/useCanvasStream.ts`
-
-重点借鉴内容:
-
-- run 行为与页面状态的解耦方式
-
-#### `apps/web/src/types.ts`
-
-参考价值:
-
-- 保存了一部分 artifact 和消息类型定义
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/types/artifact.ts`
-- `src/open-canvas/types/thread.ts`
-
-重点借鉴内容:
-
-- artifact 的前端表达方式
-
-注意:
-
-- 更建议以 `packages/shared/src/types.ts` 为主参考,而不是这个文件
-- 因为 `src/types.ts` 里同时包含了部分历史兼容结构
-
-#### `packages/shared/src/types.ts`
-
-参考价值:
-
-- 这是 open-canvas 中最重要的共享类型来源之一
-- 定义了 `ArtifactV3`、代码/文本内容、选择区和其他共享结构
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/types/artifact.ts`
-- `src/open-canvas/types/selection.ts`
-- 可作为前后端协议设计的主要参考
-
-重点借鉴内容:
-
-- `ArtifactV3` 结构
-- 文本/代码 artifact 类型边界
-- 与版本管理有关的字段组织方式
-
-建议:
-
-- DeerFlow 可以参考其字段设计
-- 但不要机械复制所有字段,保留适合当前项目的最小集
-
-#### `packages/shared/src/models.ts`
-
-参考价值:
-
-- 展示了 open-canvas 如何组织前端模型列表和模型能力
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/types/model.ts`
-- `src/open-canvas/api/models.ts`
-- `src/open-canvas/components/selectors/CanvasModelSelector.tsx`
-
-重点借鉴内容:
-
-- 模型展示结构
-- provider 概念
-- 各模型能力标签的前端呈现思路
-
-注意:
-
-- 不应作为 DeerFlow 的模型配置真源
-- DeerFlow 必须以自己后端 `/api/models` 为真源
-
-#### `packages/shared/src/utils/artifacts.ts`
-
-参考价值:
-
-- 里面有 artifact 类型判断和内容获取工具
-
-DeerFlow 对应落点:
-
-- `src/open-canvas/utils/artifact.ts`
-
-重点借鉴内容:
-
-- 获取当前版本内容
-- 判断当前 artifact 是文本还是代码
-- 为渲染器和快捷动作提供统一入口
-
-### 26.3 可以直接参考但不要直接迁移的部分
-
-以下内容可以借鉴思路,但不建议直接搬代码:
-
-- Next.js 路由和页面结构
-- `src/app/api/*` 目录中的 API Route
-- Supabase SSR 和服务端 session 相关代码
-- `next/navigation` 相关逻辑
-- `middleware.ts`
-
-原因:
-
-- DeerFlow 前端是 `React 19 + Vite + react-router-dom`
-- 这些代码依赖 Next.js 运行时
-
-### 26.4 建议的使用方式
-
-开发时推荐这样使用参考源码:
-
-1. 先按本文档实现 DeerFlow 自己的模块边界
-2. 遇到某个具体功能点时,再去查对应 open-canvas 文件
-3. 提炼实现思路,重写到 DeerFlow 的目录结构中
-4. 不要复制包含 Next.js 运行时依赖的代码
-
-### 26.5 推荐补充动作
-
-如果后面要正式进入实现阶段,建议再单独新增一份源码映射文档,例如:
-
-- `frontend-web/docs/openCanvas-source-map.md`
-
-里面可以更细地记录:
-
-- open-canvas 文件路径
-- DeerFlow 对应目标文件
-- 迁移策略
-- 是否可直接借鉴
-- 是否需要重写
diff --git a/frontend-web/docs/openCanvas-progress.md b/frontend-web/docs/openCanvas-progress.md
deleted file mode 100644
index 5e74c279..00000000
--- a/frontend-web/docs/openCanvas-progress.md
+++ /dev/null
@@ -1,312 +0,0 @@
-# Open Canvas 集成进度文档
-
-> 最后更新:2026-05-16
-> 当前分支:dhk-1
-
----
-
-## 一、整体进度概览
-
-### Canvas 系统(双栏写作/编码工作区)
-
-| 阶段 | 名称 | 状态 |
-|------|------|------|
-| 阶段一 | 前端路由与目录骨架 | ✅ 已完成 |
-| 阶段二 | Canvas 双栏布局 | ✅ 已完成 |
-| 阶段三 | 前端 artifact 类型定义 | ✅ 已完成 |
-| 阶段四 | 后端 ThreadState 扩展 | ✅ 已完成 |
-| 阶段五 | canvas_agent 注册 + 最小节点闭环 | ✅ 已完成 |
-| 阶段六 | 前后端流式同步 | ✅ 已完成 |
-| 阶段七 | 版本历史导航 | ✅ 已完成 |
-| 阶段八 | 局部选区改写(Writing Editor 级) | ✅ 已完成 |
-| 阶段九 | 快捷动作系统 | ✅ 已完成 |
-| 阶段十 | 本地模型选择支持 | ✅ 已完成 |
-
-### AI Writing Editor(文本 artifact 编辑器内置 AI 改写)
-
-| 步骤 | 名称 | 状态 |
-|------|------|------|
-| Step 1 | Tiptap 编辑器核心 | ✅ 已完成 |
-| Step 2 | 写作工具栏 + 模型选择器 | ✅ 已完成 |
-| Step 3a | 气泡菜单(选区 AI 改写入口) | ✅ 已完成 |
-| Step 3b | 段落悬停按钮(整段 AI 改写入口) | ✅ 已完成 |
-| Step 4 | AI 改写菜单 + SSE 流式接口 | ✅ 已完成 |
-| Step 5 | 候选结果预览 + 替换/插入 | ✅ 已完成 |
-| Step 6 | 后端 `POST /api/writing/rewrite`(SSE,思考分离) | ✅ 已完成 |
-| Step 7 | 视觉精修(字体、间距、排版) | ✅ 已完成 |
-
-**当前状态:全部功能已完成 🎉**
-
----
-
-## 二、已完成工作详情
-
-### 2.1 前端核心文件结构
-
-```
-frontend-web/src/open-canvas/
-├── api/
-│ ├── client.ts # LangGraph SDK 客户端实例
-│ ├── models.ts # 模型列表 API
-│ ├── rewrite.ts # AI 改写 SSE 流(streamRewrite,parseSse)
-│ ├── runs.ts # streamCanvasRun(老的直接 API,已停用)
-│ └── threads.ts # createCanvasThread、getCanvasThread
-├── components/
-│ ├── artifact/
-│ │ ├── CanvasArtifactHeader.tsx # 右栏顶部:标题、语言、版本导航
-│ │ ├── CanvasArtifactPanel.tsx # 右栏:流式预览 / 编辑器 / 空状态
-│ │ ├── CanvasArtifactVersionNav.tsx # prev/next 版本切换
-│ │ ├── CanvasEmptyState.tsx # 5 种快速开始模板
-│ │ ├── CanvasQuickActionBar.tsx # 快捷动作按钮栏
-│ │ ├── CodeArtifactRenderer.tsx # CodeMirror 代码编辑器
-│ │ ├── TextArtifactRenderer.tsx # Tiptap 文本编辑器 + AI 改写流程
-│ │ ├── WritingAiMenu.tsx # AI 改写动作菜单(polish/expand/shorten/tone)
-│ │ ├── WritingBubbleMenu.tsx # 选区气泡菜单(portal 实现)
-│ │ ├── WritingParagraphMenu.tsx # 段落悬停 AI 改写按钮
-│ │ ├── WritingRewritePreview.tsx # 改写结果预览面板(含思考过程展开)
-│ │ └── WritingToolbar.tsx # 格式工具栏(含模型选择器)
-│ ├── chat/
-│ │ └── CanvasChatPanel.tsx # 左栏:消息列表 + 输入框
-│ ├── layout/
-│ │ ├── CanvasHeader.tsx # 顶部栏:返回、折叠、流式指示
-│ │ ├── CanvasResizableLayout.tsx # 左右可拖拽双栏(含 CanvasStreamProvider)
-│ │ └── CanvasShell.tsx # 顶层壳:加载已有线程状态
-│ └── selectors/
-│ └── CanvasModelSelector.tsx # 模型下拉选择器
-├── contexts/
-│ ├── CanvasGraphContext.tsx # artifact、streamingArtifact、isStreaming
-│ ├── CanvasStreamContext.tsx # 共享 useCanvasThreadStream 实例(sendMessage)
-│ ├── CanvasThreadContext.tsx # threadId、modelName
-│ └── CanvasUIContext.tsx # isChatCollapsed、writingSelection
-├── editor/
-│ ├── TiptapEditor.tsx # EditorContent 包装器
-│ └── TiptapExtensions.ts # StarterKit + Underline + Placeholder + Markdown
-├── hooks/
-│ ├── useCanvasArtifact.ts # artifact 初始化工具
-│ ├── useCanvasQuickActions.ts # 快捷动作触发(走 CanvasStreamContext)
-│ ├── useCanvasSelection.ts # 文字/代码选区状态
-│ ├── useCanvasStream.ts # 旧直接 API 流(已停用,保留备用)
-│ ├── useCanvasThread.ts # 线程加载工具
-│ ├── useCanvasThreadStream.ts # LangGraph SDK useStream hook(主流)
-│ ├── useWritingEditor.ts # Tiptap editor 实例管理
-│ └── useWritingRewrite.ts # AI 改写状态机(ThinkTagParser、AbortController)
-├── pages/
-│ ├── CanvasHomePage.tsx # 欢迎页 + 快速开始
-│ └── CanvasThreadPage.tsx # Provider 装配层
-├── routes/
-│ └── OpenCanvasRoutes.tsx # /page/canvas/* 子路由
-├── types/
-│ ├── artifact.ts # CanvasArtifact、CanvasArtifactText、CanvasArtifactCode
-│ ├── model.ts # CanvasModel
-│ ├── quick-action.ts # QuickAction、QuickActionId
-│ ├── selection.ts # CanvasTextSelection、CanvasCodeSelection
-│ └── thread.ts # CanvasMessage、CanvasThreadValues
-└── utils/
- ├── apply-rewrite.ts # applyReplace、applyInsert(Tiptap 事务)
- ├── artifact.ts # getCurrentArtifactContent、setCurrentArtifactIndex 等
- ├── quick-actions.ts # buildQuickActionMessage(含 artifact 内容注入)
- ├── selection.ts # buildMessageWithSelection
- ├── stream.ts # 流式工具
- └── thread.ts # 线程工具
-```
-
-### 2.2 后端核心文件结构
-
-```
-offline-backend-20260512/backend/
-├── packages/harness/deerflow/agents/
-│ ├── canvas_agent/
-│ │ ├── __init__.py
-│ │ ├── agent.py # make_canvas_agent():LangGraph graph 工厂
-│ │ ├── prompt.py # build_canvas_system_prompt():含 selection_rewrite + quick_actions
-│ │ └── tools.py # update_artifact_tool:创建/追加 artifact 版本
-│ └── thread_state.py # ThreadState(含 artifact 字段、CanvasArtifact TypedDict)
-└── app/gateway/
- ├── app.py # 注册 writing router
- └── routers/
- └── writing.py # POST /api/writing/rewrite(SSE 流式)
-```
-
-**langgraph.json** 注册了两个 graph:
-- `lead_agent` → 主聊天 Agent
-- `canvas_agent` → Canvas 写作 Agent
-
-### 2.3 关键 Context 层级
-
-```
-CanvasThreadPage
- ├── CanvasThreadProvider # threadId、modelName
- │ ├── CanvasGraphProvider # artifact、streamingArtifact、isStreaming
- │ │ └── CanvasUIProvider # isChatCollapsed、writingSelection
- │ │ └── CanvasShell
- │ │ └── CanvasResizableLayout
- │ │ └── CanvasStreamProvider ← 持有 useCanvasThreadStream 单例
- │ │ ├── CanvasChatPanel # 读 useCanvasStreamContext()
- │ │ └── CanvasArtifactPanel
- │ │ └── CanvasQuickActionBar # 读 useCanvasStreamContext()
-```
-
----
-
-## 三、关键架构决策与注意事项
-
-### 3.1 流式架构:单例流(重要)
-
-**问题**:原先 `CanvasChatPanel` 调 `useCanvasThreadStream()`,`CanvasQuickActionBar` 调 `useCanvasStream()`(旧的直接 API),两套流互不相通,快捷动作响应不出现在聊天面板。
-
-**解决方案**:新增 `CanvasStreamContext`,在 `CanvasResizableLayout` 中调用 `useCanvasThreadStream()` 一次,通过 Context 共享给所有子组件。`CanvasChatPanel` 和 `useCanvasQuickActions` 都从 `useCanvasStreamContext()` 取 `sendMessage`。
-
-```
-useCanvasStream(旧,已停用) ← 不再被调用,保留备用
-useCanvasThreadStream(新,LangGraph SDK) ← 通过 CanvasStreamContext 统一共享
-```
-
-### 3.2 artifact 内容注入机制(重要)
-
-**问题**:LangGraph `create_agent` 只把 `state.messages` 传给 LLM,`state.artifact` 字段不会自动可见。Agent 做 expand/shorten 等操作时看不到要改写的内容。
-
-**解决方案**:在每次 `sendMessage` 时,把当前 artifact 内容以 `` 标签嵌入人类消息末尾。
-
-- 普通聊天:`sendMessage` 内部自动追加(仅当消息里还没有 `` 时)
-- 快捷动作:`buildQuickActionMessage(id, content)` 在构建时就包含内容
-- 聊天面板显示时:`message-list-item.tsx` 调 `stripArtifactTag()` 剥掉标签,用户看不到
-
-```typescript
-// utils/quick-actions.ts
-export function buildQuickActionMessage(id, content) {
- const body = isCodeArtifact(content) ? content.code : content.fullMarkdown;
- return `${instruction}\n\n\n${body}\n`;
-}
-
-// core/messages/utils.ts
-export function stripArtifactTag(content: string): string {
- return content.replace(/]*>[\s\S]*?<\/artifact>/g, "").trim();
-}
-```
-
-### 3.3 ThinkTagParser(AI 改写思考过程解析)
-
-后端支持两种思考格式:
-- Anthropic:独立 thinking block,以 `event: thinking` SSE 事件传输
-- DeepSeek:把思考过程内联在文本里:`...content`
-
-前端 `useWritingRewrite.ts` 的 `ThinkTagParser` 是流式状态机,处理跨 chunk 边界的 `` 标签。
-
-### 3.4 版本历史:本地切换
-
-`CanvasArtifactVersionNav` 的 prev/next 只修改本地 `CanvasGraphContext.artifact.currentIndex`,不写回后端。刷新后 currentIndex 重置为后端存储的最新值(这是有意的简化设计)。
-
-### 3.5 Writing Editor 流程(三阶段状态机)
-
-```
-idle → menu → preview → idle
- ↑ ↓
- openAiMenu closeAll
-```
-
-- **idle**:正常编辑
-- **menu**:显示 `WritingAiMenu`(选择 polish/expand/shorten/tone)
-- **preview**:显示 `WritingRewritePreview`(SSE 流式结果 + 思考过程 + 替换/插入按钮)
-
----
-
-## 四、已解决的技术难题
-
-| 问题 | 解法 |
-|------|------|
-| react-resizable-panels v4 API 变更(`orientation` 不是 `direction`,`groupRef` 不是 `ref`,`setLayout` 参数为 `{id: size}` 对象) | 参考 v4 文档,逐一修正 |
-| Flex 高度链底部裁剪 | 在 wrapper div 加 `min-h-0`,配合 `flex-1 overflow-hidden` |
-| Tiptap v3 BubbleMenu 已从 React 组件改为纯 Extension | 自定义 portal 实现:监听 `selectionUpdate`,手动计算位置 |
-| 段落悬停按钮鼠标桥接:鼠标经过 6px 间隙时按钮消失 | 文档级 `mousemove` 监听 + 380ms 延迟隐藏,判断鼠标是否在扩展热区内 |
-| 弹窗超出屏幕底部 | 检测 `spaceBelow` vs `spaceAbove`,空间不足时翻转到光标上方 |
-| DeepSeek 在文本流里混入 `` 标签 | `ThinkTagParser` 状态机,保留最后 6/7 字节防止跨 chunk 分割 |
-| loading→success 状态转换检测(stale closure) | 用 `prevStatusRef` + `useEffect` 检测 status 变化,取代直接读 state |
-| 快捷动作响应不出现在聊天面板 | `CanvasStreamContext` 统一 `sendMessage` 入口 |
-| LLM 看不到 artifact 内容(只有 state.messages) | 消息里直接嵌入 `` 标签,显示时剥离 |
-| `` 标签在聊天面板里显示 | `message-list-item.tsx` 调 `stripArtifactTag()` |
-
----
-
-## 五、当前已知限制(非 bug,设计取舍)
-
-1. **版本切换不持久化**:切换 artifact 版本后刷新页面会回到最新版本
-2. **useCanvasStream 孤立**:旧的直接 API 流 hook 保留但未被调用,可后续清理
-3. **CanvasShell 的 setMessages 冗余**:加载旧线程时 `setMessages` 写入 CanvasGraphContext,但 `CanvasChatPanel` 读的是 `thread.messages`(来自 useStream + fetchStateHistory)。setArtifact 仍有效
-4. **artifact 内容随消息发送**:大文档每条消息都附带全文,token 消耗较大。当前规模可接受
-
----
-
-## 六、快速开始下一次工作
-
-### 启动服务
-
-```bash
-# 后端(从 offline-backend-20260512/backend/)
-make dev # 启动 Gateway API on port 8001
-
-# 前端(从 frontend-web/)
-pnpm dev # 启动 Vite on port 5174
-```
-
-### 访问路径
-
-- 主页:`http://localhost:5174`
-- Canvas 入口:点击左侧导航 → Canvas,或直接访问 `http://localhost:5174/page/canvas`
-
-### 类型检查
-
-```bash
-cd frontend-web && pnpm typecheck
-# 预期:只有 CodeArtifactRenderer.tsx 的 CodeMirror 版本冲突错误(预存 bug,不影响运行)
-```
-
----
-
-## 七、如需继续开发的潜在方向
-
-| 方向 | 描述 | 优先级 |
-|------|------|------|
-| 版本切换持久化 | 将 currentIndex 变更写回后端 ThreadState | 低 |
-| artifact 内容 token 优化 | 仅发送 diff 或用 summary 代替全文 | 低 |
-| 清理 useCanvasStream | 删除未被调用的旧 hook | 低 |
-| 代码 artifact 的 AI 改写 | CodeArtifactRenderer 也接入 WritingAiMenu 流程 | 中 |
-| Canvas 选区→聊天(高级) | 用户在右栏选中文字后在左栏输入指令,自动带选区上下文 | 中 |
-| Tiptap 扩展(表格、任务列表) | 增加 Table、TaskList extension | 低 |
-
----
-
-## 八、关键文件速查
-
-| 文件 | 说明 |
-|------|------|
-| `frontend-web/docs/openCanvas-progress.md` | **本文档** |
-| `frontend-web/src/open-canvas/contexts/CanvasStreamContext.tsx` | 流单例 Context |
-| `frontend-web/src/open-canvas/hooks/useCanvasThreadStream.ts` | 主流 hook(LangGraph SDK) |
-| `frontend-web/src/open-canvas/hooks/useWritingRewrite.ts` | AI 改写状态机 |
-| `frontend-web/src/open-canvas/api/rewrite.ts` | 改写 SSE 客户端 |
-| `frontend-web/src/open-canvas/utils/quick-actions.ts` | 快捷动作消息构建 |
-| `frontend-web/src/core/messages/utils.ts` | `stripArtifactTag` 在此 |
-| `frontend-web/src/components/workspace/messages/message-list-item.tsx` | 调用 stripArtifactTag 的位置 |
-| `frontend-web/src/styles/globals.css` | Tiptap 写作编辑器样式(`.tiptap-writing-editor`) |
-| `offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/agent.py` | canvas_agent 工厂 |
-| `offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/tools.py` | update_artifact_tool |
-| `offline-backend-20260512/backend/packages/harness/deerflow/agents/canvas_agent/prompt.py` | 系统提示词 |
-| `offline-backend-20260512/backend/app/gateway/routers/writing.py` | AI 改写后端接口 |
-| `offline-backend-20260512/backend/langgraph.json` | canvas_agent 注册 |
-
----
-
-## 九、开始新对话的推荐提示词
-
-```
-我们在开发 DeerFlow 的 Canvas 写作模式(open-canvas 集成)。
-详细进度见:frontend-web/docs/openCanvas-progress.md
-
-当前全部功能已完成,包括:
-- Canvas 双栏布局、canvas_agent、版本历史
-- AI Writing Editor(Tiptap + SSE 改写 + 思考过程展开)
-- 快捷动作(通过 CanvasStreamContext 统一流)
-- artifact 内容注入(消息里嵌入 标签,显示时剥离)
-
-请根据进度文档第七节"潜在开发方向"或我指定的任务继续开发。
-```
diff --git a/frontend-web/docs/openCanvas.md b/frontend-web/docs/openCanvas.md
deleted file mode 100644
index 6ef38733..00000000
--- a/frontend-web/docs/openCanvas.md
+++ /dev/null
@@ -1,950 +0,0 @@
-# Open Canvas 模式需求拆解文档
-
-## 1. 文档目标
-
-本文档用于指导 DeerFlow 在现有架构中新增一个 `open-canvas` 风格的 Canvas 模式。
-
-目标不是直接照搬 `langchain-ai/open-canvas` 的 Next.js + LangGraph.js 实现,而是在当前项目技术栈内完成一套等价能力的实现:
-
-- 前端:基于 `React 19 + Vite + react-router-dom`
-- 后端:基于 `Python + FastAPI + LangGraph`
-- 模型层:支持云模型与本地模型自由配置
-- 状态层:支持结构化 artifact、版本历史、局部改写、快速动作、画布编辑
-
-本文档重点是把需求拆成“可实现、可排期、可验收”的阶段性任务。
-
----
-
-## 2. 最终目标
-
-### 2.1 用户视角目标
-
-用户进入一个新的 Canvas 工作模式后,可以获得与 open-canvas 接近的体验:
-
-- 左侧是聊天区,右侧是 Canvas 编辑区
-- 可以从空白文本或代码模板快速开始
-- 可以通过聊天让 AI 生成一份结构化 artifact
-- 可以对 artifact 整体重写,也可以对选中的局部内容进行改写
-- artifact 支持版本历史切换
-- artifact 支持文本和代码两种主要类型
-- 支持快速动作,例如扩写、压缩、改风格、改语气、加注释、修 bug、换语言
-- 支持启用 Web 搜索增强
-- 支持切换模型
-- 支持使用本地模型,例如 Ollama 或 OpenAI-compatible 本地服务
-
-### 2.2 工程视角目标
-
-新增的 Canvas 模式需要满足以下工程原则:
-
-- 不破坏现有 DeerFlow 工作区
-- 不影响现有基于 `artifacts: string[]` 的文件产物功能
-- 可逐阶段上线
-- 前后端状态协议清晰
-- 可兼容现有 LangGraph 线程、消息流、认证和模型接口
-
----
-
-## 3. 范围定义
-
-### 3.1 本次需求包含
-
-- 前端新增 `open-canvas` 专用目录
-- 在 [PageRoutes.tsx](../src/pages/PageRoutes.tsx) 中引入新的 Canvas 路由
-- 前端新增 Canvas 页面、上下文、组件、状态管理
-- 后端新增 Canvas 专用线程状态字段
-- 后端新增 Canvas Agent 图或在现有 Agent 体系中新增 Canvas 模式分支
-- 模型配置统一为 DeerFlow 后端配置驱动
-- 支持本地大模型配置与模型列表动态下发
-- 支持结构化 artifact 的保存、读取、更新、版本切换
-
-### 3.2 本次需求不包含
-
-- 一次性复刻 open-canvas 全部 UI 细节
-- 完整照搬 Supabase 鉴权体系
-- 直接复用 open-canvas 的 Next.js API Route
-- 直接运行 open-canvas 的 LangGraph.js 后端
-- 首阶段就实现所有快捷动作、所有富文本效果、所有搜索增强能力
-
-### 3.3 迁移原则
-
-- 借鉴 open-canvas 的能力模型,不直接迁移其运行时架构
-- 前端尽量复用其交互设计和数据结构
-- 后端以 Python 实现同等能力
-- 配置以 DeerFlow 为真源,不以 open-canvas 的 TS 文件为真源
-
----
-
-## 4. 总体设计思路
-
-### 4.1 前端设计思路
-
-在现有 `frontend-web/src` 下新增一个专用目录,例如:
-
-```text
-src/
- open-canvas/
- pages/
- routes/
- components/
- contexts/
- hooks/
- types/
- api/
- utils/
- constants/
-```
-
-建议将 Canvas 模式作为一个独立的页面子系统接入,而不是把现有工作区页面强行改造成 Canvas。
-
-建议路由形式:
-
-- `/canvas`
-- `/canvas/:threadId`
-
-也可以挂在现有页面布局下,例如:
-
-- `/workspace/canvas`
-- `/workspace/canvas/:threadId`
-
-推荐优先采用独立一级功能路由:
-
-- 逻辑更清晰
-- 状态隔离更容易
-- 不容易污染现有 `workspace` 功能
-
-### 4.2 后端设计思路
-
-后端不建议直接改造现有通用 Agent 去同时承担全部 Canvas 行为,而是建议采用以下两种方案之一:
-
-#### 方案 A:新增独立 Canvas Assistant
-
-- 新增 `canvas_agent`
-- 使用单独的 LangGraph 状态定义
-- 前端调用时通过 `assistant_id=canvas_agent`
-
-优点:
-
-- 与现有 `lead_agent` 隔离
-- 风险更低
-- 更容易对齐 open-canvas 的图结构
-
-缺点:
-
-- 会产生一套新的 Agent 图维护成本
-
-#### 方案 B:在现有 Lead Agent 中增加 Canvas 模式分支
-
-- 在 thread metadata 或 configurable 中增加 `ui_mode=canvas`
-- Agent 根据模式进入 Canvas 路径
-
-优点:
-
-- 统一入口
-
-缺点:
-
-- 会显著增加现有 Agent 复杂度
-- 容易影响现有聊天和文件产物模式
-
-推荐先做方案 A。
-
-### 4.3 模型配置设计思路
-
-模型配置统一以 DeerFlow 后端 `config.yaml` 为真源。
-
-前端不维护固定模型常量列表,而是通过 `/api/models` 获取模型能力元数据。
-
-为了实现 open-canvas 风格体验,需要后端模型接口至少返回:
-
-- `name`
-- `display_name`
-- `provider`
-- `supports_thinking`
-- `supports_reasoning_effort`
-- `supports_vision`
-- `supports_tool_calling`
-- `supports_artifact_editing`
-- `supports_code_editing`
-- `temperature_range`
-- `max_tokens_range`
-- `base_url_source` 或 provider 标识
-- `is_local`
-
----
-
-## 5. 与当前项目的关键差异
-
-### 5.1 当前 DeerFlow 现状
-
-当前 DeerFlow 线程状态中的 artifact 主要是文件路径列表:
-
-- `artifacts: string[]`
-
-这更接近“生成文件产物”的工作模式。
-
-前端当前 artifact 面板也主要是:
-
-- 文件列表
-- 文件详情
-- 文件下载/预览
-
-### 5.2 open-canvas 的关键能力
-
-open-canvas 的核心不是“生成文件”,而是“维护一个结构化的当前文档对象”:
-
-- 一个 conversation 对应一个主 artifact
-- artifact 具有类型、标题、内容、版本
-- 用户可以选中部分内容让 AI 改写
-- 每次改写都会生成新版本
-
-### 5.3 需要新增的核心能力
-
-为支持 Canvas 模式,需要在 DeerFlow 中新增一套结构化 artifact 状态:
-
-- 当前 artifact
-- 当前版本索引
-- 历史版本列表
-- 选区信息
-- 当前编辑模式
-- 快速动作状态
-- Web 搜索结果状态
-
----
-
-## 6. 分阶段实施方案
-
-以下为推荐的 8 个阶段,每个阶段都可单独开发、联调和验收。
-
----
-
-## 7. 阶段一:前端路由与目录骨架
-
-### 7.1 目标
-
-在不影响现有页面的前提下,建立 Canvas 模式的前端骨架。
-
-### 7.2 需求
-
-- 新增 `src/open-canvas/` 目录
-- 新增 Canvas 页面路由
-- 将路由接入 [PageRoutes.tsx](../src/pages/PageRoutes.tsx)
-- 提供 Canvas 首页和 Canvas Thread 页
-- 支持空状态展示
-
-### 7.3 建议目录
-
-```text
-src/open-canvas/
- api/
- threads.ts
- runs.ts
- models.ts
- components/
- layout/
- chat/
- canvas/
- artifacts/
- quick-actions/
- selectors/
- contexts/
- CanvasThreadContext.tsx
- CanvasGraphContext.tsx
- CanvasUIContext.tsx
- hooks/
- useCanvasThread.ts
- useCanvasStream.ts
- useCanvasArtifact.ts
- pages/
- CanvasHomePage.tsx
- CanvasThreadPage.tsx
- routes/
- OpenCanvasRoutes.tsx
- types/
- artifact.ts
- thread.ts
- model.ts
- utils/
- artifact.ts
- selection.ts
- stream.ts
- constants/
- index.ts
-```
-
-### 7.4 路由建议
-
-- 在 `PageRoutes.tsx` 中新增 `canvas/*`
-- 在 `src/open-canvas/routes/OpenCanvasRoutes.tsx` 中维护子路由
-
-建议形式:
-
-```text
-/canvas
-/canvas/:threadId
-```
-
-### 7.5 验收标准
-
-- 能访问 Canvas 首页
-- 能访问指定 thread 的 Canvas 页面
-- 不影响现有 `workspace/*` 与 `strategy/*`
-- 页面有独立布局骨架
-
----
-
-## 8. 阶段二:前端 Canvas 布局与交互容器
-
-### 8.1 目标
-
-先做出“左聊天、右画布”的基础容器,不接真实 AI 改写逻辑。
-
-### 8.2 需求
-
-- 左侧聊天区
-- 右侧画布区
-- 支持面板拖拽或折叠
-- 初始空状态
-- 支持“快速开始”:
- - 新建空白文本 artifact
- - 新建代码 artifact
-
-### 8.3 组件拆分建议
-
-- `CanvasShell`
-- `CanvasSidebar`
-- `CanvasChatPanel`
-- `CanvasArtifactPanel`
-- `CanvasEmptyState`
-- `QuickStartActions`
-
-### 8.4 快速开始能力
-
-用户进入空线程时可以选择:
-
-- 开始写文章
-- 开始写代码
-- 从模板开始
-
-首版模板可只做最小集:
-
-- 空白文档
-- HTML 模板
-- React 组件模板
-- Python 脚本模板
-
-### 8.5 验收标准
-
-- 页面可稳定展示双栏布局
-- 快速开始能创建前端本地 artifact 状态
-- 代码和文本可分别进入不同渲染器
-
----
-
-## 9. 阶段三:结构化 Artifact 数据模型
-
-### 9.1 目标
-
-定义 DeerFlow 的 Canvas Artifact 协议,作为前后端统一契约。
-
-### 9.2 核心要求
-
-需要支持以下能力:
-
-- 文本 artifact
-- 代码 artifact
-- 标题
-- 当前版本索引
-- 版本历史
-- 当前选中版本切换
-- 后续支持局部高亮编辑
-
-### 9.3 建议数据结构
-
-```ts
-type CanvasArtifact = {
- currentIndex: number;
- contents: Array;
-};
-
-type CanvasArtifactText = {
- index: number;
- type: "text";
- title: string;
- fullMarkdown: string;
-};
-
-type CanvasArtifactCode = {
- index: number;
- type: "code";
- title: string;
- language: string;
- code: string;
-};
-```
-
-### 9.4 线程状态建议新增字段
-
-前端与后端需共同支持:
-
-- `artifact`
-- `highlighted_text`
-- `highlighted_code`
-- `canvas_mode`
-- `web_search_enabled`
-- `web_search_results`
-- `quick_action_id`
-
-### 9.5 与现有 `artifacts: string[]` 的关系
-
-建议保留现有 `artifacts: string[]` 不动。
-
-新增字段:
-
-- `artifact` 用于 Canvas 主文档
-- `artifacts` 继续用于文件产物
-
-这样可以兼容未来“Canvas 生成网页文件、导出文件、保存文件”的场景。
-
-### 9.6 验收标准
-
-- 前端类型定义完成
-- 后端状态字段设计完成
-- 确认与现有线程状态兼容
-
----
-
-## 10. 阶段四:后端线程状态与 API 扩展
-
-### 10.1 目标
-
-让后端线程状态可以安全存储和返回 Canvas Artifact。
-
-### 10.2 需求
-
-- 扩展 Python `ThreadState`
-- 扩展状态序列化逻辑
-- 确保 `/api/threads/*` 返回结构化 `artifact`
-- 确保 LangGraph 流中可带上 `artifact` 更新
-
-### 10.3 后端改造点
-
-建议关注以下模块:
-
-- `packages/harness/deerflow/agents/thread_state.py`
-- 线程 state serializer
-- `app/gateway/routers/threads.py`
-- 流事件适配层
-
-### 10.4 注意事项
-
-- 旧线程没有 `artifact` 字段时必须兼容
-- 状态更新不能破坏现有消息结构
-- 必须确保 JSON 可序列化
-
-### 10.5 验收标准
-
-- 新建线程可写入 `artifact`
-- 读取线程时可返回 `artifact`
-- 不影响现有线程读取
-- 前端能从线程 state 中恢复 Canvas 内容
-
----
-
-## 11. 阶段五:Canvas 专用 Agent 图
-
-### 11.1 目标
-
-实现与 open-canvas 类似的 AI 路由能力。
-
-### 11.2 推荐方案
-
-新增 `canvas_agent`,单独维护图逻辑。
-
-### 11.3 节点拆分建议
-
-首版建议至少拆成这些节点:
-
-- `route_user_intent`
-- `reply_general_input`
-- `generate_artifact`
-- `rewrite_artifact`
-- `update_highlighted_text`
-- `update_highlighted_code`
-- `generate_followup`
-
-二期可加:
-
-- `rewrite_artifact_theme`
-- `rewrite_code_theme`
-- `change_length`
-- `change_reading_level`
-- `translate_artifact`
-- `add_comments`
-- `add_logs`
-- `fix_bugs`
-- `port_language`
-- `web_search_enrich`
-
-### 11.4 路由逻辑要求
-
-系统需要能判断:
-
-- 用户是在闲聊
-- 用户要新生成 artifact
-- 用户要整体重写当前 artifact
-- 用户要局部修改当前 artifact
-- 用户要执行某个快捷动作
-
-### 11.5 状态输入要求
-
-节点需要读取:
-
-- 当前消息历史
-- 当前 artifact
-- 当前选区
-- 当前模型配置
-- Web 搜索开关
-
-### 11.6 输出要求
-
-节点输出必须能更新:
-
-- `messages`
-- `artifact`
-- `web_search_results`
-- `followup`
-
-### 11.7 验收标准
-
-- 能根据输入创建新 artifact
-- 能根据输入整体改写 artifact
-- 能保留版本历史
-- 能在改写后返回自然语言 followup
-
----
-
-## 12. 阶段六:前端流式同步与版本管理
-
-### 12.1 目标
-
-让前端能像 open-canvas 一样在流式过程中更新画布与聊天。
-
-### 12.2 需求
-
-- 复用现有 LangGraph stream client
-- 在 Canvas 模式中解析 `artifact` 更新
-- 支持流式文本更新
-- 支持版本切换
-- 支持断线后恢复线程状态
-
-### 12.3 前端状态层建议
-
-- `CanvasThreadContext`:线程基础信息
-- `CanvasGraphContext`:消息、artifact、streaming 状态
-- `CanvasUIContext`:面板、编辑态、选中态
-
-### 12.4 关键能力
-
-- 当前版本切换
-- 新版本创建后自动定位到最新版本
-- 手动编辑内容后保存
-- 流结束后线程状态持久化
-
-### 12.5 验收标准
-
-- 发送消息时聊天和画布可同步更新
-- 版本数会增加
-- 切换历史版本稳定
-- 刷新页面后线程状态能恢复
-
----
-
-## 13. 阶段七:局部选区改写
-
-### 13.1 目标
-
-实现 Canvas 模式最关键的差异化能力之一:选中局部内容进行 AI 改写。
-
-### 13.2 文本选区需求
-
-- 支持在 Markdown 文本中选择一段文本
-- 记录选区位置与原始文本
-- 用户发出修改请求后,仅重写该选区
-- 将选区结果合并为新版本 artifact
-
-### 13.3 代码选区需求
-
-- 支持在代码编辑器中选择一段代码
-- 记录 start/end index 或行列信息
-- AI 只返回替换片段
-- 前端或后端负责将新片段拼接回完整代码
-
-### 13.4 实现建议
-
-首版建议:
-
-- 文本:前端记录纯文本字符位置
-- 代码:使用 CodeMirror selection 信息
-
-不建议首版就做复杂块级富文本局部编辑。
-
-### 13.5 风险
-
-- Markdown 渲染态与源码态位置映射复杂
-- 富文本块级位置难维护
-- 流式更新中选区同步容易错位
-
-### 13.6 首版折中方案
-
-首版文本只基于 `fullMarkdown` 纯文本改写,不做 BlockNote 富文本块定位。
-
-### 13.7 验收标准
-
-- 文本局部改写成功
-- 代码局部改写成功
-- 原内容未改动部分保持不变
-- 新版本正确入历史
-
----
-
-## 14. 阶段八:快捷动作系统
-
-### 14.1 目标
-
-实现 open-canvas 风格的快捷动作体系。
-
-### 14.2 首批推荐动作
-
-文本类:
-
-- 扩写
-- 精简
-- 改正式语气
-- 改口语语气
-- 改阅读难度
-- 翻译
-
-代码类:
-
-- 加注释
-- 加日志
-- 修复 bug
-- 优化代码
-- 转换语言
-
-### 14.3 设计方式
-
-快捷动作不要直接写死在 UI,建议配置化:
-
-- `id`
-- `label`
-- `type`
-- `applicable_to`
-- `prompt_template`
-
-### 14.4 后端设计
-
-快捷动作可通过:
-
-- 前端传 `quick_action_id`
-- 后端路由节点根据动作 id 选择 prompt
-
-### 14.5 验收标准
-
-- 文本动作可执行
-- 代码动作可执行
-- 结果进入新版本
-- 不同动作提示词可扩展
-
----
-
-## 15. 阶段九:模型配置统一与本地模型支持
-
-### 15.1 目标
-
-让 Canvas 模式支持和主系统统一的模型选择,并且能自由配置本地大模型。
-
-### 15.2 原则
-
-- 后端 `config.yaml` 为真源
-- 前端只展示后端暴露的模型信息
-- 不维护一份脱离后端的前端常量模型表
-
-### 15.3 本地模型支持范围
-
-建议优先支持两类:
-
-- `Ollama`
-- OpenAI-compatible 本地服务
- - LM Studio
- - vLLM
- - LocalAI
- - one-api 风格代理
-
-### 15.4 后端配置建议
-
-后端模型配置应至少支持:
-
-- `name`
-- `display_name`
-- `use`
-- `model`
-- `api_key`
-- `base_url`
-- `supports_vision`
-- `supports_thinking`
-- `supports_tool_calling`
-- `is_local`
-- `canvas_capabilities`
-
-### 15.5 Canvas 模式额外要求
-
-由于不是所有模型都适合 Canvas 改写任务,建议增加能力标识:
-
-- `supports_canvas_generation`
-- `supports_canvas_rewrite`
-- `supports_canvas_code_edit`
-- `supports_streaming_text`
-- `supports_streaming_tool_calls`
-
-### 15.6 与 open-canvas 的关系
-
-可以借鉴其命名习惯和能力分层,但不建议直接依赖其 `packages/shared/src/models.ts` 作为配置源。
-
-### 15.7 验收标准
-
-- 前端模型选择器可显示后端返回模型
-- 可切换本地模型
-- Canvas 模式能用本地模型生成和改写
-
----
-
-## 16. 阶段十:Web 搜索增强
-
-### 16.1 目标
-
-实现类似 open-canvas 的可选 Web 搜索增强。
-
-### 16.2 需求
-
-- UI 提供 Web 搜索开关
-- 后端在需要时走搜索节点
-- 搜索结果写入线程状态
-- 结果可在画布旁侧栏展示
-
-### 16.3 首版建议
-
-只做:
-
-- 开关
-- 搜索结果列表
-- 搜索结果用于生成 artifact
-
-暂不做复杂网页抓取编辑链路。
-
-### 16.4 验收标准
-
-- 打开搜索后可返回搜索结果
-- 搜索结果可参与 artifact 生成
-- 结果展示不阻塞主画布
-
----
-
-## 17. 阶段十一:持久化、导出与文件产物联动
-
-### 17.1 目标
-
-让 Canvas 文档与 DeerFlow 原有文件能力协同。
-
-### 17.2 需求
-
-- artifact 持久化在线程 state 中
-- 可手动导出为文件
-- 可生成真实 HTML/Markdown/代码文件到现有 outputs
-- Canvas artifact 与 `artifacts: string[]` 可双向关联
-
-### 17.3 推荐能力
-
-- 导出 Markdown
-- 导出代码文件
-- 另存为 artifact file
-- 从 artifact file 回填到 Canvas
-
-### 17.4 验收标准
-
-- 当前版本可导出
-- 导出文件可在现有文件面板查看
-- 文件型 artifact 和 Canvas artifact 能共存
-
----
-
-## 18. 推荐开发顺序
-
-建议严格按以下顺序推进:
-
-1. 路由和目录骨架
-2. Canvas 页面布局
-3. 前端 artifact 数据模型
-4. 后端线程状态扩展
-5. 新建 Canvas Agent 图
-6. 前后端流式同步
-7. 整体生成与整体改写
-8. 局部选区改写
-9. 快捷动作
-10. 本地模型支持完善
-11. Web 搜索增强
-12. 导出与文件联动
-
-不要先做局部改写和快捷动作,否则前面的状态与路由契约不稳定,返工会很多。
-
----
-
-## 19. 里程碑定义
-
-### M1:页面可进入
-
-- 路由接通
-- 双栏布局完成
-- 本地 mock artifact 可渲染
-
-### M2:后端状态打通
-
-- 线程可保存结构化 artifact
-- 页面刷新后能恢复
-
-### M3:AI 生成可用
-
-- 可新建文本/代码 artifact
-- 可整体改写
-- 可产生版本历史
-
-### M4:核心体验可用
-
-- 选区改写可用
-- 快捷动作可用
-- 模型切换可用
-
-### M5:增强能力完整
-
-- 本地模型支持完善
-- Web 搜索增强
-- 导出和文件联动
-
----
-
-## 20. 风险与难点
-
-### 20.1 最大难点
-
-- 结构化 artifact 状态从无到有
-- 局部选区改写的精确性
-- 流式状态与版本管理的一致性
-- 不破坏现有 DeerFlow 文件型 artifact 体系
-
-### 20.2 技术风险
-
-- 线程 state JSON 结构过大
-- 大模型对局部改写指令遵循不稳定
-- 本地模型 tool calling 与流式能力差异大
-- 文本渲染态与源码态位置映射复杂
-
-### 20.3 规避建议
-
-- 首版只支持 Markdown 源码态编辑,不做复杂富文本编辑
-- 首版只支持文本和代码两种 artifact 类型
-- 首版先做整体生成和整体改写,再做局部改写
-- 本地模型先支持“能生成、能重写”的最小能力,不强求与顶级闭源模型完全同效
-
----
-
-## 21. 验收总清单
-
-### 21.1 前端
-
-- 有独立 Canvas 路由
-- 有专用目录和模块边界
-- 双栏布局可用
-- 文本和代码两类 artifact 可渲染
-- 历史版本可切换
-- 选区改写可交互
-- 快捷动作可触发
-- 模型可切换
-
-### 21.2 后端
-
-- 线程状态支持 `artifact`
-- 新增 `canvas_agent` 或等价模式
-- 生成、重写、局部改写可用
-- 流式返回稳定
-- 模型列表接口支持 Canvas 能力元数据
-
-### 21.3 本地模型
-
-- 可配置 Ollama
-- 可配置 OpenAI-compatible 本地服务
-- 前端可见并可切换
-- 至少能完成文本生成和整体改写
-
----
-
-## 22. 推荐首版最小可交付范围
-
-如果要控制风险,建议首版只交付以下内容:
-
-- 新增 `canvas/*` 路由
-- 双栏布局
-- 结构化 `artifact`
-- `canvas_agent`
-- 文本/代码 artifact 新建
-- 整体生成
-- 整体改写
-- 历史版本切换
-- 后端模型动态加载
-- Ollama 或本地 OpenAI-compatible 至少一种可用
-
-先不要把以下内容放进首版强制目标:
-
-- 富文本块级精确编辑
-- 全量快捷动作
-- 复杂 Web 搜索增强
-- 多 artifact 并行管理
-
----
-
-## 23. 后续实施建议
-
-建议下一步基于本文档继续输出两份落地文档:
-
-### 23.1 前端实施清单
-
-逐文件列出:
-
-- 要新建哪些文件
-- 哪些组件可复用
-- 哪些页面要接入路由
-- 哪些状态要从现有 hooks 抽离
-
-### 23.2 后端实施清单
-
-逐模块列出:
-
-- 要改哪些 Python 文件
-- 新增哪些状态字段
-- 新增哪些 Agent 节点
-- 哪些 API 需要扩展
-
----
-
-## 24. 结论
-
-这个需求很大,但完全可拆。
-
-正确做法不是“先抄 UI”,而是按下面的主线推进:
-
-- 先建前端 Canvas 子系统
-- 再建结构化 artifact 状态
-- 再建 Canvas Agent 图
-- 再补局部改写和快捷动作
-- 最后统一本地模型能力
-
-只要按本文档阶段推进,这个需求是可以落地的,而且能和你现在的 DeerFlow 架构保持长期兼容。
diff --git a/frontend-web/docs/recommend-agents-progress.md b/frontend-web/docs/recommend-agents-progress.md
deleted file mode 100644
index f892f033..00000000
--- a/frontend-web/docs/recommend-agents-progress.md
+++ /dev/null
@@ -1,268 +0,0 @@
-# 智能体推荐弹窗功能 — 开发进度
-
-> 分支: `dhk-1`
-> 最后更新: 2026-05-25
-> 状态: **全部完成 ✅**
-
----
-
-## 功能概述
-
-在圆桌规划流程的 Step 1(任务意图理解)和 Step 2(多智能体圆桌研讨)之间,插入一个**推荐弹窗**:
-
-1. 用户完成任务澄清后,点击"进入多智能体研讨"→ 弹出推荐弹窗
-2. 弹窗拉取 `GET /api/agents` 候选池,调用 `roundtable-recommender` 智能体流式推荐 2-6 个最合适的智能体
-3. 推荐结果默认勾选,用户可取消勾选 / 从候选池增选(总选 2-8 个)
-4. 确认后 Step 2 使用用户最终选定的智能体列表,不再硬编码 6 个圆桌席位
-
----
-
-## 已完成的所有步骤
-
-### Step 1 — 后端:新建 `roundtable-recommender` 智能体 ✅
-
-**文件:**
-- `offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yaml`
-- `offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/SOUL.md`
-
-**要点:**
-- `id: roundtable-recommender`,`name: 智能体推荐`
-- SOUL.md 规定输出协议:自然语言推理 + `[RECOMMEND_READY]` 标记 + ` ```json {"picks": [...]} ``` ` 块
-- 每个 pick: `{"agent_id": "...", "reason": "..."}`,agent_id 必须来自候选池
-- 约束:2 ≤ picks ≤ 6,禁止使用任何工具
-
----
-
-### Step 2 — 后端:新建 `/api/recommend/stream` 接口 ✅
-
-**文件:**
-- `offline-backend-20260512/backend/app/gateway/routers/recommend.py`(新建)
-- `offline-backend-20260512/backend/app/gateway/app.py`(注册路由)
-
-**接口规格:**
-```
-POST /api/recommend/stream
-Content-Type: application/json
-
-Body:
-{
- "intent": { "objective": "...", "constraints": [...], "assumptions": [...] },
- "candidates": [{ "agent_id": "...", "name": "...", "description": "..." }, ...],
- "model": "deepseek-chat" // 可选
-}
-
-SSE 流:透传上游 LangGraph frames + 最终状态帧
-最终帧: { "status": "done"|"asking"|"error", "content": "...", "picks": [...] | null }
-```
-
-**关键实现:**
-- 无 `/init`,每次请求内部创建新 thread(stateless)
-- `_validate_picks()`:对比候选池 id、去重、夹断到 2-6,< 2 返回 `[]`
-- `_build_prompt()`:将 intent + 候选池渲染为单条 user message
-- `_RECOMMEND_READY_PATTERN`:正则提取 `[RECOMMEND_READY]` 后的 JSON 块
-- `httpx.AsyncClient(trust_env=False)`:防止 loopback 被代理拦截
-- 复用 `multi_agent.py` 的 `_auth_headers`, `_create_thread`, `_stream_upstream` 等
-
-**app.py 注册:**
-```python
-from app.gateway.routers import recommend
-app.include_router(recommend.router)
-```
-
----
-
-### Step 3 — 前端:`recommend.ts` API 客户端 ✅
-
-**文件:** `src/roundtable-planning/api/recommend.ts`
-
-**导出:**
-```typescript
-// 类型
-export interface RecommendPick { agent_id: string; reason: string }
-export interface StreamRecommendResult { status: "done"|"asking"|"error"; content: string; picks: RecommendPick[]|null }
-export type RecommendStreamPhase = "connecting"|"connected"|"streaming"
-export interface RecommendCandidate { agent_id: string; name: string; description: string }
-
-// 候选池过滤(排除规则集中管理)
-export function filterRecommendCandidates(agents: Agent[]): RecommendCandidate[]
-
-// 流式推荐
-export async function streamRecommend(req: StreamRecommendRequest): Promise
-```
-
-**排除规则(`filterRecommendCandidates`):**
-- `roundtable-intent` — Step 1 澄清专员
-- `roundtable-recommender` — 推荐者自身
-- 前缀 `roundtable-coordinator-` — 每 session 生成的一次性协调器
-
----
-
-### Step 4 — 前端:`RecommendAgentsDialog.tsx` 弹窗组件 ✅
-
-**文件:** `src/roundtable-planning/components/RecommendAgentsDialog.tsx`
-
-**Props:**
-```typescript
-export interface RecommendAgentsDialogProps {
- open: boolean;
- intent: IntentSummary | null;
- model?: string;
- onCancel: () => void;
- onConfirm: (agentIds: string[]) => void;
-}
-```
-
-**内部阶段机器:**
-```
-"idle" → "loading_agents" → "recommending" → "ready"
- ↘ "fallback" (推荐失败,兜底)
- ↘ "error"
-```
-
-**UI 分区:**
-- **推荐参会**(默认全选):推荐智能体卡片,带 reason 说明
-- **其他候选**(默认未选):可增选,补足 8 个上限
-
-**选择约束:** min 2,max 8;确认按钮在范围外 disabled
-
-**安全机制:**
-- `generationRef` 计数器防止 stale async 污染状态
-- `AbortController` 取消在途推荐请求
-- `intersectInOrder()`:前端二次验证推荐 id 在候选池中存在
-- 兜底列表 `FALLBACK_AGENT_IDS`:6 个内置 roundtable-* 智能体
-
-**确认顺序:** 推荐且已选 → 用户增选(按交互顺序)
-
----
-
-### Step 5 — 前端:`RoundtablePlanningPage.tsx` 动态 Agent 角色 ✅
-
-**删除:** `ROUNDTABLE_ROLE_TEMPLATES`, `makeRoles`, `ROUNDTABLE_AGENT_IDS`, `AGENT_NAME_TO_ROLE`(全部硬编码常量)
-
-**新增模块级:**
-```typescript
-const COMMANDER_SEAT_ID = 1
-type SelectedAgent = { agent_id: string; name: string }
-const BUILTIN_AGENT_META: Record
-const FALLBACK_AVATAR_TYPES = ["blue","purple","emerald","amber","pink"]
-function getAvatarTypeFor(agentId, index): string
-function getDisplayNameFor(agent: SelectedAgent): string
-function buildRoleTemplates(agents): ReadonlyArray>
-function makeRolesFromTemplates(templates, defaultStatus, overrides?): RoleEntry[]
-function buildAgentNameToRole(agents): Record
-const DEFAULT_SELECTED_AGENTS: SelectedAgent[] // 原 6 个内置席位
-```
-
-**新增 state + useMemo:**
-```typescript
-const [selectedAgents, setSelectedAgents] = useState(DEFAULT_SELECTED_AGENTS);
-const roleTemplates = useMemo(() => buildRoleTemplates(selectedAgents), [selectedAgents]);
-const agentNameToRole = useMemo(() => buildAgentNameToRole(selectedAgents), [selectedAgents]);
-const roundtableAgentIds = useMemo(() => selectedAgents.map(a => a.agent_id), [selectedAgents]);
-```
-
-**替换调用点:**
-| 替换前 | 替换后 |
-|---|---|
-| `AGENT_NAME_TO_ROLE[name]` | `agentNameToRole[name]` |
-| `ROUNDTABLE_AGENT_IDS` | `roundtableAgentIds` |
-| `makeRoles(status, ...)` | `makeRolesFromTemplates(roleTemplates, status, ...)` |
-| 角色面板 `role.id === 2 ? ` 链 | `getAvatar(agentNameToRole[role.agentId]?.avatarType ?? "blue", "h-7 w-7")` |
-| `HighFidelityReport seatNameMap` 用 `AGENT_NAME_TO_ROLE` | 用 `agentNameToRole` |
-| useEffect 依赖 | 加入 `roleTemplates` |
-| dispatch useCallback 依赖 | 加入 `agentNameToRole` |
-
----
-
-### Step 6 — 前端:弹窗接入页面 ✅
-
-**新增 import:**
-```typescript
-import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
-```
-
-**新增 state:**
-```typescript
-const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState(false);
-```
-
-**新增 callback `handleConfirmAgents`:**
-```typescript
-const handleConfirmAgents = useCallback((agentIds: string[]) => {
- setSelectedAgents(
- agentIds.map(id => ({
- agent_id: id,
- name: BUILTIN_AGENT_META[id]?.displayName ?? id,
- })),
- );
- step2InitRef.current = false; // 重置 Step 2,让新 coordinator 按新名单初始化
- setIsRecommendDialogOpen(false);
- setCurrentStep(2);
- setShowToast("成功确认!已切换至第二阶段圆桌研讨系统");
-}, []);
-```
-
-**两个"进入多智能体研讨"按钮改为:**
-```typescript
-onClick={() => setIsRecommendDialogOpen(true)}
-```
-
-**JSX 渲染(在 showToast 之前):**
-```tsx
- setIsRecommendDialogOpen(false)}
- onConfirm={handleConfirmAgents}
-/>
-```
-
----
-
-## 文件清单(本次改动)
-
-### 后端(新建)
-| 文件 | 说明 |
-|---|---|
-| `.deer-flow/agents/roundtable-recommender/config.yaml` | 推荐智能体元数据 |
-| `.deer-flow/agents/roundtable-recommender/SOUL.md` | 推荐智能体系统提示 |
-| `app/gateway/routers/recommend.py` | `/api/recommend/stream` 接口 |
-
-### 后端(修改)
-| 文件 | 说明 |
-|---|---|
-| `app/gateway/app.py` | 注册 `recommend.router` |
-
-### 前端(新建)
-| 文件 | 说明 |
-|---|---|
-| `src/roundtable-planning/api/recommend.ts` | 前端 SSE 客户端 |
-| `src/roundtable-planning/components/RecommendAgentsDialog.tsx` | 推荐弹窗组件 |
-
-### 前端(修改)
-| 文件 | 说明 |
-|---|---|
-| `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 动态 Agent 角色 + 弹窗接入 |
-
----
-
-## 下次开始前检查项
-
-1. **后端是否已重启** — `recommend.router` 注册需重启 FastAPI(`make dev` 或 `./scripts/restart-all.sh`)
-2. **`roundtable-recommender` 智能体已注册** — 访问 `GET /api/agents` 确认列表里有 `roundtable-recommender`
-3. **typecheck 干净** — `pnpm typecheck 2>&1 | grep roundtable-planning` 无输出
-4. **弹窗流程端到端测试**:
- - Step 1 完成意图澄清 → `intentReady` 不为 null
- - 点击"进入多智能体研讨"→ 弹窗出现,开始加载智能体列表
- - 弹窗展示推荐结果(流式文字 + 卡片列表)
- - 调整勾选 → 确认 → Step 2 用所选智能体初始化
-
----
-
-## 潜在后续工作(未列入本次范围)
-
-- 弹窗内显示每个候选智能体的完整 description(目前从 `/api/agents` 取,已有字段)
-- 推荐失败时的 Toast 提示(目前静默兜底)
-- 推荐理由(reason)在 Step 2 角色面板中展示
-- `HighFidelityReport` 的 `avatarType` 字段按新选角色动态渲染
diff --git a/frontend-web/docs/素材专家检索.md b/frontend-web/docs/素材专家检索.md
deleted file mode 100644
index 2d1f89a8..00000000
--- a/frontend-web/docs/素材专家检索.md
+++ /dev/null
@@ -1,243 +0,0 @@
-# 素材专家检索 —— 内网接口对接说明
-
-本文档说明 AI 智能写作中「素材收集专家」的检索机制、内网知识库接口的对接逻辑、
-涉及的代码文件,以及常见错误的排查方式。
-
----
-
-## 1. 检索来源切换
-
-素材专家支持多种检索来源,通过**后端配置文件**统一切换,**无需改动任何代码**。
-
-配置位置:`offline-backend-20260512/backend/config.yaml`
-
-```yaml
-ai_writing:
- # 检索来源开关:internet=互联网检索(默认)/ intranet=内网知识库 / tavily / demo
- search_provider: internet
- # 以下两项仅在 search_provider=intranet 时生效
- intranet_search_url: http://10.106.57.180:7863/knowledge_base/search_docs_es
- intranet_knowledge_base: samples
-```
-
-| `search_provider` 取值 | 含义 |
-|---|---|
-| `internet` | 互联网检索(DuckDuckGo),失败时回退到 demo 演示素材。**当前默认值** |
-| `intranet` | 内网知识库检索(调用 `search_docs_es` 接口) |
-| `tavily` | Tavily 互联网检索 |
-| `demo` | 内置演示素材,仅用于本地调试 |
-
-> 内网接口联通后,把 `search_provider` 改为 `intranet` 即可切换。
-> 配置文件支持热加载(`get_app_config()` 按 mtime 自动重载),改完一般无需重启后端。
-
----
-
-## 1.5 检索关键词数量
-
-素材专家的检索词数量由**用户在写作表单中指定**(「检索关键词数量」输入框,
-范围 `1~10`,默认 `6`)。数量越多素材越全面,但检索与排序更耗时。
-
-- 该参数随 `POST /api/ai-writing/start` 的 `keyword_count` 字段传入,写入 `AIWritingState.keyword_count`。
-- `researcher_node` 用它决定让 LLM 生成多少个检索词、并发检索多少个方向
- (`EXTRACT_KEYWORDS_USER` prompt 按该数量格式化,`search_keywords = keywords[:keyword_count]`)。
-- 非法值(缺省 / 越界 / 非数字)由前后端双重兜底:后端 `_resolve_keyword_count()`
- 规整到 `[1, 10]`,越界回退默认 `6`。
-- 补充搜索(`re_search`)时用户额外添加的关键词不会被该上限截断。
-
----
-
-## 1.6 素材总量上限
-
-素材收集专家最终保留的素材**总条数**由后端配置文件统一控制,并按检索词数**均分**到每个词。
-
-配置位置:`offline-backend-20260512/backend/config.yaml` 的 `ai_writing` 段:
-
-```yaml
-ai_writing:
- # 素材总量上限,范围 1~100,非法值回退 20
- max_materials: 20
-```
-
-均分规则 —— `每个检索词的检索条数 = ⌈ max_materials / 实际检索词数 ⌉`(向上取整):
-
-| `max_materials` | 检索词数 | 每词检索条数 |
-|---|---|---|
-| 20 | 1 | 20 |
-| 20 | 2 | 10 |
-| 20 | 3 | 7(共 21,合并后截断到 20) |
-| 20 | 5 | 4 |
-
-- 各检索词按上表条数检索 → 合并去重 → 最终统一截断到 `max_materials`。
-- 向上取整是为了保证去重后素材数仍能接近上限;去重后实际条数可能略少于上限(不同词命中同一篇),属正常。
-- 与 [1.5](#15-检索关键词数量) 的关系:`keyword_count` 决定**搜几个词**,`max_materials` 决定**总共要多少条**,两者相除得到每词条数。
-- 涉及代码:`researcher.py` 的 `_resolve_max_materials()`(读配置 + 规整)与 `_per_keyword_limit()`(均分计算)。
-
-> `config.yaml` 支持热加载,改完一般无需重启后端。
-
----
-
-## 2. 内网接口规格
-
-- **地址**:`http://10.106.57.180:7863/knowledge_base/search_docs_es`
-- **方法**:`POST`
-- **请求体**:
-
- ```json
- {
- "query": "检索关键词",
- "knowledge_base_name": "samples",
- "ts": "yes"
- }
- ```
-
-- **响应体**:一个数组,每个元素结构如下:
-
- ```json
- [
- {
- "page_content": "正文片段",
- "metadata": {
- "m_title": "标题",
- "content": "内容",
- "recUuid": "原文id",
- "m_publish":"发布时间",
- "source1": "来源"
- }
- }
- ]
- ```
-
- 我们需要的字段都在 `metadata` 里。
-
-### 字段映射
-
-后端把内网响应映射为统一的 `SearchResult` 结构:
-
-| 内网响应字段 | SearchResult 字段 | 说明 |
-|--------------------------|--------------------|------|
-| `metadata.m_title` | `title` | 标题,缺省为「(无标题)」 |
-| `metadata.content` | `content` | 内容,截断到 500 字 |
-| `metadata.source1` | `source` | 来源,缺省为「内网知识库」 |
-| `metadata.m_publish` | `published_at` | 发布时间 |
-| `metadata.recUuid` | `recUuid` | 原文 id,用于前端拼接「查看原文」链接 |
-| — | `url` | 固定为空,原文地址由前端依据 `recUuid` 拼接 |
-| — | `relevance_score` | 接口不返回分值,按返回顺序赋递减分以保留 ES 排序 |
-| — | `source_type` | 固定为 `"intranet"` |
-
----
-
-## 3. 对接逻辑与涉及文件
-
-### 后端
-
-| 文件 | 作用 |
-|------|------|
-| `packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py` | **内网检索适配器** `IntranetSearchProvider`。发起 POST 请求、解析响应、映射为 `SearchResult` |
-| `packages/harness/deerflow/agents/ai_writing/search/__init__.py` | `get_search_provider()` 工厂函数,依据 `search_provider` 配置返回对应 provider |
-| `packages/harness/deerflow/agents/ai_writing/search/base.py` | `SearchResult` 数据结构 + `BaseSearchProvider` 抽象基类 |
-| `packages/harness/deerflow/agents/ai_writing/nodes/researcher.py` | 素材收集专家节点,调用 provider 检索并把结果合并、排序为素材包 |
-| `app/gateway/routers/ai_writing.py` | `_ai_writing_config()` 读取 `config.yaml` 的 `ai_writing` 段,经 `_make_config()` 注入运行时 |
-| `config.yaml` | `ai_writing` 段,检索来源开关与内网接口地址 |
-
-调用链:
-
-```
-ai_writing.py(_make_config 注入 ai_writing_config)
- → researcher_node
- → get_search_provider(ai_writing_config) # 依据 search_provider 选择
- → IntranetSearchProvider.search() # POST search_docs_es
- → SearchResult[] # 字段映射
-```
-
-### 前端
-
-| 文件 | 作用 |
-|------|------|
-| `src/core/config/index.ts` | `INTRANET_SOURCE_URL_BASE` 原文详情页地址前缀 + `buildIntranetSourceUrl()` 构造函数 |
-| `src/open-canvas/components/ai-writing/MaterialItem.tsx` | 素材卡片,`recUuid` 非空时显示「查看原文」按钮 |
-| `src/open-canvas/components/ai-writing/CompletedInterventionCard.tsx` | 历史回显的素材卡,「原文」链接同样基于 `recUuid` |
-| `src/open-canvas/api/ai-writing.ts` | `normalizeMaterial()` 把后端 snake_case 字段转为前端 camelCase(含 `recUuid`) |
-
-「查看原文」链接拼接规则:
-
-```
-INTRANET_SOURCE_URL_BASE + recUuid
-= https://chlib.b.uat.4.cn/knowlibrary/#/knowledge/mid_detail?recUuid={recUuid}
-```
-
-- 仅当素材的 `recUuid` **非空**时才显示「查看原文」按钮。
-- 点击直接 `window.open(...)` 新标签页打开,不做站内跳转。
-
----
-
-## 4. 错误处理与排查
-
-### 4.1 内网检索返回空 / 调不通
-
-**现象**:切到 `intranet` 后,素材专家时间线出现琥珀色告警「ES 波动导致检索异常」,
-下方显示真实报错原因;若完全无结果,再提示「未检索到任何素材」。
-
-`IntranetSearchProvider.search()` 对所有异常(连接失败、超时、非 2xx、JSON 解析失败)
-都做了兜底:**捕获后记录 warning 日志、把原因收集到 `provider.errors`,并返回空列表**,
-不会让整个写作流程崩溃。`researcher_node` 在检索结束后取出 `provider.errors`,
-以非致命的 `notice` 事件推送到前端时间线(标题「ES 波动导致检索异常」+ 真实原因),
-**接口报错不影响写作流程继续**。
-
-> 容错相关代码:
-> - 检索异常收集:`intranet_stub.py` / `duckduckgo_search.py` 的 `errors` 列表
-> - 告警事件发出:`researcher.py` 的 `_notice()` 与 `progress_events`
-> - 模型调用异常:`researcher.py`(检索词/排序)、`writer_outline.py` / `writer_draft.py` /
-> `editor.py` 均会发出「模型错误」事件(致命错误用 `error`,可兜底的用 `notice`)
-> - 前端展示:`TimelineStep.tsx` 渲染 `notice`(琥珀)/ `error`(红色)的标题 + 真实原因
-
-排查步骤:
-
-1. **确认网络可达**:在后端所在机器执行
- `curl -X POST http://10.106.57.180:7863/knowledge_base/search_docs_es -H "Content-Type: application/json" -d "{\"query\":\"测试\",\"knowledge_base_name\":\"samples\",\"ts\":\"yes\"}"`
-2. **查看后端日志**:搜索关键字 `intranet search failed`,会打印具体异常(连接超时 / DNS / 401 等)。
-3. **确认配置**:`config.yaml` 的 `intranet_search_url`、`intranet_knowledge_base` 是否正确。
-
-空结果时前端已有兜底:素材确认卡会弹出「未检索到任何素材,是否在无素材的情况下继续写作」,
-用户可以选择「重新搜索」或「无数据继续写作」。
-
-### 4.2 字段缺失 / 响应结构变化
-
-`search()` 对每个字段都用 `or` 兜底(如 `m_title` 缺失时回退为「(无标题)」),
-单条数据字段缺失不会导致整批失败。
-
-若内网接口改为「数组外包一层 `data`/`results`」的结构,适配器已兼容;
-若改成其它结构,需修改 `intranet_stub.py` 中解析 `items` 的逻辑。
-
-### 4.3 「查看原文」按钮不显示
-
-**原因**:该素材的 `recUuid` 为空。这是预期行为 —— 只有内网知识库素材才带 `recUuid`,
-互联网检索(`internet`/`tavily`/`demo`)的素材没有 `recUuid`,不显示「查看原文」。
-
-如需让互联网素材也能跳转,需另行调整 `MaterialItem.tsx` 的显示条件。
-
-### 4.4 原文链接打不开 / 地址不对
-
-「查看原文」地址前缀写在 `src/core/config/index.ts` 的 `INTRANET_SOURCE_URL_BASE`。
-若内网原文详情页地址有变,只改这一个常量即可。
-
-### 4.5 检索到的素材全是无关内容
-
-**现象**:素材卡片内容与写作意图完全不沾边。
-
-**排查**:检查后端日志里素材专家实际用的检索词。常见原因是模型把关键词提取
-提示词模板里的占位符(如「关键词1」「...」)当成真关键词去检索了。
-
-`researcher.py` 的 `_clean_keywords()` 已对此做防御:剔除 `关键词\d*` / `keyword\d*` /
-`...` 等占位符,全部被滤掉时回退为写作意图本身。若仍出现,检查
-`researcher_prompts.py` 的 `EXTRACT_KEYWORDS` 提示词是否又引入了易被误抄的示例占位符。
-
----
-
-## 5. 联调上线步骤
-
-1. 确认后端机器能访问 `10.106.57.180:7863`。
-2. 把 `config.yaml` 的 `ai_writing.search_provider` 改为 `intranet`。
-3. 后端配置热加载生效(或重启后端)。
-4. 进入 AI 智能写作页面,发起一次写作,观察素材收集专家是否返回内网素材。
-5. 校验素材卡片的标题/来源/时间显示正确,「查看原文」能跳到内网详情页。
-6. 如检索为空,按 [4.1](#41-内网检索返回空--调不通) 排查。
diff --git a/frontend-web/src/roundtable-planning/index.ts b/frontend-web/src/roundtable-planning/index.ts
index 020bea8d..b7e6b294 100644
--- a/frontend-web/src/roundtable-planning/index.ts
+++ b/frontend-web/src/roundtable-planning/index.ts
@@ -1,2 +1,10 @@
+/**
+ * 圆桌规划(Roundtable Planning)模块入口。
+ *
+ * 详细架构 / API 字段速查见 `frontend-web/docs/multi-agent-frontend-dev.md`。
+ * 这里只暴露两个外部使用的组件:
+ * - `RoundtablePlanningPage`:三步合一的主页面(任务理解 → 多智能体研讨 → 方案展示)。
+ * - `HighFidelityReport`:Step 3 报告视图,可单独嵌入其它面板(如未来的「方案对比」)。
+ */
export { default as RoundtablePlanningPage } from "./pages/RoundtablePlanningPage";
export { HighFidelityReport } from "./components/HighFidelityReport";
diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
index dee1b995..abecde29 100644
--- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
+++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
@@ -1,4 +1,38 @@
-import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
+/**
+ * 圆桌规划主页面(三步合一)。
+ *
+ * 这是一个约 3 600 行的单文件页面,通过 `currentStep ∈ {1,2,3}` 在同一容器内切换
+ * 三步 UI。架构 / 状态机 / SSE 终态语义请先读 `frontend-web/docs/multi-agent-frontend-dev.md`,
+ * 这里只补充阅读源码时最容易忽略的几条规则:
+ *
+ * 1) **不要并行派活**:编排循环 `runOrchestration` 内 `for (const [subName, task] of dispatched)`
+ * 是**顺序**而非并行——历史上并行触发 N 路 SSE,每路 token 级 setState 整表会卡顿;保持顺序。
+ *
+ * 2) **三段式流式气泡**:`appendStreamingDialogue` → `appendToDialogue`(仅增量)→
+ * `finalizeDialogue`(最终文本 + confidence + `streaming=false`)。三个函数都按 `id`
+ * 匹配,绝不要用数组下标。
+ *
+ * 3) **Step 1 init 防 StrictMode 双 mount**:用单调代数 `step1InitGenRef` 而不是闭包 cancelled
+ * 标志。任何 setState 完成前都要校验 `myGen === step1InitGenRef.current`。同样模式见
+ * `RecommendAgentsDialog` 的 `generationRef`。
+ *
+ * 4) **离开 Step 2 不销毁线程**:`step2InitRef`/`threadIds` 保留,允许用户回到 Step 2 继续看
+ * 完整讨论。只有「新建任务 / 加载草稿 / 推荐确认」会复位 `step2InitRef`。
+ *
+ * 5) **草稿自动保存只在前进切换时触发**(1→2、2→3),后退不保存,避免覆盖 `furthestStep`。
+ *
+ * 6) **顶栏门禁**:Step 2 需 `intentReady`;Step 3 需 `hasConsensus`(leader.dispatched===[])。
+ *
+ * 7) **Persona 抽屉**改的 model 不会立即重启编排,下一轮该席位被派活时由 `runOrchestration`
+ * 读 `agentConfigs[meta.speakerId]?.model` 传入 special。prompt / temp 当前**仅前端记录**。
+ *
+ * 8) **常量**:`DEFAULT_MODEL`、`MAX_CYCLES=8`、`COMMANDER_SEAT_ID=1`、`INTENT_READY_RE`。
+ *
+ * 调试开关(默认开启):
+ * window.__MULTI_AGENT_INIT_TIMING__ // /api/multi-agent/init 各阶段耗时
+ * window.__MULTI_AGENT_DEBUG__ // streamMultiAgent 帧计数 + lastEvent
+ */
+import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport";
@@ -858,6 +892,10 @@ export default function RoundtablePlanningPage() {
// parallel meant N concurrent SSE streams each triggering a full-list
// setState on every token delta, which made the browser stutter badly
// when the leader 派活给多个席位。改为顺序后,同一时刻只有一个 SSE 在跑。
+ //
+ // ⚠️ 不要改回 Promise.all 并行 —— 详见 multi-agent-frontend-dev.md §5.4
+ // 与 §12.4。如果未来要并行,先把 step2RoundtableDialogues 换成 Map
+ // 或基于 ref 的局部更新,再放开 N 路 SSE。
for (const [subName, task] of leaderRes.dispatched) {
if (orchestrationCancelRef.current) break;
const meta = agentNameToRole[subName.toLowerCase()];
@@ -1055,7 +1093,11 @@ export default function RoundtablePlanningPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRoundTableRunning]);
- // Adjust budget constraint dynamically
+ // Budget slider — UI demo only.
+ // 这是一段**未对接后端**的演示逻辑:滑动预算条只会本地 setBudgetLimit 并 1.5 秒后追加一条
+ // 模拟智能体口吻的气泡(根据档位选话术)。真实研讨里预算属于约束条件,应由 Step 1 的
+ // intent.summary.constraints 承载、由总控派活时透传给 sub-agent,而不是这条单滑块。
+ // 重写此功能前请同步考虑:1) 是否真的需要可调约束;2) 若需要,改成走 runOrchestration 注入新 prompt。
const handleBudgetChange = (newVal: number) => {
setBudgetLimit(newVal);
triggerToast(`💰 研商经费调优约束变更为 ${newVal} 万!各大智能体启动资源重算...`);
@@ -1098,6 +1140,10 @@ export default function RoundtablePlanningPage() {
// Save per-seat agent config overrides. We persist locally only — the next
// time the orchestration loop dispatches to this seat it will read the
// chosen model out of `agentConfigs` and pass it on the streaming call.
+ //
+ // 目前 prompt / temp 两个字段**仅前端保留**,后端尚未开放 per-agent override 接口。
+ // 当后端开放时,应在 streamMultiAgent 的 special 调用上同时传 prompt_override / temperature,
+ // 而不是改这里的本地存储结构(后者会被 useState 重置)。
const handleSaveAgentAndRecalculate = (id: number) => {
const role = roles.find(r => r.id === id);
const roleName = role?.name || "专家智能体";
@@ -2973,7 +3019,14 @@ export default function RoundtablePlanningPage() {
)}
- {/* Selected Agent Persona Config Drawer / Drawer dialog box */}
+ {/* Selected Agent Persona Config Drawer / Drawer dialog box
+ ────────────────────────────────────────────────────────
+ 点击圆桌可视化里的某个智能体头像后展开,允许覆写该席位的:
+ - System Prompt (本地记录,后端覆写未开放)
+ - 推理模型 (下一轮派活时 runOrchestration 会读 agentConfigs[id].model)
+ - Temperature (本地记录,后端覆写未开放)
+ 保存按钮 = handleSaveAgentAndRecalculate,不会立即重启编排,只更新
+ agentConfigs;真正生效在下一次该席位被 leader 派活时。 */}
{selectedAgentId && (