后台挂起
This commit is contained in:
1 parent
788118ce3d
commit
19399482e1
21 files changed
+625
-49
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 报告,切换不同结果线时能看到各自对应的报告。
|
||||||
@@ -18,6 +18,7 @@ import { getAvatarTypeFor } from "../lib/roundtable-constants";
|
|||||||
import {
|
import {
|
||||||
isTerminalJobStatus,
|
isTerminalJobStatus,
|
||||||
type DispatchChainNode,
|
type DispatchChainNode,
|
||||||
|
type JobDialogue,
|
||||||
type RoundtableJob,
|
type RoundtableJob,
|
||||||
type StartJobPayload,
|
type StartJobPayload,
|
||||||
} from "../lib/job-types";
|
} from "../lib/job-types";
|
||||||
@@ -65,6 +66,7 @@ function mapJob(raw: Record<string, unknown>): RoundtableJob {
|
|||||||
orchestrationMode: (raw.orchestrationMode as RoundtableJob["orchestrationMode"]) ?? "recommend",
|
orchestrationMode: (raw.orchestrationMode as RoundtableJob["orchestrationMode"]) ?? "recommend",
|
||||||
chain: (raw.chain as RoundtableJob["chain"]) ?? null,
|
chain: (raw.chain as RoundtableJob["chain"]) ?? null,
|
||||||
error: (raw.error as string | null) ?? null,
|
error: (raw.error as string | null) ?? null,
|
||||||
|
dialogues: Array.isArray(raw.dialogues) ? (raw.dialogues as JobDialogue[]) : [],
|
||||||
createdAt: (raw.createdAt as string | null) ?? null,
|
createdAt: (raw.createdAt as string | null) ?? null,
|
||||||
updatedAt: (raw.updatedAt as string | null) ?? null,
|
updatedAt: (raw.updatedAt as string | null) ?? null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export function DispatchChainJobModal({
|
|||||||
consensusPercentage={job?.consensusPercentage ?? 0}
|
consensusPercentage={job?.consensusPercentage ?? 0}
|
||||||
onView={job && onView ? () => onView(job) : undefined}
|
onView={job && onView ? () => onView(job) : undefined}
|
||||||
error={job?.error}
|
error={job?.error}
|
||||||
|
dialogues={job?.dialogues}
|
||||||
pendingClarification={job?.pendingClarification}
|
pendingClarification={job?.pendingClarification}
|
||||||
onResume={
|
onResume={
|
||||||
jobId
|
jobId
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* (business-chain.ts)。连线动画 CSS 在 `styles/roundtable-planning.css` 全局段
|
* (business-chain.ts)。连线动画 CSS 在 `styles/roundtable-planning.css` 全局段
|
||||||
* (Radix Dialog portal 到 body,不在 .roundtable-planning 作用域内,故用全局类 + 语义 token)。
|
* (Radix Dialog portal 到 body,不在 .roundtable-planning 作用域内,故用全局类 + 语义 token)。
|
||||||
*/
|
*/
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useEffect, useRef, useState } from "react";
|
||||||
import { CheckIcon, ChevronRightIcon, EyeIcon, Loader2Icon, SendIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, EyeIcon, Loader2Icon, SendIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -30,9 +30,11 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
import { getAvatar } from "./Avatars";
|
import { getAvatar } from "./Avatars";
|
||||||
import { chainTagClasses } from "../lib/business-chain";
|
import { chainTagClasses } from "../lib/business-chain";
|
||||||
|
import { getAvatarTypeFor } from "../lib/roundtable-constants";
|
||||||
import type {
|
import type {
|
||||||
ChainNodeState,
|
ChainNodeState,
|
||||||
DispatchChainNode,
|
DispatchChainNode,
|
||||||
|
JobDialogue,
|
||||||
JobPhase,
|
JobPhase,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
} from "../lib/job-types";
|
} from "../lib/job-types";
|
||||||
@@ -62,6 +64,8 @@ export interface DispatchChainModalProps {
|
|||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
/** 出错详情(status==='error')。 */
|
/** 出错详情(status==='error')。 */
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
|
/** 研讨对话记录(leader 说明 + 各席位交付)。 */
|
||||||
|
dialogues?: JobDialogue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const _NON_TERMINAL: ReadonlySet<JobStatus> = new Set<JobStatus>(["queued", "running", "awaiting_input"]);
|
const _NON_TERMINAL: ReadonlySet<JobStatus> = new Set<JobStatus>(["queued", "running", "awaiting_input"]);
|
||||||
@@ -99,8 +103,24 @@ export function DispatchChainModal({
|
|||||||
onResume,
|
onResume,
|
||||||
onCancel,
|
onCancel,
|
||||||
error,
|
error,
|
||||||
|
dialogues,
|
||||||
}: DispatchChainModalProps) {
|
}: DispatchChainModalProps) {
|
||||||
const [answer, setAnswer] = useState("");
|
const [answer, setAnswer] = useState("");
|
||||||
|
const convoRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 新对话进来时自动滚到底部。
|
||||||
|
const dialogueCount = dialogues?.length ?? 0;
|
||||||
|
useEffect(() => {
|
||||||
|
const el = convoRef.current;
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
}, [dialogueCount]);
|
||||||
|
|
||||||
|
// 对话气泡的头像配色:总控固定蓝;席位优先取流程图节点色,回退按 agentId 派生。
|
||||||
|
const avatarForDialogue = (d: JobDialogue): string => {
|
||||||
|
if (d.role === "leader") return "blue";
|
||||||
|
const node = nodes.find((n) => n.agentId === d.agentId);
|
||||||
|
return node?.avatarType || (d.agentId ? getAvatarTypeFor(d.agentId, node?.order ? node.order - 1 : 0) : "blue");
|
||||||
|
};
|
||||||
// 总控节点:初始化 / 决策派活阶段高亮
|
// 总控节点:初始化 / 决策派活阶段高亮
|
||||||
const coordinatorState: ChainNodeState =
|
const coordinatorState: ChainNodeState =
|
||||||
phase === "initializing" || phase === "leader_thinking" ? "active" : "done";
|
phase === "initializing" || phase === "leader_thinking" ? "active" : "done";
|
||||||
@@ -147,7 +167,7 @@ export function DispatchChainModal({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{/* 横向流程图(横向滚动) */}
|
{/* 横向流程图(横向滚动) */}
|
||||||
<div className="custom-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden py-10">
|
<div className="custom-scrollbar shrink-0 overflow-x-auto overflow-y-hidden py-8">
|
||||||
<div className="flex min-w-max items-start gap-0 px-6">
|
<div className="flex min-w-max items-start gap-0 px-6">
|
||||||
{/* 总控节点 */}
|
{/* 总控节点 */}
|
||||||
<FlowNode
|
<FlowNode
|
||||||
@@ -192,6 +212,28 @@ export function DispatchChainModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 研讨对话(leader 说明 + 各席位交付,真网关下即真模型真实对话) */}
|
||||||
|
{dialogues && dialogues.length > 0 && (
|
||||||
|
<div
|
||||||
|
ref={convoRef}
|
||||||
|
className="custom-scrollbar min-h-0 flex-1 overflow-y-auto border-t px-1 py-3"
|
||||||
|
>
|
||||||
|
<div className="space-y-3.5">
|
||||||
|
{dialogues.map((d) => (
|
||||||
|
<div key={d.id} className="flex gap-2.5">
|
||||||
|
<div className="shrink-0">{getAvatar(avatarForDialogue(d), "h-7 w-7")}</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-semibold text-foreground/80">{d.name}</div>
|
||||||
|
<div className="mt-0.5 whitespace-pre-wrap break-words text-sm leading-relaxed text-foreground/70">
|
||||||
|
{d.content || "…"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 出错详情 */}
|
{/* 出错详情 */}
|
||||||
{status === "error" && error && (
|
{status === "error" && error && (
|
||||||
<div className="shrink-0 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
<div className="shrink-0 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||||
|
|||||||
@@ -52,16 +52,23 @@ export function MessageBubble({
|
|||||||
<div className="flex-1 min-w-0 space-y-1">
|
<div className="flex-1 min-w-0 space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={cn("text-xs font-extrabold text-slate-900", senderNameClassName)}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"roundtable-chat-message font-extrabold text-slate-900",
|
||||||
|
senderNameClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{senderName}
|
{senderName}
|
||||||
</span>
|
</span>
|
||||||
{badge && (
|
{badge && (
|
||||||
<span className="text-[calc(9px*var(--ui-font-scale,1))] font-mono font-black uppercase text-[var(--brand-900)] bg-[var(--brand-200)] border border-red-200 px-1.5 py-0.2 rounded">
|
<span className="roundtable-chat-meta font-mono font-black uppercase text-[var(--brand-900)] bg-[var(--brand-200)] border border-red-200 px-1.5 py-0.2 rounded">
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-mono font-medium shrink-0">{time}</span>
|
<span className="roundtable-chat-meta text-slate-400 font-mono font-medium shrink-0">
|
||||||
|
{time}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +82,7 @@ export function MessageBubble({
|
|||||||
*/
|
*/
|
||||||
export function MessageLoadingPlaceholder({ label }: { label?: string }) {
|
export function MessageLoadingPlaceholder({ label }: { label?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 py-1.5 text-xs text-slate-600">
|
<div className="roundtable-chat-message flex items-center gap-2 py-1.5 text-slate-600">
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||||
<span className="font-medium">{label ?? "正在连接智能体..."}</span>
|
<span className="font-medium">{label ?? "正在连接智能体..."}</span>
|
||||||
<span className="inline-flex items-center gap-0.5">
|
<span className="inline-flex items-center gap-0.5">
|
||||||
@@ -118,10 +125,10 @@ export const sharedMarkdownComponents = {
|
|||||||
<h2 className="text-sm font-bold mt-2 mb-1">{children}</h2>
|
<h2 className="text-sm font-bold mt-2 mb-1">{children}</h2>
|
||||||
),
|
),
|
||||||
h3: ({ children }: { children?: React.ReactNode }) => (
|
h3: ({ children }: { children?: React.ReactNode }) => (
|
||||||
<h3 className="text-xs font-bold mt-1.5 mb-0.5">{children}</h3>
|
<h3 className="text-sm font-bold mt-1.5 mb-0.5">{children}</h3>
|
||||||
),
|
),
|
||||||
h4: ({ children }: { children?: React.ReactNode }) => (
|
h4: ({ children }: { children?: React.ReactNode }) => (
|
||||||
<h4 className="text-xs font-bold mt-1 mb-0.5">{children}</h4>
|
<h4 className="text-sm font-bold mt-1 mb-0.5">{children}</h4>
|
||||||
),
|
),
|
||||||
strong: ({ children }: { children?: React.ReactNode }) => (
|
strong: ({ children }: { children?: React.ReactNode }) => (
|
||||||
<strong className="font-bold text-slate-900">{children}</strong>
|
<strong className="font-bold text-slate-900">{children}</strong>
|
||||||
@@ -139,13 +146,13 @@ export const sharedMarkdownComponents = {
|
|||||||
const isBlock = /language-/.test(className ?? "");
|
const isBlock = /language-/.test(className ?? "");
|
||||||
if (isBlock) {
|
if (isBlock) {
|
||||||
return (
|
return (
|
||||||
<pre className="bg-slate-100 text-slate-800 rounded-md p-2 my-1 overflow-x-auto text-[calc(11px*var(--ui-font-scale,1))] font-mono">
|
<pre className="roundtable-chat-message bg-slate-100 text-slate-800 rounded-md p-2 my-1 overflow-x-auto font-mono">
|
||||||
<code>{children}</code>
|
<code>{children}</code>
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<code className="bg-slate-100 text-slate-800 rounded px-1 py-0.5 text-[calc(11px*var(--ui-font-scale,1))] font-mono">
|
<code className="roundtable-chat-message bg-slate-100 text-slate-800 rounded px-1 py-0.5 font-mono">
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
@@ -161,7 +168,7 @@ export const sharedMarkdownComponents = {
|
|||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
table: ({ children }: { children?: React.ReactNode }) => (
|
table: ({ children }: { children?: React.ReactNode }) => (
|
||||||
<table className="my-1 border-collapse text-[calc(11px*var(--ui-font-scale,1))]">{children}</table>
|
<table className="roundtable-chat-message my-1 border-collapse">{children}</table>
|
||||||
),
|
),
|
||||||
th: ({ children }: { children?: React.ReactNode }) => (
|
th: ({ children }: { children?: React.ReactNode }) => (
|
||||||
<th className="border border-slate-300 px-1.5 py-0.5 bg-slate-50 font-bold text-left">
|
<th className="border border-slate-300 px-1.5 py-0.5 bg-slate-50 font-bold text-left">
|
||||||
@@ -254,7 +261,7 @@ export function ReasoningBlock({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
className="inline-flex items-center gap-1.5 text-[calc(11.5px*var(--ui-font-scale,1))] text-slate-500 hover:text-slate-700 transition-colors cursor-pointer"
|
className="roundtable-chat-message inline-flex items-center gap-1.5 text-slate-500 hover:text-slate-700 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<BrainIcon className={cn("size-3.5", streaming && "text-primary animate-pulse")} />
|
<BrainIcon className={cn("size-3.5", streaming && "text-primary animate-pulse")} />
|
||||||
<span className={cn(streaming && "animate-pulse")}>{headerLabel}</span>
|
<span className={cn(streaming && "animate-pulse")}>{headerLabel}</span>
|
||||||
@@ -265,7 +272,7 @@ export function ReasoningBlock({
|
|||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md border border-slate-200 bg-slate-50/60 px-3 py-2 text-[calc(11.5px*var(--ui-font-scale,1))] text-slate-600 leading-relaxed",
|
"roundtable-chat-message rounded-md border border-slate-200 bg-slate-50/60 px-3 py-2 text-slate-600",
|
||||||
contentClassName,
|
contentClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -86,10 +86,15 @@ export function MessageStepsCard({
|
|||||||
const display = getStepDisplay(lastReasoning.key, lastReasoning.label, lastReasoning.args);
|
const display = getStepDisplay(lastReasoning.key, lastReasoning.label, lastReasoning.args);
|
||||||
const Icon = display.icon;
|
const Icon = display.icon;
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center gap-2 text-[calc(11.5px*var(--ui-font-scale,1))] text-slate-500 py-0.5", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"roundtable-chat-message flex items-center gap-2 text-slate-500 py-0.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Icon className="size-3.5 text-slate-400" />
|
<Icon className="size-3.5 text-slate-400" />
|
||||||
<span>{display.label}</span>
|
<span>{display.label}</span>
|
||||||
<span className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-mono">{lastReasoning.ts}</span>
|
<span className="roundtable-chat-meta text-slate-400 font-mono">{lastReasoning.ts}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -108,7 +113,7 @@ export function MessageStepsCard({
|
|||||||
>
|
>
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={
|
label={
|
||||||
<span className="text-[calc(11.5px*var(--ui-font-scale,1))] opacity-60">
|
<span className="roundtable-chat-message opacity-60">
|
||||||
{showAbove ? "隐藏步骤" : `查看其他 ${priorToolSteps.length} 个步骤`}
|
{showAbove ? "隐藏步骤" : `查看其他 ${priorToolSteps.length} 个步骤`}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -189,9 +194,9 @@ function ToolStep({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<span className="text-[calc(12px*var(--ui-font-scale,1))] text-slate-700">
|
<span className="roundtable-chat-message text-slate-700">
|
||||||
<span className="font-medium">{display.label}</span>
|
<span className="font-medium">{display.label}</span>
|
||||||
<span className="ml-2 text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-mono">{step.ts}</span>
|
<span className="roundtable-chat-meta ml-2 text-slate-400 font-mono">{step.ts}</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -204,10 +209,10 @@ function ReasoningStep({ step, streaming }: { step: BubbleStep; streaming: boole
|
|||||||
const display = getStepDisplay(step.key, step.label, step.args);
|
const display = getStepDisplay(step.key, step.label, step.args);
|
||||||
const Icon = display.icon;
|
const Icon = display.icon;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-[calc(11.5px*var(--ui-font-scale,1))] text-slate-500">
|
<div className="roundtable-chat-message flex items-center gap-2 text-slate-500">
|
||||||
<Icon className={cn("size-3.5", streaming ? "text-primary animate-pulse" : "text-slate-400")} />
|
<Icon className={cn("size-3.5", streaming ? "text-primary animate-pulse" : "text-slate-400")} />
|
||||||
<span>{display.label}</span>
|
<span>{display.label}</span>
|
||||||
<span className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-mono">{step.ts}</span>
|
<span className="roundtable-chat-meta text-slate-400 font-mono">{step.ts}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -351,7 +351,7 @@ function Step1Message({
|
|||||||
{isStreaming && !hasContent ? (
|
{isStreaming && !hasContent ? (
|
||||||
<MessageLoadingPlaceholder label={msg.phase} />
|
<MessageLoadingPlaceholder label={msg.phase} />
|
||||||
) : isAgent ? (
|
) : isAgent ? (
|
||||||
<div className="text-xs text-slate-700 leading-relaxed font-medium break-words space-y-2 roundtable-md">
|
<div className="roundtable-chat-message text-slate-700 font-medium break-words space-y-2">
|
||||||
{introText && <MarkdownContent>{introText}</MarkdownContent>}
|
{introText && <MarkdownContent>{introText}</MarkdownContent>}
|
||||||
{parts.summary && <IntentReadySummaryCard summary={parts.summary} />}
|
{parts.summary && <IntentReadySummaryCard summary={parts.summary} />}
|
||||||
{/*
|
{/*
|
||||||
@@ -422,7 +422,7 @@ function Step1Message({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-slate-700 leading-relaxed font-medium whitespace-pre-wrap break-words roundtable-md">
|
<div className="roundtable-chat-message text-slate-700 font-medium whitespace-pre-wrap break-words">
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -289,9 +289,9 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{pendingClarification.context && (
|
{pendingClarification.context && (
|
||||||
<p className="text-[calc(11px*var(--ui-font-scale,1))] text-amber-800 italic">{pendingClarification.context}</p>
|
<p className="roundtable-chat-message text-amber-800 italic">{pendingClarification.context}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-slate-800 leading-relaxed font-medium whitespace-pre-wrap break-words">
|
<p className="roundtable-chat-message text-slate-800 font-medium whitespace-pre-wrap break-words">
|
||||||
{pendingClarification.question}
|
{pendingClarification.question}
|
||||||
</p>
|
</p>
|
||||||
{Array.isArray(pendingClarification.options) &&
|
{Array.isArray(pendingClarification.options) &&
|
||||||
@@ -310,7 +310,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
key={`${idx}-${label}`}
|
key={`${idx}-${label}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSubmitClarification(label)}
|
onClick={() => onSubmitClarification(label)}
|
||||||
className="text-[calc(11px*var(--ui-font-scale,1))] font-bold px-3 py-1.5 rounded-full bg-white border border-amber-300 text-amber-900 hover:bg-amber-100 transition active:scale-95 cursor-pointer"
|
className="roundtable-chat-message font-bold px-3 py-1.5 rounded-full bg-white border border-amber-300 text-amber-900 hover:bg-amber-100 transition active:scale-95 cursor-pointer"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
@@ -472,7 +472,7 @@ export function Step2Message({
|
|||||||
<ReasoningBlock streaming={isStreaming}>{reasoning}</ReasoningBlock>
|
<ReasoningBlock streaming={isStreaming}>{reasoning}</ReasoningBlock>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-xs text-slate-700 leading-relaxed font-medium break-words roundtable-md">
|
<div className="roundtable-chat-message text-slate-700 font-medium break-words">
|
||||||
{isStreaming && !hasContent && <MessageLoadingPlaceholder label={msg.confidence} />}
|
{isStreaming && !hasContent && <MessageLoadingPlaceholder label={msg.confidence} />}
|
||||||
<MarkdownContent>{content}</MarkdownContent>
|
<MarkdownContent>{content}</MarkdownContent>
|
||||||
{isStreaming && <StreamingCursor />}
|
{isStreaming && <StreamingCursor />}
|
||||||
@@ -594,17 +594,17 @@ function PresentFileCard({
|
|||||||
<div className="flex min-w-0 items-start gap-2">
|
<div className="flex min-w-0 items-start gap-2">
|
||||||
<FileIcon className="mt-0.5 size-4 shrink-0 text-slate-400" />
|
<FileIcon className="mt-0.5 size-4 shrink-0 text-slate-400" />
|
||||||
<span
|
<span
|
||||||
className="min-w-0 flex-1 truncate text-[calc(12px*var(--ui-font-scale,1))] font-medium text-slate-700"
|
className="roundtable-chat-message min-w-0 flex-1 truncate font-medium text-slate-700"
|
||||||
title={filename}
|
title={filename}
|
||||||
>
|
>
|
||||||
{filename}
|
{filename}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center justify-between gap-2">
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[calc(10px*var(--ui-font-scale,1))] font-mono font-semibold text-slate-500">
|
<span className="roundtable-chat-meta rounded bg-slate-100 px-1.5 py-0.5 font-mono font-semibold text-slate-500">
|
||||||
{typeLabel}
|
{typeLabel}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400">点击预览</span>
|
<span className="roundtable-chat-meta text-slate-400">点击预览</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ export interface DispatchChainNode {
|
|||||||
lastSpokeCycle?: number;
|
lastSpokeCycle?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一条研讨对话记录(leader 说明 / 某席位交付)。 */
|
||||||
|
export interface JobDialogue {
|
||||||
|
id: number;
|
||||||
|
role: "leader" | "seat";
|
||||||
|
agentId: string | null;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
cycle: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 一条后台作业的完整快照(§2.5)——对齐后端 `RoundtableJobResponse`(camelCase)。 */
|
/** 一条后台作业的完整快照(§2.5)——对齐后端 `RoundtableJobResponse`(camelCase)。 */
|
||||||
export interface RoundtableJob {
|
export interface RoundtableJob {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,6 +69,8 @@ export interface RoundtableJob {
|
|||||||
orchestrationMode: "recommend" | "chain";
|
orchestrationMode: "recommend" | "chain";
|
||||||
chain: { id: string; title: string } | null;
|
chain: { id: string; title: string } | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
/** 研讨对话记录(leader 说明 + 各席位交付),跑完 / 进行中都可看真实内容。 */
|
||||||
|
dialogues: JobDialogue[];
|
||||||
createdAt: string | null;
|
createdAt: string | null;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function renderStepArgs(
|
|||||||
<ChainOfThoughtSearchResult>{displayName}</ChainOfThoughtSearchResult>
|
<ChainOfThoughtSearchResult>{displayName}</ChainOfThoughtSearchResult>
|
||||||
)}
|
)}
|
||||||
{task && (
|
{task && (
|
||||||
<div className="mt-1 max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-600 leading-relaxed">
|
<div className="roundtable-chat-message mt-1 max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white px-2.5 py-1 text-slate-600">
|
||||||
<MarkdownContent>{task}</MarkdownContent>
|
<MarkdownContent>{task}</MarkdownContent>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -98,7 +98,7 @@ export function renderStepArgs(
|
|||||||
case "ask_clarification": {
|
case "ask_clarification": {
|
||||||
const question = typeof args.question === "string" ? args.question : "";
|
const question = typeof args.question === "string" ? args.question : "";
|
||||||
return question ? (
|
return question ? (
|
||||||
<span className="block text-[11px] text-slate-600 line-clamp-2 mt-0.5">
|
<span className="roundtable-chat-message block text-slate-600 line-clamp-2 mt-0.5">
|
||||||
{question}
|
{question}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@@ -374,8 +374,8 @@ export default function RoundtablePlanningPage() {
|
|||||||
model: selectedModel || undefined, // 模型沿用第二步选择
|
model: selectedModel || undefined, // 模型沿用第二步选择
|
||||||
orchestrationMode,
|
orchestrationMode,
|
||||||
chain: chainMeta,
|
chain: chainMeta,
|
||||||
threadIds: step2.threadIds ?? undefined,
|
// 不传 threadIds:后台作业由真网关自建全新线程,绝不复用前端本地研讨的线程
|
||||||
coordinatorName: step2.coordinatorName ?? undefined,
|
// (那些线程可能有活跃 run,复用会撞 409)。
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -10,6 +10,19 @@
|
|||||||
--rt-primary-active: var(--brand-950, #a8071a);
|
--rt-primary-active: var(--brand-950, #a8071a);
|
||||||
--rt-primary-soft: var(--brand-200, #feeded);
|
--rt-primary-soft: var(--brand-200, #feeded);
|
||||||
--rt-page-bg: #fef6f6;
|
--rt-page-bg: #fef6f6;
|
||||||
|
/* 消息列表正文字号 —— 与右侧沙箱 CodeEditor / Markdown 预览的 text-sm 对齐 */
|
||||||
|
--roundtable-chat-font-size: var(--text-sm, 0.875rem);
|
||||||
|
--roundtable-chat-meta-font-size: var(--text-xs, 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会商对话区:气泡正文、步骤卡、Markdown 块统一继承 */
|
||||||
|
.roundtable-chat-message {
|
||||||
|
font-size: var(--roundtable-chat-font-size);
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roundtable-chat-meta {
|
||||||
|
font-size: var(--roundtable-chat-meta-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roundtable-planning ::-webkit-scrollbar-thumb {
|
.roundtable-planning ::-webkit-scrollbar-thumb {
|
||||||
|
|||||||
@@ -60,3 +60,5 @@ ELEMENT_STATE_FILE=/app/backend/.deer-flow/element-bot-token
|
|||||||
# PYTHON_TRUSTED_HOST=your-python-mirror
|
# PYTHON_TRUSTED_HOST=your-python-mirror
|
||||||
MYSQL_DATABASE_URL=mysql+asyncmy://deerflow:deerflow123@192.168.0.123:3306/deerflow?charset=utf8mb4
|
MYSQL_DATABASE_URL=mysql+asyncmy://deerflow:deerflow123@192.168.0.123:3306/deerflow?charset=utf8mb4
|
||||||
DEER_FLOW_NOTEBOOK_API_BASE=http://127.0.0.1:8003
|
DEER_FLOW_NOTEBOOK_API_BASE=http://127.0.0.1:8003
|
||||||
|
# 圆桌后台作业网关:inprocess=进程内驱动真模型;simulated=模拟(默认)
|
||||||
|
ROUNDTABLE_JOB_GATEWAY=inprocess
|
||||||
@@ -49,6 +49,9 @@ class StartParams:
|
|||||||
coordinator_name: str = "roundtable-coordinator"
|
coordinator_name: str = "roundtable-coordinator"
|
||||||
# 关联草稿:作业完成后把 step3 报告写回该草稿,使「查看报告」可用。
|
# 关联草稿:作业完成后把 step3 报告写回该草稿,使「查看报告」可用。
|
||||||
draft_id: str | None = None
|
draft_id: str | None = None
|
||||||
|
# 是否为「续跑」(resume 澄清)。首启=False → 真网关建全新线程(不复用前端线程,
|
||||||
|
# 避免与前端本地研讨的活跃 run 撞 409);续跑=True → 复用作业自己的线程。
|
||||||
|
is_resume: bool = False
|
||||||
|
|
||||||
|
|
||||||
# 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。
|
# 网关工厂:给定一次启动参数,返回一个 RoundtableGateway 实例。
|
||||||
@@ -79,9 +82,10 @@ class RoundtableJobExecutor:
|
|||||||
thread_ids = params.thread_ids or {}
|
thread_ids = params.thread_ids or {}
|
||||||
coordinator_name = params.coordinator_name
|
coordinator_name = params.coordinator_name
|
||||||
# 真网关需要先进程内建好 leader/seat 线程;模拟网关无 init_threads。
|
# 真网关需要先进程内建好 leader/seat 线程;模拟网关无 init_threads。
|
||||||
# 仅首启建线程(resume 时 params.thread_ids 已带,复用、不重建)。
|
# 首启(非 resume)始终建**全新线程** —— 绝不复用前端本地研讨的线程
|
||||||
|
# (那些线程可能有活跃 run,复用会撞 409)。resume 时复用作业自己的线程。
|
||||||
init = getattr(gateway, "init_threads", None)
|
init = getattr(gateway, "init_threads", None)
|
||||||
if init is not None and not thread_ids:
|
if init is not None and not params.is_resume:
|
||||||
thread_ids, coordinator_name = await init()
|
thread_ids, coordinator_name = await init()
|
||||||
try:
|
try:
|
||||||
await self._store.update_progress(
|
await self._store.update_progress(
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class RoundtableJobResponse(BaseModel):
|
|||||||
orchestrationMode: str = "recommend"
|
orchestrationMode: str = "recommend"
|
||||||
chain: dict[str, Any] | None = None
|
chain: dict[str, Any] | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
|
dialogues: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
createdAt: datetime | str | None = None
|
createdAt: datetime | str | None = None
|
||||||
updatedAt: datetime | str | None = None
|
updatedAt: datetime | str | None = None
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ def _to_response(row: dict[str, Any]) -> RoundtableJobResponse:
|
|||||||
orchestrationMode=row.get("orchestration_mode") or "recommend",
|
orchestrationMode=row.get("orchestration_mode") or "recommend",
|
||||||
chain=row.get("chain"),
|
chain=row.get("chain"),
|
||||||
error=row.get("error"),
|
error=row.get("error"),
|
||||||
|
dialogues=row.get("dialogues") or [],
|
||||||
createdAt=row.get("created_at"),
|
createdAt=row.get("created_at"),
|
||||||
updatedAt=row.get("updated_at"),
|
updatedAt=row.get("updated_at"),
|
||||||
)
|
)
|
||||||
@@ -282,6 +284,7 @@ async def resume_job(request: Request, job_id: str, body: ResumeJobRequest) -> R
|
|||||||
thread_ids=row.get("thread_ids"),
|
thread_ids=row.get("thread_ids"),
|
||||||
coordinator_name=row.get("coordinator_name") or "roundtable-coordinator",
|
coordinator_name=row.get("coordinator_name") or "roundtable-coordinator",
|
||||||
draft_id=row.get("draft_id"),
|
draft_id=row.get("draft_id"),
|
||||||
|
is_resume=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return _to_response(updated or row)
|
return _to_response(updated or row)
|
||||||
|
|||||||
@@ -10,19 +10,6 @@ gateway:
|
|||||||
port: 8001
|
port: 8001
|
||||||
|
|
||||||
models:
|
models:
|
||||||
- name: zai-org/GLM-5-FP8
|
|
||||||
display_name: zai-org/GLM-5-FP8
|
|
||||||
use: langchain_openai:ChatOpenAI
|
|
||||||
model: zai-org/GLM-5-FP8
|
|
||||||
api_key: modalresearch_gWu46FDpVLYZWPVEA3b_2fq8Jw2UJiKMgEm_Rxmn1SA
|
|
||||||
base_url: https://api.us-west-2.modal.direct/v1
|
|
||||||
request_timeout: 600.0
|
|
||||||
max_retries: 2
|
|
||||||
max_tokens: 4096
|
|
||||||
temperature: 1.0
|
|
||||||
supports_vision: true
|
|
||||||
supports_thinking: true
|
|
||||||
|
|
||||||
- name: deepseek-chat
|
- name: deepseek-chat
|
||||||
display_name: DeepSeek Chat
|
display_name: DeepSeek Chat
|
||||||
use: langchain_openai:ChatOpenAI
|
use: langchain_openai:ChatOpenAI
|
||||||
@@ -91,6 +78,18 @@ models:
|
|||||||
temperature: 1.0
|
temperature: 1.0
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_thinking: true
|
supports_thinking: true
|
||||||
|
- name: zai-org/GLM-5-FP8
|
||||||
|
display_name: zai-org/GLM-5-FP8
|
||||||
|
use: langchain_openai:ChatOpenAI
|
||||||
|
model: zai-org/GLM-5-FP8
|
||||||
|
api_key: modalresearch_gWu46FDpVLYZWPVEA3b_2fq8Jw2UJiKMgEm_Rxmn1SA
|
||||||
|
base_url: https://api.us-west-2.modal.direct/v1
|
||||||
|
request_timeout: 600.0
|
||||||
|
max_retries: 2
|
||||||
|
max_tokens: 4096
|
||||||
|
temperature: 1.0
|
||||||
|
supports_vision: true
|
||||||
|
supports_thinking: true
|
||||||
# 识图(图片识别)配置。
|
# 识图(图片识别)配置。
|
||||||
# - model_name:指定一个 models[] 中的模型名,作为“默认识图大模型”。view_image 工具会把
|
# - model_name:指定一个 models[] 中的模型名,作为“默认识图大模型”。view_image 工具会把
|
||||||
# 图片交给该模型识别,并把识别出的文字结果返回给主对话模型 —— 这样即使主对话模型不支持
|
# 图片交给该模型识别,并把识别出的文字结果返回给主对话模型 —— 这样即使主对话模型不支持
|
||||||
|
|||||||
+6
@@ -140,6 +140,7 @@ async def _run_recommend(
|
|||||||
deliveries: dict[str, str] = {}
|
deliveries: dict[str, str] = {}
|
||||||
last_leader = ""
|
last_leader = ""
|
||||||
message = seed_message
|
message = seed_message
|
||||||
|
name_by_id = {a.agent_id: a.name for a in agents}
|
||||||
|
|
||||||
for cycle in range(1, MAX_CYCLES + 1):
|
for cycle in range(1, MAX_CYCLES + 1):
|
||||||
await progress.leader_thinking(cycle)
|
await progress.leader_thinking(cycle)
|
||||||
@@ -151,6 +152,7 @@ async def _run_recommend(
|
|||||||
)
|
)
|
||||||
if turn.final_text:
|
if turn.final_text:
|
||||||
last_leader = turn.final_text
|
last_leader = turn.final_text
|
||||||
|
await progress.add_leader_dialogue(turn.final_text, cycle)
|
||||||
|
|
||||||
if turn.is_clarification:
|
if turn.is_clarification:
|
||||||
return _LoopOutcome("clarification", question=turn.clarification, last_leader_content=last_leader, deliveries=deliveries)
|
return _LoopOutcome("clarification", question=turn.clarification, last_leader_content=last_leader, deliveries=deliveries)
|
||||||
@@ -163,6 +165,7 @@ async def _run_recommend(
|
|||||||
await progress.seat_active(agent_id)
|
await progress.seat_active(agent_id)
|
||||||
seat = await gateway.run_seat(thread_ids=thread_ids, agent_id=agent_id, task=task, model=model)
|
seat = await gateway.run_seat(thread_ids=thread_ids, agent_id=agent_id, task=task, model=model)
|
||||||
deliveries[agent_id] = seat.final_text
|
deliveries[agent_id] = seat.final_text
|
||||||
|
await progress.add_seat_dialogue(agent_id, name_by_id.get(agent_id, agent_id), seat.final_text, cycle)
|
||||||
await progress.seat_done(agent_id, cycle)
|
await progress.seat_done(agent_id, cycle)
|
||||||
|
|
||||||
await progress.bump_consensus()
|
await progress.bump_consensus()
|
||||||
@@ -208,6 +211,7 @@ async def _run_chain(
|
|||||||
)
|
)
|
||||||
if turn.final_text:
|
if turn.final_text:
|
||||||
last_leader = turn.final_text
|
last_leader = turn.final_text
|
||||||
|
await progress.add_leader_dialogue(turn.final_text, i + 1)
|
||||||
if turn.is_clarification:
|
if turn.is_clarification:
|
||||||
return _LoopOutcome("clarification", question=turn.clarification, last_leader_content=last_leader, deliveries=deliveries)
|
return _LoopOutcome("clarification", question=turn.clarification, last_leader_content=last_leader, deliveries=deliveries)
|
||||||
|
|
||||||
@@ -217,6 +221,7 @@ async def _run_chain(
|
|||||||
await progress.seat_active(seat.agent_id)
|
await progress.seat_active(seat.agent_id)
|
||||||
seat_turn = await gateway.run_seat(thread_ids=thread_ids, agent_id=seat.agent_id, task=task, model=model)
|
seat_turn = await gateway.run_seat(thread_ids=thread_ids, agent_id=seat.agent_id, task=task, model=model)
|
||||||
deliveries[seat.agent_id] = seat_turn.final_text
|
deliveries[seat.agent_id] = seat_turn.final_text
|
||||||
|
await progress.add_seat_dialogue(seat.agent_id, seat.name, seat_turn.final_text, i + 1)
|
||||||
await progress.seat_done(seat.agent_id, i + 1)
|
await progress.seat_done(seat.agent_id, i + 1)
|
||||||
|
|
||||||
if total > 0:
|
if total > 0:
|
||||||
@@ -233,6 +238,7 @@ async def _run_chain(
|
|||||||
)
|
)
|
||||||
if closeout.final_text:
|
if closeout.final_text:
|
||||||
last_leader = closeout.final_text
|
last_leader = closeout.final_text
|
||||||
|
await progress.add_leader_dialogue(closeout.final_text, total + 1)
|
||||||
|
|
||||||
return _LoopOutcome("consensus", last_leader_content=last_leader, deliveries=deliveries)
|
return _LoopOutcome("consensus", last_leader_content=last_leader, deliveries=deliveries)
|
||||||
|
|
||||||
|
|||||||
+24
@@ -50,6 +50,9 @@ class JobProgressWriter:
|
|||||||
self._state: dict[str, str] = {a.agent_id: _PENDING for a in agents}
|
self._state: dict[str, str] = {a.agent_id: _PENDING for a in agents}
|
||||||
self._spoke_cycle: dict[str, int] = {}
|
self._spoke_cycle: dict[str, int] = {}
|
||||||
self._consensus = 0
|
self._consensus = 0
|
||||||
|
# 研讨对话记录(leader 说明 + 各席位交付),随每次落库一并写入。
|
||||||
|
self._dialogues: list[dict[str, Any]] = []
|
||||||
|
self._dlg_seq = 0
|
||||||
|
|
||||||
# ── 节点快照 ──────────────────────────────────────────────────────────
|
# ── 节点快照 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -76,6 +79,7 @@ class JobProgressWriter:
|
|||||||
"""落库(始终带最新 dispatch_chain + consensus)+ 外发事件,均 best-effort。"""
|
"""落库(始终带最新 dispatch_chain + consensus)+ 外发事件,均 best-effort。"""
|
||||||
fields.setdefault("dispatch_chain", self._nodes())
|
fields.setdefault("dispatch_chain", self._nodes())
|
||||||
fields.setdefault("consensus_percentage", self._consensus)
|
fields.setdefault("consensus_percentage", self._consensus)
|
||||||
|
fields.setdefault("dialogues", self._dialogues)
|
||||||
if self._store is not None:
|
if self._store is not None:
|
||||||
try:
|
try:
|
||||||
await self._store.update_progress(self._job_id, **fields)
|
await self._store.update_progress(self._job_id, **fields)
|
||||||
@@ -140,3 +144,23 @@ class JobProgressWriter:
|
|||||||
|
|
||||||
async def error(self, message: str) -> None:
|
async def error(self, message: str) -> None:
|
||||||
await self._persist(status="error", error=message)
|
await self._persist(status="error", error=message)
|
||||||
|
|
||||||
|
# ── 对话记录 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _add(self, role: str, agent_id: str | None, name: str, content: str, cycle: int) -> None:
|
||||||
|
self._dlg_seq += 1
|
||||||
|
self._dialogues.append(
|
||||||
|
{"id": self._dlg_seq, "role": role, "agentId": agent_id, "name": name, "content": content, "cycle": cycle}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_leader_dialogue(self, content: str, cycle: int) -> None:
|
||||||
|
"""总控一轮发言(派活说明 / 共识结论)。空内容跳过。"""
|
||||||
|
if not (content or "").strip():
|
||||||
|
return
|
||||||
|
self._add("leader", None, "总控协调", content, cycle)
|
||||||
|
await self._persist()
|
||||||
|
|
||||||
|
async def add_seat_dialogue(self, agent_id: str, name: str, content: str, cycle: int) -> None:
|
||||||
|
"""某席位一轮交付。"""
|
||||||
|
self._add("seat", agent_id, name, content or "", cycle)
|
||||||
|
await self._persist()
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
"""Add dialogues column to roundtable_jobs.
|
||||||
|
|
||||||
|
存研讨对话记录(leader 说明 + 各席位交付),使后台作业跑完 / 进行中用户都能看到
|
||||||
|
真实研讨内容(而非只有流程图进度)。LONGTEXT(对话可超 64 KB),匹配
|
||||||
|
``PortableLongText`` on ``RoundtableJobRow.dialogues``。
|
||||||
|
|
||||||
|
Revision ID: 20260605_01
|
||||||
|
Revises: 20260604_01
|
||||||
|
Create Date: 2026-06-05
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
|
||||||
|
|
||||||
|
revision = "20260605_01"
|
||||||
|
down_revision = "20260604_01"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_table(table_name: str) -> bool:
|
||||||
|
inspector = inspect(op.get_bind())
|
||||||
|
return table_name in inspector.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_column(table_name: str, column_name: str) -> bool:
|
||||||
|
if not _has_table(table_name):
|
||||||
|
return False
|
||||||
|
inspector = inspect(op.get_bind())
|
||||||
|
return any(col["name"] == column_name for col in inspector.get_columns(table_name))
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
if _has_table("roundtable_jobs") and not _has_column("roundtable_jobs", "dialogues"):
|
||||||
|
col_type = LONGTEXT() if op.get_bind().dialect.name == "mysql" else sa.Text()
|
||||||
|
op.add_column("roundtable_jobs", sa.Column("dialogues", col_type, nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
if _has_column("roundtable_jobs", "dialogues"):
|
||||||
|
op.drop_column("roundtable_jobs", "dialogues")
|
||||||
+3
@@ -55,6 +55,9 @@ class RoundtableJobRow(Base):
|
|||||||
orchestration_mode: Mapped[str] = mapped_column(String(16), nullable=False, default="recommend")
|
orchestration_mode: Mapped[str] = mapped_column(String(16), nullable=False, default="recommend")
|
||||||
# chain 模式来源链条快照:JSON object { id, title }(recommend 模式为 null).
|
# chain 模式来源链条快照:JSON object { id, title }(recommend 模式为 null).
|
||||||
chain: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
chain: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||||||
|
# 研讨对话记录:JSON array of { id, role('leader'|'seat'), agentId, name, content, cycle }。
|
||||||
|
# 让用户跑完 / 进行中都能看到真实研讨内容(leader 说明 + 各席位交付),而非只有流程图。
|
||||||
|
dialogues: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||||||
# Terminal error detail when status=error.
|
# Terminal error detail when status=error.
|
||||||
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
# 时间列必须用 BeijingDateTime(项目规范,与 ai_writing_sessions / roundtable_drafts 对齐)。
|
# 时间列必须用 BeijingDateTime(项目规范,与 ai_writing_sessions / roundtable_drafts 对齐)。
|
||||||
|
|||||||
+4
-1
@@ -55,7 +55,7 @@ class RoundtableJobRepository:
|
|||||||
for k in ("created_at", "updated_at"):
|
for k in ("created_at", "updated_at"):
|
||||||
if isinstance(d.get(k), datetime):
|
if isinstance(d.get(k), datetime):
|
||||||
d[k] = d[k].isoformat()
|
d[k] = d[k].isoformat()
|
||||||
for k in ("dispatch_chain", "thread_ids", "chain"):
|
for k in ("dispatch_chain", "thread_ids", "chain", "dialogues"):
|
||||||
d[k] = _loads(d.get(k))
|
d[k] = _loads(d.get(k))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -118,6 +118,7 @@ class RoundtableJobRepository:
|
|||||||
pending_clarification=_UNSET,
|
pending_clarification=_UNSET,
|
||||||
thread_ids=_UNSET,
|
thread_ids=_UNSET,
|
||||||
coordinator_name=_UNSET,
|
coordinator_name=_UNSET,
|
||||||
|
dialogues=_UNSET,
|
||||||
error=_UNSET,
|
error=_UNSET,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
resolved_user_id = resolve_user_id(user_id, method_name="RoundtableJobRepository.update_progress")
|
resolved_user_id = resolve_user_id(user_id, method_name="RoundtableJobRepository.update_progress")
|
||||||
@@ -145,6 +146,8 @@ class RoundtableJobRepository:
|
|||||||
row.thread_ids = _dumps(thread_ids)
|
row.thread_ids = _dumps(thread_ids)
|
||||||
if coordinator_name is not _UNSET:
|
if coordinator_name is not _UNSET:
|
||||||
row.coordinator_name = coordinator_name
|
row.coordinator_name = coordinator_name
|
||||||
|
if dialogues is not _UNSET:
|
||||||
|
row.dialogues = _dumps(dialogues)
|
||||||
if error is not _UNSET:
|
if error is not _UNSET:
|
||||||
row.error = error
|
row.error = error
|
||||||
row.updated_at = datetime.now(UTC)
|
row.updated_at = datetime.now(UTC)
|
||||||
|
|||||||
Reference in new issue
Block a user