@whyshu/argent-engine 0.7.2 → 0.7.4

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 (57) hide show
  1. package/MANIFEST.json +46 -40
  2. package/engine/VERSION +1 -1
  3. package/engine/scripts/appointment-create.mjs +34 -0
  4. package/engine/scripts/appointment-stop.mjs +18 -0
  5. package/engine/scripts/appointment-worker.mjs +96 -0
  6. package/engine/scripts/appointment_grab.py +327 -0
  7. package/engine/scripts/argent-runner.mjs +223 -0
  8. package/engine/scripts/argent-setup-check.mjs +3 -3
  9. package/engine/scripts/capability-packs.lock.json +34 -40
  10. package/engine/scripts/check-agent-catalog.mjs +1 -1
  11. package/engine/scripts/check-config-catalog.mjs +0 -1
  12. package/engine/scripts/collect-evidence.mjs +2 -5
  13. package/engine/scripts/daily-report.mjs +315 -12
  14. package/engine/scripts/drill-clean-room.sh +45 -0
  15. package/engine/scripts/drill-handover.sh +4 -1
  16. package/engine/scripts/dsh-upgrade.mjs +16 -20
  17. package/engine/scripts/flow-requirements.json +5 -7
  18. package/engine/scripts/flow-timers.sh +1 -1
  19. package/engine/scripts/flow-trigger.sh +1 -1
  20. package/engine/scripts/gen-preset-rows.mjs +20 -0
  21. package/engine/scripts/handover-sheet.mjs +9 -1
  22. package/engine/scripts/invoice_stageb.py +20 -1
  23. package/engine/scripts/lib/agent-install.mjs +10 -1
  24. package/engine/scripts/lib/daily-report-core.mjs +124 -16
  25. package/engine/scripts/lib/dsh-versions.mjs +17 -1
  26. package/engine/scripts/lib/ml-ads-core.mjs +204 -0
  27. package/engine/scripts/lib/plugin-packages.mjs +1 -0
  28. package/engine/scripts/ml-ads.mjs +181 -0
  29. package/engine/scripts/ml-oauth-setup.mjs +1 -1
  30. package/engine/scripts/orchestrator.py +91 -10
  31. package/engine/scripts/p2-report-run.mjs +102 -0
  32. package/engine/scripts/patches/dsh-session--append-message-shape.patch +34 -9
  33. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +32 -8
  34. package/engine/scripts/patches/retire-check.sh +0 -4
  35. package/engine/scripts/patches/verify-hardening.mjs +4 -1
  36. package/engine/scripts/patches/verify-turn-outside-replace.mjs +64 -0
  37. package/engine/scripts/poll-fulfillment.sh +1 -1
  38. package/engine/scripts/profile-doctor.mjs +1 -1
  39. package/engine/scripts/python-runtime.sh +16 -4
  40. package/engine/scripts/release-argent.sh +8 -7
  41. package/engine/scripts/session-handoff.mjs +111 -0
  42. package/engine/scripts/session-load-check.mjs +105 -0
  43. package/engine/scripts/sync-scripts.sh +1 -1
  44. package/engine/scripts/xzh-browser.sh +37 -0
  45. package/engine/scripts/xzh_browser.py +49 -0
  46. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +30 -0
  47. package/engine/skills/ml-full-appointment/SKILL.md +98 -0
  48. package/engine/version.mjs +1 -1
  49. package/package.json +1 -1
  50. package/engine/scripts/drill-clean-machine.sh +0 -315
  51. package/engine/scripts/install-internal-role.sh +0 -190
  52. package/engine/scripts/install-one-shot.mjs +0 -665
  53. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +0 -297
  54. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +0 -11
  55. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +0 -39
  56. package/engine/scripts/patches/verify-bundled-migrators.mjs +0 -79
  57. package/engine/scripts/patches/verify-transport-diagnostics.mjs +0 -227
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-turn-outside-replace —— 补丁 `dsh-session-format-v3-to-v4--turn-outside-replace` 的自检。
4
+ *
5
+ * 这个补丁修的是**加载期**问题(不是写路径),它最危险的失效方式是"升级换了新树 ⇒ 补丁没跟着走 ⇒
6
+ * 某个旧会话突然打不开,而且没有运行期报错提示"(2026-10-17 本机就这么复现了一次)。故门禁要能
7
+ * 在**升级后立刻**判出"补丁在不在"。
8
+ *
9
+ * 判据两层:
10
+ * ① 结构(始终跑):目标模块里**确有**放宽逻辑(`Number.isSafeInteger(t)` + `t >= this.nextTurn` + 标记注释);
11
+ * 且**没有**退回成裸 `this.requireTurn(event.type)`。
12
+ * ② 行为(可选):给了 `DSH_TURN_OUTSIDE_SESSION=<一个 v4 日志路径>` 时,用真实加载校验
13
+ * (scripts/session-load-check.mjs,`validation:'current'`)证明它现在能过。
14
+ * 不给就**跳过并明说**(⛔ 不假装验过;本机已知样本:session-06732069 的 seq 30732)。
15
+ *
16
+ * 用法:node scripts/patches/verify-turn-outside-replace.mjs
17
+ * DSH_TURN_OUTSIDE_SESSION=~/.dsh/sessions/…/session.v4.jsonl.zstd node …(会展开 ~)
18
+ * 退出码:0 通过(或"结构过 + 行为跳过")/ 1 结构缺失 / 2 证据不足(scope 探不到)
19
+ */
20
+ import { createRequire } from 'node:module'
21
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+ import { spawnSync } from 'node:child_process'
24
+ import { homedir } from 'node:os'
25
+ import { requireDshScope } from '../lib/dsh-scope.mjs'
26
+
27
+ let scope
28
+ try { scope = requireDshScope({ what: '补丁自检(verify-turn-outside-replace)' }) }
29
+ catch (e) { console.error(` ❌ ${e.message}`); process.exit(2) }
30
+
31
+ const target = join(scope, 'dsh-session-format-v3-to-v4', 'lib', 'index.js')
32
+ if (!existsSync(target)) {
33
+ console.error(` ❌ 目标模块不存在:${target}(上游可能改名/改结构)`)
34
+ process.exit(1)
35
+ }
36
+ const src = readFileSync(target, 'utf8')
37
+
38
+ let pass = 0
39
+ let fail = 0
40
+ const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
41
+
42
+ ok(/Argent hotfix(0\.2\.0-rc\.2 移植版/.test(src), '放宽逻辑的标记注释在位(说明"这是本仓库的补丁,不是上游原生行为")')
43
+ ok(/if \(this\.turn === null\) \{\s*const t = data\["turn"\];\s*if \(!Number\.isSafeInteger\(t\) \|\| t >= this\.nextTurn\) throw new SessionFormatError/.test(src),
44
+ '放宽口径在位:turn 外只要求 data.turn 是已关闭的历史 turn(安全整数且 < nextTurn)')
45
+ // 反向:不能退回成"一律要求打开 turn"(那样旧会话又会打不开)
46
+ ok(!/if \(event\.type === "tool\/result" && event\["surfaceOp"\] !== "append"\) \{\s*this\.requireTurn\(event\.type\);\s*return;/.test(src),
47
+ '没有退回成裸 requireTurn(退回去 = 旧会话重新打不开)')
48
+
49
+ // ② 行为(可选,不假装)
50
+ const fixture = (process.env.DSH_TURN_OUTSIDE_SESSION || '').replace(/^~(?=$|\/)/, homedir())
51
+ if (!fixture) {
52
+ console.log(' ⏭ 行为校验跳过:未给 DSH_TURN_OUTSIDE_SESSION(结构判据已覆盖"补丁在不在")')
53
+ } else if (!existsSync(fixture)) {
54
+ console.log(` ⏭ 行为校验跳过:DSH_TURN_OUTSIDE_SESSION 指向的文件不存在(${fixture})`)
55
+ } else {
56
+ const r = spawnSync(process.execPath, ['--max-old-space-size=8192', join(import.meta.dirname, '..', 'session-load-check.mjs'), fixture], {
57
+ encoding: 'utf8', env: { ...process.env, DSH_SCOPE: scope },
58
+ })
59
+ const out = `${r.stdout || ''}${r.stderr || ''}`
60
+ ok(r.status === 0, `行为校验:该会话能过真实加载校验(${fixture})${r.status === 0 ? '' : `\n ${out.trim().split('\n').slice(-4).join('\n ')}`}`)
61
+ }
62
+
63
+ console.log(`\n${fail === 0 ? '✅' : '❌'} verify-turn-outside-replace:通过 ${pass} / 失败 ${fail}`)
64
+ process.exit(fail === 0 ? 0 : 1)
@@ -102,7 +102,7 @@ pick_py() {
102
102
  [ -x "$c" ] || continue
103
103
  "$c" -c 'import openpyxl, yaml' 2>/dev/null || continue # 硬要求:配置/发票生成必需
104
104
  "$c" -c 'import PIL, playwright' 2>/dev/null || \
105
- echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:node install-argent.mjs --venv-only)" >&2
105
+ echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:bash scripts/python-runtime.sh ensure)" >&2
106
106
  printf '%s' "$c"; return 0
107
107
  done
108
108
  return 1
@@ -518,7 +518,7 @@ function checkPackIntegrity() {
518
518
  const msg = '未找到 install.json(可能未按能力清单安装)'
519
519
  line('WARN', msg)
520
520
  record('warn', 'pkg-install-json-missing', msg,
521
- `按能力安装:node install-argent.mjs --api-key <账号 API Key>(或核对 --dsh-home / ARGENT_INSTALL_JSON=${INSTALL_JSON})`)
521
+ `按能力安装:由 @whyshu/argent 启动器安装(或核对 --dsh-home / ARGENT_INSTALL_JSON=${INSTALL_JSON})`)
522
522
  return
523
523
  }
524
524
  const doc = readInstallJson(INSTALL_JSON)
@@ -23,9 +23,11 @@ case "$(uname -s 2>/dev/null || echo unknown)" in
23
23
  esac
24
24
  # P0-1(2026-10-08):体检项 4 → 5(补 selenium)。理由:官方唯一推荐的驱动档位
25
25
  # (ZINIAO_MODE=selenium)此前在 venv 里**没装** ⇒ 选型开关静默撒谎、该档位从未运行过一次。
26
- # ⚠️ 本清单必须与 install-argent.mjs 的 pythonRuntimePlan().pkgs 和
26
+ # ⚠️ 本清单必须与 scripts/lib/python-runtime.mjs 的 venvUsable(pkgs=…) 和
27
27
  # scripts/lib/python-runtime.mjs 的 venvUsable(pkgs=…) 保持一致(缺一就会"体检红但 ensure 空转")。
28
28
  PKGS=(openpyxl yaml PIL playwright selenium)
29
+ # pip 安装名(import 名 ≠ 包名:yaml→pyyaml、PIL→pillow)
30
+ PIP_PKGS=(openpyxl pyyaml pillow playwright selenium)
29
31
  # 交付纪律:本清单变更必须与"客户机 venv 补装 selenium(bash scripts/python-runtime.sh ensure)"
30
32
  # 同批落地,否则客户机体检会先变红(4/5)。
31
33
 
@@ -53,10 +55,20 @@ cmd_check() {
53
55
  }
54
56
 
55
57
  cmd_ensure() {
56
- if [ ! -f "$REPO/install-argent.mjs" ]; then
57
- echo " ❌ 找不到 install-argent.mjs(本脚本需在 argent-dsh 仓库内运行)" >&2; return 2
58
+ # 旧安装器退役(2026-10-18):直接创建持久 venv 并 pip 安装运行时依赖
59
+ local venv_dir; venv_dir="$(dirname "$VENV_BIN")"
60
+ local pybin="${PYTHON_BIN:-$(command -v python3 2>/dev/null || true)}"
61
+ if [ -z "$pybin" ] || [ ! -x "$pybin" ]; then
62
+ echo " ❌ 找不到 python3;请先安装 Python 3.10+" >&2; return 2
58
63
  fi
59
- node "$REPO/install-argent.mjs" --venv-only --dsh-home "${DSH_HOME:-$HOME/.dsh}" || return $?
64
+ if [ ! -x "$VENV_PY" ]; then
65
+ echo " → 创建 venv:$venv_dir"
66
+ "$pybin" -m venv "$venv_dir" || return 1
67
+ fi
68
+ echo " → 安装运行时依赖:${PIP_PKGS[*]}"
69
+ "$VENV_PY" -m pip install --upgrade pip >/dev/null 2>&1 || true
70
+ "$VENV_PY" -m pip install "${PIP_PKGS[@]}" || return $?
71
+ echo " ✅ venv 就绪:$VENV_PY"
60
72
  }
61
73
 
62
74
  cmd_path() {
@@ -69,7 +69,8 @@ REMOTE="${ARGENT_RELEASE_REMOTE:-$(git -C "$REPO_DIR" remote | head -1)}"
69
69
  # 2026-10-09:补 install.sh —— 官网 macOS/Linux 一行命令 `curl .../dl/install.sh | bash` 的入口
70
70
  # (旧的 `curl .../dl/install-argent.mjs | node` 因模块级相对 import 必然 ERR_MODULE_NOT_FOUND,
71
71
  # 已被 install.sh 取代)。`install-dsh.sh` 留在这里只为**存量老命令**仍能取到,是**遗留**件。
72
- CORE_FILES=("install-argent.mjs" "install.sh" "install-dsh.sh" "install.ps1" "argent-setup.md")
72
+ # 旧一句话安装器已退役(2026-10-18):只保留安装说明页;启动器由 @whyshu/argent 承担
73
+ CORE_FILES=("argent-setup.md")
73
74
  # 目录级分发(--full 时启用;服务器对应目录有独立静态资源,非同名覆盖不删旧)
74
75
  DIR_FILES=("skills" "plugins")
75
76
 
@@ -184,7 +185,7 @@ pick_py() {
184
185
  [ -x "$c" ] || continue
185
186
  "$c" -c 'import openpyxl, yaml' 2>/dev/null || continue
186
187
  "$c" -c 'import PIL, playwright' 2>/dev/null || \
187
- echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:node install-argent.mjs --venv-only)" >&2
188
+ echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:运行 @whyshu/argent 启动器自检子命令按提示补装;或手动 pip 安装)" >&2
188
189
  printf '%s' "$c"; return 0
189
190
  done
190
191
  return 1
@@ -343,7 +344,7 @@ pick_py() {
343
344
  [ -x "$c" ] || continue
344
345
  "$c" -c 'import openpyxl, yaml' 2>/dev/null || continue # 硬要求:配置/发票生成必需
345
346
  "$c" -c 'import PIL, playwright' 2>/dev/null || \
346
- echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:node install-argent.mjs --venv-only)" >&2
347
+ echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:运行 @whyshu/argent 启动器自检子命令按提示补装;或手动 pip 安装)" >&2
347
348
  printf '%s' "$c"; return 0
348
349
  done
349
350
  return 1
@@ -537,7 +538,7 @@ if [[ $FULL -eq 1 ]]; then
537
538
  else
538
539
  echo " 3. (未指定 --full:skills/ plugins/ 目录不同步——服务器有独立静态资源)"
539
540
  fi
540
- echo " 4. 验证:本地/服务器 md5 + curl -sI ${DL_URL}/install-argent.mjs"
541
+ echo " 4. 验证:本地/服务器 md5 + curl -sI ${DL_URL}/argent-setup.md"
541
542
 
542
543
  if [[ $DRY_RUN -eq 1 ]]; then
543
544
  echo
@@ -594,11 +595,11 @@ for f in "${CORE_FILES[@]}"; do
594
595
  VERIFY_OK=0
595
596
  fi
596
597
  done
597
- http_code="$(curl -sI -o /dev/null -w '%{http_code}' "${DL_URL}/install-argent.mjs")"
598
+ http_code="$(curl -sI -o /dev/null -w '%{http_code}' "${DL_URL}/argent-setup.md")"
598
599
  if [[ "$http_code" == "200" ]]; then
599
- echo " ✓ ${DL_URL}/install-argent.mjs HTTP ${http_code}"
600
+ echo " ✓ ${DL_URL}/argent-setup.md HTTP ${http_code}"
600
601
  else
601
- echo " ✗ ${DL_URL}/install-argent.mjs HTTP ${http_code}(期望 200)"
602
+ echo " ✗ ${DL_URL}/argent-setup.md HTTP ${http_code}(期望 200)"
602
603
  VERIFY_OK=0
603
604
  fi
604
605
 
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // `session-handoff` —— 给老会话产出"续接摘要"(S4 收尾配套)
3
+ // 目的:老格式会话日志一旦退役/升级后打不开,"工作能续上"由**摘要**承担。
4
+ // 真实形状(2026-10-16 实测):header `{type:"session",version,id,createdAt,cwd,agentPreset}`;
5
+ // 事件 `{type,seq,time,data}`;用户输入在 `agent/inbox/spliced → data.inserted[].content[].text`。
6
+ // 用法:--scan | --write [--format v3|v4|all] [--limit N]
7
+ // ⛔ 只读日志、只写摘要目录;多帧 zstd 按 magic 切帧逐帧解压。
8
+ import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
9
+ import { join } from 'node:path'
10
+ import { homedir } from 'node:os'
11
+ import { zstdDecompressSync } from 'node:zlib'
12
+
13
+ const args = process.argv.slice(2)
14
+ const has = (f) => args.includes(f)
15
+ const val = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d }
16
+ const HOME = process.env.HOME || homedir()
17
+ const ROOT = join(HOME, '.dsh', 'sessions')
18
+ const OUT = join(HOME, '.dsh', 'argent', 'session-handoffs')
19
+ const FORMAT = val('--format', 'v3'); const LIMIT = Number(val('--limit', '0')) || 0; const WRITE = has('--write')
20
+ const DAYS = Number(val('--days', '10')) || 0 // 只处理最近 N 天内动过的主会话(0 = 全部)
21
+ const CHILDREN = has('--include-children') // 默认**排除**子会话(delegationDepth>0)
22
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
23
+
24
+ function readText(file) {
25
+ const buf = readFileSync(file); const pos = []; let i = 0
26
+ while ((i = buf.indexOf(MAGIC, i)) !== -1) { pos.push(i); i += 4 }
27
+ if (!pos.length) return ''
28
+ pos.push(buf.length); let out = ''
29
+ for (let k = 0; k < pos.length - 1; k++) { try { out += zstdDecompressSync(buf.subarray(pos[k], pos[k + 1])).toString('utf8') } catch { /* 坏帧跳过 */ } }
30
+ return out
31
+ }
32
+ /** 递归找所有文本块(形状容错:content[]/inserted[]/text) */
33
+ function texts(node, acc = []) {
34
+ if (!node) return acc
35
+ if (typeof node === 'string') { if (node.trim()) acc.push(node.trim()); return acc }
36
+ if (Array.isArray(node)) { for (const x of node) texts(x, acc); return acc }
37
+ if (typeof node === 'object') {
38
+ if (typeof node.text === 'string' && node.text.trim()) acc.push(node.text.trim())
39
+ for (const k of ['content', 'inserted', 'message', 'parts', 'blocks', 'data']) if (node[k] !== undefined) texts(node[k], acc)
40
+ }
41
+ return acc
42
+ }
43
+ const clean = (s, n) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n)
44
+ const isUser = (e) => /inbox|user/.test(String(e.type)) || (e.data && e.data.source && e.data.source.kind === 'user')
45
+
46
+ function summarize(file, id, project, ver) {
47
+ const lines = readText(file).split('\n').filter(Boolean)
48
+ let header = null, ts0 = null, ts1 = null, events = 0
49
+ const users = [], assistants = [], all = []
50
+ for (const l of lines) {
51
+ let e; try { e = JSON.parse(l) } catch { continue }
52
+ if (e.type === 'session') { header = e; continue }
53
+ events++
54
+ const t = e.time || (e.data && e.data.time)
55
+ if (t) { const iso = new Date(t).toISOString(); ts0 = ts0 || iso; ts1 = iso }
56
+ const blocks = texts(e.data ?? e)
57
+ if (!blocks.length) continue
58
+ const joined = clean(blocks.join(' | '), 600)
59
+ all.push(joined)
60
+ if (isUser(e)) users.push(joined); else assistants.push(joined)
61
+ }
62
+ const todos = all.join('\n').split(/||\n/).map((s) => s.trim()).filter((l) => /(^\s*-\s*\[ \])|TODO|待办|下一步|下一步是/.test(l)).slice(0, 10)
63
+ return { id, project, ver, file, bytes: statSync(file).size, created: header && header.createdAt ? new Date(header.createdAt).toISOString() : null,
64
+ ts0, ts1, events, turns: users.length + assistants.length, preset: (header && header.agentPreset) || '', cwd: (header && header.cwd) || '', depth: (header && header.delegationDepth) || 0,
65
+ title: clean(users[0], 140), lastUser: clean(users[users.length - 1], 400), lastAsst: clean(assistants[assistants.length - 1], 400), todos }
66
+ }
67
+
68
+ function walk(dir, out = []) {
69
+ let es = []; try { es = readdirSync(dir, { withFileTypes: true }) } catch { return out }
70
+ for (const e of es) {
71
+ const p = join(dir, e.name)
72
+ if (e.isDirectory()) walk(p, out)
73
+ else {
74
+ const m = e.name.match(/^session\.(v\d+)\.jsonl\.zstd$/)
75
+ if (m && (FORMAT === 'all' || m[1] === FORMAT)) out.push({ file: p, ver: m[1], id: (p.match(/session-([0-9a-f-]{8,})/) || [])[1] || p.split('/').slice(-2)[0].replace(/^session[.-]?/, '') || e.name, project: p.replace(ROOT + '/', '').split('/')[0] })
76
+ }
77
+ }
78
+ return out
79
+ }
80
+
81
+ const CUT = DAYS ? Date.now() - DAYS * 86400000 : 0
82
+ const walkAll = walk(ROOT)
83
+ const recent = DAYS ? walkAll.filter((x) => statSync(x.file).mtimeMs >= CUT) : walkAll
84
+ const all = recent.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs)
85
+ const list = LIMIT ? all.slice(0, LIMIT) : all
86
+ const rows = []; let bad = 0
87
+ for (const it of list) { try { rows.push(summarize(it.file, it.id, it.project, it.ver)) } catch { bad++ } }
88
+ const kept = CHILDREN ? rows : rows.filter((r) => !r.depth)
89
+ for (const r of rows) if (!kept.includes(r)) { /* 子会话:跳过(--include-children 可纳入) */ }
90
+ const titled = kept.filter((r) => r.title).length
91
+ console.log(JSON.stringify({ ok: true, days: DAYS, candidates: all.length, childrenExcluded: CHILDREN ? 0 : rows.length - kept.length, kept: kept.length, format: FORMAT, unreadable: bad, withTitle: titled, write: WRITE }))
92
+ if (!WRITE) { console.log(JSON.stringify(kept.slice(0, 3).map((r) => ({ id: r.id.slice(0, 8), ver: r.ver, preset: r.preset, events: r.events, turns: r.turns, title: r.title.slice(0, 70) })), null, 1)); process.exit(0) }
93
+ mkdirSync(OUT, { recursive: true })
94
+ const slug = (s) => s.replace(/[^\w.-]/g, '_')
95
+ for (const r of kept) {
96
+ writeFileSync(join(OUT, `${slug(r.project)}__${r.id}.${r.ver}.md`), [
97
+ `# 会话续接摘要|${r.id}(${r.ver})`, '',
98
+ `- 项目:\`${r.project}\` cwd:\`${r.cwd}\` preset:\`${r.preset}\``,
99
+ `- 日志:\`${r.file}\`(${(r.bytes / 1024).toFixed(0)}KB,${r.events} 事件 / ${r.turns} 轮)`,
100
+ `- 时间:${(r.created || r.ts0 || '?').slice(0, 16).replace('T', ' ')} → ${(r.ts1 || '?').slice(0, 16).replace('T', ' ')}`, '',
101
+ '## 首轮需求(标题)', '', r.title || '(未提取到)', '',
102
+ '## 最后一轮用户输入', '', r.lastUser || '(未提取到)', '',
103
+ '## 最后一条助手输出', '', r.lastAsst || '(未提取到)', '',
104
+ '## 疑似待办 / 下一步(原文行)', '', ...(r.todos.length ? r.todos.map((t) => `- ${clean(t, 200)}`) : ['(未提取到)']), '',
105
+ '> ⚠️ 自动提取(best effort):日志退役后,续接以本摘要 + 仓库文档/提交为准。', ''].join('\n'))
106
+ }
107
+ writeFileSync(join(OUT, 'INDEX.md'), ['# 老会话续接摘要索引', '', `- 生成:${new Date().toISOString().slice(0, 16).replace('T', ' ')}`,
108
+ `- 范围:format=${FORMAT} | 近 ${DAYS} 天 | 本次 ${kept.length} 条(候选 ${all.length} 条)| 有标题 ${titled} 条`, '',
109
+ '| 会话 | 版本 | preset | 时间 | 标题 | 摘要 |', '|---|---|---|---|---|---|',
110
+ ...rows.map((r) => `| \`${r.id.slice(0, 8)}\` | ${r.ver} | ${r.preset} | ${(r.created || r.ts0 || '?').slice(0, 10)} | ${(r.title || '').replace(/\|/g, '/').slice(0, 56)} | [md](${slug(r.project)}__${r.id}.${r.ver}.md) |`), ''].join('\n'))
111
+ console.log(`✅ 已写 ${kept.length} 份摘要(有标题 ${titled})+ INDEX.md ⇒ ${OUT}`)
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-load-check —— 用**真实加载路径的校验**回放 session 日志(只读)。
4
+ *
5
+ * 与 `scripts/patches/verify-session-migration.mjs` 的分工(两者都必要,别互相替代):
6
+ * · verify-session-migration:跑**格式迁移链**(v0→v4),`validation: 'transformed'`
7
+ * ⇒ 只回答"能不能迁移";v4 文件是恒等迁移,**不会**触发 v4 结构校验。
8
+ * · 本工具:`validation: 'current'` ⇒ 触发 `restoreCurrent()`(= 宿主真正加载时走的校验,
9
+ * 含 turn/step 作用域、surface 折叠、工具生命周期等)。
10
+ *
11
+ * 为什么要它(2026-10-17 实测):主会话 06732069 在 0.2.0 上打不开
12
+ * (`SessionFormatError: tool/result is outside an open turn`),而 verify-session-migration
13
+ * 对同一个文件**判绿**——因为迁移校验不跑 v4 结构校验。别再拿迁移绿灯当"会话能打开"的证据。
14
+ *
15
+ * 用法:
16
+ * DSH_SCOPE=<…/@deepseek-ai> node scripts/session-load-check.mjs <session.vN.jsonl.zstd> [...]
17
+ * 不给文件 ⇒ 校验"当前用户近 N 天最常打开的几个 v4 会话"(默认 3 个,按 mtime 取最新)。
18
+ * 退出码:0 全绿 / 1 有失败 / 2 用法或环境错误。
19
+ *
20
+ * 边界(诚实):只做**读取 + 校验**,不迁移、不写任何文件;v3 及更早日志的 v3→v4 迁移需要
21
+ * 子会话事实,本工具传空数组 ⇒ 对 v3 文件的结论仅作参考(该场景用 verify-session-migration)。
22
+ */
23
+ import { createRequire } from 'node:module'
24
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
25
+ import { join } from 'node:path'
26
+ import { requireDshScope } from './lib/dsh-scope.mjs'
27
+ import { decodeSessionLines } from './lib/zstd-frames.mjs'
28
+
29
+ let scope
30
+ try { scope = requireDshScope({ what: 'session 加载校验(session-load-check)' }) }
31
+ catch (e) { console.error(` ❌ ${e.message}`); process.exit(2) }
32
+ const requireGlobal = createRequire(scope.replace(/\/?$/, '/'))
33
+ const { createSessionFormatCatalogWithChildren, sessionFormatCatalog } =
34
+ requireGlobal('@deepseek-ai/dsh-session-format-catalog')
35
+
36
+ /** 取最近修改的 v4 日志(默认样本源)。 */
37
+ function recentV4(limit = 3) {
38
+ const root = join(process.env.DSH_HOME || join(process.env.HOME || '', '.dsh'), 'sessions')
39
+ const out = []
40
+ const walk = (dir, depth = 0) => {
41
+ let entries
42
+ try { entries = readdirSync(dir) } catch { return }
43
+ for (const name of entries) {
44
+ const p = join(dir, name)
45
+ let st
46
+ try { st = statSync(p) } catch { continue }
47
+ if (st.isDirectory()) { if (depth < 2) walk(p, depth + 1); continue }
48
+ if (/^session\.v4\.jsonl\.zstd$/.test(name)) out.push({ p, mtime: st.mtimeMs })
49
+ }
50
+ }
51
+ walk(root)
52
+ return out.sort((a, b) => b.mtime - a.mtime).slice(0, limit).map((x) => x.p)
53
+ }
54
+
55
+ function checkOne(file) {
56
+ let decoded
57
+ try { decoded = decodeSessionLines(readFileSync(file)) } catch (e) {
58
+ return { ok: false, why: `解压失败:${e.message}` }
59
+ }
60
+ const { header: headerRaw, lines } = decoded
61
+ const header = JSON.parse(headerRaw.split('\n')[0])
62
+ // v3 及更早:v3→v4 迁移需要子会话事实;本工具传空数组 ⇒ 结论仅供参考
63
+ const catalog = createSessionFormatCatalogWithChildren
64
+ ? createSessionFormatCatalogWithChildren([])
65
+ : sessionFormatCatalog
66
+ let restore
67
+ try { restore = catalog.createRestore(header, { recovery: 'strict', validation: 'current' }) } catch (e) {
68
+ return { ok: false, why: `createRestore 失败:${e.message}` }
69
+ }
70
+ let n = 0
71
+ for (const { raw } of lines) {
72
+ if (!raw.trim()) continue
73
+ n += 1
74
+ let ev
75
+ try { ev = JSON.parse(raw) } catch (e) { return { ok: false, why: `行 ${n} JSON 解析失败:${e.message}` } }
76
+ try { restore.decodeRow(ev) } catch (e) {
77
+ return { ok: false, why: `行 ${n} / seq ${ev.seq} / ${ev.type}:${e.name}: ${e.message}` }
78
+ }
79
+ }
80
+ try {
81
+ const artifact = restore.finish()
82
+ return { ok: true, detail: `v${header.version} → v${artifact.header.version},事件 ${artifact.events.length},行 ${n}`, header }
83
+ } catch (e) {
84
+ return { ok: false, why: `finish:${e.name}: ${e.message}` }
85
+ }
86
+ }
87
+
88
+ const files = process.argv.slice(2)
89
+ const targets = files.length ? files : recentV4(3)
90
+ if (targets.length === 0) { console.error(' ❌ 没有可校验的会话文件(给个路径,或确认 DSH_HOME/sessions 存在)'); process.exit(2) }
91
+
92
+ let failed = 0
93
+ for (const file of targets) {
94
+ const r = checkOne(file)
95
+ if (r.ok) {
96
+ console.log(`✅ ${file}`)
97
+ console.log(` ${r.detail}`)
98
+ } else {
99
+ failed += 1
100
+ console.log(`❌ ${file}`)
101
+ console.log(` ${r.why}`)
102
+ }
103
+ }
104
+ console.log(`\n${failed === 0 ? '✅' : '❌'} session-load-check:${targets.length - failed} 通过 / ${failed} 失败`)
105
+ process.exit(failed > 0 ? 1 : 0)
@@ -198,7 +198,7 @@ deliver_faces() {
198
198
  # ⚠️ `install-argent.mjs` 同族(2026-10-10 实测):克隆里它是**旧版**(仓库 ff4069d6 vs 克隆
199
199
  # 618f4bbd)⇒ 克隆侧跑 `--agent hr` 报「未知参数: hr」(新 flag 根本不存在)。
200
200
  # 它此前只靠 `argent-upgrade.mjs` 自举更新(滞后窗口 = 客户机装到旧安装器的风险)⇒ 一并进根文件清单。
201
- ROOT_FILES=(VERSION install-argent.mjs)
201
+ ROOT_FILES=(VERSION)
202
202
  deliver_root_files() {
203
203
  local f
204
204
  for f in "${ROOT_FILES[@]}"; do
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # 鑫中航常驻浏览器:启动/停止/状态(保留一个标签页给登录态)
3
+ set -u
4
+ ROOT="${ARGENT_DSH_HOME:-$HOME/.argent-dsh}"
5
+ DATA="${ARGENT_DATA_DIR:-$HOME/.dsh/argent}"
6
+ PY="$DATA/venv/bin/python3"
7
+ [ -x "$PY" ] || PY="$(command -v python3)"
8
+ LOG="$DATA/xzh-browser.log"
9
+ PORT="${XZH_CDP_PORT:-9223}"
10
+ case "${1:-status}" in
11
+ start)
12
+ if curl -s -m 2 "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; then
13
+ echo "already running: CDP $PORT"; exit 0
14
+ fi
15
+ mkdir -p "$DATA"
16
+ nohup "$PY" "$ROOT/scripts/xzh_browser.py" >>"$LOG" 2>&1 &
17
+ echo $! > "$DATA/xzh-browser.pid"
18
+ for i in $(seq 1 20); do
19
+ if curl -s -m 1 "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; then
20
+ echo "started: CDP $PORT"; exit 0
21
+ fi
22
+ sleep 1
23
+ done
24
+ echo "start failed; see $LOG"; tail -20 "$LOG"; exit 1
25
+ ;;
26
+ stop)
27
+ PID_FILE="$DATA/xzh-browser.pid"
28
+ if [ -f "$PID_FILE" ]; then kill "$(cat "$PID_FILE")" 2>/dev/null || true; rm -f "$PID_FILE"; fi
29
+ # 只杀监听该调试端口的 Chrome(避免误杀用户其它窗口)
30
+ PIDS=$(lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true)
31
+ [ -n "$PIDS" ] && kill $PIDS 2>/dev/null || true
32
+ echo stopped
33
+ ;;
34
+ status)
35
+ if curl -s -m 2 "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; then echo "running CDP $PORT"; else echo "not running"; exit 1; fi
36
+ ;;
37
+ esac
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """鑫中航常驻浏览器:保留一个标签页/登录态;invoice_stageb 通过 CDP 复用。
3
+
4
+ 用法:
5
+ python3 scripts/xzh_browser.py # 前台常驻(调试)
6
+ nohup ... > ~/.dsh/argent/xzh-browser.log 2>&1 &
7
+ 启动后监听 127.0.0.1:9223;invoice_stageb 会自动 connect_over_cdp。
8
+ """
9
+ import json
10
+ import os
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
16
+ from argent_datadir import resolve_data_dir # noqa: E402
17
+
18
+ DATA = Path(resolve_data_dir()['dir'])
19
+ CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
20
+ PROFILE = str(DATA / "chrome-profile-xinhang")
21
+ PORT = int(os.environ.get("XZH_CDP_PORT", "9223"))
22
+
23
+
24
+ def main():
25
+ from playwright.sync_api import sync_playwright
26
+ mapping = json.load(open(DATA / "invoice-mapping.json"))
27
+ url = mapping["xinzhonghang"]["url"]
28
+ home = url.replace("/Login/VueLoginView", "") + "/home/EleIndex?lang=cn"
29
+ p = sync_playwright().start()
30
+ ctx = p.chromium.launch_persistent_context(
31
+ user_data_dir=PROFILE,
32
+ executable_path=CHROME,
33
+ headless=False,
34
+ viewport={"width": 1440, "height": 900},
35
+ accept_downloads=True,
36
+ args=[f"--remote-debugging-port={PORT}"],
37
+ )
38
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
39
+ try:
40
+ page.goto(home, timeout=20000, wait_until="domcontentloaded")
41
+ except Exception as exc:
42
+ print(f"⚠️ 打开首页失败(保留已开标签): {exc}", flush=True)
43
+ print(f"✅ 鑫中航常驻浏览器已启动:CDP http://127.0.0.1:{PORT}|profile={PROFILE}", flush=True)
44
+ while True:
45
+ time.sleep(30)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -75,3 +75,33 @@
75
75
  - **口径变化(有意)**:新文案含「需运营…」⇒ 分类从 `needs_agent`(唤醒角色去猜)
76
76
  变为 `needs_human`(落卡等人、运营改商品)。⛔ 这是本事故要的结果,不是副作用。
77
77
 
78
+ ## 卡票先问 hub 真相;⛔ 别单步硬跑(2026-10-16 现场 79301358)
79
+
80
+ **症状**:lip 表里一张票 `FULL状态=失败`(如「步骤7 上传箱唛失败」),但门户侧其实**早就全做完了**
81
+ (运营手工做完 / 页面早已下载完标签),反复重跑仍旧失败。
82
+
83
+ **根因(两条,都要记住)**:
84
+ 1. **单步模式会绕过前置判定**:`orchestrator.py --mode step --step 7 …` 直接进步骤,**跳过了**
85
+ `step1_prepare` 里那句「**先问 hub 真相,再下结论**」——那句会按 hub-v2 三项
86
+ (预约已取消 ∧ 产品标签已下载 ∧ 箱唛已下载)判定「**运营手动完成**」并结票。
87
+ ⇒ 用单步"复现失败"会把一张**其实已完成**的票重新打成失败。
88
+ 2. **「Declara al menos」不一定是残留**:旧逻辑把点击 `Generar etiquetas` 之前就存在的该提示
89
+ 一律当"残留文案"⇒ 干等 30s 后以"平台侧异常"收口(**冤枉平台**,运营也拿不到可执行信息)。
90
+ KB-136 起改为:先读 checkbox 勾选进度 + **Andes 勾选态**(`#2/#3`),声明确实没做完时
91
+ 重勾重试、仍不行则以 `[declaration_incomplete] 已声明 N/M` 精确收口。
92
+ ⚠️ 注意 `checkbox_total` **包含我们故意跳过的 `#1`(bultos)**,所以"勾选数 < 总数"本身
93
+ **不是**声明未完成的证据(正常态就是 2/4)。
94
+
95
+ **正确处置顺序(⛔ 不要一上来就 `--mode step`)**:
96
+ ```bash
97
+ # ① 先问 hub 真相(只读探针;必须带 --store-name,否则会解析成别的店)
98
+ python3 scripts/orchestrator.py --mode hub-status --record-id <rec> --shipment-id <货件号> \
99
+ --store-name "<店铺名>"
100
+ # ② 若 operationally_complete=true ⇒ 认定「运营手动完成」并**置位**(2026-10-16 起同时写 FULL状态=已完成)
101
+ python3 scripts/orchestrator.py --mode hub-status --hub-write-note \
102
+ --record-id <rec> --shipment-id <货件号> --store-name "<店铺名>"
103
+ # ③ 门户/数据侧确实没做完 ⇒ 才进单步或定点续跑(有失败收口时,步骤7 现在会先自己问一次 hub)
104
+ ```
105
+
106
+ **卡住不动的票**(触发器不再认领):`flow_stale.py --flow <流程> [--requeue]` 回收;
107
+ **流程跑得动但不接票**:`flow_trigger.sh --flow <流程>` 手动一轮(与定时器同一路径)。
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: ml-full-appointment
3
+ description: 美客多 FULL 抢预约对话与 ERP 任务创建(BOT/gateway 只做桥;真正预约由 ERP worker 执行)。处理 SKU/货件号、字段缺口、任意日期范围、二次确认、停止与不跑步骤5-8。
4
+ ---
5
+
6
+ # 美客多 FULL 抢预约(ml-full-appointment)
7
+
8
+ ## 定位
9
+
10
+ 飞书 @美客多 FULL BOT 只是**对话/确认桥梁**:
11
+
12
+ - BOT/gateway 负责:识别意图、补齐字段、二次确认、创建 ERP 预约任务、反馈“已受理/已停止”;
13
+ - **BOT/gateway 不轮询、不操作浏览器**;
14
+ - 真正抢预约由 Huangwei 的 **ERP appointment worker** 执行;
15
+ - 成功后通知发起人;**只到预约成功,不进入 FULL 步骤 5–8**。
16
+
17
+ ## 触发意图
18
+
19
+ 用户 @BOT 说类似:
20
+
21
+ - “抢预约 SKU HW-MX-168,店铺 墨西哥-1店-子账号,10/20 到 10/25”
22
+ - “抢货件 79922453 的预约,今天起 2 天内”
23
+ - “帮我抢 FULL 预约,30 天内的”
24
+
25
+ ## 必须收集的字段
26
+
27
+ - **店铺**(必填;SKU 可能跨店)
28
+ - **SKU 或货件号**(二选一;货件号优先续传)
29
+ - **日期窗口**:用户本次给的具体要求,两种模式:
30
+ - 用户说 `最近5天/最近5个可用日` ⇒ `dateMode=available_days`, `availableDays=5`(跳过 disabled,取最近 5 个可选日,不是日历固定 5 天);
31
+ - 用户给绝对范围 `2026-10-20 ~ 2026-10-25` ⇒ `dateMode=range`, `dateFrom/dateTo`;
32
+ - 若只给 SKU:**不读取 lip 表**,必须通过对话补齐 FULL 建单所需字段:店铺、SKU、数量 `quantity`、箱数 `boxCount`(产品名可选);缺什么问什么;
33
+ - 若给货件号:校验该货件存在,店铺/记录一致,当前进度允许预约;
34
+ - 所有参数以任务形式传给 ERP,worker 只使用任务参数;
35
+ - 轮询间隔默认 60 秒(后台参数,不需用户每次提供)。
36
+
37
+ 不要把 7/14 天写成固定变量;**每个任务保存用户本次的 dateFrom/dateTo**。
38
+
39
+ ## 二次确认(必须)
40
+
41
+ 创建任务前回显一段摘要,例如:
42
+
43
+ > 确认信息:店铺 墨西哥-1店-子账号;SKU HW-MX-168;预约日期 2026-10-20 ~ 2026-10-25;只抢预约,不继续箱唛/发票。回复「开始」才执行。
44
+
45
+ 用户明确说「开始」/「确认」后才创建 ERP 任务;未确认不得创建。
46
+
47
+ ## 创建 ERP 任务
48
+
49
+ BOT/gateway 通过确定性 CLI 创建任务(不在会话里轮询):
50
+
51
+ ```bash
52
+ node ~/.argent-dsh/scripts/appointment-create.mjs --json '<任务 JSON>' [--chat-id <当前群> --open-id <发起人open_id>]
53
+ ```
54
+
55
+ 返回 `{ ok, id, status }` 后,BOT 回复“已创建任务 #id”。
56
+
57
+ 尽量传 `--chat-id`/`--open-id`,worker 完成/停止时服务端会用 FULL BOT 通知发起人并附逐日结果。
58
+
59
+ CLI 内部读取 `~/.dsh/argent/config.json` 的 `apiKey`(或 `WHYSHU_API_KEY`),调用:
60
+
61
+ ```
62
+ POST /api/argent/v1/tenants/<tenantAppId>/appointments
63
+ {
64
+ "store": "...",
65
+ "sku": "...", // 或 shipmentNo
66
+ "shipmentNo": "...",
67
+ "quantity": 100, // SKU-only 必填
68
+ "boxCount": 10, // SKU-only 必填
69
+ "productName": "", // 可选
70
+ "dateMode": "available_days",
71
+ "availableDays": 5,
72
+ // range 模式才填 dateFrom/dateTo
73
+ "windowText": "10/20-10/25",
74
+ "pollIntervalSec": 60
75
+ }
76
+ ```
77
+
78
+ 返回任务 `queued` 后,BOT 回复:
79
+
80
+ > 已创建抢预约任务 #<id>,worker 会按 60 秒一轮执行;回复「停止」可终止。
81
+
82
+ ## 用户停止
83
+
84
+ 用户说「停止」时运行:
85
+
86
+ ```bash
87
+ node ~/.argent-dsh/scripts/appointment-stop.mjs <AppointmentTask id>
88
+ ```
89
+
90
+ 只置停止请求,worker 收尾;BOT 不轮询任务状态。
91
+
92
+ ## 禁止
93
+
94
+ - ⛔ 不在 BOT/gateway 会话里执行浏览器操作或循环轮询;
95
+ - ⛔ 不跑 FULL 步骤 5–8(箱唛/标签/发票/完成态);
96
+ - ⛔ 不把 7/14 天硬编码;不替用户改日期范围;
97
+ - ⛔ 未二次确认不创建任务;
98
+ - ⛔ 同一店铺预约仍走 ERP/编排器的同店步骤4互斥锁,避免与 FULL 任务串预约。
@@ -1 +1 @@
1
- export const VERSION = "0.7.2"
1
+ export const VERSION = "0.7.4"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent-engine",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Argent 引擎(L1):流程/台账/重试/守卫/审计/浏览器自动化 —— 逐字节来自 argent-dsh 仓库",
5
5
  "type": "module",
6
6
  "engines": {