feat(roundtable): 会商席位「技能识别强化」改为按需开关(智能体级 + 链条每席位级)

弱模型补偿指令从全域生效改为默认关闭、两个 opt-in 开关取 OR:
- 智能体级: AgentConfig.seat_skill_directive(config.yaml),在智能体编辑弹窗
  (agent-card / agent-detail-panel)开关;agents 路由 create/update/读写全程穿。
- 业务链条每席位级: 存进链条 seats JSON(ChainSeatSchema.seat_skill_directive,
  无需 DB 列/迁移),在链条编辑器每个席位卡片右侧 ✨ 图标→下拉框(开关+说明)配置;
  按 agentId 查表穿到前台 useStep2Orchestration 各席位调用 + 后台作业
  seat_skill_directives:{agentId:bool} → InProcessRoundtableGateway.run_seat。
- ROUNDTABLE_SEAT_SKILL_DIRECTIVE 降级为总熔断开关(默认放行,不再 force-on)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-20 13:37:29 +08:00
1 parent a8c06ca7dc
commit 9a1b674950
24 files changed
+298 -29

No files matched your search

@@ -229,6 +229,10 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
);
const [editPublished, setEditPublished] = useState(agent.published);
const [editIsPrivate, setEditIsPrivate] = useState(agent.is_private);
// 会商席位「技能识别强化」开关(弱模型补偿)。默认关。
const [editSeatSkillDirective, setEditSeatSkillDirective] = useState(
agent.seat_skill_directive ?? false,
);
const [editSquareId, setEditSquareId] = useState(agent.square_id ?? "");
const [publishPickerOpen, setPublishPickerOpen] = useState(false);
const { data: publishSquares } = usePublishSquares();
@@ -274,6 +278,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
setEditSquareId(agent.square_id ?? "");
setSelectedSkills(agent.skills ?? []);
setEditCommonQuestions(normalizeAgentQuestions(agent.common_questions));
setEditSeatSkillDirective(agent.seat_skill_directive ?? false);
}, [
agent.common_questions,
agent.description,
@@ -282,6 +287,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
agent.published,
agent.square_id,
agent.skills,
agent.seat_skill_directive,
editOpen,
]);
@@ -295,6 +301,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
if (agentDetail) {
setEditSoul(agentDetail.soul ?? "");
setEditCommonQuestions(normalizeAgentQuestions(agentDetail.common_questions));
// 列表响应不带 config 额外字段(seat_skill_directive 为 null),详情才有真值。
setEditSeatSkillDirective(agentDetail.seat_skill_directive ?? false);
} else {
setEditSoul(agent.soul ?? "");
}
@@ -386,6 +394,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
request.skills = selectedSkills;
if (JSON.stringify(nextQuestions) !== JSON.stringify(baselineQuestions))
request.common_questions = nextQuestions;
if (editSeatSkillDirective !== ((agentDetail?.seat_skill_directive ?? agent.seat_skill_directive) ?? false))
request.seat_skill_directive = editSeatSkillDirective;
if (Object.keys(request).length === 0) {
setEditOpen(false);
@@ -960,6 +970,19 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
onChange={setEditCommonQuestions}
/>
</div>
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
<div>
<p className="text-sm font-medium">会商技能识别强化</p>
<p className="text-muted-foreground text-xs">
作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file
再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置里打开(任一即生效)。
</p>
</div>
<Switch
checked={editSeatSkillDirective}
onCheckedChange={setEditSeatSkillDirective}
/>
</div>
<div className="flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-sm font-medium">公开发布</p>
@@ -76,6 +76,8 @@ interface AgentDraft {
published: boolean;
featured: boolean;
templateName: string;
/** 会商席位「技能识别强化」开关。 */
seatSkillDirective: boolean;
}
function draftFromAgent(agent: Agent): AgentDraft {
@@ -88,6 +90,7 @@ function draftFromAgent(agent: Agent): AgentDraft {
published: agent.published,
featured: agent.featured_order != null,
templateName: agent.template_name ?? "",
seatSkillDirective: agent.seat_skill_directive ?? false,
};
}
@@ -295,6 +298,8 @@ export function AgentDetailPanel({
if (draft.soul !== (agent.soul ?? "")) request.soul = draft.soul;
if (draft.templateName.trim() !== (agent.template_name ?? ""))
request.template_name = draft.templateName.trim();
if (draft.seatSkillDirective !== (agent.seat_skill_directive ?? false))
request.seat_skill_directive = draft.seatSkillDirective;
if (nextPublished !== agent.published) request.published = nextPublished;
if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? []))
request.skills = draft.skills;
@@ -561,6 +566,21 @@ export function AgentDetailPanel({
配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。
</p>
</div>
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
<div className="pr-3">
<p className="text-sm font-medium">会商技能识别强化</p>
<p className="text-muted-foreground text-xs">
作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file
再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置中打开(任一即生效)。
</p>
</div>
<Switch
checked={draft.seatSkillDirective}
onCheckedChange={(checked) =>
setDraft({ ...draft, seatSkillDirective: checked })
}
/>
</div>
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
<div>
<p className="text-sm font-medium">公开发布</p>
+8
View File
@@ -58,6 +58,10 @@ export interface Agent {
/** 入库模板名(外部知识入库接口的 templateName)。配置后该智能体会话中的
* Markdown 产物右上角出现"发布入库"按钮。空/缺省 = 不支持入库。 */
template_name?: string | null;
/** 会商席位「技能识别强化」开关。作为会商席位运行时,是否把本智能体配置的技能
* (名称 + SKILL.md 路径 + 必须先 read_file 的硬指令) 拼进本轮任务以补偿弱模型。
* 与业务链条同名开关取 OR。null/false = 关 (默认)。 */
seat_skill_directive?: boolean | null;
tags?: Tag[];
featured_order?: number | null;
/** Whether the current user has favorited (收藏) this agent. */
@@ -91,6 +95,8 @@ export interface CreateAgentRequest {
tool_groups?: string[] | null;
common_questions?: AgentQuestion[] | null;
template_name?: string | null;
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
seat_skill_directive?: boolean | null;
}
export interface UpdateAgentRequest {
@@ -106,4 +112,6 @@ export interface UpdateAgentRequest {
common_questions?: AgentQuestion[] | null;
/** 入库模板名;传空字符串表示清除。 */
template_name?: string | null;
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
seat_skill_directive?: boolean | null;
}
+5
View File
@@ -83,6 +83,8 @@ export default function NewAgentPage() {
const [modelInput, setModelInput] = useState<string>("");
const [chatModelInput, setChatModelInput] = useState<string>("");
const [templateNameInput, setTemplateNameInput] = useState("");
// 会商席位「技能识别强化」开关(弱模型补偿)。默认关。
const [seatSkillDirectiveInput, setSeatSkillDirectiveInput] = useState(false);
const [formError, setFormError] = useState("");
const [agentId, setAgentId] = useState("");
const [agent, setAgent] = useState<Agent | null>(null);
@@ -230,6 +232,7 @@ export default function NewAgentPage() {
skills: selectedSkills,
model: modelOverride,
template_name: templateNameInput.trim(),
seat_skill_directive: seatSkillDirectiveInput,
});
setAgent(updated);
return updated;
@@ -251,6 +254,7 @@ export default function NewAgentPage() {
skills: selectedSkills,
model: modelOverride,
template_name: templateNameInput.trim(),
seat_skill_directive: seatSkillDirectiveInput,
});
setAgentId(created.id);
setAgent(created);
@@ -281,6 +285,7 @@ export default function NewAgentPage() {
selectedSkills,
soulInput,
templateNameInput,
seatSkillDirectiveInput,
t.agents.nameStepCheckError,
t.agents.nameStepNetworkError,
validateForm,
@@ -234,6 +234,12 @@ export interface StreamMultiAgentRequest {
reasoningEffort?: "minimal" | "low" | "medium" | "high";
/** 仅 special 用;ultra 档置真,给席位解锁 `task` 子代理工具。 */
subagentEnabled?: boolean;
/**
* 仅 special 用。会商席位「技能识别强化」开关(沿用所选业务链条配置)。置 true 时后端把该
* 席位配置的技能(含 SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务,补偿弱模型。
* 与该席位 agent 自身 config.yaml 的同名开关取 OR。省略 = false(关)。
*/
seatSkillDirective?: boolean;
skillStopNames?: string[];
/**
* 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付
@@ -485,6 +491,8 @@ export async function streamMultiAgent(
thinking_enabled: req.thinkingEnabled,
reasoning_effort: req.reasoningEffort,
subagent_enabled: req.subagentEnabled,
// 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。
seat_skill_directive: req.seatSkillDirective ?? false,
skill_stop_names: req.skillStopNames ?? [],
// 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。
synthesis_mode: req.synthesisMode ?? false,
@@ -98,6 +98,9 @@ function toStartBody(p: StartJobPayload) {
seedMessage: p.seedMessage,
threadIds: p.threadIds ?? null,
coordinatorName: p.coordinatorName ?? "roundtable-coordinator",
// 每席位技能识别强化映射 {agentId: bool}:沿用所选业务链条里各席位的开关;缺省 {}。
// 后端 run_seat 按 agentId 查表,与各席位 agent 自身开关取 OR。
seat_skill_directives: p.seatSkillDirectives ?? {},
...(seat
? {
thinking_enabled: seat.thinking_enabled,
@@ -150,18 +150,21 @@ export function BusinessChainPicker({
// 席位模型覆盖(链条配置):按 agent_id 取,空/缺省 = 跟随全局模型。
const modelById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.model ?? null]));
const modelOf = (id: string) => modelById.get(id) ?? null;
// 席位「会商技能识别强化」开关(链条配置):按 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;
// 链条带分层、过滤存活后仍并行 → 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) }));
.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 });
return;
}
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id) }));
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 });
};
@@ -279,6 +279,12 @@ export interface UseStep2OrchestrationOptions {
* 智能体」弹窗确认时的 `SeatSelectionResult.seatMode` 决定。缺省 `pro`。
*/
selectedMode: SeatMode;
/**
* 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。
* 按 agentId 查表后透传给该 special 席位 run,与该席位 agent 自身 config.yaml 的同名开关取
* OR。recommend / 未选链条 → 空 {}(各席位仅靠其自身开关)。
*/
seatSkillDirectives?: Record<string, boolean>;
/**
* 编排模式:
* - `recommend`(默认)—— 总控自由派活循环(`runOrchestration`);
@@ -434,6 +440,7 @@ export function useStep2Orchestration(
currentStep,
selectedModel,
selectedMode,
seatSkillDirectives = {},
orchestrationMode,
orchestrationPlan,
dagExecMode = "fast",
@@ -567,6 +574,8 @@ export function useStep2Orchestration(
const selectedModelRef = useRef<string>(selectedModel);
// 席位执行模式同样打到 ref,让席位调用在编排循环里读到最新值而不重建回调。
const selectedModeRef = useRef<SeatMode>(selectedMode);
// 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。
const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives);
const agentConfigsRef = useRef<Record<number, AgentConfig>>(agentConfigs);
const agentNameToRoleRef = useRef(agentNameToRole);
const threadIdsRef = useRef<ThreadIdMap | null>(threadIds);
@@ -606,6 +615,9 @@ export function useStep2Orchestration(
useEffect(() => {
selectedModeRef.current = selectedMode;
}, [selectedMode]);
useEffect(() => {
seatSkillDirectivesRef.current = seatSkillDirectives;
}, [seatSkillDirectives]);
useEffect(() => {
agentConfigsRef.current = agentConfigs;
}, [agentConfigs]);
@@ -1602,6 +1614,9 @@ export function useStep2Orchestration(
model: perSeatModel || selectedModelRef.current || undefined,
// 席位执行模式(快速/思考/专业问答/多智能体问答)→ thinking/reasoning/subagent。
...seatModeToRequestFields(selectedModeRef.current),
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
// agent 自身开关取 OR。
seatSkillDirective: seatSkillDirectivesRef.current[a.agentId] ?? false,
signal: subAborter.signal,
onTextDelta: (text) =>
a.isParallel ? ingestParallelDelta(a.bubbleId, text) : appendToDialogue(a.bubbleId, text),
@@ -2031,6 +2046,9 @@ export function useStep2Orchestration(
model: perSeatModel || selectedModelRef.current || undefined,
// 席位执行模式 → thinking/reasoning/subagent(与推荐/DAG 路径一致)。
...seatModeToRequestFields(selectedModeRef.current),
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该席位
// agent 自身开关取 OR。
seatSkillDirective: seatSkillDirectivesRef.current[seat.agent_id] ?? false,
signal: subAborter.signal,
onTextDelta: (text) => appendToDialogue(subBubbleId, text),
onPhaseChange: (phase, toolName, args) => {
@@ -2371,6 +2389,9 @@ export function useStep2Orchestration(
model: perSeatModel || selectedModelRef.current || undefined,
// 席位执行模式 → thinking/reasoning/subagent(与推荐/链条路径一致)。
...seatModeToRequestFields(selectedModeRef.current),
// 会商技能识别强化(该席位在所选业务链条里的开关,按 agentId 查表);与该
// 席位 agent 自身开关取 OR。
seatSkillDirective: seatSkillDirectivesRef.current[agentId] ?? false,
signal: subAborter.signal,
// §6:并行席位走 ref 全量累积 + 节流 flush(只渲染选中的);单节点
// stage 实际串行,1 路流直接 setState 不卡,沿用 appendToDialogue。
@@ -29,6 +29,13 @@ export interface ChainSeat {
* 的 `handleConfirmAgents`)→ 走现有 per-seat 模型链路。
*/
model?: string | null;
/**
* 可选「会商技能识别强化」开关(弱模型补偿)。true = 该席位作为会商子智能体执行时,把其
* 配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务。与该席位
* 智能体自身的同名开关(智能体编辑里的开关)取 OR。缺省 / false = 关。随 `seats` JSON 落库
* (无需 DB 列),在链条编辑器里逐席位开关。
*/
seat_skill_directive?: boolean;
}
/** 一条可复用的业务链条(与后端 `/api/roundtable-chains` 记录对应)。 */
@@ -80,8 +87,9 @@ export interface SeatSelectionResult {
/**
* 有序席位 —— 与现有 `handleConfirmAgents` 入参兼容。dag 模式下按 stage 展平排序。
* `model` 为该席位在来源链条里配置的模型覆盖(空 / 缺省 = 跟随全局模型)。
* `seat_skill_directive` 为该席位在来源链条里配置的「技能识别强化」开关(缺省 = 关)。
*/
agents: Array<{ agent_id: string; name: string; model?: string | null }>;
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean }>;
/**
* 决定 Step 2 走哪种编排:
* - `recommend` 总控自由派活;
@@ -135,6 +143,9 @@ export function buildChainSeatSelection(
// 席位模型覆盖(链条配置):按 agent_id 取,空/缺省 = 跟随全局。
const modelById = new Map(chain.seats.map((s) => [s.agent_id, s.model ?? null]));
const modelOf = (id: string) => modelById.get(id) ?? null;
// 席位「技能识别强化」开关(链条配置):按 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;
const survivingSeats = chain.seats.filter((s) => existingIds.has(s.agent_id));
if (survivingSeats.length < CHAIN_MIN_SEATS) return null;
@@ -156,12 +167,12 @@ 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) }));
.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 };
}
}
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id) }));
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 };
}
@@ -126,6 +126,20 @@ export function setSeatModel(groups: StageGroups, agentId: string, model: string
});
}
/**
* 设置某个席位的「会商技能识别强化」开关(不可变,供编辑器席位下拉 onChange)。
* 按 agent_id 定位,跨 stage 适用。随 `seats` JSON 落库。
*/
export function setSeatSkillDirective(groups: StageGroups, agentId: string, enabled: boolean): 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_skill_directive: enabled } : s)),
};
});
}
// ── 变换(均返回新结构,不可变)──────────────────────────────────────────────
/** 末尾追加一个**新单席位 stage**(线性追加,pool 点击加入默认行为)。 */
@@ -132,6 +132,11 @@ export interface StartJobPayload {
* reasoning/subagent 三字段发给后端。
*/
seatMode?: SeatMode;
/**
* 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。
* 后台席位 run 按 agentId 查表(与各席位智能体自身的同名开关取 OR)决定是否注入。缺省 {}。
*/
seatSkillDirectives?: Record<string, boolean>;
}
/** 终态集合——SSE/轮询判定作业是否结束。 */
@@ -37,6 +37,7 @@ import {
Plus,
Save,
Sliders,
Sparkles,
SplitSquareVertical,
Trash2,
UserPlus,
@@ -63,6 +64,12 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Switch } from "@/components/ui/switch";
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
import { ModelNameSelect } from "@/components/workspace/model-name-select";
@@ -105,6 +112,7 @@ import {
removeSeat as removeSeatFromGroups,
seatExistsInGroups,
setSeatModel,
setSeatSkillDirective,
setStageGoal,
splitSeatToNewStage,
stageGroupsToSeats,
@@ -366,6 +374,11 @@ export default function BusinessChainEditorPage() {
setStageGroups((prev) => setSeatModel(prev, agentId, model));
}, []);
// 席位「会商技能识别强化」开关:编辑器席位下拉 onChange。
const setSeatSkillDirectiveCb = useCallback((agentId: string, enabled: boolean) => {
setStageGroups((prev) => setSeatSkillDirective(prev, agentId, enabled));
}, []);
// ── 提示词 AI 优化(总控提示词 / 各 Stage 阶段目标)────────────────────────
const [optimizingCoordinator, setOptimizingCoordinator] = useState(false);
const [goalOptimizingIdx, setGoalOptimizingIdx] = useState<number | null>(null);
@@ -564,6 +577,8 @@ export default function BusinessChainEditorPage() {
description: live?.description ?? s.description ?? "",
// 席位模型覆盖:随席位一起落库(空 → null = 跟随全局)。
model: s.model ?? null,
// 席位「会商技能识别强化」开关:随席位一起落库(缺省 = false 关)。
seat_skill_directive: s.seat_skill_directive ?? false,
};
});
// 总控编排提示:trim 后空串落 null(清空),与后端「空=不注入」一致。
@@ -1131,6 +1146,7 @@ export default function BusinessChainEditorPage() {
onMergeSeat={mergeSeatToStage}
onSplitSeat={splitSeat}
onSeatModelChange={setSeatModelCb}
onSeatSkillDirectiveChange={setSeatSkillDirectiveCb}
onStageGoalChange={setGoal}
goalOptimizingIdx={goalOptimizingIdx}
onOptimizeGoal={handleOptimizeStageGoal}
@@ -1309,6 +1325,7 @@ function StagesColumn({
onMergeSeat,
onSplitSeat,
onSeatModelChange,
onSeatSkillDirectiveChange,
onStageGoalChange,
goalOptimizingIdx,
onOptimizeGoal,
@@ -1325,6 +1342,7 @@ function StagesColumn({
onMergeSeat: (agentId: string, targetStageIdx: number) => void;
onSplitSeat: (agentId: string) => void;
onSeatModelChange: (agentId: string, model: string | null) => void;
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
onStageGoalChange: (stageIdx: number, goal: string) => void;
/** 正在 AI 优化阶段目标的 stage 下标(null = 没有进行中的优化)。 */
goalOptimizingIdx: number | null;
@@ -1423,6 +1441,7 @@ function StagesColumn({
onMergeSeat={onMergeSeat}
onSplitSeat={onSplitSeat}
onSeatModelChange={onSeatModelChange}
onSeatSkillDirectiveChange={onSeatSkillDirectiveChange}
onGoalChange={(goal) => onStageGoalChange(stageIdx, goal)}
goalOptimizing={goalOptimizingIdx === stageIdx}
anyGoalOptimizing={goalOptimizingIdx !== null}
@@ -1452,6 +1471,7 @@ function StageSection({
onMergeSeat,
onSplitSeat,
onSeatModelChange,
onSeatSkillDirectiveChange,
onGoalChange,
goalOptimizing,
anyGoalOptimizing,
@@ -1470,6 +1490,7 @@ function StageSection({
onMergeSeat: (agentId: string, targetStageIdx: number) => void;
onSplitSeat: (agentId: string) => void;
onSeatModelChange: (agentId: string, model: string | null) => void;
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
onGoalChange: (goal: string) => void;
/** 本 Stage 的阶段目标正在 AI 优化中。 */
goalOptimizing: boolean;
@@ -1631,6 +1652,7 @@ function StageSection({
onMergeDown={() => onMergeSeat(seat.agent_id, stageIdx + 1)}
onSplit={() => onSplitSeat(seat.agent_id)}
onModelChange={(model) => onSeatModelChange(seat.agent_id, model)}
onSkillDirectiveChange={(enabled) => onSeatSkillDirectiveChange(seat.agent_id, enabled)}
onDetail={() => onSeatDetail(seat.agent_id, seat.name)}
/>
))}
@@ -1654,6 +1676,7 @@ function SeatStrip({
onMergeDown,
onSplit,
onModelChange,
onSkillDirectiveChange,
onDetail,
}: {
seat: ChainSeat;
@@ -1668,6 +1691,8 @@ function SeatStrip({
onSplit: () => void;
/** 席位模型覆盖变更(null = 清除,跟随全局)。 */
onModelChange: (model: string | null) => void;
/** 席位「会商技能识别强化」开关变更。 */
onSkillDirectiveChange: (enabled: boolean) => void;
onDetail: () => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
@@ -1739,8 +1764,41 @@ 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_skill_directive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Sparkles className="size-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64 p-3">
<div className="flex items-start justify-between gap-3">
<div className="space-y-0.5">
<p className="text-xs font-semibold text-foreground">会商技能识别强化</p>
<p className="text-[11px] leading-relaxed text-muted-foreground">
开启后,该席位作为会商子智能体执行时,会把已配置的技能(含 SKILL.md
路径与「先 read_file 再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在该智能体编辑里打开(任一即生效)。
</p>
</div>
<Switch
checked={seat.seat_skill_directive ?? false}
onCheckedChange={onSkillDirectiveChange}
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
onClick={onDetail}
@@ -312,6 +312,9 @@ export default function RoundtablePlanningPage() {
// Step 2 席位执行模式(快速/思考/专业问答/多智能体问答)——决定每个子智能体席位的
// 推理深度。由「选择参会智能体」弹窗确认时的 SeatSelectionResult.seatMode 决定。
const [seatMode, setSeatMode] = useState<SeatMode>(DEFAULT_SEAT_MODE);
// Step 2 每席位「技能识别强化」开关映射 {agentId: bool}:由所选业务链条里**每个席位**的同名
// 配置决定(recommend 无链条 → 空 {})。透传给每个席位 run,与各席位智能体自身的开关取 OR。
const [seatSkillDirectives, setSeatSkillDirectives] = useState<Record<string, boolean>>({});
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活;
// dag = 分层 DAG(stage 内并行、stage 间串行,配合 orchestrationPlan)。
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
@@ -467,6 +470,7 @@ export default function RoundtablePlanningPage() {
currentStep,
selectedModel,
selectedMode: seatMode,
seatSkillDirectives,
orchestrationMode,
orchestrationPlan,
dagExecMode,
@@ -944,6 +948,11 @@ export default function RoundtablePlanningPage() {
setOrchestrationMode(result.mode);
// 席位执行模式:弹窗未带(旧调用)时回退默认 pro,保证行为可预期。
setSeatMode(result.seatMode ?? DEFAULT_SEAT_MODE);
// 会商技能识别强化:取所选业务链条里**每席位**的开关 → {agentId: bool} 映射
// (recommend / 旧调用未带 → 空 {},各席位仅靠其智能体自身开关)。
setSeatSkillDirectives(
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.seat_skill_directive ?? 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");
@@ -1518,6 +1527,7 @@ export default function RoundtablePlanningPage() {
agents,
model: selectedModel || undefined, // 模型沿用第二步选择
seatMode, // 席位执行模式沿用第二步选择(后台续跑同样按此模式跑席位)
seatSkillDirectives, // 每席位技能识别强化沿用所选业务链条里各席位的开关
// 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
// 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
orchestrationMode,
@@ -1534,6 +1544,7 @@ export default function RoundtablePlanningPage() {
drafts,
selectedModel,
seatMode,
seatSkillDirectives,
orchestrationMode,
orchestrationPlan,
chainMeta,
@@ -1557,6 +1568,7 @@ export default function RoundtablePlanningPage() {
const intent = buildInitialIntent();
const model = selectedModel || undefined;
const seatModeSnap = seatMode;
const seatSkillDirectivesSnap = seatSkillDirectives;
const mode = orchestrationMode;
const chainSnap = chainMeta;
const planSnap = mode === "dag" ? orchestrationPlan : null;
@@ -1591,6 +1603,7 @@ export default function RoundtablePlanningPage() {
agents,
model,
seatMode: seatModeSnap,
seatSkillDirectives: seatSkillDirectivesSnap,
orchestrationMode: mode,
chain: chainSnap,
orchestrationPlan: planSnap,
@@ -1614,6 +1627,7 @@ export default function RoundtablePlanningPage() {
buildInitialIntent,
selectedModel,
seatMode,
seatSkillDirectives,
orchestrationMode,
orchestrationPlan,
chainMeta,
@@ -1654,6 +1668,7 @@ export default function RoundtablePlanningPage() {
agents,
model: selectedModel || undefined,
seatMode,
seatSkillDirectives,
orchestrationMode,
chain: chainMeta,
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,