dsh-mcmp 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/pipeline.js CHANGED
@@ -1,73 +1,113 @@
1
1
  /**
2
- * dsh-mcmp · 数学建模论文自动化流水线 —— 论文写作主循环模块(Host 侧)
2
+ * dsh-mcmp · 数学建模竞赛自动化论文撰写系统 —— 论文写作主循环模块(Host 侧)
3
3
  *
4
- * 职责:论文写作的主循环流程与循环自身的运行状态,覆盖所有正式工作的循环部分。
5
- * - 步骤元数据(STEPS:8 步骤 × 19 迭代/轮)与提示词构造:
6
- * buildPrompt(内含「质疑驱动」环节)、buildFinalizePrompt(兜底定稿);
7
- * - 子智能体调度:runChild / pickProvider;
8
- * - 主循环编排:runPipeline —— 按轮 × 迭代逐个启动专家子智能体,
9
- * 含断点续跑计数(completedIterations)、连续失败熔断、中止处理、
10
- * S5 识图路径解析与兜底定稿(不论成败都补一次 论文定稿.md);
11
- * - 循环运行状态(state/logs/recording/active)与对外接口:
12
- * snapshot(进度快照)、abort(中止)、reset(清空状态与续跑记录)。
4
+ * 按《架构.md》v3.17 与《提示词.md》v3.4 实现正式流水线:
5
+ * - 五阶段流水线(执行层):阶段零(初始化)→ 阶段一(EDA)→ 阶段二(建模)
6
+ * 阶段三(撰写)→ 阶段四(排版),共 22 个子阶段,每个子阶段由专家子智能体执行;
7
+ * - 中控台(Supervisor)两个职能模块,均为独立子智能体:
8
+ * 扫描验证模块(执行Agent上报 SUCCESS 后按 3.3 六维度扫描,通过→索引 VALIDATED,
9
+ * 发现问题→转交评审决策模块)、评审决策模块(唯一评级机构,P0-P3 评级;
10
+ * P0/P1/P2→定位根源阶段、物理删除(deprecated/)、索引 OBSOLETE、写入教训文件、
11
+ * 阶段级回滚重跑;P3→精确定点修改;同一阶段回滚≥10 次→FORCED_FINAL 强制锁定);
12
+ * - 核心文件:FILE_INDEX.yaml(索引)、ROLLBACK_LESSONS.yaml(教训)、
13
+ * transactions.log(事务日志)、_report.yaml(Agent上报)、Final_Paper.md(兜底定稿);
14
+ * - 执行失败(非质量问题):重试 3 次指数退避(5s/10s/20s),全失败暂停人工介入,
15
+ * 不计入回滚计数器(架构 3.8);
16
+ * - 断点续跑:按会话落盘的 substage-done/rollback 事件回放游标(仅识别 v3 流程名)。
13
17
  *
14
- * 通过 createPipeline({ fs, subagents, agents }) 创建实例;程序入口模块(lib/index.js)
15
- * 负责触发与装配,显示界面模块(lib/client.js 浮窗) /mcmp-api 路由访问
16
- * snapshot/abort/reset 完成显示与交互。
18
+ * 快照契约(供入口 /loopstatus 与显示界面浮窗渲染,见 lib/index.js 顶部注释):
19
+ * stages/cur/supervisor/total/done/pct 字段按本模块 snapshot() 输出。
20
+ * 通过 createPipeline({ fs, subagents, agents, retryBackoffMs }) 创建实例;
21
+ * 程序入口模块(lib/index.js)负责触发与装配,显示界面模块(lib/client.js)经
22
+ * /mcmp-api 路由访问 snapshot/abort/reset 完成显示与交互。
17
23
  */
18
24
 
19
- export const FLOW_NAME = '数学建模论文流水线v2'
20
- // 全流水线缺陷台账:跨步骤错误登记、裁决与向上修复的唯一权威文件
21
- export const LEDGER = '00_缺陷与裁决台账.md'
22
- export const PER_ROUND = 19
25
+ export const FLOW_NAME = '数学建模竞赛自动化论文撰写系统v3'
26
+ // 流水线子阶段总数(4+3+5+7+3);入口模块用它做「已完成无需重复」校验
27
+ export const PER_ROUND = 22
23
28
 
24
- // ---------- 步骤元数据(8 步骤 × 19 迭代/轮) ----------
25
- const STEPS = [
26
- { key: 'S1', name: '赛题分析与建模方案', phase: 'S1 赛题分析与建模方案', file: '01_赛题分析.md', iters: [
27
- { name: '赛题精读与拆解', task: '(1) 环境清理:若工作区根目录已存在且含有 00_赛题原文.md 或 01_赛题分析.md(属于上一次运行),先用 shell 命令把整个「数学建模流水线」目录改名为 数学建模流水线_历史归档_YYYYMMDD_HHMMSS(只改名、绝不删除,完整保留历史成果),再重建空目录结构;(2) 通读赛题全文(含附件、表格、图),确认所选题目与全部子问题;(3) 列出问题背景、已知条件、数据说明、待求目标与提交要求(页数、格式、支撑材料);(4) 判断每个子问题的类型(预测/优化/评价/机理/决策等);(5) 圈出关键词与易误解之处(尤其题目附录中的条件定义),给出你的理解与疑问;(6) 将赛题原文完整保存为 00_赛题原文.md;(7) 建立目录结构:代码/、代码/results/、图表/;(8) 检查《缺陷与裁决台账》00_缺陷与裁决台账.md:**若已存在(多轮运行时第二轮起必然存在),直接读取,确认表头与使用规则完整即可,绝不覆盖或删除既有条目**;若不存在,按表头「编号|发现步骤|出错文件与位置|问题描述|证据|修复方案|状态(待修复/已修复/已否决)|修复人(轮次·步骤)|备注」创建,并在文件开头写入使用规则:①任何步骤发现前序步骤(或本步骤既往迭代)的错误,必须登记本台账,并**直接修复出错文件**(用编辑工具、在修改处标注「第R轮·Sx修正:D#编号」),不得只在下游文件里绕开;②修复后检查下游引用处,能同步的立即同步,不能同步的追加台账条目;③全流水线所有步骤以本台账最新裁决为最高权威,冲突时以台账为准;④状态由修复人维护,闭环后保留记录不删除。' },
28
- { name: '建模方案定稿', task: '(1) 数据特征分析(缺失、异常、量纲、规模)与预处理方案;(2) 逐条编号的合理假设(初版)并说明理由;(3) 每个子问题列出 2-3 种候选建模思路,逐一分析优劣与可行性;(4) 为每个子问题敲定选用的模型与方法并说明理由;(5) 给出总体建模框架(问题→数据→假设→模型→求解→验证→结论);(6) 输出《建模方案定稿》,作为后续所有步骤的权威依据;(7) 列出编程实现清单、图表清单(含技术路线图/架构图)与论文结构大纲。' },
29
- ] },
30
- { key: 'S2', name: '模型建立与求解', phase: 'S2 模型建立与求解', file: '02_模型建立与求解.md', iters: [
31
- { name: '模型建立', task: '(1) 建立全题统一的符号体系(符号表:含义+单位),后续步骤必须沿用;(2) 逐条编号模型假设(在 S1 基础上完善);(3) 对每个子问题建立数学模型:决策变量/目标函数/约束或方程,给出推导过程;(4) 说明模型与赛题要求的对应关系(尤其回应赛题附录中的条件定义);(5) 指出模型适用范围与前提。' },
32
- { name: '模型求解', task: '(1) 可解析求解的给出完整推导与闭式解;(2) 需数值求解的给出算法原理、步骤与伪代码(最优化/微分方程/统计/启发式),说明复杂度、收敛性与可行性;(3) 对关键公式用手算小例子验证;(4) 明确哪些结果将在 S3 用程序实现,给出期望输出的数值口径(精度、单位)。' },
33
- { name: '模型验证设计', task: '(1) 设计验证方案:特殊/退化情形、手算对照、交叉验证、独立仿真等;(2) 设计灵敏度分析方案(扰动参数、观察指标、扰动范围);(3) 检查模型假设的合理性,列出可放宽的假设及放宽后的修正方式;(4) 总结模型优点、不足与风险;(5) 把本轮形成的符号、公式、结论固化为 S3 的权威依据;若与 S1 定稿冲突,显式裁决:在《缺陷与裁决台账》登记,并直接修复出错文件(标注修正来源),不得只在本文件写裁决。' },
34
- ] },
35
- { key: 'S3', name: '编程实现与数值验证', phase: 'S3 编程实现与数值验证', file: '03_编程实现.md', iters: [
36
- { name: '核心代码', task: '(1) 按 01/02 权威成果用 Python 实现每个子问题的核心算法,代码写入 代码/ 目录,文件名含步骤与版本(如 s3_model_v1.py),配中文注释;(2) 数据读取与预处理;(3) 确保可直接运行;运行并把关键输出记录到 03_编程实现.md;(4) 若缺依赖先 pip 安装(设 3 分钟时限,失败则改用纯标准库实现);(5) 旧代码一律保留。' },
37
- { name: '调试与数值验证', task: '(1) 用手算小例子与极端参数对照测试;(2) 修复所有 bug,处理数据缺失/异常;(3) 边界情形测试;(4) 与 02 公式逐项核对,数值不一致必须定位原因并裁决(以程序实测为准):在《缺陷与裁决台账》登记,并直接修复出错的 02/01 文件(标注修正来源);(5) 数值结果落盘到 代码/results/ 的 CSV/JSON;(6) 新版本用 _v2/_v3 后缀另存,绝不删除旧版。' },
38
- { name: '封装与自测', task: '(1) 整理为函数/类,参数化关键量;(2) 统一输出 代码/results/ 结构化数据(CSV/JSON + 结果索引);(3) 提供 main 入口一键复现全部结果;(4) 编写自测套件(断言:数值口径、概率范围、收支非负、退化情形),全部通过并保留报告;(5) 记录运行环境(语言版本、依赖及版本)与复现步骤;(6) 输出《数值验证报告》:权威数值清单,供图表与论文引用。' },
39
- ] },
40
- { key: 'S4', name: '图表与流程图生成', phase: 'S4 图表与流程图生成', file: '04_图表生成.md', iters: [
41
- { name: '基础图表与结构图', task: '(1) 依据 03 权威 results/ 生成论文所需全部图:结果曲线、对比柱状图、散点+拟合、成本/误差分解图等;(2) 同时生成总体技术路线图与系统/模型架构图(graphviz/mermaid/matplotlib/networkx 或自建引擎,源文件保存 代码/,图片输出 图表/);(3) 优先尝试 pip 安装 matplotlib/PIL(3 分钟时限,失败则沿用/自建纯标准库 PNG 引擎);(4) 每张图:中文标题、轴标签、图例、单位,分辨率不低于 300dpi(或 1800×1200 + pHYs);(5) 布局防重叠:图例自动避让、长标签截断或换行、结论框/副标题预留空间、文本框之间留足间隙;(6) 绘图数据与 results/ 交叉核对(脚本断言),不符即退出;(7) 绘图代码保存 代码/。' },
42
- { name: '美化统一', task: '(1) 统一字号、配色、线型风格(集中式 STYLE 常量);(2) 修复中文乱码(字体字形探测/回退),禁用易乱码字符(如下标数字、组合音标),统一 U+2212 为 "-";(3) 每张图加副标题(参数/口径说明)、结论框(关键数值+结论)、数据来源行;(4) 补充关键标注(重要数值、结论性说明);(5) 图内符号与论文符号体系一致;(6) 保证每张图脱离正文也能看懂。' },
43
- { name: '精选定稿', task: '(1) 从全部图中精选论文真正需要的图(结果图+技术路线图+架构图),宁缺毋滥;(2) 确定编号(图1、图2…)与插入章节;(3) 冗余图移入 图表/归档/,不物理删除;(4) 定稿图统一导出到 图表/定稿/;(5) 输出《图表清单》(编号/路径/插入章节/一句话说明)与《程序化自检报告》(尺寸/墨量/数据断言);(6) 为 S5 视觉自检做好准备:列出每张定稿图对应的绘图代码与重渲染命令。' },
29
+ // 架构核心文件名(附录A)
30
+ const INDEX_FILE = 'FILE_INDEX.yaml'
31
+ const LESSONS_FILE = 'ROLLBACK_LESSONS.yaml'
32
+ const TX_LOG = 'transactions.log'
33
+ const FINAL_PAPER = 'Final_Paper.md'
34
+ const DEPRECATED_DIR = 'deprecated/'
35
+ // 防崩溃:同一阶段回滚上限(架构 3.7);默认 10,可由 --rollback-limit 参数化配置
36
+ const DEFAULT_ROLLBACK_LIMIT = 10
37
+
38
+ // ---------- 统一质疑标准(架构 3.3,全部六个维度,Agent自检与中控台扫描同标准) ----------
39
+ const DIM_STANDARD = [
40
+ '【统一质疑标准(3.3,自检/扫描必须覆盖全部六个维度)】',
41
+ '1. 逻辑自洽性:结论是否支持上一阶段假设?段落逻辑是否顺畅?编号体系是否一致?',
42
+ '2. 模型合理性:是否符合常识?有无更优替代方案?模型复杂度与问题是否匹配?',
43
+ '3. 数据敏感性:结论对数据变化是否过于敏感?关键假设放宽后是否稳定?',
44
+ '4. 视觉合理性:图表是否清晰表达数据特征或模型结构?图表类型是否合理?可视化结果是否与数值结论一致?',
45
+ '5. 规范符合性:LaTeX语法、Markdown格式、路径是否存在、引用编号是否连续?',
46
+ '6. 教训对照:是否重复历史教训文件中的错误?逐条比对 AVOIDANCE_GUIDANCE。',
47
+ ].join('\n')
48
+
49
+ // ---------- 视觉模块两阶段流程(架构 3.3 / 提示词 2.1-5) ----------
50
+ const VISION_FLOW = [
51
+ '【视觉模块调用(涉及图表时,按两阶段流程执行)】',
52
+ '第一阶段——详实描述:调用视觉模块对图表内容进行清晰、完整、有条理的自然语言描述(数据分布、拟合/对比关系、趋势方向与关键转折点、数据密度与重叠遮蔽、分组分离度、坐标轴与标注、视觉特征、背景信息与数据来源偏见、图表叙事与行动建议)。',
53
+ '第二阶段——审阅与质疑:对视觉描述进行系统性质疑(描述合理性、上下文一致性、趋势审阅、密度与遮蔽审阅、分组审阅、背景偏见审阅、叙事与行动审阅、误导性识别、结论验证),发现问题纳入整体质疑结果按 3.4 评级处置。',
54
+ '视觉模块不可用时,必须在产出与 _report.yaml 中标记"待人工确认"。',
55
+ ].join('\n')
56
+
57
+ // ---------- 五阶段 × 22 子阶段(提示词.md 三) ----------
58
+ const STAGES = [
59
+ { key: '阶段零', name: '初始化', dir: 'stage_00_outputs', subs: [
60
+ { id: '0.1', name: '创建目录', task: '创建标准化目录结构', outputs: 'config/、raw_data/、stage_XX_outputs/、deprecated/、code/、figures/', focusDims: '规范符合性', focusItems: '目录结构完整?权限正确?', vision: false },
61
+ { id: '0.2', name: '加载数据', task: '读取竞赛附件数据', outputs: '数据加载报告(文件名、行数、列数、编码)', focusDims: '规范符合性、逻辑自洽性', focusItems: '完整读取?编码正确?隐藏Sheet?', vision: false },
62
+ { id: '0.3', name: '初始化索引', task: '创建FILE_INDEX.yaml', outputs: '含PROJ-001和DATA-001记录', focusDims: '规范符合性', focusItems: '格式正确?准确记录所有初始文件?', vision: false },
63
+ { id: '0.4', name: '初始化教训文件', task: '创建ROLLBACK_LESSONS.yaml', outputs: '空框架(LESSONS: [])', focusDims: '规范符合性', focusItems: '创建成功?格式正确?', vision: false },
44
64
  ] },
45
- { key: 'S5', name: '图表视觉自检与修复', phase: 'S5 图表视觉自检与修复', file: '05_图表自检.md', iters: [
46
- { name: '视觉自检与问题清单', vision: true, task: '(1) 逐张检查 图表/定稿/ 的全部图片,重点:①文字重叠/相互遮挡;②乱码、豆腐块、缺字;③文字被裁切/越出画布;④标题、轴标签、图例缺失或不完整;⑤标注数值与 代码/results/ 权威数据不一致;⑥曲线/柱体被文字遮挡。若你具备识图能力(视觉能力:可用):用识图工具逐张查看(必要时裁剪放大局部),列出每张图的问题清单(问题描述+图内位置+严重级:A=必须修复/B=应当修复)。若不具备识图能力(视觉能力:不可用):编写并运行 Python 分析脚本——在绘图代码中记录每个文本的包围盒(x,y,w,h)并计算两两交叠面积、文本框越界检测、字形回退(豆腐块)探测、墨量/空白检测,输出每图问题 JSON 报告。(2) 汇总为《图表视觉自检报告》写入 05_图表自检.md:每图问题清单+分级+修复建议;(3) 分析脚本保存 代码/。' },
47
- { name: '修复与复检', vision: true, task: '(1) 对《图表视觉自检报告》中全部 A/B 级问题逐条实际修复:修改绘图代码(调整布局/字号/标签/图例位置/增加间距/换行/去重),重渲染出新版 PNG(定稿目录覆盖同名或 _v2 另存,旧版移入 归档/);(2) 每条修复登记到《修复验证表》:问题→修复方式(代码改动描述)→复检证据(视觉复核结论或脚本输出:重叠数=0);(3) 修复后再次自检(识图工具或脚本),A 级问题必须清零;若仍保留 B 级,给出取舍理由;(4) 更新 04_图表生成.md 的《图表清单》,注明每张图已通过视觉自检;(5) 禁止只记录不修复:每个问题必须有"修复+复检证据"或"显式弃修理由"。' },
65
+ { key: '阶段一', name: 'EDA', dir: 'stage_01_outputs', subs: [
66
+ { id: '1.1', name: '清洗', task: '处理缺失值、异常值', outputs: '清洗后数据集(CSV)、清洗报告', focusDims: '逻辑自洽性、数据敏感性、教训对照', focusItems: '插补方法合理?剔除有依据?无未来信息?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: false },
67
+ { id: '1.2', name: '特征工程', task: '特征缩放、编码、新特征构造', outputs: '变换后数据集、特征工程报告', focusDims: '逻辑自洽性、模型合理性、教训对照', focusItems: '标准化方法合理?特征有业务意义?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: false },
68
+ { id: '1.3', name: 'EDA可视化', task: '生成EDA图表', outputs: '热力图、直方图、箱线图等', focusDims: '视觉合理性、规范符合性', focusItems: '图表清晰?类型合理?标尺标注单位?', vision: 'must' },
48
69
  ] },
49
- { key: 'S6', name: '论文撰写', phase: 'S6 论文撰写', file: '06_论文.md', iters: [
50
- { name: '初稿', task: '(1) 依据前 5 步权威成果撰写论文初稿,写入 06_论文.md 新的一节;(2) 标准结构:摘要(单独成页、含关键词,突出方法/结果/创新)、一 问题重述与分析、二 模型假设与符号说明、三 模型建立与求解(分问题、公式编号)、四 模型验证与灵敏度分析、五 模型评价与推广、六 参考文献、附录(程序清单);(3) 正文全部结论必须引用前序成果(图表、代码输出),不得凭空捏造数据;(4) 图表用相对路径引用 图表/定稿/ 的图,编号与《图表清单》一致;(5) 严禁流水线内部痕迹:不得出现"01_赛题分析.md""02 迭代3""03 步骤""流水线"等字样,证据一律表述为"支撑材料/附录";(6) 公式编号连续、符号与 02 一致。' },
51
- { name: '润色终稿', task: '(1) 以评审视角逐节质疑:摘要是否一页内?公式与符号前后一致、编号连续?图表引用齐全、编号正确?有无"显然/易得"式跳步?灵敏度结论是否量化?参考文献格式规范?(2) 修订后重写一份完整、连续的终稿,写入新的一节(标题注明「终稿」),全文完整、可直接提交——不允许用"修订段落清单"代替完整终稿;(3) 保留初稿内容不删;(4) 全文检索流水线痕迹与内部文档名并清零;(5) 结尾附《终稿质检清单》(逐项 ✓/✗)。' },
70
+ { key: '阶段二', name: '建模', dir: 'stage_02_outputs', subs: [
71
+ { id: '2.1', name: '模型筛选', task: '根据问题类型筛选候选模型', outputs: '候选模型列表、适用性分析', focusDims: '模型合理性、逻辑自洽性、教训对照', focusItems: '模型贴切问题?考虑更优方案?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: false },
72
+ { id: '2.2', name: '模型建立', task: '建立数学模型,声明假设,推导公式', outputs: '假设清单、公式推导(含LaTeX)', focusDims: '逻辑自洽性、模型合理性、规范符合性', focusItems: '假设合理?推导正确?LaTeX语法正确?', vision: false },
73
+ { id: '2.3', name: '模型求解', task: '编写代码,执行计算', outputs: '求解代码(/code/)、计算结果表', focusDims: '规范符合性、数据敏感性', focusItems: '代码可执行?结果合理?含注释?', vision: false },
74
+ { id: '2.4', name: '图表生成', task: '生成数据对比图、结构示意图、流程图、结果可视化图', outputs: '图表文件(/stage_02_outputs/figures/),索引中有FILE_ID', focusDims: '视觉合理性、规范符合性', focusItems: '图表清晰表达?类型合理?坐标轴标注完整?无截断误导?', vision: 'must' },
75
+ { id: '2.5', name: '检验与质疑', task: '按3.3标准对模型系统性质疑', outputs: '检验与质疑报告(含模板中的表格)', focusDims: '全部六个维度', focusItems: '覆盖六个质疑维度(逻辑自洽性、模型合理性、数据敏感性、视觉合理性、规范符合性、教训对照)?发现问题仅事实上报?报告本身作为阶段产出接受中控台扫描验证', vision: 'if',
76
+ extra: '检验与质疑报告模板:\n# 检验与质疑报告\n## 1. 被质疑的模型/图表\n## 2. 质疑依据(表格:维度/检查结果/说明)\n## 3. 结论(是否发现严重问题,仅事实上报)\n## 4. 改进方向' },
52
77
  ] },
53
- { key: 'S7', name: '评审与修复落实', phase: 'S7 评审与修复落实', file: '07_评审修复.md', iters: [
54
- { name: '合规检查与评审', task: '(1) 按国赛要求逐项检查 06 终稿:摘要单独成页含关键词、无学校/队员/指导教师信息、页数符合要求、图表编号与引用一一对应、公式编号连续、参考文献格式、附录程序清单、结果表数值与 results/ 一致、提交文件命名;(2) 以竞赛评审专家身份按国赛评审标准(摘要约10%、假设合理性、建模科学性、求解正确性、验证充分性、写作规范性、创新性)打分并写评语;(3) 汇总输出《合规检查报告》+《评审报告》+《问题清单》(每项含:级别 A=必须修复/B=应当修复/C=建议、位置、证据、修复建议);(4) 建立《问题跟踪表》(编号/描述/级别/状态=待修复),写入 07_评审修复.md;(5) 《问题清单》与《问题跟踪表》中凡涉及前序步骤文件的条目(模型/公式/数值/图表源头错误),同时登记到《缺陷与裁决台账》(若尚未登记),避免只在 06 论文层面打补丁。' },
55
- { name: '修复落实', task: '(1) 对《问题清单》中全部 A/B 级问题逐一实际修复,而不是只写处理意见:论文问题 → 直接用编辑工具修改 06_论文.md 终稿正文(修改后终稿仍是完整连续的全文,禁止再追加"修订段落");图表问题 修改绘图代码并重渲染、复检;数值问题 → 修改代码重跑,与 results/ 复核一致后再更新论文;源头在 01~05 的问题 → 直接修复源头文件并在《缺陷与裁决台账》登记(标注修正来源),再同步下游引用;(2) 每条修复在《问题跟踪表》中登记:修复方式(文件+改动描述)、证据(命令/复检输出/断言结果)、状态(已修复);无法修复的写明阻塞原因与后续建议(状态:受阻);(3) 修复不得破坏既有正确内容;(4) 同步更新论文中受影响的图表引用与编号。' },
56
- { name: '修复复核', task: '(1) 重新逐项核验《问题跟踪表》:每项重新检查(重跑自测/断言/视觉复检/文本检索),确认修复真实生效,状态更新为"已验证";(2) 对未闭环项继续修复;(3) 输出《修复验证报告》:问题→状态→验证证据,给出闭环率(A 级必须 100%);(4) 更新评审打分(修复后预估);(5) S8 定稿列出移交清单:论文终稿路径、图表清单、代码入口、复现命令。' },
78
+ { key: '阶段三', name: '撰写', dir: 'stage_03_outputs', subs: [
79
+ { id: '3.1', name: '重述与背景', task: '撰写问题重述与背景', outputs: '/stage_03_outputs/01_problem_restatement.md', focusDims: '逻辑自洽性、教训对照', focusItems: '准确理解题目?背景充分?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: false },
80
+ { id: '3.2', name: '假设与符号', task: '撰写假设与符号说明', outputs: '/stage_03_outputs/02_assumptions.md', focusDims: '逻辑自洽性、规范符合性、教训对照', focusItems: '假设对应模型?符号清晰定义?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: false },
81
+ { id: '3.3', name: '建模与求解章节', task: '撰写建模与求解(含图表嵌入)', outputs: '/stage_03_outputs/03_modeling_solution.md', focusDims: '逻辑自洽性、规范符合性、视觉合理性', focusItems: '图表嵌入正确?编号与正文一致?图表在索引中存在?', vision: 'if' },
82
+ { id: '3.4', name: '结果分析', task: '撰写结果分析与模型评价', outputs: '/stage_03_outputs/04_analysis.md', focusDims: '逻辑自洽性、数据敏感性、教训对照', focusItems: '分析充分?评价客观?按3.3教训对照维度逐条比对AVOIDANCE_GUIDANCE', vision: 'if' },
83
+ { id: '3.5', name: '摘要', task: '撰写摘要(最后进行)', outputs: '/stage_03_outputs/05_abstract.md', focusDims: '逻辑自洽性、规范符合性', focusItems: '覆盖核心贡献?含关键数值?', vision: false },
84
+ { id: '3.6', name: '参考文献', task: '整理参考文献', outputs: '/stage_03_outputs/06_references.md', focusDims: '规范符合性', focusItems: '编号连续?格式统一?', vision: false },
85
+ { id: '3.7', name: '附录代码', task: '将/code/核心代码整理为附录', outputs: '/stage_03_outputs/07_appendix_code.md', focusDims: '规范符合性', focusItems: '代码完整?注释清晰?变量名与正文一致?引用的代码文件在索引中存在对应记录', vision: false,
86
+ extra: '代码筛选标准:仅核心求解代码,排除测试/调试代码,单段≤100行。' },
57
87
  ] },
58
- { key: 'S8', name: '论文定稿与提交包', phase: 'S8 论文定稿与提交包', file: '08_论文定稿.md', iters: [
59
- { name: '论文定稿与提交包', task: '(1) 无论前序步骤是否全部成功,本步骤必须产出一版完整论文:检查 06_论文.md 是否有干净完整的终稿;若有,以其为底稿做最终校对后写入 论文定稿.md;若终稿缺失/混乱/不完整,则依据 00_赛题原文.md、01~07 全部成果与 代码/results/、图表/定稿/ 从头撰写完整论文,写入 论文定稿.md;(2) 论文定稿.md 必须是可直接提交的完整 Markdown:标题、摘要(含关键词)、问题重述与分析、模型假设与符号说明、模型建立与求解(公式编号)、模型验证与灵敏度分析、模型评价与推广、参考文献、附录,图表用相对路径引用;(3) 全文检索并清除流水线内部痕迹与参赛者信息;(4) 数值只采用 results/ 权威数据或前序已验证结论,缺失时明确标注"待补充"并给出推导,不得编造;(5) 输出《提交材料清单》(论文定稿.md、图表、代码、results/、复现说明)与《最后校对报告》;(6) 本步骤工作记录写入 08_论文定稿.md,最终论文全文写入 工作区根目录的 论文定稿.md;(7) 终检《缺陷与裁决台账》:凡状态仍为「待修复」且影响论文结论的条目,在本步骤内尽量闭环;确实无法闭环的,整理为「遗留缺陷清单」写入 08_论文定稿.md(供使用者参考,不得进入论文正文)。' },
88
+ { key: '阶段四', name: '排版', dir: 'stage_04_outputs', subs: [
89
+ { id: '4.1', name: '合并', task: '合并各章节', outputs: '/stage_04_outputs/merged_draft.md', focusDims: '规范符合性', focusItems: '章节顺序正确?无遗漏?', vision: false },
90
+ { id: '4.2', name: '修正路径', task: '修正图表路径', outputs: '/stage_04_outputs/merged_with_figures.md', focusDims: '规范符合性、视觉合理性', focusItems: '路径存在?引用格式正确?', vision: 'if' },
91
+ { id: '4.3', name: '最终产出', task: '生成Final_Paper.md', outputs: '/Final_Paper.md', focusDims: '全部六个维度', focusItems: '格式符合6.1?引用正确?所有图表路径有效?编号连续?', vision: 'must' },
60
92
  ] },
61
93
  ]
62
94
  const FLAT = []
63
- STEPS.forEach((st, si) => { st.iters.forEach((nm, ii) => { FLAT.push({ s: si, i: ii, name: nm.name }) }) })
95
+ STAGES.forEach((st, si) => { st.subs.forEach((sub, ii) => { FLAT.push({ s: si, i: ii, key: sub.id, name: sub.name }) }) })
96
+ // 每阶段在 FLAT 中的起始下标(阶段级回滚/断点续跑游标复位用)
97
+ const STAGE_OFFSETS = []
98
+ { let o = 0; for (const st of STAGES) { STAGE_OFFSETS.push(o); o += st.subs.length } }
99
+
100
+ // 执行失败重试(架构 3.8 / 提示词 2.2):重试 3 次指数退避
101
+ const DEFAULT_RETRY_DELAYS = [5000, 10000, 20000]
64
102
 
65
103
  /**
66
104
  * 创建论文写作主循环实例。
67
- * @param {object} services 宿主服务:{ fs, subagents, agents }
68
- * @returns 主循环对外接口:{ isRunning, progressPercent, completedIterations, snapshot, trackSession, start, abort, reset }
105
+ * @param {object} services 宿主服务:{ fs, subagents, agents } + 可选 retryBackoffMs(测试用)
106
+ * @returns 主循环对外接口:{ isRunning, progressPercent, completedIterations, snapshot, log, trackSession, start, abort, reset }
69
107
  */
70
- export function createPipeline({ fs, subagents, agents }) {
108
+ export function createPipeline({ fs, subagents, agents, retryBackoffMs }) {
109
+ const RETRY_DELAYS = Array.isArray(retryBackoffMs) && retryBackoffMs.length > 0 ? retryBackoffMs : DEFAULT_RETRY_DELAYS
110
+
71
111
  // ---------- 运行状态 ----------
72
112
  function freshState() {
73
113
  return {
@@ -81,6 +121,9 @@ export function createPipeline({ fs, subagents, agents }) {
81
121
  let active = null
82
122
  let lastSession = null // 最近一次运行所属会话,供「清空状态」追加重置标记
83
123
  const recording = new Map()
124
+ // v3 运行期状态(供 snapshot 输出 stages/supervisor 契约字段)
125
+ let stageDone = new Array(STAGES.length).fill(0) // 每阶段已完成子阶段数
126
+ let supervisor = null // { module, rollbackCount, rollbackLimit, forcedFinal, issues:{P0..P3} }
84
127
 
85
128
  function fmtTime(ts) {
86
129
  const d = new Date(ts)
@@ -128,51 +171,93 @@ export function createPipeline({ fs, subagents, agents }) {
128
171
  return undefined
129
172
  }
130
173
 
131
- // ---------- 断点续跑:从会话日志(落盘)恢复已完成迭代数(仅识别 v2 流程名) ----------
174
+ function sleep(ms, signal) {
175
+ return new Promise((resolve) => {
176
+ if (signal && signal.aborted) { resolve(); return }
177
+ const t = setTimeout(resolve, ms)
178
+ if (signal && typeof signal.addEventListener === 'function') {
179
+ signal.addEventListener('abort', () => { clearTimeout(t); resolve() }, { once: true })
180
+ }
181
+ })
182
+ }
183
+
184
+ // ---------- 事务日志(架构 3.8 / 附录A):尽力写入,失败静默 ----------
185
+ async function txLog(args, line) {
186
+ if (!fs || !args.wsDir) return
187
+ try {
188
+ const t = await fs.resolve(args.wsDir + '/' + TX_LOG)
189
+ let prev = ''
190
+ try { prev = await fs.readText(t) } catch (err) { /* 首次写入 */ }
191
+ await fs.writeText(t, prev + line + '\n')
192
+ } catch (err) { /* 宿主 fs 不支持写入时忽略(面板日志仍可见) */ }
193
+ }
194
+
195
+ // ---------- 读取 _report.yaml(执行Agent上报;处理后的移入 deprecated/ 由中控台子智能体执行) ----------
196
+ // expectId:当前子阶段 id;报告内 agent_id 与之一致才有效,防止误读上一子阶段的旧报告(架构 3.10.2)
197
+ async function readReportText(args, stage, expectId) {
198
+ if (!fs || !args.wsDir) return null
199
+ try {
200
+ const t = await fs.resolve(args.wsDir + '/' + stage.dir + '/_report.yaml')
201
+ const txt = await fs.readText(t)
202
+ if (expectId !== undefined) {
203
+ const m = /^\s*agent_id\s*:\s*"?([^"\n]+?)"?\s*$/m.exec(txt)
204
+ if (m && m[1].trim() !== expectId) return null // 旧报告残留,视为缺失
205
+ }
206
+ return txt
207
+ } catch (err) { return null }
208
+ }
209
+
210
+ async function readReportStatus(args, stage, expectId) {
211
+ const txt = await readReportText(args, stage, expectId)
212
+ if (!txt) return null
213
+ const m = /^\s*status\s*:\s*"?([A-Z_]+)"?\s*$/m.exec(txt)
214
+ return m ? m[1] : null
215
+ }
216
+
217
+ async function listOutputs(args, stage) {
218
+ if (!fs || !args.wsDir) return []
219
+ try {
220
+ const t = await fs.resolve(args.wsDir + '/' + stage.dir)
221
+ const entries = await fs.listDir(t)
222
+ return (entries || []).map((e) => e.name).slice(0, 40)
223
+ } catch (err) { return [] }
224
+ }
225
+
226
+ // ---------- 断点续跑:回放本会话最后一次 v3 运行的 substage-done/rollback 事件,得到连续完成的子阶段数 ----------
132
227
  function completedIterations(session) {
133
228
  if (!session || !Array.isArray(session.events)) return 0
134
- const runs = []
135
229
  let current = null
136
230
  for (const ev of session.events) {
137
231
  try {
138
232
  if (ev.type === 'tool-workflow/mcmp-reset') {
139
- // 用户点过「清空状态/重置」:此前的所有运行记录作废,只从标记之后开始计数
140
- runs.length = 0
233
+ // 用户点过「清空状态/重置」:此前的运行记录作废,只从标记之后开始计数
141
234
  current = null
142
235
  continue
143
236
  }
144
237
  if (ev.type === 'tool-workflow/run-start') {
145
238
  const d = ev.data || {}
146
- if (d.name === FLOW_NAME) {
147
- current = { done: new Set() }
148
- runs.push(current)
149
- } else {
150
- current = null
151
- }
152
- } else if (ev.type === 'tool-workflow/agent-end' && current) {
153
- const d = ev.data || {}
154
- if (Number.isSafeInteger(d.seq) && d.seq >= 1 && d.outcome === 'completed') current.done.add(d.seq)
239
+ current = d.name === FLOW_NAME ? [] : null
240
+ } else if (current && ev.type === 'tool-workflow/substage-done') {
241
+ current.push(ev.data || {})
242
+ } else if (current && ev.type === 'tool-workflow/rollback') {
243
+ current.push(ev.data || {})
155
244
  }
156
245
  } catch (err) { /* 单条事件解析失败忽略 */ }
157
246
  }
158
- // 一次运行从最小序号 S 起连续完成 C 个迭代,则该运行贡献 = (S-1) + C。
159
- // 这样多轮/续跑运行(seq 20、39…继续编号)也能正确累计,而不是只数 1..19。
160
- const contribution = (run) => {
161
- if (!run || run.done.size === 0) return 0
162
- let S = Infinity
163
- for (const seq of run.done) if (seq < S) S = seq
164
- let c = 0
165
- while (run.done.has(S + c)) c++
166
- return (S - 1) + c
167
- }
168
- const last = runs[runs.length - 1]
169
- let n = last ? contribution(last) : 0
170
- if (n === 0) {
171
- let m = 0
172
- for (const run of runs) m = Math.max(m, contribution(run))
173
- n = m
247
+ if (!current) return 0
248
+ let p = 0
249
+ for (const ev of current) {
250
+ if (ev.subKey !== undefined) {
251
+ const expect = FLAT[p % FLAT.length]
252
+ if (expect && expect.key === ev.subKey) p += 1
253
+ } else if (ev.targetIdx !== undefined) {
254
+ const idx = Number(ev.targetIdx)
255
+ if (Number.isSafeInteger(idx) && idx >= 0 && idx < STAGE_OFFSETS.length) {
256
+ p = Math.min(p, STAGE_OFFSETS[idx])
257
+ }
258
+ }
174
259
  }
175
- return n
260
+ return p
176
261
  }
177
262
 
178
263
  function snapshot() {
@@ -180,26 +265,23 @@ export function createPipeline({ fs, subagents, agents }) {
180
265
  const total = state.total
181
266
  const pct = total > 0 ? Math.min(100, Math.floor((done * 100) / total)) : 0
182
267
  const round = state.rounds > 0 ? Math.min(Math.floor(done / PER_ROUND) + 1, state.rounds) : 0
183
- const roundStart = (round - 1) * PER_ROUND
184
- const inRound = Math.max(0, Math.min(done - roundStart, PER_ROUND))
185
268
  let cur = null
186
269
  if (state.status === 'running' && total > 0 && done < total) {
187
270
  const f = FLAT[done % PER_ROUND]
188
- cur = { stepKey: STEPS[f.s].key, stepName: STEPS[f.s].name, stepIdx: f.s, iterIdx: f.i, iterName: f.name, iterTotal: STEPS[f.s].iters.length }
271
+ const st = STAGES[f.s]
272
+ cur = { stageKey: st.key, stageIdx: f.s, stageName: st.name, subIdx: f.i, subName: st.subs[f.i].name, subTotal: st.subs.length }
189
273
  }
190
- const counts = {}
191
- for (let k = 0; k < inRound && k < FLAT.length; k++) {
192
- const f = FLAT[k]
193
- counts[f.s] = (counts[f.s] || 0) + 1
194
- }
195
- const steps = STEPS.map((st, si) => ({
196
- key: st.key, name: st.name, file: st.file, phase: st.phase,
197
- iterTotal: st.iters.length, iters: st.iters.map((n) => n.name),
198
- done: counts[si] || 0, active: cur !== null && cur.stepIdx === si,
274
+ const stages = STAGES.map((st, si) => ({
275
+ key: st.key, name: st.name, subTotal: st.subs.length,
276
+ subs: st.subs.map((n) => n.name),
277
+ done: (stageDone && stageDone[si]) || 0,
278
+ active: cur !== null && cur.stageIdx === si,
199
279
  }))
200
280
  return {
201
281
  status: state.status, runId: state.runId, rounds: state.rounds, round, done, total, pct,
202
- cur, steps, phase: state.phase, agentLabel: state.agentLabel,
282
+ stages, cur,
283
+ supervisor: supervisor ? { ...supervisor, issues: { ...supervisor.issues } } : null,
284
+ phase: state.phase, agentLabel: state.agentLabel,
203
285
  logs: state.logs.slice(-12), files: state.files.slice(0, 80),
204
286
  error: state.error, title: state.title, wsDir: state.wsDir, problemPath: state.problemPath,
205
287
  startedAt: state.startedAt, endedAt: state.endedAt,
@@ -230,86 +312,221 @@ export function createPipeline({ fs, subagents, agents }) {
230
312
  } catch (err) { /* 目录尚不存在时忽略 */ }
231
313
  }
232
314
 
233
- function buildPrompt(round, si, ii, s, it, args) {
315
+ // ---------- 提示词构造 ----------
316
+ function visionNote(args, sub) {
317
+ if (!sub.vision) return null
318
+ const line = sub.vision === 'must'
319
+ ? '本子阶段产出含图表,【必须】按3.3两阶段流程调用视觉模块审阅;视觉模块不可用时,在产出与 _report.yaml 中标记"待人工确认"。'
320
+ : '本子阶段若涉及图表,按3.3两阶段流程调用视觉模块审阅;视觉模块不可用时,在产出与 _report.yaml 中标记"待人工确认"。'
321
+ let host = ''
322
+ if (args.vision) {
323
+ host = '宿主侧探测到识图插件(' + (args.vision.source === 'service' ? '服务 ' + args.vision.key : '工具 ' + (args.vision.tools || []).join(', ')) + '),若你的工具列表中可用,请直接调用;'
324
+ } else {
325
+ host = '宿主侧未探测到独立识图插件;'
326
+ }
327
+ return '【视觉模块】' + host + '以你自己的工具列表为准。' + line
328
+ }
329
+
330
+ function problemBlock(args) {
234
331
  const p = []
235
- p.push('你正在参与「全国大学生数学建模竞赛论文」的多轮自动化撰写流水线,担任本轮「' + s.name + ' · ' + it.name + '」的执行专家。你的目标不是"写完",而是产出**可直接放进竞赛论文、经得起评审质疑**的内容。')
236
- p.push('')
237
- p.push('【流水线位置】第 ' + round + '/' + args.rounds + ' 轮 · 步骤 ' + (si + 1) + '/' + STEPS.length + ' ' + s.name + ' · 迭代 ' + (ii + 1) + '/' + s.iters.length + '「' + it.name + '」')
238
- p.push('【工作区(绝对路径)】' + args.wsDir + ' —— 所有读写都在该目录内,使用绝对路径或相对它的路径。')
239
332
  if (args.problem.kind === 'file') {
240
- p.push('【赛题原文文件】' + args.problem.path + ' —— 先用你的文件工具读取该文件全文,作为唯一赛题依据。')
333
+ p.push('赛题原文文件: ' + args.problem.path + ' —— 先用文件工具读取全文,作为唯一赛题依据。')
241
334
  } else {
242
- p.push('【赛题原文(已完整嵌入,请逐字精读)')
335
+ p.push('赛题原文(已完整嵌入,请逐字精读):')
243
336
  p.push(args.problem.text)
244
337
  }
338
+ return p
339
+ }
340
+
341
+ // 流水线执行Agent提示词(提示词.md 2.1 系统提示词 + 2.2 用户模板 + 三、各阶段配置)
342
+ function buildExecPrompt(args, stage, sub, si, ii, reportPath) {
343
+ const p = []
344
+ p.push('你正在参与「全国大学生数学建模竞赛论文」自动化撰写系统,担任本子阶段的执行Agent。')
245
345
  p.push('')
246
- p.push('【必读文件(先全部读完再动手,禁止跳过)】')
247
- p.push('- ' + LEDGER + ' (全流水线《缺陷与裁决台账》:最高权威。先检查是否有与本步骤相关的「待修复」条目:有则在本迭代内先修复(直接修改出错文件并更新台账状态),再完成本轮任务)')
248
- p.push('- ' + s.file + ' (本步骤历次迭代成果,本次在其基础上改进)')
249
- for (let q = 0; q < si; q++) p.push('- ' + STEPS[q].file + ' (前序步骤的权威成果;若其内容与台账最新裁决冲突,以台账为准,并在自己的产出中采用台账裁决后的版本)')
250
- if (round > 1) {
251
- p.push('上一轮(第 ' + (round - 1) + ')成果已保存在上述文件中(标题形如「## 第' + (round - 1) + '轮…」)。先阅读上一轮对应内容,本轮必须在其基础上优化,质量不得低于上一轮。')
252
- }
253
- if (ii === 0) {
254
- p.push('若本步骤文件尚不存在或为空,从零建立本步骤成果。')
255
- } else {
256
- p.push('若文件中缺失上一迭代(「第' + round + '轮·迭代' + ii + ' ' + s.iters[ii - 1].name + '」)成果,说明其失败:先补齐该迭代成果,再完成本次迭代。')
257
- }
258
- if (it.vision) {
259
- p.push('')
260
- p.push('【识图能力自检(强制,回复第一行)】检查你自己的工具列表:若含有 vision_describe / read_image / vision_ocr / vision_ground / vision_crop / vision_detect 等识图工具,回复第一行写「视觉能力:可用(工具:xxx)」并优先用识图工具检查图片;若没有任何识图工具,回复第一行写「视觉能力:不可用」,改走脚本分析路径。')
261
- if (args.vision) {
262
- p.push('【宿主侧探测(仅供参考)】系统检测到识图插件: ' + (args.vision.source === 'service' ? '服务 ' + args.vision.key : '工具 ' + (args.vision.tools || []).join(', ')) + '。若你的工具列表中确有其对应的调用方式,请直接调用它完成图片检查。')
263
- } else {
264
- p.push('【宿主侧探测(仅供参考)】未检测到独立识图插件;如果你装了识图插件但此处未检出,以你自己的工具列表为准,仍然按"视觉能力:可用"路径执行。')
265
- }
346
+ p.push('【系统提示词】你是数学建模竞赛论文自动化流水线的【子阶段执行Agent】。')
347
+ p.push('【职责】')
348
+ p.push('1. 完整读取教训文件(含ACTIVE和RESOLVED)')
349
+ p.push('2. 读取索引文件,定位上游产出物')
350
+ p.push('3. 读取上游内容,执行任务,生成产出物')
351
+ p.push('4. 按3.3标准对自身产出完整自检(六个维度:逻辑自洽性、模型合理性、数据敏感性、视觉合理性、规范符合性、教训对照)')
352
+ p.push('5. 若产出物含图表,按架构3.3两阶段流程执行视觉审阅:第一阶段:调用视觉模块获取详实描述;第二阶段:Agent自行对描述进行系统性质疑,发现问题按上报流程处理;视觉模块不可用时,标记"待人工确认"')
353
+ p.push('6. 完成自检后,在产出目录下写入 _report.yaml 文件上报结果')
354
+ p.push('')
355
+ p.push('【约束】')
356
+ p.push('1. 只读 ' + INDEX_FILE + ' ' + LESSONS_FILE + ',绝不自行修改')
357
+ p.push('2. 只引用 VALIDATED / FORCED_FINAL;同阶段内可读STAGING(跨阶段禁止);FORCED_FINAL:读取其标注的瑕疵,在产出物中说明"已了解该瑕疵被系统强制接受",不作为阻断条件')
358
+ p.push('3. 严禁自行评级或建议回滚')
359
+ p.push('4. 执行失败(非质量问题)按重试机制处理,不计入回滚计数器')
360
+ p.push('')
361
+ p.push('【当前子阶段】' + stage.key + ' · ' + sub.id + ' ' + sub.name)
362
+ p.push('【任务目标】' + sub.task)
363
+ p.push('【产出物要求】' + sub.outputs)
364
+ p.push('【重点提示维度】' + sub.focusDims + '(仅为该阶段重点,不豁免其他维度)')
365
+ p.push('【重点检查项】' + sub.focusItems)
366
+ p.push('【相关教训摘要】' + LESSONS_FILE + ' 全文(必须完整读取 ACTIVE+RESOLVED)')
367
+ if (sub.extra) p.push('【补充要求】' + sub.extra)
368
+ p.push('')
369
+ p.push('【执行步骤】')
370
+ p.push('1. 完整读取 ' + LESSONS_FILE + '(含ACTIVE和RESOLVED)')
371
+ p.push('2. 读取 ' + INDEX_FILE + ',查找上游文件路径')
372
+ p.push('3. 验证上游状态:VALIDATED / FORCED_FINAL 允许引用;同阶段STAGING允许;OBSOLETE禁止;FORCED_FINAL:标注"系统强制接受,不作为阻断条件"')
373
+ p.push('4. 执行任务,生成产出物;生成最终产出物前二次确认上游状态,若变更为OBSOLETE或FORCED_FINAL,立即停止并写入 _report.yaml 上报中控台')
374
+ p.push('5. 含图表时按3.3两阶段流程执行视觉审阅;不可用时标记"待人工确认"')
375
+ p.push('6. 按3.3完整自检(全部六个维度)')
376
+ p.push('7. 写入 ' + reportPath + ' 上报结果(仅事实,不评级)。无论是否有问题都必须写入')
377
+ p.push('')
378
+ p.push('【上报文件格式(写入 ' + reportPath + ')】')
379
+ p.push('agent_id: "' + sub.id + '"')
380
+ p.push('timestamp: "当前时间"')
381
+ p.push('status: "SUCCESS | HAS_ISSUES | NEEDS_REVIEW"')
382
+ p.push('issues:')
383
+ p.push(' - problem: "问题描述(仅事实)"')
384
+ p.push(' location: "发生位置"')
385
+ p.push(' involved_files: ["FILE_ID"]')
386
+ p.push(' impact: "影响范围"')
387
+ p.push('(无问题时 issues 为空数组)')
388
+ p.push('')
389
+ p.push('【执行失败处理(非质量问题)】重试上限3次(指数退避:5s/10s/20s),全失败后暂停流水线,人工介入,不计入回滚计数器。失败日志写入 /' + TX_LOG + '。')
390
+ p.push('')
391
+ p.push('【注意事项】不自行修改索引或教训文件;不自行决定回滚;{重点提示维度}仅为重点提示,不豁免其他维度。')
392
+ p.push('')
393
+ p.push(DIM_STANDARD)
394
+ p.push('')
395
+ p.push(VISION_FLOW)
396
+ const vn = visionNote(args, sub)
397
+ if (vn) { p.push(''); p.push(vn) }
398
+ p.push('')
399
+ p.push('【Harness 上下文】')
400
+ p.push('工作区(绝对路径): ' + args.wsDir + ' —— 所有读写都在该目录内,使用绝对路径或相对它的路径。')
401
+ problemBlock(args).forEach((l) => p.push(l))
402
+ p.push('索引文件: ' + args.wsDir + '/' + INDEX_FILE + '(只读);教训文件: ' + args.wsDir + '/' + LESSONS_FILE + '(只读,启动时必须完整读取)')
403
+ p.push('上报文件: ' + reportPath + '(必须写入,格式见上)')
404
+ if (si === 0 && ii === 0) {
405
+ p.push('本子阶段(0.1 创建目录)需创建标准化目录结构: config/、raw_data/、stage_00_outputs/、stage_01_outputs/、stage_02_outputs/、stage_03_outputs/、stage_04_outputs/、' + DEPRECATED_DIR + '、code/、figures/。')
266
406
  }
407
+ p.push('论文类文件(stage_03_outputs/ 各章节、' + FINAL_PAPER + ')不得出现流水线内部痕迹("FILE_INDEX""_report.yaml""中控台""子阶段"等),证据一律表述为"支撑材料/附录"。')
408
+ p.push('回复第一行必须写「上报状态: SUCCESS|HAS_ISSUES|NEEDS_REVIEW」,与 ' + reportPath + ' 保持一致;回复结尾附不超过 200 字总结(完成内容、产出文件清单含路径)。')
409
+ return p.join('\n')
410
+ }
411
+
412
+ // 中控台·扫描验证模块提示词(提示词.md 4.1)
413
+ function buildScanPrompt(args, stage, sub, reportPath, outputFiles) {
414
+ const p = []
415
+ p.push('你正在参与「全国大学生数学建模竞赛论文」自动化撰写系统,担任中控台的【扫描验证模块】。')
416
+ p.push('')
417
+ p.push('【系统提示词】你是中控台的【扫描验证模块】。')
418
+ p.push('【触发时机】流水线Agent提交 _report.yaml 且 status 为 "SUCCESS" 时,由中控台启动本模块。')
419
+ p.push('【职责】')
420
+ p.push('1. 读取Agent的产出物(从索引中获取文件路径)')
421
+ p.push('2. 按3.3标准对产出物执行完整扫描验证(六个维度:逻辑自洽性、模型合理性、数据敏感性、视觉合理性、规范符合性、教训对照)')
422
+ p.push('3. 若产出物含图表,按3.3两阶段流程调用视觉模块审阅')
423
+ p.push('4. 发现的问题按P0-P3标准评级')
424
+ p.push('5. 发现问题时,将问题及评级结果转交【评审决策模块】处理')
425
+ p.push('6. 未发现问题时,更新索引状态为 VALIDATED')
426
+ p.push('')
427
+ p.push('【约束】扫描验证模块只负责发现和评级问题,不执行修改或删除;发现问题后必须转交评审决策模块,不得自行处置;未发现问题时直接更新索引状态。')
267
428
  p.push('')
268
- p.push('【工作纪律】')
269
- p.push('1. 质疑先行:开工前对既有成果提出至少 3 条具体质疑(假设是否成立、是否偏离赛题、推导有无漏洞、数据与结论是否一致、格式是否规范),并逐条给出处理决定(采纳并改进 / 否决并说明理由)。质疑清单必须放在回复最前面。')
270
- p.push('2. 只改有据之处:经得起质疑的既有内容必须保留,不做无谓重写。')
271
- p.push('3. 禁止编造:所有数值与结论必须来自赛题、实际计算或前序成果;信息不足时明确写出你的假设并说明理由,不得假装已知。')
272
- p.push('4. 符号与术语一致:沿用前序步骤的符号体系,新增符号必须先定义。')
273
- p.push('5. 论文级表达:推导完整、步骤清晰、结论可复现,杜绝"显然""易得"式跳步。')
274
- p.push('6. 修复闭环:发现的每个问题要么在本迭代内真正修复并给出证据(修改后的文件/复检输出/断言结果),要么明确记录阻塞原因与后续建议;禁止"只把问题写进文档而不修复"。')
275
- p.push('7. 无流水线痕迹:论文类文件(06_论文.md、08_论文定稿.md、论文定稿.md)不得出现内部文档引用("01_赛题分析.md""02 迭代3""03 步骤""流水线"等),证据一律表述为"支撑材料/附录"。')
276
- p.push('8. 缺陷闭环(跨步骤):发现前序步骤(或本步骤既往迭代)的错误时,必须:①登记《缺陷与裁决台账》(编号 D#、证据、修复方案);②用编辑工具**直接修复出错文件**,在修改处标注「第R轮·Sx修正:D#编号」;③同步受影响的全部下游引用(论文/图表/代码/结果文件),做不到同步的追加台账条目。禁止只在自己文件里写"前序有误"后绕开。')
429
+ p.push('【待扫描的Agent产出】')
430
+ p.push('- Agent ID: ' + sub.id + ' ' + sub.name)
431
+ p.push('- 报告文件: ' + reportPath + '(先读取)')
432
+ p.push('- 当前阶段: ' + stage.key + ' ' + stage.name)
433
+ p.push('- 产出物: ' + (outputFiles.length > 0 ? outputFiles.join(', ') : '从索引文件定位 ' + stage.dir + '/ 下本次产出'))
277
434
  p.push('')
278
- p.push('【本轮任务】' + it.task)
435
+ p.push('【执行步骤】')
436
+ p.push('1. 从索引中读取产出物文件路径(若本次产出尚未登记,先按 STAGING 登记:FILE_ID、路径、摘要、DEPENDENCIES)')
437
+ p.push('2. 按3.3标准逐个扫描验证产出物(全部六个维度)')
438
+ p.push('3. 含图表时按3.3两阶段流程调用视觉模块')
439
+ p.push('4. 发现问题时按3.4标准评级')
440
+ p.push('5. 输出扫描结果')
279
441
  p.push('')
280
- p.push('【交付要求(强制)】')
281
- p.push('1. 完整成果追加写入 ' + s.file + ',标题「## 第' + round + '轮·迭代' + (ii + 1) + ' ' + it.name + '」;内容完整可复现,不得缩写,严禁删除或覆盖已有内容。')
282
- p.push('2. 代码保存到 ' + args.wsDir + '/代码/,新版本用 _vN 后缀,旧版本一律保留;图片保存到 ' + args.wsDir + '/图表/,旧图保留。')
442
+ p.push(DIM_STANDARD)
283
443
  p.push('')
284
- p.push('【回复结构】开头为「质疑清单:」;随后是本次工作与成果;结尾附不超过 200 字总结:完成内容、关键改进、产出文件清单(含路径)。')
444
+ p.push(VISION_FLOW)
445
+ p.push('')
446
+ p.push('【Harness 上下文】')
447
+ p.push('工作区(绝对路径): ' + args.wsDir)
448
+ p.push('索引文件: ' + args.wsDir + '/' + INDEX_FILE + '(扫描验证模块负责维护其状态)')
449
+ p.push('教训文件: ' + args.wsDir + '/' + LESSONS_FILE + '(只读,先完整读取)')
450
+ p.push('索引更新前必须执行DAG环检测(架构 R-016):发现跨阶段循环依赖时,将警告写入 ' + args.wsDir + '/' + TX_LOG + ' 并阻断本次验证,提示人工介入解除循环依赖。')
451
+ p.push('更新索引时记录时间与当前阶段(架构 R-015);未发现问题时更新索引状态为 VALIDATED;发现问题必须转交评审决策模块,不自行处置。')
452
+ p.push('处理完成后,将本次 ' + reportPath + ' 移入 ' + args.wsDir + '/' + DEPRECATED_DIR + '(重命名加时间戳保留),避免下一子阶段误读旧报告(架构 3.10.2-4)。')
453
+ p.push('回复第一行必须写「扫描结论: PASS|HAS_ISSUES」;随后输出 JSON:')
454
+ p.push('{"module": "扫描验证模块", "scan_result": "PASS|HAS_ISSUES", "status_update": {"files_affected": ["FILE_ID"], "new_status": "VALIDATED"}, "issues_found": [{"file_id": "FILE_ID", "problem": "问题描述", "rating": "P0|P1|P2|P3", "location": "具体位置"}]}')
455
+ p.push('回复结尾附不超过 200 字总结。')
285
456
  return p.join('\n')
286
457
  }
287
458
 
459
+ // 中控台·评审决策模块提示词(提示词.md 5.1)
460
+ function buildReviewPrompt(args, stage, source, issueText, countsText, limit) {
461
+ const p = []
462
+ p.push('你正在参与「全国大学生数学建模竞赛论文」自动化撰写系统,担任中控台的【评审决策模块】。')
463
+ p.push('')
464
+ p.push('【系统提示词】你是中控台的【评审决策模块】。')
465
+ p.push('【触发时机】1. 流水线Agent提交 _report.yaml 且 status 为 "HAS_ISSUES" 或 "NEEDS_REVIEW" 时;2. 扫描验证模块发现问题并转交时。')
466
+ p.push('【职责】')
467
+ p.push('1. 阅读问题报告,按P0-P3标准评级(唯一评级机构)')
468
+ p.push('2. 通过DEPENDENCIES链追溯根源阶段')
469
+ p.push('3. 做出处置决策:P0/P1/P2:执行回滚;P3:执行精确定点修改,不触发回滚;索引按规则更新状态')
470
+ p.push('4. 回滚时:定位根源→物理删除(删除范围 = 根源阶段全部产出 ∪ 其之后所有阶段全部产出,架构 R-002)→索引OBSOLETE→计数器+1→重跑前写入教训→重跑')
471
+ p.push('5. 回滚后递归检查未被删除文件的DEPENDENCIES,将指向OBSOLETE的依赖标记为OBSOLETE_DEPENDENCY')
472
+ p.push('6. P3修改时:精确定点修改(≤3行代码/≤1个句子/≤3个参数),索引按规则更新状态')
473
+ p.push('7. 维护阶段级独立回滚计数器(递增被回滚到的阶段);回滚后验证:重扫描被回滚阶段产出物,对照原问题检查')
474
+ p.push('')
475
+ p.push('【评级标准】P0 阻断级/结论完全错误→回滚,计数器+1;P1 严重影响质量→回滚,计数器+1;P2 局部问题,不伤核心→回滚,计数器+1;P3 润色层面→精确定点修改,继续推进。')
476
+ p.push('')
477
+ p.push('【P3精确定点修改】')
478
+ p.push('1. 解析问题涉及的文件ID和具体位置')
479
+ p.push('2. 定位到问题所在行或段落,明确修改边界')
480
+ p.push('3. 执行修改:只改问题直接涉及的内容,不重写整段、不重构结构、不扩展范围')
481
+ p.push('4. 修改完成后流水线继续。索引状态:修改前为 STAGING → 直接升级为 VALIDATED(无需重新扫描);修改前为 VALIDATED → 状态保持不变')
482
+ p.push('5. 若判定无法精确定点修改(模糊/无法定位/可能引入新问题),跳过修改,标记"待人工P3修改",继续推进')
483
+ p.push('【精确定点修改边界判定】精确定点修改:≤3行代码/≤1个句子/≤3个参数;超出边界(需重构整个段落/重新设计图表结构/修改涉及多个不连续位置)默认升级为P2(保守策略)。')
484
+ p.push('')
485
+ p.push('【待决策问题】')
486
+ p.push('来源: ' + source)
487
+ p.push(issueText || '(问题报告缺失,请读取索引与教训文件自行核对当前阶段产出)')
488
+ p.push('')
489
+ p.push('【当前各阶段回滚计数器】' + countsText)
490
+ p.push('【当前阶段】' + stage.key + ' ' + stage.name)
491
+ p.push('')
492
+ p.push('【执行】')
493
+ p.push('1. 按P0-P3评级')
494
+ p.push('2. P3:执行精确定点修改(边界:≤3行代码/≤1个句子/≤3个参数);修改前为STAGING则直接升级为VALIDATED,修改前为VALIDATED则状态不变')
495
+ p.push('3. P0/P1/P2:通过DEPENDENCIES链追溯根源阶段,裁定回滚目标')
496
+ p.push('4. 回滚时检查目标阶段计数器:<' + limit + '次:物理删除根源阶段及之后所有阶段的全部产出→索引OBSOLETE→计数+1→写入教训(重跑前,先查重去重)→重跑;≥' + limit + '次:FORCED_FINAL')
497
+ p.push('5. 若最终判定无需处置(NO_ACTION),同时将本次产出在索引中的状态更新为 VALIDATED')
498
+ p.push('6. 处理完成后,将本次 _report.yaml 移入 ' + args.wsDir + '/' + DEPRECATED_DIR + '(重命名加时间戳保留),避免下一子阶段误读旧报告(架构 3.10.2-4)')
499
+ p.push('')
500
+ p.push('【Harness 上下文】')
501
+ p.push('工作区(绝对路径): ' + args.wsDir)
502
+ p.push('索引文件: ' + args.wsDir + '/' + INDEX_FILE + '(读取全文,含DEPENDENCIES链;维护 OBSOLETE/OBSOLETE_DEPENDENCY/VALIDATED 等状态)')
503
+ p.push('教训文件: ' + args.wsDir + '/' + LESSONS_FILE + '(回滚<10次时,在重跑前写入教训;写入前检查是否已有相同根因的ACTIVE教训,若有则更新TIMESTAMP和AFFECTED_STAGES)')
504
+ p.push('物理删除(将根源阶段及之后所有阶段的全部产出物移入 ' + args.wsDir + '/' + DEPRECATED_DIR + ',架构 R-002/R-003)由你使用文件工具执行;回滚计数器维护与重跑调度由系统(代码逻辑)执行,你只需给出评级与决策。')
505
+ p.push('回复第一行必须写「决策: ROLLBACK|P3_FIX|NO_ACTION」;随后输出 JSON:')
506
+ p.push('{"module": "评审决策模块", "rating": "P0|P1|P2|P3", "decision": "ROLLBACK|P3_FIX|NO_ACTION", "rollback_target": "阶段X|null", "reasoning": "...", "root_cause_stage": "阶段X|null", "affected_files": ["FILE_ID"], "p3_fix": {"fixed": true|false, "files_modified": ["FILE_ID"], "status_change": "STAGING→VALIDATED|保持不变|null", "fix_summary": "...", "deferred_to_human": false}, "lesson_written": true|false, "lesson_id": "LSN-XXX|null", "lesson_deduplicated": true|false, "stage_rollback_counts_after": {"阶段X": N}|null}')
507
+ p.push('回复结尾附不超过 200 字总结。')
508
+ return p.join('\n')
509
+ }
510
+
511
+ // 兜底定稿(防崩溃优先:不论成败都产出一版 Final_Paper.md)
288
512
  function buildFinalizePrompt(args, reason) {
289
513
  const p = []
290
- p.push('你正在参与「全国大学生数学建模竞赛论文」自动化流水线的**兜底定稿**环节。此前流水线提前结束(原因:' + reason + '),但交付要求是"不论最终结果如何,都必须产出一版完整的论文"。现在由你仅凭现有文件完成这个承诺。')
514
+ p.push('你正在参与「全国大学生数学建模竞赛论文」自动化撰写系统的**兜底定稿**环节。此前流水线提前结束(原因:' + reason + '),但交付要求是"不论最终结果如何,都必须产出一版完整的论文"。现在由你仅凭现有文件完成这个承诺。')
291
515
  p.push('')
292
516
  p.push('【工作区(绝对路径)】' + args.wsDir + ' —— 所有读写都在该目录内。')
293
- if (args.problem.kind === 'file') {
294
- p.push('【赛题原文文件】' + args.problem.path + ' —— 先用文件工具读取全文。')
295
- } else {
296
- p.push('【赛题原文(已完整嵌入)】')
297
- p.push(args.problem.text)
298
- }
517
+ problemBlock(args).forEach((l) => p.push(l))
299
518
  p.push('')
300
519
  p.push('【现有材料(全部先读,能用的必须用)】')
301
- p.push('- 00_赛题原文.md、00_缺陷与裁决台账.md(全流水线裁决,最高权威)、01_赛题分析.md、02_模型建立与求解.md、03_编程实现.md、04_图表生成.md、05_图表自检.md、06_论文.md、07_评审修复.md、08_论文定稿.md(存在即读)')
302
- p.push('- 代码/ 代码/results/ 下的全部代码与结构化结果(权威数值只从这里取)')
303
- p.push('- 图表/定稿/ 下的全部图片(论文插图用相对路径引用)')
520
+ p.push('- 索引 ' + INDEX_FILE + ' 与教训文件 ' + LESSONS_FILE + '(只读,定位各阶段产出物与已知问题;以索引最新状态为准)')
521
+ p.push('- stage_00_outputs/ ~ stage_04_outputs/ 下各子阶段产出(章节、报告、图表、清洗/变换数据)')
522
+ p.push('- code/ 下求解代码、figures/ 与各阶段 figures 下全部图表(论文插图用相对路径引用)')
304
523
  p.push('')
305
524
  p.push('【任务(必须完成)】')
306
- p.push('1. 判断 06_论文.md 中是否已有干净完整的终稿:有 以其为底稿做最终校对后写入 论文定稿.md;缺失/混乱/不完整 依据上述全部材料从头撰写完整论文,写入 论文定稿.md。')
307
- p.push('2. 论文定稿.md 必须是可直接提交的完整 Markdown:标题、摘要(单独成页、含关键词)、问题重述与分析、模型假设与符号说明、模型建立与求解(公式编号)、模型验证与灵敏度分析、模型评价与推广、参考文献、附录(程序清单);图表用相对路径引用。')
308
- p.push('3. 全文不得出现流水线内部痕迹与参赛者信息;数值只采用 results/ 权威数据或前序已验证结论,材料不足处明确标注"待补充"并给出推导,严禁编造。若台账中存在对前序结论的修正裁决,以台账为准。')
309
- p.push('4. 08_论文定稿.md 追加一节「## 兜底定稿」,记录:定稿来源(06 终稿 / 重写)、完整度自评、遗留问题清单(把《缺陷与裁决台账》中仍未闭环的条目列入)。')
310
- p.push('5. 输出《提交材料清单》与《最后校对报告》(写入 08_论文定稿.md 该节)。')
311
- p.push('')
312
- p.push('【工作纪律】质疑先行、禁止编造、无流水线痕迹、论文级表达。回复结尾附不超过 200 字总结。')
525
+ p.push('1. stage_04_outputs/ 已有完整 merged_with_figures.md ' + FINAL_PAPER + ':以其为底稿做最终校对后写入 ' + FINAL_PAPER + ';否则依据上述全部材料从头撰写完整论文,写入 ' + FINAL_PAPER + '。')
526
+ p.push('2. ' + FINAL_PAPER + ' 必须是可直接提交的完整 Markdown:标题、摘要(含关键词)、问题重述与背景、假设与符号说明、建模与求解(公式编号、图表嵌入)、结果分析与模型评价、参考文献、附录(程序清单);公式行间 $$...$$、行内 $...$,图表 ![描述](./path.png),表格 GFM,参考文献 [1](架构 6.1)。')
527
+ p.push('3. 全文不得出现流水线内部痕迹(索引/教训/_report.yaml/中控台/子阶段等)与参赛者信息;数值只采用各阶段已验证结论,材料不足处明确标注"待补充"并给出推导,严禁编造。')
528
+ p.push('4. 将《提交材料清单》与《遗留问题清单》(教训文件中 ACTIVE 教训、待人工P3修改、未闭环问题)写入 stage_04_outputs/ 的一节或单独文件,供使用者参考,不得进入论文正文。')
529
+ p.push('5. 回复结尾附不超过 200 字总结。')
313
530
  return p.join('\n')
314
531
  }
315
532
 
@@ -339,6 +556,7 @@ export function createPipeline({ fs, subagents, agents }) {
339
556
  if (stopReason === 'error') state.error = errorMsg || '流水线执行出错'
340
557
  if (stopReason === 'cancelled') state.error = '已被用户中止(兜底定稿已尝试生成)'
341
558
  state.agentLabel = ''
559
+ if (supervisor) supervisor.module = null
342
560
  }
343
561
  refreshFiles()
344
562
  }
@@ -350,7 +568,7 @@ export function createPipeline({ fs, subagents, agents }) {
350
568
  }
351
569
 
352
570
  // 清空状态 + 重置断点续跑记录:向最近一次运行所属会话追加一条重置标记,
353
- // 续跑扫描遇到该标记会把之前的运行记录全部作废,下次 /loopbegin 从第 1 个迭代全新开始。
571
+ // 续跑扫描遇到该标记会把之前的运行记录全部作废,下次 /loopbegin 从第 1 个子阶段全新开始。
354
572
  function resetRecords() {
355
573
  if (state.status === 'running') return { ok: false, reason: '流水线运行中,不能重置' }
356
574
  if (lastSession) {
@@ -359,6 +577,8 @@ export function createPipeline({ fs, subagents, agents }) {
359
577
  }
360
578
  state = freshState()
361
579
  active = null
580
+ stageDone = new Array(STAGES.length).fill(0)
581
+ supervisor = null
362
582
  return { ok: true }
363
583
  }
364
584
 
@@ -391,11 +611,10 @@ export function createPipeline({ fs, subagents, agents }) {
391
611
  }
392
612
  }
393
613
 
394
- // ---------- Host 侧编排器:从断点继续(不依赖聊天 Agent 存活) ----------
614
+ // ---------- Host 侧编排器:五阶段 × 中控台双模块,含回滚/锁定/重试/兜底 ----------
395
615
  async function runPipeline(runId, session, agent, args, controller) {
396
- let failStreak = 0
397
616
  let cancelled = false
398
- let finalDone = false // S8(定稿)是否已成功完成
617
+ let finalDone = false // 4.3 最终产出是否已成功完成(Final_Paper.md)
399
618
  let stopReason = 'completed'
400
619
  let errorMsg = undefined
401
620
  const provider = pickProvider()
@@ -404,70 +623,181 @@ export function createPipeline({ fs, subagents, agents }) {
404
623
  if (active && active.runId === runId) active = null
405
624
  return
406
625
  }
407
- const total = args.rounds * PER_ROUND
408
- for (let g = args.startIndex; g < total; g++) {
409
- if (controller.signal.aborted) { cancelled = true; break }
410
- const r = Math.floor(g / PER_ROUND) + 1
411
- const f = FLAT[g % PER_ROUND]
412
- const s = STEPS[f.s]
413
- const it = s.iters[f.i]
414
- if (g % PER_ROUND === 0) pushLog('【第 ' + r + '/' + args.rounds + ' 轮开始】')
415
- const label = 'R' + r + '·S' + (f.s + 1) + ' ' + s.name + ' ' + (f.i + 1) + '/' + s.iters.length + ' ' + it.name
416
- state.phase = s.phase
417
- state.agentLabel = label
418
- pushLog('开始: ' + label)
626
+ const total = PER_ROUND // 单遍五阶段 22 个子阶段(线性推进 + 阶段级回滚,无外循环轮次)
627
+ const limit = Number.isSafeInteger(args.rollbackLimit) && args.rollbackLimit >= 1 ? args.rollbackLimit : DEFAULT_ROLLBACK_LIMIT
628
+ const stageCounts = {} // 阶段级独立回滚计数器(架构 3.7:递增被回滚到的阶段)
629
+ const issues = { P0: 0, P1: 0, P2: 0, P3: 0 } // 累计评级(仅展示)
630
+ supervisor = { module: null, rollbackCount: 0, rollbackLimit: limit, forcedFinal: false, issues }
631
+ let totalRollbacks = 0
632
+ let attemptSeq = 0
633
+
634
+ // 启动一个子智能体并写工作流卡片;返回 { outcome, text }
635
+ async function spawn(label, phase, promptText) {
636
+ attemptSeq += 1
637
+ const seq = attemptSeq
419
638
  let started = false
420
- let outcome = 'failed'
421
- let note = ''
422
- const res = await runChild(provider, session, agent, controller, label, buildPrompt(r, f.s, f.i, s, it, args), (child) => {
639
+ const res = await runChild(provider, session, agent, controller, label, promptText, (child) => {
423
640
  started = true
424
- appendRec(runId, session, 'tool-workflow/agent-start', { runId, seq: g + 1, label, phase: s.phase, childId: String(child.id) })
641
+ appendRec(runId, session, 'tool-workflow/agent-start', { runId, seq, label, phase, childId: String(child.id) })
425
642
  }, args.agentOptions)
426
- outcome = res.outcome
427
- note = (res.text || '').trim().slice(0, 300)
428
- if (res.err) note = '执行异常: ' + String((res.err && res.err.message) || res.err).slice(0, 200)
429
- state.done = Math.min(g + 1, state.total)
430
- refreshFiles()
431
- if (outcome === 'completed') {
432
- failStreak = 0
433
- pushLog('完成: ' + label)
434
- if (s.key === 'S8' && (g + 1) === total) finalDone = true
435
- // 解析 S5 迭代1 的识图能力自报
436
- if (s.key === 'S5' && f.i === 0) {
437
- const m = /视觉能力[::]\s*(可用|不可用)/.exec(res.text || '')
438
- if (m) {
439
- state.visionLive = m[1] === '可用' ? 'vision' : 'script'
440
- pushLog('图表自检路径: ' + (m[1] === '可用' ? '识图工具(视觉自检)' : '脚本分析(无识图工具)'))
441
- }
643
+ if (started) appendRec(runId, session, 'tool-workflow/agent-end', { runId, seq, outcome: res.outcome })
644
+ return res
645
+ }
646
+
647
+ // 执行失败处理(架构 3.8):重试 3 次指数退避,全失败暂停人工介入,不计入回滚计数器
648
+ async function runAgent(kindName, label, phase, promptText) {
649
+ state.phase = phase
650
+ state.agentLabel = label
651
+ let last = null
652
+ for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
653
+ if (controller.signal.aborted) return { outcome: 'cancelled', text: '' }
654
+ const res = await spawn(label, phase, promptText)
655
+ if (res.outcome === 'completed') return res
656
+ last = res
657
+ if (controller.signal.aborted) return { outcome: 'cancelled', text: '' }
658
+ if (attempt < RETRY_DELAYS.length) {
659
+ const ms = RETRY_DELAYS[attempt]
660
+ pushLog(kindName + '执行失败,' + Math.round(ms / 1000) + 's 后重试(' + (attempt + 1) + '/' + RETRY_DELAYS.length + ')')
661
+ await sleep(ms, controller.signal)
442
662
  }
443
- } else {
444
- failStreak += 1
445
- pushLog(outcome === 'cancelled' ? '中止: ' : '失败: ' + label + (note ? '(' + note.slice(0, 80) + ')' : ''))
446
663
  }
447
- if (started) appendRec(runId, session, 'tool-workflow/agent-end', { runId, seq: g + 1, outcome })
448
- if ((g + 1) % PER_ROUND === 0 && !controller.signal.aborted) pushLog('【第 ' + r + '/' + args.rounds + ' 轮完成】')
664
+ pushLog(kindName + '重试 ' + RETRY_DELAYS.length + ' 次仍失败,流水线暂停,请人工介入(不计入回滚计数器)')
665
+ await txLog(args, fmtTime(Date.now()) + ' [执行失败] ' + kindName + '(' + label + ') 重试' + RETRY_DELAYS.length + '次仍失败,流水线暂停,人工介入')
666
+ return { outcome: 'failed', text: (last && last.text) || '' }
667
+ }
668
+
669
+ let g = args.startIndex || 0 // 全局平铺游标(已完成的子阶段数)
670
+ while (g < total) {
449
671
  if (controller.signal.aborted) { cancelled = true; break }
450
- if (failStreak >= 3) {
672
+ const f = FLAT[g % PER_ROUND]
673
+ const si = f.s
674
+ const ii = f.i
675
+ const stage = STAGES[si]
676
+ const sub = stage.subs[ii]
677
+ const execLabel = stage.key + '·' + sub.id + ' ' + sub.name
678
+ const reportPath = args.wsDir + '/' + stage.dir + '/_report.yaml'
679
+ pushLog('开始: ' + execLabel)
680
+
681
+ // ---- ① 流水线执行Agent ----
682
+ const exec = await runAgent('执行Agent', execLabel, stage.key + ' ' + stage.name, buildExecPrompt(args, stage, sub, si, ii, reportPath))
683
+ if (exec.outcome === 'cancelled') { cancelled = true; break }
684
+ if (exec.outcome !== 'completed') {
451
685
  stopReason = 'error'
452
- errorMsg = '连续 3 个子任务失败,流水线提前终止。常见原因:模型服务限流(429)/账户余额不足(402)/上下文过长/工作区文件问题;可尝试 /loopbegin --model 提供商/模型 换用其他模型,或 /loopreset 后重来。已有成果全部保留。'
686
+ errorMsg = '执行Agent连续失败(含重试),流水线暂停,请人工介入(架构 3.8,不计入回滚计数器)'
453
687
  break
454
688
  }
689
+
690
+ // ---- 读取 _report.yaml 上报状态(文件优先,回复第一行兜底) ----
691
+ let reportStatus = await readReportStatus(args, stage, sub.id)
692
+ if (!reportStatus) {
693
+ const m = /上报状态[::]\s*(SUCCESS|HAS_ISSUES|NEEDS_REVIEW)/.exec(exec.text || '')
694
+ reportStatus = m ? m[1] : null
695
+ }
696
+ if (reportStatus === null) pushLog('未读取到 _report.yaml 上报,按 SUCCESS 处理并交扫描验证模块复核')
697
+
698
+ // ---- ② 中控台:SUCCESS → 扫描验证模块;HAS_ISSUES/NEEDS_REVIEW → 评审决策模块 ----
699
+ // 上报缺失(null)按 SUCCESS 处理(扫描验证模块复核兜底),与上面的日志口径一致
700
+ let needReview = reportStatus !== null && reportStatus !== 'SUCCESS'
701
+ let issueText = ''
702
+ if (needReview) {
703
+ const repTxt = await readReportText(args, stage, sub.id)
704
+ issueText = (repTxt || exec.text || '').slice(0, 2000)
705
+ }
706
+ if (!needReview) {
707
+ supervisor.module = '扫描验证模块'
708
+ const outputs = await listOutputs(args, stage)
709
+ const scan = await runAgent('扫描验证模块', '中控台·扫描验证·' + sub.id, '中控台·扫描验证', buildScanPrompt(args, stage, sub, reportPath, outputs))
710
+ if (scan.outcome === 'cancelled') { cancelled = true; break }
711
+ if (scan.outcome !== 'completed') {
712
+ stopReason = 'error'
713
+ errorMsg = '扫描验证模块连续失败(含重试),流水线暂停,请人工介入(架构 3.8,不计入回滚计数器)'
714
+ break
715
+ }
716
+ const mScan = /扫描结论[::]\s*(PASS|HAS_ISSUES)/.exec(scan.text || '')
717
+ const verdict = mScan ? mScan[1] : 'HAS_ISSUES'
718
+ pushLog('扫描验证: ' + (verdict === 'PASS' ? '通过,索引已更新为 VALIDATED' : '发现问题,转交评审决策模块'))
719
+ if (verdict !== 'PASS') {
720
+ needReview = true
721
+ issueText = '扫描验证模块发现问题(转交评审决策模块):\n' + (scan.text || '').slice(0, 2000)
722
+ }
723
+ }
724
+
725
+ // ---- ③ 中控台:评审决策模块(唯一评级机构) ----
726
+ if (needReview) {
727
+ supervisor.module = '评审决策模块'
728
+ const countsText = STAGES.map((st, idx) => st.key + ': ' + (stageCounts[idx] || 0) + ' 次').join(', ')
729
+ const source = issueText.indexOf('扫描验证模块发现问题') === 0 ? '扫描验证模块转交' : 'Agent上报'
730
+ const review = await runAgent('评审决策模块', '中控台·评审决策·' + sub.id, '中控台·评审决策', buildReviewPrompt(args, stage, source, issueText, countsText, limit))
731
+ if (review.outcome === 'cancelled') { cancelled = true; break }
732
+ if (review.outcome !== 'completed') {
733
+ stopReason = 'error'
734
+ errorMsg = '评审决策模块连续失败(含重试),流水线暂停,请人工介入(架构 3.8,不计入回滚计数器)'
735
+ break
736
+ }
737
+ const txt = review.text || ''
738
+ const rating = (/评级[::]\s*(P[0-3])/.exec(txt) || /"rating"\s*:\s*"(P[0-3])"/.exec(txt) || [])[1]
739
+ if (rating) issues[rating] = (issues[rating] || 0) + 1
740
+ let decision = (/决策[::]\s*(ROLLBACK|P3_FIX|NO_ACTION)/.exec(txt) || /"decision"\s*:\s*"(ROLLBACK|P3_FIX|NO_ACTION)"/.exec(txt) || [])[1]
741
+ if (!decision) {
742
+ decision = 'NO_ACTION'
743
+ pushLog('评审决策输出未识别,按 NO_ACTION 处理(请人工复核)')
744
+ }
745
+ pushLog('评审决策: ' + (rating || '未评级') + ' · ' + decision + (decision === 'P3_FIX' && /"deferred_to_human"\s*:\s*true/.test(txt) ? ' · 已标记待人工P3修改' : ''))
746
+ if (decision === 'ROLLBACK') {
747
+ const targetKey = (/回滚目标[::]\s*(阶段[零一二三四])/.exec(txt) || /"rollback_target"\s*:\s*"(阶段[零一二三四])"/.exec(txt) || [])[1]
748
+ const idx = targetKey ? STAGES.findIndex((st) => st.key === targetKey) : -1
749
+ const targetIdx = idx >= 0 ? idx : si // 无法解析时保守回滚到当前阶段
750
+ if ((stageCounts[targetIdx] || 0) >= limit) {
751
+ // 已 FORCED_FINAL 锁定:回滚请求降级为 P3 处理(架构 3.7),不再计数/不再回滚,继续推进
752
+ pushLog('评审决策: 回滚目标 ' + STAGES[targetIdx].key + ' 已 FORCED_FINAL 锁定,本次降级为 P3 处理,继续推进')
753
+ } else {
754
+ stageCounts[targetIdx] = (stageCounts[targetIdx] || 0) + 1
755
+ totalRollbacks += 1
756
+ supervisor.rollbackCount = totalRollbacks
757
+ appendRec(runId, session, 'tool-workflow/rollback', { runId, targetIdx, count: stageCounts[targetIdx], rating: rating || '', subKey: sub.id })
758
+ if (stageCounts[targetIdx] >= limit) {
759
+ // 防崩溃(架构 3.7):同一阶段回滚≥上限 → FORCED_FINAL 强制锁定,通知人工,问题降级为 P3 处理,继续推进
760
+ supervisor.forcedFinal = true
761
+ pushLog('强制锁定: ' + STAGES[targetIdx].key + ' 回滚已达 ' + limit + ' 次,状态 FORCED_FINAL,请人工介入(问题降级为 P3 处理,流水线继续推进)')
762
+ await txLog(args, fmtTime(Date.now()) + ' [强制锁定] ' + STAGES[targetIdx].key + ' 回滚≥' + limit + ' 次,FORCED_FINAL,通知人工')
763
+ } else {
764
+ const rollbackTo = STAGE_OFFSETS[targetIdx]
765
+ pushLog('阶段级回滚: 根源定位 ' + STAGES[targetIdx].key + ',删除该阶段及之后全部产出(评审决策模块已执行物理删除),从该阶段重跑(第 ' + stageCounts[targetIdx] + ' 次回滚)')
766
+ g = rollbackTo
767
+ // 游标回退后同步进度:已完成的子阶段 = 根源阶段之前的全部子阶段(架构 R-002 只删根源及之后)
768
+ state.done = g
769
+ for (let k = targetIdx; k < stageDone.length; k++) stageDone[k] = 0
770
+ supervisor.module = null
771
+ continue // 回滚重跑,不记子阶段完成
772
+ }
773
+ }
774
+ }
775
+ }
776
+ supervisor.module = null
777
+ // ---- 子阶段通过:登记完成、推进游标 ----
778
+ appendRec(runId, session, 'tool-workflow/substage-done', { runId, subKey: sub.id, pass: 1 })
779
+ stageDone[si] = (stageDone[si] || 0) + 1
780
+ if (sub.id === '4.3') finalDone = true
781
+ g += 1
782
+ state.done = g
783
+ refreshFiles()
784
+ pushLog('完成: ' + execLabel)
455
785
  }
786
+
456
787
  if (cancelled) { stopReason = 'cancelled'; errorMsg = '已被用户中止' }
457
- // ---------- 兜底定稿:不论成败,只要 S8 未完成,就补一次论文定稿 ----------
788
+ // ---------- 兜底定稿:不论成败,只要 4.3 未完成,就补一次论文定稿(防崩溃优先) ----------
458
789
  if (!finalDone) {
459
- pushLog('正在执行兜底定稿:生成 论文定稿.md(不论前序结果如何)')
460
- state.phase = 'S8 兜底·论文定稿'
461
- state.agentLabel = 'S8 兜底·论文定稿'
790
+ pushLog('正在执行兜底定稿:生成 ' + FINAL_PAPER + '(不论前序结果如何)')
791
+ state.phase = '兜底·论文定稿'
792
+ state.agentLabel = '兜底·论文定稿'
462
793
  const fresh = makeController()
463
- // 关键:把兜底定稿的控制器登记为当前活跃控制器,
464
- // 否则面板「中止」与 /loopabort 只会 abort 已失效的主控制器,兜底定稿无法被中止
794
+ // 关键:把兜底定稿的控制器登记为当前活跃控制器,否则面板「中止」与 /loopabort 无法中止兜底定稿
465
795
  if (active && active.runId === runId) active.controller = fresh
466
- const reason = stopReason === 'cancelled' ? '用户中止' : stopReason === 'error' ? errorMsg : '正常流程已结束但定稿步骤未完成'
796
+ const reason = stopReason === 'cancelled' ? '用户中止' : stopReason === 'error' ? errorMsg : '正常流程已结束但最终产出未完成'
467
797
  const fres = await runChild(provider, session, agent, fresh, '兜底·论文定稿(不论成败)', buildFinalizePrompt(args, reason), undefined, args.agentOptions)
468
798
  if (fres.outcome === 'completed') {
469
799
  finalDone = true
470
- pushLog('兜底定稿完成: 论文定稿.md 已生成')
800
+ pushLog('兜底定稿完成: ' + FINAL_PAPER + ' 已生成')
471
801
  } else {
472
802
  pushLog('兜底定稿未能完成: ' + (fres.text || '').trim().slice(0, 80) + ' —— 请查看工作区已有成果')
473
803
  if (fres.outcome === 'cancelled') {
@@ -475,7 +805,7 @@ export function createPipeline({ fs, subagents, agents }) {
475
805
  errorMsg = '已被用户中止(兜底定稿未完成)'
476
806
  } else if (stopReason === 'completed') {
477
807
  stopReason = 'error'
478
- errorMsg = '流水线迭代已完成,但兜底定稿生成失败'
808
+ errorMsg = '流水线子阶段已完成,但兜底定稿生成失败'
479
809
  }
480
810
  }
481
811
  }
@@ -485,7 +815,7 @@ export function createPipeline({ fs, subagents, agents }) {
485
815
 
486
816
  // 启动一次运行:创建 run 记录与工作流卡片、初始化运行状态、后台启动主循环(立即返回)
487
817
  function start({ session, agent, args, resumeCount, explicitModel }) {
488
- // 断点续跑:主循环从该迭代开始(入口模块解析出的续跑计数)
818
+ // 断点续跑:主循环从该子阶段开始(入口模块解析出的续跑计数)
489
819
  args.startIndex = resumeCount
490
820
  const runId = mintRunId()
491
821
  const rec = { session, ok: true }
@@ -498,20 +828,23 @@ export function createPipeline({ fs, subagents, agents }) {
498
828
  state = freshState()
499
829
  state.status = 'running'
500
830
  state.runId = runId
501
- state.rounds = args.rounds
502
- state.total = args.rounds * PER_ROUND
831
+ state.rounds = 1
832
+ state.total = PER_ROUND
503
833
  state.done = resumeCount
504
- state.round = Math.min(Math.floor(resumeCount / PER_ROUND) + 1, args.rounds)
834
+ state.round = 1
505
835
  state.startedAt = Date.now()
506
836
  state.wsDir = args.wsDir
507
837
  state.problemPath = args.problemPath
508
838
  state.title = args.title
509
839
  state.vision = args.vision
840
+ stageDone = new Array(STAGES.length).fill(0)
841
+ const limit = Number.isSafeInteger(args.rollbackLimit) && args.rollbackLimit >= 1 ? args.rollbackLimit : DEFAULT_ROLLBACK_LIMIT
842
+ supervisor = { module: null, rollbackCount: 0, rollbackLimit: limit, forcedFinal: false, issues: { P0: 0, P1: 0, P2: 0, P3: 0 } }
510
843
  const controller = makeController()
511
844
  active = { runId, session, controller }
512
- pushLog('流水线已启动: ' + args.rounds + ' × 19 次迭代,输出目录 ' + args.wsDir + (resumeCount > 0 ? '(断点续跑:跳过已完成的 ' + resumeCount + ' 次迭代)' : '') + '(关闭本聊天窗口不影响后台执行)')
513
- if (args.vision) pushLog('识图能力探测: ' + (args.vision.source === 'service' ? '检测到识图服务 ' + args.vision.key : '检测到识图工具 ' + (args.vision.tools || []).join(',')) + '(S5 将自动调用)')
514
- else pushLog('识图能力探测:未检测到独立识图插件,S5 将让子智能体自检(无识图则走脚本分析)')
845
+ pushLog('流水线已启动: 五阶段 ' + PER_ROUND + ' 个子阶段(中控台双重质疑:扫描验证 + 评审决策;同一阶段回滚上限 ' + limit + ' 次),输出目录 ' + args.wsDir + (resumeCount > 0 ? '(断点续跑:从第 ' + (resumeCount + 1) + ' 个子阶段继续)' : '') + '(关闭本聊天窗口不影响后台执行)')
846
+ if (args.vision) pushLog('识图能力探测: ' + (args.vision.source === 'service' ? '检测到识图服务 ' + args.vision.key : '检测到识图工具 ' + (args.vision.tools || []).join(',')) + '(涉及图表的子阶段将自动调用)')
847
+ else pushLog('识图能力探测:未检测到独立识图插件,涉及图表的子阶段将标记"待人工确认"')
515
848
  if (explicitModel) pushLog('子任务模型路由:强制 ' + args.agentOptions.provider + '/' + args.agentOptions.model + '(--model 显式覆盖)')
516
849
  else pushLog('子任务模型路由:跟随父对话当前选择 ' + args.agentOptions.provider + '/' + args.agentOptions.model + ' (无法读取时兜底 deepseek-official/deepseek-v4-flash)')
517
850
  refreshFiles()
@@ -527,7 +860,7 @@ export function createPipeline({ fs, subagents, agents }) {
527
860
  isRunning: () => state.status === 'running',
528
861
  // 当前进度百分比(与启动校验提示的口径一致)
529
862
  progressPercent: () => (state.total > 0 ? Math.floor((state.done * 100) / state.total) : 0),
530
- // 断点续跑:已完成的迭代数(入口模块据此决定起始迭代)
863
+ // 断点续跑:已连续完成的子阶段数(入口模块据此决定起始子阶段)
531
864
  completedIterations,
532
865
  // 进度快照(供 /mcmp-api/state 与 /loopstatus)
533
866
  snapshot,