diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
index 1ef6d6ac..4f168b1d 100644
--- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
+++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
@@ -100,6 +100,7 @@ import {
buildChainSeatSelection,
type SeatSelectionResult,
} from "@/roundtable-planning/lib/business-chain";
+import { DEFAULT_SEAT_MODE, type SeatMode } from "@/roundtable-planning/lib/seat-mode";
import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types";
import type { IntentSummary } from "@/roundtable-planning/api/intent";
import {
@@ -108,6 +109,7 @@ import {
startJob,
startJobKeepalive,
} from "@/roundtable-planning/api/roundtable-jobs";
+import { useRoundtableJobNotifier } from "@/roundtable-planning/components/RoundtableJobNotifier";
import { getChain } from "@/roundtable-planning/api/chains";
import { filterRecommendCandidates } from "@/roundtable-planning/api/recommend";
import { listAgents } from "@/core/agents";
@@ -169,6 +171,8 @@ function readEmbedHashQueryPrefill(): RoundtablePrefill | null {
export default function RoundtablePlanningPage() {
const { theme } = useTheme();
+ // 全工作区跨页的后台作业完成通知器(Provider 挂在 PageLayout 常驻层)。
+ const { watchJob } = useRoundtableJobNotifier();
const isDark = theme === "dark";
const navigate = useNavigate();
const location = useLocation();
@@ -214,6 +218,9 @@ export default function RoundtablePlanningPage() {
// 「导入会商」弹窗(输入分享码把别人分享的会商复制到本账号)。
const [isImportOpen, setIsImportOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState("");
+ // Step 2 席位执行模式(快速/思考/专业问答/多智能体问答)——决定每个子智能体席位的
+ // 推理深度。由「选择参会智能体」弹窗确认时的 SeatSelectionResult.seatMode 决定。
+ const [seatMode, setSeatMode] = useState(DEFAULT_SEAT_MODE);
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活;
// dag = 分层 DAG(stage 内并行、stage 间串行,配合 orchestrationPlan)。
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
@@ -329,6 +336,7 @@ export default function RoundtablePlanningPage() {
const step2 = useStep2Orchestration({
currentStep,
selectedModel,
+ selectedMode: seatMode,
orchestrationMode,
orchestrationPlan,
dagExecMode,
@@ -739,6 +747,8 @@ export default function RoundtablePlanningPage() {
// result.mode 决定 Step 2 编排:recommend = 总控自由派活;chain = 按链条顺序派活;
// dag = 分层并行(来源链条带 stages,携带 result.plan)。
setOrchestrationMode(result.mode);
+ // 席位执行模式:弹窗未带(旧调用)时回退默认 pro,保证行为可预期。
+ setSeatMode(result.seatMode ?? DEFAULT_SEAT_MODE);
setChainMeta(result.mode === "recommend" ? null : result.chain ?? null);
setOrchestrationPlan(result.mode === "dag" ? result.plan ?? null : null);
setDagExecMode(result.mode === "dag" ? result.dagExecMode ?? "fast" : "fast");
@@ -1118,9 +1128,11 @@ export default function RoundtablePlanningPage() {
// 「后台挂起」核心(手动按钮 / 离开页面自动挂起共用):幂等保证草稿存在 →
// 同草稿已有在跑作业则跳过 → 启动后台作业。后端另有「同草稿未终态作业幂等返回」
// 兜底(覆盖跨标签页 / beforeunload 与本函数并发触发),绝不重复建作业。
- const startBackgroundJob = useCallback(async (): Promise => {
+ // 返回本次后台作业的 jobId(已在跑则返回既有作业 id),供「完成时提示」监听;
+ // 未发起(无席位)返回 null。
+ const startBackgroundJob = useCallback(async (): Promise => {
const agents = step2.selectedAgents.map((a) => ({ agentId: a.agent_id, name: a.name }));
- if (agents.length === 0) return false;
+ if (agents.length === 0) return null;
// 后台挂起发起前**立刻强制存一次**当前聊天(保住发起时刻的本地研讨记录,防丢)。
const draftId = await drafts.saveDraftNow(2);
// 防重复:该草稿已有在跑作业就不再新建(避免重复作业 / 前后端双跑)。
@@ -1130,28 +1142,31 @@ export default function RoundtablePlanningPage() {
(existing.status === "running" ||
existing.status === "awaiting_input" ||
existing.status === "queued");
- if (!alreadyRunning) {
- await startJob({
- draftId: draftId ?? undefined,
- intent: buildInitialIntent(),
- agents,
- model: selectedModel || undefined, // 模型沿用第二步选择
- // 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
- // 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
- orchestrationMode,
- chain: chainMeta,
- // dag 模式:把整份分层计划带给后端(含 stages / 阶段目标 / 总控提示词 / finalSynthesis)。
- // 非 dag 时为 null(后端按 mode 走 chain/recommend)。
- orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
- // 不传 threadIds:后台作业由真网关自建全新线程,绝不复用前端本地研讨的线程
- // (那些线程可能有活跃 run,复用会撞 409)。
- });
+ if (alreadyRunning) {
+ return existing?.id ?? null;
}
- return true;
+ const job = await startJob({
+ draftId: draftId ?? undefined,
+ intent: buildInitialIntent(),
+ agents,
+ model: selectedModel || undefined, // 模型沿用第二步选择
+ seatMode, // 席位执行模式沿用第二步选择(后台续跑同样按此模式跑席位)
+ // 后台作业引擎支持 dag(后端 _run_dag:stage 串行 / stage 内并行)——
+ // 直接把当前编排模式透传,业务链的分层 / 并行 / 阶段目标 / 总控提示词全部保留。
+ orchestrationMode,
+ chain: chainMeta,
+ // dag 模式:把整份分层计划带给后端(含 stages / 阶段目标 / 总控提示词 / finalSynthesis)。
+ // 非 dag 时为 null(后端按 mode 走 chain/recommend)。
+ orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
+ // 不传 threadIds:后台作业由真网关自建全新线程,绝不复用前端本地研讨的线程
+ // (那些线程可能有活跃 run,复用会撞 409)。
+ });
+ return job.id;
}, [
step2,
drafts,
selectedModel,
+ seatMode,
orchestrationMode,
orchestrationPlan,
chainMeta,
@@ -1165,17 +1180,20 @@ export default function RoundtablePlanningPage() {
triggerToast("⚠️ 请先选择研讨席位再后台挂起");
return;
}
+ let jobId: string | null = null;
try {
- await startBackgroundJob();
+ jobId = await startBackgroundJob();
} catch (err) {
triggerToast(`❌ 后台挂起失败:${(err as Error).message}`);
return;
}
+ // 注册到全工作区通知器,作业完成时**跨页**弹「达成共识同款」提示。
+ if (jobId) watchJob(jobId);
// 重置回第一步(同新建任务);后台作业在历史记录里继续跑、可点开看进度。
resetToStep1();
triggerToast("🚀 已转入后台,任务会自动跑完研讨、总结报告与结果绘制;可在「历史记录」查看进度");
void drafts.refresh();
- }, [step2, drafts, startBackgroundJob, resetToStep1, triggerToast]);
+ }, [step2, drafts, startBackgroundJob, resetToStep1, triggerToast, watchJob]);
// ── 自动后台挂起:研讨进行中离开本页面(路由卸载)或刷新 / 关标签页时,任务自动转后台 ──
// 最新状态进 ref:卸载 cleanup 与 beforeunload 都是「拍快照时刻」的同步回调,
@@ -1205,6 +1223,7 @@ export default function RoundtablePlanningPage() {
intent: buildInitialIntent(),
agents,
model: selectedModel || undefined,
+ seatMode,
orchestrationMode,
chain: chainMeta,
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
diff --git a/offline-backend-20260512/backend/app/gateway/deps.py b/offline-backend-20260512/backend/app/gateway/deps.py
index 109d32d2..7813ffc2 100644
--- a/offline-backend-20260512/backend/app/gateway/deps.py
+++ b/offline-backend-20260512/backend/app/gateway/deps.py
@@ -259,7 +259,14 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
_app = app # 闭包捕获 app,供网关访问 app.state(checkpointer/thread_store)
gateway_factory = lambda p: InProcessRoundtableGateway( # noqa: E731
- _app, user_id=p.user_id, agents=p.agents, model=p.model
+ _app,
+ user_id=p.user_id,
+ agents=p.agents,
+ model=p.model,
+ # 席位执行模式沿用 Step2 选择,后台席位 run 据此覆盖 policy 的 thinking/reasoning + 开子代理。
+ seat_thinking_enabled=p.seat_thinking_enabled,
+ seat_reasoning_effort=p.seat_reasoning_effort,
+ seat_subagent_enabled=p.seat_subagent_enabled,
)
logger.info("[roundtable-jobs] gateway = inprocess (real models, default)")
diff --git a/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py b/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
index 948075c8..b0c494d4 100644
--- a/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
+++ b/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
@@ -164,6 +164,7 @@ async def _run_agent_turn(
skill_stop_names: list[str],
thinking_enabled: bool,
reasoning_effort: str,
+ subagent_enabled: bool = False,
) -> list[dict[str, Any]]:
"""进程内驱动 agent_name 在 thread_id 上跑一轮(发 message),阻塞到完成,返回最终 messages。
@@ -184,7 +185,8 @@ async def _run_agent_turn(
"reasoning_effort": reasoning_effort,
"thinking_enabled": thinking_enabled,
"is_plan_mode": False,
- "subagent_enabled": False,
+ # ultra 席位才置真 → 解锁 task 子代理工具(席位 policy 未禁 task)。leader 恒 False。
+ "subagent_enabled": subagent_enabled,
"thread_id": thread_id,
"memory_recall_disabled": True,
# 与 multi_agent._run_payload 对齐:关 builtin 记忆注入,避免单例总控/席位
@@ -262,7 +264,13 @@ def _read_summary_md(app, thread_id: str, user_id: str) -> str | None:
# it all at once」后发出超长 write_file,单次输出被 max_tokens 截断 → 工具调用从未执行、
# run 以 success 收尾但 outputs 下没有 .html → step3.html 回退成开场白文字。
# 自愈:检测产物缺失/截断后,同线程追加纠偏消息(要求 write_file 分段 + append 续写)重试。
-_ARTIFACT_RETRY_LIMIT = 2
+#
+# 取值依据(2026-06-15 修复「后台 HTML 看板只生成一半、概率渲染失败」):模型单轮输出受
+# ``config.yaml`` 的 ``max_tokens``(deepseek-chat = 4096)硬截。一份完整看板 HTML 几十 KB
+# ≈ 上万 token,单轮写不完;旧值 2(最多 3 轮 ≈ 12k token)对长报告不够 → 落库半截 HTML →
+# 渲染失败。调到 5(最多 6 轮 append ≈ 24k token,足够绝大多数看板)显著降低截断概率。
+# 循环命中完整产物(``