@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,588 @@
1
+ #!/usr/bin/env python3
2
+ """flow_failure —— **失败分类 + 未处置失败扫描**("出错有 agent 接手"回路的确定性内核)。
3
+
4
+ 设计口径(2026-09-16 用户确认):
5
+ "出错有 agent 接手" ≠ 另一个 subagent 抢活;而是**把掌舵者叫醒,让它判断**
6
+ 「重试 / 等人 / 升级」,并把判断与理由记进台账。
7
+
8
+ 关键分界(避免死循环烧 token):
9
+ · **retryable**(网络抖动/超时/429/5xx)→ 确定性可重试,**不叫醒任何人**(脚本自己重试即可);
10
+ · **needs_human**(缺字段/需运营补数据/路由选错行)→ 唤醒也无用,应记 blocked + 落卡等人;
11
+ · **needs_agent**(其余:选择器变化/未知退出码/门户异常)→ **只有这一类唤醒角色判断**。
12
+
13
+ 用法:
14
+ python3 scripts/flow_failure.py --error "P1 路由守卫拦截:货代=冲港" [--attempt 2] [--json]
15
+ python3 scripts/flow_failure.py --scan --flow wenyao-invoice [--ledger L] [--json]
16
+ → 列出"尚未处置"的失败(该 case 最近一次 failed 之后没有 wake/retry/blocked/done 事件)
17
+ 并附分类、唤醒次数、冷却剩余秒数。
18
+ python3 scripts/flow_failure.py --flow full --case full__<rec> [--ledger L]
19
+ → 打印该 case 的**规范失败原因**(引擎写进台账 `failed.reason` 的那一串 = 表备注同一串;
20
+ 老台账没有该字段时回落到 `error` 去掉身份前缀)。
21
+ **口径统一入口**:写 `blocked` 时 `--error` 逐字用它 ⇒ 表备注与账本同源同文
22
+ (⛔ 不许改写/翻译/加解读;生产实测 79096447 的误导就是"解读"造成的)。
23
+ python3 scripts/flow_failure.py --flow <flow> --error "<原始原因>" [--missing 产品图片,HS编码] --ops-message
24
+ → 组成**运营可见文案**(`结论:<业务类|系统类|我方缺陷类>|<原因>|动作:<可照做的动作>`)——
25
+ 这是**唯一措辞来源**:发票流程写表「发票错误信息」与台账 `blocked/failed` 必须用它产出的
26
+ **同一串**(`scripts/invoice_fail_closeout.py` 就是这条链的执行者)。⛔ 代码级异常
27
+ (`AttributeError`/裸栈/`object has no attribute`…)一律归**我方缺陷类**且**不回原文**。
28
+ python3 scripts/flow_failure.py --flow <flow> --release-only
29
+ → 打印该流程的放行位(门控列)、Rule R 是否开启、上限/冷却与运营动作提示。
30
+ 退出码:0 正常;2 参数错误 / `--case` 取不到原因。
31
+
32
+ **失败前核验的两句文案也在这里**(`verified_effective_note()` / `unverified_reason()`,2026-10-09):
33
+ 幂等写步骤(3/4/8)判成功时台账/备注必须说清"报错但已核验生效(幂等)"并带证据来源;
34
+ 核验**不可用**时仍判失败、但文案必须写明「核验不可用(原因)」。⛔ 不许在别处再写第二套措辞。
35
+ """
36
+ from __future__ import annotations
37
+ import argparse
38
+ import json
39
+ import os
40
+ import pathlib
41
+ import re
42
+ import sys
43
+ import time
44
+
45
+ try:
46
+ from flow_registry import consumer_doc as _registry_doc # P2a:base + 业务线合并(唯一真源)
47
+ except Exception: # noqa: BLE001 —— 半坏机器上退回单文件(升级链路要能跑)
48
+ def _registry_doc(path=None):
49
+ _p = pathlib.Path(path) if path else (HERE / "flow-requirements.json")
50
+ return json.loads(_p.read_text(encoding="utf-8"))
51
+
52
+
53
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent / 'lib'))
54
+ from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
55
+
56
+ # 台账默认位置:数据目录走**唯一判据**(三级链;勿在此复制链)
57
+ DEFAULT_LEDGER = pathlib.Path(resolve_data_dir()['dir']) / 'flow-ledger.jsonl'
58
+ HANDLED_EVENTS = ('wake', 'retry', 'blocked', 'done', 'failed_ack')
59
+
60
+ # 分类规则(顺序敏感:先匹配到的赢)
61
+ def rule_patterns(group: str) -> list:
62
+ """从注册表读某组失败分类规则(P3:判据唯一权威副本);读不到返回空(调用方用内置兜底)。"""
63
+ try:
64
+ doc = _registry_doc() # 兜底语义不变(读不到用内置规则;冲突由门禁响亮拦)
65
+ grp = (doc.get('failureClasses') or {}).get(group) or {}
66
+ return [str(x) for x in (grp.get('patterns') or []) if str(x).strip()]
67
+ except Exception:
68
+ return []
69
+
70
+
71
+ _BUILTIN = {
72
+ 'needs_human': (
73
+ r'缺(?:[失了少]|\s)|未填|为空|空值|missing|不存在|未就绪|闸门|校验不通过|需运营|等运营|请补|'
74
+ r'路由守卫|货代.*[≠!=]|不匹配|非.{0,6}货代|选错|人工|'
75
+ # 2026-09-17 门禁金样例暴露:这类是**可人工处理**的运行前提问题(有人去授权/登录即可),
76
+ # 不是"需要角色判断"的模糊故障 ⇒ 归 needs_human(落卡等人),不惊动角色白跑一趟。
77
+ r'未授权|未登录|登录失效|登录墙|login_expired|无权限|permission'),
78
+ # 2026-09-18:页面/选择器类(UI 选择器超时 ≠ 网络超时)。旧架构既定规则 = 选择器/业务类
79
+ # **绝不自动重置**;但下面的 retryable 里有 `超时|timeout` 会把它吞掉 ⇒ 允许清号重建 = 重复建单。
80
+ # 本组在 retryable **之前**匹配 ⇒ 归 needs_agent(交掌舵者判断)。
81
+ 'pageFlaky': (
82
+ r'等待选择器超时|选择器\s*超时|selector\s*(timeout|超时)|wait_for_selector|'
83
+ r'locator.*(timeout|超时)|input\[[a-z]+\]|date_input'),
84
+ 'retryable': (
85
+ r'timeout|timed out|超时|连接|connection|ECONN|ETIMEDOUT|EAI_AGAIN|socket|网络|'
86
+ r'429|502|503|504|rate limit|限流|too many requests|temporar|稍后重试|重试即可|'
87
+ # 2026-09-23 实测:美客多限流/前端模块加载失败的原字样(此前 `rate limit` 带空格,
88
+ # 匹配不上 ML 的 `local_rate_limited` 下划线写法 ⇒ 被归到"需判断",没走自动重试)。
89
+ r'local_rate_limited|rate_limit|resolveProps|fbm-shipment-planning-frontend|'
90
+ r'Error loading|上下文销毁|context destroyed|Cannot load\.|'
91
+ # 2026-09-17(真实失败票实测):**浏览器/页面瞬时类** —— 旧架构对这类本来就是
92
+ # 「下一轮轮询自动重置重跑」(AUTO_RETRY_MAX=2),不是要人判断的业务失败。
93
+ r'页面导航失败|导航失败|Target page|browser has been closed|Target closed|Page\.goto|'
94
+ r'net::ERR_|ERR_SOCKS_CONNECTION_FAILED|NS_ERROR_|context or browser|browser context'),
95
+ # 2026-10-10(FULL 自动路径成功率修复批):**仓容满**。判据与文案的唯一真源是
96
+ # `scripts/ml_capacity_probe.py`(`PLATFORM_QUOTES` + `capacity_failure_text`),
97
+ # 这里是**注册表**侧的同一份口径(`failureClasses.capacityFull`)。内置兜底只留"平台原文
98
+ # 片段 + 分类标记",与注册表逐字一致;⛔ 不在这里另加"预约页形态"类判据(那是另一条链)。
99
+ 'capacityFull': (
100
+ r'capacity_full|liberar espacio de almacenamiento|liberar espacio|'
101
+ r'retirar stock de baja calidad|retirando stock de baja calidad|'
102
+ r'stock de baja calidad|仓容满|库容满'),
103
+ }
104
+ # 注册表优先(P3 判据收敛):注册表有该组规则就用它,否则用内置兜底(保证老部署/离线也能跑)
105
+ EFFECTIVE = {
106
+ 'capacityFull': rule_patterns('capacityFull') or [_BUILTIN['capacityFull']],
107
+ 'pageFlaky': rule_patterns('pageFlaky') or [_BUILTIN['pageFlaky']],
108
+ 'needs_human': rule_patterns('needsHuman') or [_BUILTIN['needs_human']],
109
+ 'retryable': rule_patterns('flowRetryable') or [_BUILTIN['retryable']],
110
+ }
111
+ # 顺序敏感:pageFlaky **必须**在 retryable 之前(否则被 `超时|timeout` 吞掉)。
112
+ # 2026-10-10:**capacityFull 排在最前**(在 needs_human 之前)—— 仓容满的文案里带
113
+ # `需运营` 与 `仓容满` 两个字样,若让 needsHuman 先匹配就再也分不出"是哪条链",
114
+ # 而它俩的正确动作完全不同(补数据/放行 vs 清低质库存腾库容),且只有它允许当日在
115
+ # 预检证据下解除 defer。⚠️ 它**不**抢 retryable 的活:本组只认平台原文与显式标记。
116
+ # 分组 → **派发用的 kind**:pageFlaky 派发为 needs_agent(唤醒掌舵者判断),
117
+ # 但它的理由单独给(GROUP_WHY),避免与"未知类兜底"混为一谈。
118
+ GROUPS = (('capacityFull', 'needs_human'), ('pageFlaky', 'needs_agent'),
119
+ ('needs_human', 'needs_human'), ('retryable', 'retryable'))
120
+ RULES = tuple((g, k, re.compile('|'.join(EFFECTIVE[g]), re.I)) for g, k in GROUPS)
121
+ DEFAULT_KIND = 'needs_agent'
122
+ GROUP_WHY = {
123
+ 'pageFlaky': '页面/选择器类(UI 选择器超时,**非网络类**)⇒ 交掌舵者判断;'
124
+ '**禁止自动清号重建**(旧架构规则:选择器/业务类绝不自动重置,防重复建单)',
125
+ 'capacityFull': '仓容(库容)满(平台原文拒绝继续发货)⇒ 前提问题,正确动作**唯一**'
126
+ '(需运营清低质库存腾库容)⇒ 落卡等人;有**独立**的当日重试语义'
127
+ '(预检发现仓容恢复 ⇒ 当日解除 defer),⛔ 绝不与"预约页形态异常"混链',
128
+ }
129
+ KIND_WHY = {
130
+ 'retryable': '确定性可重试(网络/限流类)⇒ 脚本自行重试,不唤醒角色',
131
+ 'needs_human': '需人工/运营介入(缺数据或选错行)⇒ 应记 blocked 落卡等人,唤醒角色无意义',
132
+ 'needs_agent': '需要判断(其余未知类)⇒ 唤醒掌舵者判断「重试/等人/升级」',
133
+ }
134
+
135
+
136
+ def classify(error: str, attempt: int | None = None, max_attempts: int = 3) -> dict:
137
+ """把一段失败文本分类成 retryable / needs_human / needs_agent。"""
138
+ text = str(error or '')
139
+ kind, why = DEFAULT_KIND, KIND_WHY[DEFAULT_KIND]
140
+ for grp, k, pat in RULES:
141
+ if pat.search(text):
142
+ kind, why = k, (GROUP_WHY.get(grp) or KIND_WHY[k])
143
+ break
144
+ if kind == 'retryable' and attempt is not None and attempt >= max_attempts:
145
+ kind, why = 'needs_agent', f'已达自动重试上限(attempt={attempt})⇒ 需角色判断'
146
+ return {'kind': kind, 'reason': why, 'error': text[:300]}
147
+
148
+
149
+ # ── 运营可见文案:**唯一措辞来源**(2026-10-09,缺陷 B)────────────────────────
150
+ # 背景(客户机现场):`recvvgPQgoUOwx` 跑失败后,表里「发票就绪」被取消、而「发票错误信息」
151
+ # **是空的**(原因只在台账里)⇒ 运营看到"勾没了 + 没有任何说明",以为脚本坏了、也不知道怎么处理。
152
+ # 纪律(用户口径):
153
+ # ① 失败必须写原因 + **可照做的动作**,且**表字段与台账同源同文**(同一函数产出、同一串);
154
+ # ② 先一句**结论**(业务类 / 系统类 / 我方缺陷类,明确区分),再一句**动作**;
155
+ # ③ ⛔ 绝不把裸异常栈 / "某某 object has no attribute" 直接塞给运营(那既不可读也不可执行)。
156
+ # 出口只有本模块的三个函数:`ops_category` / `ops_message`(+ `release_hint` 取放行位文案)。
157
+ OPS_CATEGORY_BUSINESS = '业务类'
158
+ OPS_CATEGORY_SYSTEM = '系统类'
159
+ OPS_CATEGORY_DEFECT = '我方缺陷类'
160
+ OPS_PREFIX = '结论:'
161
+ # **我方缺陷类**判据(代码级异常特征)——刻意**窄**:只认"这是程序自己炸了"的形态,
162
+ # 不认业务词(缺字段/路由不匹配等仍归业务类)。命中即**不把原文**给运营(见 ops_reason)。
163
+ CODE_NOISE = (
164
+ r'object has no attribute',
165
+ r'\bAttributeError\b', r'\bTypeError\b', r'\bKeyError\b', r'\bIndexError\b',
166
+ r'\bValueError\b', r'\bNameError\b', r'\bUnboundLocalError\b', r'\bZeroDivisionError\b',
167
+ r'\bRecursionError\b', r'\bSyntaxError\b', r'\bIndentationError\b',
168
+ r'\bModuleNotFoundError\b', r'\bImportError\b', r'\bFileNotFoundError\b',
169
+ r'ERR_MODULE_NOT_FOUND', r'ERR_INVALID_ARG_TYPE',
170
+ r'Traceback \(most recent call last\)', r'\bNoneType\b',
171
+ r'\.py", line \d+', r'\bat [\w$.]+ \(', r'\bTypeError:', r'\bError: ',
172
+ )
173
+ _CODE_NOISE_RE = tuple(re.compile(p, re.I) for p in CODE_NOISE)
174
+ OPS_REASON_LIMIT = 300 # 台账 `--detail` 的上限就是 300(flow_ledger.py)⇒ 同源同文必须 ≤ 它
175
+
176
+
177
+ def is_code_noise(text: str) -> bool:
178
+ """这段失败文本是不是**代码级异常**(= 我方缺陷类)?"""
179
+ t = str(text or '')
180
+ return bool(t.strip()) and any(p.search(t) for p in _CODE_NOISE_RE)
181
+
182
+
183
+ def _flow_spec(flow_id: str) -> dict:
184
+ """注册表里某流程的声明(走别名;读不到返回 {})。"""
185
+ try:
186
+ doc = _registry_doc()
187
+ except Exception: # noqa: BLE001
188
+ return {}
189
+ canon = (doc.get('aliases') or {}).get(flow_id, flow_id)
190
+ return ((doc.get('flows') or {}).get(canon) or {})
191
+
192
+
193
+ def _picks_of(src: dict) -> list:
194
+ """流程声明的全部 `pick`(**变体优先,其次顶层**)+ 各自的 fields。
195
+
196
+ 为什么两份都要看:FULL 的 pick 写在 `source.variants.<名>.pick`,发票流程写在**顶层**
197
+ `source.pick` —— 旧实现只看变体 ⇒ 发票流程的放行位/提示恒为空(2026-10-09 一并修对)。
198
+ """
199
+ out = []
200
+ variants = src.get('variants') or {}
201
+ order = [k for k in [str(src.get('default') or ''), *variants.keys()] if k in variants] or list(variants)
202
+ for name in order:
203
+ v = variants.get(name) or {}
204
+ out.append(((v.get('pick') or {}), (v.get('fields') or {})))
205
+ out.append(((src.get('pick') or {}), (src.get('fields') or {})))
206
+ return out
207
+
208
+
209
+ def release_config(flow_id: str) -> dict:
210
+ """人工放行(Rule R)声明 → `{'enabled','maxPerCase','cooldownMin','field','hint'}`。
211
+
212
+ `field` = 该流程**自己的门控列**(例如物流流程是「就绪」、发票流程是「发票就绪」)——⛔ 不写死、不照抄。
213
+ `hint` = 注册表 `failureClasses.opsAction.text` 把 `{release}` 换成该列名后的运营动作提示;
214
+ 流程**没开** `releaseReDispatch` ⇒ `hint=''`(宁可不提示,也不写一句兑现不了的话)。
215
+ """
216
+ src = (_flow_spec(flow_id).get('source') or {})
217
+ tpl = ''
218
+ try:
219
+ tpl = str(((_registry_doc().get('failureClasses') or {}).get('opsAction') or {}).get('text') or '')
220
+ except Exception: # noqa: BLE001
221
+ tpl = ''
222
+ fallback_field = ''
223
+ for pick, fields in _picks_of(src):
224
+ field = str(((pick.get('release') or {}).get('field') or (fields or {}).get('ready') or ''))
225
+ rd = pick.get('releaseReDispatch') or {}
226
+ if not fallback_field and field:
227
+ fallback_field = field
228
+ if rd.get('enabled') and field:
229
+ return {'enabled': True, 'maxPerCase': int(rd.get('maxPerCase') or 0) or 3,
230
+ 'cooldownMin': int(rd.get('cooldownMin') or 0), 'field': field,
231
+ 'hint': (tpl.replace('{release}', field) if tpl else '')}
232
+ return {'enabled': False, 'maxPerCase': 0, 'cooldownMin': 0, 'field': fallback_field, 'hint': ''}
233
+
234
+
235
+ def release_hint(flow_id: str) -> str:
236
+ """该流程的"运营动作提示"(未开 Rule R ⇒ 空串)。**唯一实现**:orchestrator 与发票侧共用。"""
237
+ return release_config(flow_id)['hint']
238
+
239
+
240
+ def ops_category(reason: str, kind: str | None = None) -> str:
241
+ """结论类别(给运营看的第一句):业务类 / 系统类 / 我方缺陷类。
242
+
243
+ 判据(顺序敏感):**代码级异常特征 ⇒ 我方缺陷类**(哪怕分类器兜到 needs_agent);
244
+ 其余按 `classify()` 的 kind:needs_human ⇒ 业务类,retryable/needs_agent ⇒ 系统类。
245
+ """
246
+ if is_code_noise(reason):
247
+ return OPS_CATEGORY_DEFECT
248
+ k = kind or str(classify(reason).get('kind') or '')
249
+ return OPS_CATEGORY_BUSINESS if k == 'needs_human' else OPS_CATEGORY_SYSTEM
250
+
251
+
252
+ def ops_reason(reason: str, category: str) -> str:
253
+ """结论里的"原因"部分:单行化 + 去身份前缀;**我方缺陷类一律不回原文**(防裸栈进表)。"""
254
+ if category == OPS_CATEGORY_DEFECT:
255
+ return '流程脚本内部错误(异常明细在运行日志/台账,⛔ 未把原始栈写进本列)'
256
+ one = ' '.join(canonical_reason(reason).split())
257
+ return one or '(原因缺失:请把台账 failed.error 原文交维护者核对)'
258
+
259
+
260
+ def _brief(items, n: int = 6) -> str:
261
+ """把字段清单压成可读的一行(超过 n 项省略)——防止动作句被长度上限截掉尾巴。"""
262
+ xs = [str(x) for x in (items or [])]
263
+ return '、'.join(xs[:n]) + ('…' if len(xs) > n else '')
264
+
265
+
266
+ def ops_action(flow_id: str, category: str, missing=(), kind: str | None = None) -> str:
267
+ """动作部分(第二句):**可照做**,且只说兑现得了的话(未开 Rule R 就不说"会自动重跑")。"""
268
+ rc = release_config(flow_id)
269
+ rel = rc['field'] or '放行位'
270
+ if category == OPS_CATEGORY_DEFECT:
271
+ return f"无需你处理:已上报问述科技排查(本行已留证据);请勿重复勾选「{rel}」"
272
+ if category == OPS_CATEGORY_BUSINESS:
273
+ what = f"补齐 {_brief(missing)} 后" if missing else "按上面的原因在表内补齐数据后"
274
+ tail = (f",系统下一轮会自动重跑(无需通知运维)" if rc['enabled']
275
+ else ",并通知运维重跑(本流程未开「人工放行重跑」)")
276
+ return f"{what}重新勾选「{rel}」{tail}"
277
+ k = kind or DEFAULT_KIND
278
+ if k == 'retryable':
279
+ return "无需你补数据:系统会自动重试(网络/门户瞬时类);若下一轮仍未恢复,会由角色接手判断"
280
+ return f"无需你补数据:已转交运维/角色判断(页面/门户类);请勿重复勾选「{rel}」(重复勾选不会加速)"
281
+
282
+
283
+ def ops_message(flow_id: str, reason: str, *, missing=(), kind: str | None = None,
284
+ limit: int = OPS_REASON_LIMIT) -> str:
285
+ """**结论 + 动作**的运营可见文案 —— 表字段与台账 `blocked/failed` 文案的**唯一来源**。
286
+
287
+ 形状:`结论:<类别>|<原因>|动作:<动作>`(单行;≤ `limit` = 台账 `--detail` 上限 300,
288
+ 这样"表里那串"与"台账里那串"可以**逐字节相等**,不会因截断而分叉)。
289
+ 已带 `结论:` 前缀的输入**原样返回**(幂等:重复收口不会套两层壳)。
290
+ """
291
+ text = str(reason or '').strip()
292
+ if text.startswith(OPS_PREFIX):
293
+ return text[:limit]
294
+ k = kind or str(classify(text).get('kind') or DEFAULT_KIND)
295
+ cat = ops_category(text, k)
296
+ act = ops_action(flow_id, cat, missing, k)
297
+ body = ops_reason(text, cat)
298
+ head = f"{OPS_PREFIX}{cat}|"
299
+ tail = f"|动作:{act}"
300
+ room = max(20, limit - len(head) - len(tail))
301
+ if len(body) > room:
302
+ body = body[:max(1, room - 1)] + '…'
303
+ return f"{head}{body}{tail}"[:limit]
304
+
305
+
306
+ # ── 「失败前核验」的两句**唯一措辞**(2026-10-09,步骤3/4/8 幂等误报)──────────────
307
+ # 现场:货件 79191341 的取消其实**已生效**(列表页 `Reserva cancelada`、`appointment` 空),
308
+ # 而引擎把"平台错误页导致按钮找不到"报成了失败 ⇒ 表 `FULL状态=失败`、台账被人工纠正成 done。
309
+ # 判据层在 `scripts/flow_write_verify.py`(纯函数三态);**文案**只在这里,⛔ 不许在别处再写一套。
310
+ VERIFY_OK_MARK = '已核验生效(幂等)'
311
+ VERIFY_UNAVAILABLE_MARK = '核验不可用'
312
+
313
+
314
+ def verified_effective_note(step_title: str, truth: str, evidence: str,
315
+ *, limit: int = OPS_REASON_LIMIT) -> str:
316
+ """**判成功**时的台账/备注串(唯一措辞):
317
+ `<步骤> 脚本报错但核验为<真相>(<证据来源>)⇒ 判成功(已核验生效(幂等))`。
318
+
319
+ 为什么必须带证据来源:运维看到的不能只是"引擎说成功了",而是"**凭什么**说成功"
320
+ ——hub-v2 的哪一项(判据名)/ 列表页的 `status`,可当场复核(本现场就是这么纠正的)。
321
+ """
322
+ ev = ' '.join(str(evidence or '').split()) or '无证据串(不应发生)'
323
+ head = f"{' '.join(str(step_title or '').split())} 脚本报错但核验为{str(truth or '已生效').strip()}"
324
+ tail = f"⇒ 判成功({VERIFY_OK_MARK})"
325
+ room = max(20, limit - len(head) - len(tail) - 3)
326
+ if len(ev) > room:
327
+ ev = ev[:max(1, room - 1)] + '…'
328
+ return f"{head}({ev}){tail}"[:limit]
329
+
330
+
331
+ def unverified_reason(reason: str, why: str, *, limit: int = OPS_REASON_LIMIT) -> str:
332
+ """**失败侧**的文案收口(唯一实现):
333
+
334
+ · `why` 为空 ⇒ **原样返回**(= 核验"明确否定"或没跑核验 ⇒ **保持现在的失败路径与文案**,逐字不变);
335
+ · `why` 非空 ⇒ 在原失败原因尾部追加 `;核验不可用(<why>)`(⛔ 不改原话、⛔ 不当成功)。
336
+
337
+ 为什么不可用也要写进文案:运维看到"脚本说没取消"时,必须知道**引擎有没有核验过**
338
+ ——没核验过(探针 store_busy / 页面残缺 / 超时)与"核验过且确认没取消"的处置完全不同。
339
+ """
340
+ base = ' '.join(str(reason or '').split())
341
+ w = ' '.join(str(why or '').split())
342
+ if not w:
343
+ return base[:limit]
344
+ suffix = f";{VERIFY_UNAVAILABLE_MARK}({w})"
345
+ room = max(10, limit - len(suffix))
346
+ if len(base) > room:
347
+ base = base[:max(1, room - 1)] + '…'
348
+ return f"{base}{suffix}"[:limit]
349
+
350
+
351
+ def read(path: pathlib.Path) -> list:
352
+ if not path.exists():
353
+ return []
354
+ out = []
355
+ for ln in path.read_text(encoding='utf-8').splitlines():
356
+ ln = ln.strip()
357
+ if not ln:
358
+ continue
359
+ try:
360
+ out.append(json.loads(ln))
361
+ except Exception:
362
+ continue
363
+ return out
364
+
365
+
366
+ def _prefixes(flow: str | None) -> tuple:
367
+ """该流程在台账里可能出现的**所有** caseId 前缀(P2a)。
368
+
369
+ 台账里写的是"当时那条流程叫什么":业务线落位前的短名(如 `<短名>__…`)与落位后的规范 id
370
+ (`<vertical>.<flow>`)并存。失败回路按前缀归属 ⇒ 只认规范前缀会让历史未处置**集体消失**
371
+ (统计与失败卡都会少算)。这里把规范 id 与所有指向它的别名一起作为可接受前缀。
372
+ """
373
+ if not flow:
374
+ return ()
375
+ names = {flow}
376
+ try:
377
+ import flow_registry # 延迟导入:失败回路要在半坏机器上也能跑
378
+ for short, target in (flow_registry.load_doc().get('aliases') or {}).items():
379
+ if target == flow:
380
+ names.add(short)
381
+ except Exception: # noqa: BLE001
382
+ pass
383
+ return tuple(f'{n}__' for n in sorted(names, key=len, reverse=True))
384
+
385
+
386
+ def _prefix(flow: str | None) -> str | None:
387
+ """兼容旧调用点:返回**最长**前缀(规范 id 优先)。"""
388
+ ps = _prefixes(flow)
389
+ return ps[0] if ps else None
390
+
391
+
392
+ def case_prefixes(flow: str | None) -> tuple:
393
+ """**公开入口**:该流程在台账里可能出现的全部 caseId 前缀(规范 id + 指向它的旧短名)。
394
+
395
+ 给"按前缀找该流程 case"的外部工具用(如 `scripts/flow_status_reconcile.py` 的对账);
396
+ ⛔ 别在调用方自己拼 `<flow>__` —— 台账里写的是"当时那条流程叫什么",只认规范前缀会让
397
+ 历史 case **集体消失**(见 `_prefixes` 的说明)。
398
+ """
399
+ return _prefixes(flow)
400
+
401
+
402
+ def pending(path: pathlib.Path, flow: str | None = None, now: int | None = None,
403
+ cooldown_min: int = 15, max_attempts: int = 3) -> list:
404
+ """未处置的失败清单(含分类/唤醒次数/冷却)。
405
+
406
+ 判定:该 case 的**最后一次 failed** 之后,若没有任何处置事件(wake/retry/blocked/done)
407
+ ⇒ 仍未处置。
408
+ """
409
+ now = int(now if now is not None else time.time())
410
+ prefixes = _prefixes(flow)
411
+ events = [e for e in read(path)
412
+ if e.get('caseId') and (not prefixes or str(e['caseId']).startswith(prefixes))]
413
+
414
+ by_case: dict = {}
415
+ for e in events:
416
+ by_case.setdefault(str(e['caseId']), []).append(e)
417
+
418
+ out = []
419
+ for cid, evs in by_case.items():
420
+ evs.sort(key=lambda e: (e.get('ts') or 0))
421
+ failed = [e for e in evs if e.get('event') == 'failed']
422
+ if not failed:
423
+ continue
424
+ last = failed[-1]
425
+ ts = last.get('ts') or 0
426
+ after = [e for e in evs if (e.get('ts') or 0) >= ts and e.get('event') in HANDLED_EVENTS]
427
+ # 2026-09-18(用户实测"失败了没人管"):`blocked` 过去被当成"已处置"⇒ 队列清空、
428
+ # 工作台也看不到 ⇒ 半成品残留这类"等人裁定"的票彻底消失。改为:
429
+ # · blocked 之后**没有** failed_ack ⇒ 仍留在队列(标 awaitingAck,kind=needs_human ⇒ 只展示不唤醒);
430
+ # · 人工在工作台确认(ackCase ⇒ failed_ack)后才真正消案。
431
+ if after:
432
+ acked = [e for e in after if e.get('event') != 'blocked']
433
+ if not acked and any(e.get('event') == 'blocked' for e in after):
434
+ pass # 落到下面:作为"待人工确认"入队
435
+ else:
436
+ continue
437
+ wakes = [e for e in evs if e.get('event') == 'wake']
438
+ last_wake = max([w.get('ts') or 0 for w in wakes], default=0)
439
+ cool = max(0, cooldown_min * 60 - (now - last_wake)) if last_wake else 0
440
+ attempt = last.get('attempt') or len(failed)
441
+ cls = classify(last.get('error') or '', attempt, max_attempts)
442
+ awaiting = bool(after) and all(e.get('event') == 'blocked' for e in after)
443
+ out.append({
444
+ 'caseId': cid,
445
+ 'attempt': attempt,
446
+ 'error': cls['error'],
447
+ 'ts': ts,
448
+ 'phase': last.get('phase'),
449
+ # 已判 blocked 但**人工未确认**(failed_ack)⇒ 只展示、不唤醒(kind=needs_human)
450
+ 'kind': 'needs_human' if awaiting else cls['kind'],
451
+ 'awaitingAck': awaiting,
452
+ 'reason': ('已判 blocked、等人工确认(未 failed_ack)⇒ 不唤醒角色' if awaiting else cls['reason']),
453
+ 'wakeCount': len(wakes),
454
+ 'cooldownLeftSec': cool,
455
+ })
456
+ out.sort(key=lambda x: (-(x['ts'] or 0)))
457
+ return out
458
+
459
+
460
+ def canonical_reason(error_text: str) -> str:
461
+ """台账 `failed.error` → **规范失败原因**(表备注与账本 blocked 的同源同文口径)。
462
+
463
+ 台账里的 `failed.error` 形如 `[rec=<id>] [sku=<…>] [store=<…>] <err_type> <失败原因>`
464
+ (身份前缀是缺陷⑯ 为"摘要自证属于哪张票"加的,见 orchestrator._ledger_record_full_failure)。
465
+ 本函数把**身份前缀剥掉**,剩下的就是引擎写给**表备注**的同一串原因(`orchestrator._fail_note`
466
+ 里的 `<原因>` 部分)⇒ 角色/运维写 `blocked` 时**逐字**用它,就不会再出现
467
+ "表里写『未找到 Vehículo 运输选项』、账本写『平台无预约时段』"这种两处口径不一致
468
+ (生产实测 79096447 的误导就是这样产生的:账本那句是**模型对页面的解读**,不是引擎的判据)。
469
+ """
470
+ text = str(error_text or '').strip()
471
+ # 剥身份前缀:`[rec=…]`、`[sku=…]`、`[store=…]`(可能缺项,只剥**开头连续**的那些)
472
+ while text.startswith('['):
473
+ end = text.find(']')
474
+ if end <= 0 or '=' not in text[:end]:
475
+ break
476
+ text = text[end + 1:].lstrip()
477
+ return text
478
+
479
+
480
+ def reasons(path: pathlib.Path, case_id: str, flow: str | None = None) -> dict:
481
+ """取某 case 的**规范失败原因**(= 引擎写进表备注的同一串,逐字)。
482
+
483
+ 优先读引擎单列的 `reason` 字段(2026-10-09 起写入,与表备注同源同文);老台账没这个字段
484
+ ⇒ 回落到 `error` 去掉身份前缀(`canonical_reason`,也是同一句话,只是还带着 err_type 前缀)。
485
+
486
+ 找不到该 case / 没有 failed 事件 ⇒ `{'ok': False, 'reason': ''}`(调用方据此**照实说
487
+ "取不到规范原因"**,⛔ 不要自己编一句顶上 —— 生产实测 79096447 的误导正是"自己解读"造成的)。
488
+ `case_id` 可给全(`<flow>__<ticket>`)或只给 ticket(给了 `flow` 就自动补前缀)。
489
+ """
490
+ cid = str(case_id or '').strip()
491
+ if flow and cid and '__' not in cid:
492
+ cid = f'{flow}__{cid}'
493
+ if not cid:
494
+ return {'ok': False, 'case_id': '', 'reason': '', 'why': '需要 --case <caseId|ticket>'}
495
+ evs = [e for e in read(path) if str(e.get('caseId') or '') == cid and e.get('event') == 'failed']
496
+ if not evs:
497
+ return {'ok': False, 'case_id': cid, 'reason': '',
498
+ 'why': f'台账里没有 {cid} 的 failed 事件(先确认票号/流程前缀)'}
499
+ evs.sort(key=lambda e: (e.get('ts') or 0))
500
+ last = evs[-1]
501
+ reason = str(last.get('reason') or '').strip() or canonical_reason(last.get('error') or '')
502
+ return {'ok': True, 'case_id': cid, 'reason': reason,
503
+ 'errType': str(last.get('errType') or ''),
504
+ 'source': 'reason' if str(last.get('reason') or '').strip() else 'error(去身份前缀)',
505
+ 'attempt': last.get('attempt'), 'ts': last.get('ts'), 'phase': last.get('phase')}
506
+
507
+
508
+ def main(argv=None) -> int:
509
+ ap = argparse.ArgumentParser(description='失败分类 / 未处置失败扫描 / 规范失败原因')
510
+ ap.add_argument('--error', help='一段失败文本(分类用)')
511
+ ap.add_argument('--attempt', type=int)
512
+ ap.add_argument('--max-attempts', type=int, default=3)
513
+ ap.add_argument('--scan', action='store_true', help='扫描台账里未处置的失败')
514
+ ap.add_argument('--flow', help='只看该流程(caseId 前缀)')
515
+ ap.add_argument('--case', dest='case', help='取该 case 的**规范失败原因**(写账本 blocked 时逐字用)')
516
+ ap.add_argument('--ops-message', action='store_true',
517
+ help='把 --error 组成**运营可见文案**(结论+动作;表字段与台账的同一串)')
518
+ ap.add_argument('--missing', help='逗号分隔的缺口字段名(业务类动作用它写"补齐 X 后…")')
519
+ ap.add_argument('--release-only', action='store_true',
520
+ help='只打印该流程的放行位与运营动作提示(诊断用)')
521
+ ap.add_argument('--limit', type=int, default=OPS_REASON_LIMIT, help='文案长度上限(默认 300 = 台账 detail 上限)')
522
+ ap.add_argument('--ledger', default=str(DEFAULT_LEDGER))
523
+ ap.add_argument('--cooldown-min', type=int, default=15)
524
+ ap.add_argument('--json', action='store_true')
525
+ ap.add_argument('--patterns', action='store_true', help='打印实际生效的分类规则(供门禁比对)')
526
+ a = ap.parse_args(argv)
527
+
528
+ if a.patterns:
529
+ # 门禁用:打印**实际生效**的分类规则(注册表优先,离线时是内置兜底)
530
+ print(json.dumps(EFFECTIVE, ensure_ascii=False, indent=2) if a.json else json.dumps(EFFECTIVE, ensure_ascii=False))
531
+ return 0
532
+
533
+ if a.release_only:
534
+ rc = release_config(a.flow or '')
535
+ print(json.dumps(rc, ensure_ascii=False, indent=2) if a.json
536
+ else f" {a.flow or '(缺 --flow)'}|放行位 {rc['field'] or '(无)'}|重跑 {'开' if rc['enabled'] else '未开'}"
537
+ f"|上限 {rc['maxPerCase'] or '-'} 次|冷却 {rc['cooldownMin'] or '-'} 分|提示 {rc['hint'] or '(空)'}")
538
+ return 0
539
+
540
+ if a.ops_message:
541
+ if not a.error:
542
+ print('需要 --error(原始失败原因;⛔ 不要给空串 —— 正是缺陷 B 的现场)', file=sys.stderr)
543
+ return 2
544
+ miss = [x.strip() for x in str(a.missing or '').split(',') if x.strip()]
545
+ msg = ops_message(a.flow or '', a.error, missing=miss,
546
+ kind=classify(a.error, a.attempt, a.max_attempts).get('kind'), limit=a.limit)
547
+ print(json.dumps({'flow': a.flow, 'category': ops_category(a.error),
548
+ 'message': msg, 'len': len(msg)}, ensure_ascii=False, indent=2) if a.json else msg)
549
+ return 0
550
+
551
+ if a.case:
552
+ # 口径统一的**取数口**:账本 blocked 的 --error 必须逐字用本命令的输出
553
+ # (⛔ 不许改写/翻译/加自己的解读 —— 生产实测就是"解读"把运维带偏的)。
554
+ r = reasons(pathlib.Path(a.ledger), a.case, a.flow)
555
+ if a.json:
556
+ print(json.dumps(r, ensure_ascii=False, indent=2))
557
+ elif r['ok']:
558
+ print(r['reason'])
559
+ else:
560
+ print(f"(取不到规范原因:{r['why']})", file=sys.stderr)
561
+ return 0 if r['ok'] else 2
562
+
563
+ if a.scan:
564
+ items = pending(pathlib.Path(a.ledger), a.flow, cooldown_min=a.cooldown_min,
565
+ max_attempts=a.max_attempts)
566
+ if a.json:
567
+ print(json.dumps({'flow': a.flow, 'pending': items}, ensure_ascii=False, indent=2))
568
+ else:
569
+ if not items:
570
+ print(' 无未处置失败')
571
+ for it in items:
572
+ print(f" {it['caseId']} attempt={it['attempt']} {it['kind']} "
573
+ f"唤醒×{it['wakeCount']} 冷却{it['cooldownLeftSec']}s {it['error'][:60]}")
574
+ return 0
575
+
576
+ if not a.error:
577
+ print('需要 --error(分类)或 --scan(扫描)', file=sys.stderr)
578
+ return 2
579
+ cls = classify(a.error, a.attempt, a.max_attempts)
580
+ if a.json:
581
+ print(json.dumps(cls, ensure_ascii=False))
582
+ else:
583
+ print(f" {cls['kind']}|{cls['reason']}")
584
+ return 0
585
+
586
+
587
+ if __name__ == '__main__':
588
+ raise SystemExit(main())