后台挂起
This commit is contained in:
1 parent
788118ce3d
commit
19399482e1
21 files changed
+626
-50
No files matched your search
@@ -0,0 +1,404 @@
|
||||
# 多智能体会商 Step2 多结果线方案
|
||||
|
||||
## 背景
|
||||
|
||||
当前「多智能体会商 / 多智能体规划」里,一个历史草稿只保存一份 `step2` 快照。用户从历史记录加载草稿后,如果再次点击「智能体推荐」并在弹窗里点击「进入研讨」,新的 Step2 研讨会继续写入同一份 `draft.step2`,结果是第一次生成内容会被第二次生成内容覆盖,或者页面只能展示最新一次结果。
|
||||
|
||||
产品期望是:
|
||||
|
||||
- 用户从历史记录进入已有草稿时,进入第二步后,左下角能看到第一次生成结果。
|
||||
- 用户再次点击「智能体推荐」并点击「进入研讨」时,要开启一条新的研讨结果线。
|
||||
- 第二次生成结果显示在左下角,但不能影响第一次生成结果。
|
||||
- 用户之后再次查看同一个历史草稿时,可以分别查看第一次生成结果和第二次生成结果。
|
||||
|
||||
因此,这个需求的核心不是简单增加一个展示区域,而是把 Step2 从「单结果」升级为「一个草稿下多条研讨 run」。
|
||||
|
||||
## 现状
|
||||
|
||||
相关位置:
|
||||
|
||||
- `src/roundtable-planning/api/drafts.ts`
|
||||
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx`
|
||||
- `src/roundtable-planning/hooks/useStep2Orchestration.ts`
|
||||
- `src/roundtable-planning/components/Step2Panel.tsx`
|
||||
- `src/roundtable-planning/components/RecommendAgentsDialog.tsx`
|
||||
|
||||
当前 `DraftStep2Snapshot` 结构大致是:
|
||||
|
||||
```ts
|
||||
export interface DraftStep2Snapshot {
|
||||
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: unknown[];
|
||||
lastLeaderContent: string;
|
||||
hasConsensus: boolean;
|
||||
consensusPercentage: number;
|
||||
budgetLimit: number;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
orchestrationMode?: "recommend" | "chain";
|
||||
chain?: { id: string; title: string } | null;
|
||||
}
|
||||
```
|
||||
|
||||
这意味着一个草稿只有一份:
|
||||
|
||||
- 参会智能体
|
||||
- 多智能体 thread 映射
|
||||
- 研讨气泡
|
||||
- 总控最终内容
|
||||
- 共识状态
|
||||
- 沙箱产物 stub
|
||||
- 编排模式和业务链条
|
||||
|
||||
当前 `RoundtablePlanningPage.tsx` 里的 `handleConfirmAgents` 在用户点击「进入研讨」后会:
|
||||
|
||||
1. 规范化选中的 agents。
|
||||
2. 写入 `step2.selectedAgents`。
|
||||
3. 调用 `step2.resetInitGate()`。
|
||||
4. 设置 `orchestrationMode` / `chainMeta`。
|
||||
5. 切到 `currentStep = 2`。
|
||||
|
||||
它没有先把当前 Step2 结果归档成「第一次生成结果」,也没有为新的研讨创建独立 run。因此第二次研讨天然会复用同一个 `draft.step2` 写入位置。
|
||||
|
||||
## 目标模型
|
||||
|
||||
建议引入 Step2 run 概念:一个草稿可以有多条 Step2 结果线,每条结果线保存一份完整的 Step2 快照。
|
||||
|
||||
```ts
|
||||
interface Step2RunSnapshot {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
source: "initial" | "recommend" | "chain" | "manual";
|
||||
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: unknown[];
|
||||
lastLeaderContent: string;
|
||||
hasConsensus: boolean;
|
||||
consensusPercentage: number;
|
||||
budgetLimit: number;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
orchestrationMode?: "recommend" | "chain";
|
||||
chain?: { id: string; title: string } | null;
|
||||
step3?: DraftStep3Snapshot | null;
|
||||
}
|
||||
```
|
||||
|
||||
`DraftStep2Snapshot` 保留现有字段作为当前激活 run 的镜像,同时新增 `runs` 和 `activeRunId`:
|
||||
|
||||
```ts
|
||||
export interface DraftStep2Snapshot {
|
||||
activeRunId?: string;
|
||||
runs?: Step2RunSnapshot[];
|
||||
|
||||
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: unknown[];
|
||||
lastLeaderContent: string;
|
||||
hasConsensus: boolean;
|
||||
consensusPercentage: number;
|
||||
budgetLimit: number;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
orchestrationMode?: "recommend" | "chain";
|
||||
chain?: { id: string; title: string } | null;
|
||||
}
|
||||
```
|
||||
|
||||
这样做的好处:
|
||||
|
||||
- 老代码仍然可以从 `draft.step2.step2RoundtableDialogues` 读取当前结果。
|
||||
- 新 UI 可以从 `draft.step2.runs` 展示第一次、第二次、第三次生成结果。
|
||||
- 老草稿没有 `runs` 时,可以在 hydrate 时把原来的 `step2` 包装成一条默认 run。
|
||||
- 暂时不需要改数据库表结构,因为 `step2` 本身就是 JSON 快照。
|
||||
|
||||
## 行为设计
|
||||
|
||||
### 1. 历史记录加载
|
||||
|
||||
用户点击历史记录加载草稿时:
|
||||
|
||||
1. 拉取完整草稿。
|
||||
2. 如果 `draft.step2.runs` 存在,使用 `activeRunId` 找到当前结果线。
|
||||
3. 如果 `draft.step2.runs` 不存在,但旧字段里有 `step2RoundtableDialogues`,自动包装成一条 run:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: "legacy-run",
|
||||
title: "第一次生成结果",
|
||||
createdAt: draft.updatedAt,
|
||||
source: "initial",
|
||||
...draft.step2
|
||||
}
|
||||
```
|
||||
|
||||
4. hydrate 当前 active run 到 Step2 runtime。
|
||||
5. 左下角展示 run 列表。
|
||||
|
||||
### 2. 再次点击智能体推荐进入研讨
|
||||
|
||||
用户在已有历史草稿中再次点击「智能体推荐」并确认进入研讨时:
|
||||
|
||||
1. 先把当前页面里的 Step2 状态保存为当前 active run。
|
||||
2. 创建一条新的 run:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
title: `第 ${runs.length + 1} 次生成结果`,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: result.mode,
|
||||
selectedAgents: normalizedAgents,
|
||||
threadIds: null,
|
||||
coordinatorName: null,
|
||||
step2RoundtableDialogues: [],
|
||||
lastLeaderContent: "",
|
||||
hasConsensus: false,
|
||||
consensusPercentage: 0,
|
||||
budgetLimit: DEFAULT_BUDGET,
|
||||
seatStubMessages: {},
|
||||
orchestrationMode: result.mode,
|
||||
chain: result.mode === "chain" ? result.chain ?? null : null
|
||||
}
|
||||
```
|
||||
|
||||
3. 将 `activeRunId` 切到新 run。
|
||||
4. 清空 Step2 runtime。
|
||||
5. 设置新选中的 agents / mode / chain。
|
||||
6. `resetInitGate()`,进入 Step2 后重新创建一组独立 thread。
|
||||
7. 自动保存草稿。
|
||||
|
||||
关键点:第二次 run 必须使用新的 `threadIds`,不能沿用第一次 run 的 `threadIds`,否则后端 thread 历史会混在一起。
|
||||
|
||||
### 3. 左下角结果列表
|
||||
|
||||
左下角建议展示一个「生成结果」区域:
|
||||
|
||||
- 第一次生成结果
|
||||
- 第二次生成结果
|
||||
- 第三次生成结果
|
||||
|
||||
每条展示:
|
||||
|
||||
- 标题
|
||||
- 创建时间
|
||||
- 状态:研讨中 / 已达成共识 / 未完成 / 已生成报告
|
||||
- 使用模式:智能体推荐 / 业务链条
|
||||
|
||||
点击某条结果时:
|
||||
|
||||
1. 保存当前 active run。
|
||||
2. 切换 `activeRunId`。
|
||||
3. hydrate 被点击的 run。
|
||||
4. 中间 Step2 对话区显示该 run 的研讨气泡。
|
||||
5. 如果该 run 已有 `step3`,进入 Step3 时展示对应报告。
|
||||
|
||||
### 4. Step3 结果归属
|
||||
|
||||
目前 `draft.step3` 是草稿级别的一份报告。如果只保留这一份,第二次结果生成报告后仍会覆盖第一次报告。
|
||||
|
||||
建议:
|
||||
|
||||
- `draft.step3` 继续作为当前 active run 的镜像,兼容现有 Step3 页面。
|
||||
- 每条 `Step2RunSnapshot.step3` 保存自己的报告。
|
||||
- `onReportSaved` 时同时写入当前 active run 的 `step3`。
|
||||
|
||||
这样用户切换到第一次生成结果时,可以看到第一次的 Step3 报告;切换到第二次生成结果时,可以看到第二次的 Step3 报告。
|
||||
|
||||
## 建议改动点
|
||||
|
||||
### 1. `api/drafts.ts`
|
||||
|
||||
扩展类型:
|
||||
|
||||
- 新增 `Step2RunSnapshot`。
|
||||
- `DraftStep2Snapshot` 增加 `activeRunId?: string` 和 `runs?: Step2RunSnapshot[]`。
|
||||
|
||||
同时保留旧字段,避免影响现有保存、加载和 Step3。
|
||||
|
||||
### 2. `useStep2Orchestration.ts`
|
||||
|
||||
新增或暴露两个能力:
|
||||
|
||||
- `getCurrentRunSnapshot()`:从当前 Step2 runtime 生成一条 run 快照。
|
||||
- `hydrateRunSnapshot(run)`:把某条 run 恢复到 Step2 runtime。
|
||||
|
||||
`hydrateFromDraft` 继续存在,但内部可以优先选择 active run 来 hydrate。
|
||||
|
||||
### 3. `RoundtablePlanningPage.tsx`
|
||||
|
||||
新增页面级状态:
|
||||
|
||||
```ts
|
||||
const [step2Runs, setStep2Runs] = useState<Step2RunSnapshot[]>([]);
|
||||
const [activeStep2RunId, setActiveStep2RunId] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
调整几个流程:
|
||||
|
||||
- `getStep2Snapshot` 输出 `runs` 和 `activeRunId`。
|
||||
- `hydrateStep2` 兼容旧草稿并恢复 runs。
|
||||
- `handleConfirmAgents` 在新建研讨前先归档当前 active run,再创建新 run。
|
||||
- `onReportSaved` 把 Step3 报告写回当前 active run。
|
||||
|
||||
### 4. `Step2Panel.tsx`
|
||||
|
||||
新增左下角结果列表展示。建议作为一个独立组件,避免 Step2Panel 继续变大:
|
||||
|
||||
```tsx
|
||||
<Step2RunSwitcher
|
||||
runs={step2Runs}
|
||||
activeRunId={activeStep2RunId}
|
||||
onSelect={handleSelectStep2Run}
|
||||
/>
|
||||
```
|
||||
|
||||
组件职责只做展示和选择,不直接读写 draft。
|
||||
|
||||
### 5. `Step3Panel.tsx`
|
||||
|
||||
传入当前 active run 的报告:
|
||||
|
||||
- 如果当前 run 有 `step3`,展示它。
|
||||
- 如果没有,按现有逻辑生成新报告。
|
||||
- 生成完成后通过 `onReportSaved` 写回 run。
|
||||
|
||||
## 兼容策略
|
||||
|
||||
老草稿没有 `runs` 字段时,不做数据迁移,前端加载时即时包装。
|
||||
|
||||
判断规则:
|
||||
|
||||
```ts
|
||||
function normalizeStep2Runs(step2: DraftStep2Snapshot): {
|
||||
runs: Step2RunSnapshot[];
|
||||
activeRunId: string | null;
|
||||
} {
|
||||
if (step2.runs?.length) {
|
||||
return {
|
||||
runs: step2.runs,
|
||||
activeRunId: step2.activeRunId ?? step2.runs[0].id,
|
||||
};
|
||||
}
|
||||
|
||||
const hasLegacyContent =
|
||||
step2.step2RoundtableDialogues?.length > 0 ||
|
||||
Boolean(step2.threadIds) ||
|
||||
Boolean(step2.lastLeaderContent);
|
||||
|
||||
if (!hasLegacyContent) {
|
||||
return { runs: [], activeRunId: null };
|
||||
}
|
||||
|
||||
const legacyRun: Step2RunSnapshot = {
|
||||
id: "legacy-run",
|
||||
title: "第一次生成结果",
|
||||
createdAt: new Date().toISOString(),
|
||||
source: step2.orchestrationMode ?? "initial",
|
||||
selectedAgents: step2.selectedAgents,
|
||||
threadIds: step2.threadIds,
|
||||
coordinatorName: step2.coordinatorName,
|
||||
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
||||
lastLeaderContent: step2.lastLeaderContent,
|
||||
hasConsensus: step2.hasConsensus,
|
||||
consensusPercentage: step2.consensusPercentage,
|
||||
budgetLimit: step2.budgetLimit,
|
||||
seatStubMessages: step2.seatStubMessages,
|
||||
orchestrationMode: step2.orchestrationMode,
|
||||
chain: step2.chain,
|
||||
};
|
||||
|
||||
return { runs: [legacyRun], activeRunId: legacyRun.id };
|
||||
}
|
||||
```
|
||||
|
||||
## 风险点
|
||||
|
||||
### 1. 自动保存覆盖
|
||||
|
||||
Step2 现在会在每个气泡收尾时自动保存。如果引入多 run,自动保存必须先把当前 runtime 合并回 active run,再写入 `draft.step2.runs`。
|
||||
|
||||
否则会出现 UI 切到第二次结果,但保存时仍更新第一次 run 的情况。
|
||||
|
||||
### 2. threadIds 混用
|
||||
|
||||
每条 run 必须拥有自己的 `threadIds`。新建 run 时要清空 `threadIds` 并让 Step2 重新 init。
|
||||
|
||||
如果复用旧 thread,会导致两次研讨的后端上下文互相污染。
|
||||
|
||||
### 3. Step3 报告覆盖
|
||||
|
||||
如果只保存草稿级 `step3`,多 run 的报告无法分别查看。
|
||||
|
||||
建议把报告写入当前 run,同时保留草稿级 `step3` 作为 active run 镜像。
|
||||
|
||||
### 4. 正在运行中的 run
|
||||
|
||||
如果配合后台任务能力,run 还需要带 job 信息:
|
||||
|
||||
```ts
|
||||
jobId?: string | null;
|
||||
jobStatus?: JobStatus | null;
|
||||
```
|
||||
|
||||
这样左下角可以显示「第二次生成结果正在运行」,并打开派活链路弹窗。
|
||||
|
||||
## 分阶段实现建议
|
||||
|
||||
### P0:纯前端多 run 快照
|
||||
|
||||
目标:先解决“第一次 / 第二次结果分别可查看”。
|
||||
|
||||
- 扩展 `DraftStep2Snapshot` 类型。
|
||||
- 页面维护 `step2Runs` / `activeStep2RunId`。
|
||||
- `handleConfirmAgents` 新建 run。
|
||||
- 左下角展示 run 列表。
|
||||
- 切换 run 能恢复不同 Step2 对话。
|
||||
|
||||
不改后端表结构。
|
||||
|
||||
### P1:Step3 绑定 run
|
||||
|
||||
目标:每次研讨结果对应自己的报告。
|
||||
|
||||
- `Step2RunSnapshot.step3` 保存报告。
|
||||
- `onReportSaved` 写回 active run。
|
||||
- 切换 run 时同步 Step3 显示。
|
||||
|
||||
### P2:后台任务绑定 run
|
||||
|
||||
目标:和后台挂起执行方案打通。
|
||||
|
||||
- run 增加 `jobId` / `jobStatus`。
|
||||
- 历史列表和左下角都能显示某条 run 正在执行。
|
||||
- 点击正在执行的 run 打开派活链路弹窗。
|
||||
|
||||
### P3:后端 run 表
|
||||
|
||||
如果后续一个草稿下 run 数量很多,或者需要服务端按 run 查询、删除、重命名,再考虑新增后端表:
|
||||
|
||||
- `roundtable_runs`
|
||||
- `draft_id`
|
||||
- `run_id`
|
||||
- `title`
|
||||
- `status`
|
||||
- `step2_snapshot`
|
||||
- `step3_snapshot`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
短期不建议一开始就上表,先用现有 `step2` JSON 快照落地更快。
|
||||
|
||||
## 验收清单
|
||||
|
||||
- 从历史记录加载旧草稿时,左下角出现「第一次生成结果」。
|
||||
- 点击「第一次生成结果」能看到原来的 Step2 研讨气泡。
|
||||
- 在历史草稿中重新点击「智能体推荐」并「进入研讨」后,左下角新增「第二次生成结果」。
|
||||
- 第二次研讨开始时,Step2 对话区为空或只显示新 run 初始化内容。
|
||||
- 第二次 run 使用新的 `threadIds`,不会复用第一次 run 的后端线程。
|
||||
- 切回「第一次生成结果」时,第一次的对话、共识状态、沙箱产物仍可查看。
|
||||
- 切回「第二次生成结果」时,第二次的对话继续显示。
|
||||
- 刷新页面后再次加载该历史草稿,两条结果线都还在。
|
||||
- 如果两次都生成了 Step3 报告,切换不同结果线时能看到各自对应的报告。
|
||||
Reference in new issue
Block a user