@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,411 @@
1
+ #!/usr/bin/env python3
2
+ """env_probe —— 认领前的**环境预检**(只读):浏览器客户端到底能不能干活。
3
+
4
+ ## 为什么要有它(2026-09-23 实测)
5
+ 紫鸟客户端"进程在、Bridge 通、但**未登录**"时,FULL 的认领**照常把票派给角色**,
6
+ 角色跑进去才发现环境不可用 ⇒ 记 blocked。代价有两笔:
7
+ ① 白烧一轮派发与角色算力;
8
+ ② 这些票的**认领租约到期后变成「停滞」**,而停滞票当时没有任何自动回收路径
9
+ ⇒ 票静静躺着,还占着同店铺的批次(那天"可派发 6 却连续 4 轮无票可认领")。
10
+ 本探针把这类"环境不可用"提前到**认领之前**发现:不可用就不认领,日志一句话说清原因。
11
+
12
+ 语义:exit 0 = 可用;exit 2 = 不可用(**不**重试、**不**认领本轮);exit 3 = 流程未声明探针(无需探)。
13
+ 只读:`ziniao-cli store list` 不点浏览器、不改任何数据。
14
+
15
+ ## 回退判据(2026-10-11 加,**只放宽这一处认领前闸门**)
16
+ 生产现场实测:客户机 FULL 触发器**每轮**都被本预检挡下("Bridge 正常但没有任何店铺")、
17
+ **当天 0 票处理**;而同一时刻浏览器客户端在跑、**本地 1 店店铺窗口开着且 CDP 可达**
18
+ (`curl http://127.0.0.1:32826/json/version` → `Chrome/142…`),用
19
+ `orchestrator.py --mode full --cdp-url http://127.0.0.1:32826` 手工跑**同一批 6 票全成功**。
20
+ ⇒ 判据过窄:云端店铺列表(`store list` 的 `data.items`)拉不到 ≠ 环境不可用。
21
+
22
+ 判据是**三层**(顺序固定;第 0 层非空 ⇒ 直接可用,⛔ 两层回退一次都不探):
23
+
24
+ | 层 | 判据 | 实现 | 现场实测(2026-10-11) |
25
+ |---|---|---|---|
26
+ | 0 | 云端店铺列表**非空** ⇒ 可用 | 既有(**逐字不变**) | 正常路径 |
27
+ | ① | 本机**已运行的店铺窗口**(`getRunningInfo`) | `scripts/lib/webdriver_ipc.py` | 上午现场有效(1 店窗口在跑,`32826` 可连) |
28
+ | ② | 本机 **CDP 端口 + `chrome_<store_id>` 归属 + 页面可达** | `scripts/lib/cdp_ports.py` | **恢复窗口现场:`getBrowserList`=0、`getRunningInfo` 也是 0**(窗口明明在跑)⇒ 真救回来的是这一层:实测端口 `[13018, 27893]`,1 店=27893 / 2 店=13018 |
29
+
30
+ ⇒ 第 ① 层**不是万能的**:同一天里 `getRunningInfo` 也返回 0,而窗口在跑、CDP 可连。
31
+ 第 ② 层才是"窗口在跑"这件事最贴近事实的证据 —— 端口 ∧ 进程命令行里的 user-data 目录
32
+ `…/chrome_<store_id>` **一一对应到店铺**(⛔ 归属确认不到就判不可用,绝不猜是哪家店)。
33
+
34
+ 保守边界(回归钉住,见 `scripts/tests/env-probe-cdp-port-fallback.test.py`):
35
+ · 列表为空 ∧ **两层都没有**窗口/端口 ⇒ 仍判**不可用**(文案与改动前**逐字一致**);
36
+ · **有端口但归属不上任何在册店铺 ⇒ 不可用**(⛔ 不猜、⛔ 不取第一个端口);
37
+ · 两层探针**本身失败/超时** ⇒ 仍判不可用,且文案写明"回退判据不可用(原因)"
38
+ (⛔ "查不到"绝不当"可用";⛔ 绝不上抛:预检崩掉比判不可用更糟);
39
+ · `store list` 正常且非空 ⇒ 行为**逐字不变**(不触发任何回退、零次额外探针);
40
+ · 只对**该流程在册/声明的店铺**判(`~/.dsh/argent/stores.json`;⛔ 不遍历无关店铺);
41
+ · ⛔ **只读**:⛔ 不开窗、⛔ 不关窗、⛔ 不写 `cdp-port-map.json`(那是
42
+ `scripts/ensure-store-windows.py` 的职责)、⛔ 不改任何状态;
43
+ · ⛔ "窗口已开" **不等于** "账号已授权" —— 该等价关系**只**用于这一处认领前闸门,
44
+ 不得被别的判据(店铺归属、授权、开票资格…)引用。
45
+
46
+ 开关:`ARGENT_PREFLIGHT_LOCAL_WINDOW_FALLBACK=0`(`false`/`off`/`no` 亦可)**关闭两层回退**,
47
+ 默认**开启**(理由:回退只是把"能不能干活"的判据换成更贴近事实的那个证据,且所有保守边界都还在);
48
+ 更细的一层:`ARGENT_PREFLIGHT_CDP_PORT_FALLBACK=0` 只关第 ② 层(第 ① 层照旧)。
49
+ 流程 spec 也可写 `localWindowFallback: false`(两层一起关)/ `cdpPortFallback: false`(只关第 ② 层)。
50
+ """
51
+ from __future__ import annotations
52
+ import argparse, json, os, pathlib, subprocess, sys
53
+
54
+ HERE = pathlib.Path(__file__).resolve().parent
55
+ sys.path.insert(0, str(HERE))
56
+ sys.path.insert(0, str(HERE / 'lib'))
57
+ from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
58
+ import webdriver_ipc # noqa: E402 客户端 WebDriver 只读查询的**唯一实现**(⛔ 不另造客户端)
59
+ import cdp_ports # noqa: E402 CDP 端口发现/归属/page 探针的**唯一实现**(⛔ 不另造第二份)
60
+ PROBE_TIMEOUT = 45
61
+ # 默认清单命令(**唯一字面量**:多处默认值都引用它 —— 第二份字面量会被 engine-words 记成新增)
62
+ DEFAULT_STORE_CMD = ['ziniao-cli', 'store', 'list']
63
+ # 回退判据的开关(默认开;见模块 docstring 的"回退判据"段)。
64
+ LOCAL_WINDOW_FALLBACK_ENV = 'ARGENT_PREFLIGHT_LOCAL_WINDOW_FALLBACK'
65
+ # 第 ② 层(CDP 端口)的**更细**开关:默认开;关它只退掉第 ② 层(第 ① 层照旧)。
66
+ CDP_PORT_FALLBACK_ENV = 'ARGENT_PREFLIGHT_CDP_PORT_FALLBACK'
67
+ # 本地窗口查询超时(秒):比清单命令短 —— 它只是回退证据,不该把认领前的预算吃光。
68
+ LOCAL_WINDOW_TIMEOUT = 20.0
69
+ # 第 ② 层的单次探针超时(秒):`/json/version` 与 `/json/list` 都是本机回环只读,2s 足够。
70
+ CDP_PROBE_TIMEOUT = 2.0
71
+ # "没有任何店铺"的**既有文案**:回退未通过时必须**逐字**保持(改动前什么样,现在还什么样)。
72
+ NO_STORE_DETAIL = "Bridge 正常但**没有任何店铺**(未登录 / 账号无授权)"
73
+
74
+
75
+ def _off(raw: str) -> bool:
76
+ return str(raw or '').strip().lower() in ('0', 'false', 'off', 'no')
77
+
78
+
79
+ def local_window_fallback_enabled(spec: dict | None = None) -> bool:
80
+ """回退判据是否启用:**默认开**;env 关闭优先,其次流程 spec 的 `localWindowFallback:false`。
81
+
82
+ 为什么默认开:现场的根本代价是"**整天 0 票**",而回退只在"云端列表拿不到"时生效,
83
+ 且"本地没有窗口 / 查不到"仍然一律判不可用(保守边界全在)。要一键退回旧行为用
84
+ `ARGENT_PREFLIGHT_LOCAL_WINDOW_FALLBACK=0`(现场排障、复现旧日志时用)。
85
+ """
86
+ if (spec or {}).get('localWindowFallback') is False:
87
+ return False
88
+ return not _off(os.environ.get(LOCAL_WINDOW_FALLBACK_ENV, ''))
89
+
90
+
91
+ def cdp_port_fallback_enabled(spec: dict | None = None) -> bool:
92
+ """**第 ② 层**(CDP 端口 + 店铺归属)是否启用:默认开,且**从属于**粗开关。
93
+
94
+ 从属关系是有意的(回归⑤钉住):`ARGENT_PREFLIGHT_LOCAL_WINDOW_FALLBACK=0` ⇒ **两层回退都不走、
95
+ 零探针**、文案逐字旧常量 —— 现场排障时"一键回到改动前"必须仍然成立。
96
+ 只想关第 ② 层(第 ① 层照旧)时用 `ARGENT_PREFLIGHT_CDP_PORT_FALLBACK=0`,
97
+ 或流程 spec 写 `cdpPortFallback: false`。
98
+ """
99
+ if not local_window_fallback_enabled(spec):
100
+ return False
101
+ if (spec or {}).get('cdpPortFallback') is False:
102
+ return False
103
+ return not _off(os.environ.get(CDP_PORT_FALLBACK_ENV, ''))
104
+
105
+
106
+ def local_wd_port(spec: dict | None = None) -> int:
107
+ """本机 WebDriver IPC 端口:env `ZINIAO_WD_PORT` → spec `wdPort` → 官方默认 19515。"""
108
+ raw = os.environ.get('ZINIAO_WD_PORT') or (spec or {}).get('wdPort') or webdriver_ipc.DEFAULT_WD_PORT
109
+ try:
110
+ return int(raw)
111
+ except (TypeError, ValueError):
112
+ return webdriver_ipc.DEFAULT_WD_PORT
113
+
114
+
115
+ def _local_running_windows(port: int, timeout: float = LOCAL_WINDOW_TIMEOUT) -> dict:
116
+ """**只读**查本机"已运行的店铺窗口"(测试注入点)→ `webdriver_ipc.running_windows_probe`。
117
+
118
+ ⛔ 只读:只发 `getRunningInfo`(免凭据),⛔ 不开窗口、不点按钮、不改任何状态。
119
+ ⛔ **绝不上抛**(本函数的契约):异常一律折成 `{"ok": False, "error": "…"}` ——
120
+ "查不到"与"没有"必须分开(`error` 字段),且**预检绝不能因为回退这一段而崩掉**
121
+ (崩掉 = 认领前的整条链路报 traceback;那比"判不可用"更糟、更难排查)。
122
+ """
123
+ try:
124
+ probe = webdriver_ipc.running_windows_probe(port, timeout=timeout)
125
+ except Exception as exc: # noqa: BLE001
126
+ return {'ok': False, 'count': 0, 'summary': '',
127
+ 'error': f'{type(exc).__name__}: {exc}', 'port': port}
128
+ if not isinstance(probe, dict): # 契约外的返回值 ⇒ 当作"查不到"
129
+ return {'ok': False, 'count': 0, 'summary': '',
130
+ 'error': f'running_windows_probe 返回非 dict:{type(probe).__name__}', 'port': port}
131
+ return probe
132
+
133
+
134
+ def _fallback_detail(spec: dict, *, lead: str, summary: str, count: int) -> str:
135
+ """第 ① 层回退(本机已运行窗口)通过时的说明文案(**逐字不变**:既有回归按它断言)。"""
136
+ return (f"就绪(**回退判据**:`{_cmd_label(spec)}`{lead},"
137
+ f"但本地已运行 {count} 个窗口:{summary})")
138
+
139
+
140
+ def _cdp_fallback_detail(spec: dict, *, head: str, summary: str, count: int) -> str:
141
+ """第 ② 层回退(CDP 端口 + 归属)通过时的说明文案。
142
+
143
+ **必须**含「回退判据(CDP 端口)」与「店铺列表为空,但本机已运行属于 N 个店铺的窗口:…」
144
+ —— 回归按这几段逐字断言(⛔ 别改措辞而不改回归)。
145
+ """
146
+ return (f"就绪(**回退判据(CDP 端口)**:{head},但本机已运行属于 {count} 个店铺的窗口:"
147
+ f"{summary};依据 `{_cmd_label(spec)}`)")
148
+
149
+
150
+ def _engine_store_scope(spec: dict) -> tuple:
151
+ """第 ② 层要用的**既有事实源** → `(scope, error)`(**测试注入点**)。
152
+
153
+ · `scope['stores']` = `[(店名, store_id), …]` —— **只含该流程在册/声明的店铺**
154
+ (流程 spec 声明子集 ⇒ 只用子集;⛔ 不遍历无关店铺);
155
+ · `scope['markers']` = `lsof` 行里匹配客户端进程的子串(引擎那一份字面量,⛔ 本文件不复制)。
156
+
157
+ ⚠️ **为什么用 `importlib` 动态取引擎,而不是行首 `import orchestrator`**:
158
+ `env_probe.py` 属 `pack.engine`(**每个带 flow 的能力包都带**),而 `scripts/orchestrator.py`
159
+ 只在 full 的能力包脚本集里 ⇒ 静态 import 会让 invoice/daily-report 的
160
+ `check-pack-closure --strict` 报缺件(实测 3 条,含它的两跳依赖)。而这条引用在那些机器上
161
+ **永远走不到**:只有声明了 `envProbe` 的流程才进得了第 ② 层(今天只有 full;
162
+ `flow_probe_spec()` 对别的流程返回 `{}` ⇒ `main()` 在 import 之前就 exit 3)。
163
+ ⇒ 形态 = **可选依赖**:取不到就**如实报"回退判据不可用"**(⛔ 不静默降级、⛔ 不猜店铺),
164
+ 降级路径由回归钉住。同族先例:`scripts/flow_config.py` 用 `importlib` 取业务线适配器。
165
+ """
166
+ try:
167
+ import importlib
168
+ orch = importlib.import_module('orchestrator')
169
+ except Exception as exc: # noqa: BLE001
170
+ return {}, f"引擎店铺解析不可用({type(exc).__name__}: {exc})"
171
+ markers = tuple(str(m).strip() for m in
172
+ (getattr(orch, 'CDP_CLIENT_PROC_MARKERS', ()) or ()) if str(m).strip())
173
+ if not markers:
174
+ return {}, '引擎未声明客户端进程名判据(CDP_CLIENT_PROC_MARKERS 缺失)'
175
+ declared = [str(n).strip() for n in ((spec or {}).get('stores') or []) if str(n).strip()]
176
+ scope: list = []
177
+ if declared:
178
+ # 流程声明了店铺子集 ⇒ **只用子集**;解析走引擎既有实现(⛔ 不新造第二份)
179
+ for name in declared:
180
+ info, _dev = orch._resolve_store(name)
181
+ sid = str((info or {}).get('store_id') or '').strip()
182
+ if sid:
183
+ scope.append((name, sid))
184
+ if not scope:
185
+ return {}, f"流程声明的店铺在册清单里一个都解析不到({declared})"
186
+ else:
187
+ # 在册店铺 = stores.json;**缺失/损坏时回退内置映射** —— 与 `orchestrator._resolve_store()`
188
+ # 同一口径(内置映射是开发/存量机器的兜底,⛔ 不在这里另立一套判据)。
189
+ stores = orch._load_stores() or dict(getattr(orch, 'STORE_MAP', {}) or {})
190
+ for key in sorted(stores):
191
+ sid = str((stores.get(key) or {}).get('store_id') or '').strip()
192
+ if sid:
193
+ scope.append((str(key), sid))
194
+ if not scope:
195
+ return {}, '本机店铺清单为空(stores.json 缺失 / 损坏?)'
196
+ return {'stores': scope, 'markers': markers}, ''
197
+
198
+
199
+ def _cdp_port_fallback(spec: dict, timeout: float = CDP_PROBE_TIMEOUT) -> dict:
200
+ """**第 ② 层回退**:本机 CDP 端口 → `chrome_<store_id>` 归属 → 页面可达(**只读**,⛔ 绝不上抛)。
201
+
202
+ 三态与第 ① 层同一形状(别把三态揉成两态):
203
+ · `ok=True` ⇒ 有**属于在册店铺**且**可驱动**的窗口(`count` = 几家店,`summary` = 证据行);
204
+ · `ok=False, error=''` ⇒ 查得到但**没有**(真没有:没端口 / 端口归属不上任何在册店铺);
205
+ · `ok=False, error!=''` ⇒ **查不到**(引擎/探针失败)—— 与"没有"**必须分开**,
206
+ "查不到"**不得**当可用。
207
+
208
+ 实现全部取自 `scripts/lib/cdp_ports.py`(端口发现/归属/页面探针的**唯一实现**);
209
+ 本函数只做**策略**:范围(哪些店)、顺序、证据行、三态折叠。⛔ 不写任何文件。
210
+ """
211
+ scope, err = _engine_store_scope(spec)
212
+ if err:
213
+ return {'ok': False, 'count': 0, 'summary': '', 'error': err, 'ports': []}
214
+ try:
215
+ ports = cdp_ports.discover_ports(scope['markers'])
216
+ except Exception as exc: # noqa: BLE001
217
+ return {'ok': False, 'count': 0, 'summary': '', 'ports': [],
218
+ 'error': f"端口发现失败({type(exc).__name__}: {exc})"}
219
+ if not ports:
220
+ # 一个端口都没有 ⇒ 没有可归属的东西:**一次归属查询都不发**(真·没有,不是查不到)
221
+ return {'ok': False, 'count': 0, 'summary': '', 'error': '', 'ports': []}
222
+ lines: list = []
223
+ for name, store_id in scope['stores']:
224
+ try:
225
+ port = cdp_ports.match_port_by_user_data_dir(store_id, ports)
226
+ if not port:
227
+ continue
228
+ # 可驱动判据(两条都要过):`/json/version` 可读(discover 已保证,这里再核一次防漂移)
229
+ # ∧ `/json/list` 至少一个 page 目标("浏览器进程在" ≠ "有可驱动的标签页")。
230
+ if not cdp_ports.is_cdp_port(int(port), timeout=timeout):
231
+ continue
232
+ pages = cdp_ports.page_targets(int(port), timeout=timeout)
233
+ if not pages:
234
+ continue
235
+ lines.append(webdriver_ipc.window_item_line({
236
+ 'downloadPath': name,
237
+ 'debuggingPort': int(port),
238
+ 'core_version': cdp_ports.browser_version(int(port), timeout=timeout) or '?',
239
+ # handle 取 `/json/list` 首个 page 目标的 id(CDP 附着的句柄;与第 ① 层的
240
+ # `mainHandle` 同义:都是"那个可驱动的窗口句柄",只是来源不同 —— 如实标注)。
241
+ 'mainHandle': cdp_ports.target_handle(pages) or '?',
242
+ }))
243
+ except Exception as exc: # noqa: BLE001
244
+ return {'ok': False, 'count': 0, 'summary': '', 'ports': list(ports),
245
+ 'error': f"端口归属/页面探针失败({type(exc).__name__}: {exc})"}
246
+ return {'ok': bool(lines), 'count': len(lines), 'summary': ' | '.join(lines),
247
+ 'error': '', 'ports': list(ports)}
248
+
249
+
250
+ def throttle_marker(flow: str) -> tuple:
251
+ """平台限流熔断标记 → (是否生效, 说明)。
252
+
253
+ 文件:`<ARGENT_DATA_DIR>/THROTTLE_<FLOW>`(JSON:reason/since/until)
254
+ 为什么(2026-09-23):美客多限流时,光暂停定时器不够 —— 角色手里还有票在重跑,
255
+ 而人类/角色都没有"现在平台在限流"的统一信号。标记文件给了**一个所有脚本都能读的事实**,
256
+ 到期自动失效(until),无需人工清理。
257
+ """
258
+ import time as _time
259
+ d = pathlib.Path(resolve_data_dir()['dir'])
260
+ f = d / f'THROTTLE_{flow.upper().replace("-", "_")}'
261
+ if not f.exists():
262
+ return False, ''
263
+ try:
264
+ doc = json.loads(f.read_text(encoding='utf-8'))
265
+ except Exception: # noqa: BLE001
266
+ doc = {}
267
+ until = str(doc.get('until') or '')
268
+ if until:
269
+ try:
270
+ if _time.mktime(_time.strptime(until, '%Y-%m-%dT%H:%M:%S')) < _time.time():
271
+ return False, f'熔断标记已过期({until})'
272
+ except Exception: # noqa: BLE001
273
+ pass
274
+ return True, f"平台限流熔断中:{doc.get('reason') or '(未写原因)'}(至 {until or '未设'})"
275
+
276
+
277
+ def flow_probe_spec(flow: str) -> dict:
278
+ """从注册表读该流程声明的环境探针(`envProbe`)。读不到 ⇒ 无探针。"""
279
+ try:
280
+ import flow_config
281
+ spec = flow_config.load_config(flow, strict=False)
282
+ return (spec or {}).get('envProbe') or {}
283
+ except Exception: # noqa: BLE001
284
+ return {}
285
+
286
+
287
+ def _store_list_probe(spec: dict) -> tuple:
288
+ """跑清单命令 → `(bridge_spoke, ok, items, out, stderr, err_detail)`。
289
+
290
+ `bridge_spoke` = **Bridge 有响应**(CLI 吐出了带 `ok` 键的 JSON)—— 只有它才允许走回退判据:
291
+ 命令缺失/超时/输出不成 JSON ⇒ 连"Bridge 活没活"都不知道 ⇒ 保守判不可用(⛔ 不猜)。
292
+ """
293
+ cmd = list(spec.get('cmd') or DEFAULT_STORE_CMD)
294
+ try:
295
+ p = subprocess.run(cmd, capture_output=True, text=True,
296
+ timeout=int(spec.get('timeoutSec') or PROBE_TIMEOUT))
297
+ except FileNotFoundError:
298
+ return False, False, [], '', '', f"找不到命令 {cmd[0]}(紫鸟 CLI 未安装?)"
299
+ except subprocess.TimeoutExpired:
300
+ return False, False, [], '', '', f"`{' '.join(cmd)}` 超时 {PROBE_TIMEOUT}s(客户端卡住/未启动)"
301
+ out = (p.stdout or '').strip()
302
+ doc: dict = {}
303
+ try:
304
+ doc = json.loads(out[out.find('{'):]) if '{' in out else {}
305
+ except Exception: # noqa: BLE001
306
+ doc = {}
307
+ spoke = isinstance(doc, dict) and 'ok' in doc
308
+ items = ((doc.get('data') or {}).get('items') or []) if isinstance(doc, dict) else []
309
+ return spoke, bool(doc.get('ok')), list(items), out, (p.stderr or ''), ''
310
+
311
+
312
+ def _cmd_label(spec: dict) -> str:
313
+ """清单命令的可读标签(日志里说清"是哪条命令拿不到店铺")。"""
314
+ return ' '.join(list(spec.get('cmd') or DEFAULT_STORE_CMD))
315
+
316
+
317
+ def probe_ziniao_store(spec: dict) -> tuple:
318
+ """紫鸟 ZClaw Bridge:能列出店铺 ⇒ 客户端在且已登录。→ (ok, 说明)
319
+
320
+ 列表为空(或 `ok:false` 但 Bridge 有响应)时走**两层回退判据**(顺序固定,见模块 docstring):
321
+ ① 本机已运行窗口(`getRunningInfo`,复用 `scripts/lib/webdriver_ipc.py`);
322
+ ② 本机 CDP 端口 + `chrome_<store_id>` 归属 + 页面可达(复用 `scripts/lib/cdp_ports.py`)。
323
+ 两层都不成立 ⇒ 不可用(文案逐字回到旧常量;只有探针**本身失败**才写"回退判据不可用(原因)")。
324
+ """
325
+ spoke, ok_flag, items, out, err_out, err_detail = _store_list_probe(spec)
326
+ if err_detail:
327
+ return False, err_detail
328
+ if ok_flag and items:
329
+ return True, f"紫鸟就绪({len(items)} 个店铺)" # ← 正常路径:**逐字不变**
330
+ # ── 走到这里 = 云端店铺列表拿不到(为空 / 未就绪)⇒ 尝试两层回退判据 ──────────────────
331
+ raw = (out or err_out or '')[:120]
332
+ if not ok_flag: # 判据优先级与改动前**一致**:ok:false 先于"列表为空"
333
+ no_list = f"Bridge 未就绪或**客户端未登录**:{raw}"
334
+ lead = f" 未就绪(原文:{raw})"
335
+ head = f"未就绪(原文:{raw})"
336
+ else:
337
+ no_list = NO_STORE_DETAIL
338
+ lead = " 店铺列表为空"
339
+ head = "店铺列表为空"
340
+ if not spoke:
341
+ # Bridge 连"有没有响应"都测不出 ⇒ ⛔ 不回退(文案原样保留)
342
+ return False, no_list
343
+ if not local_window_fallback_enabled(spec):
344
+ # 粗开关关闭 ⇒ 行为与改动前**逐字一致**(连探针都不发:日志里不该出现回退痕迹)
345
+ return False, no_list
346
+ # ── ① 本机已运行的店铺窗口(`getRunningInfo`)──────────────────────────────────────
347
+ probe = _local_running_windows(local_wd_port(spec))
348
+ if probe.get('ok'):
349
+ return True, _fallback_detail(spec, lead=lead,
350
+ summary=str(probe.get('summary') or ''),
351
+ count=int(probe.get('count') or 0))
352
+ # ── ② 本机 CDP 端口 + 店铺归属(现场实测:`getRunningInfo` 也会是 0,而窗口在跑)────────
353
+ cdp_error = ''
354
+ if cdp_port_fallback_enabled(spec):
355
+ cdp = _cdp_port_fallback(spec)
356
+ if cdp.get('ok'):
357
+ return True, _cdp_fallback_detail(spec, head=head,
358
+ summary=str(cdp.get('summary') or ''),
359
+ count=int(cdp.get('count') or 0))
360
+ cdp_error = str(cdp.get('error') or '')
361
+ # ── 两层都不成立 ⇒ 不可用;把"探针没跑成"如实写出来(否则又成"猜")──────────────────
362
+ reasons = []
363
+ if probe.get('error'):
364
+ reasons.append(f"本机窗口查询失败({probe['error']})")
365
+ if cdp_error:
366
+ reasons.append(f"CDP 端口探针失败({cdp_error})")
367
+ if reasons:
368
+ return False, f"{no_list};回退判据不可用:{';'.join(reasons)}"
369
+ return False, no_list
370
+
371
+
372
+ KINDS = {'ziniao-store': probe_ziniao_store}
373
+
374
+
375
+ def main(argv=None) -> int:
376
+ ap = argparse.ArgumentParser(description='认领前环境预检(只读)')
377
+ ap.add_argument('--flow', required=True)
378
+ ap.add_argument('--json', action='store_true')
379
+ a = ap.parse_args(argv)
380
+ # ① 熔断标记优先(人工/自动写入的"平台在限流"事实)——比探针更权威,且会自行过期
381
+ hot, hot_why = throttle_marker(a.flow)
382
+ if hot:
383
+ if a.json:
384
+ print(json.dumps({'flow': a.flow, 'probe': 'throttle', 'ok': False, 'detail': hot_why},
385
+ ensure_ascii=False))
386
+ else:
387
+ print(f" ❌ 环境预检(throttle):{hot_why}")
388
+ return 2
389
+
390
+ spec = flow_probe_spec(a.flow)
391
+ kind = str(spec.get('kind') or '')
392
+ if not kind:
393
+ if a.json:
394
+ print(json.dumps({'flow': a.flow, 'probe': None, 'ok': True, 'skipped': True}, ensure_ascii=False))
395
+ else:
396
+ print(f" · 流程 {a.flow} 未声明 envProbe ⇒ 跳过环境预检")
397
+ return 3
398
+ fn = KINDS.get(kind)
399
+ if fn is None:
400
+ print(f"❌ 未知探针类型 {kind!r}(可用:{', '.join(KINDS)})", file=sys.stderr)
401
+ return 2
402
+ ok, why = fn(spec)
403
+ if a.json:
404
+ print(json.dumps({'flow': a.flow, 'probe': kind, 'ok': ok, 'detail': why}, ensure_ascii=False))
405
+ else:
406
+ print(f" {'✅' if ok else '❌'} 环境预检({kind}):{why}")
407
+ return 0 if ok else 2
408
+
409
+
410
+ if __name__ == '__main__':
411
+ raise SystemExit(main())
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 流程/告警类 **飞书交互卡片** 共用构造器(2026-09-17,用户口径:
4
+ * 「推送到飞书的信息全都改成卡片」→ 范围=**流程/告警类**,日常对话保持文本)。
5
+ *
6
+ * 为什么单独一个模块:卡片 JSON 的形状一旦散落在各处(gateway / 脚本 / 插件)就会各自漂移,
7
+ * 而卡片是要给人看的东西 —— 一处改、处处变。构造器是**纯函数**(无 IO、无网络),
8
+ * `scripts/tests/feishu-card.test.mjs` 直接 import 断言;发送只有 `sendCard()` 一个出口。
9
+ *
10
+ * 通道复用 `daily-report.mjs` 已跑通的那条:
11
+ * lark-cli --profile <p> im +messages-send --chat-id <id> --msg-type interactive --content <json> --as bot
12
+ *
13
+ * 用法(默认 dry-run,只打印卡片 JSON;加 --send 才真发):
14
+ * node scripts/feishu-card.mjs --template flow-result --to internal \
15
+ * --flow wenyao-invoice --flow-title 上海文鳐发票流程 \
16
+ * --result "76265639=done|已上传发票与标签" --note "本轮 1 票完成"
17
+ * node scripts/feishu-card.mjs --template await-release --to internal --flow full --count 91
18
+ * node scripts/feishu-card.mjs --template failure-alert --to internal --flow full \
19
+ * --case full__recvvgPQgoEFR3 --level needs_agent --error "门户超时" --attempt 2
20
+ */
21
+ import { spawnSync } from 'node:child_process'
22
+ import { readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import { resolveDataDir } from './lib/argent-datadir.mjs'
25
+ import { isDirectRun } from './lib/is-direct-run.mjs'
26
+
27
+ // 数据目录:内核三级链(唯一判据)—— 卡片要读 `<数据目录>/config.json`
28
+ const DATA_DIR = resolveDataDir({ env: process.env }).dir
29
+ const CONFIG = join(DATA_DIR, 'config.json')
30
+ const LARK_BIN = process.env.LARK_BIN || 'lark-cli'
31
+ const MAX_MD = 4000
32
+
33
+ /** 卡片配色(飞书 header.template)。 */
34
+ export const COLORS = { ok: 'green', warn: 'orange', fail: 'red', info: 'blue', muted: 'grey' }
35
+
36
+ export const STATUS_LEVEL = {
37
+ done: 'ok', ok: 'ok', 完成: 'ok', 已完成: 'ok',
38
+ blocked: 'warn', await_release: 'warn', 等人: 'warn', need_fields: 'warn',
39
+ failed: 'fail', fail: 'fail', error: 'fail', 失败: 'fail',
40
+ needs_human: 'fail', needs_agent: 'warn',
41
+ }
42
+ export const worstLevel = (levels) => {
43
+ const order = ['fail', 'warn', 'ok', 'info', 'muted']
44
+ const known = levels.filter((l) => order.includes(l))
45
+ return known.sort((a, b) => order.indexOf(a) - order.indexOf(b))[0] || 'info'
46
+ }
47
+ export const levelOf = (status) => STATUS_LEVEL[String(status || '').trim().toLowerCase()] || STATUS_LEVEL[String(status || '').trim()] || 'info'
48
+
49
+ const clip = (s, n = MAX_MD) => {
50
+ const t = String(s ?? '')
51
+ return t.length <= n ? t : t.slice(0, n - 20) + `\n…(截断 ${t.length - n + 20} 字)`
52
+ }
53
+ export const md = (content) => ({ tag: 'markdown', content: clip(content) })
54
+ export const hr = () => ({ tag: 'hr' })
55
+ export const note = (text) => ({ tag: 'note', elements: [{ tag: 'plain_text', content: clip(text, 500) }] })
56
+ /** 键值对(两列布局):`[['票号','76265639'], …]`;奇数个自动补空。 */
57
+ export const kv = (pairs = []) => {
58
+ const f = pairs.filter(Boolean).map(([k, v]) => ({
59
+ is_short: true,
60
+ text: { tag: 'lark_md', content: `**${clip(k, 40)}**\n${clip(v ?? '-', 300)}` },
61
+ }))
62
+ return f.length ? { tag: 'div', fields: f } : null
63
+ }
64
+
65
+ /** 基础卡片:标题 + 配色 + 内容块(自动去掉空块)。 */
66
+ export function card({ title, level = 'info', elements = [], subtitle = '' }) {
67
+ const els = []
68
+ if (subtitle) els.push(md(subtitle))
69
+ for (const e of elements) if (e) els.push(e)
70
+ return {
71
+ config: { wide_screen_mode: true, update_multi: true },
72
+ header: { template: COLORS[level] || COLORS.info, title: { tag: 'plain_text', content: clip(title, 120) } },
73
+ elements: els.length ? els : [md('(无内容)')],
74
+ }
75
+ }
76
+
77
+ /** ① 流程结果汇报:`results = [{ticket, status, detail}]`。 */
78
+ export function flowResultCard({ flow = '', flowTitle = '', results = [], ledger = '', noteText = '' } = {}) {
79
+ const level = worstLevel(results.map((r) => levelOf(r.status)))
80
+ const done = results.filter((r) => levelOf(r.status) === 'ok').length
81
+ const bad = results.filter((r) => ['fail', 'warn'].includes(levelOf(r.status))).length
82
+ const lines = results.map((r) => {
83
+ const icon = { ok: '✅', warn: '⏸', fail: '❌', info: '·', muted: '·' }[levelOf(r.status)] || '·'
84
+ return `${icon} **${r.ticket || '(无票号)'}** ${r.status || ''}${r.detail ? ` — ${r.detail}` : ''}`
85
+ })
86
+ return card({
87
+ title: `${flowTitle || flow} 本轮结果`,
88
+ level,
89
+ subtitle: `完成 ${done} 需关注 ${bad} 共 ${results.length} 票`,
90
+ elements: [
91
+ lines.length ? md(lines.join('\n')) : md('(本轮无票)'),
92
+ hr(),
93
+ kv([['流程', flow || '-'], ['台账', ledger ? '已记录' : '未记录']]),
94
+ noteText ? note(noteText) : null,
95
+ ],
96
+ })
97
+ }
98
+
99
+ /** ② 需人工裁定(用户口径 2026-09-17:needs_human 由用户本人确认)。 */
100
+ export function needHumanCard({ flow = '', flowTitle = '', caseId = '', ticket = '', reason = '', ask = '', where = '会话里回一句,或在群里回' } = {}) {
101
+ return card({
102
+ title: `需要你确认 ${flowTitle || flow}`,
103
+ level: 'fail',
104
+ subtitle: '这条不能自动往下走:要么缺数据,要么得你拍板。',
105
+ elements: [
106
+ kv([['票', ticket || caseId || '-'], ['流程', flow || '-']]),
107
+ reason ? md(`**卡在哪**\n${reason}`) : null,
108
+ ask ? md(`**要你定**\n${ask}`) : null,
109
+ note(`怎么回:${where}`),
110
+ ],
111
+ })
112
+ }
113
+
114
+ /** ③ 待放行汇总(FULL 的 await_release 桶:运营勾「就绪」才开跑)。 */
115
+ export function awaitReleaseCard({ flow = '', flowTitle = '', count = 0, samples = [], gate = '就绪', noteText = '' } = {}) {
116
+ const many = count >= 20
117
+ return card({
118
+ title: `${flowTitle || flow} 待放行 ${count} 张`,
119
+ level: many ? 'warn' : 'info',
120
+ subtitle: `系统已识别、卡在放行门(**${gate}**):没人勾就不会跑。`,
121
+ elements: [
122
+ samples.length ? md('例如:\n' + samples.slice(0, 5).map((s) => `· ${s}`).join('\n')) : null,
123
+ noteText ? note(noteText) : null,
124
+ note('这是提醒,不是故障:放行由人做(本次不需要任何脚本动作)。'),
125
+ ],
126
+ })
127
+ }
128
+
129
+ /** ④ 失败告警(角色被唤醒处置前后都可用)。 */
130
+ export function failureAlertCard({ flow = '', flowTitle = '', caseId = '', level = 'needs_agent', error = '', attempt = '', max = '', action = '' } = {}) {
131
+ const lvl = levelOf(level)
132
+ const label = { needs_agent: '需要判断', needs_human: '需要人补/拍板', retryable: '可自动重试' }[level] || level
133
+ return card({
134
+ title: `流程失败 ${flowTitle || flow}`,
135
+ level: lvl === 'info' ? 'warn' : lvl,
136
+ subtitle: `分类:**${label}**${attempt ? ` 第 ${attempt}${max ? `/${max}` : ''} 次` : ''}`,
137
+ elements: [
138
+ kv([['票据', caseId || '-'], ['流程', flow || '-']]),
139
+ error ? md(`**错误摘要**\n${clip(error, 800)}`) : null,
140
+ action ? md(`**下一步**\n${action}`) : null,
141
+ note(level === 'needs_human' ? '需要你确认后才会继续(会话里回一句即可)。' : '角色会接手判断;可重试类不需要人介入。'),
142
+ ],
143
+ })
144
+ }
145
+
146
+ // ─────────────────────────── 发送 ───────────────────────────
147
+
148
+ const readJson = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} } }
149
+ const readEnvFile = (p) => {
150
+ const out = {}
151
+ try {
152
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
153
+ const t = line.trim()
154
+ if (!t || t.startsWith('#') || !t.includes('=')) continue
155
+ const i = t.indexOf('=')
156
+ out[t.slice(0, i).trim()] = t.slice(i + 1).trim()
157
+ }
158
+ } catch { /* 文件不存在 = 空 */ }
159
+ return out
160
+ }
161
+
162
+ /**
163
+ * 收件人解析:`internal`=内部群(告警/流程/需人工)、`daily`=日报群;也接受显式 `oc_…`。
164
+ * 顺序:显式 chatId → 部署 env → config.json → 报错(**绝不猜一个群发出去**)。
165
+ */
166
+ export function resolveSink(to, { env = process.env, dataDir = DATA_DIR } = {}) {
167
+ const cfg = readJson(join(dataDir, 'config.json'))
168
+ const fullEnv = { ...readEnvFile(join(dataDir, 'fulfillment.env')), ...env }
169
+ const dailyEnv = { ...readEnvFile(join(dataDir, 'daily-report.env')), ...env }
170
+ const key = String(to || 'internal').trim()
171
+ if (key.startsWith('oc_')) return { chatId: key, profile: fullEnv.NOTIFY_PROFILE || 'cli_a93b9354c6e35cd1', kind: 'explicit' }
172
+ if (key === 'daily') {
173
+ const chatId = dailyEnv.ML_DAILY_CHAT_ID || cfg.mlDailyChatId || ''
174
+ return { chatId, profile: dailyEnv.LARK_PROFILE || cfg.mlDailyAlertProfile || 'ml-daily', kind: 'daily' }
175
+ }
176
+ const chatId = fullEnv.FULL_INTERNAL_CHAT_ID || cfg.feishuChatId || ''
177
+ return { chatId, profile: fullEnv.NOTIFY_PROFILE || cfg.mlDailyAlertProfile || 'cli_a93b9354c6e35cd1', kind: 'internal' }
178
+ }
179
+
180
+ /** 真发:走 lark-cli 的 interactive 卡片通道(与日报同一条)。 */
181
+ export function sendCard(cardJson, { chatId, profile, timeoutMs = 60000, bin = LARK_BIN } = {}) {
182
+ if (!chatId) throw new Error('缺少 chatId(用 --to internal|daily|oc_xxx 指定,或配好部署 env)')
183
+ if (!profile) throw new Error('缺少 profile(用 --profile 指定,或配 NOTIFY_PROFILE)')
184
+ const r = spawnSync(bin, ['--profile', String(profile), 'im', '+messages-send',
185
+ '--chat-id', String(chatId), '--msg-type', 'interactive',
186
+ '--content', JSON.stringify(cardJson), '--as', 'bot'],
187
+ { encoding: 'utf8', timeout: timeoutMs })
188
+ if (r.status !== 0) {
189
+ throw new Error(`lark-cli 卡片推送失败: ${(r.error?.message || r.stderr || r.stdout || '').slice(0, 300)}`)
190
+ }
191
+ return { ok: true, chatId, profile }
192
+ }
193
+
194
+ // ─────────────────────────── CLI ───────────────────────────
195
+
196
+ const arg = (argv, name, dflt = '') => {
197
+ const i = argv.indexOf(`--${name}`)
198
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : dflt
199
+ }
200
+ const argAll = (argv, name) => argv.map((a, i) => (a === `--${name}` ? argv[i + 1] : null)).filter(Boolean)
201
+ const parseResult = (s) => {
202
+ const [ticket, rest = ''] = String(s).split('=')
203
+ const [status, detail = ''] = rest.split('|')
204
+ return { ticket: ticket.trim(), status: (status || '').trim(), detail: detail.trim() }
205
+ }
206
+
207
+ export function buildFromArgv(argv = process.argv.slice(2)) {
208
+ const t = arg(argv, 'template', 'flow-result')
209
+ const common = { flow: arg(argv, 'flow'), flowTitle: arg(argv, 'flow-title') || arg(argv, 'title') }
210
+ if (t === 'flow-result') {
211
+ return flowResultCard({ ...common, results: argAll(argv, 'result').map(parseResult), ledger: arg(argv, 'ledger'), noteText: arg(argv, 'note') })
212
+ }
213
+ if (t === 'need-human') {
214
+ return needHumanCard({ ...common, caseId: arg(argv, 'case'), ticket: arg(argv, 'ticket'), reason: arg(argv, 'reason'), ask: arg(argv, 'ask') })
215
+ }
216
+ if (t === 'await-release') {
217
+ return awaitReleaseCard({ ...common, count: Number(arg(argv, 'count', '0')) || 0, samples: argAll(argv, 'sample'), gate: arg(argv, 'gate', '就绪'), noteText: arg(argv, 'note') })
218
+ }
219
+ if (t === 'failure-alert') {
220
+ return failureAlertCard({ ...common, caseId: arg(argv, 'case'), level: arg(argv, 'level', 'needs_agent'), error: arg(argv, 'error'), attempt: arg(argv, 'attempt'), max: arg(argv, 'max'), action: arg(argv, 'action') })
221
+ }
222
+ throw new Error(`未知模板: ${t}(可选 flow-result / need-human / await-release / failure-alert)`)
223
+ }
224
+
225
+ export function main(argv = process.argv.slice(2)) {
226
+ let cardJson
227
+ try {
228
+ cardJson = buildFromArgv(argv)
229
+ } catch (e) {
230
+ console.error(`❌ ${e.message}`)
231
+ return 2
232
+ }
233
+ const send = argv.includes('--send')
234
+ const to = arg(argv, 'to', 'internal')
235
+ const base = resolveSink(to)
236
+ const sink = { ...base, profile: arg(argv, 'profile') || base.profile }
237
+ if (!send) {
238
+ console.log(JSON.stringify({ dryRun: true, to, chatId: sink.chatId ? `${String(sink.chatId).slice(0, 8)}…` : '', profile: sink.profile, card: cardJson }, null, 2))
239
+ return 0
240
+ }
241
+ try {
242
+ const r = sendCard(cardJson, { chatId: sink.chatId, profile: sink.profile })
243
+ console.log(`✅ 卡片已发送(${r.chatId.slice(0, 8)}… / ${r.profile})`)
244
+ return 0
245
+ } catch (e) {
246
+ console.error(`❌ ${e.message}`)
247
+ return 3
248
+ }
249
+ }
250
+
251
+ const isMain = isDirectRun(import.meta.url)
252
+ if (isMain) process.exit(main())