diff --git a/frontend-web/.env.development b/frontend-web/.env.development index cca4033a..3837c075 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -10,6 +10,9 @@ VITE_MOCK_COP_TASK_DETAIL=true # 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:true 走本地样例假数据(真实接口未就绪时调试用);联调通过后置 false。 VITE_MOCK_SQ_REPORT=true +# 深链任务详情(cop-task-detail:任务名称/描述)数据源:true 走本地假数据(真实接口未就绪时调试用);联调通过后置 false。 +VITE_MOCK_COP_TASK_DETAIL=true + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api diff --git a/frontend-web/.env.network b/frontend-web/.env.network index a8f304b8..ae615eb8 100644 --- a/frontend-web/.env.network +++ b/frontend-web/.env.network @@ -10,6 +10,9 @@ VITE_MOCK_COP_TASK_DETAIL=false # 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:默认真实接口;真实接口未就绪可改 true 走本地样例假数据。 VITE_MOCK_SQ_REPORT=false +# 深链任务详情(cop-task-detail)数据源:默认真实接口;真实接口未就绪可改 true 走本地假数据。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://47.88.25.99:7001 VITE_LANGGRAPH_BASE_URL=http://47.88.25.99:7001/api diff --git a/frontend-web/.env.sit b/frontend-web/.env.sit index f94d33ed..31dda6ed 100644 --- a/frontend-web/.env.sit +++ b/frontend-web/.env.sit @@ -10,6 +10,9 @@ VITE_MOCK_COP_TASK_DETAIL=false # 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:默认真实接口;真实接口未就绪可改 true 走本地样例假数据。 VITE_MOCK_SQ_REPORT=false +# 深链任务详情(cop-task-detail)数据源:默认真实接口;真实接口未就绪可改 true 走本地假数据。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index cfd7e8c2..c28771b0 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -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} /> +
+
+

会商技能识别强化

+

+ 作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file + 再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置里打开(任一即生效)。 +

+
+ +

公开发布

diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index 9a40afc3..0d6bc0b1 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -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 文件可一键发布到知识入库接口。

+
+
+

会商技能识别强化

+

+ 作为会商席位时,把已配置技能(含 SKILL.md 路径与「先 read_file + 再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在业务链条配置中打开(任一即生效)。 +

+
+ + setDraft({ ...draft, seatSkillDirective: checked }) + } + /> +

公开发布

diff --git a/frontend-web/src/core/agents/types.ts b/frontend-web/src/core/agents/types.ts index 0af55b85..283993f8 100644 --- a/frontend-web/src/core/agents/types.ts +++ b/frontend-web/src/core/agents/types.ts @@ -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; } diff --git a/frontend-web/src/core/auth/task-deeplink.ts b/frontend-web/src/core/auth/task-deeplink.ts index c8a2322c..60076bd6 100644 --- a/frontend-web/src/core/auth/task-deeplink.ts +++ b/frontend-web/src/core/auth/task-deeplink.ts @@ -35,6 +35,9 @@ export interface CopTaskDetail { /** 任务详情整包暂存的 sessionStorage key(目标页/后续可读取)。 */ export const COP_TASK_DETAIL_STORAGE_KEY = "login.copTaskDetail"; +/** 行动详情整包暂存的 sessionStorage key(xdfx 行动分析:会商/单智能体读取行动名称/叙事)。 */ +export const ACTION_DETAIL_STORAGE_KEY = "login.actionDetail"; + /** goPath 基础码 → 业务码(配置管理 businessCode)。 */ const GOPATH_BUSINESS_CODE: Record = { "3qfx": "3Q", // 三情分析 @@ -74,8 +77,28 @@ function consumerBaseUrl(): string { return typeof value === "string" ? value.replace(/\/+$/, "") : ""; } -/** 拉取任务详情;缺少 consumer 地址 / 返回不合法时返回 null。 */ +/** cop-task-detail 是否走本地假数据:开关 `VITE_MOCK_COP_TASK_DETAIL=true`(真实接口未就绪时调试用)。 */ +function copTaskDetailMockEnabled(): boolean { + return (import.meta.env as Record).VITE_MOCK_COP_TASK_DETAIL === "true"; +} + +/** cop-task-detail 本地假数据(与真实 data 字段同构;mock 开关开启时返回)。 */ +function mockCopTaskDetail(taskId: string): CopTaskDetail { + return { + id: taskId || "2055", + overview: "暑期文旅消费高峰期间的舆情态势综合研判与引导任务(本地假数据)。", + taskDirection: "正向引导 + 风险预警", + taskName: "任务01:暑期文旅舆情综合研判", + taskContent: + "围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。", + }; +} + +/** 拉取任务详情;``VITE_MOCK_COP_TASK_DETAIL=true`` 走假数据;缺少 consumer 地址 / 返回不合法时返回 null。 */ export async function fetchCopTaskDetail(taskId: string): Promise { + if (copTaskDetailMockEnabled()) { + return mockCopTaskDetail(taskId); + } const base = consumerBaseUrl(); if (!base || !taskId) return null; // service 拦截器已统一注入 `Authorization: Bearer admin` 并返回 response.data(整包 body)。 @@ -111,6 +134,25 @@ export function readCopTaskDetail(): CopTaskDetail | null { } } +/** 把行动详情整包暂存到 sessionStorage(xdfx 行动分析用;隐私模式/quota 异常时静默失败)。 */ +export function persistActionDetail(detail: ActionDetail): void { + try { + sessionStorage.setItem(ACTION_DETAIL_STORAGE_KEY, JSON.stringify(detail)); + } catch { + /* noop */ + } +} + +/** 读取暂存的行动详情(会商左上角「当前行动」、单智能体行动详情卡都可读)。 */ +export function readActionDetail(): ActionDetail | null { + try { + const raw = sessionStorage.getItem(ACTION_DETAIL_STORAGE_KEY); + return raw ? (JSON.parse(raw) as ActionDetail) : null; + } catch { + return null; + } +} + /** 行动详情接口 ``{action_detail_url}/{actionId}`` 返回的 ``data`` 字段(xdfx 行动分析用)。 */ export interface ActionDetail { /** 行动id(深链 ?taskId= 传入的就是它)。 */ @@ -148,7 +190,7 @@ export async function fetchActionDetail(actionId: string): Promise; - return { + const detail: ActionDetail = { id: d.id != null ? String(d.id) : "", taskId: d.taskId != null ? String(d.taskId) : "", purposeId: d.purposeId != null ? String(d.purposeId) : "", @@ -157,6 +199,10 @@ export async function fetchActionDetail(actionId: string): Promise(""); const [chatModelInput, setChatModelInput] = useState(""); const [templateNameInput, setTemplateNameInput] = useState(""); + // 会商席位「技能识别强化」开关(弱模型补偿)。默认关。 + const [seatSkillDirectiveInput, setSeatSkillDirectiveInput] = useState(false); const [formError, setFormError] = useState(""); const [agentId, setAgentId] = useState(""); const [agent, setAgent] = useState(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, diff --git a/frontend-web/src/roundtable-planning/api/chains.ts b/frontend-web/src/roundtable-planning/api/chains.ts index 57df4440..6138efef 100644 --- a/frontend-web/src/roundtable-planning/api/chains.ts +++ b/frontend-web/src/roundtable-planning/api/chains.ts @@ -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; } diff --git a/frontend-web/src/roundtable-planning/api/flow-extract.ts b/frontend-web/src/roundtable-planning/api/flow-extract.ts index 44c735de..731361a5 100644 --- a/frontend-web/src/roundtable-planning/api/flow-extract.ts +++ b/frontend-web/src/roundtable-planning/api/flow-extract.ts @@ -12,7 +12,7 @@ import { getBackendBaseURL } from "@/core/config"; import type { FlowGraphData } from "@/roundtable-planning/lib/flow-graph"; /** 第三步输入框意图分类结果(与后端 /intent 对应)。 */ -export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "qa" | "unclear"; +export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "ingest" | "qa" | "unclear"; /** * 识别第三步输入框消息的意图(一次模型调用)。失败回退 "unclear"(前端发起澄清)。 @@ -22,6 +22,8 @@ export async function classifyFlowIntent(req: { message: string; hasReport: boolean; hasFlow: boolean; + /** 当前是否可入库(入库按钮已出现、未入库);为真才允许识别出 ingest 意图。 */ + canIngest?: boolean; model?: string; signal?: AbortSignal; }): Promise<{ intent: FlowIntent; reason: string }> { @@ -33,6 +35,7 @@ export async function classifyFlowIntent(req: { message: req.message, has_report: req.hasReport, has_flow: req.hasFlow, + can_ingest: req.canIngest ?? false, model: req.model, }), signal: req.signal, @@ -82,6 +85,10 @@ export interface FlowExtractRequest { chainLevels?: string[]; /** 业务链总控编排提示(写明层级时优先用它)。 */ coordinatorPrompt?: string | null; + /** 深链接业务码(rwfx→6BF 等)。非空 → 后端按业务链抽取规范强制注入完整层级。 */ + businessCode?: string | null; + /** 根节点 label 强制值(xdfx:本次行动名称)。非空 → 根节点 n1 用它,不用任务名当根。 */ + rootLabel?: string | null; /** 推理模型名(沿用第三步所选)。 */ model?: string; /** Phase 2 问答修改:当前流程图 + 指令 → 按指令改图(其余不变)。 */ @@ -94,6 +101,11 @@ export interface FlowExtractRequest { onValidating?: () => void; /** 校验/修复流式增量(同样含思考 + 正文,前端实时显示校验过程)。 */ onValidateDelta?: (text: string) => void; + /** + * 模型自动容错:后端某模型抽取失败,已切到下一个模型重抽。上层据此**清空已累计的抽取增量** + * (`onExtractDelta` 之前吐出的半截内容)并提示用户。`failedModel`/`nextModel`/`reason` 描述切换。 + */ + onModelSwitch?: (info: { failedModel: string; nextModel: string; reason: string }) => void; } /** @@ -109,6 +121,8 @@ export async function streamFlowExtract(req: FlowExtractRequest): Promise ({ sender: s.sender, content: s.content })), chain_levels: req.chainLevels ?? [], coordinator_prompt: req.coordinatorPrompt ?? null, + business_code: req.businessCode ?? null, + root_label: req.rootLabel ?? null, model: req.model, current_graph: req.currentGraph ?? null, instruction: req.instruction ?? null, @@ -152,6 +166,12 @@ export async function streamFlowExtract(req: FlowExtractRequest): Promise, ) => void; + /** + * 模型自动容错:后端检测到当前模型出问题(兜底错误文案 / 交付为空等),已自动切到下一个 + * 模型并即将重跑本轮时触发。上层据此把该角色「正在流式的失败气泡」重置(清空已累计的增量 + * 文本)并给用户一条「『X』模型异常,已切换『Y』继续」的提示。此回调可能多次触发(连续多个 + * 模型都失败时)。触发后,本次 streamMultiAgent 仍会继续用新模型流式,最终照常 resolve。 + */ + onModelSwitch?: (info: ModelSwitchInfo) => void; +} + +/** 模型自动容错通知(`streamMultiAgent` 的 `onModelSwitch` 回调入参)。 */ +export interface ModelSwitchInfo { + /** 角色:leader / seat / seat_parallel / report / summary / dashboard / ingest。 */ + role: string; + /** 出问题的智能体 agent_name。 */ + agentName: string; + /** 失败的模型名。 */ + failedModel: string; + /** 已切换到的下一个模型名。 */ + nextModel: string; + /** 失败原因(已翻译成简短中文,如「服务繁忙」)。 */ + reason: string; } export interface ClarificationPayload { @@ -319,6 +351,8 @@ export interface StreamMultiAgentResult { content: string; /** Echoed agent_name of the agent that produced this run. */ agentName: string | null; + /** 实际产出本轮结果的模型(发生模型自动容错时即最终切换到的模型);后端未下发时为 null。 */ + modelUsed?: string | null; /** * Populated when the leader called ask_clarification — the run ended without * dispatching, and the model is asking the user a question instead. When @@ -485,6 +519,10 @@ export async function streamMultiAgent( thinking_enabled: req.thinkingEnabled, reasoning_effort: req.reasoningEffort, subagent_enabled: req.subagentEnabled, + // 仅 special 用;会商技能识别强化(沿用所选业务链条开关)。省略 → 后端按 false(关)处理。 + seat_skill_directive: req.seatSkillDirective ?? false, + // 仅 summary 单例用;深链接业务码 → 后端注入业务链完整性核对。省略/空 → 不注入。 + business_code: req.businessCode ?? null, skill_stop_names: req.skillStopNames ?? [], // 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。 synthesis_mode: req.synthesisMode ?? false, @@ -540,6 +578,7 @@ export async function streamMultiAgent( content: "", agentName: null, clarification: null, + modelUsed: null, }; // Inferred SSE phase + a tiny helper so we only fire onPhaseChange when the @@ -665,6 +704,28 @@ export async function streamMultiAgent( }); } + // 模型自动容错帧:后端检测到当前模型出问题,已切到下一个模型并即将重跑本轮。 + // 不是终态帧(无 content)→ 不结束流。清空已累计的(失败尝试的)tool_call 增量, + // 把 phase 退回 connected,并通知上层重置该角色气泡 + 给用户提示。随后新模型的 + // 流式增量会照常通过 onTextDelta 继续累计到(已被上层清空的)气泡。 + if ( + parsed && + typeof parsed === "object" && + (parsed as Record).status === "model_switch" + ) { + const obj = parsed as Record; + toolCallAcc.clear(); + currentPhase = "connected"; + req.onModelSwitch?.({ + role: typeof obj.role === "string" ? obj.role : "", + agentName: typeof obj.agent_name === "string" ? obj.agent_name : req.agentName, + failedModel: typeof obj.failed_model === "string" ? obj.failed_model : "", + nextModel: typeof obj.next_model === "string" ? obj.next_model : "", + reason: typeof obj.reason === "string" ? obj.reason : "", + }); + continue; + } + // Phase inference from LangGraph messages-tuple frames. We only flip // to tool_calling/tool_result on chunks that clearly indicate them; // everything else with non-empty AI text means we're streaming. @@ -765,6 +826,10 @@ export async function streamMultiAgent( if (typeof parsed.agent_name === "string") { result.agentName = parsed.agent_name; } + { + const mu = (parsed as Record).model_used; + if (typeof mu === "string" && mu) result.modelUsed = mu; + } // SSE error 帧(后端 stream_generator 在席位/Step3 run 失败时下发 // `{status:"error", content, agent_name}`):上报,便于收集用户侧报错。 if (result.message === "error") { diff --git a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts index bcf251cd..5bf1790a 100644 --- a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts +++ b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts @@ -98,6 +98,18 @@ 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 ?? {}, + // 每席位推理深度覆盖映射 {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, + // 深链接业务码:后台总结报告据此做业务链完整性核对(写实业务如 rwfx 才生效)。空 → 不注入。 + business_code: p.businessCode ?? null, ...(seat ? { thinking_enabled: seat.thinking_enabled, diff --git a/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx b/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx index 0254d886..88de95a1 100644 --- a/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx +++ b/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx @@ -150,19 +150,25 @@ 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; + // 席位「推理深度覆盖」(链条配置 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) })); - 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) })); - 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 跳过)。 diff --git a/frontend-web/src/roundtable-planning/components/DashboardChatPanel.tsx b/frontend-web/src/roundtable-planning/components/DashboardChatPanel.tsx index b590c90e..2bffa930 100644 --- a/frontend-web/src/roundtable-planning/components/DashboardChatPanel.tsx +++ b/frontend-web/src/roundtable-planning/components/DashboardChatPanel.tsx @@ -156,7 +156,7 @@ export function DashboardChatPanel({ {/* 聊天记录 */}
-
+
{phase === "creating" && dialogues.length === 0 && (
diff --git a/frontend-web/src/roundtable-planning/components/Step1Panel.tsx b/frontend-web/src/roundtable-planning/components/Step1Panel.tsx index ceb91abc..b75c4948 100644 --- a/frontend-web/src/roundtable-planning/components/Step1Panel.tsx +++ b/frontend-web/src/roundtable-planning/components/Step1Panel.tsx @@ -181,7 +181,8 @@ export function Step1Panel(props: Step1PanelProps) {
{interactiveMessages.map((msg) => ( {isInitializing && step2RoundtableDialogues.length === 0 && (
diff --git a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx index b465d099..e91b3c6b 100644 --- a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx @@ -592,7 +592,7 @@ function AgentProgressPanel({
{phase === "creating" && dialogues.length === 0 && (
diff --git a/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx b/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx index e16151cf..5ad08e05 100644 --- a/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx @@ -707,7 +707,7 @@ function AgentProgressPanel({ {/* 消息列表 —— 与第 2 步同款气泡(流式正文 + 工具步骤卡 + 文件卡)。 */}
-
+
{phase === "creating" && dialogues.length === 0 && (
diff --git a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts index 70bfb38f..642f317c 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts @@ -20,6 +20,7 @@ import { useArtifacts } from "@/components/workspace/artifacts"; import { initMultiAgent, streamMultiAgent, + type ModelSwitchInfo, type ClarificationPayload, type LeaderDispatch, type StreamPhase, @@ -279,6 +280,26 @@ export interface UseStep2OrchestrationOptions { * 智能体」弹窗确认时的 `SeatSelectionResult.seatMode` 决定。缺省 `pro`。 */ selectedMode: SeatMode; + /** + * 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。 + * 按 agentId 查表后透传给该 special 席位 run,与该席位 agent 自身 config.yaml 的同名开关取 + * OR。recommend / 未选链条 → 空 {}(各席位仅靠其自身开关)。 + */ + seatSkillDirectives?: Record; + /** + * 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位的配置)。 + * 派活该 special 席位时按 agentId 查表,**席位覆盖优先、否则跟随链条级 `selectedMode`**: + * `seatModeToRequestFields(seatModes[agentId] ?? selectedMode)`。模型卡顿时把次要席位降到 + * `flash` 即可大幅提速。recommend / 未选链条 / 未覆盖 → 跟随 `selectedMode`。 + */ + seatModes?: Record; + /** + * 「研讨前并行取数」开关(来源业务链条的 gatherFirst,逐条 opt-in,缺省 false)。true = 正式研讨 + * 前先对**全席位并行**跑一轮「只取数」(各席位只调技能取数、不研讨),产出作为共享资料(peerDeliveries) + * 注入后续研讨轮,把最慢的技能检索前置并行、给(尤其 chain 串行的)研讨段提速。每轮研讨只在**本轮 + * 研讨真正启动时**取数一次(resetInitGate 复位后下一轮再取),澄清续跑/干预不重复取数。 + */ + gatherFirst?: boolean; /** * 编排模式: * - `recommend`(默认)—— 总控自由派活循环(`runOrchestration`); @@ -434,6 +455,9 @@ export function useStep2Orchestration( currentStep, selectedModel, selectedMode, + seatSkillDirectives = {}, + seatModes = {}, + gatherFirst = false, orchestrationMode, orchestrationPlan, dagExecMode = "fast", @@ -567,6 +591,16 @@ export function useStep2Orchestration( const selectedModelRef = useRef(selectedModel); // 席位执行模式同样打到 ref,让席位调用在编排循环里读到最新值而不重建回调。 const selectedModeRef = useRef(selectedMode); + // 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。 + const seatSkillDirectivesRef = useRef>(seatSkillDirectives); + // 每席位推理深度覆盖映射(所选业务链条各席位 seatMode)打到 ref,席位调用按 agentId 读最新值; + // 覆盖优先、否则跟随 selectedModeRef。 + const seatModesRef = useRef>(seatModes); + // 「研讨前并行取数」开关 + 本轮取数产物({name,content}[])+ 本轮是否已取过的闸门。取数产物作为 + // peerDeliveries 注入研讨轮席位;闸门保证一轮研讨只取一次(resetInitGate / resetStep2State 复位)。 + const gatherFirstRef = useRef(gatherFirst); + const gatheredDeliveriesRef = useRef>([]); + const gatherPhaseDoneRef = useRef(false); const agentConfigsRef = useRef>(agentConfigs); const agentNameToRoleRef = useRef(agentNameToRole); const threadIdsRef = useRef(threadIds); @@ -606,6 +640,15 @@ export function useStep2Orchestration( useEffect(() => { selectedModeRef.current = selectedMode; }, [selectedMode]); + useEffect(() => { + seatSkillDirectivesRef.current = seatSkillDirectives; + }, [seatSkillDirectives]); + useEffect(() => { + seatModesRef.current = seatModes; + }, [seatModes]); + useEffect(() => { + gatherFirstRef.current = gatherFirst; + }, [gatherFirst]); useEffect(() => { agentConfigsRef.current = agentConfigs; }, [agentConfigs]); @@ -1162,6 +1205,24 @@ export function useStep2Orchestration( [], ); + /** + * 模型自动容错通知(前台):后端切到下一个模型重跑本轮时调用。弹 toast + 在时间线插一条 + * 「系统提示」气泡,让用户看到「『X』模型异常 → 已切换『Y』继续」。 + */ + const notifyModelSwitch = useCallback( + (info: ModelSwitchInfo) => { + const who = info.agentName || "智能体"; + triggerToast(`⚠️「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`); + appendDialogue({ + sender: "系统提示", + avatarType: "amber", + content: `⚠️ 模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`, + confidence: "模型容错", + }); + }, + [appendDialogue, triggerToast], + ); + const phaseLabel = useCallback( ( phase: StreamPhase, @@ -1277,6 +1338,20 @@ export function useStep2Orchestration( ); }, []); + /** + * 生成某气泡的 onModelSwitch 处理器:弹提示 + 把该气泡里「失败尝试已累计的半截内容」清空 + * (随后新模型的流式增量会重新填进同一气泡),并把状态标为「模型切换中…」。 + */ + const makeModelSwitchHandler = useCallback( + (bubbleId: number, isParallel: boolean) => (info: ModelSwitchInfo) => { + notifyModelSwitch(info); + if (isParallel) parallelStreamRef.current.delete(bubbleId); + setDialogueContent(bubbleId, ""); + setDialogueConfidence(bubbleId, "模型切换中…"); + }, + [notifyModelSwitch, setDialogueContent, setDialogueConfidence], + ); + // ─── §6 并行流式:全接收 / 惰性渲染 / 节流 ─────────────────────────────── /** * 节流 flush(~80ms 一拍,约 12fps):**仅被选中**的席位把 ref 累积的全文挂到 @@ -1573,7 +1648,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 → 空)。 @@ -1601,8 +1677,13 @@ 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, signal: subAborter.signal, + onModelSwitch: makeModelSwitchHandler(a.bubbleId, a.isParallel), onTextDelta: (text) => a.isParallel ? ingestParallelDelta(a.bubbleId, text) : appendToDialogue(a.bubbleId, text), onPhaseChange: (phase, toolName, args) => { @@ -1683,10 +1764,16 @@ 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, signal: leaderAborter.signal, + onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false), onTextDelta: (text) => appendToDialogue(leaderBubbleId, text), onPhaseChange: (phase, toolName, args) => { const label = phaseLabel(phase, "leader", toolName); @@ -1954,10 +2041,15 @@ 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, signal: leaderAborter.signal, + onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false), onTextDelta: (text) => appendToDialogue(leaderBubbleId, text), onPhaseChange: (phase, toolName, args) => { const label = phaseLabel(phase, "leader", toolName); @@ -2028,10 +2120,18 @@ 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, signal: subAborter.signal, + onModelSwitch: makeModelSwitchHandler(subBubbleId, false), onTextDelta: (text) => appendToDialogue(subBubbleId, text), onPhaseChange: (phase, toolName, args) => { const label = phaseLabel(phase, "special", toolName); @@ -2091,8 +2191,11 @@ export function useStep2Orchestration( "各链条席位已按顺序完成发言,请你综合所有席位的交付,输出最终方案与结论。不要再派活、不要再向任何席位分配任务。", // 链条收口就是综合轮:让后端注入各席位完整交付正文。 synthesisMode: true, + // 削减重复广播:收口轮不派活、研讨已结束,把总控收口 prompt 播进席位 thread 纯浪费,suppress。 + suppressPreBroadcast: true, model: selectedModelRef.current || undefined, signal: closeoutAborter.signal, + onModelSwitch: makeModelSwitchHandler(closeoutBubbleId, false), onTextDelta: (text) => appendToDialogue(closeoutBubbleId, text), onPhaseChange: (phase, toolName, args) => { const label = phaseLabel(phase, "leader", toolName); @@ -2189,7 +2292,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) => @@ -2253,6 +2357,7 @@ export function useStep2Orchestration( files: takeOnce(pendingInjectFilesRef), model: selectedModelRef.current || undefined, signal: leaderAborter.signal, + onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false), onTextDelta: (text) => appendToDialogue(leaderBubbleId, text), onPhaseChange: (phase, toolName, args) => { const label = phaseLabel(phase, "leader", toolName); @@ -2370,10 +2475,15 @@ 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, signal: subAborter.signal, // §6:并行席位走 ref 全量累积 + 节流 flush(只渲染选中的);单节点 // stage 实际串行,1 路流直接 setState 不卡,沿用 appendToDialogue。 + onModelSwitch: makeModelSwitchHandler(bubbleId, isParallel), onTextDelta: (text) => isParallel ? ingestParallelDelta(bubbleId, text) : appendToDialogue(bubbleId, text), onPhaseChange: (phase, toolName, args) => { @@ -2495,10 +2605,132 @@ export function useStep2Orchestration( ], ); + // 研讨前「并行取数」阶段:对**全席位并行**跑一轮「只取数、不研讨」,产出存进 gatheredDeliveriesRef + // 作为共享资料注入后续研讨轮(recommend/dag 经 priorDeliveries、chain 经 peerDeliveries)。复用 + // recommend 同款并行批显示。每轮研讨只取一次(由 runActiveOrchestration 的 gatherPhaseDoneRef 闸门控制)。 + const runGatherPhase = useCallback( + async (ids: ThreadIdMap): Promise => { + 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, + onModelSwitch: makeModelSwitchHandler(bubbleId, true), + 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 ( @@ -2513,7 +2745,7 @@ export function useStep2Orchestration( } return runOrchestration(seedMessage, ids, coord); }, - [runOrchestration, runChainOrchestration, runDagOrchestration], + [runGatherPhase, runOrchestration, runChainOrchestration, runDagOrchestration], ); // ─── Effects ───────────────────────────────────────────────────── @@ -2829,6 +3061,9 @@ export function useStep2Orchestration( step2InitRef.current = false; // 新一轮研讨:允许 DAG 计划重新执行一遍。 dagPlanExecutedRef.current = false; + // 新一轮研讨:允许「研讨前并行取数」再取一次(清掉上一轮取数产物 + 闸门)。 + gatherPhaseDoneRef.current = false; + gatheredDeliveriesRef.current = []; setInitNonce((n) => n + 1); }, []); @@ -2947,6 +3182,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]); diff --git a/frontend-web/src/roundtable-planning/hooks/useStep3Dashboard.ts b/frontend-web/src/roundtable-planning/hooks/useStep3Dashboard.ts index c39d9773..fd154cc1 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep3Dashboard.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep3Dashboard.ts @@ -325,6 +325,12 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da newMessage: message, model: selectedModel || undefined, signal: aborter.signal, + // 模型自动容错:后端切到下一个模型重跑 → 清空失败尝试的半截累计(acc + 气泡)。 + onModelSwitch: (info) => { + acc = ""; + setDialogues((prev) => prev.map((d) => (d.id === bubbleId ? { ...d, content: "" } : d))); + setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}(${info.reason})`); + }, onTextDelta: (text) => { appendToDialogue(bubbleId, text); acc += text; diff --git a/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts b/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts index 41bbc4f2..5eb4d4e2 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts @@ -770,6 +770,16 @@ export function useStep3Report( newMessage: prompt, model: selectedModel || undefined, signal: aborter.signal, + // 模型自动容错:后端切到下一个模型重跑 → 清空失败尝试半截正文 + 步骤卡留可见提示。 + onModelSwitch: (info) => { + setDialogues((prev) => prev.map((d) => (d.id === bubbleId ? { ...d, content: "" } : d))); + setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`); + appendStepIfNew( + bubbleId, + `⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`, + `model_switch:${info.nextModel}`, + ); + }, onTextDelta: (text) => appendToDialogue(bubbleId, text), onPhaseChange: (p, toolName, toolArgs) => { const label = reportPhaseLabel(p, toolName); diff --git a/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts b/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts index b4f9cb5b..804dcbc4 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts @@ -49,6 +49,7 @@ import { type FlowIntent, } from "@/roundtable-planning/api/flow-extract"; import { nowTime } from "@/roundtable-planning/lib/roundtable-constants"; +import { readActionDetail } from "@/core/auth/task-deeplink"; /** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */ const SUMMARY_OUTPUT_PATH = "/mnt/user-data/outputs/方案总结报告.md"; @@ -179,10 +180,19 @@ function buildMaterials(opts: { seatDeliveries: ReportSeatDelivery[]; consensusText: string; coordinatorPrompt?: string | null; + /** xdfx:本次「行动名称」,作为材料抬头喂进总结(报告围绕该行动展开)。 */ + actionName?: string | null; }): string { - const { intentReady, seatDeliveries, consensusText, coordinatorPrompt } = opts; + const { intentReady, seatDeliveries, consensusText, coordinatorPrompt, actionName } = opts; const parts: string[] = []; + const an = actionName?.trim(); + if (an) { + parts.push("# 本次行动"); + parts.push(an); + parts.push(""); + } + const cp = coordinatorPrompt?.trim(); if (cp) { parts.push("# 业务链总控编排提示(最高优先级,按它的关注点/结构/口径来组织报告)"); @@ -294,6 +304,7 @@ function buildSummaryPrompt(opts: { seatDeliveries: ReportSeatDelivery[]; consensusText: string; coordinatorPrompt?: string | null; + actionName?: string | null; flowMode?: boolean; }): string { const parts: string[] = []; @@ -315,6 +326,18 @@ function buildSummaryPrompt(opts: { } parts.push(""); parts.push(buildMaterials(opts)); + // 意图对齐 + 缺口核对 + 防幻觉护栏(弱席位场景的质量兜底)——两种模式通用,与后端 build_summary_prompt 同义。 + parts.push(""); + parts.push( + "【动笔前必做:意图对齐与缺口核对】各席位可能由能力较弱的模型产出,未必扣住用户意图、未必充分。" + + "你是这份总结的质量兜底,不能只把各席位交付简单拼接。请先把上面的「任务意图」(目标/约束/关键假设)拆成" + + "用户核心诉求清单(编排提示对最终产出的要求也并入),逐条核对各席位交付+共识是否覆盖、够不够、有无跑题或矛盾;" + + "再据此组织:① 能从已有原料重组/推理补上的→直接补全讲透;② 缺真实信息补不了的→绝不凭空编。" + + "**写进 .md 报告文件的必须是一份完整、干净、可直接使用、无需用户修改的报告——不要在报告文件里写「待补充/不足/⚠️」之类提示或占位**。" + + "**但在对话回复正文、报告之后,必须追加一段简短「核对说明」**(只在对话里、**不写进 .md 文件**):逐条说明产出是否覆盖了用户意图、哪些充分/哪些不足/有哪些可补充(可在对话里让我补全);" + + "**即便都已覆盖、无明显缺口,也要明确写一句「已核对:各诉求均已覆盖,无明显缺口」,这段核对说明任何时候都不要省略。**" + + "最终产出要明显比各席位交付的简单拼接更扣用户意图。", + ); if (opts.flowMode) { parts.push( "请严格遵循你的职责设定:全程简体中文。" + @@ -1031,9 +1054,21 @@ export function useStep3Summary( dAgentThreadId: { [agentName]: threadId }, displayName: "方案总结报告", newMessage: prompt, + // 深链接业务码:后端为 summary 单例注入「业务链完整性核对」(核对产出相对业务链完不完整)。 + businessCode: ingestBusinessCodeRef.current, model: selectedModel || undefined, files: files && files.length > 0 ? files : undefined, signal: aborter.signal, + // 模型自动容错:后端切到下一个模型重跑 → 清空失败尝试的半截正文 + 在步骤卡留一条可见提示。 + onModelSwitch: (info) => { + setDialogues((prev) => prev.map((d) => (d.id === bubbleId ? { ...d, content: "" } : d))); + setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`); + appendStepIfNew( + bubbleId, + `⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`, + `model_switch:${info.nextModel}`, + ); + }, onTextDelta: (text) => appendToDialogue(bubbleId, text), onPhaseChange: (p, toolName, pArgs) => { const label = summaryPhaseLabel(p, toolName); @@ -1126,10 +1161,25 @@ export function useStep3Summary( seatDeliveries, chainLevels, coordinatorPrompt, + // 深链接业务码:后端按业务链抽取规范强制注入完整层级(根治"无论什么业务都按目的→行为体抽")。 + businessCode: ingestBusinessCodeRef.current, + // xdfx(7BF):根节点用「行动名称」(流程图根节点显示当前行动,而非任务名)。 + rootLabel: + ingestBusinessCodeRef.current === "7BF" ? readActionDetail()?.actionName ?? null : null, model: selectedModel || undefined, currentGraph: opts?.currentGraph ?? null, instruction: opts?.instruction, signal: aborter.signal, + // 模型自动容错:后端切到下一个模型重抽 → 清空抽取气泡里失败尝试的半截内容 + 留可见提示。 + onModelSwitch: (info) => { + setDialogues((prev) => prev.map((d) => (d.id === extractBubbleId ? { ...d, content: "" } : d))); + setDialogueConfidence(extractBubbleId, `模型切换:${info.failedModel}→${info.nextModel}`); + appendStepIfNew( + extractBubbleId, + `⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`, + `model_switch:${info.nextModel}`, + ); + }, onExtractDelta: (t) => appendToDialogue(extractBubbleId, t), onValidating: () => { setFlowPhase("validating"); @@ -1242,6 +1292,9 @@ export function useStep3Summary( seatDeliveries, consensusText, coordinatorPrompt, + // xdfx(7BF):把本次行动名称喂进总结材料,让报告围绕该行动展开。 + actionName: + ingestBusinessCodeRef.current === "7BF" ? readActionDetail()?.actionName ?? null : null, flowMode, }); await streamOnce({ bubbleId, prompt, aborter, flowRound: flowMode }); @@ -1498,10 +1551,17 @@ export function useStep3Summary( if (!intent) { // **立刻**显示「智能体调度中」气泡(不等意图接口返回),消除连接卡顿时的"卡住"感。 dispatchBubbleId = appendStreamingDialogue({ sender: "智能体", avatarType: "blue" }); + // 入库可用(按钮已出现、未入库 / 未在跑)时把 canIngest 传给意图识别——让「帮我入库」被识别为 + // ingest 而不是落到 qa/unclear(之前主分类器不懂入库,只有「稍后入库待命」时才识别入库,是个缺口)。 + const canIngestNow = + ingestAvailableRef.current && + ingestStatusRef.current !== "running" && + ingestStatusRef.current !== "done"; const r = await classifyFlowIntent({ message: text, hasReport: !!currentMdRef.current.trim(), hasFlow: !!flowGraphRef.current, + canIngest: canIngestNow, model: selectedModel || undefined, }); intent = r.intent; @@ -1513,6 +1573,12 @@ export function useStep3Summary( setPendingFlowClarification(text); // 组件渲染选项让用户自己定 return; } + if (intent === "ingest") { + // 识别为入库(后端已保证仅 can_ingest 时才会返回 ingest)→ 直接入库;复用调度气泡。 + if (dispatchBubbleId != null) finalizeDialogue(dispatchBubbleId, "正在入库…", "入库中"); + await runIngest(); + return; + } if (intent === "regenerate_flow" || intent === "modify_flow") { const flowAborter = new AbortController(); aborterRef.current = flowAborter; diff --git a/frontend-web/src/roundtable-planning/lib/business-chain.ts b/frontend-web/src/roundtable-planning/lib/business-chain.ts index 46b8d2a6..fc3918c8 100644 --- a/frontend-web/src/roundtable-planning/lib/business-chain.ts +++ b/frontend-web/src/roundtable-planning/lib/business-chain.ts @@ -29,6 +29,22 @@ export interface ChainSeat { * 的 `handleConfirmAgents`)→ 走现有 per-seat 模型链路。 */ model?: string | null; + /** + * 可选「会商技能识别强化」开关(弱模型补偿)。true = 该席位作为会商子智能体执行时,把其 + * 配置的技能(名称 + SKILL.md 路径 + 必须先 read_file 的硬指令)拼进本轮任务。与该席位 + * 智能体自身的同名开关(智能体编辑里的开关)取 OR。缺省 / false = 关。随 `seats` JSON 落库 + * (无需 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` 记录对应)。 */ @@ -61,6 +77,12 @@ export interface BusinessChain { * 缺省 = 未设,调用方回退 `DEFAULT_SEAT_MODE`(pro)。 */ seatMode?: SeatMode; + /** + * 可选「研讨前并行取数」开关(逐条 opt-in,缺省 false)。true = 选入研讨时,研讨前先跑一轮 + * **全席位并行『取数』**(各席位只调技能取数、不研讨),产出作为共享资料注入后续研讨轮,把最慢 + * 的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速。三种编排通用。后端列 `gather_first`。 + */ + gatherFirst?: boolean; /** 是否已发布为公共(所有账号可在「公共」筛选查看 / 选入研讨)。 */ isPublic?: boolean; /** 是否当前用户所有(仅本人可编辑 / 删除 / 发布;公共筛选里别人的链条为 false)。 */ @@ -80,8 +102,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; seatMode?: SeatMode | null }>; /** * 决定 Step 2 走哪种编排: * - `recommend` 总控自由派活; @@ -107,6 +130,11 @@ export interface SeatSelectionResult { * 缺省由调用方按 `DEFAULT_SEAT_MODE`(pro)处理。 */ seatMode?: SeatMode; + /** + * 「研讨前并行取数」开关(来源链条的 `gatherFirst`)。true = 研讨前先并行取数再研讨。 + * 三种编排通用,缺省 false。recommend(无链条)= false。 + */ + gatherFirst?: boolean; } /** 链条席位数硬约束,与圆桌推荐弹窗的 `MIN_SELECTED`/`MAX_SELECTED` 对齐。 */ @@ -135,6 +163,13 @@ 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; + // 席位「推理深度覆盖」(链条配置 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; @@ -156,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) })); - 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) })); - 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 }; } /** diff --git a/frontend-web/src/roundtable-planning/lib/chain-stages.ts b/frontend-web/src/roundtable-planning/lib/chain-stages.ts index a077cc0c..b5a85676 100644 --- a/frontend-web/src/roundtable-planning/lib/chain-stages.ts +++ b/frontend-web/src/roundtable-planning/lib/chain-stages.ts @@ -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 { @@ -126,6 +127,35 @@ 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)), + }; + }); +} + +/** + * 设置某个席位的「推理深度覆盖」(不可变,供编辑器席位下拉 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 点击加入默认行为)。 */ diff --git a/frontend-web/src/roundtable-planning/lib/dag-types.ts b/frontend-web/src/roundtable-planning/lib/dag-types.ts index 6f1ae7b4..ae4e3557 100644 --- a/frontend-web/src/roundtable-planning/lib/dag-types.ts +++ b/frontend-web/src/roundtable-planning/lib/dag-types.ts @@ -392,6 +392,13 @@ export function buildDagStageLeaderTask(args: { lines.push( `【本阶段目标】${goalText}`, "请据此目标为每个席位拆解任务,并要求他们**显式承接上一阶段的产出**来完成本阶段目标。", + "⚠️ 派活时**严格忠实于本阶段目标,不要自行新增目标里没有的「数量上限 / 只取最有代表性的 N 组 / 精简归纳成几条」等限制**," + + "也不要照搬其它阶段的数量(如上一阶段筛了 3 组,不代表本阶段也只要 3 组);" + + "目标没要求筛选 / 限定数量时,就要求席位**按其职责把每一条都完整输出**——" + + "若上一阶段有 N 条产出需逐条处理,就让席位**逐条、全部**产出(有多少条就出多少条),绝不替席位归纳 / 合并 / 只取代表性的几条。", + "⚠️ 也**不要替席位新增目标 / 席位职责之外的「输出体例 / 风格」要求**(如『写成连贯故事 / 要有场景感 / " + + "举具体城市景区为例 / 虚构时间地点情节』等)——派活只需转达本阶段目标 + 上一阶段产物 + 承接关系," + + "**让席位严格按它自己的职责设定(SOUL)输出**,且**忠实基于上一阶段的真实产物做分析,不要诱导席位脱离输入去编造 / 虚构事例**。", ); } lines.push( diff --git a/frontend-web/src/roundtable-planning/lib/job-live.ts b/frontend-web/src/roundtable-planning/lib/job-live.ts index c5f107ec..6560fda0 100644 --- a/frontend-web/src/roundtable-planning/lib/job-live.ts +++ b/frontend-web/src/roundtable-planning/lib/job-live.ts @@ -66,6 +66,10 @@ function toStep2Dialogue(d: JobDialogue, idxByAgent: Map): Step2 if (d.role === "leader") { return { ...base, sender: "总控协调", avatarType: "blue", confidence: "派活说明" }; } + // 模型自动容错的系统提示气泡(后端 progress.model_switch 以 role="system" 落库)。 + if (d.role === "system") { + return { ...base, sender: d.name || "系统提示", avatarType: "amber", confidence: "模型容错" }; + } const aid = d.agentId ?? ""; return { ...base, diff --git a/frontend-web/src/roundtable-planning/lib/job-types.ts b/frontend-web/src/roundtable-planning/lib/job-types.ts index 7c1da33e..30daffa3 100644 --- a/frontend-web/src/roundtable-planning/lib/job-types.ts +++ b/frontend-web/src/roundtable-planning/lib/job-types.ts @@ -52,7 +52,8 @@ export interface DispatchChainNode { /** 一条研讨对话记录(leader 说明 / 某席位交付)。 */ export interface JobDialogue { id: number; - role: "leader" | "seat"; + // "system" = 模型自动容错等系统提示气泡(后端 progress.model_switch 落库)。 + role: "leader" | "seat" | "system"; agentId: string | null; name: string; content: string; @@ -132,6 +133,26 @@ export interface StartJobPayload { * reasoning/subagent 三字段发给后端。 */ seatMode?: SeatMode; + /** + * 每席位「技能识别强化」开关映射 {agentId: bool}(沿用所选业务链条里每个席位的配置)。 + * 后台席位 run 按 agentId 查表(与各席位智能体自身的同名开关取 OR)决定是否注入。缺省 {}。 + */ + seatSkillDirectives?: Record; + /** + * 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位单独配的 + * 推理深度)。后台席位 run 按 agentId 查表,覆盖优先、否则跟随链条级 `seatMode`。缺省 {}。 + */ + seatModes?: Record; + /** + * 「研讨前并行取数」开关(来源链条的 gatherFirst)。true = 后台作业研讨前先并行取数再研讨。 + * 在 `toStartBody` 里下发为后端 `gather_first`。缺省 false。 + */ + gatherFirst?: boolean; + /** + * 深链接业务码(rwfx→6BF 等)。透传给后台总结报告的「业务链完整性核对」(写实业务如 rwfx 才生效)。 + * 普通会商省略。`toStartBody` 里下发为后端 `business_code`。 + */ + businessCode?: string | null; } /** 终态集合——SSE/轮询判定作业是否结束。 */ diff --git a/frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx b/frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx index 9fd1e2eb..192e4f07 100644 --- a/frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx @@ -27,6 +27,7 @@ import { Bot, ChevronDown, ChevronUp, + Gauge, GitBranch, Globe, GripVertical, @@ -37,6 +38,7 @@ import { Plus, Save, Sliders, + Sparkles, SplitSquareVertical, Trash2, UserPlus, @@ -63,6 +65,15 @@ import { import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + 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"; @@ -104,7 +115,9 @@ import { moveStage, removeSeat as removeSeatFromGroups, seatExistsInGroups, + setSeatMode as setSeatModeOnGroups, setSeatModel, + setSeatSkillDirective, setStageGoal, splitSeatToNewStage, stageGroupsToSeats, @@ -165,6 +178,8 @@ export default function BusinessChainEditorPage() { const [coordinatorPrompt, setCoordinatorPrompt] = useState(""); // 默认席位执行模式(快速/思考/专业问答/多智能体)—— 选入研讨时弹窗默认用它。 const [seatMode, setSeatMode] = useState(DEFAULT_SEAT_MODE); + // 链条级「研讨前并行取数」开关(逐条 opt-in,默认关):开 = 选入研讨时研讨前先并行取数再研讨。 + const [gatherFirst, setGatherFirst] = useState(false); // 分层编排是唯一数据源;落库时展平为 seats + 折叠为 stages。 const [stageGroups, setStageGroups] = useState([]); const [saving, setSaving] = useState(false); @@ -290,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); }, []); @@ -302,6 +318,7 @@ export default function BusinessChainEditorPage() { setDescription(""); setCoordinatorPrompt(""); setSeatMode(DEFAULT_SEAT_MODE); + setGatherFirst(false); setStageGroups([]); setStep(1); }, []); @@ -366,6 +383,16 @@ export default function BusinessChainEditorPage() { setStageGroups((prev) => setSeatModel(prev, agentId, model)); }, []); + // 席位「会商技能识别强化」开关:编辑器席位下拉 onChange。 + const setSeatSkillDirectiveCb = useCallback((agentId: string, enabled: boolean) => { + 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(null); @@ -564,6 +591,10 @@ export default function BusinessChainEditorPage() { description: live?.description ?? s.description ?? "", // 席位模型覆盖:随席位一起落库(空 → null = 跟随全局)。 model: s.model ?? null, + // 席位「会商技能识别强化」开关:随席位一起落库(缺省 = false 关)。 + seat_skill_directive: s.seat_skill_directive ?? false, + // 席位「推理深度覆盖」:随席位一起落库(null = 跟随链条级 seatMode)。 + seat_mode: s.seat_mode ?? null, }; }); // 总控编排提示:trim 后空串落 null(清空),与后端「空=不注入」一致。 @@ -582,6 +613,7 @@ export default function BusinessChainEditorPage() { stageGoals, coordinatorPrompt: coordPrompt, seatMode, + gatherFirst, }); if (!updated) { toast.error("链条已不存在,可能被删除"); @@ -601,6 +633,7 @@ export default function BusinessChainEditorPage() { stageGoals, coordinatorPrompt: coordPrompt, seatMode, + gatherFirst, }); setCurrentChainId(created.id); toast.success("已创建业务链条"); @@ -616,6 +649,7 @@ export default function BusinessChainEditorPage() { description, coordinatorPrompt, seatMode, + gatherFirst, stageGroups, seatCountInvalid, currentChainId, @@ -754,6 +788,22 @@ export default function BusinessChainEditorPage() {

+ {/* 研讨前并行取数:逐条 opt-in 的提速开关(默认关)。 */} +
+
+ +

+ 开启后,选入研讨时会**先并行跑一轮「只取数」**:各席位同时用技能把相关信息收集回来(不研讨), + 产出作为共享资料注入后续研讨轮。把最慢的技能检索前置、并行做一次,给(尤其按链条顺序串行的) + 研讨段大幅提速;研讨阶段席位仍可按需再调技能。 +

+
+ +
+ {/* 总控提示词:label + AI 优化(配色沿用现有 primary)+ 大号等宽编辑区 */}
@@ -1131,6 +1181,8 @@ export default function BusinessChainEditorPage() { onMergeSeat={mergeSeatToStage} onSplitSeat={splitSeat} onSeatModelChange={setSeatModelCb} + onSeatSkillDirectiveChange={setSeatSkillDirectiveCb} + onSeatModeChange={setSeatModeCb} onStageGoalChange={setGoal} goalOptimizingIdx={goalOptimizingIdx} onOptimizeGoal={handleOptimizeStageGoal} @@ -1309,6 +1361,8 @@ function StagesColumn({ onMergeSeat, onSplitSeat, onSeatModelChange, + onSeatSkillDirectiveChange, + onSeatModeChange, onStageGoalChange, goalOptimizingIdx, onOptimizeGoal, @@ -1325,6 +1379,8 @@ 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; + onSeatModeChange: (agentId: string, mode: SeatMode | null) => void; onStageGoalChange: (stageIdx: number, goal: string) => void; /** 正在 AI 优化阶段目标的 stage 下标(null = 没有进行中的优化)。 */ goalOptimizingIdx: number | null; @@ -1423,6 +1479,8 @@ function StagesColumn({ onMergeSeat={onMergeSeat} onSplitSeat={onSplitSeat} onSeatModelChange={onSeatModelChange} + onSeatSkillDirectiveChange={onSeatSkillDirectiveChange} + onSeatModeChange={onSeatModeChange} onGoalChange={(goal) => onStageGoalChange(stageIdx, goal)} goalOptimizing={goalOptimizingIdx === stageIdx} anyGoalOptimizing={goalOptimizingIdx !== null} @@ -1452,6 +1510,8 @@ function StageSection({ onMergeSeat, onSplitSeat, onSeatModelChange, + onSeatSkillDirectiveChange, + onSeatModeChange, onGoalChange, goalOptimizing, anyGoalOptimizing, @@ -1470,6 +1530,8 @@ 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; + onSeatModeChange: (agentId: string, mode: SeatMode | null) => void; onGoalChange: (goal: string) => void; /** 本 Stage 的阶段目标正在 AI 优化中。 */ goalOptimizing: boolean; @@ -1631,6 +1693,8 @@ 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)} + onSeatModeChange={(mode) => onSeatModeChange(seat.agent_id, mode)} onDetail={() => onSeatDetail(seat.agent_id, seat.name)} /> ))} @@ -1654,6 +1718,8 @@ function SeatStrip({ onMergeDown, onSplit, onModelChange, + onSkillDirectiveChange, + onSeatModeChange, onDetail, }: { seat: ChainSeat; @@ -1668,6 +1734,10 @@ function SeatStrip({ onSplit: () => void; /** 席位模型覆盖变更(null = 清除,跟随全局)。 */ 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 } = @@ -1739,8 +1809,85 @@ function SeatStrip({
- {/* 控制:查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */} + {/* 控制:推理深度 / 技能识别强化 / 查看详情 / 并入上一阶段 / 独立成阶段 / 并入下一阶段 / 移除 */}
+ {/* 席位推理深度覆盖:小图标 → 下拉框(跟随链条默认 + 四档)。设了覆盖 = 高亮。 + 「卡顿提速」用:把信息收集/边角席位降到「快速」,只给关键席位留「专业/多智能体」。 */} + + + + + + + 席位推理深度 + + 覆盖链条级默认,仅本席位生效。模型卡顿时把次要席位降到「快速」可大幅提速。 + + + + onSeatModeChange(null)} + > + 跟随链条默认 + + {SEAT_MODE_OPTIONS.map((opt) => ( + onSeatModeChange(opt.value)} + > + {opt.label} + {opt.description} + + ))} + + + {/* 会商技能识别强化:小图标 → 下拉框(开关 + 说明)。开启 = 高亮。 */} + + + + + +
+
+

会商技能识别强化

+

+ 开启后,该席位作为会商子智能体执行时,会把已配置的技能(含 SKILL.md + 路径与「先 read_file 再用」硬指令)拼进任务,补偿弱模型不识别技能。也可在该智能体编辑里打开(任一即生效)。 +

+
+ +
+
+
)} - {!inWujieEmbed && ( + {/* 深链接(taskId)模式:业务链由 businessCode 自动决定,隐藏手动「业务链条」入口。 */} + {!inWujieEmbed && !prefillRef.current?.taskId && ( )} - + {/* 深链接(taskId)模式:任务固定,把「新建任务」换成「重新分析」——清空会话、对同一任务重做意图分析。 */} + {prefillRef.current?.taskId ? ( + + ) : ( + + )}