@
feat(圆桌): Step3 三产物并行 + 大屏多页模式 + 后台挂起查看恢复流程 后台挂起 Step3: - engine 总结报告/结果绘制/大屏数据 asyncio.gather 三流并行,故障隔离 - request.state 回归修复(进程内虚拟 request 补 state),恢复后台报告 - ROUNDTABLE_JOB_GATEWAY 默认 inprocess;产物自愈重试(截断/缺失纠偏) - 大屏后端只产 report-json(dashboardJson),前端按当前主题渲染 结果绘制大屏多页模式(模式二): - report-template 模板渲染(report-json→大屏HTML),零截断、主题跟随 - 与模式一同款交互(流式+沙箱+可修改);沙箱新窗口打开按钮 最终方案展示:左侧席位栏+总控头像、交付/文件合并、收展动画、内联沙箱 后台挂起查看=恢复实时流程: - handleLoadDraft 返回 Promise,await hydrate 后订阅 streamJob SSE 跟随 - saveDraftNow 强制存(发起前+查看前各一次)防丢 第二步优化:总控判断串/并行(并行单列可写)、席位上下文+交付模板、 peerDeliveries、失败重试、共识标记、流程图小窗(缩放/拖动/并行批) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> @
This commit is contained in:
1 parent
a7596001c5
commit
d5f319d9cb
51 files changed
+5016
-1186
No files matched your search
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* report-template.ts 纯函数单测(大屏多页模式的契约层)。
|
||||
* 运行方式(项目无 vitest,对齐 flow-graph.test.ts):
|
||||
* npx esbuild src/roundtable-planning/lib/report-template.test.ts --bundle --format=esm \
|
||||
* --platform=node --outfile=.tmp-tests/report-template.test.mjs
|
||||
* node --test .tmp-tests/report-template.test.mjs
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildDashboardPrompt,
|
||||
extractReportData,
|
||||
isDashboardHtml,
|
||||
renderDashboardHtml,
|
||||
RT_DASHBOARD_MARKER,
|
||||
stripReportFence,
|
||||
} from "./report-template";
|
||||
|
||||
const SAMPLE = [
|
||||
"下面是大屏数据。",
|
||||
"```report-json",
|
||||
JSON.stringify({
|
||||
title: "智能机房选址方案大屏",
|
||||
subtitle: "三地候选 · 综合评审",
|
||||
kpis: [
|
||||
{ label: "总投资", value: "1.2", unit: "亿元", trend: "up" },
|
||||
{ label: "工期", value: "14", unit: "个月" },
|
||||
],
|
||||
consensus: { headline: "推荐 B 地块方案", points: ["供电冗余最高", "扩容空间充足"] },
|
||||
charts: [
|
||||
{ type: "bar", title: "候选地块评分", items: [{ label: "A", value: 72 }, { label: "B", value: 89 }] },
|
||||
{ type: "donut", title: "投资构成", items: [{ label: "土建", value: 40 }, { label: "设备", value: 60 }] },
|
||||
],
|
||||
timeline: [{ label: "阶段一", desc: "勘察与立项" }],
|
||||
seats: [
|
||||
{ id: "seat-1", name: "情报收集", summary: "完成三地基础数据采集", sections: [{ heading: "数据来源", bullets: ["政务公开", "实地走访"] }], metrics: [{ label: "覆盖率", value: 92 }], risks: [], dependsOn: [] },
|
||||
{ id: "seat-2", name: "方案设计", summary: "B 地块双回路供电方案", sections: [], metrics: [], risks: [{ label: "供电瓶颈", level: "high" }], dependsOn: ["seat-1"] },
|
||||
],
|
||||
}),
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
test("extractReportData:完整块解析 + 字段归一化", () => {
|
||||
const d = extractReportData(SAMPLE)!;
|
||||
assert.equal(d.title, "智能机房选址方案大屏");
|
||||
assert.equal(d.kpis.length, 2);
|
||||
assert.equal(d.consensus.headline, "推荐 B 地块方案");
|
||||
assert.equal(d.charts.length, 2);
|
||||
assert.equal(d.seats.length, 2);
|
||||
assert.deepEqual(d.seats[1].dependsOn, ["seat-1"]);
|
||||
});
|
||||
|
||||
test("extractReportData:dependsOn 只保留真实存在的席位 id(剔除模型编造/自引用)", () => {
|
||||
const text = [
|
||||
"```report-json",
|
||||
JSON.stringify({
|
||||
title: "t",
|
||||
seats: [
|
||||
{ id: "s1", name: "甲", dependsOn: ["s1", "s2", "ghost"] },
|
||||
{ id: "s2", name: "乙", dependsOn: [] },
|
||||
],
|
||||
}),
|
||||
"```",
|
||||
].join("\n");
|
||||
const d = extractReportData(text)!;
|
||||
assert.deepEqual(d.seats[0].dependsOn, ["s2"]);
|
||||
});
|
||||
|
||||
test("extractReportData:无 fence / 坏 JSON / 空数据返回 null", () => {
|
||||
assert.equal(extractReportData("没有数据块"), null);
|
||||
assert.equal(extractReportData("```report-json\n{broken\n```"), null);
|
||||
assert.equal(extractReportData("```report-json\n{}\n```"), null);
|
||||
assert.equal(extractReportData(null), null);
|
||||
});
|
||||
|
||||
test("renderDashboardHtml:完整文档 + marker + 总览/席位多页 + :target 导航", () => {
|
||||
const d = extractReportData(SAMPLE)!;
|
||||
const html = renderDashboardHtml(d, { generatedAt: "2026-06-13 10:00" });
|
||||
assert.ok(html.startsWith("<!doctype html>"));
|
||||
assert.ok(html.includes(RT_DASHBOARD_MARKER));
|
||||
assert.ok(html.trimEnd().endsWith("</html>"));
|
||||
assert.ok(isDashboardHtml(html));
|
||||
// 多页结构:总览 + 每席位一页
|
||||
assert.ok(html.includes('id="overview"'));
|
||||
assert.ok(html.includes('id="seat-1"'));
|
||||
assert.ok(html.includes('id="seat-2"'));
|
||||
// 纯 CSS :target 导航(零 JS:无 allow-scripts 沙箱也能切页)
|
||||
assert.ok(html.includes(".page:target"));
|
||||
assert.ok(!html.includes("<script"));
|
||||
// 席位卡片跳转 + 依赖互相跳转(方案设计 依赖→ 情报收集)
|
||||
assert.ok(html.includes('href="#seat-2"'));
|
||||
assert.ok(html.includes('class="dep" href="#seat-1"'));
|
||||
// 反向「被依赖」chip(情报收集页里出现 方案设计 ← 承接)
|
||||
assert.ok(html.includes("承接本席位"));
|
||||
// 共识详情纯 CSS 弹窗
|
||||
assert.ok(html.includes('id="consensus-modal"'));
|
||||
// 图表 SVG 由模板生成
|
||||
assert.ok(html.includes("<svg"));
|
||||
});
|
||||
|
||||
test("renderDashboardHtml:主题跟随 —— dark=深蓝规范 / light=浅红规范", () => {
|
||||
const d = extractReportData(SAMPLE)!;
|
||||
const dark = renderDashboardHtml(d, { theme: "dark" });
|
||||
// 深蓝规范:页面 #0C1836 / 卡片 #122347 / 主色 #1677FF(tokens.css §1)
|
||||
assert.ok(dark.includes("#0C1836"));
|
||||
assert.ok(dark.includes("#122347"));
|
||||
assert.ok(dark.includes("#1677FF"));
|
||||
const light = renderDashboardHtml(d, { theme: "light" });
|
||||
// 浅红规范:页面 #FAFAFA / 容器 #FFFFFF / 主色 #F5222D(§1 色彩)
|
||||
assert.ok(light.includes("#FAFAFA"));
|
||||
assert.ok(light.includes("#F5222D"));
|
||||
assert.ok(!light.includes("#0C1836"));
|
||||
// 缺省 = dark(向后兼容)
|
||||
const def = renderDashboardHtml(d);
|
||||
assert.ok(def.includes("#0C1836"));
|
||||
});
|
||||
|
||||
test("renderDashboardHtml:模型文本全部 HTML 转义(防注入/破版)", () => {
|
||||
const text = [
|
||||
"```report-json",
|
||||
JSON.stringify({
|
||||
title: '<img src=x onerror=alert(1)>',
|
||||
seats: [{ id: "s1", name: "甲<b>", summary: 'a"b<c>', dependsOn: [] }],
|
||||
consensus: { headline: "<script>bad()</script>", points: [] },
|
||||
}),
|
||||
"```",
|
||||
].join("\n");
|
||||
const d = extractReportData(text)!;
|
||||
const html = renderDashboardHtml(d);
|
||||
assert.ok(!html.includes("<img src=x"));
|
||||
assert.ok(!html.includes("<script>bad"));
|
||||
assert.ok(html.includes("<script>bad()</script>"));
|
||||
});
|
||||
|
||||
test("stripReportFence:闭合块替换为引导语;未闭合(流式中)用「正在输出」措辞", () => {
|
||||
const done = stripReportFence(SAMPLE, false);
|
||||
assert.ok(done.includes("已生成大屏数据"));
|
||||
assert.ok(!done.includes("```report-json"));
|
||||
const streaming = stripReportFence('说明\n```report-json\n{"title":', true);
|
||||
assert.ok(streaming.includes("正在输出大屏结构化数据"));
|
||||
assert.ok(!streaming.includes("```report-json"));
|
||||
// 无 fence 原样返回
|
||||
assert.equal(stripReportFence("普通文字", false), "普通文字");
|
||||
});
|
||||
|
||||
test("buildDashboardPrompt:禁工具声明 + 原料齐全 + 契约说明", () => {
|
||||
const prompt = buildDashboardPrompt({
|
||||
intentText: "选址决策",
|
||||
seatDeliveries: [{ sender: "情报收集", content: "三地数据" }],
|
||||
consensusText: "推荐 B 地块",
|
||||
});
|
||||
assert.ok(prompt.includes("不要调用任何工具"));
|
||||
assert.ok(prompt.includes("选址决策"));
|
||||
assert.ok(prompt.includes("情报收集") && prompt.includes("三地数据"));
|
||||
assert.ok(prompt.includes("推荐 B 地块"));
|
||||
assert.ok(prompt.includes("```report-json"));
|
||||
assert.ok(prompt.includes("dependsOn"));
|
||||
});
|
||||
Reference in new issue
Block a user