feat(roundtable): Step2 提速四件套 + 方案总结报告意图对齐
Step2 提速: - 每席位推理深度覆盖(seat_mode): 业务链条逐席位可配 flash/thinking/pro/ultra, 覆盖链条级 seatMode;前台三处 dispatch + 后台 job 透传, run_seat 用 _SEAT_MODE_PARAMS 解析 - recommend 后端批内并行: engine._run_recommend 由串行 for 改 asyncio.gather(对齐前端) - 研讨前并行取数两段式(gather_first, 业务链条 opt-in): 研讨前全席位并行只取数, 产出作为共享资料注入研讨轮(_seat_message gathered + 前台 runGatherPhase); 新增 gather_first 列(migration 20260620_02 + idempotent SQL) - 削减重复广播: suppressPreBroadcast 扩到 recommend(round>=2)/chain(i>0)/chain收口, 保留首轮背景打底,省 N 次读写大 thread 方案总结报告效果: - summary 加"意图对齐+缺口核对+防幻觉护栏": SOUL + 后端 build_summary_prompt + 前端 buildSummaryPrompt 三处对齐; 推断标注(综合推断)、缺真实信息标注待补充交给用户、绝不凭空编 Tests: test_roundtable_seat_modes / test_roundtable_gather_phase / test_roundtable_summary_prompt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9a1b674950
commit
3ab674e438
25 files changed
+985
-44
No files matched your search
@@ -31,6 +31,8 @@ export interface ChainCreatePayload {
|
||||
coordinatorPrompt?: string | null;
|
||||
/** 默认席位执行模式(flash/thinking/pro/ultra);null = 未设。 */
|
||||
seatMode?: SeatMode | null;
|
||||
/** 研讨前并行取数开关(逐条 opt-in);缺省 false。 */
|
||||
gatherFirst?: boolean;
|
||||
}
|
||||
|
||||
/** Partial update — send only what changed. */
|
||||
@@ -46,6 +48,8 @@ export interface ChainUpdatePayload {
|
||||
coordinatorPrompt?: string | null;
|
||||
/** 默认席位执行模式;send `null` to clear, omit to leave. */
|
||||
seatMode?: SeatMode | null;
|
||||
/** 研讨前并行取数开关;send true/false 改,omit 不改。 */
|
||||
gatherFirst?: boolean;
|
||||
/** 发布开关:true 发布为公共 / false 取消发布;omit 不改。仅 owner 生效。 */
|
||||
isPublic?: boolean;
|
||||
}
|
||||
@@ -76,6 +80,7 @@ interface ServerChain {
|
||||
stage_goals?: (string | null)[] | null;
|
||||
coordinator_prompt?: string | null;
|
||||
seat_mode?: string | null;
|
||||
gather_first?: boolean;
|
||||
is_public?: boolean;
|
||||
is_mine?: boolean;
|
||||
owner_name?: string | null;
|
||||
@@ -93,6 +98,7 @@ function toChain(s: ServerChain): BusinessChain {
|
||||
stageGoals: Array.isArray(s.stage_goals) ? s.stage_goals : null,
|
||||
coordinatorPrompt: s.coordinator_prompt ?? null,
|
||||
seatMode: (s.seat_mode as SeatMode | null) ?? undefined,
|
||||
gatherFirst: s.gather_first ?? false,
|
||||
isPublic: s.is_public ?? false,
|
||||
// 后端缺省时按「我的」处理(向后兼容旧响应)。
|
||||
isMine: s.is_mine ?? true,
|
||||
@@ -112,6 +118,7 @@ function fromCreate(p: ChainCreatePayload) {
|
||||
...(p.stageGoals !== undefined ? { stage_goals: p.stageGoals } : {}),
|
||||
...(p.coordinatorPrompt !== undefined ? { coordinator_prompt: p.coordinatorPrompt } : {}),
|
||||
...(p.seatMode !== undefined ? { seat_mode: p.seatMode } : {}),
|
||||
...(p.gatherFirst !== undefined ? { gather_first: p.gatherFirst } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,6 +133,7 @@ function fromUpdate(p: ChainUpdatePayload) {
|
||||
if (p.stageGoals !== undefined) out.stage_goals = p.stageGoals;
|
||||
if (p.coordinatorPrompt !== undefined) out.coordinator_prompt = p.coordinatorPrompt;
|
||||
if (p.seatMode !== undefined) out.seat_mode = p.seatMode;
|
||||
if (p.gatherFirst !== undefined) out.gather_first = p.gatherFirst;
|
||||
if (p.isPublic !== undefined) out.is_public = p.isPublic;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,13 @@ function toStartBody(p: StartJobPayload) {
|
||||
// 每席位技能识别强化映射 {agentId: bool}:沿用所选业务链条里各席位的开关;缺省 {}。
|
||||
// 后端 run_seat 按 agentId 查表,与各席位 agent 自身开关取 OR。
|
||||
seat_skill_directives: p.seatSkillDirectives ?? {},
|
||||
// 每席位推理深度覆盖映射 {agentId: SeatMode|null}:沿用所选业务链条里各席位单独配的推理深度;
|
||||
// 后端 run_seat 按 agentId 查表,覆盖优先、否则跟随作业级 seatMode(thinking/reasoning/subagent)。
|
||||
seat_modes: Object.fromEntries(
|
||||
Object.entries(p.seatModes ?? {}).filter(([, v]) => v != null) as [string, string][],
|
||||
),
|
||||
// 研讨前并行取数开关(来源链条 gatherFirst):true = 后台研讨前先并行取数再研讨。缺省 false。
|
||||
gather_first: p.gatherFirst ?? false,
|
||||
...(seat
|
||||
? {
|
||||
thinking_enabled: seat.thinking_enabled,
|
||||
|
||||
@@ -153,19 +153,22 @@ export function BusinessChainPicker({
|
||||
// 席位「会商技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
// 席位「推理深度覆盖」(链条配置 seat_mode):按 agent_id 取,null/缺省 = 跟随链条级 seatMode。
|
||||
const seatModeById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.seat_mode ?? null]));
|
||||
const seatModeOf = (id: string) => seatModeById.get(id) ?? null;
|
||||
|
||||
// 链条带分层、过滤存活后仍并行 → dag 编排(携带快速/沙箱子模式);否则退回线性 chain。
|
||||
if (dagPlan) {
|
||||
// dag:席位顺序按 stage 展平(与 plan 一致)。
|
||||
const agents = dagPlan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id) }));
|
||||
onConfirm({ agents, mode: "dag", chain: chainMeta, plan: dagPlan, dagExecMode });
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), seatMode: seatModeOf(id) }));
|
||||
onConfirm({ agents, mode: "dag", chain: chainMeta, plan: dagPlan, dagExecMode, gatherFirst: selectedChain.gatherFirst ?? false });
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id) }));
|
||||
onConfirm({ agents, mode: "chain", chain: chainMeta });
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
onConfirm({ agents, mode: "chain", chain: chainMeta, gatherFirst: selectedChain.gatherFirst ?? false });
|
||||
};
|
||||
|
||||
// 整片脱敏:链条卡片区(链条名/描述/席位智能体名)。输入框不受影响(Hook 跳过)。
|
||||
|
||||
@@ -285,6 +285,20 @@ export interface UseStep2OrchestrationOptions {
|
||||
* OR。recommend / 未选链条 → 空 {}(各席位仅靠其自身开关)。
|
||||
*/
|
||||
seatSkillDirectives?: Record<string, boolean>;
|
||||
/**
|
||||
* 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位的配置)。
|
||||
* 派活该 special 席位时按 agentId 查表,**席位覆盖优先、否则跟随链条级 `selectedMode`**:
|
||||
* `seatModeToRequestFields(seatModes[agentId] ?? selectedMode)`。模型卡顿时把次要席位降到
|
||||
* `flash` 即可大幅提速。recommend / 未选链条 / 未覆盖 → 跟随 `selectedMode`。
|
||||
*/
|
||||
seatModes?: Record<string, SeatMode | null>;
|
||||
/**
|
||||
* 「研讨前并行取数」开关(来源业务链条的 gatherFirst,逐条 opt-in,缺省 false)。true = 正式研讨
|
||||
* 前先对**全席位并行**跑一轮「只取数」(各席位只调技能取数、不研讨),产出作为共享资料(peerDeliveries)
|
||||
* 注入后续研讨轮,把最慢的技能检索前置并行、给(尤其 chain 串行的)研讨段提速。每轮研讨只在**本轮
|
||||
* 研讨真正启动时**取数一次(resetInitGate 复位后下一轮再取),澄清续跑/干预不重复取数。
|
||||
*/
|
||||
gatherFirst?: boolean;
|
||||
/**
|
||||
* 编排模式:
|
||||
* - `recommend`(默认)—— 总控自由派活循环(`runOrchestration`);
|
||||
@@ -441,6 +455,8 @@ export function useStep2Orchestration(
|
||||
selectedModel,
|
||||
selectedMode,
|
||||
seatSkillDirectives = {},
|
||||
seatModes = {},
|
||||
gatherFirst = false,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
dagExecMode = "fast",
|
||||
@@ -576,6 +592,14 @@ export function useStep2Orchestration(
|
||||
const selectedModeRef = useRef<SeatMode>(selectedMode);
|
||||
// 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。
|
||||
const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives);
|
||||
// 每席位推理深度覆盖映射(所选业务链条各席位 seatMode)打到 ref,席位调用按 agentId 读最新值;
|
||||
// 覆盖优先、否则跟随 selectedModeRef。
|
||||
const seatModesRef = useRef<Record<string, SeatMode | null>>(seatModes);
|
||||
// 「研讨前并行取数」开关 + 本轮取数产物({name,content}[])+ 本轮是否已取过的闸门。取数产物作为
|
||||
// peerDeliveries 注入研讨轮席位;闸门保证一轮研讨只取一次(resetInitGate / resetStep2State 复位)。
|
||||
const gatherFirstRef = useRef<boolean>(gatherFirst);
|
||||
const gatheredDeliveriesRef = useRef<Array<{ name: string; content: string }>>([]);
|
||||
const gatherPhaseDoneRef = useRef<boolean>(false);
|
||||
const agentConfigsRef = useRef<Record<number, AgentConfig>>(agentConfigs);
|
||||
const agentNameToRoleRef = useRef(agentNameToRole);
|
||||
const threadIdsRef = useRef<ThreadIdMap | null>(threadIds);
|
||||
@@ -618,6 +642,12 @@ export function useStep2Orchestration(
|
||||
useEffect(() => {
|
||||
seatSkillDirectivesRef.current = seatSkillDirectives;
|
||||
}, [seatSkillDirectives]);
|
||||
useEffect(() => {
|
||||
seatModesRef.current = seatModes;
|
||||
}, [seatModes]);
|
||||
useEffect(() => {
|
||||
gatherFirstRef.current = gatherFirst;
|
||||
}, [gatherFirst]);
|
||||
useEffect(() => {
|
||||
agentConfigsRef.current = agentConfigs;
|
||||
}, [agentConfigs]);
|
||||
@@ -1585,7 +1615,8 @@ export function useStep2Orchestration(
|
||||
"③ 风险 / 注意事项(如有);④ 与其它席位的依赖 / 承接关系(如有)。";
|
||||
// 优化4:跨轮累积各席位**完整交付**,作为 peerDeliveries 传给后续席位的 run
|
||||
// (后端注入 context,席位用 read_peer_delivery 按需取全文,不进 prompt 不耗 token)。
|
||||
const priorDeliveries: Array<{ name: string; content: string }> = [];
|
||||
// 研讨前并行取数(gatherFirst)的产物作为初始 peerDeliveries 注入:研讨席位开局即带共享资料。
|
||||
const priorDeliveries: Array<{ name: string; content: string }> = [...gatheredDeliveriesRef.current];
|
||||
|
||||
// 共享的「单席位交付执行」——串行/并行分支共用。含**失败自动重试一次**(优化5,
|
||||
// 对齐 Step3 自愈:网络错误 / 502 重试,abort 不重试)。返回交付正文(aborted → 空)。
|
||||
@@ -1613,7 +1644,8 @@ export function useStep2Orchestration(
|
||||
peerDeliveries: priorDeliveries,
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式(快速/思考/专业问答/多智能体问答)→ thinking/reasoning/subagent。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 席位覆盖优先(业务链条里给该席位单独配的推理深度),否则跟随链条级 selectedMode。
|
||||
...seatModeToRequestFields(seatModesRef.current[a.agentId] ?? selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
|
||||
// agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[a.agentId] ?? false,
|
||||
@@ -1698,6 +1730,11 @@ export function useStep2Orchestration(
|
||||
// 第 2 轮起(已有席位交付)才是「综合轮」:让后端注入各席位完整交付供总控收口。
|
||||
// 首轮纯派活、无交付,传 false 保持上下文精简。
|
||||
synthesisMode: safetyCounter > 1,
|
||||
// 削减重复广播(2026-06-20):首轮(safetyCounter==1)保留预广播——它把任务背景播进各席位
|
||||
// thread,为席位打底;第 2 轮起的续轮/综合广播是「总控收到:<续轮 prompt>」纯噪声 + N 次
|
||||
// 读写巨大席位 thread 的纯浪费(还会让弱模型从历史模仿派活),故 suppress。席位的前序交付
|
||||
// 另经「席位交付广播」(broadcastSeatDeliveryToOthers)送达,不受影响。
|
||||
suppressPreBroadcast: safetyCounter > 1,
|
||||
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
|
||||
files: takeOnce(pendingInjectFilesRef),
|
||||
model: selectedModelRef.current || undefined,
|
||||
@@ -1969,6 +2006,10 @@ export function useStep2Orchestration(
|
||||
dAgentThreadId: ids,
|
||||
agentName: coord,
|
||||
newMessage: leaderTask,
|
||||
// 削减重复广播(2026-06-20):首个席位(i===0)保留预广播为其打底任务背景;从第 2 个席位起
|
||||
// suppress——后续席位的前序交付已由「席位交付广播」送达其 thread,总控派活 prompt 再播一遍
|
||||
// 是 N 次读写巨大 thread 的纯浪费(还会让弱模型模仿派活)。
|
||||
suppressPreBroadcast: i > 0,
|
||||
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
|
||||
files: takeOnce(pendingInjectFilesRef),
|
||||
model: selectedModelRef.current || undefined,
|
||||
@@ -2043,9 +2084,13 @@ export function useStep2Orchestration(
|
||||
agentName: seat.agent_id,
|
||||
displayName: seatName,
|
||||
newMessage: task,
|
||||
// 研讨前并行取数(gatherFirst)的产物作为共享资料注入:chain 席位也能直接引用、少重复检索。
|
||||
// chain 串行靠 thread 广播看前序交付;取数资料这里另经 peerDeliveries 一并带上。
|
||||
peerDeliveries: gatheredDeliveriesRef.current,
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式 → thinking/reasoning/subagent(与推荐/DAG 路径一致)。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 席位覆盖优先(业务链条里给该席位单独配的推理深度),否则跟随链条级 selectedMode。
|
||||
...seatModeToRequestFields(seatModesRef.current[seat.agent_id] ?? selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
|
||||
// agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[seat.agent_id] ?? false,
|
||||
@@ -2109,6 +2154,8 @@ export function useStep2Orchestration(
|
||||
"各链条席位已按顺序完成发言,请你综合所有席位的交付,输出最终方案与结论。不要再派活、不要再向任何席位分配任务。",
|
||||
// 链条收口就是综合轮:让后端注入各席位完整交付正文。
|
||||
synthesisMode: true,
|
||||
// 削减重复广播:收口轮不派活、研讨已结束,把总控收口 prompt 播进席位 thread 纯浪费,suppress。
|
||||
suppressPreBroadcast: true,
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: closeoutAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(closeoutBubbleId, text),
|
||||
@@ -2207,7 +2254,8 @@ export function useStep2Orchestration(
|
||||
let finishedAllStages = true;
|
||||
// 跨 stage 累积的**完整交付** {name, content}[] —— 注入下一 stage 席位的 run context
|
||||
// (peerDeliveries),供 read_peer_delivery 工具按需取全文(§12.9 传摘要按需传全文)。
|
||||
const priorDeliveries: Array<{ name: string; content: string }> = [];
|
||||
// 研讨前并行取数(gatherFirst)的产物作为初始注入:首个 stage 席位开局即带共享资料。
|
||||
const priorDeliveries: Array<{ name: string; content: string }> = [...gatheredDeliveriesRef.current];
|
||||
|
||||
// Phase 7:初始化 DAG 流程图节点状态 —— 计划内所有席位置 pending,执行过程中逐个点亮。
|
||||
const markDagNode = (agentId: string, state: ChainNodeState) =>
|
||||
@@ -2388,7 +2436,8 @@ export function useStep2Orchestration(
|
||||
peerDeliveries: priorDeliveries,
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
// 席位执行模式 → thinking/reasoning/subagent(与推荐/链条路径一致)。
|
||||
...seatModeToRequestFields(selectedModeRef.current),
|
||||
// 席位覆盖优先(业务链条里给该席位单独配的推理深度),否则跟随链条级 selectedMode。
|
||||
...seatModeToRequestFields(seatModesRef.current[agentId] ?? selectedModeRef.current),
|
||||
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该
|
||||
// 席位 agent 自身开关取 OR。
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[agentId] ?? false,
|
||||
@@ -2516,10 +2565,131 @@ export function useStep2Orchestration(
|
||||
],
|
||||
);
|
||||
|
||||
// 研讨前「并行取数」阶段:对**全席位并行**跑一轮「只取数、不研讨」,产出存进 gatheredDeliveriesRef
|
||||
// 作为共享资料注入后续研讨轮(recommend/dag 经 priorDeliveries、chain 经 peerDeliveries)。复用
|
||||
// recommend 同款并行批显示。每轮研讨只取一次(由 runActiveOrchestration 的 gatherPhaseDoneRef 闸门控制)。
|
||||
const runGatherPhase = useCallback(
|
||||
async (ids: ThreadIdMap): Promise<void> => {
|
||||
const agents = selectedAgentsRef.current;
|
||||
if (agents.length === 0) return;
|
||||
// 与后端 engine._GATHER_TASK 同义:只取数、不研讨、不下结论。
|
||||
const GATHER_TASK =
|
||||
"【资料收集阶段——只取数、不研讨】\n现在是正式研讨**之前**的并行资料收集阶段。请你**只负责把与本次任务、" +
|
||||
"与你这个席位职责相关的真实信息/数据收集、整理出来**(优先用你配置的技能去检索/获取真实信息,没有技能可凭专业知识梳理)," +
|
||||
"为后续正式研讨打底。\n**这一步不要做分析评估、不要给方案、不要下结论**——把判断与方案留到研讨阶段。" +
|
||||
"请把你收集到的关键信息整理成条理清晰的事实/要点清单输出,**不要向用户提问或等待确认**。";
|
||||
const seatPrep = agents.map((a) => {
|
||||
const meta = agentNameToRoleRef.current[a.agent_id.toLowerCase()];
|
||||
const seatName = meta?.displayName ?? a.name;
|
||||
const avatarType = meta?.avatarType ?? "blue";
|
||||
const seatThreadId = resolveSeatThreadId(ids, a.agent_id);
|
||||
const bubbleId = appendStreamingDialogue({
|
||||
sender: seatName,
|
||||
avatarType,
|
||||
initialContent: "",
|
||||
confidence: "并行取数中",
|
||||
agentThreadId: seatThreadId,
|
||||
});
|
||||
return { agentId: a.agent_id, seatName, avatarType, seatThreadId, bubbleId, meta };
|
||||
});
|
||||
markParallelSpeakers(
|
||||
seatPrep.map((p) => p.meta?.speakerId).filter((v): v is number => typeof v === "number"),
|
||||
);
|
||||
setParallelBatch(
|
||||
seatPrep.map((p) => ({
|
||||
agentId: p.agentId,
|
||||
displayName: p.seatName,
|
||||
avatarType: p.avatarType,
|
||||
status: "running" as const,
|
||||
bubbleId: p.bubbleId,
|
||||
})),
|
||||
);
|
||||
setSelectedParallelSeats(seatPrep.slice(0, 1).map((p) => p.agentId));
|
||||
const markBatchStatus = (agentId: string, status: ParallelSeatView["status"]) =>
|
||||
setParallelBatch((prev) => prev.map((p) => (p.agentId === agentId ? { ...p, status } : p)));
|
||||
|
||||
const results = await Promise.all(
|
||||
seatPrep.map(async ({ agentId, seatName, seatThreadId, bubbleId, meta }) => {
|
||||
const perSeatModel = meta ? agentConfigsRef.current[meta.speakerId]?.model : undefined;
|
||||
const subAborter = new AbortController();
|
||||
activeAbortersRef.current.add(subAborter);
|
||||
try {
|
||||
const specRes = await streamMultiAgent({
|
||||
agentType: "special",
|
||||
dAgentThreadId: ids,
|
||||
agentName: agentId,
|
||||
displayName: seatName,
|
||||
newMessage: GATHER_TASK,
|
||||
model: perSeatModel || selectedModelRef.current || undefined,
|
||||
...seatModeToRequestFields(seatModesRef.current[agentId] ?? selectedModeRef.current),
|
||||
seatSkillDirective: seatSkillDirectivesRef.current[agentId] ?? false,
|
||||
signal: subAborter.signal,
|
||||
onTextDelta: (text) => ingestParallelDelta(bubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
const label = phaseLabel(phase, "special", toolName);
|
||||
setDialogueConfidence(bubbleId, label);
|
||||
appendStepIfNew(bubbleId, label, `${phase}:${toolName ?? ""}`, args);
|
||||
handleArtifactToolPhase(phase, toolName, args, seatThreadId);
|
||||
},
|
||||
onUpstreamFrame: (frame) => {
|
||||
handleUpstreamFrame(frame, seatThreadId);
|
||||
ingestSubagentTaskFrame(frame, bubbleId);
|
||||
},
|
||||
});
|
||||
activeAbortersRef.current.delete(subAborter);
|
||||
finalizeDialogue(bubbleId, specRes.content, "资料收集");
|
||||
markBatchStatus(agentId, "done");
|
||||
if (meta) markSpeakerIdle(meta.speakerId);
|
||||
parallelStreamRef.current.delete(bubbleId);
|
||||
return specRes.content?.trim() ? { name: seatName, content: specRes.content } : null;
|
||||
} catch (err) {
|
||||
activeAbortersRef.current.delete(subAborter);
|
||||
// 取数失败不阻断研讨:该席位无资料即可,研讨阶段它仍可自行检索。
|
||||
finalizeDialogue(bubbleId, `(取数失败) ${(err as Error).message}`, "取数失败");
|
||||
markBatchStatus(agentId, "done");
|
||||
parallelStreamRef.current.delete(bubbleId);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
setParallelBatch([]);
|
||||
setSelectedParallelSeats([]);
|
||||
resetParallelStreams();
|
||||
gatheredDeliveriesRef.current = results.filter(
|
||||
(r): r is { name: string; content: string } => !!r,
|
||||
);
|
||||
},
|
||||
[
|
||||
appendStreamingDialogue,
|
||||
finalizeDialogue,
|
||||
ingestParallelDelta,
|
||||
resetParallelStreams,
|
||||
markParallelSpeakers,
|
||||
markSpeakerIdle,
|
||||
resolveSeatThreadId,
|
||||
handleArtifactToolPhase,
|
||||
handleUpstreamFrame,
|
||||
ingestSubagentTaskFrame,
|
||||
setDialogueConfidence,
|
||||
appendStepIfNew,
|
||||
],
|
||||
);
|
||||
|
||||
// 按当前模式分发到对应编排函数。init / 恢复 / 干预 / 澄清回复统一走这里。
|
||||
const runActiveOrchestration = useCallback(
|
||||
(seedMessage: string, ids: ThreadIdMap, coord: string) => {
|
||||
async (seedMessage: string, ids: ThreadIdMap, coord: string) => {
|
||||
const mode = orchestrationModeRef.current;
|
||||
// 研讨前并行取数(opt-in,每轮一次):先把取数产物准备好,再分发到具体编排(研讨轮注入资料)。
|
||||
// gatherPhaseDoneRef 闸门保证澄清续跑 / 干预重入不重复取数;置位在 await 前,避免并发重入双跑。
|
||||
if (gatherFirstRef.current && !gatherPhaseDoneRef.current) {
|
||||
gatherPhaseDoneRef.current = true;
|
||||
try {
|
||||
await runGatherPhase(ids);
|
||||
} catch (err) {
|
||||
console.warn("[roundtable] 研讨前并行取数阶段失败,跳过取数继续研讨:", err);
|
||||
}
|
||||
}
|
||||
// dag:**只在计划尚未跑过时**执行整张 DAG;一旦跑过(用户干预 / 恢复 / 澄清回复),
|
||||
// 改走 leader 自由派活循环,由总控接着派活,而不是把 DAG 从头重跑一遍。
|
||||
if (
|
||||
@@ -2534,7 +2704,7 @@ export function useStep2Orchestration(
|
||||
}
|
||||
return runOrchestration(seedMessage, ids, coord);
|
||||
},
|
||||
[runOrchestration, runChainOrchestration, runDagOrchestration],
|
||||
[runGatherPhase, runOrchestration, runChainOrchestration, runDagOrchestration],
|
||||
);
|
||||
|
||||
// ─── Effects ─────────────────────────────────────────────────────
|
||||
@@ -2850,6 +3020,9 @@ export function useStep2Orchestration(
|
||||
step2InitRef.current = false;
|
||||
// 新一轮研讨:允许 DAG 计划重新执行一遍。
|
||||
dagPlanExecutedRef.current = false;
|
||||
// 新一轮研讨:允许「研讨前并行取数」再取一次(清掉上一轮取数产物 + 闸门)。
|
||||
gatherPhaseDoneRef.current = false;
|
||||
gatheredDeliveriesRef.current = [];
|
||||
setInitNonce((n) => n + 1);
|
||||
}, []);
|
||||
|
||||
@@ -2968,6 +3141,8 @@ export function useStep2Orchestration(
|
||||
hasAutoOpenedRef.current = new Set();
|
||||
pendingInjectPromptRef.current = null;
|
||||
dagPlanExecutedRef.current = false;
|
||||
gatherPhaseDoneRef.current = false;
|
||||
gatheredDeliveriesRef.current = [];
|
||||
deselectArtifact();
|
||||
step2InitRef.current = false;
|
||||
}, [abortAllInFlight, deselectArtifact, setArtifacts, resetParallelStreams]);
|
||||
|
||||
@@ -315,6 +315,16 @@ function buildSummaryPrompt(opts: {
|
||||
}
|
||||
parts.push("");
|
||||
parts.push(buildMaterials(opts));
|
||||
// 意图对齐 + 缺口核对 + 防幻觉护栏(弱席位场景的质量兜底)——两种模式通用,与后端 build_summary_prompt 同义。
|
||||
parts.push("");
|
||||
parts.push(
|
||||
"【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" +
|
||||
"你是这份总结的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)拆成" +
|
||||
"用户核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、有无跑题或矛盾;" +
|
||||
"再据此组织并按护栏补强:① 能从已有原料重组/推理补上的→直接补全讲透;② 属于你自己的推断/建议→显式标注「(综合推断)」;" +
|
||||
"③ 缺真实信息、靠现有原料补不了的→绝不凭空编,用一行『> ⚠️ 待补充:<具体缺什么>。可在对话里让我补全』标出,交给用户。" +
|
||||
"最终产出要明显比各席位交付的简单拼接更扣用户意图。",
|
||||
);
|
||||
if (opts.flowMode) {
|
||||
parts.push(
|
||||
"请严格遵循你的职责设定:全程简体中文。" +
|
||||
|
||||
@@ -36,6 +36,15 @@ export interface ChainSeat {
|
||||
* (无需 DB 列),在链条编辑器里逐席位开关。
|
||||
*/
|
||||
seat_skill_directive?: boolean;
|
||||
/**
|
||||
* 可选「席位推理深度覆盖」:覆盖链条级 `seatMode`,只对该席位生效。值是同一套
|
||||
* `SeatMode`(flash/thinking/pro/ultra);null / 缺省 = 跟随链条级默认。用于「卡顿提速」:
|
||||
* 把信息收集/边角席位单独降到 `flash`(不思考、最快),只给关键席位留 `pro`/`ultra`。
|
||||
* 随 `seats` JSON 落库(无需 DB 列,但需在后端 `ChainSeatSchema` 显式声明,否则 model_dump
|
||||
* 会剥掉)。**字段名用 snake_case `seat_mode`** 与 `model`/`seat_skill_directive` 一致(seats
|
||||
* 数组在 `api/chains.ts` 不做 camel↔snake 转换,键名须与后端 schema 逐字匹配)。
|
||||
*/
|
||||
seat_mode?: SeatMode | null;
|
||||
}
|
||||
|
||||
/** 一条可复用的业务链条(与后端 `/api/roundtable-chains` 记录对应)。 */
|
||||
@@ -68,6 +77,12 @@ export interface BusinessChain {
|
||||
* 缺省 = 未设,调用方回退 `DEFAULT_SEAT_MODE`(pro)。
|
||||
*/
|
||||
seatMode?: SeatMode;
|
||||
/**
|
||||
* 可选「研讨前并行取数」开关(逐条 opt-in,缺省 false)。true = 选入研讨时,研讨前先跑一轮
|
||||
* **全席位并行『取数』**(各席位只调技能取数、不研讨),产出作为共享资料注入后续研讨轮,把最慢
|
||||
* 的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速。三种编排通用。后端列 `gather_first`。
|
||||
*/
|
||||
gatherFirst?: boolean;
|
||||
/** 是否已发布为公共(所有账号可在「公共」筛选查看 / 选入研讨)。 */
|
||||
isPublic?: boolean;
|
||||
/** 是否当前用户所有(仅本人可编辑 / 删除 / 发布;公共筛选里别人的链条为 false)。 */
|
||||
@@ -89,7 +104,7 @@ export interface SeatSelectionResult {
|
||||
* `model` 为该席位在来源链条里配置的模型覆盖(空 / 缺省 = 跟随全局模型)。
|
||||
* `seat_skill_directive` 为该席位在来源链条里配置的「技能识别强化」开关(缺省 = 关)。
|
||||
*/
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean }>;
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean; seatMode?: SeatMode | null }>;
|
||||
/**
|
||||
* 决定 Step 2 走哪种编排:
|
||||
* - `recommend` 总控自由派活;
|
||||
@@ -115,6 +130,11 @@ export interface SeatSelectionResult {
|
||||
* 缺省由调用方按 `DEFAULT_SEAT_MODE`(pro)处理。
|
||||
*/
|
||||
seatMode?: SeatMode;
|
||||
/**
|
||||
* 「研讨前并行取数」开关(来源链条的 `gatherFirst`)。true = 研讨前先并行取数再研讨。
|
||||
* 三种编排通用,缺省 false。recommend(无链条)= false。
|
||||
*/
|
||||
gatherFirst?: boolean;
|
||||
}
|
||||
|
||||
/** 链条席位数硬约束,与圆桌推荐弹窗的 `MIN_SELECTED`/`MAX_SELECTED` 对齐。 */
|
||||
@@ -146,6 +166,10 @@ export function buildChainSeatSelection(
|
||||
// 席位「技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(chain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
// 席位「推理深度覆盖」(链条配置 seat_mode):按 agent_id 取,null/缺省 = 跟随链条级 seatMode。
|
||||
// 输出到 SeatSelectionResult.agents[].seatMode(camel,喂给编排/作业的 seatModes map)。
|
||||
const seatModeById = new Map(chain.seats.map((s) => [s.agent_id, s.seat_mode ?? null]));
|
||||
const seatModeOf = (id: string) => seatModeById.get(id) ?? null;
|
||||
|
||||
const survivingSeats = chain.seats.filter((s) => existingIds.has(s.agent_id));
|
||||
if (survivingSeats.length < CHAIN_MIN_SEATS) return null;
|
||||
@@ -167,13 +191,13 @@ export function buildChainSeatSelection(
|
||||
if (hasParallel || hasGoal || hasCoord) {
|
||||
const agents = plan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "fast", seatMode: chain.seatMode };
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), seatMode: seatModeOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "fast", seatMode: chain.seatMode, gatherFirst: chain.gatherFirst ?? false };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta, seatMode: chain.seatMode };
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta, seatMode: chain.seatMode, gatherFirst: chain.gatherFirst ?? false };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* 对应需求文档 `docs/roundtable-dag-orchestration.md` §2(分层编辑器,竖列分组)。
|
||||
*/
|
||||
import type { BusinessChain, ChainSeat } from "@/roundtable-planning/lib/business-chain";
|
||||
import type { SeatMode } from "@/roundtable-planning/lib/seat-mode";
|
||||
|
||||
/** 一个并行 stage:含其并行席位与可选「本阶段目标」(注入给总控/席位,仅 dag 模式生效)。 */
|
||||
export interface StageGroup {
|
||||
@@ -140,6 +141,21 @@ export function setSeatSkillDirective(groups: StageGroups, agentId: string, enab
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个席位的「推理深度覆盖」(不可变,供编辑器席位下拉 onChange)。
|
||||
* `mode` 为 null = 清除覆盖(跟随链条级 seatMode)。按 agent_id 定位,跨 stage 适用。
|
||||
* 随 `seats` JSON 落库。
|
||||
*/
|
||||
export function setSeatMode(groups: StageGroups, agentId: string, mode: SeatMode | null): StageGroups {
|
||||
return groups.map((g) => {
|
||||
if (!g.seats.some((s) => s.agent_id === agentId)) return g;
|
||||
return {
|
||||
...g,
|
||||
seats: g.seats.map((s) => (s.agent_id === agentId ? { ...s, seat_mode: mode } : s)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ── 变换(均返回新结构,不可变)──────────────────────────────────────────────
|
||||
|
||||
/** 末尾追加一个**新单席位 stage**(线性追加,pool 点击加入默认行为)。 */
|
||||
|
||||
@@ -137,6 +137,16 @@ export interface StartJobPayload {
|
||||
* 后台席位 run 按 agentId 查表(与各席位智能体自身的同名开关取 OR)决定是否注入。缺省 {}。
|
||||
*/
|
||||
seatSkillDirectives?: Record<string, boolean>;
|
||||
/**
|
||||
* 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位单独配的
|
||||
* 推理深度)。后台席位 run 按 agentId 查表,覆盖优先、否则跟随链条级 `seatMode`。缺省 {}。
|
||||
*/
|
||||
seatModes?: Record<string, SeatMode | null>;
|
||||
/**
|
||||
* 「研讨前并行取数」开关(来源链条的 gatherFirst)。true = 后台作业研讨前先并行取数再研讨。
|
||||
* 在 `toStartBody` 里下发为后端 `gather_first`。缺省 false。
|
||||
*/
|
||||
gatherFirst?: boolean;
|
||||
}
|
||||
|
||||
/** 终态集合——SSE/轮询判定作业是否结束。 */
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Gauge,
|
||||
GitBranch,
|
||||
Globe,
|
||||
GripVertical,
|
||||
@@ -67,6 +68,9 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -111,6 +115,7 @@ import {
|
||||
moveStage,
|
||||
removeSeat as removeSeatFromGroups,
|
||||
seatExistsInGroups,
|
||||
setSeatMode as setSeatModeOnGroups,
|
||||
setSeatModel,
|
||||
setSeatSkillDirective,
|
||||
setStageGoal,
|
||||
@@ -173,6 +178,8 @@ export default function BusinessChainEditorPage() {
|
||||
const [coordinatorPrompt, setCoordinatorPrompt] = useState("");
|
||||
// 默认席位执行模式(快速/思考/专业问答/多智能体)—— 选入研讨时弹窗默认用它。
|
||||
const [seatMode, setSeatMode] = useState<SeatMode>(DEFAULT_SEAT_MODE);
|
||||
// 链条级「研讨前并行取数」开关(逐条 opt-in,默认关):开 = 选入研讨时研讨前先并行取数再研讨。
|
||||
const [gatherFirst, setGatherFirst] = useState<boolean>(false);
|
||||
// 分层编排是唯一数据源;落库时展平为 seats + 折叠为 stages。
|
||||
const [stageGroups, setStageGroups] = useState<StageGroups>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -298,6 +305,7 @@ export default function BusinessChainEditorPage() {
|
||||
setDescription(chain.description ?? "");
|
||||
setCoordinatorPrompt(chain.coordinatorPrompt ?? "");
|
||||
setSeatMode(chain.seatMode ?? DEFAULT_SEAT_MODE);
|
||||
setGatherFirst(chain.gatherFirst ?? false);
|
||||
setStageGroups(chainToStageGroups(chain));
|
||||
setStep(1);
|
||||
}, []);
|
||||
@@ -310,6 +318,7 @@ export default function BusinessChainEditorPage() {
|
||||
setDescription("");
|
||||
setCoordinatorPrompt("");
|
||||
setSeatMode(DEFAULT_SEAT_MODE);
|
||||
setGatherFirst(false);
|
||||
setStageGroups([]);
|
||||
setStep(1);
|
||||
}, []);
|
||||
@@ -379,6 +388,11 @@ export default function BusinessChainEditorPage() {
|
||||
setStageGroups((prev) => setSeatSkillDirective(prev, agentId, enabled));
|
||||
}, []);
|
||||
|
||||
// 席位「推理深度覆盖」:编辑器席位小图标下拉 onChange(null = 清除,跟随链条级 seatMode)。
|
||||
const setSeatModeCb = useCallback((agentId: string, mode: SeatMode | null) => {
|
||||
setStageGroups((prev) => setSeatModeOnGroups(prev, agentId, mode));
|
||||
}, []);
|
||||
|
||||
// ── 提示词 AI 优化(总控提示词 / 各 Stage 阶段目标)────────────────────────
|
||||
const [optimizingCoordinator, setOptimizingCoordinator] = useState(false);
|
||||
const [goalOptimizingIdx, setGoalOptimizingIdx] = useState<number | null>(null);
|
||||
@@ -579,6 +593,8 @@ export default function BusinessChainEditorPage() {
|
||||
model: s.model ?? null,
|
||||
// 席位「会商技能识别强化」开关:随席位一起落库(缺省 = false 关)。
|
||||
seat_skill_directive: s.seat_skill_directive ?? false,
|
||||
// 席位「推理深度覆盖」:随席位一起落库(null = 跟随链条级 seatMode)。
|
||||
seat_mode: s.seat_mode ?? null,
|
||||
};
|
||||
});
|
||||
// 总控编排提示:trim 后空串落 null(清空),与后端「空=不注入」一致。
|
||||
@@ -597,6 +613,7 @@ export default function BusinessChainEditorPage() {
|
||||
stageGoals,
|
||||
coordinatorPrompt: coordPrompt,
|
||||
seatMode,
|
||||
gatherFirst,
|
||||
});
|
||||
if (!updated) {
|
||||
toast.error("链条已不存在,可能被删除");
|
||||
@@ -616,6 +633,7 @@ export default function BusinessChainEditorPage() {
|
||||
stageGoals,
|
||||
coordinatorPrompt: coordPrompt,
|
||||
seatMode,
|
||||
gatherFirst,
|
||||
});
|
||||
setCurrentChainId(created.id);
|
||||
toast.success("已创建业务链条");
|
||||
@@ -631,6 +649,7 @@ export default function BusinessChainEditorPage() {
|
||||
description,
|
||||
coordinatorPrompt,
|
||||
seatMode,
|
||||
gatherFirst,
|
||||
stageGroups,
|
||||
seatCountInvalid,
|
||||
currentChainId,
|
||||
@@ -769,6 +788,22 @@ export default function BusinessChainEditorPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 研讨前并行取数:逐条 opt-in 的提速开关(默认关)。 */}
|
||||
<div className={cn("flex items-start justify-between gap-3 border border-border bg-muted/30 p-3", R_CARD)}>
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-1 text-xs font-semibold text-foreground">
|
||||
<Gauge className="size-3.5 text-primary" />
|
||||
研讨前并行取数(提速)
|
||||
</label>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
开启后,选入研讨时会**先并行跑一轮「只取数」**:各席位同时用技能把相关信息收集回来(不研讨),
|
||||
产出作为共享资料注入后续研讨轮。把最慢的技能检索前置、并行做一次,给(尤其按链条顺序串行的)
|
||||
研讨段大幅提速;研讨阶段席位仍可按需再调技能。
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={gatherFirst} onCheckedChange={setGatherFirst} className="mt-0.5 shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* 总控提示词:label + AI 优化(配色沿用现有 primary)+ 大号等宽编辑区 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@@ -1147,6 +1182,7 @@ export default function BusinessChainEditorPage() {
|
||||
onSplitSeat={splitSeat}
|
||||
onSeatModelChange={setSeatModelCb}
|
||||
onSeatSkillDirectiveChange={setSeatSkillDirectiveCb}
|
||||
onSeatModeChange={setSeatModeCb}
|
||||
onStageGoalChange={setGoal}
|
||||
goalOptimizingIdx={goalOptimizingIdx}
|
||||
onOptimizeGoal={handleOptimizeStageGoal}
|
||||
@@ -1326,6 +1362,7 @@ function StagesColumn({
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onSeatModeChange,
|
||||
onStageGoalChange,
|
||||
goalOptimizingIdx,
|
||||
onOptimizeGoal,
|
||||
@@ -1343,6 +1380,7 @@ function StagesColumn({
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatModeChange: (agentId: string, mode: SeatMode | null) => void;
|
||||
onStageGoalChange: (stageIdx: number, goal: string) => void;
|
||||
/** 正在 AI 优化阶段目标的 stage 下标(null = 没有进行中的优化)。 */
|
||||
goalOptimizingIdx: number | null;
|
||||
@@ -1442,6 +1480,7 @@ function StagesColumn({
|
||||
onSplitSeat={onSplitSeat}
|
||||
onSeatModelChange={onSeatModelChange}
|
||||
onSeatSkillDirectiveChange={onSeatSkillDirectiveChange}
|
||||
onSeatModeChange={onSeatModeChange}
|
||||
onGoalChange={(goal) => onStageGoalChange(stageIdx, goal)}
|
||||
goalOptimizing={goalOptimizingIdx === stageIdx}
|
||||
anyGoalOptimizing={goalOptimizingIdx !== null}
|
||||
@@ -1472,6 +1511,7 @@ function StageSection({
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onSeatModeChange,
|
||||
onGoalChange,
|
||||
goalOptimizing,
|
||||
anyGoalOptimizing,
|
||||
@@ -1491,6 +1531,7 @@ function StageSection({
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatModeChange: (agentId: string, mode: SeatMode | null) => void;
|
||||
onGoalChange: (goal: string) => void;
|
||||
/** 本 Stage 的阶段目标正在 AI 优化中。 */
|
||||
goalOptimizing: boolean;
|
||||
@@ -1653,6 +1694,7 @@ function StageSection({
|
||||
onSplit={() => onSplitSeat(seat.agent_id)}
|
||||
onModelChange={(model) => onSeatModelChange(seat.agent_id, model)}
|
||||
onSkillDirectiveChange={(enabled) => onSeatSkillDirectiveChange(seat.agent_id, enabled)}
|
||||
onSeatModeChange={(mode) => onSeatModeChange(seat.agent_id, mode)}
|
||||
onDetail={() => onSeatDetail(seat.agent_id, seat.name)}
|
||||
/>
|
||||
))}
|
||||
@@ -1677,6 +1719,7 @@ function SeatStrip({
|
||||
onSplit,
|
||||
onModelChange,
|
||||
onSkillDirectiveChange,
|
||||
onSeatModeChange,
|
||||
onDetail,
|
||||
}: {
|
||||
seat: ChainSeat;
|
||||
@@ -1693,6 +1736,8 @@ function SeatStrip({
|
||||
onModelChange: (model: string | null) => void;
|
||||
/** 席位「会商技能识别强化」开关变更。 */
|
||||
onSkillDirectiveChange: (enabled: boolean) => void;
|
||||
/** 席位「推理深度覆盖」变更(null = 跟随链条级 seatMode)。 */
|
||||
onSeatModeChange: (mode: SeatMode | null) => void;
|
||||
onDetail: () => void;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
@@ -1764,8 +1809,52 @@ function SeatStrip({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 控制:技能识别强化 / 查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */}
|
||||
{/* 控制:推理深度 / 技能识别强化 / 查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */}
|
||||
<div className="flex shrink-0 items-center gap-0.5 border-l border-border px-2">
|
||||
{/* 席位推理深度覆盖:小图标 → 下拉框(跟随链条默认 + 四档)。设了覆盖 = 高亮。
|
||||
「卡顿提速」用:把信息收集/边角席位降到「快速」,只给关键席位留「专业/多智能体」。 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title="席位推理深度(覆盖链条默认;降到「快速」可在模型卡顿时大幅提速)"
|
||||
className={cn(
|
||||
"p-1 transition",
|
||||
R_CTRL,
|
||||
seat.seat_mode
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Gauge className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
<DropdownMenuLabel className="text-[11px] font-semibold">
|
||||
席位推理深度
|
||||
<span className="mt-0.5 block text-[10px] font-normal text-muted-foreground">
|
||||
覆盖链条级默认,仅本席位生效。模型卡顿时把次要席位降到「快速」可大幅提速。
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className={cn("text-xs", !seat.seat_mode && "font-semibold text-primary")}
|
||||
onClick={() => onSeatModeChange(null)}
|
||||
>
|
||||
跟随链条默认
|
||||
</DropdownMenuItem>
|
||||
{SEAT_MODE_OPTIONS.map((opt) => (
|
||||
<DropdownMenuItem
|
||||
key={opt.value}
|
||||
className={cn("flex-col items-start gap-0 text-xs", seat.seat_mode === opt.value && "font-semibold text-primary")}
|
||||
onClick={() => onSeatModeChange(opt.value)}
|
||||
>
|
||||
<span>{opt.label}</span>
|
||||
<span className="text-[10px] font-normal leading-tight text-muted-foreground">{opt.description}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* 会商技能识别强化:小图标 → 下拉框(开关 + 说明)。开启 = 高亮。 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -315,6 +315,13 @@ export default function RoundtablePlanningPage() {
|
||||
// Step 2 每席位「技能识别强化」开关映射 {agentId: bool}:由所选业务链条里**每个席位**的同名
|
||||
// 配置决定(recommend 无链条 → 空 {})。透传给每个席位 run,与各席位智能体自身的开关取 OR。
|
||||
const [seatSkillDirectives, setSeatSkillDirectives] = useState<Record<string, boolean>>({});
|
||||
// Step 2 每席位「推理深度覆盖」映射 {agentId: SeatMode|null}:由所选业务链条里**每个席位**单独配
|
||||
// 的推理深度决定(null/未配 → 跟随链条级 seatMode)。透传给席位 run(覆盖优先、否则跟随 seatMode)。
|
||||
// 「卡顿提速」用:把次要席位降到 flash 即可大幅缩短第二步耗时。
|
||||
const [seatModes, setSeatModes] = useState<Record<string, SeatMode | null>>({});
|
||||
// Step 2「研讨前并行取数」开关:由所选业务链条的 gatherFirst 决定(recommend / 无链条 → false)。
|
||||
// true = 研讨前先并行跑一轮全席位『取数』,产出作为共享资料注入研讨轮,给(尤其 chain 串行)研讨段提速。
|
||||
const [gatherFirst, setGatherFirst] = useState<boolean>(false);
|
||||
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活;
|
||||
// dag = 分层 DAG(stage 内并行、stage 间串行,配合 orchestrationPlan)。
|
||||
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
|
||||
@@ -471,6 +478,8 @@ export default function RoundtablePlanningPage() {
|
||||
selectedModel,
|
||||
selectedMode: seatMode,
|
||||
seatSkillDirectives,
|
||||
seatModes,
|
||||
gatherFirst,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
dagExecMode,
|
||||
@@ -953,6 +962,13 @@ export default function RoundtablePlanningPage() {
|
||||
setSeatSkillDirectives(
|
||||
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.seat_skill_directive ?? false])),
|
||||
);
|
||||
// 每席位推理深度覆盖:取所选业务链条里**每席位**单独配的 seatMode → {agentId: SeatMode|null}
|
||||
// 映射(recommend / 未配 → null,跟随链条级 seatMode)。
|
||||
setSeatModes(
|
||||
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.seatMode ?? null])),
|
||||
);
|
||||
// 研讨前并行取数:取所选业务链条的开关(recommend / 旧调用未带 → false)。
|
||||
setGatherFirst(result.gatherFirst ?? false);
|
||||
setChainMeta(result.mode === "recommend" ? null : result.chain ?? null);
|
||||
setOrchestrationPlan(result.mode === "dag" ? result.plan ?? null : null);
|
||||
setDagExecMode(result.mode === "dag" ? result.dagExecMode ?? "fast" : "fast");
|
||||
@@ -1528,6 +1544,8 @@ export default function RoundtablePlanningPage() {
|
||||
model: selectedModel || undefined, // 模型沿用第二步选择
|
||||
seatMode, // 席位执行模式沿用第二步选择(后台续跑同样按此模式跑席位)
|
||||
seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关
|
||||
seatModes, // 每席位推理深度覆盖沿用所选业务链条里各席位的配置
|
||||
gatherFirst, // 研讨前并行取数沿用所选业务链条的开关
|
||||
// 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
|
||||
// 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
|
||||
orchestrationMode,
|
||||
@@ -1545,6 +1563,8 @@ export default function RoundtablePlanningPage() {
|
||||
selectedModel,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
seatModes,
|
||||
gatherFirst,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
chainMeta,
|
||||
@@ -1569,6 +1589,8 @@ export default function RoundtablePlanningPage() {
|
||||
const model = selectedModel || undefined;
|
||||
const seatModeSnap = seatMode;
|
||||
const seatSkillDirectivesSnap = seatSkillDirectives;
|
||||
const seatModesSnap = seatModes;
|
||||
const gatherFirstSnap = gatherFirst;
|
||||
const mode = orchestrationMode;
|
||||
const chainSnap = chainMeta;
|
||||
const planSnap = mode === "dag" ? orchestrationPlan : null;
|
||||
@@ -1604,6 +1626,8 @@ export default function RoundtablePlanningPage() {
|
||||
model,
|
||||
seatMode: seatModeSnap,
|
||||
seatSkillDirectives: seatSkillDirectivesSnap,
|
||||
seatModes: seatModesSnap,
|
||||
gatherFirst: gatherFirstSnap,
|
||||
orchestrationMode: mode,
|
||||
chain: chainSnap,
|
||||
orchestrationPlan: planSnap,
|
||||
@@ -1628,6 +1652,8 @@ export default function RoundtablePlanningPage() {
|
||||
selectedModel,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
seatModes,
|
||||
gatherFirst,
|
||||
orchestrationMode,
|
||||
orchestrationPlan,
|
||||
chainMeta,
|
||||
@@ -1669,6 +1695,8 @@ export default function RoundtablePlanningPage() {
|
||||
model: selectedModel || undefined,
|
||||
seatMode,
|
||||
seatSkillDirectives,
|
||||
seatModes,
|
||||
gatherFirst,
|
||||
orchestrationMode,
|
||||
chain: chainMeta,
|
||||
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
|
||||
|
||||
Reference in new issue
Block a user