@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,333 @@
1
+ #!/usr/bin/env python3
2
+ """产物归属解析 —— 单店多标签并发时的**第二道防线**(纯函数,无 I/O、无宿主依赖)。
3
+
4
+ ## 为什么需要它(2026-10-09 评估)
5
+ 单店多标签页并行跑多票时,同一店铺浏览器只有一个下载目录,两票的**箱唛**会先后落进
6
+ 同一个目录。历史上靠"目录里新出现/最新的文件"来认领(`_snapshot_download_dir` 差值),
7
+ 同店并行时必然串号(P5 结论,见 `docs/本机接班运行清单.md` §8.12)。
8
+
9
+ 本模块把"文件名 → 哪一票"这件事**变成一个可测的纯函数**,并钉死一条铁律:
10
+ **判不出来就报错,绝不猜**(`ambiguous` / `unmatched` 两个显式出口,没有"取最新"这种兜底)。
11
+
12
+ ## 关键判据:强键 / 弱键(这是"不能靠感觉"的部分)
13
+ 从今天台账的真实文件名取证(见 `docs/Argent-ML-FULL-*` 评估文档):
14
+
15
+ | 产物 | 原始下载名 | 重命名后(上传飞书的名字) | 含什么键 |
16
+ |---|---|---|---|
17
+ | 箱唛 | `Envio-<货件号>-Etiquetas-de-bultos.pdf` | `79084293-20箱-100张纹身转印纸.pdf` | **货件号** |
18
+ | 产品标 | `Etiquetas-de-producto-<ML码>.pdf` | `产品标+HW-MX-119+MEHQ15678+…+recvvgPQgoUOwx.pdf` | **ML码**(重命名后另有 record_id) |
19
+
20
+ ⇒ **货件号是全局唯一的票级标识;ML码是"产品/listing"级标识,同一 SKU 的两票可能同码。**
21
+ 所以:
22
+
23
+ - **强键(单独命中即可归属,票级唯一)**:`shipment_id`(货件号)、`record_id`(表行号)
24
+ - **弱键(只有"全批次唯一持有者"时才算数)**:`ml_code`
25
+
26
+ ⛔ 这条区分不是洁癖:用户提出的"产品标带 ML 码 ⇒ 可映射"在**同一产品并发两票**时不成立
27
+ (两票 ML 码相同 ⇒ 原始下载名分辨不出来)。因此产品标**不能只靠文件名归属**,
28
+ 必须靠第一道防线(逐标签页下载目录,`<下载目录>/<record_id>/`,见 `per_tab_dir()`)。
29
+ 本模块是"漏进来之后"的兜底与验收判据,不是唯一防线。
30
+
31
+ ## 冲突处置(⛔ 不许猜)
32
+ - 命中 **≥2 个强键** ⇒ `ambiguous`:文件必须移入 `ambiguous/` 子目录**并且报错**(`plan.ambiguous`)。
33
+ - 命中 **0 个强键、≥2 个弱键** ⇒ `ambiguous`(同上:同 ML 码两票就是这种形态)。
34
+ - 命中 **0 个键** ⇒ `unmatched`:既不入账也不猜,等下一轮或交人工。
35
+ - **强键胜出**:恰有 1 个强键 + 另有弱键重叠 ⇒ 归强键那一票(票级唯一 > 产品级),
36
+ 但 `reason` 里记明重叠,便于事后审计。
37
+
38
+ 跑法(回归):`scripts/tests/artifact-attribution.test.py`
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import os
43
+ import re
44
+ from dataclasses import dataclass, field
45
+ from pathlib import Path
46
+ from typing import Any, Iterable, Mapping, Optional, Sequence
47
+
48
+ # ── CDP 下载落点的作用域事实(设计依据,写死在这里防止文档漂移)──────────────────
49
+ # 权威判据 = CDP 协议定义(`Browser.setDownloadBehavior` 的参数只有 `browserContextId`,
50
+ # 没有 targetId/pageId;`Page.setDownloadBehavior` 已被标 deprecated):
51
+ # · Browser.setDownloadBehavior{behavior, browserContextId?, downloadPath, eventsEnabled}
52
+ # ⇒ 作用域 = **BrowserContext**(省略 browserContextId = 默认 context)⇒ **不能按标签页分**
53
+ # · Page.setDownloadBehavior ⇒ deprecated,且无 target 参数(靠所在 session 绑定)
54
+ # ⇒ 本项目所有标签页都取 `contexts[0]`(同一登录态)⇒ **CDP 层做不到"每标签页一个 downloadPath"**。
55
+ # 真正可用的逐页隔离 = Playwright 的 `page.on("download")` + `download.save_as(<票目录>)`
56
+ # (生产日志已实证该事件在 connect_over_cdp 下会触发),即 `per_tab_dir()` 的形态。
57
+ CDP_DOWNLOAD_PATH_SCOPE = "browser-context" # browser | browser-context | page
58
+ CDP_DOWNLOAD_PATH_PER_TAB_OK = False # ⛔ 不要改成 True 除非协议变了
59
+
60
+ # ── 解析规则(正则)────────────────────────────────────────────────────────────
61
+ # 货件号:ML 的 shipment id 是 7~10 位纯数字(实测 79084293 / 79103608 / 77840234)
62
+ RE_SHIPMENT_ID = re.compile(r"(?<!\d)(\d{7,10})(?!\d)")
63
+ # ML码:2~6 个大写字母 + 4~10 位数字(实测 MEHQ15678)。刻意**不**匹配 MXCD05(地址码)
64
+ RE_ML_CODE = re.compile(r"(?<![A-Z0-9])([A-Z]{2,6}\d{4,10})(?![A-Z0-9])")
65
+ # record_id:rec + 10~20 位(实测 recvvgPQgoUOwx / recvs4bZSwb3ej)
66
+ RE_RECORD_ID = re.compile(r"(?<![A-Za-z0-9])(rec[A-Za-z0-9]{10,20})(?![A-Za-z0-9])")
67
+
68
+ # 无关文件(不是本流程的交付物):按历史实现 `_is_claimed_pdf` 的口径
69
+ RE_IRRELEVANT = re.compile(r"(listado|descargar|preparation)", re.IGNORECASE)
70
+
71
+ # 产物分类关键词(中英文都认:原始名是西语,重命名后是中文)
72
+ KW_BOX = ("Etiquetas-de-bultos", "箱唛")
73
+ KW_PRODUCT = ("Etiquetas-de-producto", "产品标")
74
+
75
+ STRONG_KEYS = ("shipment_id", "record_id")
76
+ WEAK_KEYS = ("ml_code",)
77
+
78
+ # 开关:FULL_STORE_TABS(默认 1 = 零行为变化)。上限 3 是**协议级**上限的做法:
79
+ # 见评估文档「并发上限与收益」——单票实测峰值已达 404~414 次/分,×3 会越过平台退化阈值。
80
+ STORE_TABS_DEFAULT = 1
81
+ STORE_TABS_MAX = 3
82
+
83
+
84
+ def store_tabs_from_env(env: Optional[Mapping[str, str]] = None) -> int:
85
+ """读 `FULL_STORE_TABS`,**默认 1**(= 今天的串行行为,一个开关就能回滚)。
86
+
87
+ 非法/越界值一律**收敛到合法区间**而不是报错(这是并发开关,不是数据校验点;
88
+ 取 0/负数会让票静默不跑,故下限钉在 1)。读不到 ⇒ 1。
89
+ """
90
+ raw = (env if env is not None else os.environ).get("FULL_STORE_TABS")
91
+ try:
92
+ n = int(str(raw).strip())
93
+ except (TypeError, ValueError):
94
+ return STORE_TABS_DEFAULT
95
+ return max(1, min(STORE_TABS_MAX, n))
96
+
97
+
98
+ def per_tab_dir(base_dir: Any, record_id: str) -> Path:
99
+ """第一道防线:该票的**专属下载目录**(`<下载目录>/<record_id>/`)。
100
+
101
+ record_id 用**完整值**(不是前 8 位):同批创建的飞书记录前缀相同
102
+ (`recvs4bZSwb3ej` 与 `recvs4bZSwp6YJ` 同为 `recvs4bZ` 开头),截断会互相覆盖。
103
+ """
104
+ rid = str(record_id or "").strip()
105
+ if not rid:
106
+ raise ValueError("per_tab_dir 需要非空 record_id(空值会把两票的产物混进同一目录)")
107
+ if "/" in rid or "\\" in rid or rid in (".", ".."):
108
+ raise ValueError(f"record_id 含路径分隔符,拒绝拼路径:{rid!r}")
109
+ return Path(base_dir) / rid
110
+
111
+
112
+ # ── 数据结构 ──────────────────────────────────────────────────────────────────
113
+ @dataclass(frozen=True)
114
+ class TicketKeys:
115
+ """一票的**可匹配键**。字段名与表/引擎口径一致(别名由 `normalize_ticket` 收敛)。"""
116
+
117
+ record_id: str = ""
118
+ shipment_id: str = ""
119
+ ml_code: str = ""
120
+ sku: str = ""
121
+ store: str = ""
122
+
123
+ @property
124
+ def discriminating(self) -> bool:
125
+ """是否至少有一个可用于归属的键(否则这票无法参与归属判定)。"""
126
+ return bool(self.shipment_id or self.record_id or self.ml_code)
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class Claim:
131
+ """一个"这文件像我"的声明。strength: strong=票级唯一键命中;weak=产品级键命中。"""
132
+
133
+ record_id: str
134
+ key: str
135
+ strength: str # strong | weak
136
+ detail: str = ""
137
+
138
+
139
+ @dataclass
140
+ class IngestPlan:
141
+ """归属计划(纯数据)。调用方按 `by_ticket` 上传、按 `ambiguous` 报错并隔离。"""
142
+
143
+ by_ticket: dict = field(default_factory=dict) # record_id -> [(filename, reason)]
144
+ ambiguous: list = field(default_factory=list) # [{"file","claimants","reason"}]
145
+ unmatched: list = field(default_factory=list) # [filename](无任何键命中)
146
+ irrelevant: list = field(default_factory=list) # [filename](非本流程产物)
147
+
148
+ @property
149
+ def ok(self) -> bool:
150
+ """只有"没有歧义"才算干净 —— 未匹配不算错(可能是别的东西),歧义一定是错。"""
151
+ return not self.ambiguous
152
+
153
+ def to_dict(self) -> dict:
154
+ return {
155
+ "by_ticket": {k: [n for n, _ in v] for k, v in self.by_ticket.items()},
156
+ "ambiguous": list(self.ambiguous),
157
+ "unmatched": list(self.unmatched),
158
+ "irrelevant": list(self.irrelevant),
159
+ "ok": self.ok,
160
+ }
161
+
162
+
163
+ # ── 键提取与归一 ──────────────────────────────────────────────────────────────
164
+ def extract_keys(filename: str) -> dict:
165
+ """从文件名抽出三族键(各为 frozenset,可能多值)。纯函数、不抛异常。"""
166
+ name = str(filename or "")
167
+ return {
168
+ "shipment_id": frozenset(RE_SHIPMENT_ID.findall(name)),
169
+ "ml_code": frozenset(RE_ML_CODE.findall(name)),
170
+ "record_id": frozenset(RE_RECORD_ID.findall(name)),
171
+ }
172
+
173
+
174
+ def classify_filename(filename: str) -> str:
175
+ """`box_label` / `product_label` / `irrelevant` / `other`。"""
176
+ name = str(filename or "")
177
+ if RE_IRRELEVANT.search(name):
178
+ return "irrelevant"
179
+ if any(k in name for k in KW_BOX):
180
+ return "box_label"
181
+ if any(k in name for k in KW_PRODUCT):
182
+ return "product_label"
183
+ return "other"
184
+
185
+
186
+ # 表字段 → TicketKeys 的别名收敛表(lip / legacy / 引擎内部名都可能出现)
187
+ _FIELD_ALIASES = {
188
+ "record_id": ("record_id", "recordId", "_record_id", "id"),
189
+ "shipment_id": ("shipment_id", "shipmentId", "货件号", "货件编号"),
190
+ "ml_code": ("ml_code", "mlCode", "ML码", "ML编码"),
191
+ "sku": ("sku", "SKU"),
192
+ "store": ("store", "store_name", "storeName", "店铺"),
193
+ }
194
+
195
+
196
+ def normalize_ticket(src: Any) -> TicketKeys:
197
+ """把 dict / TicketKeys / 具名对象 收敛成 TicketKeys(缺字段留空,不抛)。"""
198
+ if isinstance(src, TicketKeys):
199
+ return src
200
+ get = src.get if isinstance(src, Mapping) else (lambda k, d=None: getattr(src, k, d))
201
+ out = {}
202
+ for canon, aliases in _FIELD_ALIASES.items():
203
+ val = ""
204
+ for a in aliases:
205
+ try:
206
+ v = get(a)
207
+ except Exception: # noqa: BLE001
208
+ v = None
209
+ if v not in (None, ""):
210
+ val = str(v).strip()
211
+ break
212
+ out[canon] = val
213
+ return TicketKeys(**out)
214
+
215
+
216
+ # ── 归属判定 ──────────────────────────────────────────────────────────────────
217
+ def _ml_holders(tickets: Sequence[TicketKeys]) -> dict:
218
+ """ml_code → 持有它的票数(=1 才够"票级唯一",才能当强键用)。"""
219
+ holders: dict = {}
220
+ for t in tickets:
221
+ if t.ml_code:
222
+ holders[t.ml_code] = holders.get(t.ml_code, 0) + 1
223
+ return holders
224
+
225
+
226
+ def claimants(filename: str, tickets: Sequence[TicketKeys]) -> list:
227
+ """列出所有"自称是本票产物"的票(去重,保留最强的那个键)。空列表 = 无票认领。"""
228
+ keys = extract_keys(filename)
229
+ holders = _ml_holders(tickets)
230
+ out: dict = {}
231
+ for t in tickets:
232
+ if not t.record_id:
233
+ continue # 没有 record_id 无法归属(也无法落账)
234
+ claim: Optional[Claim] = None
235
+ if t.shipment_id and t.shipment_id in keys["shipment_id"]:
236
+ claim = Claim(t.record_id, "shipment_id", "strong", f"货件号 {t.shipment_id}")
237
+ elif t.record_id in keys["record_id"]:
238
+ claim = Claim(t.record_id, "record_id", "strong", f"record_id {t.record_id}")
239
+ elif t.ml_code and t.ml_code in keys["ml_code"]:
240
+ n = holders.get(t.ml_code, 0)
241
+ if n == 1:
242
+ claim = Claim(t.record_id, "ml_code", "strong",
243
+ f"ML码 {t.ml_code}(全批次唯一持有者)")
244
+ else:
245
+ claim = Claim(t.record_id, "ml_code", "weak",
246
+ f"ML码 {t.ml_code}(**{n} 票共有**,不足以定票)")
247
+ if claim is not None:
248
+ out[t.record_id] = claim
249
+ return list(out.values())
250
+
251
+
252
+ def plan_ingest(filenames: Iterable[str], tickets: Iterable[Any]) -> IngestPlan:
253
+ """把一批文件名分配给一批票。**判不出就报歧义,绝不猜**(核心判据,纯函数)。"""
254
+ tks = [normalize_ticket(t) for t in tickets]
255
+ plan = IngestPlan()
256
+ for name in filenames:
257
+ fname = str(name)
258
+ if classify_filename(fname) == "irrelevant":
259
+ plan.irrelevant.append(fname)
260
+ continue
261
+ claims = claimants(fname, tks)
262
+ strong = [c for c in claims if c.strength == "strong"]
263
+ weak = [c for c in claims if c.strength == "weak"]
264
+ if len(strong) == 1:
265
+ # 强键票级唯一 ⇒ 归属确定;弱键重叠只记账(不改变结论)
266
+ extra = (";另有弱键重叠:" + ", ".join(sorted(c.record_id for c in weak))
267
+ if weak else "")
268
+ plan.by_ticket.setdefault(strong[0].record_id, []).append(
269
+ (fname, f"强键 {strong[0].detail}{extra}"))
270
+ elif len(strong) >= 2:
271
+ plan.ambiguous.append({
272
+ "file": fname,
273
+ "claimants": sorted(c.record_id for c in strong),
274
+ "reason": "命中 ≥2 个票级唯一键(强键冲突)⇒ 移入 ambiguous/ 并报错,不得猜",
275
+ "keys": sorted({c.key for c in strong}),
276
+ })
277
+ elif len(weak) == 1:
278
+ plan.by_ticket.setdefault(weak[0].record_id, []).append(
279
+ (fname, f"弱键 {weak[0].detail}"))
280
+ elif len(weak) >= 2:
281
+ plan.ambiguous.append({
282
+ "file": fname,
283
+ "claimants": sorted(c.record_id for c in weak),
284
+ "reason": (f"仅弱键(ML码)命中且 {len(weak)} 票共有 ⇒ 不足以定票 ⇒ "
285
+ "移入 ambiguous/ 并报错,不得猜"),
286
+ "keys": sorted({c.key for c in weak}),
287
+ })
288
+ else:
289
+ plan.unmatched.append(fname)
290
+ return plan
291
+
292
+
293
+ def ambiguous_moves(plan: IngestPlan, download_dir: Any,
294
+ ambiguous_dirname: str = "ambiguous") -> list:
295
+ """歧义文件的**隔离动作**(`下载目录/<file>` → `下载目录/ambiguous/<file>`)。
296
+
297
+ 只算路径、不落盘(保持纯函数)⇒ 便于在夹具里断言,也便于 dry-run 打印。
298
+ """
299
+ base = Path(download_dir)
300
+ dst_dir = base / ambiguous_dirname
301
+ return [(base / str(a["file"]), dst_dir / str(a["file"]))
302
+ for a in plan.ambiguous]
303
+
304
+
305
+ def apply_moves(moves: Iterable[Any]) -> list:
306
+ """执行 `ambiguous_moves()` 的搬运(唯一有 I/O 的函数,显式与纯函数分开)。
307
+
308
+ 目标已存在 ⇒ 加 `.1`/`.2` 后缀,**绝不覆盖**(歧义文件也是证据,不能被吞掉)。
309
+ """
310
+ done = []
311
+ for src, dst in moves:
312
+ s, d = Path(src), Path(dst)
313
+ if not s.exists():
314
+ continue
315
+ d.parent.mkdir(parents=True, exist_ok=True)
316
+ target = d
317
+ i = 0
318
+ while target.exists():
319
+ i += 1
320
+ target = d.with_name(f"{d.stem}.{i}{d.suffix}")
321
+ s.replace(target)
322
+ done.append((str(s), str(target)))
323
+ return done
324
+
325
+
326
+ __all__ = [
327
+ "CDP_DOWNLOAD_PATH_SCOPE", "CDP_DOWNLOAD_PATH_PER_TAB_OK",
328
+ "STRONG_KEYS", "WEAK_KEYS", "STORE_TABS_DEFAULT", "STORE_TABS_MAX",
329
+ "TicketKeys", "Claim", "IngestPlan",
330
+ "store_tabs_from_env", "per_tab_dir",
331
+ "extract_keys", "classify_filename", "normalize_ticket",
332
+ "claimants", "plan_ingest", "ambiguous_moves", "apply_moves",
333
+ ]