@whyshu/argent-engine 0.7.3 → 0.7.5

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 (45) hide show
  1. package/MANIFEST.json +39 -28
  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 +33 -39
  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/dsh-upgrade.mjs +18 -5
  15. package/engine/scripts/flow-requirements.json +5 -7
  16. package/engine/scripts/flow-timers.sh +1 -1
  17. package/engine/scripts/flow-trigger.sh +1 -1
  18. package/engine/scripts/gen-preset-rows.mjs +20 -0
  19. package/engine/scripts/invoice_stageb.py +20 -1
  20. package/engine/scripts/lib/agent-install.mjs +10 -1
  21. package/engine/scripts/lib/daily-report-core.mjs +124 -16
  22. package/engine/scripts/lib/dsh-versions.mjs +17 -1
  23. package/engine/scripts/lib/ml-ads-core.mjs +204 -0
  24. package/engine/scripts/lib/plugin-packages.mjs +1 -0
  25. package/engine/scripts/ml-ads.mjs +181 -0
  26. package/engine/scripts/ml-oauth-setup.mjs +1 -1
  27. package/engine/scripts/p2-report-run.mjs +102 -0
  28. package/engine/scripts/patches/dsh-session--append-message-shape.patch +34 -9
  29. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +44 -0
  30. package/engine/scripts/patches/verify-hardening.mjs +4 -1
  31. package/engine/scripts/patches/verify-turn-outside-replace.mjs +64 -0
  32. package/engine/scripts/poll-fulfillment.sh +1 -1
  33. package/engine/scripts/profile-doctor.mjs +1 -1
  34. package/engine/scripts/python-runtime.sh +16 -4
  35. package/engine/scripts/release-argent.sh +8 -7
  36. package/engine/scripts/session-load-check.mjs +105 -0
  37. package/engine/scripts/sync-scripts.sh +1 -1
  38. package/engine/scripts/xzh-browser.sh +37 -0
  39. package/engine/scripts/xzh_browser.py +49 -0
  40. package/engine/skills/ml-full-appointment/SKILL.md +98 -0
  41. package/engine/version.mjs +1 -1
  42. package/package.json +1 -1
  43. package/engine/scripts/drill-clean-machine.sh +0 -315
  44. package/engine/scripts/install-internal-role.sh +0 -190
  45. package/engine/scripts/install-one-shot.mjs +0 -665
@@ -1,665 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * install-one-shot —— **干净机器一条命令**(D1 的编排入口;2026-10-05)
4
- *
5
- * ## 为什么是"新增一层薄编排"而不是塞进 `install-argent.mjs`
6
- * 判据文档 `docs/Argent-1.0-退出判据与版本规则.md` §6 Q6 已定 **B(新增一层编排入口)**,
7
- * 本文件就是该结论的落地,理由三条:
8
- * ① **职责**:`install-argent.mjs`(1975 行)只承担"装配"一件事;许可(授权判定)与技能归类
9
- * 判据都在**内核侧**,把它们内联进安装器会让安装器承担内核判断(与《宿主适配契约》§1 的
10
- * 分层边界冲突);
11
- * ② **三段各有唯一实现**:装配 = `install-argent.mjs`|技能同步 = `scripts/argent-upgrade.mjs`
12
- * 导出的 `classifySkills()`/`syncSkills()`(判据真源 `scripts/check-skill-classification.mjs#classify()`)|
13
- * 许可 = `scripts/install-license.mjs`。编排只做「串联 + 逐步给结论」,
14
- * ⛔ **绝不复制任何一段的判断**(本文件里没有第二份技能名单、第二份验签、第二份 patch 逻辑);
15
- * ③ **可单独重跑**:每段本身就是一条独立命令 ⇒ `--only <段>` 天然可重跑,不必"从头再来一遍"。
16
- *
17
- * ## 契约(一条命令,5 段)
18
- * ```
19
- * node scripts/install-one-shot.mjs [--profile <p>] [--dsh-home <h>] [--tree <dir>]
20
- * [--from-packs <包目录/tgz/URL,…>]
21
- * [--license-file <f>] [--license-secret-file <f>]
22
- * [--only <段,…>] [--apply] [--json] [--report <f>]
23
- * [--timeout <秒>] [--no-provider] [--no-venv]
24
- * ```
25
- * | 段 | 做什么 | 调用谁(唯一实现) | 判据(结论行) |
26
- * |---|---|---|---|
27
- * | `precheck` | 宿主/树前置检查 | 本文件(只做**存在性/版本**探测,不做业务判断)+ `core/lib/argent-datadir.mjs#resolveDataDir` | 0 项失败 |
28
- * | `install` | profile 装配 | `node <树>/install-argent.mjs …`(离线面 `--from-packs` / 在线面) | 退出码 0 ∧ 装机清单或 profile patch 在位 |
29
- * | `skills` | 技能同步(**只发外发集**) | `scripts/argent-upgrade.mjs` 的 `classifySkills()`+`syncSkills()` | 外发集非空 ∧ 源缺件 0 |
30
- * | `license` | 许可安装(+可选**实例令牌**,机器自带凭证) | `node <树>/scripts/install-license.mjs --file … [--instance-token-file …]` | 退出码 0;**未给 `--license-file` ⇒ `skipped`,不算失败**(结论里标「未装许可」) |
31
- * | `verify` | 收尾自检 | `install-argent.mjs --list-capabilities`/`plugin-bootcheck.mjs`/`check-role-mapping.mjs`/`core/cli/argent.mjs license status` | 逐条 `ok/fail/n-a`(`n-a` = 前置未满足,**点名前置 + 给单跑命令**,不冒充通过) |
32
- *
33
- * ## 四条语义(判据原话:幂等 / 默认 dry-run / 每步可单独重跑 / 失败即停且打印"下一步")
34
- * ① **默认 dry-run**:不带 `--apply` 只打印**将要执行的每一步的确切命令**与判据,**零写入**;
35
- * ② **幂等**:每段都调既有幂等实现(`--from-packs` 逐文件 sha 跳过;`syncSkills` 内容一致即跳过;
36
- * `install-license` 与现役逐字节相同 ⇒ `unchanged:true` 不重写)⇒ 同一条命令重跑结论一致、不产生 churn;
37
- * ③ **每步可单独重跑**:`--only <段>`;失败时打印的"下一步"就是该段的单跑命令;
38
- * ④ **失败即停**:任一段 `fail` ⇒ 立刻停,后续段标 `skipped`,并打印「下一步:<确切命令>」。
39
- *
40
- * ## 工具解析("先目标树、后自身树")
41
- * 每个被调用的脚本按 `<--tree>/<相对路径>` → 本脚本所在树的同名文件 依次解析
42
- * (`install-argent.mjs` 对 `argent-capabilities.mjs` 用的是同一口径):
43
- * 干净机器上先跑一次 `--from-packs` 把目标树铺好,之后所有段都用**目标树自己那份**实现。
44
- *
45
- * ## 判据边界(诚实声明;⛔ 不为了让判据变绿而伪造自动化)
46
- * · **许可文件必须人工带入**:服务端签发(`issue-license-file.mjs`,需账号/JWT)**不在本命令范围**;
47
- * 没带 `--license-file` ⇒ 本命令只把机器带到"**只差许可文件**",且**明确**标"未装许可"。
48
- * · **账号登录(OAuth 一键登录)**永远是人工步骤:本命令只做本机文件面,不代点授权。
49
- * · **模型凭证真探针**(`check-model-auth.py --online`)要联网 + 真 Key ⇒ 离线演练用安装器既有的
50
- * `ARGENT_SKIP_MODEL_PROBE=1` 逃逸阀(安装器自己会打警告,不静默)。
51
- * · 在线装配面的第 7 步(插件装入 profile)需要包管理器;本命令**不替它做决定**,只把安装器的输出
52
- * 原样透传(含 `pnpm` 不可用时的回退与警告)。
53
- *
54
- * 退出码:0 各段均 ok(`skipped`/`n-a` 不算失败,但会在结论里点名计数)|1 有段 `fail`|2 用法/环境错误。
55
- */
56
- import { spawnSync } from 'node:child_process'
57
- import { createRequire } from 'node:module'
58
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
59
- import { homedir } from 'node:os'
60
- import { dirname, isAbsolute, join, resolve } from 'node:path'
61
- import { fileURLToPath, pathToFileURL } from 'node:url'
62
- import { loadRegistryDoc } from './lib/registry.mjs' // 注册表唯一入口(⛔ 不直读 base)
63
-
64
- const require_ = createRequire(import.meta.url)
65
- const HERE = dirname(fileURLToPath(import.meta.url))
66
- /** 本脚本所在树(仓库里 = 仓库根;装机后 = 安装克隆根)。 */
67
- const SELF_TREE = resolve(HERE, '..')
68
-
69
- // ── 参数 ─────────────────────────────────────────────────────────────
70
- const argv = process.argv.slice(2)
71
- const argOf = (n, d = null) => { const i = argv.indexOf(n); return i > -1 ? argv[i + 1] ?? d : null }
72
- const has = (n) => argv.includes(n)
73
- const STEPS = ['precheck', 'install', 'skills', 'license', 'verify']
74
-
75
- if (has('--help') || has('-h')) {
76
- console.log(`Argent 干净机器一条命令(薄编排:只串联既有能力,不复制判断)
77
-
78
- 用法:
79
- node scripts/install-one-shot.mjs [选项] # 默认 dry-run:只打印将执行的命令与判据
80
- node scripts/install-one-shot.mjs --apply # 真执行
81
-
82
- 选项:
83
- --profile <p> 目标 DSH profile(默认 web)
84
- --dsh-home <h> 目标 DSH_HOME(默认 $DSH_HOME 或 ~/.dsh)
85
- --tree <dir> Argent 树(默认 $ARGENT_DISH_HOME 或 ~/.argent-dsh,与安装器同口径)
86
- --from-packs <s,…> 离线包(目录/tgz/http(s) URL,逗号分隔)⇒ 装配走安装器离线面(不需要 git/网络)
87
- --license-file <f> 许可文件(缺 ⇒ license 段 skipped,结论标「未装许可」,**不算失败**)
88
- --license-secret-file <f> 许可密钥文件(可选;传给 install-license.mjs)
89
- --instance-token-file <f> 实例令牌文件(可选;**机器自带凭证**,与许可一并装 ⇒ 不再依赖 30 天账号令牌)
90
- --only <段,…> 只跑这些段(${STEPS.join(' / ')})
91
- --apply 真执行(缺省 dry-run)
92
- --no-provider 装配时跳过模型 provider 配置(透传 --skip-provider)
93
- --no-venv 装配时跳过 Python 运行时(透传 --skip-venv;离线/演练用)
94
- --timeout <秒> 单段超时(默认 1800)
95
- --json 结论以 JSON 打印到 stdout(人话输出照常)
96
- --report <f> 把结论 JSON 写到文件
97
- --help 本帮助
98
-
99
- 退出码:0 各段均 ok | 1 有段 fail | 2 用法/环境错误
100
- `)
101
- process.exit(0)
102
- }
103
-
104
- const profile = argOf('--profile') || process.env.DSH_PROFILE || 'web'
105
- const dshHome = resolve(argOf('--dsh-home') || process.env.DSH_HOME || join(homedir(), '.dsh'))
106
- const treeArg = argOf('--tree') || process.env.ARGENT_DISH_HOME || process.env.ARGENT_HOME || ''
107
- // ⚠ 默认值与安装器同口径(~/.argent-dsh)——**绝不**默认指到"本脚本所在树":
108
- // 在线面第 2 步会对 <树> 做 `git pull`,若默认指到开发仓库,跑一次安装器就会 pull 开发树。
109
- const tree = resolve(treeArg || join(homedir(), '.argent-dsh'))
110
- const fromPacks = argOf('--from-packs') || ''
111
- const licenseFile = argOf('--license-file') ? resolve(argOf('--license-file')) : ''
112
- const licenseSecret = argOf('--license-secret-file') ? resolve(argOf('--license-secret-file')) : ''
113
- const instanceTokenFile = argOf('--instance-token-file') ? resolve(argOf('--instance-token-file')) : ''
114
- const apply = has('--apply')
115
- const asJson = has('--json')
116
- const reportPath = argOf('--report') ? resolve(argOf('--report')) : ''
117
- const timeoutS = Number(argOf('--timeout') || 1800)
118
- const noProvider = has('--no-provider')
119
- const noVenv = has('--no-venv')
120
- const onlyRaw = argOf('--only')
121
- const only = onlyRaw ? String(onlyRaw).split(',').map((x) => x.trim()).filter(Boolean) : STEPS.slice()
122
- for (const s of only) {
123
- if (!STEPS.includes(s)) { console.error(`✗ 未知段「${s}」(可选:${STEPS.join(' / ')})`); process.exit(2) }
124
- }
125
- if (!Number.isFinite(timeoutS) || timeoutS <= 0) { console.error('✗ --timeout 必须是正秒数'); process.exit(2) }
126
- if (licenseSecret && !licenseFile) { console.error('✗ --license-secret-file 必须与 --license-file 一起给'); process.exit(2) }
127
- if (instanceTokenFile && !licenseFile) { console.error('✗ --instance-token-file 必须与 --license-file 一起给(两者走同一条离线下发链路、且要互相核对归属/设备码)'); process.exit(2) }
128
-
129
- /** 工具解析:目标树优先,回落到本脚本所在树(两处都是同一条相对路径)。 */
130
- const tool = (rel) => {
131
- const p = join(tree, rel)
132
- return existsSync(p) ? p : join(SELF_TREE, rel)
133
- }
134
- /** 该工具是否来自**目标树**(用于结论里说清"用的是哪一份实现")。 */
135
- const toolSource = (rel) => (existsSync(join(tree, rel)) ? 'tree' : 'self')
136
-
137
- const installer = tool('install-argent.mjs')
138
- const licenseInstaller = tool(join('scripts', 'install-license.mjs'))
139
- const upgradeScript = tool(join('scripts', 'argent-upgrade.mjs'))
140
- const classifierScript = tool(join('scripts', 'check-skill-classification.mjs'))
141
- const bootcheckScript = tool(join('scripts', 'plugin-bootcheck.mjs'))
142
- const roleMappingScript = tool(join('scripts', 'check-role-mapping.mjs'))
143
- const coreCli = tool(join('core', 'cli', 'argent.mjs'))
144
-
145
- const dataDir = join(dshHome, 'argent')
146
- const skillsDst = join(dshHome, 'skills')
147
- const profileDir = join(dshHome, 'profiles', profile)
148
- const installJson = join(dataDir, 'install.json')
149
-
150
- /** 子进程环境:**只钉 DSH_HOME**,其余继承(ARGENT_DATA_DIR 若在环境里按内核既有优先级链生效)。 */
151
- const childEnv = { ...process.env, DSH_HOME: dshHome }
152
-
153
- // ── 输出 ─────────────────────────────────────────────────────────────
154
- const useColor = process.stdout.isTTY && !process.env.NO_COLOR
155
- const c = (n, s) => (useColor ? `\x1b[${n}m${s}\x1b[0m` : s)
156
- const say = (...m) => console.log(...m)
157
- const head = (id, title) => say(`\n${c(1, `[${STEPS.indexOf(id) + 1}/${STEPS.length}] ${id}`)} ${title}`)
158
- const okLine = (t) => say(` ${c(32, '✅')} ${t}`)
159
- const warnLine = (t) => say(` ${c(33, '⚠️')} ${t}`)
160
- const badLine = (t) => say(` ${c(31, '✗')} ${t}`)
161
- const infoLine = (t) => say(` ${c(90, '·')} ${t}`)
162
- const cmdLine = (t) => say(` ${c(36, '▶ 命令')} ${t}`)
163
-
164
- const startedAt = Date.now()
165
- const results = []
166
- const rec = (id, status, summary, extra = {}) => {
167
- const r = { id, status, summary, checks: extra.checks || [], commands: extra.commands || [], error: extra.error || '' }
168
- results.push(r)
169
- return r
170
- }
171
-
172
- function runCmd(cmd, argsArr, { timeoutMs = timeoutS * 1000, env = childEnv } = {}) {
173
- const shown = [cmd, ...argsArr].map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ')
174
- const r = spawnSync(cmd, argsArr, { env, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 })
175
- const out = `${r.stdout || ''}${r.stderr || ''}`
176
- const timedOut = r.status === null && !r.error ? true : /ETIMEDOUT/.test(String((r.error && r.error.code) || ''))
177
- return { shown, code: r.status === null ? (timedOut ? 124 : 127) : r.status, out, signal: r.signal || '', err: String((r.error && r.error.message) || '') }
178
- }
179
-
180
- /** "下一步":失败即停时点名该段的**单跑命令**。 */
181
- function stepNext(id, hint) {
182
- say(`\n ${c(1, '下一步')}:`)
183
- say(` ${c(36, singleRunCmd(id))}`)
184
- say(` ↳ ${hint}`)
185
- }
186
- function singleRunCmd(id, { withApply = true } = {}) {
187
- const parts = [`node ${join(SELF_TREE, 'scripts', 'install-one-shot.mjs')}`, `--only ${id}`]
188
- if (withApply) parts.push('--apply')
189
- parts.push(`--profile ${profile}`, `--dsh-home ${dshHome}`)
190
- if (treeArg) parts.push(`--tree ${tree}`)
191
- if (id === 'install' && fromPacks) parts.push(`--from-packs ${fromPacks}`)
192
- if (id === 'license' && licenseFile) parts.push(`--license-file ${licenseFile}`)
193
- if (noProvider) parts.push('--no-provider')
194
- if (noVenv) parts.push('--no-venv')
195
- return parts.join(' ')
196
- }
197
-
198
- /** 数据目录:**唯一判据在内核里** ⇒ 只调 `core/lib/argent-datadir.mjs#resolveDataDir()`,绝不自己拼第二份。 */
199
- async function effectiveDataDir() {
200
- try {
201
- const p = tool(join('core', 'lib', 'argent-datadir.mjs'))
202
- if (!existsSync(p)) return null
203
- const mod = await import(pathToFileURL(p).href)
204
- return typeof mod.resolveDataDir === 'function' ? mod.resolveDataDir() : null
205
- } catch { return null }
206
- }
207
-
208
- // ── ① precheck ───────────────────────────────────────────────────────
209
- async function stepPrecheck() {
210
- head('precheck', '宿主/树前置检查(只做存在性/版本探测,不做业务判断)')
211
- const checks = []
212
- const add = (ok, text, extra = {}) => { checks.push({ ok, text, ...extra }); (ok ? okLine : warnLine)(text) }
213
- const which = (cmd) => spawnSync('which', [cmd], { encoding: 'utf8' }).status === 0
214
-
215
- const nodeMajor = Number(process.versions.node.split('.')[0])
216
- if (nodeMajor < 20) { badLine(`Node.js ${process.version} 过低(需要 ≥ 20)`); checks.push({ ok: false, text: `Node ${process.version} < 20` }) }
217
- else add(true, `Node.js ${process.version}(≥ 20)`)
218
-
219
- const hasGit = which('git')
220
- add(hasGit, hasGit ? 'git 在位' : '未找到 git(离线装配面不需要;在线装配面需要)')
221
- if (which('dsh')) {
222
- const v = spawnSync('dsh', ['--version'], { encoding: 'utf8', env: childEnv })
223
- add(true, `dsh 在位${v.status === 0 ? `(${String(v.stdout || '').trim().split('\n')[0]})` : '(版本读不出)'}`)
224
- } else {
225
- warnLine('未找到宿主(dsh)—— 安装器会**拒绝安装并给指引**(宿主由客户自带;⛔ 不代装):'
226
- + '按 dsh **官方**运行方式准备窗口内版本的宿主后重跑(首选 `npx @deepseek-ai/dsh@<窗口内版本> web`;'
227
- + '跑过一次后它的 npx 缓存会被识别为宿主);⛔ 旧的 --install-host 代装开关已废弃,别再照抄')
228
- checks.push({ ok: true, warn: true, text: 'dsh 不在 PATH' })
229
- }
230
- const pm = ['pnpm', 'npm', 'corepack'].filter(which)
231
- infoLine(`包管理器:${pm.length ? pm.join(' / ') : '(都没有)'}——插件装入 profile 由安装器自行决定,本命令不替它做决定`)
232
- checks.push({ ok: true, info: true, text: `包管理器 ${pm.join('/') || '无'}` })
233
-
234
- const dd = await effectiveDataDir()
235
- infoLine(`目标:profile=${profile}|DSH_HOME=${dshHome}|树=${tree}(工具来源:installer=${toolSource('install-argent.mjs')})`)
236
- infoLine(`数据目录:${dd ? `${dd.dir}(判据来源 ${dd.source}${dd.isolated ? ';隔离实例' : ''})` : `${dataDir}(内核 datadir 模块读不到,按默认拼)`}`)
237
-
238
- const packs = fromPacks.split(',').map((x) => x.trim()).filter(Boolean)
239
- if (packs.length) {
240
- let bad = 0
241
- for (const spec of packs) {
242
- if (/^https?:\/\//i.test(spec)) { infoLine(`离线包(URL,装配时下载):${spec}`); continue }
243
- const abs = isAbsolute(spec) ? spec : resolve(spec)
244
- if (!existsSync(abs)) { bad++; badLine(`离线包不存在:${abs}`) }
245
- else okLine(`离线包:${abs}${statSync(abs).isDirectory() ? '(目录)' : ''}`)
246
- }
247
- if (bad) checks.push({ ok: false, text: `${bad} 个离线包路径不存在` })
248
- infoLine('装配面:**离线**(安装器 --from-packs 分支:不需要 git、不需要联网、不经过包管理器)')
249
- if (packs.length > 1) {
250
- // 2026-10-09:这里原先写着"多包同装会被 planPackInstall 拦下(缺口㉔)⇒ 建议一次装一个"——
251
- // 那是**过期结论**:拦下它的是"计划阶段没豁免注册表投影"这个 bug(同批复核里修掉,判据与
252
- // `installPackFiles` 的 ⑤ 共用同一个 `isRegistryProjection()`)。现在多包一起装是**推荐**路径:
253
- // 同批发布的包本就该一起装(跨包共用引擎文件必须同版),一起装还能让注册表投影落成并集。
254
- infoLine(`给了 ${packs.length} 个离线包:**一起装**(同批发布的包应当同装 —— 跨包共用引擎文件必须同版;`
255
- + '`scripts/flow-requirements.json` 是逐能力过滤的投影,落到目标机时按"保留/合并"处理 ⇒ 结果是各投影的并集)')
256
- checks.push({ ok: true, text: `多包一起装(${packs.length} 个包同批)` })
257
- }
258
- } else {
259
- const treeOk = existsSync(join(tree, '.git')) || existsSync(installer)
260
- add(treeOk, treeOk ? `在线装配面:树已在位(${tree})` : `树不在位(${tree})——在线面会 git clone`)
261
- infoLine('装配面:**在线**(git clone/更新 → dsh → preset → provider → 插件装入 profile)')
262
- }
263
-
264
- if (!existsSync(installer)) { badLine(`找不到安装器:${installer}(--tree 指错了?)`); checks.push({ ok: false, text: `缺 ${installer}` }) }
265
- else infoLine(`安装器:${installer}`)
266
-
267
- // 目标可写:**不创建任何目录**(dry-run 必须零写入)——只探"最近的已存在祖先"
268
- let writable = true
269
- let probe = dshHome
270
- while (!existsSync(probe) && dirname(probe) !== probe) probe = dirname(probe)
271
- try {
272
- const st = statSync(probe)
273
- writable = (st.mode & 0o200) !== 0 && spawnSync('test', ['-w', probe]).status === 0
274
- } catch { writable = false }
275
- add(writable, writable ? `目标可写(最近已存在祖先 ${probe})` : `目标不可写(最近已存在祖先 ${probe})`)
276
-
277
- const failed = checks.filter((x) => x.ok === false)
278
- const status = failed.length ? 'fail' : 'ok'
279
- rec('precheck', status, failed.length ? `${failed.length} 项前置失败` : '前置检查通过', {
280
- checks,
281
- error: failed.map((f) => f.text).join(';'),
282
- })
283
- if (failed.length) stepNext('precheck', '修掉上面点名的前置项后重跑本段')
284
- return status === 'ok'
285
- }
286
-
287
- // ── ② install ────────────────────────────────────────────────────────
288
- function stepInstall() {
289
- head('install', fromPacks ? 'profile 装配 —— 离线面(--from-packs:不需要 git/网络/包管理器)' : 'profile 装配 —— 在线面(现有 install 流程)')
290
- const args = [installer, '--profile', profile, '--dsh-home', dshHome, '--home', tree]
291
- if (fromPacks) args.push('--from-packs', fromPacks)
292
- if (noProvider || fromPacks) args.push('--skip-provider')
293
- if (noVenv || fromPacks) args.push('--skip-venv')
294
- args.push('--telemetry-decline')
295
- if (fromPacks && !noProvider) infoLine('离线面自动跳过 provider 配置(--skip-provider)与 Python 运行时(--skip-venv):二者都要联网')
296
- const shown = `node ${args.map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ')}`
297
- cmdLine(shown)
298
- const artifact = fromPacks ? installJson : join(profileDir, 'cordis.patch.yml')
299
- infoLine(`判据:退出码 0 ∧ ${artifact} 在位`)
300
-
301
- if (!apply) {
302
- infoLine('dry-run:未执行(加 --apply 真执行)')
303
- rec('install', 'dry-run', 'dry-run:未执行装配', { commands: [shown] })
304
- return true
305
- }
306
- const r = runCmd(process.execPath, args)
307
- for (const l of r.out.trim().split('\n').slice(-6)) infoLine(l)
308
- const artifactOk = existsSync(artifact)
309
- if (r.code === 0 && artifactOk) {
310
- okLine(`装配完成:${artifact}`)
311
- rec('install', 'ok', fromPacks ? '离线包装配完成(装机清单已写)' : '在线装配完成(profile patch 在位)', { commands: [shown] })
312
- return true
313
- }
314
- badLine(`装配失败:退出码 ${r.code}${r.signal ? `(${r.signal})` : ''}${artifactOk ? '' : `;产物缺失 ${artifact}`}`)
315
- rec('install', 'fail', `退出码 ${r.code}${artifactOk ? '' : ' 且产物缺失'}`, {
316
- commands: [shown],
317
- error: (r.err || r.out.trim().split('\n').slice(-3).join(' / ')),
318
- })
319
- stepNext('install', '看上面安装器的最后几行(它自己会点名缺什么、下一步做什么)')
320
- return false
321
- }
322
-
323
- // ── ③ skills(只发外发集)─────────────────────────────────────────────
324
- /**
325
- * 目标树是「完整树」还是「能力包树(只装了部分能力)」—— 判据(2026-10-09 修)。
326
- *
327
- * ⛔ **不能再看 `verticals/<id>/flows.json` 在不在**:能力包 2026-10-09 起**必须**逐字带上它
328
- * (不带 ⇒ 纯包装机的 `flow_registry.py --json` exit 2,见回归 `tests/install-registry-loadable.test.mjs`),
329
- * 于是旧判据("有 flows.json = 完整树")会把能力包树**误判成完整树** ⇒ 把"未安装的能力"
330
- * 报成"仓库缺陷"(假红)。
331
- *
332
- * 现判据 = **业务线 manifest 声明的能力** 与 **树上注册表声明的能力** 之差:
333
- * `verticals/<id>/manifest.json` 逐字进包(声明该线**全部**能力),而 base 投影按能力过滤
334
- * ⇒ 只装了部分能力时,必有一批 manifest 声明的能力不在树上。读不出(无业务线/无注册表/python 不可用)
335
- * ⇒ 按"能力包树"处理(宁 warn 不 fail:那本来就是**不完整树**的形态)。
336
- */
337
- function isPartialPackTree(root) {
338
- const vRoot = join(root, 'verticals')
339
- const declared = new Set()
340
- try {
341
- for (const d of readdirSync(vRoot, { withFileTypes: true })) {
342
- if (!d.isDirectory()) continue
343
- const mf = join(vRoot, d.name, 'manifest.json')
344
- if (!existsSync(mf)) continue
345
- for (const c of (JSON.parse(readFileSync(mf, 'utf8')).capabilities || [])) {
346
- if (c && c.id) declared.add(String(c.id))
347
- }
348
- }
349
- } catch { return true }
350
- if (!declared.size) return true
351
- try {
352
- const doc = loadRegistryDoc({ root })
353
- const installed = new Set(Object.keys(doc.capabilities || {}).filter((k) => !k.startsWith('_')))
354
- return [...declared].some((c) => !installed.has(c))
355
- } catch { return true }
356
- }
357
-
358
- async function stepSkills() {
359
- head('skills', '技能同步(只发外发集;内部技能不外发)')
360
- if (!existsSync(upgradeScript)) {
361
- badLine(`缺 ${upgradeScript}(技能同步的唯一实现在升级器里)`)
362
- rec('skills', 'fail', '缺 scripts/argent-upgrade.mjs', { error: upgradeScript })
363
- stepNext('skills', '先把树补齐(--from-packs 或 git pull)再重跑本段')
364
- return false
365
- }
366
- infoLine(`判据真源:${classifierScript}(本段调用它的 classify(),⛔ 不写第二份名单)`)
367
- const mod = await import(pathToFileURL(upgradeScript).href)
368
- const cls = mod.classifySkills({ root: tree })
369
- const external = cls.external || []
370
- const internal = cls.internal || []
371
- infoLine(`外发集:${external.length} 项${external.length ? `(${external.slice(0, 8).join('、')}${external.length > 8 ? ' …' : ''})` : ''}`)
372
- infoLine(`内部集:${internal.length} 项(按设计**不外发**)`)
373
- if (!cls.ok) {
374
- warnLine(`技能归类门禁不通过(${cls.errors.length} 条)⇒ 外发集仍严格等于业务线声明,照发;未归类目录一个都不发、不删`)
375
- for (const e of (cls.errors || []).slice(0, 5)) infoLine(`[${e.rule}] ${e.message}`)
376
- }
377
- const clsChecks = (cls.errors || []).map((e) => ({ ok: false, warn: true, text: `[${e.rule}] ${e.message}` }))
378
- if (!external.length) {
379
- // dry-run 里"外发集为空"多半只是因为**装配没跑、目标树还是空的**(真执行时是硬失败)。
380
- // ⇒ dry-run 下如实标"判不出"并点名前置,不假红(也不假绿)。
381
- if (!apply) {
382
- warnLine(`外发集读不出(目标树还没铺:${join(tree, 'verticals')} 不存在或没有 manifest#skills)⇒ dry-run 下判不出,先跑 install 段`)
383
- rec('skills', 'n-a', '外发集读不出(dry-run:目标树未铺,先跑 install 段)', { checks: clsChecks, error: '目标树未铺' })
384
- return true
385
- }
386
- badLine(`外发集为空(读不到 ${join(tree, 'verticals')}/*/manifest.json#skills?)—— 不猜名单、不同步`)
387
- rec('skills', 'fail', '外发集为空', { error: 'verticals/*/manifest.json#skills 读不到', checks: clsChecks })
388
- stepNext('skills', `先跑 node ${classifierScript} 看归类门禁为什么红`)
389
- return false
390
- }
391
- if (!apply) {
392
- infoLine(`dry-run:未执行。将把上面 ${external.length} 项从 ${join(tree, 'skills')} 同步到 ${skillsDst}`)
393
- rec('skills', 'dry-run', `dry-run:外发集 ${external.length} 项未同步`, { checks: clsChecks })
394
- return true
395
- }
396
- const res = mod.syncSkills({ srcRoot: join(tree, 'skills'), dstRoot: skillsDst, external, internal })
397
- for (const name of res.written || []) okLine(`已写入 ${name}`)
398
- for (const name of res.synced || []) if (!(res.written || []).includes(name)) infoLine(`内容已一致(幂等跳过):${name}`)
399
- const hostExtra = [...(res.hostInternal || []), ...(res.hostExtra || [])]
400
- if (hostExtra.length) warnLine(`宿主技能根多出 ${hostExtra.length} 项(不在外发集;**只报告不删**):${hostExtra.slice(0, 8).join('、')}`)
401
-
402
- // 「外发集里有、本机树里没有」的判据**分两种树**(不是放宽,是按树的性质给不同结论):
403
- // · **完整树**(业务线声明的能力**全在**树上):业务线真源齐全 ⇒ 声明了却没有 SKILL.md
404
- // 是**仓库缺陷** ⇒ 判 fail(与 `argent-upgrade.mjs` 同判据,不许在这里变松);
405
- // · **能力包树**(`--from-packs` 铺的:只装了部分能力):本机只装了
406
- // 部分能力 ⇒ 别家能力的技能本来就该缺席 ⇒ 如实 **warn** 并点名"装对应能力包即可补齐"。
407
- const missing = res.missing || []
408
- const packTree = isPartialPackTree(tree)
409
- let failed = false
410
- if (missing.length && !packTree) {
411
- failed = true
412
- badLine(`源缺 SKILL.md(完整树:声明了却没有 = 仓库缺陷):${missing.join('、')}`)
413
- } else if (missing.length) {
414
- warnLine(`外发集里 ${missing.length} 项本机树未提供(能力包树:属**未安装的能力**):${missing.join('、')}`)
415
- infoLine('⇒ 装对应能力包后重跑本段即可补齐;宿主里已有的不会被删')
416
- }
417
- const syncedOk = (res.synced || []).length - missing.length
418
- rec('skills', failed ? 'fail' : 'ok',
419
- failed
420
- ? `外发集 ${res.synced.length} 项,源缺 ${missing.length} 项`
421
- : `技能同步 ${syncedOk}/${external.length} 项已就位(宿主根现有 ${(res.synced || []).length - missing.length} 项;`
422
- + `内部 ${internal.length} 项按设计不同步)`
423
- + (missing.length ? `;${missing.length} 项属未安装的能力` : ''),
424
- { checks: clsChecks, commands: [`node ${upgradeScript} run`], error: failed ? `源缺 ${missing.join('、')}` : '' })
425
- if (failed) stepNext('skills', '补齐源技能目录(skills/<名>/SKILL.md)后重跑本段')
426
- return !failed
427
- }
428
-
429
- // ── ④ license ────────────────────────────────────────────────────────
430
- function stepLicense() {
431
- head('license', '许可安装')
432
- const singleSeg = singleRunCmd('license', { withApply: true })
433
- if (!licenseFile) {
434
- warnLine('未提供 --license-file ⇒ **未装许可**(不是失败:许可需服务端签发后人工带入)')
435
- cmdLine(`(拿到许可文件后单跑本段)${singleSeg.replace('--license-file', '--license-file <服务端签发的许可文件>')}`)
436
- rec('license', 'skipped', '未装许可(未提供 --license-file)', {
437
- commands: [`node ${licenseInstaller} --file <许可文件> --dsh-home ${dshHome}`],
438
- })
439
- return true
440
- }
441
- if (!existsSync(licenseFile)) {
442
- badLine(`许可文件不存在:${licenseFile}`)
443
- rec('license', 'fail', `许可文件不存在:${licenseFile}`, { error: licenseFile })
444
- stepNext('license', '核对 --license-file 路径(服务端签发件)')
445
- return false
446
- }
447
- if (!existsSync(licenseInstaller)) {
448
- badLine(`缺 ${licenseInstaller}(许可安装的唯一实现)`)
449
- rec('license', 'fail', '缺 scripts/install-license.mjs', { error: licenseInstaller })
450
- stepNext('license', '先把树补齐(--from-packs 或 git pull)再重跑本段')
451
- return false
452
- }
453
- const args = [licenseInstaller, '--file', licenseFile, '--json']
454
- if (licenseSecret) args.push('--secret-file', licenseSecret)
455
- if (instanceTokenFile) args.push('--instance-token-file', instanceTokenFile)
456
- if (!apply) args.push('--dry-run')
457
- const shown = `node ${args.map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ')}`
458
- cmdLine(shown)
459
- infoLine('判据:退出码 0(=已装 / 幂等未变);`regression_refused`、`device_mismatch`、`bad_signature` 等拒绝面一律非零且**不落盘**')
460
- const r = runCmd(process.execPath, args)
461
- const doc = (() => { try { return JSON.parse(r.out.trim().split('\n').filter((l) => l.trim().startsWith('{')).pop() || '') } catch { return null } })()
462
- if (r.code === 0) {
463
- const what = !apply ? 'dry-run:验签通过、未落盘' : (doc && doc.unchanged ? '许可与现役一致(幂等未重写)' : '许可已安装')
464
- okLine(`许可:${what}${doc && doc.signatureVersion ? `(sigV=${doc.signatureVersion})` : ''}`)
465
- rec('license', apply ? 'ok' : 'dry-run', what, { commands: [shown] })
466
- return true
467
- }
468
- badLine(`许可安装被拒:退出码 ${r.code}${doc && doc.reason ? `(reason=${doc.reason})` : ''}`)
469
- for (const l of r.out.trim().split('\n').slice(-4)) infoLine(l)
470
- rec('license', 'fail', `退出码 ${r.code}${doc && doc.reason ? `(${doc.reason})` : ''}`, {
471
- commands: [shown], error: doc && doc.reason ? String(doc.reason) : r.out.trim().split('\n').slice(-2).join(' / '),
472
- })
473
- stepNext('license', doc && doc.reason === 'device_mismatch'
474
- ? '换机场景:加 --rebind(自助重绑,最多 3 次)或让服务端重新签发'
475
- : '看上面 install-license 的最后几行(它自己会点名 reason 与修法)')
476
- return false
477
- }
478
-
479
- // ── ⑤ verify ─────────────────────────────────────────────────────────
480
- async function stepVerify() {
481
- head('verify', '收尾自检(逐条给判据;前置未满足的一律标 n-a 并点名)')
482
- const checks = []
483
- const add = (status, text, extra = {}) => {
484
- checks.push({ status, text, ...extra })
485
- if (status === 'ok') okLine(text); else if (status === 'fail') badLine(text); else warnLine(text)
486
- }
487
- const cmds = {
488
- caps: `node ${installer} --list-capabilities --dsh-home ${dshHome}`,
489
- lic: `node ${coreCli} license status`,
490
- boot: `node ${bootcheckScript} --profile ${profile}`,
491
- role: `node ${roleMappingScript}`,
492
- }
493
- const commands = Object.values(cmds)
494
-
495
- // dry-run:只点名"将要跑的四条自检 + 各自前置",不跑(自检里有一条会真起实例,dry-run 不该有副作用)
496
- if (!apply) {
497
- infoLine('自检四条(dry-run 不执行):')
498
- for (const [k, v] of Object.entries(cmds)) infoLine(` · ${k}: ${v}`)
499
- rec('verify', 'dry-run', 'dry-run:自检未执行', { commands })
500
- return true
501
- }
502
-
503
- // a2) 装机清单**逐文件账实相符**(离线面唯一强判据;也是 D1 验收要的 install.json 证据)。
504
- // 只调 `install-argent.mjs` 落盘时用的**同一个** sha16File 原语,不另写一份哈希口径。
505
- if (existsSync(installJson)) {
506
- try {
507
- const capsLib = await (async () => {
508
- const p = tool(join('scripts', 'argent-capabilities.mjs'))
509
- return existsSync(p) ? import(pathToFileURL(p).href) : null
510
- })()
511
- const doc = JSON.parse(readFileSync(installJson, 'utf8'))
512
- const caps = doc.capabilities || []
513
- const recorded = caps.reduce((n, c) => n + Object.keys(c.files || {}).length, 0)
514
- if (!capsLib) add('n-a', `装机清单账实相符未跑:读不到 ${join(tree, 'scripts', 'argent-capabilities.mjs')}`, { command: cmds.caps })
515
- else {
516
- const bad = []
517
- for (const c of caps) {
518
- for (const [rel, sha] of Object.entries(c.files || {})) {
519
- const got = capsLib.sha16File(join(tree, rel))
520
- if (got !== String(sha)) bad.push(`${rel}(${c.id}:记录 ${sha} 实际 ${got === null ? '缺失' : got})`)
521
- }
522
- }
523
- if (bad.length) { add('fail', `装机清单账实不符:${bad.length}/${recorded} 个文件`, { command: cmds.caps }); for (const b of bad.slice(0, 5)) infoLine(b) }
524
- else add('ok', `装机清单账实相符:${caps.length} 个能力 / ${recorded} 个文件逐文件 sha 一致`)
525
- }
526
- } catch (e) { add('fail', `装机清单比对崩了:${String(e && e.message || e).slice(0, 120)}`, { command: cmds.caps }) }
527
- } else {
528
- add('n-a', `装机清单不存在:${installJson}`, { precondition: installJson, command: cmds.caps })
529
- }
530
-
531
- // b) license status(恒跑)
532
- // ⚠️ 退出码语义(2026-10-05 实测):**许可缺失也是 exit 1**(`denied(missing)`)⇒
533
- // 判据必须看**reason**,不能只看退出码 —— 否则"还没装许可"会被误判成自检失败。
534
- // 但其它 denied(expired / bad_signature / device_mismatch …)是真问题 ⇒ 判 fail。
535
- if (existsSync(coreCli)) {
536
- const r = runCmd(process.execPath, [coreCli, 'license', 'status'])
537
- const lines = r.out.trim().split('\n')
538
- const line = ((lines.find((l) => l.includes('许可状态')) || lines[0] || '').trim().split('许可状态:')[1] || '').trim()
539
- const reason = (/denied[((]([a-z_]+)/.exec(line) || [])[1] || ''
540
- if (reason === 'missing') add('n-a', `许可状态:${line}(**未装许可**,不是自检失败 ⇒ 见 license 段)`, { command: cmds.lic })
541
- else if (r.code !== 0) add('fail', `license status 退出码 ${r.code}${reason ? `(${reason})` : ''}:${line}`, { command: cmds.lic })
542
- else add('ok', `许可状态:${line}`)
543
- } else {
544
- add('n-a', `缺 ${coreCli}(树不完整)`, { precondition: coreCli, command: cmds.lic })
545
- }
546
-
547
- // c) plugin-bootcheck(前置:profile 能解析宿主依赖图)
548
- const scopeInProfile = join(profileDir, 'node_modules', '@deepseek-ai')
549
- if (!existsSync(bootcheckScript)) {
550
- add('n-a', `缺 ${bootcheckScript}`, { precondition: bootcheckScript, command: cmds.boot })
551
- } else if (!existsSync(scopeInProfile)) {
552
- add('n-a', `plugin-bootcheck 未跑:profile 里没有 @deepseek-ai 依赖链(${scopeInProfile})—— 需要包管理器/宿主初始化`, { precondition: scopeInProfile, command: cmds.boot })
553
- } else {
554
- // plugin-bootcheck 读 REAL_DSH_HOME(不是 DSH_HOME)⇒ 显式钉到目标 home,否则会去看真实 ~/.dsh
555
- const r = runCmd(process.execPath, [bootcheckScript, '--profile', profile], {
556
- timeoutMs: Math.max(timeoutS, 900) * 1000,
557
- env: { ...childEnv, REAL_DSH_HOME: dshHome },
558
- })
559
- const tail = r.out.trim().split('\n').slice(-3)
560
- if (r.code === 0 && /DONE fail=0/.test(r.out)) add('ok', 'plugin-bootcheck:启动冒烟通过(DONE fail=0)')
561
- else { add('fail', `plugin-bootcheck 失败(退出码 ${r.code})`, { command: cmds.boot }); for (const l of tail) infoLine(l) }
562
- }
563
-
564
- // d) check-role-mapping(前置:运行时已有 preset)
565
- const presetRoot = join(dshHome, '.agent-presets')
566
- const hasPresets = existsSync(presetRoot) && readdirSync(presetRoot).length > 0
567
- if (!existsSync(roleMappingScript)) {
568
- add('n-a', `缺 ${roleMappingScript}`, { precondition: roleMappingScript, command: cmds.role })
569
- } else if (!hasPresets) {
570
- add('n-a', `check-role-mapping 未跑:运行时还没有 preset(${presetRoot})—— 角色未落位`, { precondition: presetRoot, command: cmds.role })
571
- } else {
572
- const r = runCmd(process.execPath, [roleMappingScript])
573
- if (r.code === 0) add('ok', 'check-role-mapping:角色映射一致')
574
- else { add('fail', `check-role-mapping 退出码 ${r.code}`, { command: cmds.role }); for (const l of r.out.trim().split('\n').slice(-4)) infoLine(l) }
575
- }
576
-
577
- const failed = checks.filter((x) => x.status === 'fail')
578
- const na = checks.filter((x) => x.status === 'n-a')
579
- const okN = checks.filter((x) => x.status === 'ok')
580
- if (na.length) warnLine(`自检真跑通过 ${okN.length}/${checks.length};${na.length} 条**前置未满足未跑**(上面已点名前置与单跑命令)—— 这条结论**不冒充通过**`)
581
- rec('verify', failed.length ? 'fail' : 'ok',
582
- failed.length ? `${failed.length} 条自检失败` : `自检真跑通过 ${okN.length}/${checks.length}${na.length ? `,${na.length} 条前置未满足未跑` : ''}`,
583
- { checks, commands })
584
- if (failed.length) stepNext('verify', '按上面点名的那条自检的单跑命令定位(自检红 = 机器还没到可用状态)')
585
- return failed.length === 0
586
- }
587
-
588
- // ── 主流程 ───────────────────────────────────────────────────────────
589
- say('═══════════════════════════════════════════════════')
590
- say(` Argent 干净机器一条命令(${STEPS.length} 段:${STEPS.join(' → ')})`)
591
- say('═══════════════════════════════════════════════════')
592
- say(` 模式:${apply ? c(1, 'apply(真执行)') : 'dry-run(只打印将执行的命令与判据;加 --apply 真执行)'}`)
593
- say(` 目标:profile=${profile}|DSH_HOME=${dshHome}|树=${tree}`)
594
- say(` 装配面:${fromPacks ? `离线包(${fromPacks};不需要 git/网络)` : '在线(git clone/更新 + dsh + provider + 插件)'}`)
595
- if (only.length !== STEPS.length) say(` 只跑:${only.join(' / ')}`)
596
-
597
- let stopped = false
598
- const skipped = []
599
- for (const id of STEPS) {
600
- if (!only.includes(id) || stopped) { skipped.push({ id, why: stopped ? '失败即停' : '未选(--only)' }); continue }
601
- let cont = true
602
- if (id === 'precheck') cont = await stepPrecheck()
603
- else if (id === 'install') cont = stepInstall()
604
- else if (id === 'skills') cont = await stepSkills()
605
- else if (id === 'license') cont = stepLicense()
606
- else if (id === 'verify') cont = await stepVerify()
607
- if (!cont) stopped = true
608
- }
609
- for (const s of skipped) {
610
- rec(s.id, 'skipped', s.why === '失败即停' ? '因上段失败而停' : '未选(--only)', {})
611
- say(`\n${c(90, `[${s.id}] ⏭ 跳过(${s.why})`)}`)
612
- }
613
-
614
- // ── 结论 ─────────────────────────────────────────────────────────────
615
- const byId = Object.fromEntries(results.map((r) => [r.id, r]))
616
- const st = (id) => (byId[id] || {}).status || 'skipped'
617
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1)
618
- const verdict = {
619
- ok: results.every((r) => r.status !== 'fail'),
620
- mode: apply ? 'apply' : 'dry-run',
621
- profile, dshHome, tree, dataDir,
622
- treeToolsFrom: { installer: toolSource('install-argent.mjs'), license: toolSource(join('scripts', 'install-license.mjs')) },
623
- fromPacks: fromPacks || null,
624
- elapsedSeconds: Number(elapsed),
625
- steps: results,
626
- /** 三段判据行(D1 验收要的三行,单独抽出来便于对账)。 */
627
- segments: {
628
- install: (byId.install || {}).summary || '—',
629
- skills: (byId.skills || {}).summary || '—',
630
- license: (byId.license || {}).summary || '—',
631
- },
632
- licenseInstalled: st('license') === 'ok',
633
- selfCheck: (byId.verify || {}).summary || '—',
634
- humanSteps: [
635
- '许可签发(服务端 issue-license-file.mjs,需账号/JWT)——本命令只装已签发的文件',
636
- '问述账号 OAuth 一键登录(设置 → Argent)——必须人工点授权',
637
- ],
638
- }
639
- const mark = (s) => ({ ok: '✅', fail: '✗ ', skipped: '⏭ ', 'n-a': '⚠️ ', 'dry-run': '▷ ' }[s] || '? ')
640
-
641
- say('\n═══════════════════════════════════════════════════')
642
- say(` 结论(${verdict.mode}|耗时 ${elapsed}s)`)
643
- say('═══════════════════════════════════════════════════')
644
- for (const r of results) say(` ${mark(r.status)} ${r.id.padEnd(9)}:${r.summary}`)
645
- say(`\n 三段判据行(D1 验收要看的三行):`)
646
- say(` 装配 :${verdict.segments.install}`)
647
- say(` 技能同步 :${verdict.segments.skills}`)
648
- say(` 许可 :${verdict.segments.license}`)
649
- say(`\n 人工步骤(本命令**不代做**):`)
650
- for (const h of verdict.humanSteps) say(` · ${h}`)
651
- if (!apply) say(`\n ▷ 这是 dry-run(零写入)。真执行:在同一命令上加 ${c(1, '--apply')}`)
652
- if (!verdict.ok) {
653
- say(`\n ${c(31, '✗ 没跑完 —— 先修上面点名的段,再按"下一步"里的单跑命令重试。')}`)
654
- } else if (apply && !verdict.licenseInstalled) {
655
- say(`\n ${c(33, '⚠️ 编排跑通,但结论是「只差人工步骤」:许可未装 + 账号未登录。')}`)
656
- say(` ${c(33, '⇒ 不要把这条结论读成"机器已经可用"。')}`)
657
- } else if (apply) {
658
- say(`\n ${c(32, '✅ 编排跑通(逐段判据见上)。')}`)
659
- }
660
-
661
- if (reportPath) {
662
- try { writeFileSync(reportPath, `${JSON.stringify(verdict, null, 2)}\n`) } catch (e) { warnLine(`报告写不出(${e.message}):${reportPath}`) }
663
- }
664
- if (asJson) console.log(JSON.stringify(verdict, null, 2))
665
- process.exit(verdict.ok ? 0 : 1)