dsh-mcmp 2.2.0 → 2.2.1

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.
Files changed (3) hide show
  1. package/README.md +1 -3
  2. package/lib/index.js +24 -130
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,8 +19,6 @@ DeepSeek Harness **持久化 Cordis 插件**:面向全国大学生数学建模
19
19
  - **成果全落盘**:每步迭代完整追加保存(含质疑清单),旧内容绝不覆盖;代码保留全部历史版本;全新运行时旧目录自动归档为 `数学建模流水线_历史归档_*`
20
20
  - **实时进度面板**:独立浮动窗口(可拖拽/最小化/关闭),显示百分比进度条、当前轮/步骤/迭代、8 步骤打点、日志、产出文件,支持一键中止、重置(全新开始)
21
21
  - **断点续跑与重置**:中断/关闭窗口不丢进度,再次 `/loopbegin` 自动续跑;`/loopreset` 或面板「重置(全新开始)」清空续跑记录,下次从头跑
22
- - **迭代失败自动重试**:单个子任务因模型限流/过载(429、5xx)等瞬时故障失败时,自动退避重试(默认 15s/45s,最多 2 次),吸收抖动、避免"连续 3 败提前终止";确实持续失败才终止,并提示排查方向
23
- - **`--from` 文件自动补全**:`--from 文件名` 支持部分匹配,自动查找工作区根目录下的 `.md` 文档(前缀优先、包含次之;唯一命中即采用,歧义列出候选);只写 `--from` 会列出全部 MD 文档供选择——不用再复制粘贴完整文件名
24
22
  - **原生工作流卡片**:聊天区实时显示每个子智能体的运行状态(按阶段分组)
25
23
 
26
24
  ## 八大步骤(每轮 19 次迭代)
@@ -44,7 +42,7 @@ DeepSeek Harness **持久化 Cordis 插件**:面向全国大学生数学建模
44
42
  2. **触发插件**:发送一条以 `/loopbegin` 开头的消息:
45
43
  - `/loopbegin` —— 默认 1 轮
46
44
  - `/loopbegin --round=3` —— 3 轮外循环(上限 10)
47
- - `/loopbegin --from 题目.txt` —— 从文件读取赛题。**支持部分匹配自动补全**:只写 `--from` 会列出根目录全部 MD 文档;`--from 2024` 会自动匹配到 `2024 年高教社杯全国大学生数学建模竞赛题目.md`(唯一命中即用,歧义列出候选)
45
+ - `/loopbegin --from 题目.txt` —— 从文件读取赛题(支持含空格/中文的文件名,如 `--from 2024 年高教社杯全国大学生数学建模竞赛题目.md`,也可用 `--from=文件` 形式)
48
46
  3. **查看进度**:右下角浮动面板(百分比、步骤打点、日志、文件)+ 聊天区工作流卡片。
49
47
 
50
48
  辅助命令:`/loopstatus`(查看进度)、`/loopabort`(中止,已产出成果保留)、`/loopreset`(清空状态与断点续跑记录,下次 `/loopbegin` 从头全新开始;等价于面板「重置(全新开始)」按钮)。
package/lib/index.js CHANGED
@@ -29,38 +29,12 @@ export function apply(ctx) {
29
29
  const subagents = ctx.get('subagents')
30
30
  const systemPrompt = ctx.get('systemPrompt')
31
31
  const webServer = ctx.get('webServer')
32
- const timerSvc = ctx.get('timer')
33
- console.log('dsh-mcmp: services available -> commands=' + !!commands + ' fs=' + !!fs + ' agents=' + !!agents + ' subagents=' + !!subagents + ' systemPrompt=' + !!systemPrompt + ' webServer=' + !!webServer + ' timer=' + !!timerSvc)
32
+ console.log('dsh-mcmp: services available -> commands=' + !!commands + ' fs=' + !!fs + ' agents=' + !!agents + ' subagents=' + !!subagents + ' systemPrompt=' + !!systemPrompt + ' webServer=' + !!webServer)
34
33
 
35
34
  const FLOW_NAME = '数学建模论文流水线v2'
36
35
  // 全流水线缺陷台账:跨步骤错误登记、裁决与向上修复的唯一权威文件
37
36
  const LEDGER = '00_缺陷与裁决台账.md'
38
37
 
39
- // 迭代级重试:一次迭代失败(如模型 429/5xx 瞬时抖动)后自动退避重试,
40
- // 避免"连续 3 个子任务失败"被瞬时故障触发。重试间隔可用 --retry-ms=N 覆盖(测试/调优用)。
41
- const DEFAULT_RETRY_DELAYS = [15000, 45000]
42
- const MAX_ITER_RETRY = 2
43
-
44
- function sleepAbortable(ms, signal) {
45
- return new Promise((resolve) => {
46
- let settled = false
47
- let timerDispose = null
48
- const finish = () => {
49
- if (settled) return
50
- settled = true
51
- if (timerDispose) { try { timerDispose() } catch (err) { /* ignore */ } }
52
- resolve()
53
- }
54
- if (signal && signal.aborted) { resolve(); return }
55
- if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', finish, { once: true })
56
- if (timerSvc && typeof timerSvc.timeout === 'function') {
57
- try { timerDispose = timerSvc.timeout(finish, ms) } catch (err) { finish() }
58
- } else {
59
- finish() // 无 timer 服务时不做等待,直接重试
60
- }
61
- })
62
- }
63
-
64
38
  // ---------- 识图能力探测(宿主侧,仅供参考;以子智能体自检为准) ----------
65
39
  function detectVision() {
66
40
  // 1) 宿主服务:常见的识图服务键
@@ -139,7 +113,6 @@ export function apply(ctx) {
139
113
  let state = freshState()
140
114
  let active = null
141
115
  let lastSession = null // 最近一次运行所属会话,供「清空状态」追加重置标记
142
- let starting = false // 启动锁:async startPipeline 中等待目录扫描时防止并发启动
143
116
  const recording = new Map()
144
117
 
145
118
  function fmtTime(ts) {
@@ -316,67 +289,6 @@ export function apply(ctx) {
316
289
  return filtered.trim()
317
290
  }
318
291
 
319
- // ---------- --from 的文件补全:列出/匹配工作区根目录的 .md 文档 ----------
320
- async function listMdFiles(dir) {
321
- if (!fs || !dir) return []
322
- try {
323
- const root = await fs.resolve(dir)
324
- const entries = await fs.listDir(root)
325
- return entries
326
- .filter((e) => e && e.type === 'file' && /\.md$/i.test(e.name || ''))
327
- .map((e) => e.name)
328
- .sort()
329
- } catch (err) { /* 目录不存在/不可读时返回空 */ }
330
- return []
331
- }
332
-
333
- function pickMdMatch(names, partial) {
334
- const p = (partial || '').toLowerCase()
335
- const exact = names.filter((n) => n.toLowerCase() === p)
336
- if (exact.length === 1) return { kind: 'match', name: exact[0] }
337
- const starts = names.filter((n) => n.toLowerCase().startsWith(p))
338
- if (starts.length === 1) return { kind: 'match', name: starts[0] }
339
- if (starts.length > 1) return { kind: 'ambiguous', names: starts }
340
- const contains = names.filter((n) => n.toLowerCase().indexOf(p) >= 0)
341
- if (contains.length === 1) return { kind: 'match', name: contains[0] }
342
- if (contains.length > 1) return { kind: 'ambiguous', names: contains }
343
- return { kind: 'none' }
344
- }
345
-
346
- async function resolveFromPath(cwd, raw) {
347
- // 返回 { kind:'ok', path } | { kind:'error', text }
348
- let p = (raw || '').trim().replace(/^["']|["']$/g, '')
349
- const cwdNorm = cwd.replace(/[\\/]+$/, '')
350
- if (p.length === 0) {
351
- const names = await listMdFiles(cwd)
352
- if (names.length === 0) return { kind: 'error', text: '--from 未指定文件,且工作区根目录下没有 .md 文档。' }
353
- return { kind: 'error', text: '--from 未指定文件。工作区根目录下的 MD 文档:\n' + names.map((n) => ' - ' + n).join('\n') + '\n请用 --from 文件名(支持部分匹配,如 --from 2024)重新指定。' }
354
- }
355
- // 绝对路径
356
- if (/^[A-Za-z]:[\\/]/.test(p) || p.indexOf('/') === 0 || p.indexOf('\\') === 0) {
357
- return { kind: 'ok', path: p }
358
- }
359
- // 含路径分隔符的相对路径:直接按路径解析并验证存在
360
- if (p.indexOf('/') >= 0 || p.indexOf('\\') >= 0) {
361
- const full = cwdNorm + '/' + p
362
- try {
363
- const t = await fs.resolve(full)
364
- const st = await fs.stat(t)
365
- if (st && st.type === 'file') return { kind: 'ok', path: full }
366
- return { kind: 'error', text: '路径不是文件: ' + p }
367
- } catch (err) { /* fallthrough 到模糊匹配 */ }
368
- }
369
- // 模糊匹配文件名
370
- const names = await listMdFiles(cwd)
371
- const pick = pickMdMatch(names, p)
372
- if (pick.kind === 'match') return { kind: 'ok', path: cwdNorm + '/' + pick.name }
373
- if (pick.kind === 'ambiguous') {
374
- return { kind: 'error', text: '--from "' + p + '" 匹配到多个文档,请补充关键字:\n' + pick.names.map((n) => ' - ' + n).join('\n') }
375
- }
376
- const hint = names.length > 0 ? '\n工作区根目录现有 MD 文档:\n' + names.map((n) => ' - ' + n).join('\n') : ''
377
- return { kind: 'error', text: '未找到匹配 "' + p + '" 的 MD 文档。' + hint }
378
- }
379
-
380
292
  function buildPrompt(round, si, ii, s, it, args) {
381
293
  const p = []
382
294
  p.push('你正在参与「全国大学生数学建模竞赛论文」的多轮自动化撰写流水线,担任本轮「' + s.name + ' · ' + it.name + '」的执行专家。你的目标不是"写完",而是产出**可直接放进竞赛论文、经得起评审质疑**的内容。')
@@ -564,23 +476,10 @@ export function apply(ctx) {
564
476
  let started = false
565
477
  let outcome = 'failed'
566
478
  let note = ''
567
- // 迭代级重试:失败(如模型 429/5xx 瞬时抖动)后自动退避重试,吸收瞬时故障
568
- const retryDelays = Array.isArray(args.retryDelays) && args.retryDelays.length ? args.retryDelays : DEFAULT_RETRY_DELAYS
569
- let res = null
570
- for (let attempt = 0; ; attempt++) {
571
- res = await runChild(provider, session, agent, controller, label, buildPrompt(r, f.s, f.i, s, it, args), (child) => {
572
- if (!started) {
573
- started = true
574
- appendRec(runId, session, 'tool-workflow/agent-start', { runId, seq: g + 1, label, phase: s.phase, childId: String(child.id) })
575
- }
576
- })
577
- if (res.outcome !== 'failed' || attempt >= MAX_ITER_RETRY) break
578
- const delayMs = retryDelays[Math.min(attempt, retryDelays.length - 1)] || 0
579
- pushLog('迭代失败,将在 ' + Math.round(delayMs / 1000) + 's 后重试(第 ' + (attempt + 1) + '/' + MAX_ITER_RETRY + ' 次): ' + label)
580
- await sleepAbortable(delayMs, controller.signal)
581
- if (controller.signal.aborted) { cancelled = true; break }
582
- }
583
- if (cancelled) break
479
+ const res = await runChild(provider, session, agent, controller, label, buildPrompt(r, f.s, f.i, s, it, args), (child) => {
480
+ started = true
481
+ appendRec(runId, session, 'tool-workflow/agent-start', { runId, seq: g + 1, label, phase: s.phase, childId: String(child.id) })
482
+ })
584
483
  outcome = res.outcome
585
484
  note = (res.text || '').trim().slice(0, 300)
586
485
  if (res.err) note = '执行异常: ' + String((res.err && res.err.message) || res.err).slice(0, 200)
@@ -641,13 +540,12 @@ export function apply(ctx) {
641
540
  if (active && active.runId === runId) active = null
642
541
  }
643
542
 
644
- // ---------- 共享的流水线启动逻辑(async:--from 需要异步扫描目录;启动锁防并发) ----------
645
- async function startPipeline(agent, rawInput) {
646
- if (state.status === 'running' || starting) {
647
- return { kind: 'error', text: '已有流水线正在运行(当前进度 ' + (state.total > 0 ? Math.floor((state.done * 100) / state.total) : 0) + '%)。可在浮动面板点「中止」或运行 /loopabort 后再启动新流水线。' }
648
- }
649
- starting = true
543
+ // ---------- 共享的流水线启动逻辑(同步执行,永不阻塞命令通道) ----------
544
+ function startPipeline(agent, rawInput) {
650
545
  try {
546
+ if (state.status === 'running') {
547
+ return { kind: 'error', text: '已有流水线正在运行(当前进度 ' + (state.total > 0 ? Math.floor((state.done * 100) / state.total) : 0) + '%)。可在浮动面板点「中止」或运行 /loopabort 后再启动新流水线。' }
548
+ }
651
549
  if (!subagents) return { kind: 'error', text: '子智能体服务不可用(当前部署未提供),无法启动流水线。' }
652
550
  const session = agent && agent.session
653
551
  const cwd = session && session.header ? session.header.cwd : undefined
@@ -662,21 +560,20 @@ export function apply(ctx) {
662
560
  if (!Number.isSafeInteger(rounds) || rounds < 1) rounds = 1
663
561
  if (rounds > 10) return { kind: 'error', text: '--round 最大为 10。每轮 19 次迭代、每次迭代启动一个专家子智能体,轮数过大会非常耗时。' }
664
562
  const fresh = /(?:^|\s)--fresh\b/i.test(raw)
665
- // 迭代失败重试间隔(毫秒),默认 [15s, 45s];--retry-ms=N 可覆盖(测试/调优)
666
- const mRetry = /(?:^|\s)--retry-ms\s*[=:]?\s*(\d+)/i.exec(raw)
667
- const retryDelays = mRetry ? [Math.max(1, parseInt(mRetry[1], 10)), Math.max(1, parseInt(mRetry[1], 10))] : DEFAULT_RETRY_DELAYS
668
- // --from:允许只写 --from(列出根目录 MD 文档)或部分文件名(自动匹配补全)
669
- const hasFrom = /(?:^|\s)--from\b/i.test(raw)
670
- const mFrom = hasFrom ? /(?:^|\s)--from(?:=)?\s*([^\r\n]+?)(?=\s+--|\r?$)/i.exec(raw) : null
563
+ // --from 路径:允许含空格,遇到下一个 -- 标志或行尾即结束,避免吞掉后面的 --round/--fresh
564
+ const mFrom = /(?:^|\s)--from(?:=)?\s*([^\r\n]+?)(?=\s+--|\r?$)/i.exec(raw)
671
565
  let problem = null
672
- if (hasFrom) {
673
- const resolved = await resolveFromPath(cwd, mFrom ? mFrom[1] : '')
674
- if (resolved.kind === 'error') return { kind: 'error', text: resolved.text }
675
- problem = { kind: 'file', path: resolved.path }
566
+ if (mFrom) {
567
+ let p = mFrom[1].trim().replace(/^["']|["']$/g, '')
568
+ if (p.length === 0) return { kind: 'error', text: '--from 需要指定赛题文件路径。' }
569
+ const abs = /^[A-Za-z]:[\\/]/.test(p) || p.indexOf('/') === 0 || p.indexOf('\\') === 0
570
+ ? p
571
+ : cwd.replace(/[\\/]+$/, '') + '/' + p
572
+ problem = { kind: 'file', path: abs }
676
573
  } else {
677
574
  const text = extractProblem(session)
678
575
  if (text.trim().length < 40) {
679
- return { kind: 'error', text: '未能从对话中提取到赛题。请先把完整赛题粘贴到对话框并发送,再发送以 /loopbegin 开头的消息;或使用 /loopbegin --from 文件名(支持部分匹配自动查找根目录 .md 文档)。' }
576
+ return { kind: 'error', text: '未能从对话中提取到赛题。请先把完整赛题粘贴到对话框并发送,再发送以 /loopbegin 开头的消息;或使用 /loopbegin --from 题目.txt 指定赛题文件。' }
680
577
  }
681
578
  problem = { kind: 'text', text: text.length > 6000 ? text.slice(0, 6000) + '\n…(原文过长,已截断)' : text }
682
579
  }
@@ -684,7 +581,7 @@ export function apply(ctx) {
684
581
  const problemPath = problem.kind === 'file' ? problem.path : wsDir + '/00_赛题原文.md'
685
582
  const title = problem.kind === 'text' ? problem.text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)[0] : problem.path
686
583
  const vision = detectVision()
687
- const args = { rounds, wsDir, problemPath, problem, title: title ? title.slice(0, 40) : '赛题', vision, retryDelays }
584
+ const args = { rounds, wsDir, problemPath, problem, title: title ? title.slice(0, 40) : '赛题', vision }
688
585
  // 断点续跑:扫描本会话落盘的 v2 工作流记录,跳过已完成的迭代
689
586
  const resumeCount = fresh ? 0 : completedIterations(session)
690
587
  args.startIndex = resumeCount
@@ -730,8 +627,6 @@ export function apply(ctx) {
730
627
  }
731
628
  } catch (err) {
732
629
  return { kind: 'error', text: '启动失败: ' + String((err && err.message) || err) }
733
- } finally {
734
- starting = false
735
630
  }
736
631
  }
737
632
 
@@ -741,7 +636,7 @@ export function apply(ctx) {
741
636
  commands.register({
742
637
  name: 'loopbegin',
743
638
  description: '启动数学建模论文自动化流水线 v2(8 步骤:含图表视觉自检、评审修复闭环、论文定稿;每轮 19 次迭代、多轮外循环,成果全部落盘)',
744
- input: { hint: '--round=N 外循环轮数(默认 1);--from 文件名(支持部分匹配,自动查找根目录 .md 文档,如 --from 2024);--fresh 从头重跑' },
639
+ input: { hint: '--round=N 外循环轮数(默认 1);可选 --from 题目文件 指定赛题文件;--fresh 从头重跑' },
745
640
  handler: (invocation) => startPipeline(invocation.agent, invocation.rawInput),
746
641
  })
747
642
  } catch (err) { console.warn('dsh-mcmp: loopbegin 注册失败(可能被其他实例占用): ' + String((err && err.message) || err)) }
@@ -799,9 +694,8 @@ export function apply(ctx) {
799
694
  const agent = agents && agents.get(session.id)
800
695
  if (!agent) return
801
696
  console.log('dsh-mcmp: 检测到以 /loopbegin 开头的用户消息,自动启动流水线')
802
- Promise.resolve(startPipeline(agent, firstLine)).then((res) => {
803
- if (res && res.kind === 'error') pushLog('消息触发启动失败: ' + res.text)
804
- }).catch(() => { /* 忽略 */ })
697
+ const res = startPipeline(agent, firstLine)
698
+ if (res && res.kind === 'error') pushLog('消息触发启动失败: ' + res.text)
805
699
  } catch (err) { /* 触发器自身异常忽略,不影响消息流 */ }
806
700
  }, { global: true })
807
701
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcmp",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "DeepSeek Harness 插件:数学建模论文自动化流水线 v2(8 步骤 × 19 迭代 × N 轮:图表视觉自检与修复、评审问题修复闭环、跨步骤缺陷台账与向上修复、论文兜底定稿、质疑驱动、断点续跑、进度面板)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",