@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.
- package/MANIFEST.json +341 -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 +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -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 +378 -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/collect-evidence.mjs +42 -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 +135 -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 +47 -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 +261 -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/final-check.mjs +43 -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 +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -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 +1025 -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 +2278 -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 +195 -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.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -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 +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -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/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -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 +137 -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 +74 -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 +35 -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 +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -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 +738 -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 +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""门禁用探针:打印引擎**生效的**网络类判据(P3)。
|
|
3
|
+
|
|
4
|
+
为什么单独一个文件:`python3 -c` 传多行脚本在某些调用方式下会失败(实测门禁里报
|
|
5
|
+
"Command failed" 而手工跑同一段却正常),而门禁**不允许静默跳过**。改成文件调用,
|
|
6
|
+
参数传递不再有歧义;输出用 `__JSON__` 前缀标记,容忍 stdout 噪声。
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
13
|
+
import orchestrator as o # noqa: E402
|
|
14
|
+
|
|
15
|
+
print("__JSON__" + json.dumps(o._engine_network_rules()))
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""当日 FULL 归因报表 —— **只读、零网络、零写入**(FULL 自动路径成功率修复批,2026-10-10)。
|
|
3
|
+
|
|
4
|
+
## 为什么需要它
|
|
5
|
+
|
|
6
|
+
客户机 10-10 跑约 30 条 FULL,**约 1/3 失败**;同一天用 CLI 手动串行跑 6 票**全部 8/8 成功**。
|
|
7
|
+
"为什么自动路径比手动差"必须能**一条命令答出来**,而不是靠人翻日志 —— 否则每轮都只能凭印象
|
|
8
|
+
调参(本批的窗口默认值、仓容闸门正是这么定下来的)。本脚本就是那个"答出来"的口子。
|
|
9
|
+
|
|
10
|
+
## 数据源(**只有两个本地文件**,⛔ 不联网、不读表、不写任何东西)
|
|
11
|
+
|
|
12
|
+
| 源 | 默认路径 | 用途 |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| 统一台账 | `<数据目录>/flow-ledger.jsonl` | 当日 case 数 / 终态 / 小时×终态 / 店铺×终态 / 时间线 |
|
|
15
|
+
| 失败诊断 | `<数据目录>/fulfillment-logs/failures.jsonl` | 失败原因分类 / 反事实三态(**页面证据在这里**) |
|
|
16
|
+
|
|
17
|
+
数据目录走内核**唯一判据**(`scripts/lib/argent_datadir.py`,与编排器/CLI 同一份),
|
|
18
|
+
⛔ 不在这里复制三级链。
|
|
19
|
+
|
|
20
|
+
## 输出五段(每段都带**计数 + 证据来源**,可复算)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
① 当日 case 数 / 终态分布(done / failed / blocked / 其他)
|
|
24
|
+
② 失败原因分类计数(仓容满 | 预约页形态异常 | 步骤8 找不到取消按钮 | 提交未跳转 hub-v2 | 其他)
|
|
25
|
+
③ 按小时 × 终态、按店铺 × 终态
|
|
26
|
+
④ 反事实估算:若「窗口=<给定值> ∧ 仓容可用」⇒ 本可成功 / 仍会失败 / 证据不足(**三态,⛔ 不许猜**)
|
|
27
|
+
⑤ 与「手动路径」对照的时间线(同一 case 的人工 done 事件 vs 自动 failed 事件)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 口径(**逐条对应实现,改这条 = 改判据**)
|
|
31
|
+
|
|
32
|
+
### 终态(①③)
|
|
33
|
+
- 只看**当日**(`--date`,本地时区)的事件;台账 `ts` 是 epoch 秒。
|
|
34
|
+
- **信息性事件不算状态**(与 `flow_triage.INFO_EVENTS` **同一口径**:`phase` / `wake` /
|
|
35
|
+
`policy_apply` / `policy_new`)⇒ 取当日最后一条**状态性**事件:
|
|
36
|
+
`done` ⇒ done;`failed` ⇒ failed;`blocked` / `failed_ack` ⇒ blocked;
|
|
37
|
+
`planned` / `dispatched` / `retry` / `requeued` / 无 ⇒ 其他。
|
|
38
|
+
- 为什么不能只数事件:一张票一天可能 `failed` 3 次再 `done` —— 数事件会把"最终成功"报成失败。
|
|
39
|
+
|
|
40
|
+
### 失败原因分类(②)
|
|
41
|
+
判据是**失败文案里的标记串**(⛔ 不猜语义),逐条出自既有实现:
|
|
42
|
+
|
|
43
|
+
| 桶 | 判据(正则,命中即归桶;**顺序即优先级**) | 出处 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| 仓容满 | `capacity_full` / 平台原文片段(`liberar espacio` 等)/ `仓容满` / `库容满` | `scripts/ml_capacity_probe.py#PLATFORM_QUOTES`(**同一份**) |
|
|
46
|
+
| 预约页形态异常 | `预约页形态异常` / `calendar_absent` | `orchestrator.APPT_FORM_ANOMALY` |
|
|
47
|
+
| 步骤8 找不到取消按钮 | `Cancelar reserva` ∧ `未出现`/`未执行`/`找不到`,或 `cancel_button_not_found` | 步骤8 取消链 |
|
|
48
|
+
| 提交未跳转 hub-v2 | `hub-v2` ∧ `未跳转`/`未生效`/`permancer`/`no redirig` | 步骤4 提交链 |
|
|
49
|
+
| 其他 | 其余全部 | —— |
|
|
50
|
+
|
|
51
|
+
### 反事实三态(④)—— **前提 = 窗口取 `--window` ∧ 仓容可用**
|
|
52
|
+
对**每一条**失败记录(`failures.jsonl` 优先;台账 `failed` 作补充),按**当时的页面证据**判:
|
|
53
|
+
|
|
54
|
+
| 结论 | 判据(**全部**条件都要满足) |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `would_succeed` | 阻塞**唯一落在**两个轴上之一:① 仓容满(前提"仓容可用"直接消除该阻塞 ⇒ 前提即结论)② 顺延窗口耗尽(文案含 `已顺延到 平台今天+N 天`,且 `--window - 1 > N` ⇒ 更大窗口会继续往later 日期试) |
|
|
57
|
+
| `would_fail` | 证据显示阻塞**不在**这两个轴上(页面形态 / 坏货件号 / CDP/网络 / 步骤8 取消按钮 / 数据缺口…)⇒ 前提消除不了它 |
|
|
58
|
+
| `insufficient` | 证据缺失或不足(无文案 / 判不出属于哪条链 / 窗口类但**解析不出**当时试到第几天 ⇒ ⛔ 不许猜"更大窗口就能成") |
|
|
59
|
+
|
|
60
|
+
⛔ **三态不许猜**:`insufficient` 是一个**正常且必须存在**的结论("证据不足" ≠ "会失败")。
|
|
61
|
+
|
|
62
|
+
### 时间线(⑤)
|
|
63
|
+
同一 case 当日既有**自动 failed** 又有**人工 done** ⇒ 打印两条事件的时刻与文本
|
|
64
|
+
(人工 done 的判据 = `event=done` ∧ `by` 不是引擎自己的 `orchestrator`)。
|
|
65
|
+
|
|
66
|
+
## 用法
|
|
67
|
+
|
|
68
|
+
python3 scripts/analyze-full-day.py # 今天
|
|
69
|
+
python3 scripts/analyze-full-day.py --date 2026-10-10 --json
|
|
70
|
+
python3 scripts/analyze-full-day.py --ledger L --failures F --store 墨西哥-1店-子账号 --window 5
|
|
71
|
+
|
|
72
|
+
退出码:0 正常(**即使当天 0 条 case 也是 0**,那是事实不是错误);2 参数/源不可读。
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
from __future__ import annotations
|
|
76
|
+
|
|
77
|
+
import argparse
|
|
78
|
+
import datetime as dt
|
|
79
|
+
import json
|
|
80
|
+
import pathlib
|
|
81
|
+
import re
|
|
82
|
+
import sys
|
|
83
|
+
from typing import Any, Optional
|
|
84
|
+
|
|
85
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
86
|
+
sys.path.insert(0, str(HERE))
|
|
87
|
+
sys.path.insert(0, str(HERE / "lib"))
|
|
88
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿复制链)
|
|
89
|
+
|
|
90
|
+
# 仓容判据**同一份**(⛔ 不在这里再写一遍平台原文片段)
|
|
91
|
+
try:
|
|
92
|
+
from ml_capacity_probe import ACTION_TEXT as _CAPACITY_ACTION # noqa: E402
|
|
93
|
+
from ml_capacity_probe import CLASS_MARKER as _CAPACITY_MARKER # noqa: E402
|
|
94
|
+
from ml_capacity_probe import DEFER_TAG as _CAPACITY_TAG # noqa: E402
|
|
95
|
+
from ml_capacity_probe import PLATFORM_QUOTES as _PLATFORM_QUOTES # noqa: E402
|
|
96
|
+
except Exception: # noqa: BLE001
|
|
97
|
+
_CAPACITY_MARKER, _CAPACITY_TAG = "capacity_full", "仓容满"
|
|
98
|
+
_CAPACITY_ACTION = "需运营清低质库存腾库容"
|
|
99
|
+
_PLATFORM_QUOTES = ("liberar espacio",)
|
|
100
|
+
|
|
101
|
+
DATA_DIR = pathlib.Path(resolve_data_dir()["dir"])
|
|
102
|
+
DEFAULT_LEDGER = DATA_DIR / "flow-ledger.jsonl"
|
|
103
|
+
DEFAULT_FAILURES = DATA_DIR / "fulfillment-logs" / "failures.jsonl"
|
|
104
|
+
|
|
105
|
+
# ── 终态口径(与 flow_triage 同一份:信息性事件不算状态)──────────────────────
|
|
106
|
+
INFO_EVENTS = ("phase", "wake", "policy_apply", "policy_new")
|
|
107
|
+
STATE_BUCKETS = {
|
|
108
|
+
"done": "done",
|
|
109
|
+
"failed": "failed",
|
|
110
|
+
"blocked": "blocked",
|
|
111
|
+
"failed_ack": "blocked",
|
|
112
|
+
}
|
|
113
|
+
OTHER = "其他"
|
|
114
|
+
|
|
115
|
+
# ── 失败原因分类(顺序即优先级;判据是**标记串**,⛔ 不猜语义)────────────────
|
|
116
|
+
_QUOTE_RE = "|".join(re.escape(q) for q in _PLATFORM_QUOTES)
|
|
117
|
+
BUCKET_CAPACITY = "仓容满"
|
|
118
|
+
BUCKET_APPT_FORM = "预约页形态异常"
|
|
119
|
+
BUCKET_STEP8_CANCEL = "步骤8 找不到取消按钮"
|
|
120
|
+
BUCKET_HUBV2 = "提交未跳转 hub-v2"
|
|
121
|
+
BUCKET_OTHER = "其他"
|
|
122
|
+
BUCKET_ORDER = (BUCKET_CAPACITY, BUCKET_APPT_FORM, BUCKET_STEP8_CANCEL, BUCKET_HUBV2, BUCKET_OTHER)
|
|
123
|
+
REASON_RULES = (
|
|
124
|
+
(BUCKET_CAPACITY, "capacity-full", re.compile(
|
|
125
|
+
rf"{re.escape(_CAPACITY_MARKER)}|{_QUOTE_RE}|{re.escape(_CAPACITY_TAG)}|库容满", re.I)),
|
|
126
|
+
(BUCKET_APPT_FORM, "appointment-form", re.compile(r"预约页形态异常|calendar_absent", re.I)),
|
|
127
|
+
(BUCKET_STEP8_CANCEL, "step8-cancel-button", re.compile(
|
|
128
|
+
r"(?:Cancelar reserva|cancel_button_not_found|取消预约).{0,40}?"
|
|
129
|
+
r"(?:未出现|未执行|找不到|按钮|not found|超时)", re.I)),
|
|
130
|
+
(BUCKET_HUBV2, "submit-no-hubv2", re.compile(
|
|
131
|
+
r"hub-v2.{0,30}?(?:未跳转|未生效|未到达|no redirig)|(?:未跳转|未生效).{0,20}?hub-v2", re.I)),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ── 反事实前提里"阻塞在哪条链"的判据(**与上面同一批标记**,⛔ 不另写一套)──────
|
|
135
|
+
WINDOW_EXHAUSTED_RE = re.compile(r"已顺延到\s*平台今天\+?(\d+)\s*天")
|
|
136
|
+
WINDOW_AXIS_RE = re.compile(
|
|
137
|
+
r"可用时段不足|时段不足|hub-v2.{0,30}?(?:未跳转|未生效)|(?:未跳转|未生效).{0,20}?hub-v2"
|
|
138
|
+
r"|当前无可预约时段|无可用日|时段被平台拒", re.I)
|
|
139
|
+
NON_AXIS_RE = re.compile(
|
|
140
|
+
r"预约页形态异常|calendar_absent|bad_shipment_id|取号错位|不可解析|"
|
|
141
|
+
r"未找到\s*Veh[íi]culo|CDP|ECONN|ETIMEDOUT|EAI_AGAIN|net::ERR|Target closed|"
|
|
142
|
+
r"browser has been closed|Cancelar reserva|取消预约|SKU.{0,40}在店铺|未授权|登录|"
|
|
143
|
+
# 代码级缺陷(我方缺陷类)同样落在前提之外:换窗口/腾库容都修不好它
|
|
144
|
+
r"object has no attribute|AttributeError|Traceback|TypeError|KeyError",
|
|
145
|
+
re.I)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
149
|
+
# 读源(**只读**;每行都带 `src` 与行号,保证结论可复算)
|
|
150
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
|
|
152
|
+
def read_jsonl(path: pathlib.Path) -> list:
|
|
153
|
+
"""逐行读 JSONL,返回 `[{... , "_src": "<path>", "_line": N}]`(坏行跳过但计数)。"""
|
|
154
|
+
out: list = []
|
|
155
|
+
try:
|
|
156
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
157
|
+
except OSError:
|
|
158
|
+
return out
|
|
159
|
+
for i, line in enumerate(text.splitlines(), 1):
|
|
160
|
+
line = line.strip()
|
|
161
|
+
if not line:
|
|
162
|
+
continue
|
|
163
|
+
try:
|
|
164
|
+
d = json.loads(line)
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
continue
|
|
167
|
+
if not isinstance(d, dict):
|
|
168
|
+
continue
|
|
169
|
+
d["_src"], d["_line"] = str(path), i
|
|
170
|
+
out.append(d)
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ledger_date_of(ev: dict) -> str:
|
|
175
|
+
"""台账事件日期(epoch → **本地**日期;与 failures.jsonl 的本地时间串同口径)。"""
|
|
176
|
+
ts = ev.get("ts")
|
|
177
|
+
try:
|
|
178
|
+
return dt.datetime.fromtimestamp(float(ts)).date().isoformat()
|
|
179
|
+
except (TypeError, ValueError, OSError, OverflowError):
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def failures_date_of(rec: dict) -> str:
|
|
184
|
+
"""失败诊断日期(`YYYY-MM-DD HH:MM:SS` 前 10 位)。"""
|
|
185
|
+
return str(rec.get("ts") or "")[:10]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
STORE_IN_ERROR_RE = re.compile(r"\[store=([^\]]*)\]")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def store_of(ev: dict) -> str:
|
|
192
|
+
"""事件所属店铺:优先 `store` 字段,其次 `error` 里的 `[store=…]` 身份前缀。"""
|
|
193
|
+
s = str(ev.get("store") or "").strip()
|
|
194
|
+
if s:
|
|
195
|
+
return s
|
|
196
|
+
m = STORE_IN_ERROR_RE.search(str(ev.get("error") or ""))
|
|
197
|
+
return m.group(1).strip() if m else ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def case_of(ev: dict) -> str:
|
|
201
|
+
return str(ev.get("caseId") or "")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def record_of(case_id: str) -> str:
|
|
205
|
+
"""`full__<rec>` → `<rec>`(非 full case 原样返回)。"""
|
|
206
|
+
return case_id.split("__", 1)[1] if "__" in case_id else case_id
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
# 纯判据
|
|
211
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
def state_bucket(event: str) -> str:
|
|
214
|
+
"""状态性事件 → 终态桶;信息性事件返回空串(**不算状态**)。"""
|
|
215
|
+
e = str(event or "")
|
|
216
|
+
if e in INFO_EVENTS:
|
|
217
|
+
return ""
|
|
218
|
+
return STATE_BUCKETS.get(e, OTHER)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def classify_reason(text: str) -> tuple:
|
|
222
|
+
"""失败文案 → 原因桶。返回 `(bucket, rule_id)`。"""
|
|
223
|
+
t = str(text or "")
|
|
224
|
+
for bucket, rid, pat in REASON_RULES:
|
|
225
|
+
if pat.search(t):
|
|
226
|
+
return bucket, rid
|
|
227
|
+
return BUCKET_OTHER, "none"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def counterfactual(reason: str, window: int, *, extra_text: str = "") -> dict:
|
|
231
|
+
"""反事实三态 —— **前提 = 窗口取 `window` ∧ 仓容可用**(判据表见模块 docstring)。
|
|
232
|
+
|
|
233
|
+
返回 `{verdict, axis, basis, observed_window}`;`verdict ∈ {would_succeed, would_fail, insufficient}`。
|
|
234
|
+
⛔ 三态不许猜:解析不出"当时试到第几天"就判 `insufficient`,**不**默认"更大窗口就能成"。
|
|
235
|
+
"""
|
|
236
|
+
t = f"{reason or ''} {extra_text or ''}".strip()
|
|
237
|
+
if not t:
|
|
238
|
+
return {"verdict": "insufficient", "axis": "none",
|
|
239
|
+
"basis": "无失败文案(证据缺失)", "observed_window": None}
|
|
240
|
+
|
|
241
|
+
# ① 仓容轴:前提"仓容可用"**直接消除**该阻塞 ⇒ 前提即结论
|
|
242
|
+
if REASON_RULES[0][2].search(t):
|
|
243
|
+
# 但若同一段证据里另有一条**不在前提射程内**的硬阻塞 ⇒ 前提消除不了它
|
|
244
|
+
if NON_AXIS_RE.search(t):
|
|
245
|
+
m = NON_AXIS_RE.search(t)
|
|
246
|
+
return {"verdict": "would_fail", "axis": "capacity+other",
|
|
247
|
+
"basis": f"仓容是该条阻塞,但同时存在前提消除不了的硬阻塞「{m.group(0)}」",
|
|
248
|
+
"observed_window": None}
|
|
249
|
+
return {"verdict": "would_succeed", "axis": "capacity",
|
|
250
|
+
"basis": f"阻塞唯一落在仓容轴:命中平台原文/`{_CAPACITY_MARKER}`;"
|
|
251
|
+
f"前提「仓容可用」直接消除该阻塞", "observed_window": None}
|
|
252
|
+
|
|
253
|
+
# ② 顺延窗口轴:必须**同时**有"窗口耗尽"证据与"更大窗口能继续"的算术关系
|
|
254
|
+
if WINDOW_AXIS_RE.search(t):
|
|
255
|
+
if NON_AXIS_RE.search(t):
|
|
256
|
+
m = NON_AXIS_RE.search(t)
|
|
257
|
+
return {"verdict": "would_fail", "axis": "window+other",
|
|
258
|
+
"basis": f"窗口类失败但同时存在前提消除不了的硬阻塞「{m.group(0)}」",
|
|
259
|
+
"observed_window": None}
|
|
260
|
+
m = WINDOW_EXHAUSTED_RE.search(t)
|
|
261
|
+
if not m:
|
|
262
|
+
return {"verdict": "insufficient", "axis": "window",
|
|
263
|
+
"basis": "窗口类失败,但文案里**没有**「已顺延到 平台今天+N 天」⇒ "
|
|
264
|
+
"解析不出当时试到第几天(⛔ 不猜)", "observed_window": None}
|
|
265
|
+
n = int(m.group(1))
|
|
266
|
+
observed = n + 1
|
|
267
|
+
if window > observed:
|
|
268
|
+
return {"verdict": "would_succeed", "axis": "window",
|
|
269
|
+
"basis": f"当时窗口耗尽于 平台今天+{n} 天(= 窗口 {observed});"
|
|
270
|
+
f"给定窗口 {window} > {observed} ⇒ 会继续往更晚日期试",
|
|
271
|
+
"observed_window": observed}
|
|
272
|
+
return {"verdict": "would_fail", "axis": "window",
|
|
273
|
+
"basis": f"当时窗口已试到 平台今天+{n} 天(= 窗口 {observed}),"
|
|
274
|
+
f"给定窗口 {window} ≤ {observed} ⇒ 换窗口也够不到",
|
|
275
|
+
"observed_window": observed}
|
|
276
|
+
|
|
277
|
+
# ③ 非前提轴(页面形态/坏号/CDP/取消按钮…)⇒ 前提消除不了
|
|
278
|
+
if NON_AXIS_RE.search(t):
|
|
279
|
+
m = NON_AXIS_RE.search(t)
|
|
280
|
+
return {"verdict": "would_fail", "axis": "non-axis",
|
|
281
|
+
"basis": f"阻塞不在窗口/仓容两轴上(命中「{m.group(0)}」)⇒ 前提消除不了它",
|
|
282
|
+
"observed_window": None}
|
|
283
|
+
|
|
284
|
+
return {"verdict": "insufficient", "axis": "unknown",
|
|
285
|
+
"basis": "文案既不像仓容/窗口类、也没命中任何已知非前提轴 ⇒ 判不出(⛔ 不猜)",
|
|
286
|
+
"observed_window": None}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
290
|
+
# 报表装配
|
|
291
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
292
|
+
|
|
293
|
+
def build_report(ledger: list, failures: list, *, date: str, store: str = "",
|
|
294
|
+
window: int = 5) -> dict:
|
|
295
|
+
"""纯函数:两份已读入的记录 + 参数 → 报表(**无 IO、无时间依赖**,可纯夹具回归)。"""
|
|
296
|
+
def _store_ok(s: str) -> bool:
|
|
297
|
+
return (not store) or (s == store)
|
|
298
|
+
|
|
299
|
+
# ── 当日事件(台账)────────────────────────────────────────────────────
|
|
300
|
+
day_events = [e for e in ledger
|
|
301
|
+
if ledger_date_of(e) == date and _store_ok(store_of(e))]
|
|
302
|
+
by_case: dict[str, list] = {}
|
|
303
|
+
for e in day_events:
|
|
304
|
+
by_case.setdefault(case_of(e), []).append(e)
|
|
305
|
+
for lst in by_case.values():
|
|
306
|
+
lst.sort(key=lambda x: (float(x.get("ts") or 0), int(x.get("_line") or 0)))
|
|
307
|
+
|
|
308
|
+
# 店铺回填(台账事件常没有 store 字段 ⇒ 从当日失败诊断里按 record_id 补)
|
|
309
|
+
rec_store = {}
|
|
310
|
+
for r in failures:
|
|
311
|
+
rid = str(r.get("record_id") or "")
|
|
312
|
+
if rid and r.get("store"):
|
|
313
|
+
rec_store.setdefault(rid, str(r["store"]))
|
|
314
|
+
|
|
315
|
+
def _store_of_case(cid: str) -> str:
|
|
316
|
+
for e in by_case.get(cid, []):
|
|
317
|
+
s = store_of(e)
|
|
318
|
+
if s:
|
|
319
|
+
return s
|
|
320
|
+
return rec_store.get(record_of(cid), "") or "(未知)"
|
|
321
|
+
|
|
322
|
+
# ── ① 当日 case 数 / 终态分布 ──────────────────────────────────────────
|
|
323
|
+
terminal: dict[str, int] = {"done": 0, "failed": 0, "blocked": 0, OTHER: 0}
|
|
324
|
+
case_rows = []
|
|
325
|
+
failed_ever = 0
|
|
326
|
+
for cid, evs in sorted(by_case.items()):
|
|
327
|
+
states = [(e, state_bucket(e.get("event"))) for e in evs]
|
|
328
|
+
states = [(e, b) for e, b in states if b]
|
|
329
|
+
last_ev, last_b = (states[-1] if states else (None, OTHER))
|
|
330
|
+
terminal[last_b] = terminal.get(last_b, 0) + 1
|
|
331
|
+
ever = any(str(e.get("event") or "") == "failed" for e in evs)
|
|
332
|
+
failed_ever += 1 if ever else 0
|
|
333
|
+
case_rows.append({
|
|
334
|
+
"case": cid, "record_id": record_of(cid), "store": _store_of_case(cid),
|
|
335
|
+
"events_on_day": len(evs),
|
|
336
|
+
"failed_ever": ever,
|
|
337
|
+
"last_state_event": (last_ev or {}).get("event") or "",
|
|
338
|
+
"last_state_ts": int(last_ev.get("ts") or 0) if last_ev else None,
|
|
339
|
+
"terminal": last_b,
|
|
340
|
+
"src": [f"{(last_ev or {}).get('_src', '')}:{(last_ev or {}).get('_line', '')}"]
|
|
341
|
+
if last_ev else [],
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
# ── ② 失败原因分类计数 ────────────────────────────────────────────────
|
|
345
|
+
day_failures = [r for r in failures
|
|
346
|
+
if failures_date_of(r) == date and _store_ok(str(r.get("store") or ""))]
|
|
347
|
+
day_failed_events = [e for e in day_events if str(e.get("event") or "") == "failed"]
|
|
348
|
+
reasons: list = []
|
|
349
|
+
for r in day_failures:
|
|
350
|
+
text = str(r.get("error_message") or "")
|
|
351
|
+
b, rid = classify_reason(text)
|
|
352
|
+
reasons.append({"bucket": b, "rule": rid, "record_id": str(r.get("record_id") or ""),
|
|
353
|
+
"store": str(r.get("store") or ""), "ts": str(r.get("ts") or ""),
|
|
354
|
+
"step": r.get("failed_step"), "err_type": str(r.get("error_type") or ""),
|
|
355
|
+
"reason": text, "source": "failures",
|
|
356
|
+
"src": f"{r.get('_src')}:{r.get('_line')}"})
|
|
357
|
+
# 台账里 failed 但当日 failures.jsonl **已经**记过同一次失败 ⇒ 跳过(⛔ 不许双重计数):
|
|
358
|
+
# 同一次失败会同时进两处(`_record_failure` 写 failures.jsonl、`_ledger_record_full_failure`
|
|
359
|
+
# 写台账)⇒ 两条都数会把"当日失败数"翻倍,归因表当场失真。
|
|
360
|
+
# 去重键 =(record_id, 分钟级时刻):两处的时间戳都取自同一次失败,分钟必然相同。
|
|
361
|
+
_fail_keys = {(str(r.get("record_id") or ""), str(r.get("ts") or "")[:16])
|
|
362
|
+
for r in day_failures}
|
|
363
|
+
_seen = {(x["record_id"], x["reason"][:60]) for x in reasons}
|
|
364
|
+
deduped = 0
|
|
365
|
+
for e in day_failed_events:
|
|
366
|
+
text = str(e.get("reason") or e.get("error") or "")
|
|
367
|
+
rid = record_of(case_of(e))
|
|
368
|
+
_at = (dt.datetime.fromtimestamp(float(e.get("ts") or 0)).strftime("%Y-%m-%d %H:%M")
|
|
369
|
+
if e.get("ts") else "")
|
|
370
|
+
if (rid, _at) in _fail_keys:
|
|
371
|
+
deduped += 1
|
|
372
|
+
continue
|
|
373
|
+
if (rid, text[:60]) in _seen:
|
|
374
|
+
continue
|
|
375
|
+
b, rule = classify_reason(text)
|
|
376
|
+
reasons.append({"bucket": b, "rule": rule, "record_id": rid, "store": store_of(e),
|
|
377
|
+
"ts": dt.datetime.fromtimestamp(float(e.get("ts") or 0)).strftime("%Y-%m-%d %H:%M:%S")
|
|
378
|
+
if e.get("ts") else "",
|
|
379
|
+
"step": None, "err_type": str(e.get("errType") or ""),
|
|
380
|
+
"reason": text, "source": "ledger",
|
|
381
|
+
"src": f"{e.get('_src')}:{e.get('_line')}"})
|
|
382
|
+
reason_counts = {b: 0 for b in BUCKET_ORDER}
|
|
383
|
+
for x in reasons:
|
|
384
|
+
reason_counts[x["bucket"]] = reason_counts.get(x["bucket"], 0) + 1
|
|
385
|
+
|
|
386
|
+
# ── ③ 小时 × 终态 / 店铺 × 终态 ───────────────────────────────────────
|
|
387
|
+
hour_state: dict[str, dict] = {}
|
|
388
|
+
store_state: dict[str, dict] = {}
|
|
389
|
+
for row in case_rows:
|
|
390
|
+
ts = row.get("last_state_ts")
|
|
391
|
+
hour = dt.datetime.fromtimestamp(ts).strftime("%H") if ts else "??"
|
|
392
|
+
h = hour_state.setdefault(hour, {k: 0 for k in ("done", "failed", "blocked", OTHER)})
|
|
393
|
+
h[row["terminal"]] = h.get(row["terminal"], 0) + 1
|
|
394
|
+
s = store_state.setdefault(row["store"] or "(未知)",
|
|
395
|
+
{k: 0 for k in ("done", "failed", "blocked", OTHER)})
|
|
396
|
+
s[row["terminal"]] = s.get(row["terminal"], 0) + 1
|
|
397
|
+
|
|
398
|
+
# ── ④ 反事实三态(逐条失败)────────────────────────────────────────────
|
|
399
|
+
counter = []
|
|
400
|
+
for x in reasons:
|
|
401
|
+
cf = counterfactual(x["reason"], window)
|
|
402
|
+
counter.append({**{k: x[k] for k in ("record_id", "store", "ts", "bucket", "reason", "src")},
|
|
403
|
+
"window": window, **cf})
|
|
404
|
+
cf_counts = {"would_succeed": 0, "would_fail": 0, "insufficient": 0}
|
|
405
|
+
for c in counter:
|
|
406
|
+
cf_counts[c["verdict"]] = cf_counts.get(c["verdict"], 0) + 1
|
|
407
|
+
|
|
408
|
+
# ── ⑤ 与手动路径对照的时间线 ──────────────────────────────────────────
|
|
409
|
+
timelines = []
|
|
410
|
+
for cid, evs in sorted(by_case.items()):
|
|
411
|
+
fails = [e for e in evs if str(e.get("event") or "") == "failed"]
|
|
412
|
+
dones = [e for e in evs if str(e.get("event") or "") == "done"]
|
|
413
|
+
manual = [e for e in dones if str(e.get("by") or "") not in ("", "orchestrator")]
|
|
414
|
+
if fails and manual:
|
|
415
|
+
timelines.append({
|
|
416
|
+
"case": cid, "record_id": record_of(cid), "store": _store_of_case(cid),
|
|
417
|
+
"auto_failed": [{"ts": int(e.get("ts") or 0),
|
|
418
|
+
"at": dt.datetime.fromtimestamp(float(e.get("ts") or 0))
|
|
419
|
+
.strftime("%Y-%m-%d %H:%M:%S"),
|
|
420
|
+
"text": str(e.get("reason") or e.get("error") or "")[:200],
|
|
421
|
+
"src": f"{e.get('_src')}:{e.get('_line')}"} for e in fails],
|
|
422
|
+
"manual_done": [{"ts": int(e.get("ts") or 0),
|
|
423
|
+
"at": dt.datetime.fromtimestamp(float(e.get("ts") or 0))
|
|
424
|
+
.strftime("%Y-%m-%d %H:%M:%S"),
|
|
425
|
+
"by": str(e.get("by") or ""),
|
|
426
|
+
"text": str(e.get("detail") or e.get("reason") or "")[:200],
|
|
427
|
+
"src": f"{e.get('_src')}:{e.get('_line')}"} for e in manual],
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
"schema": "argent-full-day-attribution/1",
|
|
432
|
+
"date": date,
|
|
433
|
+
"filters": {"store": store or None, "window": window},
|
|
434
|
+
"sources": {
|
|
435
|
+
"ledger": {"events_total": len(ledger), "events_on_day": len(day_events)},
|
|
436
|
+
"failures": {"records_total": len(failures), "records_on_day": len(day_failures)},
|
|
437
|
+
},
|
|
438
|
+
"cases": {"count": len(case_rows), "terminal": terminal,
|
|
439
|
+
# 「曾失败」与「终态=失败」是**两个不同的数**:一张票当天失败过、后来人工补完
|
|
440
|
+
# ⇒ 终态算 done,但它确实是自动路径的一次失败(成功率口径要看这个数)。
|
|
441
|
+
"failed_ever": failed_ever, "rows": case_rows},
|
|
442
|
+
"failures": {"count": len(reasons), "by_reason": reason_counts, "rows": reasons,
|
|
443
|
+
"ledger_only": len(reasons) - len(day_failures),
|
|
444
|
+
"deduped_ledger_events": deduped},
|
|
445
|
+
"matrix": {"by_hour": hour_state, "by_store": store_state},
|
|
446
|
+
"counterfactual": {"window": window, "counts": cf_counts, "rows": counter,
|
|
447
|
+
"action_if_capacity": _CAPACITY_ACTION},
|
|
448
|
+
"timelines": {"count": len(timelines), "rows": timelines},
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
453
|
+
# 渲染(人读)
|
|
454
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
def render(rep: dict) -> str:
|
|
457
|
+
L = []
|
|
458
|
+
L.append(f"═══ FULL 当日归因 {rep['date']} ═══"
|
|
459
|
+
+ (f"(店铺={rep['filters']['store']})" if rep["filters"]["store"] else "")
|
|
460
|
+
+ f" 反事实窗口={rep['filters']['window']}")
|
|
461
|
+
L.append(f"源:台账 {rep['sources']['ledger']['events_on_day']}/"
|
|
462
|
+
f"{rep['sources']['ledger']['events_total']} 条(当日/全部);"
|
|
463
|
+
f"失败诊断 {rep['sources']['failures']['records_on_day']}/"
|
|
464
|
+
f"{rep['sources']['failures']['records_total']} 条")
|
|
465
|
+
L.append("")
|
|
466
|
+
c = rep["cases"]
|
|
467
|
+
L.append(f"① 当日 case 数 = {c['count']}|终态:" +
|
|
468
|
+
" / ".join(f"{k} {v}" for k, v in c["terminal"].items())
|
|
469
|
+
+ f"|当日**曾失败**的 case = {c.get('failed_ever', 0)}"
|
|
470
|
+
+ "(终态=done 里可能含「失败后人工补完」,两个数口径不同)")
|
|
471
|
+
for r in c["rows"]:
|
|
472
|
+
L.append(f" · {r['record_id']} [{r['store']}] 当日事件 {r['events_on_day']} 条 ⇒ "
|
|
473
|
+
f"{r['terminal']}(末条状态事件={r['last_state_event'] or '(无)'};{r['src'][0] if r['src'] else ''})")
|
|
474
|
+
L.append("")
|
|
475
|
+
f = rep["failures"]
|
|
476
|
+
L.append(f"② 失败记录 {f['count']} 条|分类:" +
|
|
477
|
+
" | ".join(f"{k} {f['by_reason'].get(k, 0)}" for k in BUCKET_ORDER))
|
|
478
|
+
for x in f["rows"]:
|
|
479
|
+
L.append(f" · [{x['bucket']}] {x['record_id']} {x['ts']} step={x['step']} "
|
|
480
|
+
f"rule={x['rule']}|{x['reason'][:90]}({x['src']})")
|
|
481
|
+
L.append("")
|
|
482
|
+
L.append("③ 小时 × 终态:")
|
|
483
|
+
for h in sorted(rep["matrix"]["by_hour"]):
|
|
484
|
+
d = rep["matrix"]["by_hour"][h]
|
|
485
|
+
L.append(f" {h} 时:" + " ".join(f"{k}={d.get(k, 0)}" for k in ("done", "failed", "blocked", OTHER)))
|
|
486
|
+
L.append(" 店铺 × 终态:")
|
|
487
|
+
for s in sorted(rep["matrix"]["by_store"]):
|
|
488
|
+
d = rep["matrix"]["by_store"][s]
|
|
489
|
+
L.append(f" {s}:" + " ".join(f"{k}={d.get(k, 0)}" for k in ("done", "failed", "blocked", OTHER)))
|
|
490
|
+
L.append("")
|
|
491
|
+
cf = rep["counterfactual"]
|
|
492
|
+
L.append(f"④ 反事实(窗口={cf['window']} ∧ 仓容可用):"
|
|
493
|
+
f"本可成功 {cf['counts'].get('would_succeed', 0)} / "
|
|
494
|
+
f"仍会失败 {cf['counts'].get('would_fail', 0)} / "
|
|
495
|
+
f"证据不足 {cf['counts'].get('insufficient', 0)}")
|
|
496
|
+
for x in cf["rows"]:
|
|
497
|
+
L.append(f" · [{x['verdict']}] {x['record_id']}(axis={x['axis']}){x['basis']}")
|
|
498
|
+
L.append("")
|
|
499
|
+
t = rep["timelines"]
|
|
500
|
+
L.append(f"⑤ 与手动路径对照的时间线:{t['count']} 个 case")
|
|
501
|
+
for row in t["rows"]:
|
|
502
|
+
L.append(f" · {row['record_id']} [{row['store']}]")
|
|
503
|
+
for e in row["auto_failed"]:
|
|
504
|
+
L.append(f" 自动 failed {e['at']}|{e['text'][:80]}({e['src']})")
|
|
505
|
+
for e in row["manual_done"]:
|
|
506
|
+
L.append(f" 人工 done {e['at']}|by={e['by']}|{e['text'][:80]}({e['src']})")
|
|
507
|
+
L.append("")
|
|
508
|
+
L.append("(本报表**只读**:零网络、零写入;每条结论都带计数与证据行号,可复算)")
|
|
509
|
+
return "\n".join(L)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _main(argv: Optional[list[str]] = None) -> int:
|
|
513
|
+
p = argparse.ArgumentParser(
|
|
514
|
+
prog="analyze-full-day.py",
|
|
515
|
+
description="当日 FULL 归因报表(只读、零网络、零写入;数据源=统一台账 + 失败诊断)")
|
|
516
|
+
p.add_argument("--ledger", default=str(DEFAULT_LEDGER), help=f"统一台账(默认 {DEFAULT_LEDGER})")
|
|
517
|
+
p.add_argument("--failures", default=str(DEFAULT_FAILURES),
|
|
518
|
+
help=f"失败诊断 JSONL(默认 {DEFAULT_FAILURES})")
|
|
519
|
+
p.add_argument("--date", default=dt.date.today().isoformat(),
|
|
520
|
+
help="统计日期 YYYY-MM-DD(默认今天,本地时区)")
|
|
521
|
+
p.add_argument("--store", default="", help="只看该店铺(精确匹配)")
|
|
522
|
+
p.add_argument("--window", type=int, default=None,
|
|
523
|
+
help="反事实前提里的顺延窗口(天);缺省 = 当前生效值"
|
|
524
|
+
"(`flow_config.appointment_max_day()`:env → 声明 default)")
|
|
525
|
+
p.add_argument("--json", action="store_true", help="机读形状(含每条的 src/行号)")
|
|
526
|
+
a = p.parse_args(argv)
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
dt.date.fromisoformat(a.date)
|
|
530
|
+
except ValueError:
|
|
531
|
+
print(f"⛔ --date 必须是 YYYY-MM-DD:{a.date}", file=sys.stderr)
|
|
532
|
+
return 2
|
|
533
|
+
|
|
534
|
+
window = a.window
|
|
535
|
+
if window is None:
|
|
536
|
+
try:
|
|
537
|
+
sys.path.insert(0, str(HERE))
|
|
538
|
+
from flow_config import appointment_max_day as _amd # noqa: E402
|
|
539
|
+
window = int(_amd())
|
|
540
|
+
except Exception: # noqa: BLE001
|
|
541
|
+
window = 5
|
|
542
|
+
if window < 1:
|
|
543
|
+
print("⛔ --window 必须 ≥1", file=sys.stderr)
|
|
544
|
+
return 2
|
|
545
|
+
|
|
546
|
+
ledger_path, fail_path = pathlib.Path(a.ledger), pathlib.Path(a.failures)
|
|
547
|
+
ledger, failures = read_jsonl(ledger_path), read_jsonl(fail_path)
|
|
548
|
+
# ⛔ 「读不到文件」与「文件在但当天没数据」必须分得开:前者是**用法/环境问题**(exit 2),
|
|
549
|
+
# 后者是**事实**(0 条,exit 0)—— 混成一种会把"路径写错"报成"今天全成功"。
|
|
550
|
+
missing = [str(x) for x in (ledger_path, fail_path) if not x.exists()]
|
|
551
|
+
if len(missing) == 2:
|
|
552
|
+
print(f"⛔ 两个数据源都读不到(路径写错?):{', '.join(missing)}", file=sys.stderr)
|
|
553
|
+
return 2
|
|
554
|
+
|
|
555
|
+
rep = build_report(ledger, failures, date=a.date, store=a.store, window=window)
|
|
556
|
+
rep["sources"]["paths"] = {"ledger": str(ledger_path), "failures": str(fail_path),
|
|
557
|
+
"missing": missing}
|
|
558
|
+
if a.json:
|
|
559
|
+
print(json.dumps(rep, ensure_ascii=False, indent=2))
|
|
560
|
+
else:
|
|
561
|
+
print(render(rep))
|
|
562
|
+
if missing:
|
|
563
|
+
print(f"⚠️ 数据源缺失(按空处理):{', '.join(missing)}", file=sys.stderr)
|
|
564
|
+
return 0
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
if __name__ == "__main__":
|
|
568
|
+
sys.exit(_main())
|