圆桌草稿持久化:localStorage 迁至后端 MySQL

新增 /api/roundtable-drafts 草稿 CRUD 与会话级推荐历史;前端 useDraftPersistence 改走 api/drafts,删除本地 drafts.ts。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-05-31 00:23:25 +08:00
1 parent 24e92c437d
commit 838cae0d2d
16 files changed
+1410 -480

No files matched your search

+65 -2
View File
@@ -20,6 +20,7 @@
- **leader 共识 = `status: []` 空数组**;澄清 = `status: "clarification"`(字符串);派活 = `status: [[name, task], ...]`。这三者**互斥不混用**。
- **后端 picks clamp 2–6,前端用户可手动加到 8**。
- **不写死任何具体模型名**:`intent / recommend / multi_agent` 三个 router 的 `model_name = str(payload.get("model") or "")`,空串会被下游 `lead_agent._resolve_model_name()` 自动回退到 `config.yaml` 的 `models[0]`(与 `/api/ai-writing` 行为一致)。内网部署只需配 `config.yaml`,前后端均无需改硬编码。
- **草稿/推荐历史持久化(2026-06)**:圆桌页的「历史草稿」从浏览器 `localStorage` 迁到**后端 MySQL**(`/api/roundtable-drafts`,按登录用户隔离);新增**会话级推荐历史**让推荐弹窗回显上次结果。两张表 `roundtable_drafts` / `roundtable_recommend_history`,启动 `create_all` 自动建表,详见 §7A。
---
@@ -73,7 +74,12 @@ offline-backend-20260512/backend/
│ ├── multi_agent.py # Step 2:init(SSE) + run/stream + 共享的 loopback 工具
│ ├── intent.py # Step 1:init + stream(import multi_agent 工具)
│ ├── recommend.py # Step 1→2:推荐 stream(import multi_agent 工具)
│ └── artifacts.py # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用)
│ ├── artifacts.py # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用)
│ └── roundtable_drafts.py # 持久化:草稿 CRUD + 推荐历史(§7A,2026-06 新增)
├── packages/harness/deerflow/persistence/roundtable_drafts/
│ ├── model.py # ORM:RoundtableDraftRow + RoundtableRecommendHistoryRow
│ ├── sql.py # RoundtableDraftRepository(CRUD + 历史)
│ └── __init__.py # make_roundtable_draft_store(sf)
├── packages/harness/deerflow/tools/builtins/
│ └── clarification_utils.py # resolve_allow_multiple + coerce_bool(Step1/Step2 共享)
└── .deer-flow/agents/
@@ -110,8 +116,15 @@ offline-backend-20260512/backend/
| GET | `/api/threads/{thread_id}/artifacts` | 3 | 列出该 thread 的 outputs 文件 |
| GET | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 预览/下载单个产出(含 `.skill/` 内文件提取) |
| PATCH | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 编辑文本类产物(白名单后缀) |
| GET | `/api/roundtable-drafts` | 持久化 | 列出当前用户草稿(轻量元数据) |
| POST | `/api/roundtable-drafts` | 持久化 | 新建草稿 |
| GET | `/api/roundtable-drafts/{id}` | 持久化 | 取草稿全量(含 step1/step2 快照) |
| PUT | `/api/roundtable-drafts/{id}` | 持久化 | 局部更新(title / furthest_step / step1 / step2) |
| DELETE | `/api/roundtable-drafts/{id}` | 持久化 | 删除草稿 |
| GET | `/api/roundtable-drafts/{id}/recommendations` | 持久化 | 列出该草稿的推荐历史(倒序) |
| POST | `/api/roundtable-drafts/{id}/recommendations` | 持久化 | 追加一次推荐记录 |
字段级请求/响应见各 step 的 API 文档;下文只写**后端实现语义**。
字段级请求/响应见各 step 的 API 文档;下文只写**后端实现语义**。圆桌草稿/推荐历史持久化见 §7A。
---
@@ -589,6 +602,53 @@ Step 3 **没有** 单独的 `multi_agent` 路由;使用既有 artifacts 能力
---
## 7A. 圆桌草稿与推荐历史持久化(`roundtable_drafts.py`,2026-06)
圆桌规划页的「历史记录」此前只存浏览器 `localStorage`(前端 `utils/drafts.ts`),换设备/清缓存即丢、大对话还会撞 5MB 配额。2026-06 迁到**后端 MySQL**,按登录用户隔离、跨设备保留;并新增**会话级推荐历史**,让推荐弹窗能回显上次推荐结果。
### 7A.1 数据模型(`deerflow.persistence.roundtable_drafts`)
仿 `user_prompts`(最简 CRUD)+ `ai_writing_sessions`(大 JSON 用 `PortableLongText` 存)两个现有模式。两张表:
- **`roundtable_drafts`**(会话主表,一条 = 一次完整圆桌会话)
- `id`(PK, str64) / `user_id`(idx) / `title`(512) / `furthest_step`(int, 1|2|3)
- `step1` / `step2`:前端 `DraftStep{1,2}Snapshot` 的 JSON,列类型 **`PortableLongText`**(MySQL `LONGTEXT`),避免 Step 2 长对话 + `seatStubMessages` 撞 `TEXT` 64KB 上限
- `created_at` / `updated_at`:**`BeijingDateTime`**(项目规范,存北京时间且 tz-aware)
- **`roundtable_recommend_history`**(推荐历史,按 `draft_id` 归属)
- `id`(PK) / `user_id`(idx) / `draft_id`(idx) / `objective`(512) / `status`(32: done|fallback|asking|error) / `model`(128)
- `rationale` / `picks`(JSON) / `candidates`(JSON 候选池快照):均 `PortableLongText`
- `created_at`:`BeijingDateTime`
JSON 列在 repo(`sql.py`)里用 `json.dumps/loads` 手动序列化(与 `ai_writing_sessions.transcript` 同法)。两个 Row 已登记进 `persistence/models/__init__.py`,**启动时 `Base.metadata.create_all` 自动建表**——新部署无需手动建表(需 mysql 账号有建库/表权限)。
### 7A.2 路由(`app/gateway/routers/roundtable_drafts.py`,前缀 `/api/roundtable-drafts`)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `` | `list_drafts` → 轻量元数据列表(不含 step 大 blob),按 `updated_at` 倒序 |
| POST | `` | `create_draft`,可带客户端 `id`(稳定 id 跨 create+update) |
| GET | `/{id}` | `get_draft` 全量;非属主 → 404 |
| PUT | `/{id}` | `update_draft`,`model_dump(exclude_unset=True)` → 只更新客户端实际发的字段 |
| DELETE | `/{id}` | 204;非属主 → 404 |
| GET | `/{id}/recommendations` | 推荐历史倒序 |
| POST | `/{id}/recommendations` | 追加一次推荐(先校验草稿属主,404 拦截越权) |
鉴权/属主:用 `_current_user_id(request)`(登录态取 `request.state.user.id`,无登录回退 `get_effective_user_id()` 的 `"default"`),与 `user_prompts` 一致;所有 repo 读写按 `user_id` 过滤,跨用户访问一律 404。store 在 `deps.py` 接为 `app.state.roundtable_draft_store`(`make_roundtable_draft_store(sf)`),`app.py` `include_router`。
### 7A.3 与前端的契约要点
- **PUT 不带 `title` 时不改标题**:前端自动保存只发 `furthest_step`/`step1`/`step2`,不发 `title`,因此**不会覆盖用户手动重命名**;标题只在 create(派生)与显式重命名(只发 title)时改。
- **推荐时机**:前端在打开推荐弹窗前先 `ensureDraft` 落地草稿拿到 `draft_id`,推荐完成后 POST 一条历史;重开弹窗 GET 历史回显最近一次,可「重新分析」再 POST。
- **字段命名**:后端 snake_case(`furthest_step`/`created_at`),前端 `api/drafts.ts` 在边界映射成 camelCase。
### 7A.4 设计取舍
- **整存 JSON 而非拆表**:step1/step2 形状宽且常变(前端 hook 里是 loose 类型),整存 JSON 避免每次改前端快照都要迁库;代价是后端不校验嵌套字段(与 `ai_writing_sessions.transcript` 同取舍)。
- **推荐历史绑定草稿而非全局**:语义是「这个会话上次推荐了什么」,故按 `draft_id` 归属;这也要求推荐前必有 `draft_id`(前端 `ensureDraft` 保证)。
- **本地 dev 走 sqlite**:`config.yaml` `database.backend` 本地默认 sqlite,走同一套 SQLAlchemy 代码,`PortableLongText/JSON` 各自降级;生产切 mysql 用同一份代码。
---
## 8. 内置席位 SOUL 设计要点
每个 `roundtable-*`(除 intent / recommender)遵循统一结构:角色定位、核心职责、输出风格(emoji + 简短结论)、边界(只产出本席位、等总控派活)。
@@ -1195,6 +1255,9 @@ client = httpx.AsyncClient(transport=transport, base_url=...)
| 推荐路由 | `offline-backend-20260512/backend/app/gateway/routers/recommend.py` |
| Step 2 路由 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` |
| Step 3 列出/下载/编辑 | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` |
| 草稿/推荐历史路由(§7A) | `offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py` |
| 草稿/推荐历史持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/` |
| store 接线 | `offline-backend-20260512/backend/app/gateway/deps.py`(`app.state.roundtable_draft_store`) |
| 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py` |
| 澄清单选/多选 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py` |
| 任务理解 Agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/` |
+42 -24
View File
@@ -14,6 +14,7 @@
- **步骤折叠卡与主聊天对齐**:思考过程 / 工具调用展示走 `MessageStepsCard`,**always-show-last-tool + 折叠 "查看其他 N 个步骤"** 的视觉与主聊天 `chats/new` 的 `MessageGroup` 一致。工具图标 / 文案映射在 `lib/step-display.tsx`,新增工具时与 `src/core/i18n/locales/zh-CN.ts` 的 `toolCalls` 字段对齐。
- **`<think>` 标签自动提取为「思考过程」**:`lib/reasoning.ts::splitInlineReasoning(text)` 把 `<think>...</think>`(含未闭合的流式中段)抽离成 `reasoning`,正文走 markdown;由 `MessageBubble.tsx::ReasoningBlock` 渲染一个折叠卡。Step 1 / Step 2 气泡和推荐弹窗的 rationale 都走这一套,与主聊天 `Reasoning` 元素的视觉对齐。
- **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。
- **草稿/历史持久化在后端(2026-06)**:草稿不再存浏览器 `localStorage`,改走后端 MySQL(`/api/roundtable-drafts`,按登录用户隔离、跨设备保留)。`api/drafts.ts` 封装 CRUD,`useDraftPersistence` 异步对接、对外接口不变。**推荐弹窗带「会话级推荐历史」**:每个草稿一串历史,重开弹窗先显示上次推荐结果,可「重新分析」重跑(详见 §5.7 / §6.2)。后端实现见 backend doc 的「圆桌草稿持久化」一节。
- **不写死模型名**:与 `/page/canvas/ai-writing` 同样的策略 —— 全局选中模型由 `selectedModel` state 维护,首次 `useModels()` 完成后初始化为 `availableModels[0].name`,顶栏「模型」下拉可切换。所有 API 调用统一传 `model: selectedModel || undefined`;后端缺省由 `lead_agent._resolve_model_name()` 回退到 `config.yaml` 的 `models[0]`。**内网部署只需在 `config.yaml` 配自家模型即可,前后端均无需改动**。
- **席位区间硬约束**:推荐弹窗要求 **2 ≤ 选中数 ≤ 8**(`MIN_SELECTED` / `MAX_SELECTED`)。
- **协调智能体单例(2026-05)**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"` 是固定常量,不再每会话生成新 id。后端 seeder 自动落地,「本次席位列表」改由后端通过 thread 历史 human 消息注入,前端无感。`newCoordinatorName()` 仅作为兼容性壳保留。
@@ -54,7 +55,8 @@ frontend-web/src/roundtable-planning/
│ └── RoundtablePlanningPage.tsx # 主页面:组合 hook + 顶栏/侧栏 + 三步路由(≈1 150 行)
├── api/
│ ├── intent.ts # Step 1:initIntent、streamIntent
│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent
│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent;**+ 推荐历史 listRecommendHistory / saveRecommendHistory**
│ ├── drafts.ts # **草稿后端 CRUD**:listDrafts / getDraft / createDraft / updateDraft / deleteDraft / deriveDraftName(替代旧 localStorage utils/drafts.ts)
│ └── multi-agent.ts # Step 2/3:initMultiAgent、streamMultiAgent、listSessionArtifacts、buildArtifactDownloadUrl
├── components/
│ ├── Avatars.tsx # 所有 SVG 头像 + getAvatar 选择器
@@ -73,14 +75,13 @@ frontend-web/src/roundtable-planning/
│ ├── useAutoScroll.ts # 滚动跟随 + 用户上滑暂停跟随
│ ├── useStep1Intent.ts # Step 1 状态机:init、send、stream、abort、INTENT_READY 同步
│ ├── useStep2Orchestration.ts # Step 2 状态机:initMultiAgent + 编排循环 + 对话流管理 + Persona
│ └── useDraftPersistence.ts # 草稿:自动保存(前进切换)/手动保存/加载/重命名/删除
│ └── useDraftPersistence.ts # 草稿(后端版):异步走 api/drafts;自动保存(前进切换)/手动保存/加载/重命名/删除/ensureDraft
├── lib/
│ ├── clarification.ts # resolveAllowMultiple(单选/多选解析),与主聊天 ClarificationCard 对齐
│ ├── reasoning.ts # splitInlineReasoning:从模型流式文本里提取 `<think>...</think>` 思考过程
│ ├── roundtable-constants.ts # 类型(RoleEntry / SelectedAgent) + 常量 + 纯工具函数(buildRoleTemplates / splitIntentReadyBlock 等)
│ └── step-display.tsx # Step 2 ChainOfThought 折叠卡图标 + 文案 + 子节点渲染
├── utils/
│ └── drafts.ts # localStorage 草稿存档(list / upsert / get / delete / rename / deriveName)
├── utils/ # (旧 drafts.ts 已删除 —— 草稿改走后端 api/drafts.ts)
└── styles/
└── roundtable-planning.css # 页面主题变量(浅红品牌色 + 深色覆盖)
```
@@ -430,19 +431,28 @@ isOrchestrating = false
- **禁用态**:`availableModels.length === 0` 或 `modelsLoading` → `disabled`,placeholder 显示「加载中…」或「未配置模型」
- **可改换为 Radix Select**:当前用原生 `<select>` 与 Persona 抽屉保持一致;如需统一样式可改为 `@/components/ui/select`,行为不变
### 5.7 草稿持久化(`utils/drafts.ts`)
### 5.7 草稿持久化(`api/drafts.ts` + 后端 MySQL,2026-06 改造)
- 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 流程逐个置「在线」
> **重大变更**:草稿从浏览器 `localStorage` 迁到**后端 MySQL**,按登录用户隔离、跨设备/会话保留。旧 `utils/drafts.ts` 已删除;`useDraftPersistence` 仍保持对外接口不变(列表项含 `name`,由后端 `title` 映射而来),主页面无需改动。后端表/路由见 backend doc「圆桌草稿持久化」。
- **API 客户端 `api/drafts.ts`**:`listDrafts()`(轻量元数据)/ `getDraft(id)`(全量 step1+step2)/ `createDraft` / `updateDraft`(partial)/ `deleteDraft` / `deriveDraftName`。snake_case↔camelCase 映射在这一层收口(后端 `furthest_step/created_at/updated_at` ↔ 前端 `furthestStep/...`)。
- **快照形状不变**:`DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`DraftStep2Snapshot.{threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages}`;以 JSON 整存进后端 `step1`/`step2`(`PortableLongText`/LONGTEXT,避免大对话超 64KB)。
- `deriveDraftName({intentReady, interactiveMessages})`:优先 `intentReady.objective`,否则第一个 user 消息,否则 `未命名草稿 <时间>`,截断 40 字。**仅在 create 时写一次**(见下方 bug 修复)。
- **列表加载**:挂载时 `useEffect` 调 `listDrafts()`;增删改后 `refreshList()` 重拉。
- **自动保存策略**:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发。
- **手动保存 / `ensureDraft`**:`handleManualSave` 走同一条保存队列;`ensureDraft(furthestStep?)` 幂等保证后端有一条对应草稿并返回 id(**推荐弹窗打开前调用**,让推荐历史有 `draft_id` 归属)。
- **加载草稿 `handleLoadDraft`**:
- 走 `getDraft(id)` **实时拉服务端最新全量**(不再读内存里可能过期的列表快照);404 → toast「草稿已不存在」并刷新列表。
- **先**置 `loadedFromDraftRef.current = true`,再 `setCurrentStep(draft.furthestStep)`(顺序不能反)。
- `setStep2InitGate(draft.furthestStep >= 2)`:曾走到 Step 2 才跳过自动 init。
- 角色面板基线:被恢复时全部「在线」,否则 init 流程逐个「在线」。
**本次随手修复的 4 个旧 localStorage 版 bug**(对接后端时一并解决):
1. **自动保存覆盖手动重命名** —— 现在所有 `updateDraft` 一律**不带 title**;标题只由 create(派生)与显式「重命名」改写。
2. **快速连续前进(1→2→3)产生重复草稿** —— 所有保存经一条**串行队列**(`saveQueueRef`)+ 同步镜像 `currentDraftIdRef`,create 完成即写 id,后续保存读到 id 走 update 而非再 create。
3. **加载读到内存旧列表快照** —— 改为 `getDraft(id)` 实时拉。
4. **localStorage 配额静默写失败 / 数据丢失** —— 迁后端后不复存在。
---
@@ -460,15 +470,22 @@ isOrchestrating = false
### 6.2 `RecommendAgentsDialog`
- 打开时管线:`listAgents() → filterRecommendCandidates() → streamRecommend()`
- **新增 `draftId` prop(2026-06)**:本次推荐归属的草稿/会话 id。由主页面在打开弹窗前 `await drafts.ensureDraft(1)` 拿到并传入(见 §6.6)。`null` 时退化为「直接推荐、不存历史」的旧行为。
- 打开时管线(`start(forceFresh=false)`):`listAgents() → filterRecommendCandidates()` → **若 `draftId` 有历史 → `listRecommendHistory()` 取最近一次直接展示(不重跑模型)**;否则 `streamRecommend()` 跑新推荐。
- 阶段 UI(`InternalState.phase`):
- `idle` → `loading_agents` → `recommending`(顶部「模型生成内容」区流式;下方推荐卡 streaming=true)→ `ready` / `fallback` / `error`
- `idle` → `loading_agents` → **`loading_history`** → **`history`**(展示上次推荐,可直接进入研讨或「重新分析」)
- 或 `loading_agents` → `recommending`(顶部「模型生成内容」流式;推荐卡 streaming=true)→ `ready` / `fallback` / `error`
- **推荐历史(会话级)**:
- 打开时若该 `draftId` 有历史,进入 `history` 态:蓝色提示条标注上次推荐时间,picks 默认勾选、rationale 回显,**不**自动重跑模型。
- 「**重新分析**」按钮(`history` 态显示该文案,其它态为「重新推荐」)→ `start(true)` 强制跑新推荐,跳过历史。
- 每次**新推荐**完成(`ready`/`fallback`)后 `saveRecommendHistory(draftId, {objective,status,model,rationale,picks,candidates})` best-effort 追加一条(失败仅告警不阻断)。
- `history`/`fallback`/`ready` 三态都允许 `onConfirm`(用户可直接采纳历史 picks)。
- **布局分两段**:
- **顶部「模型生成内容」**:`RecommenderOutput` —— 状态行(`spinner + 调度推荐 / 已连接 / 正在生成`)+ `MessageStepsCard` 步骤卡 + `ReasoningBlock`(从 `<think>` 拆出来)+ markdown rationale + 末尾 `StreamingCursor`。视觉与 Step 1/Step 2 气泡的内容区一致,参考主聊天 `chats/new`。
- **底部「推荐参会智能体」**:候选卡片(`AgentCard`),分「推荐参会 / 其他候选」两组。
- 选择约束:`2 ≤ selected ≤ 8`;底部实时显示「至少再选 N 个」「已选 X / 8」
- 失败兜底:候选为空 → error;status==="asking" 或 picks 不足 2 → `fallback`,自动用 `FALLBACK_AGENT_IDS` 与候选池交集勾选
- 反竞态:内部 `generationRef`,每次 `runRecommendation` 自增,所有 setState 都校验 `myGen === generationRef.current`,避免「重新推荐」时旧 stream 写脏 state
- 反竞态:内部 `generationRef`,每次 `start()` 自增,所有 setState(含历史读取/流式回调)都校验 `myGen === generationRef.current`,避免「重新分析」时旧 stream / 旧历史写脏 state
- `onConfirm(agentIds)` → 父组件 `handleConfirmAgents`:复位 `step2InitRef` 并 `setCurrentStep(2)`,触发 Step 2 init
### 6.3 `HighFidelityReport`(Step 3)
@@ -613,7 +630,7 @@ interface StreamMultiAgentResult {
| `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 |
| ~~`STORAGE_KEY`~~ | 已废弃 —— 草稿改存后端 `/api/roundtable-drafts`,不再用 localStorage | (旧 utils/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 |
@@ -699,8 +716,8 @@ window.__MULTI_AGENT_DEBUG__ = true; // stream 帧计数(默认开)
1. **不要在 Panel 组件里写 fetch / setState 业务流程** —— Panel 只渲染,事件透传到 hook。
2. **hook 之间不要互相 import** —— 跨 hook 的数据通过主页面的 ref / callback 桥接(参考 `buildInitialIntent` 与 `loadedFromDraftRef` 的实现)。
3. **草稿 snapshot 形状**(Step 1 的 `Step1Snapshot` / Step 2 的 `Step2Snapshot`)与 `utils/drafts.ts` 的 `DraftStep{1,2}Snapshot` 是同一份语义,改字段时两边都要改。
4. **lint/typecheck**:重构后 `pnpm typecheck` 与 `pnpm build` 必须保持与重构前同样的 0 个新增错误(预存量 50 个 pre-existing 错误来自其它模块如 `next/link` shim,与本特性无关)。
3. **草稿 snapshot 形状**(Step 1 的 `Step1Snapshot` / Step 2 的 `Step2Snapshot`)与 `api/drafts.ts` 的 `DraftStep{1,2}Snapshot` 是同一份语义,改字段时两边都要改;后端以 JSON 整存,不强约束嵌套字段。
4. **lint/typecheck**:重构后 `pnpm typecheck` 与 `pnpm build` 必须保持 0 个新增错误(预存量错误来自其它无关模块如 `strategy-components` / `next/link` shim,与本特性无关)。
### 12.7 文档协同
@@ -731,7 +748,8 @@ window.__MULTI_AGENT_DEBUG__ = true; // stream 帧计数(默认开)
| 常量/工具函数 | `frontend-web/src/roundtable-planning/lib/roundtable-constants.ts` |
| ChainOfThought 渲染 | `frontend-web/src/roundtable-planning/lib/step-display.tsx` |
| Step 1 API | `frontend-web/src/roundtable-planning/api/intent.ts` |
| 推荐 API | `frontend-web/src/roundtable-planning/api/recommend.ts` |
| 推荐 API(含推荐历史) | `frontend-web/src/roundtable-planning/api/recommend.ts` |
| 草稿 API(后端 CRUD) | `frontend-web/src/roundtable-planning/api/drafts.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` |
@@ -740,7 +758,7 @@ window.__MULTI_AGENT_DEBUG__ = true; // stream 帧计数(默认开)
| 回底按钮 | `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` |
| 后端草稿/历史路由 | `offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py` |
| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
| 样式 | `frontend-web/src/roundtable-planning/styles/roundtable-planning.css` |
| 主聊天澄清参考 | `frontend-web/src/components/workspace/messages/message-list.tsx` |