@whyshu/argent-engine 0.0.0-stage → 0.6.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.
Files changed (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * profile-doctor —— dsh profile「组合体检 + 自愈」
4
+ *
5
+ * ## 为什么存在(三类真实事故,2026-09-14 一天内全踩)
6
+ *
7
+ * 1. **重复挂载**:`argent-skin` 既在 `dsh.profile.bundles`,又在 `cordis.patch.yml` 有 insert 行
8
+ * ⇒ 加载器报 `duplicate loader entry id: argent-skin` ⇒ dsh web **启动即失败**,
9
+ * 在 LaunchAgent KeepAlive 下变成 crash loop(用户若不先验证就重启,服务直接起不来)。
10
+ * 2. **依赖指向已删除的临时目录**:`file:/tmp/kb061-repo/plugins/argent-core`(/tmp 被清后)
11
+ * ⇒ **任何 pnpm 操作都失败**(`[ENOENT] scandir …`)⇒ `dsh plugin add/remove`、安装器全废。
12
+ * 3. **退役插件仍在必需清单里**:老安装器把 `argent-files`(原生 dsh 已覆盖)当必需 patch 行,
13
+ * 会自动补回 ⇒ 等于反复埋雷。
14
+ *
15
+ * 共同根因:**profile 组合是"多来源手工拼装"的(bundles + cordis.patch.yml + dependencies + node_modules),
16
+ * 任何一处漂移都不会立刻报错,只在"下次重启"或"下次 pnpm"时爆**。所以必须有一道
17
+ * 廉价、可自动修、且在安装/升级时强制跑的门禁。
18
+ *
19
+ * ## 用法
20
+ * node scripts/profile-doctor.mjs # 体检当前 web profile(只报告,不改)
21
+ * node scripts/profile-doctor.mjs --profile tui
22
+ * node scripts/profile-doctor.mjs --json # 机器可读报告(含"若加 --fix 会做什么"的计划)
23
+ * node scripts/profile-doctor.mjs --fix # 自动修复组合(改前备份到 ~/.dsh/argent/backup/)
24
+ * node scripts/profile-doctor.mjs --fix --allow-package-manager # 重指依赖后**才**跑包管理器
25
+ * node scripts/profile-doctor.mjs --fix --bootcheck # 修完再跑启动冒烟
26
+ * node scripts/profile-doctor.mjs --profile-dir /tmp/x # 对任意 profile 目录体检(测试用)
27
+ * node scripts/profile-doctor.mjs --fix --backup-dir /tmp/bak # 备份改指(测试**必须**这样隔离)
28
+ *
29
+ * ## 两条与备份有关的纪律(2026-10-04)
30
+ *
31
+ * ① **调用者取证**:每次落备份时,在**备份旁侧**追加一行到 `<备份根>/doctor-callers.jsonl`
32
+ * (`{at, pid, ppid, parentCmd, ancestors, argv, cwd, profile, backedUp, backupFile}`)。
33
+ * 由来:真实备份目录里堆了 **1023 份** `*.doctor-<ts>`(自 2026-09-14),而 web profile 的
34
+ * mtime 纹丝不动 ⇒ 谁都说不清"谁在周期性跑 doctor"。doctor 自己看不见调用者,而调用方
35
+ * 通常隔着好几层(会话 → bash → 测试 → node)—— 只有把**进程祖先链**记下来才定位得到。
36
+ * 纪律:**只记进程线索,绝不记环境变量**(那里有密钥);`argv`/命令行一律脱敏;
37
+ * **取证失败绝不影响主流程**(全程 try/catch)。
38
+ * ② **空转抑制**:将要写入的内容与现文件**逐字节相同** ⇒ **不写、也不产备份**(打印
39
+ * "内容无变化,跳过备份")。有变化时行为与旧实现完全一致:**先备份再写**。
40
+ * ⛔ 这条抑制的是"同一目录被反复修成同一个样子",**不削弱**"写前必有备份"。
41
+ * ③ 备份文件名唯一(`<名字>.doctor-<时间戳>`,同秒冲突时后缀 `.2`/`.3`…):同一次运行会对
42
+ * 同一个文件备份多次,而时间戳只到秒 ⇒ 旧实现里**最早那份(未改动)会被后来者覆盖**,
43
+ * "写前必有备份"在最关键的一次反而丢了。
44
+ *
45
+ * ⚠️ **测试必须改指备份根**(`--backup-dir` / `ARGENT_BACKUP_DIR`):doctor 的备份根默认是
46
+ * `<dsh-home>/argent/backup`,而 `--profile-dir` 只改 profile 目录 —— 夹具 profile 的备份
47
+ * 会**落进真实数据目录**。2026-10-04 取证的 1023 份堆料就是这么来的(见测试用例 ⑧/⑨)。
48
+ *
49
+ * ## ⛔ 包管理器必须显式授权(2026-10-03)
50
+ * 重指 `file:` 依赖后为了让 `node_modules` 与 manifest 一致,旧实现会**自动**
51
+ * `dsh plugin --profile <p> install`(= pnpm install)。这条在 `--fix` 里是**静默副作用**:
52
+ * `argent-upgrade.mjs run` 的第 7.5 步无条件带 `--fix` ⇒ 每次升级都会在 **web profile**
53
+ * (`~/.dsh/AGENTS.md` 明令避免跑 pnpm 的那棵树)上跑一次包管理器,并把 5 条 `file:`
54
+ * 依赖从已消失的临时目录重指到仓库。现在:**没有 `--allow-package-manager` 就不跑**,
55
+ * 只把"依赖已重指、包管理器已跳过、下一步该敲什么"如实打出来。
56
+ *
57
+ * 除 profile 组合外,还会核对**装机清单里的能力包完整性**(批 B4,只报不改):
58
+ * `<dsh-home>/argent/install.json` 的逐文件 sha16 vs 当前文件实际 sha ⇒ 不符即 FAIL;
59
+ * 环境变量 `ARGENT_INSTALL_JSON` 可覆盖清单路径(测试/多实例用)。
60
+ *
61
+ * ## 退出码
62
+ * 0 = 无问题(或已全部修复) 1 = 发现问题(未加 --fix 时)/ 修复失败
63
+ */
64
+ import { execFileSync } from 'node:child_process'
65
+ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
66
+ import { createRequire } from 'node:module'
67
+ import { homedir, tmpdir } from 'node:os'
68
+ import { basename, dirname, join, resolve } from 'node:path'
69
+ import { fileURLToPath } from 'node:url'
70
+ import { describeCapability, isRegistryProjection, readInstallJson, sha16File } from './argent-capabilities.mjs'
71
+ // bundle 层名单与升级器**共用一份**(2026-09-28:两处知识分叉过 —— doctor 报红而升级器
72
+ // 以为正常,客户机 P3 卡在体检门禁上)
73
+ import { bundleLayerMap, pluginRowName, retiredRowMap } from './lib/plugin-lifecycle.mjs'
74
+ import { exactPluginIdOf, migrateBundleNames, pluginIdOf } from './lib/plugin-packages.mjs'
75
+ import { renderInsertBlock } from './lib/patch-compose.mjs'
76
+ import { resolveDshBin } from './lib/dsh-scope.mjs'
77
+
78
+ const SELF = fileURLToPath(import.meta.url)
79
+ const REPO = resolve(dirname(SELF), '..')
80
+ const HOME = homedir()
81
+ const DSH_HOME = process.env.DSH_HOME || join(HOME, '.dsh')
82
+
83
+ /** 规范插件根:所有 file: 依赖都应当落在这里(或下面的 packs-cache)。 */
84
+ const CANONICAL_ROOTS = [join(REPO, 'plugins')]
85
+ /** 允许的例外:市场包缓存(.tgz)等。 */
86
+ const ALLOWED_PREFIXES = [join(DSH_HOME, 'argent', 'packs-cache')]
87
+ /** 已退役插件:{id: 原因};patch 行里出现即移除。名单见 lib/plugin-lifecycle.mjs。 */
88
+ const RETIRED = retiredRowMap()
89
+ /** 已迁移到 bundle 层的插件:出现在 patch 行即为重复挂载。名单见 lib/plugin-lifecycle.mjs。 */
90
+ const BUNDLE_MIGRATED = bundleLayerMap()
91
+ /** 必须存在的 patch 行。 */
92
+ const REQUIRED_ROWS = ['argent-core']
93
+
94
+ // ── 参数 ──────────────────────────────────────────────────────────────
95
+ const argv = process.argv.slice(2)
96
+ const flag = (n) => argv.includes(n)
97
+ const opt = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
98
+ const FIX = flag('--fix')
99
+ const RUN_BOOTCHECK = flag('--bootcheck')
100
+ /** 机器可读输出(升级器在决定"要不要 --fix"前先跑一次只读体检拿计划)。 */
101
+ const JSON_OUT = flag('--json')
102
+ /** **显式**授权在真实 profile 上跑包管理器(= `dsh plugin install` → pnpm install)。缺省不给。 */
103
+ const ALLOW_PACKAGE_MANAGER = flag('--allow-package-manager')
104
+ const PROFILE = opt('--profile', 'web')
105
+ const PROFILE_DIR = opt('--profile-dir', join(DSH_HOME, 'profiles', PROFILE))
106
+ /**
107
+ * 备份根(`<dsh-home>/argent/backup`):改前留档 + 调用者线索都落这里。
108
+ * `--backup-dir` / `ARGENT_BACKUP_DIR` 可改指 —— **测试一律要改指**,否则夹具 profile 的备份
109
+ * 会写进真实数据目录(2026-10-04 取证的 1023 份堆料正是这么来的)。
110
+ */
111
+ const BACKUP_DIR = resolve(opt('--backup-dir', process.env.ARGENT_BACKUP_DIR || join(DSH_HOME, 'argent', 'backup')))
112
+ /** 是否就是 dsh 真正挂载的那个 profile 目录;只有它才允许跑 pnpm(否则会误改线上 profile)。 */
113
+ const IS_REAL_PROFILE = resolve(PROFILE_DIR) === resolve(join(DSH_HOME, 'profiles', PROFILE))
114
+
115
+ const problems = [] // {level:'error'|'warn', code, msg, fix?}
116
+ const fixes = [] // 已执行的修复
117
+ const add = (level, code, msg, fix) => problems.push({ level, code, msg, fix })
118
+
119
+ // ── 读取 ──────────────────────────────────────────────────────────────
120
+ const manifestPath = join(PROFILE_DIR, 'package.json')
121
+ const patchPath = join(PROFILE_DIR, 'cordis.patch.yml')
122
+ if (!existsSync(manifestPath)) {
123
+ console.error(`[doctor] 找不到 profile manifest:${manifestPath}`)
124
+ process.exit(1)
125
+ }
126
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
127
+ const deps = manifest.dependencies || {}
128
+ const bundles = manifest?.dsh?.profile?.bundles || []
129
+ let patchText = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : ''
130
+ const patchLines = patchText.split('\n')
131
+
132
+ // ── 工具 ──────────────────────────────────────────────────────────────
133
+ const under = (p, root) => p === root || p.startsWith(root + '/')
134
+ const hasPackageJson = (dir) => { try { return existsSync(join(dir, 'package.json')) } catch { return false } }
135
+
136
+ /**
137
+ * 某插件在 `dsh.profile.bundles` 里**有没有登记**。
138
+ * ⚠️ W9(2026-10-13):条目是**包名**(`@whyshu/argent-skin`),存量机器上是裸名
139
+ * (`argent-skin`,见 migrateBundleNames)⇒ 只按 `row.id` 字面比对会**漏判"重复挂载"**,
140
+ * 而漏判的后果是加载器 `duplicate loader entry id` ⇒ **dsh web 启动即失败**。
141
+ * 判据经唯一真源归一:**严格**认「裸 id」与「本仓 npm 名」两种精确写法(⛔ 别的 scope 不算我们
142
+ * 的重复挂载 —— `@acme/argent-skin` 与 `argent-skin` 是**两个不同的包**)。
143
+ */
144
+ function bundleLanded(rowId) {
145
+ return bundles.some((b) => exactPluginIdOf(b) === rowId)
146
+ }
147
+
148
+ /** patch 行名 → 包名 / 子路径(`argent-core/llm-whyshu` → `argent-core` + `llm-whyshu`)。 */
149
+ function splitRowName(spec) {
150
+ const parts = String(spec).split('/')
151
+ const pkg = String(spec).startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
152
+ return { pkg, sub: String(spec).slice(pkg.length).replace(/^\//, '') }
153
+ }
154
+
155
+ /**
156
+ * patch 行引用的包**是否真的装了**(判据**不放宽**,只补子路径导出这一形态)。
157
+ *
158
+ * 事故(2026-10-03,假阳性):旧实现只看字面目录 `node_modules/<row.name>`
159
+ * ⇒ 行名是**子路径导出**时(`name: 'argent-core/llm-whyshu'`,真实解析到
160
+ * `node_modules/argent-core/src/llm-whyshu.js`,见包内 `exports["./llm-whyshu"]`)
161
+ * 那个"目录"根本不存在 ⇒ 每次体检都报 `row-pkg-missing`(且 `--fix` 给出
162
+ * `dsh plugin add file:<repo>/plugins/argent-core/llm-whyshu` 这种**错误的**修法)。
163
+ *
164
+ * 判据:
165
+ * · 无子路径 ⇒ 仍按字面目录存在(原判据,一字不改);
166
+ * · 有子路径 ⇒ ① 以 profile 为锚 `require.resolve`(走 exports 映射);
167
+ * ② 退回读包的 `exports` 是否声明该子路径(含 `./*` 通配);
168
+ * ③ 无 `exports` 字段时才退回字面文件判定。
169
+ * ⇒ "子路径导出"不再报缺;真缺包(目录/包名不存在、exports 未声明该子路径)**照样报**。
170
+ */
171
+ function rowPackageInstalled(profileDir, spec, deps = { createRequire, existsSync, readFileSync }) {
172
+ const { pkg, sub } = splitRowName(spec)
173
+ const pkgDir = join(profileDir, 'node_modules', pkg)
174
+ if (!sub) return deps.existsSync(pkgDir)
175
+ try {
176
+ deps.createRequire(join(profileDir, 'package.json')).resolve(spec)
177
+ return true
178
+ } catch { /* 继续看 exports 声明 */ }
179
+ let pj = null
180
+ try { pj = JSON.parse(deps.readFileSync(join(pkgDir, 'package.json'), 'utf8')) } catch { return false }
181
+ const ex = pj && pj.exports
182
+ if (ex && typeof ex === 'object') {
183
+ const keys = Object.keys(ex)
184
+ if (keys.includes(`./${sub}`)) return true
185
+ for (const k of keys) {
186
+ if (!k.includes('*')) continue
187
+ const re = new RegExp('^' + k.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$')
188
+ if (re.test(`./${sub}`)) return true
189
+ }
190
+ return false // 有 exports 却没声明该子路径 ⇒ 加载必失败,照报
191
+ }
192
+ return deps.existsSync(join(pkgDir, sub)) || deps.existsSync(join(pkgDir, `${sub}.js`))
193
+ }
194
+
195
+ // ── 备份(写前留档)+ 调用者取证 ────────────────────────────────────────
196
+ /**
197
+ * 命令行脱敏:把"看起来是密钥"的片段换成 `***`。
198
+ * ⛔ 取证**不许**把密钥写进日志 —— 祖先链里的命令行可能带着 token/api-key(别的进程的)。
199
+ */
200
+ function redact(text) {
201
+ return String(text ?? '')
202
+ .replace(/((?:--?)?(?:api[-_]?key|access[-_]?token|token|secret|password|passwd)[=\s:]+)\S+/gi, '$1***')
203
+ .replace(/\bBearer\s+\S+/gi, 'Bearer ***')
204
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, 'sk-***')
205
+ .replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g, '***jwt***')
206
+ }
207
+
208
+ /** 单行化 + 脱敏 + 截断(日志只留可读线索,不留大块文本)。 */
209
+ function clip(text, max = 400) {
210
+ const t = redact(text).replace(/\s+/g, ' ').trim()
211
+ return t.length > max ? `${t.slice(0, max)}…` : t
212
+ }
213
+
214
+ /** Linux:读 `/proc` 拿 cmdline + ppid。macOS 没有 `/proc` ⇒ 返回 null,调用方退回 `ps`。 */
215
+ function procInfo(pid) {
216
+ try {
217
+ const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean).join(' ')
218
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
219
+ const ppid = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])
220
+ return { pid: Number(pid), ppid: Number.isFinite(ppid) ? ppid : 0, command }
221
+ } catch { return null }
222
+ }
223
+
224
+ /** 兜底:`ps -o pid=,ppid=,command= -p <pid>`(macOS / Linux 同形)。失败 ⇒ null(取证失败不得影响主流程)。 */
225
+ function psInfo(pid) {
226
+ try {
227
+ const out = execFileSync('ps', ['-o', 'pid=,ppid=,command=', '-p', String(pid)],
228
+ { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] })
229
+ const line = String(out).split('\n').map((l) => l.trim()).filter(Boolean)[0]
230
+ const m = line ? /^(\d+)\s+(\d+)\s+(.*)$/.exec(line) : null
231
+ return m ? { pid: Number(m[1]), ppid: Number(m[2]), command: m[3] } : null
232
+ } catch { return null }
233
+ }
234
+
235
+ /** 进程祖先链(最多 depth 层):`[父, 祖父, …]`。取不到就返回已经拿到的部分。 */
236
+ function ancestorChain(startPid, depth = 4) {
237
+ const chain = []
238
+ let pid = Number(startPid)
239
+ for (let i = 0; i < depth && Number.isFinite(pid) && pid > 1; i++) {
240
+ const info = procInfo(pid) || psInfo(pid)
241
+ if (!info) break
242
+ chain.push({ pid: info.pid, command: clip(info.command, 300) })
243
+ pid = info.ppid
244
+ }
245
+ return chain
246
+ }
247
+
248
+ /**
249
+ * 把**一条**调用者线索追加到 `<备份根>/doctor-callers.jsonl`(JSONL:一行一条,便于 `tail`/`jq`)。
250
+ * **best-effort**:任何失败都吞掉并返回 null —— ⛔ 取证失败绝不允许影响体检/修复主流程。
251
+ */
252
+ function recordCaller(entry) {
253
+ try {
254
+ const chain = ancestorChain(process.ppid)
255
+ const rec = {
256
+ at: new Date().toISOString(),
257
+ pid: process.pid,
258
+ ppid: process.ppid,
259
+ parentCmd: chain.length > 0 ? chain[0].command : null, // = 谁直接 spawn 了 doctor
260
+ ancestors: chain, // 再往上:bash / 会话 / 测试入口
261
+ argv: process.argv.slice(1).map((a) => clip(a, 200)), // doctor 自己的参数(脱敏)
262
+ cwd: process.cwd(),
263
+ profile: PROFILE_DIR,
264
+ backupDir: BACKUP_DIR,
265
+ ...entry,
266
+ }
267
+ mkdirSync(BACKUP_DIR, { recursive: true })
268
+ appendFileSync(join(BACKUP_DIR, 'doctor-callers.jsonl'), JSON.stringify(rec) + '\n')
269
+ return rec
270
+ } catch { return null }
271
+ }
272
+
273
+ const backupStamp = () => new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15)
274
+
275
+ /**
276
+ * 写前留档:把 `file` 原样复制成 `<备份根>/<名字>.doctor-<时间戳>`,并记一行调用者线索。
277
+ * 时间戳只到秒,而同一次运行会对同一文件备份多次(重指 → 删行 → 补必需行)⇒ 撞名时后缀
278
+ * `.2`/`.3`…:⛔ 绝不让后来者覆盖最早那份(那才是"未改动"的原件)。
279
+ */
280
+ function backup(file) {
281
+ mkdirSync(BACKUP_DIR, { recursive: true })
282
+ const base = `${basename(file)}.doctor-${backupStamp()}`
283
+ let target = join(BACKUP_DIR, base)
284
+ for (let n = 2; existsSync(target); n++) target = join(BACKUP_DIR, `${base}.${n}`)
285
+ cpSync(file, target)
286
+ recordCaller({ backedUp: file, backupFile: target })
287
+ return target
288
+ }
289
+
290
+ /** 本轮因"内容无变化"而跳过的写入(供报告/`--json` 如实回显)。 */
291
+ const skippedWrites = []
292
+
293
+ /**
294
+ * **写前必有备份**,但**只在内容真的要变**时才写(2026-10-04 空转抑制):
295
+ * · 逐字节相同 ⇒ **不写、不产备份**(记一行"内容无变化,跳过备份");
296
+ * · 有变化 ⇒ 与旧实现**完全一致**:先 `backup()`,再 `writeFileSync()`。
297
+ * ⛔ 不要把它改成"先备份再比较"——那正是白堆备份的形态。
298
+ */
299
+ function writeIfChanged(file, nextText) {
300
+ let current = null
301
+ try { current = readFileSync(file, 'utf8') } catch { current = null }
302
+ if (current !== null && current === nextText) {
303
+ skippedWrites.push(basename(file))
304
+ return false
305
+ }
306
+ backup(file)
307
+ writeFileSync(file, nextText)
308
+ return true
309
+ }
310
+
311
+ function parsePatchRows(lines) {
312
+ // 逐行扫描 `- id: X` / 紧随的 `name: 'Y'`,返回行号(0 基)便于精确删改。
313
+ //
314
+ // ⚠️ 两种条目语义不同(2026-09-29 修):
315
+ // · **insert 行**:`- id: X` + `name: 'pkg'` ⇒ 引入一个包,要检查它装没装;
316
+ // · **覆盖行(override)**:只有 `- id: X` + `config:`,没有 `name` ⇒ 按 id 改**已存在**
317
+ // 那一行的配置(cordis patch 的标准写法)。旧实现把 name 兜底成 id ⇒ 于是
318
+ // `- id: agent-preset-registry / config:` 这种覆盖行被当成 insert 行,报
319
+ // "row-pkg-missing:包 agent-preset-registry 未安装" ⇒ 升级门禁卡死(本轮实测)。
320
+ const rows = []
321
+ for (let i = 0; i < lines.length; i++) {
322
+ const m = /^\s*-\s*id:\s*([A-Za-z0-9_.@/-]+)\s*$/.exec(lines[i])
323
+ if (!m) continue
324
+ const nameM = i + 1 < lines.length ? /^\s*name:\s*['"]?([\w@/.-]+)['"]?\s*$/.exec(lines[i + 1]) : null
325
+ rows.push({ index: i, lines: nameM ? 2 : 1, id: m[1], name: nameM ? nameM[1] : null, isOverride: !nameM })
326
+ }
327
+ return rows
328
+ }
329
+
330
+ // ── 检查 1:dependencies 的 file: 规格是否指向存在且规范的路径 ──────────
331
+ const repoint = {}
332
+ /** 修复计划:**执行前**把"会改什么、要不要跑包管理器"如实报出来(升级器 / --json 消费)。 */
333
+ const fixPlan = { depsToRepoint: [], bundlesToRename: [], patchRowsToDrop: 0, requiredRowsToAdd: [], runsPackageManager: false, packageManagerCommand: null }
334
+ /**
335
+ * 依赖键 → 规范插件目录(`<REPO>/plugins/<插件 id>`)。
336
+ * ⚠️ W9(2026-10-13):键现在是**包名**(`@whyshu/argent-core`),⛔ 目录名恒是**裸 id**
337
+ * ⇒ 只用键当目录名会算出 `<REPO>/plugins/@whyshu/argent-core`(不存在)⇒ 该依赖被判成
338
+ * "目标不存在且规范路径也没有"(`dep-missing-unfixable`,无法自愈)。
339
+ */
340
+ const canonicalDirOf = (name) => join(REPO, 'plugins', pluginIdOf(name) || String(name))
341
+ for (const [name, spec] of Object.entries(deps)) {
342
+ const s = String(spec)
343
+ if (!s.startsWith('file:')) continue
344
+ const p = s.slice(5)
345
+ if (!existsSync(p)) {
346
+ // 目标已不存在:若能映射回规范根则重指,否则只能报错
347
+ const guess = canonicalDirOf(name)
348
+ if (hasPackageJson(guess)) {
349
+ repoint[name] = `file:${guess}`
350
+ fixPlan.depsToRepoint.push({ name, from: p, to: guess })
351
+ add('error', 'dep-missing', `依赖 ${name} → ${p}(**目标不存在**)`,
352
+ `重指到规范路径 ${guess}`)
353
+ } else {
354
+ add('error', 'dep-missing-unfixable', `依赖 ${name} → ${p}(目标不存在,且 ${guess} 也没有该插件)`,
355
+ '需人工确认该插件是否还存在')
356
+ }
357
+ continue
358
+ }
359
+ if (ALLOWED_PREFIXES.some((r) => under(p, r))) continue
360
+ if (!CANONICAL_ROOTS.some((r) => under(p, r))) {
361
+ const guess = canonicalDirOf(name)
362
+ if (hasPackageJson(guess)) {
363
+ repoint[name] = `file:${guess}`
364
+ fixPlan.depsToRepoint.push({ name, from: p, to: guess })
365
+ add('warn', 'dep-foreign', `依赖 ${name} → ${p}(**不在规范插件根内**;临时目录/别处副本会在清理后失效)`,
366
+ `重指到 ${guess}`)
367
+ } else {
368
+ add('warn', 'dep-foreign', `依赖 ${name} → ${p}(不在规范插件根内,且规范路径无此插件)`, '请人工确认')
369
+ }
370
+ }
371
+ }
372
+
373
+ // ── 检查 2/3/4:patch 行——退役、重复挂载、必需缺失、包缺失 ────────────
374
+ const rows = parsePatchRows(patchLines)
375
+ const dropIndexes = new Set()
376
+ for (const row of rows) {
377
+ // 覆盖行只改已存在行的 config,不引入包 ⇒ 与"包是否存在/是否退役"无关
378
+ if (row.isOverride) continue
379
+ if (RETIRED[row.id] || RETIRED[row.name]) {
380
+ for (let k = 0; k < row.lines; k++) dropIndexes.add(row.index + k)
381
+ add('error', 'row-retired', `patch 行 ${row.id}:${RETIRED[row.id] || RETIRED[row.name]}`, `移除该行`)
382
+ continue
383
+ }
384
+ if (BUNDLE_MIGRATED[row.id] && bundleLanded(row.id)) {
385
+ for (let k = 0; k < row.lines; k++) dropIndexes.add(row.index + k)
386
+ add('error', 'row-duplicate',
387
+ `patch 行 ${row.id} 与 dsh.profile.bundles 重复(${BUNDLE_MIGRATED[row.id]})`
388
+ + ` ⇒ 加载器会报 duplicate loader entry id,**dsh web 启动即失败**`, '移除该 patch 行')
389
+ continue
390
+ }
391
+ if (BUNDLE_MIGRATED[row.id]) {
392
+ add('warn', 'row-migrated', `patch 行 ${row.id}:${BUNDLE_MIGRATED[row.id]},但它不在 bundles 里`, `确认是否应登记进 bundles`)
393
+ }
394
+ // 行存在但包没装 → 启动时会 failed to apply loader entry
395
+ // ⚠️ 判据是"**能不能解析到**",不是"字面目录在不在":行名允许**子路径导出**
396
+ // (`argent-core/llm-whyshu` → 包内 exports["./llm-whyshu"]);旧实现按字面目录判
397
+ // ⇒ 2026-10-03 之前每次都误报 row-pkg-missing。见 rowPackageInstalled()。
398
+ if (!rowPackageInstalled(PROFILE_DIR, row.name) && !row.name.startsWith('@deepseek-ai/')) {
399
+ add('error', 'row-pkg-missing', `patch 行 ${row.id} 的包 ${row.name} 未安装(node_modules 里解析不到)`,
400
+ `dsh plugin --profile ${PROFILE} add file:${join(REPO, 'plugins', splitRowName(row.name).pkg)}`)
401
+ }
402
+ }
403
+ for (const id of REQUIRED_ROWS) {
404
+ if (!rows.some((r) => r.id === id)) {
405
+ add('error', 'row-required-missing', `缺少必需 patch 行 ${id}(缺失会导致重启后对应端点 404)`, `补上该行`)
406
+ }
407
+ }
408
+ // 修复计划(供 --json / 升级器"执行前明示"):包管理器只有在**依赖被重指**时才需要跑
409
+ fixPlan.patchRowsToDrop = dropIndexes.size
410
+ fixPlan.requiredRowsToAdd = REQUIRED_ROWS.filter((id) => !rows.some((r) => r.id === id))
411
+ fixPlan.runsPackageManager = fixPlan.depsToRepoint.length > 0
412
+ fixPlan.packageManagerCommand = `dsh plugin --profile ${PROFILE} install(= pnpm install;⛔ 只对真实 profile,且需显式 --allow-package-manager)`
413
+
414
+ // ── 修复 ──────────────────────────────────────────────────────────────
415
+ let manifestDirty = false
416
+ if (FIX && Object.keys(repoint).length > 0) {
417
+ for (const [name, spec] of Object.entries(repoint)) deps[name] = spec
418
+ manifest.dependencies = deps
419
+ // 内容无变化 ⇒ 不写也不备份(有变化时先 backup 再写,与旧实现一致)
420
+ if (writeIfChanged(manifestPath, JSON.stringify(manifest, null, 2) + '\n')) {
421
+ for (const [name, spec] of Object.entries(repoint)) fixes.push(`依赖 ${name} 重指到 ${spec.slice(5)}`)
422
+ manifestDirty = true
423
+ }
424
+ }
425
+ // ── W9(2026-10-13)`dsh.profile.bundles`:裸名 ⇒ npm 名(迁移的**第三处**;幂等 + 写前备份)──
426
+ // 裸名条目(`argent-skin`)在 profile 里**解析不到**(实体在 `node_modules/@whyshu/argent-skin`)
427
+ // ⇒ 该 bundle 加载不到 ⇒ **皮肤/UI 壳静默不加载**(装完零报错)。判据/迁移走唯一真源。
428
+ // 与依赖重指**分开**处理:bundles 改的是**键名**,不需要包管理器 ⇒ 不置 manifestDirty(否则会
429
+ // 连带触发 pnpm 那一步),但仍走 writeIfChanged(写前必有备份)。
430
+ const bundlesMig = migrateBundleNames(bundles)
431
+ fixPlan.bundlesToRename = bundlesMig.renamed.map((r) => ({ from: r.from, to: r.to }))
432
+ // 读侧也**响亮点名**(⛔ 不静默):真机现场"只读体检只剩一条 install.json WARN",
433
+ // 而真正致命的裸名条目**一个字都没报** —— 用户据此以为装好了。
434
+ for (const r of bundlesMig.renamed) {
435
+ add('warn', 'bundle-bare-name',
436
+ `dsh.profile.bundles 里的 ${r.from} 是**裸名**(应为 ${r.to})⇒ 该 bundle 在 profile 里**解析不到**、不会加载`
437
+ + '(皮肤/UI 壳静默不生效)', `--fix 迁移到 ${r.to}`)
438
+ }
439
+ for (const s of bundlesMig.stale) {
440
+ add('warn', 'bundle-stale-alias',
441
+ `dsh.profile.bundles 里的 ${s} 与同一个 bundle 的**规范名并存** ⇒ 会重复挂载`, `--fix 摘掉 ${s}`)
442
+ }
443
+ if (FIX && (bundlesMig.renamed.length > 0 || bundlesMig.stale.length > 0)) {
444
+ manifest.dsh = manifest.dsh || {}
445
+ manifest.dsh.profile = manifest.dsh.profile || {}
446
+ manifest.dsh.profile.bundles = bundlesMig.bundles
447
+ if (writeIfChanged(manifestPath, JSON.stringify(manifest, null, 2) + '\n')) {
448
+ for (const r of bundlesMig.renamed) fixes.push(`bundles 条目 ${r.from} → ${r.to}(npm 名)`)
449
+ if (bundlesMig.stale.length) fixes.push(`摘掉 bundles 里的陈旧别名 ${bundlesMig.stale.join(', ')}(同一 bundle 不许留两个名字)`)
450
+ }
451
+ }
452
+ if (FIX && dropIndexes.size > 0) {
453
+ const nextText = patchLines.filter((_, i) => !dropIndexes.has(i)).join('\n')
454
+ if (writeIfChanged(patchPath, nextText)) {
455
+ patchText = nextText
456
+ fixes.push(`移除 ${dropIndexes.size} 行退役/重复 patch 行`)
457
+ }
458
+ }
459
+ if (FIX && problems.some((p) => p.code === 'row-required-missing')) {
460
+ const missing = REQUIRED_ROWS.filter((id) => !rows.some((r) => r.id === id))
461
+ if (missing.length > 0) {
462
+ // ⚠️ W9:`name:` 是**包名**(`@whyshu/argent-core`),`id:` 仍是裸 id —— 旧实现在这里
463
+ // 写 `name: '<裸 id>'`,是"profile 面向字符串漏迁 scope"的**第四个写入点**(装完那一行解析不到)。
464
+ // 渲染一律走**唯一实现** `patch-compose.mjs#renderInsertBlock()`(⛔ 不在本文件里再拼一份)。
465
+ const block = renderInsertBlock(missing.map((id) => ({ id, name: pluginRowName(id) })))
466
+ let t = readFileSync(patchPath, 'utf8')
467
+ t = t.replace(/^\s*\[\]\s*$/gm, '').replace(/\n{3,}$/, '\n\n')
468
+ if (writeIfChanged(patchPath, t.trimEnd() + '\n\n' + block + '\n')) fixes.push(`补上必需行 ${missing.join(', ')}`)
469
+ }
470
+ }
471
+ if (FIX && manifestDirty) {
472
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
473
+ // 规格变了必须让 node_modules 跟上(否则 manifest 与磁盘不一致)。
474
+ // ⚠ 只对 dsh 真正挂载的 profile 执行——`--profile-dir` 指向别处时(测试/合成目录)
475
+ // 跑 pnpm 会误改线上 profile。
476
+ // ⚠⚠ 2026-10-03:**必须显式授权**(--allow-package-manager)才跑包管理器。
477
+ // 旧实现只要 --fix 就会跑 ⇒ `argent-upgrade.mjs run` 的第 7.5 步无条件带 --fix,
478
+ // 于是每次升级都在 web profile 上跑了一次 pnpm install(`~/.dsh/AGENTS.md` 明令避免),
479
+ // 还把 5 条 file: 依赖从已消失的临时目录重指到仓库 —— 全是**静默副作用**。
480
+ // 现在的规矩:改 manifest 照旧,但**包管理器这一步不替用户做主**。
481
+ if (!IS_REAL_PROFILE) {
482
+ fixes.push('(非真实 profile 目录,跳过 pnpm install;真实场景会自动重链)')
483
+ } else if (!ALLOW_PACKAGE_MANAGER) {
484
+ const cmd = `dsh plugin --profile ${PROFILE} install`
485
+ add('warn', 'pkg-manager-skipped',
486
+ `依赖已重指,但**未运行包管理器**(缺显式 --allow-package-manager):node_modules 可能与 manifest 不一致`,
487
+ `确认后自行执行:${cmd}(= pnpm install;web profile 上是红线动作,先读 ~/.dsh/AGENTS.md)`)
488
+ fixes.push(`已跳过包管理器(未给 --allow-package-manager)⇒ 需要时手动跑:${cmd}`)
489
+ } else {
490
+ try {
491
+ // 2026-09-28:写死 homebrew 路径 ⇒ 客户机上这里直接 ENOENT,自愈失效。用共享解析。
492
+ execFileSync(resolveDshBin(), ['plugin', '--profile', PROFILE, 'install'],
493
+ { stdio: 'pipe', cwd: HOME, timeout: 300000 })
494
+ fixes.push('已 pnpm install 使 node_modules 与 manifest 一致(--allow-package-manager 已显式授权)')
495
+ } catch (error) {
496
+ add('error', 'pnpm-failed', `修复后 pnpm install 失败:${String(error.stderr || error.message).slice(0, 300)}`, '需人工介入')
497
+ }
498
+ }
499
+ }
500
+
501
+ // ── 检查 5:装机清单「包完整性」(批 B4;只报不改)──────────────────────
502
+ // 依据 `<dsh-home>/argent/install.json`(ARGENT_INSTALL_JSON 可覆盖):
503
+ // ① 逐文件 sha16:装机时记录的 sha vs 当前文件实际 sha ⇒ 不符/缺失即 FAIL(改一个脚本就能报出来);
504
+ // ② 服务端清单 sha256 为空的条目 ⇒ 只 WARN(发布流程待填),**不算 FAIL**;
505
+ // ③ 能力包工具缺失 / 能力不在本地注册表 ⇒ WARN 并跳过(客户机上不能因为工具不在就判死)。
506
+ const INSTALL_JSON = process.env.ARGENT_INSTALL_JSON || join(DSH_HOME, 'argent', 'install.json')
507
+ const CAP_PACK = join(REPO, 'scripts', 'capability_pack.py')
508
+ const packLines = [] // [['PASS'|'WARN'|'FAIL', 文案]],报告区统一打印
509
+
510
+ function checkPackIntegrity() {
511
+ const line = (tag, msg) => packLines.push([tag, msg])
512
+ // 与其它检查一致:登记进 problems 参与退出码判定,但由本区自己打印(printed=true 防重复)
513
+ const record = (level, code, msg, fix) => {
514
+ add(level, code, msg, fix)
515
+ problems[problems.length - 1].printed = true
516
+ }
517
+ if (!existsSync(INSTALL_JSON)) {
518
+ const msg = '未找到 install.json(可能未按能力清单安装)'
519
+ line('WARN', msg)
520
+ record('warn', 'pkg-install-json-missing', msg,
521
+ `按能力安装:node install-argent.mjs --api-key <账号 API Key>(或核对 --dsh-home / ARGENT_INSTALL_JSON=${INSTALL_JSON})`)
522
+ return
523
+ }
524
+ const doc = readInstallJson(INSTALL_JSON)
525
+ if (!doc) {
526
+ const msg = `install.json 解析失败:${INSTALL_JSON}`
527
+ line('WARN', msg)
528
+ record('warn', 'pkg-install-json-broken', msg, '删除该文件后用安装器重写(它只是装机记录,不是配置)')
529
+ return
530
+ }
531
+ const caps = (Array.isArray(doc.capabilities) ? doc.capabilities : []).filter((c) => c && c.id)
532
+ line('INFO', `套餐 ${doc.plan || '未记录'}|能力 ${caps.length} 个${doc.installedAt ? `|安装于 ${doc.installedAt}` : ''}`)
533
+ if (!caps.length) {
534
+ line('WARN', '装机清单里没有能力(免费版或服务端清单为空)—— 跳过包完整性核对')
535
+ return
536
+ }
537
+ // 能力包工具缺失:只 WARN。**但装机记录里若已有逐文件 sha,照样核对**
538
+ // (降级不等于放弃:客户机上没有 python 时,至少还能发现"文件被改过")
539
+ const hasTool = existsSync(CAP_PACK)
540
+ if (!hasTool) {
541
+ const msg = `未找到能力包工具 ${CAP_PACK}(按装机记录核对,跳过本地包清单)`
542
+ line('WARN', msg)
543
+ record('warn', 'pkg-tool-missing', msg, '确认仓库完整(scripts/capability_pack.py 应随 git 分发)')
544
+ }
545
+ for (const c of caps) {
546
+ const id = String(c.id)
547
+ const recorded = c.files && typeof c.files === 'object' ? c.files : {}
548
+ const useRecorded = Object.keys(recorded).length > 0
549
+ const d = hasTool ? describeCapability(REPO, id) : { ok: false, error: '未找到能力包工具' }
550
+ if (!d.ok && !useRecorded) {
551
+ const msg = `能力 ${id} 包清单取不到(${d.error})`
552
+ line('WARN', msg)
553
+ record('warn', 'pkg-describe-failed', msg, `手工核对:python3 ${CAP_PACK} --describe ${id}`)
554
+ continue
555
+ }
556
+ if (!d.ok) line('INFO', `能力 ${id} 包清单取不到(${d.error})—— 退回按装机记录核对`)
557
+ const expected = useRecorded ? recorded : (d.integrity || {})
558
+ const basis = useRecorded ? '装机记录' : '本地能力包清单'
559
+ // 整包 sha256 缺失是**独立**的一档(2026-09-21 修):早前它被塞在"文件清单为空"分支里,
560
+ // 而目录展开后每个能力都有逐文件 sha ⇒ 这个告警变成**不可达**(服务端未填 sha 的真实现场反而看不见)。
561
+ let shaMissing = false
562
+ if (String(c.sha256 ?? '') === '') {
563
+ const msg = `能力 ${id} 未提供 sha256(服务端清单待发布流程填充)`
564
+ line('WARN', msg)
565
+ record('warn', 'pkg-sha256-missing', msg, '服务端清单填充 sha256 后重跑安装器刷新 install.json')
566
+ shaMissing = true
567
+ }
568
+ if (!Object.keys(expected).length) {
569
+ if (!shaMissing) {
570
+ const msg = `能力 ${id} 无可核对的文件清单(装机记录无 files,本地包清单也为空)`
571
+ line('WARN', msg)
572
+ record('warn', 'pkg-no-filelist', msg, `python3 ${CAP_PACK} --describe ${id}`)
573
+ }
574
+ continue
575
+ }
576
+ let failed = 0
577
+ let empty = 0
578
+ let projected = 0
579
+ for (const [rel, want] of Object.entries(expected)) {
580
+ if (!want) { empty++; continue }
581
+ // ⑤ 同源:`--from-packs` 装出来的记录里含**包内注册表投影**(`PACK.json#sourceRegistry`),
582
+ // 而落盘时它按"目标机覆盖 ⇒ 保留 / 缺本包条目 ⇒ 合并"处理(`installPackFiles()` 的既有语义)
583
+ // ⇒ 盘上那份与记录里那份 sha **设计上就不同**。判据用能力清单模块的**同一个**函数
584
+ // (`isRegistryProjection()`,唯一真源);否则每次 `--from-packs` 安装后医生都误报 FAIL
585
+ // `pkg-sha-mismatch`,还给一条走不通的修法("回退该文件或重跑安装器" —— 重跑还是这份差异)。
586
+ if (isRegistryProjection(rel, [c])) { projected++; continue }
587
+ const got = sha16File(resolve(REPO, rel))
588
+ if (got === null) {
589
+ failed++
590
+ const msg = `能力 ${id} 包完整性:${rel} 文件缺失(期望 ${want})`
591
+ line('FAIL', msg)
592
+ record('error', 'pkg-file-missing', msg, `该文件不在当前代码树里(基准=${basis})—— 请 git pull 或重跑安装器`)
593
+ } else if (got !== String(want)) {
594
+ failed++
595
+ const msg = `能力 ${id} 包完整性:${rel} sha 不符(期望 ${want} vs 实际 ${got})`
596
+ line('FAIL', msg)
597
+ record('error', 'pkg-sha-mismatch', msg, `文件已被改动/降级(基准=${basis})—— 回退该文件或重跑安装器刷新记录`)
598
+ }
599
+ }
600
+ if (failed) continue
601
+ if (empty) {
602
+ const msg = `能力 ${id} 有 ${empty} 个文件 sha 为空(未校验)`
603
+ line('WARN', msg)
604
+ record('warn', 'pkg-sha-empty', msg, '服务端清单/装机记录补全 sha 后复检')
605
+ continue
606
+ }
607
+ line('PASS', `能力 ${id} v${d.version || c.version || '?'} 包完整性:${Object.keys(expected).length - projected} 个文件 sha 一致(基准=${basis})`
608
+ + (projected ? `;另有 ${projected} 个注册表投影差异按设计跳过(⑤)` : ''))
609
+ }
610
+ }
611
+ checkPackIntegrity()
612
+
613
+ // ── 报告 ──────────────────────────────────────────────────────────────
614
+ // --fix 能自动处理的 code;不在集合里的 error 修完仍然存在 ⇒ 必须非零退出(门禁不可被绕过)
615
+ const AUTO_FIXABLE = new Set(['dep-missing', 'dep-foreign', 'row-retired', 'row-duplicate', 'row-required-missing'])
616
+ const errs = problems.filter((p) => p.level === 'error')
617
+ const warns = problems.filter((p) => p.level === 'warn')
618
+ const remaining = FIX ? errs.filter((p) => !AUTO_FIXABLE.has(p.code)) : errs
619
+ /** 报告行:人类模式逐行打印;`--json` 模式原样放进 JSON(升级器读它做摘要,不再解析文本)。 */
620
+ const reportLines = []
621
+ const say = (line = '') => reportLines.push(line)
622
+ say(`[doctor] profile = ${PROFILE_DIR}`)
623
+ say(`[doctor] 规范插件根 = ${CANONICAL_ROOTS[0]}`)
624
+ say(`[doctor] 备份根 = ${BACKUP_DIR}`)
625
+ say(`[doctor] 包完整性 = ${INSTALL_JSON}`)
626
+ say(`[doctor] 模式 = ${FIX ? '--fix(自动修复)' : '只报告(未给 --fix)'}`
627
+ + `${FIX && IS_REAL_PROFILE && ALLOW_PACKAGE_MANAGER ? ' + 包管理器已显式授权(--allow-package-manager)' : ''}`)
628
+ for (const [tag, msg] of packLines) say(` [${tag}] ${msg}`)
629
+ if (problems.length === 0) {
630
+ say(' ✅ 体检通过:依赖路径规范、无重复挂载、无退役行、必需行齐全')
631
+ } else {
632
+ for (const p of problems) {
633
+ if (p.printed) continue // 包完整性一节已自行打印([PASS]/[WARN]/[FAIL]),避免重复
634
+ const mark = FIX && AUTO_FIXABLE.has(p.code) && p.level === 'error' ? '🔧已修' : (p.level === 'error' ? '✗' : '⚠')
635
+ say(` ${mark} [${p.code}] ${p.msg}\n → ${p.fix}`)
636
+ }
637
+ }
638
+ for (const f of fixes) say(` 🔧 ${f}`)
639
+ // 空转抑制:内容与现文件逐字节相同 ⇒ 不写、不产备份(如实打印,别让"没动"看起来像"动了")
640
+ for (const s of skippedWrites) say(` ⏭ 内容无变化,跳过备份:${s}`)
641
+
642
+ // 启动冒烟:人类模式随报告打印;JSON 模式只把结论塞进 JSON(否则 stdout 会掺进非 JSON 文本)
643
+ let bootcheck = null
644
+ if (RUN_BOOTCHECK && (FIX || problems.length === 0)) {
645
+ try {
646
+ const out = execFileSync('node', [join(REPO, 'scripts', 'plugin-bootcheck.mjs')], { encoding: 'utf8', timeout: 600000 })
647
+ const line = (out.split('\n').find((l) => l.includes('PASS') || l.includes('FAIL')) || '(无结论)').trim()
648
+ bootcheck = { ok: line.includes('PASS'), line }
649
+ say(`[doctor] 启动冒烟(plugin-bootcheck):${line}`)
650
+ } catch (error) {
651
+ const line = String(error.stdout || error.message).split('\n').slice(-4).join(' | ')
652
+ bootcheck = { ok: false, line }
653
+ say(`[doctor] 启动冒烟(plugin-bootcheck):✗ ${line}`)
654
+ }
655
+ }
656
+
657
+ if (errs.length > 0 && !FIX) {
658
+ say(`\n[doctor] 发现 ${errs.length} 个致命问题、${warns.length} 个警告 —— 加 --fix 自动修复,或按上面提示人工处理`)
659
+ }
660
+ if (remaining.length > 0 && FIX) {
661
+ say(`\n[doctor] 仍有 ${remaining.length} 个问题无法自动修复(${remaining.map((p) => p.code).join(', ')})——请按提示人工处理`)
662
+ }
663
+ const exitCode = ((errs.length > 0 && !FIX) || remaining.length > 0 || (bootcheck && !bootcheck.ok)) ? 1 : 0
664
+
665
+ if (JSON_OUT) {
666
+ console.log(JSON.stringify({
667
+ ok: exitCode === 0,
668
+ profile: PROFILE_DIR,
669
+ realProfile: IS_REAL_PROFILE,
670
+ backupDir: BACKUP_DIR,
671
+ callerLog: join(BACKUP_DIR, 'doctor-callers.jsonl'),
672
+ fix: FIX,
673
+ allowPackageManager: ALLOW_PACKAGE_MANAGER,
674
+ problems,
675
+ fixPlan,
676
+ fixes,
677
+ skippedWrites,
678
+ errCodes: errs.map((p) => p.code),
679
+ warnCodes: warns.map((p) => p.code),
680
+ bootcheck,
681
+ reportLines,
682
+ }, null, 2))
683
+ process.exit(exitCode)
684
+ }
685
+ for (const line of reportLines) console.log(line)
686
+ process.exit(exitCode)