@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,503 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""wenyao_run.py — 上海文鳐发票流程**编排器**(2026-09-11)
|
|
3
|
+
|
|
4
|
+
把 ①→⑫ 串成带阶段号的单向流水线,供单票/批量调用,并把每阶段结果落成 JSONL 事件
|
|
5
|
+
(`~/.dsh/argent/wenyao-logs/run-<货件号>-<时间>.jsonl`),便于上板/审计。
|
|
6
|
+
|
|
7
|
+
两种模式
|
|
8
|
+
--------
|
|
9
|
+
· `--shadow`(**影子模式**):只跑**本地无副作用**的阶段(读飞书行 → 取数 → 填报 → 自检),
|
|
10
|
+
并把后续会做什么**逐条打印成计划**;**不写飞书、不开浏览器、不导入不导出**。
|
|
11
|
+
用来把推演结果和真实链路比对,确认无误再切主。
|
|
12
|
+
· `--yes`(真跑):按阶段顺序执行,包含飞书回写与门户写操作。
|
|
13
|
+
|
|
14
|
+
阶段
|
|
15
|
+
----
|
|
16
|
+
P1 路由 读 lip 行,判定货代=上海文鳐,选门户模板
|
|
17
|
+
P2 取数 汇总填报输入 + 下载 lip 产品图(按需)
|
|
18
|
+
P3 填报 调 wenyao_fill_template 生成 xlsx(只写第 2、8 行)
|
|
19
|
+
P4 自检 锚点/装得下/G2/除 2、8 行外逐格一致
|
|
20
|
+
P5 回写① 发票文件 + 状态「填写中」
|
|
21
|
+
P6 登录 门户登录态(过期则人工过滑块)
|
|
22
|
+
P7 查重 按货件号查门户(0 条才继续)
|
|
23
|
+
P8 导入 导入订单(**对方生产系统写操作**)
|
|
24
|
+
P9 勾选 查回并勾选该货件号
|
|
25
|
+
P10 进仓单 导出进仓单(集货仓 - 东莞集货仓),只留本地
|
|
26
|
+
P11 唛头 导出唛头(异步任务)
|
|
27
|
+
P12 取件 下载 DDP 包 → 解压 → 按 `运单-<货件号>-<箱数>箱-<中文品名>.pdf` 改名
|
|
28
|
+
P13 回写② 大货标签
|
|
29
|
+
P14 收口 发票状态=已完成 + 清过时错误信息 + 释放「发票就绪」放行勾
|
|
30
|
+
|
|
31
|
+
并发纪律(2026-10-12 事故固化 —— 三条票 `79212271 / 79298694 / 79210359` 同抢同一个标签页)
|
|
32
|
+
--------------------------------------------------------------------------------------
|
|
33
|
+
· **每条跑批一个自己的标签页**:门户模块 `connect()` 默认 `context.new_page()` 新建,
|
|
34
|
+
并在 `finally` 只关自己那个页(`sess.close()`);⛔ 用户自己开着的门户页一个都不动。
|
|
35
|
+
旧实现三条执行者复用**同一个 page** ⇒ 导航/点击互相打断 ⇒ 死循环。
|
|
36
|
+
· **门户阶段串行**:P6→P14 包在门户模块 `portal_lock()`(每门户一把 flock,
|
|
37
|
+
`<数据目录>/` 下的门户锁文件,有界等待 300s / `WENYAO_PORTAL_LOCK_TIMEOUT`,抢锁每 15s 打印一行)
|
|
38
|
+
⇒ 超时**明确失败**(⛔ 绝不无限等待),且锁在**开浏览器之前**拿(超时连页都不开)。
|
|
39
|
+
· **每票自己的下载目录**:`portal-<货件号>/`(在既有下载根目录下),下载行为尽量按 context 绑定,
|
|
40
|
+
下载后校验 `suggestedFilename` 与包内容归属,不符即报错(⛔ 不静默用错文件)。
|
|
41
|
+
· 开始/结束各打一行:`[门户] 本次标签页=<targetId 前 8 位>|锁=<获得/等待 Ns>|票=<货件号>`
|
|
42
|
+
(与阶段日志**同源**:都走本文件的 `log()`,⛔ 不新造第二套日志)。
|
|
43
|
+
|
|
44
|
+
用法
|
|
45
|
+
----
|
|
46
|
+
# 影子模式(不产生任何副作用)
|
|
47
|
+
python3 scripts/wenyao_run.py --shipment 76637122 \
|
|
48
|
+
--english-name "42-Piece Professional Makeup Brush and Sponge Cosmetic Bag - Pink" --shadow
|
|
49
|
+
|
|
50
|
+
# 真跑
|
|
51
|
+
python3 scripts/wenyao_run.py --shipment 76637122 --english-name "…" --yes
|
|
52
|
+
|
|
53
|
+
材质英文(S 列 `{材质},{材质英文}`)走**三级解析**:① SKU 字典 → ② 通用小词典 →
|
|
54
|
+
③ `--material-en`(会话传入,政策A 允许 LLM 译)。三级都没命中 ⇒ P3 **阻断**(不落盘)并
|
|
55
|
+
指路「请翻译成英文后用 --material-en 重跑」;③ 命中时真跑会把译文写回字典(confirmed:false)。
|
|
56
|
+
⛔ HS 编码那条人工红线**只管 HS**(不预填/不写候选/不自动确认),不适用于材质英文。
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import argparse
|
|
61
|
+
import importlib.util
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import subprocess
|
|
65
|
+
import sys
|
|
66
|
+
import time
|
|
67
|
+
from datetime import datetime
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
|
|
70
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
71
|
+
HOME = Path.home()
|
|
72
|
+
# 数据目录:**唯一判据**在 `scripts/lib/argent_datadir.py`(三级链,与 JS 内核同语义)
|
|
73
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
|
|
74
|
+
from argent_datadir import resolve_data_dir # noqa: E402 —— 勿在此复制判据
|
|
75
|
+
ARG = Path(resolve_data_dir()['dir'])
|
|
76
|
+
OUT_DIR = ARG / "wenyao-output"
|
|
77
|
+
DOWN_DIR = ARG / "wenyao-downloads"
|
|
78
|
+
FIX_DIR = ARG / "wenyao-fixtures"
|
|
79
|
+
LOG_DIR = ARG / "wenyao-logs"
|
|
80
|
+
|
|
81
|
+
# 2026-09-20(迭代1-残留项):表/身份/字段 id **一律运行时解析**,不再写死。
|
|
82
|
+
# 旧写法(BASE/TABLE/PROFILE + 21 条 fld id)的后果:换客户=改代码;客户另建同名字段的表时
|
|
83
|
+
# id 全不同 ⇒ 写回/上传会打到错的列。解析入口见 scripts/lip_table.py(表走部署 env,字段 id 走
|
|
84
|
+
# `+field-list` 运行时映射);解析失败**响亮失败**,不猜表。
|
|
85
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
86
|
+
import lip_table as _lip # noqa: E402
|
|
87
|
+
|
|
88
|
+
_TARGET = _lip.target("wenyao-invoice")
|
|
89
|
+
BASE = _TARGET["base"]
|
|
90
|
+
TABLE = _TARGET["table"]
|
|
91
|
+
PROFILE = _TARGET["profile"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def FIELD_ID_OF(name: str) -> str:
|
|
95
|
+
"""字段名 → 表内字段 id(运行时解析;查不到则原样按名传)。"""
|
|
96
|
+
return _lip.field_id(name, _TARGET)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def log(phase: str, msg: str, **extra) -> None:
|
|
100
|
+
line = {"at": datetime.now().isoformat(timespec="seconds"), "phase": phase, "msg": msg, **extra}
|
|
101
|
+
print(f"[wenyao][{phase}] {msg}" + (f" {json.dumps(extra, ensure_ascii=False)}" if extra else ""))
|
|
102
|
+
if _LOG is not None:
|
|
103
|
+
with _LOG.open("a") as f:
|
|
104
|
+
f.write(json.dumps(line, ensure_ascii=False) + "\n")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_LOG = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ───────────────────────── lip 读写 ─────────────────────────
|
|
111
|
+
|
|
112
|
+
def _lark(args: list) -> dict:
|
|
113
|
+
out = subprocess.run(["lark-cli", "--profile", PROFILE, "base", *args, "--as", "bot", "--format", "json"],
|
|
114
|
+
capture_output=True, text=True, check=True).stdout
|
|
115
|
+
j = json.loads(out)
|
|
116
|
+
if j.get("ok") is False:
|
|
117
|
+
raise RuntimeError(f"lark-cli: {j.get('error', {}).get('code')} {j.get('error', {}).get('message')}")
|
|
118
|
+
return j
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def read_row(record_id: str, names: list, batch: int = 7) -> dict:
|
|
122
|
+
"""按**字段名**分批窄投影读(一次 ≤7 列,避免宽投影错位;按接口回显的 fields 名映射)。"""
|
|
123
|
+
out: dict = {}
|
|
124
|
+
for i in range(0, len(names), batch):
|
|
125
|
+
chunk = names[i:i + batch]
|
|
126
|
+
args = ["+record-get", "--base-token", BASE, "--table-id", TABLE, "--record-id", record_id]
|
|
127
|
+
for n in chunk:
|
|
128
|
+
args += ["--field-id", FIELD_ID_OF(n)]
|
|
129
|
+
d = _lark(args)["data"]
|
|
130
|
+
for n, v in zip(d["fields"], d["data"][0]):
|
|
131
|
+
out[n] = v
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def find_record_id(shipment_no: str) -> str | None:
|
|
136
|
+
"""按货件号找 record_id(只取两列,避免宽投影)。"""
|
|
137
|
+
out = subprocess.run(["lark-cli", "--profile", PROFILE, "base", "+record-list", "--base-token", BASE,
|
|
138
|
+
"--table-id", TABLE, "--as", "bot", "--format", "ndjson", "--limit", "2000",
|
|
139
|
+
"--field-id", "货件号"], capture_output=True, text=True, check=True).stdout
|
|
140
|
+
man = json.loads(out)
|
|
141
|
+
for line in open(man["record_file"]):
|
|
142
|
+
if not line.strip().startswith("{"):
|
|
143
|
+
continue
|
|
144
|
+
r = json.loads(line)
|
|
145
|
+
if str(r.get("货件号", "")).strip() == shipment_no:
|
|
146
|
+
return r["record_id"]
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def pick(v):
|
|
151
|
+
if isinstance(v, list):
|
|
152
|
+
return pick(v[0]) if v else ""
|
|
153
|
+
return v if v is not None else ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ───────────────────────── 阶段实现 ─────────────────────────
|
|
157
|
+
|
|
158
|
+
def p1_route(shipment_no: str) -> dict:
|
|
159
|
+
rid = find_record_id(shipment_no)
|
|
160
|
+
if not rid:
|
|
161
|
+
raise SystemExit(f"❌ lip 表里找不到货件号 {shipment_no}")
|
|
162
|
+
row = read_row(rid, ["货件号", "货代公司", "发票状态", "FULL状态", "国家", "产品名称"])
|
|
163
|
+
carrier = pick(row.get("货代公司"))
|
|
164
|
+
status = pick(row.get("发票状态"))
|
|
165
|
+
log("P1", f"record_id={rid} 货代={carrier} 发票状态={status or '(空)'} FULL={pick(row.get('FULL状态'))}")
|
|
166
|
+
if carrier != "上海文鳐":
|
|
167
|
+
raise SystemExit(f"❌ 货代是「{carrier}」,本编排只处理上海文鳐")
|
|
168
|
+
if status in ("已完成",):
|
|
169
|
+
log("P1", f"⚠️ 该行已是「{status}」,重跑前请确认", level="warn")
|
|
170
|
+
return {"record_id": rid, "carrier": carrier, "status": status}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def p2_fetch(record_id: str) -> dict:
|
|
174
|
+
names = ["产品名称", "货件号", "SKU", "店铺", "数量", "箱数", "装箱数",
|
|
175
|
+
"重量", "长", "宽", "高", "材质", "HS编码", "产品图"]
|
|
176
|
+
row = read_row(record_id, names)
|
|
177
|
+
data = {k: pick(v) for k, v in row.items()}
|
|
178
|
+
img = row.get("产品图")
|
|
179
|
+
img = img[0] if isinstance(img, list) and img else None
|
|
180
|
+
log("P2", f"取数完成:{data['产品名称']} / 数量{data['数量']} / 箱数{data['箱数']} / HS {data['HS编码']}"
|
|
181
|
+
f" / 图片 {'有' if img else '无'}")
|
|
182
|
+
local_img = None
|
|
183
|
+
if img:
|
|
184
|
+
local_img = DOWN_DIR / f"{data['货件号']}-产品图.png"
|
|
185
|
+
if local_img.exists() and local_img.stat().st_size == int(img.get("size") or 0):
|
|
186
|
+
log("P2", f"产品图已存在,跳过下载 → {local_img.name}")
|
|
187
|
+
else:
|
|
188
|
+
subprocess.run(["lark-cli", "--profile", PROFILE, "api", "GET",
|
|
189
|
+
f"/open-apis/drive/v1/medias/{img['file_token']}/download",
|
|
190
|
+
"--as", "bot", "--output", f"./{local_img.name}"],
|
|
191
|
+
cwd=str(DOWN_DIR), capture_output=True, text=True, check=True)
|
|
192
|
+
log("P2", f"产品图已下载 → {local_img.name}({local_img.stat().st_size} B)")
|
|
193
|
+
# 归一成填报器要的行结构
|
|
194
|
+
fixture = {"产品名称": data["产品名称"], "货件号": data["货件号"], "SKU": data["SKU"],
|
|
195
|
+
"店铺": data["店铺"], "数量": int(float(data["数量"])), "箱数": int(float(data["箱数"])),
|
|
196
|
+
"装箱数": int(float(data["装箱数"])), "重量": float(data["重量"]),
|
|
197
|
+
"长": float(data["长"]), "宽": float(data["宽"]), "高": float(data["高"]),
|
|
198
|
+
"材质": data["材质"], "HS编码": data["HS编码"],
|
|
199
|
+
"产品图": (img or {}).get("file_token", "")}
|
|
200
|
+
FIX_DIR.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
fx = FIX_DIR / f"{data['货件号']}.json"
|
|
202
|
+
fx.write_text(json.dumps(fixture, ensure_ascii=False))
|
|
203
|
+
return {"fixture": fixture, "fixture_path": str(fx), "image": str(local_img) if local_img else None}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _load_filler():
|
|
207
|
+
spec = importlib.util.spec_from_file_location("wenyao_fill", ROOT / "scripts" / "wenyao_fill_template.py")
|
|
208
|
+
mod = importlib.util.module_from_spec(spec)
|
|
209
|
+
spec.loader.exec_module(mod)
|
|
210
|
+
return mod
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def p3_fill(fixture_path: str, english_name: str, today: str, image: str | None,
|
|
214
|
+
material_en: str = "", write_dict: bool = False) -> dict:
|
|
215
|
+
filler = _load_filler()
|
|
216
|
+
m = filler.load_map()
|
|
217
|
+
row = json.loads(Path(fixture_path).read_text())
|
|
218
|
+
plan = filler.plan_cells(row, m, today, english_name, material_en=material_en)
|
|
219
|
+
out = OUT_DIR / f"{row['货件号']}-上海文鳐发票-{row['产品名称']}.xlsx"
|
|
220
|
+
mat = plan["material"]
|
|
221
|
+
# ── 材质英文:三级解析(① SKU 字典 → ② 小词典 → ③ --material-en)────────────────
|
|
222
|
+
# ⛔ 闸门判据 = **三级都无法解析**(`plan['gaps']`,文案已指路:请翻译后用 --material-en 重跑),
|
|
223
|
+
# **不是**「英文为空 ⇒ 按报关敏感字段拦」——「不预填/不写候选/不自动确认」那条人工红线
|
|
224
|
+
# **只管 HS 编码**;把它套到材质英文上正是 2026-10-12 两条票被误阻的成因。
|
|
225
|
+
log("P3", f"材质英文:{mat['中文'] or '(空)'},{mat['英文'] or '(空)'}"
|
|
226
|
+
f"(命中={mat['命中'] or '未命中'})",
|
|
227
|
+
**({"来源": mat["来源"]} if mat.get("来源") else {}))
|
|
228
|
+
for _n in mat["备注"]:
|
|
229
|
+
log("P3", f"材质英文备注:{_n}")
|
|
230
|
+
if mat.get("冲突"):
|
|
231
|
+
log("P3", f"材质英文冲突(未写回,交人):{mat['冲突']}")
|
|
232
|
+
# ⚠️ 缺口必须在**写盘之前**拦(2026-09-19 修):原顺序是先 fill_xlsx 落盘、再 raise,
|
|
233
|
+
# 于是"缺卖家ID"这种票会在产物目录里留下一张**内容不全的 xlsx** —— 虽然不会被上传,
|
|
234
|
+
# 但放在那里迟早被人当成成品拿去导入(与本次"错 PO 静默顶替"同一类风险:错了还留在台面上)。
|
|
235
|
+
if plan["gaps"]:
|
|
236
|
+
# 冲突的审计要在这里落(写回路径走不到 ⇒ 不记的话交人后无从复核;无冲突时是 no-op)
|
|
237
|
+
filler.record_material_audit(plan)
|
|
238
|
+
raise SystemExit(f"❌ 填报存在缺口,**未生成文件**:{plan['gaps']}")
|
|
239
|
+
filler.fill_xlsx(ARG / "wenyao-downloads" / "跨境电商-下单模板.xlsx",
|
|
240
|
+
plan, out, image or None)
|
|
241
|
+
# ③ 命中(会话传入定音)⇒ 写回 SKU 字典(confirmed:false,政策A)+ 审计留痕。
|
|
242
|
+
# 影子/预演**不写字典**(保持"零副作用"承诺),只把写回计划报出来。
|
|
243
|
+
if write_dict:
|
|
244
|
+
wb = filler.apply_material_writeback(plan)
|
|
245
|
+
else:
|
|
246
|
+
planned = mat.get("写回") or {}
|
|
247
|
+
wb = {"written": False, "动作": "预演:不写字典", "sku": planned.get("sku", ""),
|
|
248
|
+
"条目": planned.get("条目")}
|
|
249
|
+
if planned:
|
|
250
|
+
log("P3", f"材质英文写回计划(真跑 P3 会写字典):{wb['sku']} → "
|
|
251
|
+
f"{(wb['条目'] or {}).get('材质英文')}(confirmed=false)")
|
|
252
|
+
if wb.get("written"):
|
|
253
|
+
log("P3", f"材质英文写回字典:{wb['sku']} → {(wb.get('条目') or {}).get('材质英文')}"
|
|
254
|
+
f"(confirmed=false;{wb.get('动作')})", 字典=wb.get("path", ""), 审计=wb.get("审计", ""))
|
|
255
|
+
log("P3", f"已生成 {out.name}({out.stat().st_size} B)锚点={plan['image_cell']} 缺口={plan['gaps']}")
|
|
256
|
+
return {"xlsx": str(out), "gaps": plan["gaps"], "image_cell": plan["image_cell"],
|
|
257
|
+
"material": {k: v for k, v in mat.items() if k != "字典"}, "writeback": wb}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def p4_verify(xlsx: str, template: str) -> dict:
|
|
261
|
+
import openpyxl
|
|
262
|
+
a = openpyxl.load_workbook(xlsx)["Sheet1"]
|
|
263
|
+
b = openpyxl.load_workbook(template)["Sheet1"]
|
|
264
|
+
diffs = []
|
|
265
|
+
for r in range(1, max(a.max_row, b.max_row) + 1):
|
|
266
|
+
if r in (2, 8):
|
|
267
|
+
continue
|
|
268
|
+
for c in range(1, max(a.max_column, b.max_column) + 1):
|
|
269
|
+
if a.cell(r, c).value != b.cell(r, c).value:
|
|
270
|
+
diffs.append(f"{openpyxl.utils.get_column_letter(c)}{r}")
|
|
271
|
+
ws = a
|
|
272
|
+
anchor = None
|
|
273
|
+
for im in getattr(ws, "_images", []):
|
|
274
|
+
fr = im.anchor._from
|
|
275
|
+
anchor = f"{openpyxl.utils.get_column_letter(fr.col + 1)}{fr.row + 1}"
|
|
276
|
+
# ⚠️ 必须读 drawing XML 的 <ext>:openpyxl 的 im.width/im.height 是**原图像素**,不是显示尺寸
|
|
277
|
+
import re as _re, zipfile as _zip
|
|
278
|
+
w = h = 0
|
|
279
|
+
with _zip.ZipFile(xlsx) as z:
|
|
280
|
+
xml = z.read("xl/drawings/drawing1.xml").decode()
|
|
281
|
+
m2 = _re.search(r'<ext cx="(\d+)" cy="(\d+)"', xml)
|
|
282
|
+
if m2:
|
|
283
|
+
w, h = round(int(m2.group(1)) / 9525), round(int(m2.group(2)) / 9525)
|
|
284
|
+
box_w = round(ws.column_dimensions["AA"].width * 7 + 5)
|
|
285
|
+
box_h = round(ws.row_dimensions[8].height * 96 / 72)
|
|
286
|
+
res = {"only_rows_2_8_changed": not diffs, "diffs": diffs[:10], "anchor": anchor,
|
|
287
|
+
"g2": ws["G2"].value, "images": len(getattr(ws, "_images", [])),
|
|
288
|
+
"image_in_box": (0 < w <= box_w and 0 < h <= box_h)}
|
|
289
|
+
log("P4", f"自检:只改两行={res['only_rows_2_8_changed']} 差异={len(diffs)} 图={res['images']}张@{anchor} "
|
|
290
|
+
f"装得下={res['image_in_box']} G2={res['g2']}")
|
|
291
|
+
if diffs:
|
|
292
|
+
raise SystemExit(f"❌ 除第 2、8 行外有差异:{diffs[:6]}")
|
|
293
|
+
return res
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _writeback(record_id: str, field: str, file: str, status: str, clear_error: bool,
|
|
297
|
+
real: bool, clear_ready: bool = False) -> dict:
|
|
298
|
+
cmd = ["node", str(ROOT / "scripts" / "wenyao-invoice-writeback.mjs"),
|
|
299
|
+
"--record-id", record_id, "--allow-record", record_id,
|
|
300
|
+
"--field", field, "--file", file, "--status", status]
|
|
301
|
+
if clear_error:
|
|
302
|
+
cmd.append("--clear-error")
|
|
303
|
+
if clear_ready:
|
|
304
|
+
# 2026-09-18:释放运营的放行勾(**单向**:只允许 false;勾选权永远归运营)。
|
|
305
|
+
# 漏这一步的后果:该行长期停在 `FULL状态=已完成 ∧ 发票就绪=true` = 表上看着"还等着跑";
|
|
306
|
+
# 且一旦台账那條 done 失效(身份漂移/台账轮转),开启闸门会重新放行 ⇒ 重复开票/重复上传。
|
|
307
|
+
# 鑫中航 runner 一直有这个动作,文鳐此前漏了(全表 38 条已完成行唯一一条残留即由此而来)。
|
|
308
|
+
cmd.append("--clear-ready")
|
|
309
|
+
cmd.append("--yes" if real else "--dry-run")
|
|
310
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
311
|
+
print("\n".join(" " + l for l in r.stdout.strip().splitlines()))
|
|
312
|
+
return {"rc": r.returncode, "stdout": r.stdout[-1500:]}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ───────────────────────── 主流程 ─────────────────────────
|
|
316
|
+
|
|
317
|
+
def main() -> int:
|
|
318
|
+
global _LOG
|
|
319
|
+
ap = argparse.ArgumentParser()
|
|
320
|
+
ap.add_argument("--shipment", required=True, help="货件号,如 76637122")
|
|
321
|
+
ap.add_argument("--english-name", required=True, help="英文品名(LLM 产出,人工/上游提供)")
|
|
322
|
+
ap.add_argument("--today", default="")
|
|
323
|
+
ap.add_argument("--material-en", default="",
|
|
324
|
+
help="材质英文(三级解析的第③级:SKU 字典与小词典都没命中时由会话传入;"
|
|
325
|
+
"政策A 允许 LLM 译,命中后写回字典并标 confirmed=false)")
|
|
326
|
+
ap.add_argument("--shadow", action="store_true", help="影子模式:不产生任何副作用")
|
|
327
|
+
ap.add_argument("--yes", action="store_true", help="真跑")
|
|
328
|
+
ap.add_argument("--from", dest="from_phase", default="P1",
|
|
329
|
+
help="从哪个阶段开始(断点续跑),如 --from P12")
|
|
330
|
+
a = ap.parse_args()
|
|
331
|
+
real = bool(a.yes) and not a.shadow
|
|
332
|
+
frm = int(str(a.from_phase).upper().lstrip("P") or 1)
|
|
333
|
+
if not (a.shadow or a.yes):
|
|
334
|
+
raise SystemExit("必须显式指定 --shadow(推演)或 --yes(真跑)")
|
|
335
|
+
|
|
336
|
+
today = a.today or datetime.now().strftime("%Y-%m-%d")
|
|
337
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
_LOG = LOG_DIR / f"run-{a.shipment}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.jsonl"
|
|
339
|
+
log("P0", f"模式={'影子(shadow)' if a.shadow else '真跑'} 货件号={a.shipment} 模板={today}", log=str(_LOG))
|
|
340
|
+
|
|
341
|
+
# ── 本地阶段(影子模式也执行)──
|
|
342
|
+
r1 = p1_route(a.shipment)
|
|
343
|
+
r2 = p2_fetch(r1["record_id"])
|
|
344
|
+
r3 = {"xlsx": str(OUT_DIR / f"{a.shipment}-上海文鳐发票-{r2['fixture']['产品名称']}.xlsx")}
|
|
345
|
+
if frm <= 4 and not a.shadow:
|
|
346
|
+
pass
|
|
347
|
+
if frm <= 3 or a.shadow:
|
|
348
|
+
# write_dict=真跑:③ 命中时把材质英文写回 SKU 字典(影子/预演只报写回计划,不写)
|
|
349
|
+
r3 = p3_fill(r2["fixture_path"], a.english_name, today, r2["image"],
|
|
350
|
+
material_en=a.material_en, write_dict=real)
|
|
351
|
+
if frm <= 4 or a.shadow:
|
|
352
|
+
r4 = p4_verify(r3["xlsx"], str(DOWN_DIR / "跨境电商-下单模板.xlsx"))
|
|
353
|
+
else:
|
|
354
|
+
r4 = {"skipped": True}
|
|
355
|
+
if not Path(r3["xlsx"]).exists():
|
|
356
|
+
raise SystemExit(f"❌ 找不到填报件 {r3['xlsx']}(--from 续跑需要它已存在)")
|
|
357
|
+
|
|
358
|
+
if a.shadow:
|
|
359
|
+
fx = r2["fixture"]
|
|
360
|
+
log("SHADOW", "以下是**真跑**会做的动作(现在一条都不执行):")
|
|
361
|
+
plan = [
|
|
362
|
+
("P5 回写①", f"上传 `{Path(r3['xlsx']).name}` 到 `发票文件`,并置 `发票状态=填写中`"),
|
|
363
|
+
("P6 登录 ", "连 CDP 9333 检查登录态;过期需人工过阿里云滑块"),
|
|
364
|
+
("P7 查重 ", f"门户按货件号 {a.shipment} 查询,**0 条才继续**(避免重复单)"),
|
|
365
|
+
("P8 导入 ", f"导入 {Path(r3['xlsx']).name}(写对方生产系统)"),
|
|
366
|
+
("P9 勾选 ", f"查回 {a.shipment} 并勾选该行"),
|
|
367
|
+
("P10 进仓单", "导出进仓单 → 集货仓 - 东莞集货仓(HNBD),产物只留本地"),
|
|
368
|
+
("P11 唛头 ", "导出唛头(异步任务)"),
|
|
369
|
+
("P12 取件 ", f"下载 DDP 包 → 解压 → 改名 `运单-{a.shipment}-{fx['箱数']}箱-{fx['产品名称']}.pdf`"),
|
|
370
|
+
("P13 回写②", "上传该 PDF 到 `大货标签`"),
|
|
371
|
+
("P14 收口 ", "发票状态=已完成 + 清空 `发票错误信息` + 释放「发票就绪」放行勾"),
|
|
372
|
+
]
|
|
373
|
+
for k, v in plan:
|
|
374
|
+
print(f" {k} │ {v}")
|
|
375
|
+
log("SHADOW", "影子模式结束:仅生成本地文件,未触碰飞书/门户",
|
|
376
|
+
xlsx=Path(r3["xlsx"]).name, fixture=Path(r2["fixture_path"]).name)
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
# ── 写操作阶段(真跑)──
|
|
380
|
+
if frm <= 5:
|
|
381
|
+
log("P5", "回写①:发票文件 + 状态=填写中")
|
|
382
|
+
_writeback(r1["record_id"], "发票文件", r3["xlsx"], "填写中", False, True)
|
|
383
|
+
else:
|
|
384
|
+
log("P5", f"跳过(--from P{frm})")
|
|
385
|
+
|
|
386
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
387
|
+
import wenyao_portal as wp
|
|
388
|
+
|
|
389
|
+
# ── 门户阶段(P6→P12)与收口(P13/P14):**每门户一把有界 flock** ──────────────────────
|
|
390
|
+
# 粒度 = **本门户**(默认 9333 那个 Chrome profile),⛔ 不是全局锁(不牵连 FULL / 其它流程)。
|
|
391
|
+
# 锁在**开浏览器之前**拿 ⇒ 超时连页都不开("不进入门户阶段");⛔ 绝不无限等待
|
|
392
|
+
# —— 无限等待正是 2026-10-12 三条票(79212271/79298694/79210359)死循环的形态。
|
|
393
|
+
try:
|
|
394
|
+
portal_timeout = wp.portal_lock_timeout()
|
|
395
|
+
except RuntimeError as e: # WENYAO_PORTAL_LOCK_TIMEOUT 非法 ⇒ fail-closed
|
|
396
|
+
log("门户", f"❌ {e}")
|
|
397
|
+
raise SystemExit(1)
|
|
398
|
+
try:
|
|
399
|
+
with wp.portal_lock(site=f"portal.{a.shipment}", timeout=portal_timeout,
|
|
400
|
+
reporter=lambda m: log("门户", m)) as waited:
|
|
401
|
+
final = p6_p14_portal(a, r1, r2, r3, frm, wp, waited)
|
|
402
|
+
except (wp.PortalLockTimeout, wp.LockError) as e:
|
|
403
|
+
log("门户", f"❌ {e}")
|
|
404
|
+
raise SystemExit(1)
|
|
405
|
+
|
|
406
|
+
log("DONE", f"全链路完成:{a.shipment}", record_id=r1["record_id"], xlsx=Path(r3["xlsx"]).name,
|
|
407
|
+
mark=Path(final).name, log=str(_LOG))
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _lock_desc(waited: float) -> str:
|
|
412
|
+
"""锁的状态文案(日志口径:`锁=<获得/等待 Ns>`)。"""
|
|
413
|
+
return "获得" if waited < 0.5 else f"等待 {waited:.0f}s"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def p6_p14_portal(a, r1, r2, r3, frm: int, wp, waited: float) -> str:
|
|
417
|
+
"""门户阶段(P6 登录态 → P12 取件改名)+ 回写收口(P13/P14)。返回改名后的唛头 PDF 路径。
|
|
418
|
+
|
|
419
|
+
⚠️ **调用方必须已持有门户锁**(`wp.portal_lock`)—— 本函数不再自己加锁。
|
|
420
|
+
⚠️ 本次跑批用**自己的标签页**(`wp.connect` 隔离)与**自己的下载目录**,`finally` 只关自己那个页。
|
|
421
|
+
"""
|
|
422
|
+
from playwright.sync_api import sync_playwright
|
|
423
|
+
dl_dir = DOWN_DIR / f"portal-{a.shipment}" # 本次跑批**自己的**下载目录(下载事件不错配)
|
|
424
|
+
final = ""
|
|
425
|
+
with sync_playwright() as p:
|
|
426
|
+
sess = wp.connect(p)
|
|
427
|
+
tid = (sess.target_id or "?")[:8]
|
|
428
|
+
log("门户", f"本次标签页={tid}|锁={_lock_desc(waited)}|票={a.shipment}")
|
|
429
|
+
try:
|
|
430
|
+
page = sess.page
|
|
431
|
+
wp.install_hooks(page)
|
|
432
|
+
scope = wp.set_download_dir(sess.browser, page, dl_dir)
|
|
433
|
+
log("P6", f"自己的标签页 targetId={tid}|下载目录={dl_dir.name}|下载绑定={scope['scope']}")
|
|
434
|
+
|
|
435
|
+
st = wp.login_state(page)
|
|
436
|
+
log("P6", f"登录态:needLogin={st['needLogin']} onList={st['onList']}")
|
|
437
|
+
if st["needLogin"]:
|
|
438
|
+
raise SystemExit("❌ 门户未登录(需人工过滑块后再跑)")
|
|
439
|
+
|
|
440
|
+
q = wp.query_shipment(page, a.shipment)
|
|
441
|
+
log("P7", f"查重:共 {q['count']} 条")
|
|
442
|
+
if q["count"] != 0 and frm <= 7:
|
|
443
|
+
raise SystemExit(f"❌ 门户已有 {q['count']} 条 {a.shipment},停止导入以免重复")
|
|
444
|
+
if q["count"] != 0:
|
|
445
|
+
log("P7", f"续跑(--from P{frm}):已有 {q['count']} 条属预期,跳过导入与导出")
|
|
446
|
+
|
|
447
|
+
if frm > 8:
|
|
448
|
+
log("P8", f"跳过(--from P{frm})")
|
|
449
|
+
imp = {"ok": True, "text": "(skipped)"}
|
|
450
|
+
else:
|
|
451
|
+
imp = wp.import_order(page, r3["xlsx"])
|
|
452
|
+
log("P8", f"导入结果:{imp['text'] or '(无 toast)'}")
|
|
453
|
+
if not imp["ok"]:
|
|
454
|
+
raise SystemExit("❌ 导入未报成功,停止后续")
|
|
455
|
+
|
|
456
|
+
if frm > 9:
|
|
457
|
+
log("P9", f"跳过勾选(--from P{frm})")
|
|
458
|
+
else:
|
|
459
|
+
q2 = wp.query_shipment(page, a.shipment)
|
|
460
|
+
log("P9", f"查回:共 {q2['count']} 条 → {q2['rows'][0][:70] if q2['rows'] else '(无)'}")
|
|
461
|
+
sel = wp.select_shipment(page, a.shipment)
|
|
462
|
+
log("P9", f"勾选:{sel['summary']}")
|
|
463
|
+
if not sel["selected"]:
|
|
464
|
+
raise SystemExit("❌ 未勾选成功")
|
|
465
|
+
|
|
466
|
+
rc = {"url": None} if frm > 10 else wp.export_receipt(page)
|
|
467
|
+
log("P10", f"进仓单地址:{'已生成' if rc['url'] else '未捕获'}", url=(rc["url"] or "")[:80])
|
|
468
|
+
|
|
469
|
+
mk = {"ok": True, "text": "(skipped)"} if frm > 11 else wp.export_marks(page)
|
|
470
|
+
log("P11", f"唛头导出:{mk['text']}")
|
|
471
|
+
if not mk["ok"]:
|
|
472
|
+
raise SystemExit("❌ 导出唛头未报成功")
|
|
473
|
+
|
|
474
|
+
# ⛔ 归属校验:下载文件名/包内容与本票不符 ⇒ 报错(绝不静默用错文件)
|
|
475
|
+
dl = wp.download_latest_ddp(page, dl_dir, expect_shipment=a.shipment)
|
|
476
|
+
log("P12", f"已下载 {dl['name']}({dl['size']} B)",
|
|
477
|
+
归属=("文件名命中" if dl["owned"]["checked"] else f"包内容命中 {dl['inner'][:1]}"))
|
|
478
|
+
files = wp.unzip_marks(dl["path"], DOWN_DIR / f"ddp-{a.shipment}")
|
|
479
|
+
pdfs = [f for f in files if f.lower().endswith(".pdf")]
|
|
480
|
+
if not pdfs:
|
|
481
|
+
raise SystemExit(f"❌ DDP 包里没有 PDF:{files}")
|
|
482
|
+
final = wp.rename_mark(pdfs[0], a.shipment, r2["fixture"]["箱数"], r2["fixture"]["产品名称"])
|
|
483
|
+
log("P12", f"改名完成 → {Path(final).name}")
|
|
484
|
+
finally:
|
|
485
|
+
sess.close() # ⛔ 只关自己这个页;别人/用户的页一个都不动
|
|
486
|
+
log("门户", f"本次标签页={tid}|锁=释放(本次等待 {waited:.0f}s)|票={a.shipment}")
|
|
487
|
+
|
|
488
|
+
if frm <= 13:
|
|
489
|
+
log("P13", "回写②:大货标签")
|
|
490
|
+
_writeback(r1["record_id"], "大货标签", final, "填写中", False, True)
|
|
491
|
+
else:
|
|
492
|
+
log("P13", f"跳过(--from P{frm})")
|
|
493
|
+
|
|
494
|
+
if frm <= 14:
|
|
495
|
+
log("P14", "收口:发票状态=已完成 + 清空错误信息 + 释放「发票就绪」放行勾")
|
|
496
|
+
_writeback(r1["record_id"], "大货标签", final, "已完成", True, True, clear_ready=True)
|
|
497
|
+
else:
|
|
498
|
+
log("P14", f"跳过(--from P{frm})")
|
|
499
|
+
return final
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""文鳐「影子模式」对比器 —— 判断新票的影子产物能否**放行切主**。
|
|
3
|
+
|
|
4
|
+
## 为什么需要
|
|
5
|
+
|
|
6
|
+
门户 `my.ffl-tech.com` 是对方**生产系统**,登录/导入/导出都会留真实痕迹;生产 lip 表更不能试错。
|
|
7
|
+
所以每票先跑 `wenyao_run.py --shadow`(只跑本地无副作用阶段)产出 xlsx,
|
|
8
|
+
再与**已验收通过的票**(黄金样本)逐格比对:
|
|
9
|
+
**只有"随行变化"的列允许不同,其余任何差异都说明模板/结构被动过 ⇒ 拒绝切主。**
|
|
10
|
+
|
|
11
|
+
## 判据(默认从 `wenyao-template-map.json` 推导,无需手写名单)
|
|
12
|
+
|
|
13
|
+
| source | 含义 | 允许不同? |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `const` | 固定值(关联单号/币种/是否带电…) | ❌ **必须完全相同** |
|
|
16
|
+
| `field` / `llm` / `calc` / `storemap` / `image` | 随货件/行变化的值 | ✅ 允许不同 |
|
|
17
|
+
| `const` + `perRow: true` | **常量模板,但取值随行变化**(如 S 产品材质 = `{材质},{材质英文}`) | ✅ 允许不同 |
|
|
18
|
+
|
|
19
|
+
⚠️ `perRow` 不是"这一列不用管":该列的**判据在填报器**(材质英文三级解析:SKU 字典 → 小词典 →
|
|
20
|
+
会话传入 `--material-en`),三级都解析不出来时填报器**直接阻断、不落盘**(文案指路)⇒ 走到本对比器
|
|
21
|
+
的影子产物一定已带非空英文。本对比器只做"模板/结构有没有被动过",不再把**每票都不同的材质文本**
|
|
22
|
+
当成结构漂移(2026-10-12 实测:两条票因 S8 与黄金样本材质不同而被判"意外差异"⇒ 闸门 exit 1 误阻)。
|
|
23
|
+
|
|
24
|
+
## 用法
|
|
25
|
+
|
|
26
|
+
python3 scripts/wenyao_shadow_diff.py --shadow <新票影子.xlsx> --golden <已验收票.xlsx>
|
|
27
|
+
python3 scripts/wenyao_shadow_diff.py ... --allow C,D --deny A # 手工覆盖
|
|
28
|
+
python3 scripts/wenyao_shadow_diff.py ... --json # 机器可读
|
|
29
|
+
|
|
30
|
+
退出码:0 = 可放行;1 = 有意外差异(**不要切主**);2 = 参数/文件问题。
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import json
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from openpyxl import load_workbook
|
|
40
|
+
|
|
41
|
+
DEFAULT_MAP = Path(__file__).resolve().with_name("wenyao-template-map.json")
|
|
42
|
+
PER_ROW_SOURCES = {"field", "llm", "calc", "storemap", "image"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_policy(map_path: Path) -> tuple[set[str], set[str], dict[str, str]]:
|
|
46
|
+
"""返回 (允许不同的列, 必须相同的列, 列→表头)。
|
|
47
|
+
|
|
48
|
+
`perRow: true` 的列按**随行变化**处理(`const` 模板 + 行数据,如 S 产品材质)——
|
|
49
|
+
它的判据在填报器(三级解析 + 未命中即阻断),不在这里逐字比对。
|
|
50
|
+
"""
|
|
51
|
+
data = json.loads(map_path.read_text(encoding="utf-8"))
|
|
52
|
+
allow: set[str] = set()
|
|
53
|
+
deny: set[str] = set()
|
|
54
|
+
headers: dict[str, str] = {}
|
|
55
|
+
for c in data.get("columns", []):
|
|
56
|
+
col = str(c.get("col", "")).strip().upper()
|
|
57
|
+
if not col:
|
|
58
|
+
continue
|
|
59
|
+
headers[col] = str(c.get("header", ""))
|
|
60
|
+
if c.get("source") in PER_ROW_SOURCES or c.get("perRow"):
|
|
61
|
+
allow.add(col)
|
|
62
|
+
else:
|
|
63
|
+
deny.add(col)
|
|
64
|
+
return allow, deny, headers
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def norm(v) -> str:
|
|
68
|
+
"""None 与空串等价(模板清空行后两者混用),其余去首尾空白。"""
|
|
69
|
+
if v is None:
|
|
70
|
+
return ""
|
|
71
|
+
return str(v).strip()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def column_letter(idx: int) -> str:
|
|
75
|
+
s = ""
|
|
76
|
+
while idx > 0:
|
|
77
|
+
idx, r = divmod(idx - 1, 26)
|
|
78
|
+
s = chr(65 + r) + s
|
|
79
|
+
return s
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compare(shadow_path: Path, golden_path: Path, allow: set[str], deny: set[str]):
|
|
83
|
+
"""逐格比对两张工作簿,返回 (意外差异列表, 允许差异列表, 统计)。"""
|
|
84
|
+
a = load_workbook(shadow_path, data_only=True)
|
|
85
|
+
b = load_workbook(golden_path, data_only=True)
|
|
86
|
+
unexpected, allowed, same = [], [], 0
|
|
87
|
+
for sheet in sorted(set(a.sheetnames) | set(b.sheetnames)):
|
|
88
|
+
if sheet not in a.sheetnames or sheet not in b.sheetnames:
|
|
89
|
+
unexpected.append({"sheet": sheet, "cell": "-", "col": "-", "reason": "工作表只存在于一侧",
|
|
90
|
+
"shadow": sheet in a.sheetnames, "golden": sheet in b.sheetnames})
|
|
91
|
+
continue
|
|
92
|
+
wa, wb = a[sheet], b[sheet]
|
|
93
|
+
rows = max(wa.max_row, wb.max_row)
|
|
94
|
+
cols = max(wa.max_column, wb.max_column)
|
|
95
|
+
for r in range(1, rows + 1):
|
|
96
|
+
for c in range(1, cols + 1):
|
|
97
|
+
va, vb = norm(wa.cell(r, c).value), norm(wb.cell(r, c).value)
|
|
98
|
+
if va == vb:
|
|
99
|
+
same += 1
|
|
100
|
+
continue
|
|
101
|
+
col = column_letter(c)
|
|
102
|
+
item = {"sheet": sheet, "cell": f"{col}{r}", "col": col, "shadow": va, "golden": vb}
|
|
103
|
+
# 列在白名单里且**不在**必须相同名单里 → 允许
|
|
104
|
+
if col in allow and col not in deny:
|
|
105
|
+
allowed.append(item)
|
|
106
|
+
else:
|
|
107
|
+
unexpected.append(item)
|
|
108
|
+
return unexpected, allowed, {"相同单元格": same, "允许差异": len(allowed), "意外差异": len(unexpected)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv=None) -> int:
|
|
112
|
+
ap = argparse.ArgumentParser(description="文鳐影子产物对比器(放行判据)")
|
|
113
|
+
ap.add_argument("--shadow", required=True, help="新票影子产物 xlsx")
|
|
114
|
+
ap.add_argument("--golden", required=True, help="已验收通过的票产物 xlsx(黄金样本)")
|
|
115
|
+
ap.add_argument("--map", default=str(DEFAULT_MAP), help="模板映射(默认 scripts/wenyao-template-map.json)")
|
|
116
|
+
ap.add_argument("--allow", default="", help="手工追加允许不同的列,逗号分隔(如 C,D)")
|
|
117
|
+
ap.add_argument("--deny", default="", help="手工追加必须相同的列,逗号分隔(如 A)")
|
|
118
|
+
ap.add_argument("--json", action="store_true", help="输出 JSON")
|
|
119
|
+
ap.add_argument("--show", type=int, default=20, help="文本模式最多打印多少条明细")
|
|
120
|
+
a = ap.parse_args(argv)
|
|
121
|
+
|
|
122
|
+
shadow, golden = Path(a.shadow), Path(a.golden)
|
|
123
|
+
for p in (shadow, golden):
|
|
124
|
+
if not p.exists():
|
|
125
|
+
print(f"文件不存在: {p}", file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
try:
|
|
128
|
+
allow, deny, headers = load_policy(Path(a.map))
|
|
129
|
+
except Exception as exc: # noqa: BLE001
|
|
130
|
+
print(f"读取映射失败: {exc}", file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
allow |= {c.strip().upper() for c in a.allow.split(",") if c.strip()}
|
|
133
|
+
deny |= {c.strip().upper() for c in a.deny.split(",") if c.strip()}
|
|
134
|
+
|
|
135
|
+
unexpected, allowed, stats = compare(shadow, golden, allow, deny)
|
|
136
|
+
ok = not unexpected
|
|
137
|
+
if a.json:
|
|
138
|
+
print(json.dumps({"pass": ok, "stats": stats, "unexpected": unexpected, "allowed": allowed},
|
|
139
|
+
ensure_ascii=False, indent=2))
|
|
140
|
+
else:
|
|
141
|
+
print(f"影子: {shadow.name} 黄金: {golden.name}")
|
|
142
|
+
print(f"允许不同的列({len(allow)}): {','.join(sorted(allow))}")
|
|
143
|
+
print(f"必须相同的列({len(deny)}): {','.join(sorted(deny))}")
|
|
144
|
+
print(f"统计: {stats}")
|
|
145
|
+
for tag, items in (("❌ 意外差异", unexpected), ("✅ 允许差异", allowed)):
|
|
146
|
+
if not items:
|
|
147
|
+
continue
|
|
148
|
+
print(f"\n{tag}({len(items)}):")
|
|
149
|
+
for it in items[: a.show]:
|
|
150
|
+
h = headers.get(it.get("col", ""), "")
|
|
151
|
+
print(f" {it['sheet']}!{it['cell']:<6} {h:<22} 黄金={it['golden']!r} → 影子={it['shadow']!r}"
|
|
152
|
+
if "reason" not in it else f" {it['sheet']}: {it['reason']}")
|
|
153
|
+
if len(items) > a.show:
|
|
154
|
+
print(f" …还有 {len(items) - a.show} 条")
|
|
155
|
+
print("\n" + ("✅ 可放行切主(无意外差异)" if ok else "❌ 拒绝切主:存在意外差异"))
|
|
156
|
+
return 0 if ok else 1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
raise SystemExit(main())
|