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/README.md +72 -59
- package/lib/client.js +60 -16
- package/lib/index.js +92 -25
- package/lib/pipeline.js +547 -214
- package/package.json +2 -2
package/lib/pipeline.js
CHANGED
|
@@ -1,73 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-mcmp ·
|
|
2
|
+
* dsh-mcmp · 数学建模竞赛自动化论文撰写系统 —— 论文写作主循环模块(Host 侧)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 = '
|
|
20
|
-
//
|
|
21
|
-
export const
|
|
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
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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: '
|
|
46
|
-
{
|
|
47
|
-
{
|
|
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: '
|
|
50
|
-
{ name: '
|
|
51
|
-
{ name: '
|
|
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: '
|
|
54
|
-
{ name: '
|
|
55
|
-
{ name: '
|
|
56
|
-
{ name: '
|
|
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: '
|
|
59
|
-
{ name: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
248
|
-
p.push('
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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('
|
|
270
|
-
p.push('
|
|
271
|
-
p.push('
|
|
272
|
-
p.push('
|
|
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('
|
|
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(
|
|
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('
|
|
514
|
+
p.push('你正在参与「全国大学生数学建模竞赛论文」自动化撰写系统的**兜底定稿**环节。此前流水线提前结束(原因:' + reason + '),但交付要求是"不论最终结果如何,都必须产出一版完整的论文"。现在由你仅凭现有文件完成这个承诺。')
|
|
291
515
|
p.push('')
|
|
292
516
|
p.push('【工作区(绝对路径)】' + args.wsDir + ' —— 所有读写都在该目录内。')
|
|
293
|
-
|
|
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('-
|
|
302
|
-
p.push('-
|
|
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.
|
|
307
|
-
p.push('2.
|
|
308
|
-
p.push('3.
|
|
309
|
-
p.push('4.
|
|
310
|
-
p.push('5.
|
|
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:标题、摘要(含关键词)、问题重述与背景、假设与符号说明、建模与求解(公式编号、图表嵌入)、结果分析与模型评价、参考文献、附录(程序清单);公式行间 $$...$$、行内 $...$,图表 ,表格 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
|
|
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 //
|
|
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 =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
|
641
|
+
appendRec(runId, session, 'tool-workflow/agent-start', { runId, seq, label, phase, childId: String(child.id) })
|
|
425
642
|
}, args.agentOptions)
|
|
426
|
-
outcome
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
// ---------- 兜底定稿:不论成败,只要
|
|
788
|
+
// ---------- 兜底定稿:不论成败,只要 4.3 未完成,就补一次论文定稿(防崩溃优先) ----------
|
|
458
789
|
if (!finalDone) {
|
|
459
|
-
pushLog('正在执行兜底定稿:生成
|
|
460
|
-
state.phase = '
|
|
461
|
-
state.agentLabel = '
|
|
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('兜底定稿完成:
|
|
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 =
|
|
502
|
-
state.total =
|
|
831
|
+
state.rounds = 1
|
|
832
|
+
state.total = PER_ROUND
|
|
503
833
|
state.done = resumeCount
|
|
504
|
-
state.round =
|
|
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('流水线已启动: ' +
|
|
513
|
-
if (args.vision) pushLog('识图能力探测: ' + (args.vision.source === 'service' ? '检测到识图服务 ' + args.vision.key : '检测到识图工具 ' + (args.vision.tools || []).join(',')) + '(
|
|
514
|
-
else pushLog('
|
|
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,
|