@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,314 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""按**能力**观测(迭代 3-⑬):把"按 flow 平铺"的台账聚合成「能力 → 成功率 / 耗时 / 卡点 / 集成健康度」。
|
|
3
|
+
|
|
4
|
+
## 为什么要按能力聚合
|
|
5
|
+
2026-09-20 的三次故障(step7 箱唛 64s、放行勾漏清、PO 映射错)都不是"某条 flow 坏了",而是
|
|
6
|
+
**某个能力/某个集成**的共性问题的两次显形。按 flow 平铺只能看到"今天哪条红了",
|
|
7
|
+
按能力看才能看到"发票这个能力最近三次都卡在同一阶段"。
|
|
8
|
+
|
|
9
|
+
## 口径(都从既有台账派生,不新增埋点)
|
|
10
|
+
- 台账事件:`dispatched / done / failed / blocked / failed_ack / phase / retry / wake`;
|
|
11
|
+
- **成功率** = `done / (done + failed + blocked)`(`failed_ack`=人工判定与流程无关 ⇒ 不计入分母);
|
|
12
|
+
- **耗时** = 同一 caseId 的 `dispatched → 终态` 的秒数差(取终态前最近一次 dispatched);
|
|
13
|
+
- **卡点** = `phase` 事件里**失败 case 最后停留的阶段**(各能力最多的一条);
|
|
14
|
+
- **集成健康度** = 该集成适配器声明的 `ERROR_CLASSES` 对失败文本的分类命中数 + 最近一次命中时间;
|
|
15
|
+
- **未处置** = 走 `flow_failure.classify`(与失败回路**同一套判据**,不另立一套)。
|
|
16
|
+
|
|
17
|
+
用法:
|
|
18
|
+
python3 scripts/flow_capability_stats.py [--days 7] [--json] [--flow <id>] [--ledger <path>]
|
|
19
|
+
退出码:0 正常;2 读取失败。
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import datetime as dt
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import pathlib
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from flow_registry import consumer_doc as _registry_doc # P2a:base + 业务线合并(唯一真源)
|
|
32
|
+
except Exception: # noqa: BLE001 —— 半坏机器上退回单文件(升级链路要能跑)
|
|
33
|
+
def _registry_doc(path=None):
|
|
34
|
+
_p = pathlib.Path(path) if path else (HERE / "flow-requirements.json")
|
|
35
|
+
return json.loads(_p.read_text(encoding="utf-8"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
39
|
+
sys.path.insert(0, str(HERE))
|
|
40
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
41
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
42
|
+
from stdio_utf8 import use_utf8_stdio # noqa: E402 出口编码钉 UTF-8(中文 Windows 的 GBK 管道下非 ASCII 会崩)
|
|
43
|
+
|
|
44
|
+
REGISTRY = HERE / "flow-requirements.json"
|
|
45
|
+
TERMINAL = ("done", "failed", "blocked", "failed_ack")
|
|
46
|
+
PHASE_GAP_MAX = 6 * 3600 # 阶段分段上限(超过视为挂起,不计入阶段耗时)
|
|
47
|
+
KIND_CN = {"needs_human": "等人补数据", "needs_agent": "需判断/集成问题",
|
|
48
|
+
"retryable": "可重试", "unknown": "未分类"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load() -> dict:
|
|
52
|
+
return _registry_doc()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_ledger(path: pathlib.Path) -> list:
|
|
56
|
+
out = []
|
|
57
|
+
if not path.exists():
|
|
58
|
+
return out
|
|
59
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
60
|
+
line = line.strip()
|
|
61
|
+
if not line:
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
rec = json.loads(line)
|
|
65
|
+
except Exception: # noqa: BLE001
|
|
66
|
+
continue
|
|
67
|
+
if isinstance(rec, dict):
|
|
68
|
+
out.append(rec)
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def median(xs: list) -> float:
|
|
73
|
+
xs = sorted(float(x) for x in xs)
|
|
74
|
+
if not xs:
|
|
75
|
+
return 0.0
|
|
76
|
+
mid = len(xs) // 2
|
|
77
|
+
return xs[mid] if len(xs) % 2 else (xs[mid - 1] + xs[mid]) / 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ts(rec: dict) -> float:
|
|
81
|
+
try:
|
|
82
|
+
return float(rec.get("ts") or 0)
|
|
83
|
+
except Exception: # noqa: BLE001
|
|
84
|
+
return 0.0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _flow_of(case_id: str) -> str:
|
|
88
|
+
return str(case_id).split("__", 1)[0] if "__" in str(case_id) else ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _classify(error: str, attempt=None) -> str:
|
|
92
|
+
try:
|
|
93
|
+
from flow_failure import classify # 与失败回路同一套判据
|
|
94
|
+
return str(classify(error, attempt).get("kind") or "")
|
|
95
|
+
except Exception: # noqa: BLE001
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _adapter_error_hits(integration: str, errors: list) -> dict:
|
|
100
|
+
"""集成健康度:用适配器自己的 ERROR_CLASSES 给失败文本归类(不另立一份词表)。"""
|
|
101
|
+
if not integration:
|
|
102
|
+
return {"status": "", "hits": {}, "byKind": {}, "lastHitAt": ""}
|
|
103
|
+
try:
|
|
104
|
+
import integration_contract as C
|
|
105
|
+
mod = C.load(integration)
|
|
106
|
+
except Exception: # noqa: BLE001
|
|
107
|
+
return {"status": "", "hits": {}, "byKind": {}, "lastHitAt": ""}
|
|
108
|
+
import re
|
|
109
|
+
rules = []
|
|
110
|
+
for pair in (getattr(mod, "ERROR_CLASSES", None) or {}).items() if isinstance(getattr(mod, "ERROR_CLASSES", None), dict) else []:
|
|
111
|
+
rules.append((str(pair[0]), str(pair[1])))
|
|
112
|
+
if not rules:
|
|
113
|
+
for item in (getattr(mod, "ERROR_CLASSES", None) or []):
|
|
114
|
+
try:
|
|
115
|
+
pat, kind = item[0], item[1]
|
|
116
|
+
except Exception: # noqa: BLE001
|
|
117
|
+
continue
|
|
118
|
+
rules.append((str(pat), str(kind)))
|
|
119
|
+
hits, by_kind, last = {}, {}, 0.0
|
|
120
|
+
for err, ts in errors:
|
|
121
|
+
# ① 集成自身错误词表命中("这个集成在报错")
|
|
122
|
+
for pat, kind in rules:
|
|
123
|
+
try:
|
|
124
|
+
if re.search(pat, err, re.I):
|
|
125
|
+
hits[kind] = hits.get(kind, 0) + 1
|
|
126
|
+
last = max(last, ts)
|
|
127
|
+
break
|
|
128
|
+
except re.error:
|
|
129
|
+
continue
|
|
130
|
+
# ② 失败**性质**(与失败回路同一判据):区分"集成坏了(needs_agent)"与
|
|
131
|
+
# "运营没填数据(needs_human)"—— 否则面板上"失败 5 次"会被读成"文鳐门户不稳",
|
|
132
|
+
# 实际是等人补字段(2026-09-20 用户读面板时问到的正是这一处)。
|
|
133
|
+
k = _classify(err) or "unknown"
|
|
134
|
+
by_kind[k] = by_kind.get(k, 0) + 1
|
|
135
|
+
return {
|
|
136
|
+
"status": str(getattr(mod, "STATUS", "") or ""),
|
|
137
|
+
"hits": hits,
|
|
138
|
+
"byKind": by_kind,
|
|
139
|
+
"lastHitAt": dt.datetime.fromtimestamp(last).strftime("%Y-%m-%d %H:%M") if last else "",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def stats(days: float = 7, ledger: pathlib.Path | None = None, only_flow: str = "",
|
|
144
|
+
doc: dict | None = None) -> dict:
|
|
145
|
+
d = doc or load()
|
|
146
|
+
since = dt.datetime.now().timestamp() - days * 86400
|
|
147
|
+
# 台账目录走**唯一判据**(三级链;勿在此复制链 —— 此前硬编码 ⇒ 隔离实例读生产台账)
|
|
148
|
+
led = ledger or (pathlib.Path(resolve_data_dir()['dir']) / "flow-ledger.jsonl")
|
|
149
|
+
recs = [r for r in read_ledger(led) if _ts(r) >= since]
|
|
150
|
+
|
|
151
|
+
caps = {k: v for k, v in (d.get("capabilities") or {}).items() if not k.startswith("_")}
|
|
152
|
+
flows = d.get("flows") or {}
|
|
153
|
+
out = {}
|
|
154
|
+
for cid, c in caps.items():
|
|
155
|
+
out[cid] = {"minPlan": c.get("minPlan", ""), "flows": [], "runs": 0, "done": 0, "failed": 0,
|
|
156
|
+
"blocked": 0, "acked": 0, "successRate": None, "avgSec": None, "slowestPhase": "",
|
|
157
|
+
"stuckAt": "", "pending": [], "integrations": {}, "lastAt": ""}
|
|
158
|
+
for fid, spec in flows.items():
|
|
159
|
+
cid = str((spec or {}).get("capability") or "")
|
|
160
|
+
if cid not in out:
|
|
161
|
+
continue
|
|
162
|
+
# P2a:`--flow` 可能给短名(旧 id)而注册表是规范 id ⇒ 先解析别名
|
|
163
|
+
_alias0 = d.get("aliases") or {}
|
|
164
|
+
_only = _alias0.get(only_flow, only_flow) if only_flow else ""
|
|
165
|
+
if _only and fid != _only:
|
|
166
|
+
continue
|
|
167
|
+
out[cid]["flows"].append(fid)
|
|
168
|
+
# P2a:台账里的 caseId 前缀是**写台账时的流程名**(历史与新数据都可能是旧名),
|
|
169
|
+
# 而注册表里是规范 id ⇒ 归属比较必须过一遍别名,否则统计会集体归零。
|
|
170
|
+
_alias = d.get("aliases") or {}
|
|
171
|
+
_canon = lambda pfx: _alias.get(pfx, pfx) # noqa: E731
|
|
172
|
+
mine = [r for r in recs if _canon(_flow_of(str(r.get("caseId") or ""))) == fid]
|
|
173
|
+
by_case: dict = {}
|
|
174
|
+
for r in mine:
|
|
175
|
+
by_case.setdefault(str(r.get("caseId") or ""), []).append(r)
|
|
176
|
+
errs = []
|
|
177
|
+
for case_id, rs in by_case.items():
|
|
178
|
+
rs.sort(key=_ts)
|
|
179
|
+
disp = [x for x in rs if x.get("event") == "dispatched"]
|
|
180
|
+
term = [x for x in rs if x.get("event") in TERMINAL]
|
|
181
|
+
# runs = 窗口内有活动的 case 数(不是 dispatched 事件数):跨窗口的长任务
|
|
182
|
+
# 可能"终态在窗口内、派发在窗口外",只数 dispatched 会让成功率分母与分子对不上
|
|
183
|
+
# (2026-09-20 实测 full 出现 done=25 / runs=1 的假象)。
|
|
184
|
+
if disp or term:
|
|
185
|
+
out[cid]["runs"] += 1
|
|
186
|
+
if term:
|
|
187
|
+
ev = term[-1].get("event")
|
|
188
|
+
if ev == "done":
|
|
189
|
+
out[cid]["done"] += 1
|
|
190
|
+
elif ev == "failed":
|
|
191
|
+
out[cid]["failed"] += 1
|
|
192
|
+
elif ev == "blocked":
|
|
193
|
+
out[cid]["blocked"] += 1
|
|
194
|
+
elif ev == "failed_ack":
|
|
195
|
+
out[cid]["acked"] += 1
|
|
196
|
+
if disp:
|
|
197
|
+
secs = max(0.0, _ts(term[-1]) - _ts(disp[-1]))
|
|
198
|
+
if secs and secs < 86400:
|
|
199
|
+
out[cid].setdefault("_dur", []).append(secs)
|
|
200
|
+
if term[-1].get("event") in ("failed", "blocked"):
|
|
201
|
+
phases = [x for x in rs if x.get("event") == "phase"]
|
|
202
|
+
if phases:
|
|
203
|
+
out[cid]["stuckAt"] = str(phases[-1].get("phase") or "") or out[cid]["stuckAt"]
|
|
204
|
+
# 集成健康度要的是**所有失败事件**(含被 retry 后成功的):只统计终态失败会漏掉
|
|
205
|
+
# "这个集成老在超时"这种最该看见的信号(2026-09-20 实测:net::ERR 重试成功后健康度里查无此事)。
|
|
206
|
+
for x in rs:
|
|
207
|
+
if x.get("event") != "failed":
|
|
208
|
+
continue
|
|
209
|
+
errs.append((str(x.get("error") or x.get("detail") or "").strip(), _ts(x)))
|
|
210
|
+
# 阶段耗时:相邻 phase 事件的间隔归给**前一个**阶段(最后一段归给终态)
|
|
211
|
+
phases = [x for x in rs if x.get("event") == "phase"]
|
|
212
|
+
marks = [(str(x.get("phase") or ""), _ts(x)) for x in phases]
|
|
213
|
+
if term:
|
|
214
|
+
marks.append(("(收尾)", _ts(term[-1])))
|
|
215
|
+
for i in range(len(marks) - 1):
|
|
216
|
+
name, t0 = marks[i]
|
|
217
|
+
gap = marks[i + 1][1] - t0
|
|
218
|
+
# 只统计 <6h 的分段:隔夜挂起的 case(等运营补数据)会把"某阶段耗时"拉到几十小时,
|
|
219
|
+
# 那种数字没有诊断价值(≠ 该阶段真的慢)。窗口外的分段直接丢弃并在输出里标注。
|
|
220
|
+
if name and 0 < gap < PHASE_GAP_MAX:
|
|
221
|
+
out[cid].setdefault("_phase", {}).setdefault(name, []).append(gap)
|
|
222
|
+
out[cid]["lastAt"] = dt.datetime.fromtimestamp(max([_ts(r) for r in mine] or [0])).strftime("%Y-%m-%d %H:%M") if mine else ""
|
|
223
|
+
# 未处置:**直接复用失败回路的 pending()**(不是"我照它写一遍")——
|
|
224
|
+
# 判据含"blocked 之后没有 failed_ack 仍算未处置"这条 2026-09-18 的修正,
|
|
225
|
+
# 自己重写必然慢慢漂移。
|
|
226
|
+
try:
|
|
227
|
+
from flow_failure import pending as failure_pending
|
|
228
|
+
for it in failure_pending(led, flow=fid):
|
|
229
|
+
out[cid]["pending"].append({
|
|
230
|
+
"caseId": str(it.get("caseId") or ""), "kind": str(it.get("kind") or ""),
|
|
231
|
+
"error": str(it.get("error") or "")[:160],
|
|
232
|
+
"at": dt.datetime.fromtimestamp(float(it.get("ts") or 0)).strftime("%Y-%m-%d %H:%M"),
|
|
233
|
+
})
|
|
234
|
+
except Exception: # noqa: BLE001
|
|
235
|
+
pass
|
|
236
|
+
# 集成健康度(能力下可能挂多个集成,如发票 = 文鳐 + 鑫中航)
|
|
237
|
+
integ = str((spec or {}).get("integration") or "")
|
|
238
|
+
if integ:
|
|
239
|
+
slot = out[cid]["integrations"].setdefault(
|
|
240
|
+
integ, {"status": "", "hits": {}, "byKind": {}, "lastHitAt": "", "errors": 0, "lastError": ""})
|
|
241
|
+
h = _adapter_error_hits(integ, errs)
|
|
242
|
+
slot["status"] = h["status"] or slot["status"]
|
|
243
|
+
slot["errors"] += len(errs)
|
|
244
|
+
for k, n in (h.get("byKind") or {}).items():
|
|
245
|
+
slot["byKind"][k] = slot["byKind"].get(k, 0) + n
|
|
246
|
+
if errs:
|
|
247
|
+
newest = max(errs, key=lambda e: e[1])
|
|
248
|
+
if newest[1] >= 0 and (not slot["lastError"] or True):
|
|
249
|
+
slot["lastError"] = str(newest[0])[:160]
|
|
250
|
+
for k, v in (h["hits"] or {}).items():
|
|
251
|
+
slot["hits"][k] = slot["hits"].get(k, 0) + v
|
|
252
|
+
slot["lastHitAt"] = max(slot["lastHitAt"], h["lastHitAt"])
|
|
253
|
+
|
|
254
|
+
for cid, v in out.items():
|
|
255
|
+
dur = v.pop("_dur", [])
|
|
256
|
+
phase = v.pop("_phase", {})
|
|
257
|
+
v["avgSec"] = round(sum(dur) / len(dur)) if dur else None
|
|
258
|
+
# 中位数而不是均值:长挂起的 case(隔夜等运营补数据)会把均值拉到小时级,
|
|
259
|
+
# 均值看着像"每条发票要跑 3 小时",中位数才反映典型耗时。
|
|
260
|
+
v["medianSec"] = round(median(dur)) if dur else None
|
|
261
|
+
v["phases"] = {k: round(median(x)) for k, x in
|
|
262
|
+
sorted(phase.items(), key=lambda kv: -median(kv[1]))[:5]}
|
|
263
|
+
v["slowestPhase"] = (f"{next(iter(v['phases']))} ≈{v['phases'][next(iter(v['phases']))]}s"
|
|
264
|
+
if v["phases"] else "")
|
|
265
|
+
denom = v["done"] + v["failed"] + v["blocked"]
|
|
266
|
+
v["successRate"] = round(v["done"] / denom, 3) if denom else None
|
|
267
|
+
v["pendingCount"] = len(v["pending"])
|
|
268
|
+
v["pending"] = v["pending"][:5]
|
|
269
|
+
return {"windowDays": days, "ledger": str(led), "generatedAt": dt.datetime.now().isoformat(timespec="seconds"),
|
|
270
|
+
"capabilities": out}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main(argv=None) -> int:
|
|
274
|
+
use_utf8_stdio() # 唯一出口编码判据:见 scripts/lib/stdio_utf8.py 的事故说明
|
|
275
|
+
ap = argparse.ArgumentParser(description="按能力观测(成功率/耗时/卡点/集成健康度)")
|
|
276
|
+
ap.add_argument("--days", type=float, default=7)
|
|
277
|
+
ap.add_argument("--ledger")
|
|
278
|
+
ap.add_argument("--flow")
|
|
279
|
+
ap.add_argument("--json", action="store_true")
|
|
280
|
+
a = ap.parse_args(argv)
|
|
281
|
+
try:
|
|
282
|
+
data = stats(a.days, pathlib.Path(a.ledger) if a.ledger else None, a.flow or "")
|
|
283
|
+
except Exception as exc: # noqa: BLE001
|
|
284
|
+
print(f"❌ 读取台账失败:{exc}", file=sys.stderr)
|
|
285
|
+
return 2
|
|
286
|
+
if a.json:
|
|
287
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
288
|
+
return 0
|
|
289
|
+
print(f" 窗口:最近 {data['windowDays']:g} 天 · 台账 {data['ledger']}")
|
|
290
|
+
for cid, v in data["capabilities"].items():
|
|
291
|
+
rate = "—" if v["successRate"] is None else f"{v['successRate'] * 100:.0f}%"
|
|
292
|
+
dur = "—" if v["medianSec"] is None else f"中位 {v['medianSec']}s"
|
|
293
|
+
print(f" {cid:18} 档位={v['minPlan']:11} 流程={len(v['flows'])} 跑={v['runs']} "
|
|
294
|
+
f"成功={v['done']}/失败={v['failed']}/等人={v['blocked']} 成功率={rate} 平均={dur}")
|
|
295
|
+
if v["stuckAt"]:
|
|
296
|
+
print(f" 卡点:{v['stuckAt']}")
|
|
297
|
+
if v.get("slowestPhase"):
|
|
298
|
+
print(" 阶段耗时中位(只算 <6h 的分段):"
|
|
299
|
+
+ "、".join(f"{k}≈{s2}s" for k, s2 in v["phases"].items()))
|
|
300
|
+
for name, h in (v["integrations"] or {}).items():
|
|
301
|
+
hits = "、".join(f"{k}×{n}" for k, n in (h["hits"] or {}).items()) or "无命中"
|
|
302
|
+
kinds = "、".join(f"{KIND_CN.get(k, k)} {n}" for k, n in (h.get("byKind") or {}).items()) or "—"
|
|
303
|
+
print(f" 集成 {name}({h['status'] or '?'}):失败 {h['errors']} 次({kinds})· 词表命中 {hits}"
|
|
304
|
+
+ (f" · 最近 {h['lastHitAt']}" if h["lastHitAt"] else ""))
|
|
305
|
+
if h.get("lastError"):
|
|
306
|
+
print(f" 最近错误:{h['lastError'][:120]}")
|
|
307
|
+
if v["pendingCount"]:
|
|
308
|
+
print(f" 未处置 {v['pendingCount']} 条:"
|
|
309
|
+
+ ";".join(f"{p['caseId']}[{p['kind']}]" for p in v["pending"][:3]))
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""流程「案件(case)」归一化层 —— 把**任意来源**的字段收敛成统一中间表示。
|
|
3
|
+
|
|
4
|
+
为什么需要:数据源现在只有飞书表,将来会有别的方式(用户在会话里给字段、CSV、其他系统…)。
|
|
5
|
+
做法是**要求侧与供给侧分离**:要求侧固定(`scripts/flow-requirements.json`:每流程必需字段 + 谁补),
|
|
6
|
+
供给侧做**可插拔适配器**;两者之间用 **case** 对接,下游(分诊/闸门/执行/回写)只消费 case。
|
|
7
|
+
|
|
8
|
+
case 契约(version 1):
|
|
9
|
+
{
|
|
10
|
+
"version": 1,
|
|
11
|
+
"caseId": "<flowId>__<ticketKey>", # 幂等键:同流程同票 = 同 case
|
|
12
|
+
"flowId": "wenyao-invoice",
|
|
13
|
+
"ticketKey": "76637122",
|
|
14
|
+
"fields": { "<字段名>": <值>, ... },
|
|
15
|
+
"provenance": { # ★硬要求★ 每个字段的来源与提供者
|
|
16
|
+
"<字段名>": {"sourceId": "...", "sourceKind": "feishu-base|conversation|cli-json|...",
|
|
17
|
+
"providedBy": "<谁>", "at": "<ISO8601>"}
|
|
18
|
+
},
|
|
19
|
+
"sources": ["<sourceId>", ...], # 本 case 累积过的来源
|
|
20
|
+
"conflicts": [ {"field": "...", "kept": {...}, "rejected": {...}} ],
|
|
21
|
+
"createdAt": "...", "updatedAt": "..."
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
用法:
|
|
25
|
+
python3 scripts/flow_case.py --flow wenyao-invoice --ticket 76637122 \
|
|
26
|
+
--source-id lip-feishu --source-kind feishu-base --provided-by 运营 \
|
|
27
|
+
--fields-from-json /tmp/row.json [--out-dir ~/.dsh/argent/flow-cases] [--json]
|
|
28
|
+
|
|
29
|
+
退出码:0 = 写入/合并成功且无冲突 ; 1 = 有冲突(**落卡等人确认**,case 仍写入); 2 = 参数/IO 错误
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import datetime as _dt
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import pathlib
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
# 数据目录:**唯一判据**在 `scripts/lib/argent_datadir.py`(三级链 `ARGENT_DATA_DIR` →
|
|
41
|
+
# `$DSH_HOME/argent` → `~/.dsh/argent`,与 JS 内核 `core/lib/argent-datadir.mjs` 同语义)。
|
|
42
|
+
# 历史:本文件曾**连 ARGENT_DATA_DIR 都不看** ⇒ 隔离跑(MCP 影子沙箱、测试夹具)写的 case
|
|
43
|
+
# 全落进**真实** `~/.dsh/argent/flow-cases/`(2026-10-02 实测:`r-flow__T-NEW` 夹具件污染生产目录);
|
|
44
|
+
# 后改两级链(不看 DSH_HOME)。现在一律走唯一判据。
|
|
45
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent / 'lib'))
|
|
46
|
+
from argent_datadir import resolve_data_dir # noqa: E402
|
|
47
|
+
DEFAULT_OUT = pathlib.Path(resolve_data_dir()['dir']) / 'flow-cases'
|
|
48
|
+
CASE_VERSION = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _now() -> str:
|
|
52
|
+
return _dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def safe_key(ticket_key: str) -> str:
|
|
56
|
+
"""文件名安全化:票号可能含 `/`(一个单元格写两个货件号)等路径字符。
|
|
57
|
+
|
|
58
|
+
逻辑键(caseId 内的 ticketKey)保持原样,仅**落盘文件名**替换危险字符,
|
|
59
|
+
避免 `a/b` 被当成目录。
|
|
60
|
+
"""
|
|
61
|
+
bad = '/\\:*?"<>|\n\r\t '
|
|
62
|
+
out = ''.join('-' if c in bad else c for c in str(ticket_key)).strip('-')
|
|
63
|
+
return out or 'unknown'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def case_id(flow_id: str, ticket_key: str) -> str:
|
|
67
|
+
return f"{flow_id}__{ticket_key}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_case(flow_id: str, ticket_key: str, fields: dict, source_id: str,
|
|
71
|
+
source_kind: str, provided_by: str, at: str | None = None) -> dict:
|
|
72
|
+
"""从**一个来源**构造 case(新 case)。"""
|
|
73
|
+
at = at or _now()
|
|
74
|
+
return {
|
|
75
|
+
"version": CASE_VERSION,
|
|
76
|
+
"caseId": case_id(flow_id, ticket_key),
|
|
77
|
+
"flowId": flow_id,
|
|
78
|
+
"ticketKey": str(ticket_key),
|
|
79
|
+
"fields": dict(fields),
|
|
80
|
+
"provenance": {k: {"sourceId": source_id, "sourceKind": source_kind,
|
|
81
|
+
"providedBy": provided_by, "at": at} for k in fields},
|
|
82
|
+
"sources": [source_id],
|
|
83
|
+
"conflicts": [],
|
|
84
|
+
"createdAt": at,
|
|
85
|
+
"updatedAt": at,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def merge_case(existing: dict, flow_id: str, ticket_key: str, fields: dict, source_id: str,
|
|
90
|
+
source_kind: str, provided_by: str, at: str | None = None) -> tuple[dict, list]:
|
|
91
|
+
"""把一个来源的字段**合并**进已有 case。
|
|
92
|
+
|
|
93
|
+
规则(对应红线要求):
|
|
94
|
+
1. **新字段** → 直接并入,记 provenance;
|
|
95
|
+
2. **同值** → 仅追加来源(幂等,不产生冲突);
|
|
96
|
+
3. **不同值** → **不覆盖**,记入 `conflicts` 交人确认(**绝不静默选一个**)。
|
|
97
|
+
· 例外(2026-09-18 修):`_` 前缀的**合成字段** 与 **同一来源**的再次读数 ⇒ 覆盖
|
|
98
|
+
(否则系统字段会被第一次读到的旧值冻住 —— 详见循环内注释)。
|
|
99
|
+
⚠️ 这条正是"同一票在飞书表与会话里同时出现"的处理方式:冲突落卡等人。
|
|
100
|
+
"""
|
|
101
|
+
at = at or _now()
|
|
102
|
+
out = json.loads(json.dumps(existing)) # 深拷贝,避免调用方对象被改
|
|
103
|
+
conflicts = []
|
|
104
|
+
for k, v in fields.items():
|
|
105
|
+
cur = out["fields"].get(k)
|
|
106
|
+
prev = out["provenance"].get(k) or {}
|
|
107
|
+
# 2026-09-18 真实故障修复:**系统维护字段必须刷新**,不能被下面的"不覆盖"规则冻住。
|
|
108
|
+
# 现场:运营在飞书表勾了「就绪」,但 case 第一次读到的是空值 ⇒ 之后每次合并都判
|
|
109
|
+
# "不同值 ⇒ 不覆盖" ⇒ `_phase` 永远停在 await_release、分诊恒为 to_dispatch=0,
|
|
110
|
+
# 触发器连续报"无票可认领"(勾了就绪也不跑)。清掉 case 目录才恢复,说明就是这里。
|
|
111
|
+
# 两类必须覆盖:① `_` 前缀的合成字段(`_phase`/`_record_id`…,系统算出来的);
|
|
112
|
+
# ② **同一来源**再次读到的值(同一个源刷新自己,不是跨源冲突)。
|
|
113
|
+
if k in out["fields"] and (str(k).startswith("_") or prev.get("sourceId") == source_id):
|
|
114
|
+
if str(cur) != str(v):
|
|
115
|
+
out["fields"][k] = v
|
|
116
|
+
out["provenance"][k] = {"sourceId": source_id, "sourceKind": source_kind,
|
|
117
|
+
"providedBy": provided_by, "at": at}
|
|
118
|
+
continue
|
|
119
|
+
if k not in out["fields"]:
|
|
120
|
+
out["fields"][k] = v
|
|
121
|
+
out["provenance"][k] = {"sourceId": source_id, "sourceKind": source_kind,
|
|
122
|
+
"providedBy": provided_by, "at": at}
|
|
123
|
+
elif str(cur) == str(v):
|
|
124
|
+
out["provenance"].setdefault(k, {})
|
|
125
|
+
out["provenance"][k].setdefault("alsoSeenFrom", [])
|
|
126
|
+
if source_id not in out["provenance"][k]["alsoSeenFrom"]:
|
|
127
|
+
out["provenance"][k]["alsoSeenFrom"].append(source_id)
|
|
128
|
+
else:
|
|
129
|
+
conflicts.append({
|
|
130
|
+
"field": k,
|
|
131
|
+
"kept": {"value": cur, "provenance": out["provenance"].get(k)},
|
|
132
|
+
"rejected": {"value": v, "sourceId": source_id, "sourceKind": source_kind,
|
|
133
|
+
"providedBy": provided_by, "at": at},
|
|
134
|
+
})
|
|
135
|
+
if source_id not in out["sources"]:
|
|
136
|
+
out["sources"].append(source_id)
|
|
137
|
+
out["conflicts"] = (out.get("conflicts") or []) + conflicts
|
|
138
|
+
out["updatedAt"] = at
|
|
139
|
+
return out, conflicts
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main(argv=None) -> int:
|
|
143
|
+
ap = argparse.ArgumentParser(description="流程 case 归一化(多源 → 统一中间表示)")
|
|
144
|
+
ap.add_argument("--flow", required=True, help="流程 id,如 wenyao-invoice")
|
|
145
|
+
ap.add_argument("--ticket", required=True, help="票据键,如货件号 76637122")
|
|
146
|
+
ap.add_argument("--fields-from-json", required=True,
|
|
147
|
+
help="字段 JSON 文件路径(对象),或 - 表示从 stdin 读")
|
|
148
|
+
ap.add_argument("--source-id", required=True, help="来源标识,如 lip-feishu / chat-oc_xxx")
|
|
149
|
+
ap.add_argument("--source-kind", default="cli-json",
|
|
150
|
+
help="来源类型:feishu-base | conversation | cli-json | csv | ...")
|
|
151
|
+
ap.add_argument("--provided-by", default="unknown", help="提供者(运营/用户/角色名)")
|
|
152
|
+
ap.add_argument("--out-dir", default=str(DEFAULT_OUT))
|
|
153
|
+
ap.add_argument("--json", action="store_true", help="把 case 打到 stdout")
|
|
154
|
+
a = ap.parse_args(argv)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
raw = sys.stdin.read() if a.fields_from_json == "-" else pathlib.Path(a.fields_from_json).read_text(encoding="utf-8")
|
|
158
|
+
fields = json.loads(raw)
|
|
159
|
+
if not isinstance(fields, dict):
|
|
160
|
+
raise ValueError("字段必须是 JSON 对象")
|
|
161
|
+
except Exception as exc: # noqa: BLE001
|
|
162
|
+
print(f"读取字段失败: {exc}", file=sys.stderr)
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
out_dir = pathlib.Path(a.out_dir).expanduser()
|
|
166
|
+
path = out_dir / f"{safe_key(case_id(a.flow, a.ticket))}.json"
|
|
167
|
+
try:
|
|
168
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
if path.exists():
|
|
170
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
171
|
+
case, conflicts = merge_case(existing, a.flow, a.ticket, fields,
|
|
172
|
+
a.source_id, a.source_kind, a.provided_by)
|
|
173
|
+
else:
|
|
174
|
+
case = build_case(a.flow, a.ticket, fields, a.source_id, a.source_kind, a.provided_by)
|
|
175
|
+
conflicts = []
|
|
176
|
+
path.write_text(json.dumps(case, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
177
|
+
except Exception as exc: # noqa: BLE001
|
|
178
|
+
print(f"写入失败: {exc}", file=sys.stderr)
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
if a.json:
|
|
182
|
+
print(json.dumps(case, ensure_ascii=False, indent=2))
|
|
183
|
+
else:
|
|
184
|
+
print(f"case {case['caseId']} 字段 {len(case['fields'])} 个|来源 {len(case['sources'])} 个"
|
|
185
|
+
f"|冲突 {len(conflicts)} 个 → {path}")
|
|
186
|
+
for c in conflicts:
|
|
187
|
+
print(f" ⚠ 冲突 [{c['field']}] 保留={c['kept']['value']!r}"
|
|
188
|
+
f"({c['kept']['provenance'].get('sourceId') if c['kept'].get('provenance') else '?'})"
|
|
189
|
+
f" 拒绝={c['rejected']['value']!r}({c['rejected']['sourceId']})→ 需人确认")
|
|
190
|
+
return 1 if conflicts else 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|