@whyshu/argent-engine 0.0.0-stage → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.json +307 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +377 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +134 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +46 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +260 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1024 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2275 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +194 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent_datadir.py +262 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/run-tests.sh +256 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +136 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +73 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +34 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +706 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +21 -4
- 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
|
+
]
|