@whyshu/argent-engine 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -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 +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -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 +378 -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/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env python3
2
+ """模型凭证体检(**装机红灯门禁** + 流程/客户机预检共用)—— 2026-09-29 事故固化。
3
+
4
+ 为什么要有它(真实事故):
5
+ 2026-09-14 whyshu.com 把 `JWT_SECRET` **硬切换**(无灰度,见 whyshu-svr docs/jwt-secret-rotation.md)
6
+ ⇒ 所有存量问述 token 当场失效。客户机上那把 2026-08-22 签发的 token **还在**、
7
+ `exp` 还写着 2026-11-20,看起来一切正常;而 09-28 生产切到客户机后,
8
+ 2026-09-29 北京 14:03 的日报第一次真正调模型 ⇒ 每一步都是 `DeepSeek API error (HTTP 401)`,
9
+ 角色会话空转、票据「停滞 → 回收 → 重派」循环,**没有任何告警** —— 唯一信号是"日报没来"。
10
+ 两个教训固化在这里:
11
+ ① **"配了" ≠ "能用"**:装机必须同时查"有没有"和"活不活";
12
+ ② **一个都没有 = 红灯**:装完也是坏的,不能绿灯放行。
13
+
14
+ 判据(模式感知,不猜):
15
+ 1. 读 profile 组合补丁(`profiles/<profile>/cordis.patch.yml`)判定**当前可用 provider**
16
+ (`llm-deepseek`=官方直连用 DEEPSEEK_API_KEY;`llm-whyshu`=问述代理用 WHYSHU_API_KEY);
17
+ 没有该段 = DeepSeek 官方直连模式(必需 DEEPSEEK_API_KEY)。
18
+ 2. 取凭证(**只读、不打印值**):环境变量 → dsh credentials 的 `refs:` 段 →
19
+ `argent/config.json` 的 `apiKey` → `argent/whyshu.env`。
20
+ 3. 离线死证(无需联网):JWT 的 `exp` 已过;或用**历史默认密钥**验签命中
21
+ (= 2026-09-14 轮换前签发的,必然已被服务端拒绝)。
22
+ 4. 在线探针(`--online`):`GET <provider>/models`;200 = 活;401/403 = **已失效(红灯)**;
23
+ 其余/网络不可达 = 黄灯(**不冤枉凭证**)。
24
+
25
+ 红线:
26
+ · 只读:不写任何配置、不发模型请求(只打一次 `/models` 清单接口)、不打印凭证值(只给 sha256 指纹)。
27
+ · 网络不可达**不是**凭证问题 ⇒ 只判黄灯,不判红灯。
28
+
29
+ 用法:
30
+ python3 scripts/check-model-auth.py # 离线判定(有没有 + 离线死证)
31
+ python3 scripts/check-model-auth.py --online # 追加在线探针(装机默认走这条)
32
+ python3 scripts/check-model-auth.py --json # 机器可读(installer/预检 消费)
33
+ python3 scripts/check-model-auth.py --dsh-home ~/.dsh
34
+ 退出码:0 = 无红灯;1 = 有红灯(凭证缺失/已失效);2 = 脚本自身出错。
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import argparse
40
+ import base64
41
+ import hashlib
42
+ import hmac
43
+ import json
44
+ import os
45
+ import pathlib
46
+ import re
47
+ import sys
48
+ import urllib.error
49
+ import urllib.request
50
+
51
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent / "lib"))
52
+ from stdio_utf8 import use_utf8_stdio # noqa: E402 出口编码钉 UTF-8(中文 Windows 的 GBK 管道下非 ASCII 会崩)
53
+
54
+ # 2026-09-14 轮换前的**默认**密钥(已作废、公开在运维文档里)。它不能用来签发任何东西,
55
+ # 只用于一条离线判据:命中 ⇒ 这把 token 必定是轮换前签发的 ⇒ 服务端一定拒绝。
56
+ HISTORICAL_DEFAULT_SECRET = "whyshu-dev-secret-change-me"
57
+
58
+ WHYSHU_PROBE = "https://whyshu.com/api/argent/v1/models"
59
+ DEEPSEEK_PROBE = "https://api.deepseek.com/v1/models"
60
+ TIMEOUT_DEFAULT = 12
61
+
62
+ GROUP = "模型凭证"
63
+
64
+
65
+ # ── 输出 ──────────────────────────────────────────────────────────────────────
66
+
67
+ class Checks:
68
+ """与 customer-preflight.py 的 Checks 同形状(id/level/title/detail/fix),便于直接搬运。"""
69
+
70
+ def __init__(self) -> None:
71
+ self.items: list[dict] = []
72
+ self.mode = "unknown" # whyshu / deepseek / unknown(= 默认 provider,兼容旧消费方)
73
+ self.providers: list[str] = [] # 当前可用 provider(deepseek-official / whyshu)
74
+
75
+ def add(self, cid: str, level: str, title: str, detail: str = "", fix: str = "") -> None:
76
+ self.items.append({"id": cid, "group": GROUP, "level": level,
77
+ "title": title, "detail": detail, "fix": fix})
78
+
79
+ @property
80
+ def level(self) -> str:
81
+ if any(i["level"] == "fail" for i in self.items):
82
+ return "fail"
83
+ if any(i["level"] == "warn" for i in self.items):
84
+ return "warn"
85
+ return "ok"
86
+
87
+
88
+ def fingerprint(secret: str) -> str:
89
+ """凭证指纹:sha256 前 12 位(与 docs/jwt-secret-rotation.md 同一口径)。永不打印原值。"""
90
+ return "sha256:" + hashlib.sha256(secret.encode()).hexdigest()[:12]
91
+
92
+
93
+ # ── 配置 / 凭据读取(全部只读,容忍半坏机器)────────────────────────────────────
94
+
95
+ def settings_path(dsh_home: pathlib.Path) -> pathlib.Path:
96
+ return dsh_home / "settings.yaml"
97
+
98
+
99
+ def detect_providers(dsh_home: pathlib.Path) -> tuple[list[str], str, str]:
100
+ """从 profile 组合补丁判定**当前可用 provider**(2026-09-30 起取代 settings.yaml 判据)。
101
+
102
+ 为什么改:模型/provider 现在由 dsh 的**模型选择器**决定,两个 provider 并存:
103
+ · `llm-deepseek`(官方直连,provider id `deepseek-official`,用 DEEPSEEK_API_KEY)
104
+ · `llm-whyshu`(问述代理,provider id `whyshu`,用 WHYSHU_API_KEY)
105
+ 旧逻辑读 settings.yaml 的 `llm-deepseek` 段 —— 那个文件已不再由 Argent 维护(用户明确要求不写它)。
106
+ 返回 (providers, default_provider, 证据)。
107
+ """
108
+ prof = os.environ.get("DSH_PROFILE", "web")
109
+ patch = dsh_home / "profiles" / prof / "cordis.patch.yml"
110
+ if not patch.exists():
111
+ profs = sorted([d.name for d in (dsh_home / "profiles").iterdir() if (d / "cordis.patch.yml").exists()]) if (dsh_home / "profiles").exists() else []
112
+ if profs:
113
+ patch = dsh_home / "profiles" / profs[0] / "cordis.patch.yml"
114
+ try:
115
+ text = patch.read_text(encoding="utf-8", errors="replace")
116
+ except OSError:
117
+ return [], "", f"{patch} 不存在 ⇒ 无法判定可用 provider(按旧判据回落)"
118
+ provs = []
119
+ if re.search(r"(?m)^\s*-?\s*id:\s*llm-deepseek\s*$", text):
120
+ provs.append("deepseek-official")
121
+ if re.search(r"(?m)^\s*-?\s*id:\s*llm-whyshu\s*$", text):
122
+ provs.append("whyshu")
123
+ m = re.search(r"(?m)^\s*provider:\s*(\S+)\s*$", text)
124
+ default = m.group(1) if m else ""
125
+ return provs, default, f"{patch.relative_to(dsh_home)} 声明 provider={provs or '(无)'},默认={default or '(未设)'}"
126
+
127
+
128
+ def detect_mode(dsh_home: pathlib.Path) -> tuple[str, str]:
129
+ """判定计费模式。返回 (mode, 证据文案)。mode ∈ whyshu / deepseek / unknown。"""
130
+ p = settings_path(dsh_home)
131
+ try:
132
+ text = p.read_text(encoding="utf-8", errors="replace")
133
+ except OSError:
134
+ return "deepseek", f"{p} 不存在 ⇒ 官方直连(无代理段)"
135
+ if re.search(r"(?m)^llm-deepseek:\s*$", text):
136
+ return "whyshu", f"{p} 有 llm-deepseek 段 ⇒ 走问述代理"
137
+ if re.search(r"(?m)^agent-default-model:", text):
138
+ return "deepseek", f"{p} 无 llm-deepseek 段 ⇒ DeepSeek 官方直连"
139
+ return "unknown", f"{p} 既无 llm-deepseek 也无 agent-default-model ⇒ 判不出模型路径"
140
+
141
+
142
+ def read_refs(cred_file: pathlib.Path) -> dict[str, str]:
143
+ """只读 credentials 的 `refs:` 段(三段格式:version/refs/records)。
144
+
145
+ ⛔ 绝不写这个文件(2026-09-14 事故:行操作把它写坏 ⇒ dsh 起不来);
146
+ 也只认 refs 段,records 段一律不碰。
147
+ """
148
+ out: dict[str, str] = {}
149
+ try:
150
+ text = cred_file.read_text(encoding="utf-8", errors="replace")
151
+ except OSError:
152
+ return out
153
+ in_refs = False
154
+ for line in text.splitlines():
155
+ if re.match(r"^refs:\s*$", line):
156
+ in_refs = True
157
+ continue
158
+ if in_refs and line and not line[0].isspace():
159
+ break
160
+ if in_refs:
161
+ m = re.match(r"^\s+([A-Za-z0-9_.\-/]+):\s*(.+?)\s*$", line)
162
+ if m:
163
+ out[m.group(1)] = m.group(2).strip().strip('"').strip("'")
164
+ return out
165
+
166
+
167
+ def read_env_file(path: pathlib.Path, key: str) -> str:
168
+ try:
169
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
170
+ if line.startswith(key + "="):
171
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
172
+ except OSError:
173
+ pass
174
+ return ""
175
+
176
+
177
+ def read_config_key(dsh_home: pathlib.Path) -> str:
178
+ try:
179
+ doc = json.loads((dsh_home / "argent" / "config.json").read_text(encoding="utf-8"))
180
+ except (OSError, ValueError):
181
+ return ""
182
+ return str(doc.get("apiKey") or "").strip()
183
+
184
+
185
+ def collect(dsh_home: pathlib.Path) -> dict[str, tuple[str, str]]:
186
+ """收集两把凭证。返回 {逻辑名: (值, 来源)};值可能为空串(= 未配置)。"""
187
+ refs = read_refs(dsh_home / ".credentials.yaml")
188
+ home = pathlib.Path(os.path.expanduser("~"))
189
+ alt = read_refs(home / ".dsh" / ".credentials.yaml") if dsh_home != home / ".dsh" else {}
190
+
191
+ def pick(*cands: tuple[str, str]) -> tuple[str, str]:
192
+ for value, src in cands:
193
+ if value:
194
+ return value, src
195
+ return "", ""
196
+
197
+ deepseek = pick(
198
+ (os.environ.get("DEEPSEEK_API_KEY", ""), "环境变量 DEEPSEEK_API_KEY"),
199
+ (refs.get("DEEPSEEK_API_KEY", ""), f"{dsh_home}/.credentials.yaml"),
200
+ (alt.get("DEEPSEEK_API_KEY", ""), "~/.dsh/.credentials.yaml"),
201
+ )
202
+ whyshu = pick(
203
+ (os.environ.get("WHYSHU_API_KEY", ""), "环境变量 WHYSHU_API_KEY"),
204
+ (refs.get("WHYSHU_API_KEY", ""), f"{dsh_home}/.credentials.yaml"),
205
+ (alt.get("WHYSHU_API_KEY", ""), "~/.dsh/.credentials.yaml"),
206
+ (read_config_key(dsh_home), f"{dsh_home}/argent/config.json"),
207
+ (read_env_file(dsh_home / "argent" / "whyshu.env", "WHYSHU_API_KEY"),
208
+ f"{dsh_home}/argent/whyshu.env"),
209
+ )
210
+ return {"deepseek": deepseek, "whyshu": whyshu}
211
+
212
+
213
+ # ── 离线死证 ──────────────────────────────────────────────────────────────────
214
+
215
+ def jwt_payload(token: str) -> dict | None:
216
+ parts = token.split(".")
217
+ if len(parts) != 3:
218
+ return None
219
+ try:
220
+ seg = parts[1] + "=" * (-len(parts[1]) % 4)
221
+ doc = json.loads(base64.urlsafe_b64decode(seg))
222
+ return doc if isinstance(doc, dict) else None
223
+ except Exception: # noqa: BLE001
224
+ return None
225
+
226
+
227
+ def dead_reason(token: str, now: int) -> str:
228
+ """离线判定"必然已死"的原因;判不出来返回空串(不代表活着)。"""
229
+ payload = jwt_payload(token)
230
+ if payload is None:
231
+ return ""
232
+ exp = payload.get("exp")
233
+ if isinstance(exp, (int, float)) and exp < now:
234
+ return f"token 已于 {_fmt(exp)} 过期(exp 判据)"
235
+ parts = token.split(".")
236
+ try:
237
+ expected = hmac.new(HISTORICAL_DEFAULT_SECRET.encode(),
238
+ f"{parts[0]}.{parts[1]}".encode(), hashlib.sha256).digest()
239
+ seg = parts[2] + "=" * (-len(parts[2]) % 4)
240
+ if hmac.compare_digest(expected, base64.urlsafe_b64decode(seg)):
241
+ return ("签名用的是 2026-09-14 轮换前的旧默认密钥 ⇒ 服务端必定 401"
242
+ "(见 docs/jwt-secret-rotation.md)")
243
+ except Exception: # noqa: BLE001
244
+ pass
245
+ return ""
246
+
247
+
248
+ def _fmt(ts: float) -> str:
249
+ import datetime as dt
250
+ return dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
251
+
252
+
253
+ # ── 在线探针 ──────────────────────────────────────────────────────────────────
254
+
255
+ def probe(url: str, token: str, timeout: int) -> tuple[str, str]:
256
+ """探一次模型清单接口。返回 (verdict, detail),verdict ∈ ok / dead / unknown。"""
257
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
258
+ try:
259
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
260
+ return ("ok", f"HTTP {resp.status}(凭证可用)") if resp.status == 200 else \
261
+ ("unknown", f"HTTP {resp.status}(非预期状态,未据此判死)")
262
+ except urllib.error.HTTPError as e:
263
+ if e.code in (401, 403):
264
+ body = ""
265
+ try:
266
+ body = e.read().decode("utf-8", "replace")[:120]
267
+ except Exception: # noqa: BLE001
268
+ pass
269
+ return "dead", f"HTTP {e.code}{(':' + body) if body else ''}"
270
+ return "unknown", f"HTTP {e.code}(非鉴权错误,未据此判死)"
271
+ except Exception as e: # noqa: BLE001
272
+ return "unknown", f"未探到({type(e).__name__}: {str(e)[:80]})—— 网络不可达不判死"
273
+
274
+
275
+ # ── 判定 ──────────────────────────────────────────────────────────────────────
276
+
277
+ LOGIN_FIX = ("在客户机浏览器打开 http://127.0.0.1:<port> → 设置 → Argent → "
278
+ "「✦ 一键登录问述科技账号」(设备流:用问述账号手机号/微信授权一次)")
279
+ SWITCH_FIX = ("或改走官方直连:`bash scripts/provider-switch.sh deepseek`"
280
+ "(需本机已有可用的 DEEPSEEK_API_KEY)")
281
+
282
+
283
+ def evaluate(dsh_home: pathlib.Path, online: bool, timeout: int, now: int | None = None) -> Checks:
284
+ import time
285
+ now = int(now if now is not None else time.time())
286
+ chk = Checks()
287
+ provs, default_prov, prov_why = detect_providers(dsh_home)
288
+ chk.providers = provs
289
+ if provs:
290
+ # mode = **默认 provider**(兼容旧消费方:whyshu / deepseek)
291
+ chk.mode = "whyshu" if default_prov == "whyshu" else ("deepseek" if default_prov == "deepseek-official" else "unknown")
292
+ why = prov_why
293
+ else:
294
+ mode, why = detect_mode(dsh_home)
295
+ chk.mode = mode
296
+ label = {"whyshu": "问述代理(扣积分)", "deepseek": "DeepSeek 官方直连", "unknown": "未知"}[chk.mode]
297
+
298
+ if provs:
299
+ pretty = "、".join({"deepseek-official": "官方直连(deepseek-official)", "whyshu": "问述代理(whyshu)"}.get(x, x) for x in provs)
300
+ chk.add("auth.mode", "ok", f"当前可用 provider:{pretty}|默认:{label}", why)
301
+ else:
302
+ chk.add("auth.mode", "ok" if chk.mode != "unknown" else "warn", f"当前可用 provider:{label}(未从组合判定)", why)
303
+
304
+ # ── 两把凭证的存在性与离线死证 ──
305
+ # 级别规则(**这里最容易误伤**):与当前模式匹配的那把缺失 = 红灯;两把都没有 = 双红灯
306
+ # (用户口径:装机时"一个都没配"必须红灯);**非必需的那把**缺失只是黄灯 ——
307
+ # 直连模式的机器本来就不需要问述 token,判红会让 flow_preflight 把流程全停。
308
+ # 并存的 provider ⇒ **各自的凭据都必需**(两个都得能用);无法判定 provider 时按旧判据回落
309
+ needs: set[str] = set()
310
+ for x in (provs or ([chk.mode] if chk.mode in ("whyshu", "deepseek") else [])):
311
+ needs.add("deepseek" if x in ("deepseek-official", "deepseek") else ("whyshu" if x == "whyshu" else x))
312
+ creds = collect(dsh_home)
313
+ present = {n: bool(v) for n, (v, _s) in creds.items()}
314
+ any_present = any(present.values())
315
+ detail = {}
316
+ for name, human in (("deepseek", "DEEPSEEK_API_KEY"), ("whyshu", "WHYSHU_API_KEY")):
317
+ value, src = creds[name]
318
+ if not value:
319
+ required_here = (name in needs) or (not any_present)
320
+ detail[name] = {"present": False, "dead": "", "value": ""}
321
+ chk.add(f"auth.cred.{name}", "fail" if required_here else "warn",
322
+ f"{human} 未配置" if required_here else f"{human} 未配置(当前模式不需要)",
323
+ "环境变量与 dsh credentials / config.json 都取不到",
324
+ "见 `python3 scripts/check-model-auth.py --help`(两把配任意一把即可,但必须与计费模式匹配)"
325
+ if required_here else "")
326
+ continue
327
+ dead = dead_reason(value, now) if name == "whyshu" else ""
328
+ required_here = (name in needs)
329
+ detail[name] = {"present": True, "dead": dead, "value": value}
330
+ # 死证也分「必需」与「用不到」:**当前模式用不到的那把死了只是黄灯** ——
331
+ # 2026-09-29 实测(客户机切直连后,旧的死问述 token 还在 credentials 里):
332
+ # 若判红,flow_preflight 会把**正常工作的**流程全停(假红比漏报更贵)。
333
+ chk.add(f"auth.cred.{name}",
334
+ "fail" if (dead and required_here) else ("warn" if dead else "ok"),
335
+ (f"{human} 已失效" if required_here else f"{human} 已失效(当前模式不需要)") if dead
336
+ else f"{human} 已配置",
337
+ (f"{dead}|" if dead else "") + f"来源 {src}|长度 {len(value)}|指纹 {fingerprint(value)}",
338
+ ("重新登录换新 token:" + LOGIN_FIX) if dead else "")
339
+
340
+ NEED_LABEL = {"whyshu": "问述代理", "deepseek": "官方直连"}
341
+ # ── 必需项:**并存 provider 各自的凭据都必须可用** ──
342
+ if needs:
343
+ for need in sorted(needs):
344
+ d = detail.get(need, {})
345
+ other = "deepseek" if need == "whyshu" else "whyshu"
346
+ other_ok = detail.get(other, {}).get("present") and not detail.get(other, {}).get("dead")
347
+ if not d.get("present"):
348
+ chk.add("auth.required", "fail",
349
+ f"provider「{NEED_LABEL.get(need, need)}」所需的凭证缺失:{'WHYSHU_API_KEY' if need == 'whyshu' else 'DEEPSEEK_API_KEY'}",
350
+ f"另一把({other}){'已配置' if other_ok else '也没配'}",
351
+ (SWITCH_FIX if other_ok else LOGIN_FIX))
352
+ elif d.get("dead"):
353
+ chk.add("auth.required", "fail", f"provider「{NEED_LABEL.get(need, need)}」所需的凭证**已失效**",
354
+ d["dead"],
355
+ ((SWITCH_FIX if other_ok else "") + (";" if other_ok else "") + "或重新登录:" + LOGIN_FIX))
356
+ else:
357
+ chk.add("auth.required", "ok", f"provider「{NEED_LABEL.get(need, need)}」所需凭证在位", f"指纹 {fingerprint(d['value'])}")
358
+ else:
359
+ # ⚠️ 2026-09-30 修复:此前误写成 `for ... else`(Python 的 for-else 只要不 break 就执行)
360
+ # ⇒ 即便已正确判定出 provider,也会**每次误报**"判不出计费模式"黄灯。现归属 `if needs:`。
361
+ usable = [n for n, v in detail.items() if v.get("present") and not v.get("dead")]
362
+ if not any(v.get("present") for v in detail.values()):
363
+ chk.add("auth.required", "fail", "模型凭证一个都没有配置(红灯)",
364
+ "既没有 DEEPSEEK_API_KEY,也没有 WHYSHU_API_KEY",
365
+ LOGIN_FIX + ";" + SWITCH_FIX)
366
+ elif not usable:
367
+ chk.add("auth.required", "fail", "未判定出可用 provider,且**已配的凭证都已失效**",
368
+ "两把都在,但没有一把离线可判为可用", LOGIN_FIX)
369
+ else:
370
+ chk.add("auth.required", "warn", "未从组合判定出可用 provider,跳过「必需项」判定",
371
+ "未读到 profiles/<profile>/cordis.patch.yml 的 provider 实例(回落旧判据也判不出)",
372
+ "确认 profile 组合补丁存在且含 llm-deepseek / llm-whyshu 实例后重跑")
373
+
374
+ # ── 在线探针(可选)──
375
+ if online:
376
+ for name, url, human in (("deepseek", DEEPSEEK_PROBE, "DeepSeek 官方直连"),
377
+ ("whyshu", WHYSHU_PROBE, "问述代理")):
378
+ d = detail.get(name, {})
379
+ if not d.get("present"):
380
+ continue
381
+ if d.get("dead"):
382
+ chk.add(f"auth.live.{name}", "warn", f"{human}:跳过探针(离线已判死)", d["dead"])
383
+ continue
384
+ verdict, why2 = probe(url, d["value"], timeout)
385
+ level = {"ok": "ok", "dead": "fail", "unknown": "warn"}[verdict]
386
+ title = {"ok": f"{human} 探针通过", "dead": f"{human} 探针判死:凭证已失效",
387
+ "unknown": f"{human} 未探到(不判死)"}[verdict]
388
+ chk.add(f"auth.live.{name}", level, title, why2,
389
+ ("重新登录换新 token:" + LOGIN_FIX) if level == "fail" else "")
390
+ else:
391
+ chk.add("auth.live", "warn", "未做在线探针(只查了「有没有」)",
392
+ "加 --online 真打一次 /models:本事故的坑正是「配了但已失效」",
393
+ "python3 scripts/check-model-auth.py --online")
394
+
395
+ return chk
396
+
397
+
398
+ # ── CLI ──────────────────────────────────────────────────────────────────────
399
+
400
+ def main(argv=None) -> int:
401
+ use_utf8_stdio() # 唯一出口编码判据:见 scripts/lib/stdio_utf8.py 的事故说明
402
+ ap = argparse.ArgumentParser(description="模型凭证体检(装机红灯门禁)")
403
+ ap.add_argument("--dsh-home", default=os.environ.get("DSH_HOME") or "~/.dsh")
404
+ ap.add_argument("--online", action="store_true", help="追加在线探针(/models,401/403 判死)")
405
+ ap.add_argument("--json", action="store_true")
406
+ ap.add_argument("--timeout", type=int, default=TIMEOUT_DEFAULT)
407
+ ap.add_argument("--quiet", action="store_true", help="只输出红灯(装机脚本里用)")
408
+ a = ap.parse_args(argv)
409
+
410
+ dsh_home = pathlib.Path(os.path.expanduser(a.dsh_home))
411
+ try:
412
+ chk = evaluate(dsh_home, a.online, a.timeout)
413
+ except Exception as e: # noqa: BLE001
414
+ print(f"✗ check-model-auth 自身出错:{type(e).__name__}: {e}", file=sys.stderr)
415
+ return 2
416
+
417
+ fails = [i for i in chk.items if i["level"] == "fail"]
418
+ if a.json:
419
+ print(json.dumps({"level": chk.level, "mode": chk.mode, "providers": chk.providers, "online": a.online,
420
+ "dshHome": str(dsh_home), "checks": chk.items}, ensure_ascii=False, indent=2))
421
+ else:
422
+ icon = {"ok": "\x1b[32m✓\x1b[0m", "warn": "\x1b[33m⚠\x1b[0m", "fail": "\x1b[1;31m✗\x1b[0m"}
423
+ if not a.quiet:
424
+ print("\x1b[1;35m✦\x1b[0m 模型凭证体检(装机红灯门禁)")
425
+ for i in chk.items:
426
+ if a.quiet and i["level"] != "fail":
427
+ continue
428
+ print(f" {icon[i['level']]} {i['title']}")
429
+ if i["detail"]:
430
+ print(f" {i['detail']}")
431
+ if i["fix"] and i["level"] in ("fail", "warn"):
432
+ print(f" ↳ {i['fix']}")
433
+ if fails:
434
+ print(f" \x1b[1;31m⛔ 红灯 {len(fails)} 项\x1b[0m:装完这台机器任何角色会话都会在"
435
+ f"**第一步**失败(模型 401),流程只会空转(停滞→回收→重派),且目前**不会告警**。")
436
+ elif not a.quiet:
437
+ warn_n = sum(1 for i in chk.items if i["level"] == "warn")
438
+ print(f" 结论:{'绿灯' if not warn_n else f'绿灯({warn_n} 项黄灯待确认)'}")
439
+ return 1 if fails else 0
440
+
441
+
442
+ if __name__ == "__main__":
443
+ sys.exit(main())