圆桌 Step2:固定协调员单例、优化调度性能、修正研讨中状态

将 roundtable-coordinator 改为内置单例避免重复创建;legacy sync 加 TTL 减少 runs/stream 延迟;前端仅当前发言者显示研讨中。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-05-28 23:23:40 +08:00
1 parent 2371364911
commit de597b602a
9 files changed
+507 -280

No files matched your search

@@ -68,13 +68,20 @@ export interface RecommendCandidate {
* Agent ids that must never enter the candidate pool. Centralized here so
* every caller (recommend dialog, future tools) applies the same rule.
*
* - `roundtable-intent` — Step 1 clarification specialist, no role in research.
* - `roundtable-recommender`— the recommender itself, can't recommend itself.
* - `roundtable-coordinator-*` — disposable Step 2 coordinator shells (DB
* accumulates one per session); meaningless as roundtable participants.
* - `roundtable-intent` — Step 1 clarification specialist, no role in research.
* - `roundtable-recommender` — the recommender itself, can't recommend itself.
* - `roundtable-coordinator` — Step 2 coordinator singleton (2026-05 改造后固定 id,
* ensure 自动落地;它是"协调者",不能反过来作为席位被协调)。
* - `roundtable-coordinator-*` 前缀 — 历史残留:2026-05 之前每次 init 会创建
* `roundtable-coordinator-<ts36>` 形式的临时 agent,DB 仍可能保留这些遗留记录。
* 保留前缀过滤可以在管理员清理之前先把它们隐藏起来,避免出现在推荐弹窗候选池里。
*/
const EXCLUDED_AGENT_ID_PREFIX = "roundtable-coordinator-";
const EXCLUDED_AGENT_IDS = new Set<string>(["roundtable-intent", "roundtable-recommender"]);
const EXCLUDED_AGENT_IDS = new Set<string>([
"roundtable-intent",
"roundtable-recommender",
"roundtable-coordinator",
]);
/**
* Drop excluded ids from a raw `GET /api/agents` list and project each entry
@@ -833,16 +833,19 @@ export function useStep2Orchestration(
const markSpeakerStreaming = useCallback(
(speakerId: number) => {
setActiveSpeakerSet((prev) => {
if (prev.has(speakerId)) return prev;
const next = new Set(prev);
next.add(speakerId);
return next;
});
// 编排循环已经改成**顺序**派 special(详见 §5.4 / 文件顶部注释),
// 任意时刻只有一个子智能体在跑。因此这里既要把当前发言席位置为"研讨中",
// 又要把上一轮 / 历史残留下来的"研讨中"统一清成"在线",保证右侧"参与角色"
// 面板任何时刻**只有一个** sub agent 显示研讨中,与对话区实际在 stream 的
// 那一个一致。activeSpeakerSet 沿用 Set 形态(虽然实际只放 1 个)以兼容外部
// 消费方,真正的"高亮排他"靠 roles 的 status reset。
setActiveSpeakerSet(() => new Set([speakerId]));
setRoles((prev) =>
prev.map((item) =>
item.id === speakerId ? { ...item, status: "研讨中" } : item,
),
prev.map((item) => {
if (item.id === speakerId) return { ...item, status: "研讨中" };
if (item.status === "研讨中") return { ...item, status: "在线" };
return item;
}),
);
},
[setRoles],
@@ -912,7 +915,14 @@ export function useStep2Orchestration(
safetyCounter < MAX_CYCLES
) {
safetyCounter += 1;
highlightSpeaker(2);
// Leader(协调智能体)正在派活,**不应该**让任何 sub agent 显示"研讨中"——
// 协调者本身不在右侧"参与角色"面板里(COMMANDER_SEAT_ID=1 是人类主持人,
// sub agent 从 id=2 起)。历史代码这里写的是 `highlightSpeaker(2)`,会
// 错把第一个 sub agent 误标为研讨中,叠加 special 派活时另一个 sub 也被
// markSpeakerStreaming,就出现"两个 sub 同时研讨中"的视觉 bug。
// 改成 null 后:leader 跑的阶段所有 sub 都是"在线",special 跑哪个就只
// 高亮哪个。activeSpeakerId 同步置空,圆桌可视化也不会高亮错的位置。
highlightSpeaker(null);
// 总控也可能直接 write_file —— 之前只接了 special 流,导致总控写文件时沙箱不自动打开。
// 这里把 leader thread id 绑到气泡 + artifact handler 上,与 special 同样的行为。
@@ -130,8 +130,27 @@ export function nowTime(): string {
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
}
/**
* 圆桌总控协调智能体的**固定单例 id**。后端 `_roundtable_seed.COORDINATOR_AGENT_ID`
* 与本常量必须保持一致(2026-05 改造前是 `roundtable-coordinator-<ts36>` 形式,
* 每次会话生成新 id 导致 `.deer-flow/agents/` 累积大量壳目录)。
*
* 改造后:
* - 后端 ensure 机制保证磁盘上始终存在 `.deer-flow/agents/roundtable-coordinator/`;
* - 本次会话的「可调度席位列表」由后端 init 通过 human 消息注入 coordinator
* thread 历史,不再写入 SOUL,所以单例 SOUL 长期稳定;
* - 前端 `streamMultiAgent({ coordinatorName })` 等位置不需要任何改动,因为
* `coordinatorName` 仍然是一个**字符串**,只是值不再每次变化。
*/
export const COORDINATOR_AGENT_ID = "roundtable-coordinator";
/**
* 兼容性保留:历史上叫 `newCoordinatorName()`,意为"生成新的一次性 coordinator id"。
* 改造为单例后实际只返回常量;命名不再准确,但留下避免触发跨文件级联改名。
* 新代码请直接使用 {@link COORDINATOR_AGENT_ID}。
*/
export function newCoordinatorName(): string {
return `roundtable-coordinator-${Date.now().toString(36)}`;
return COORDINATOR_AGENT_ID;
}
/**