@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,454 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""flow-auto-retry —— 流程失败「可用性类」的**受守卫自动重试**(用户口径方案 a;2026-10-13)。
|
|
3
|
+
|
|
4
|
+
背景(真实现场):可用性类失败(网络/限流/浏览器瞬时类)会落到台账 `failed` → `blocked`,
|
|
5
|
+
此后触发器只报"需人工",**不再自动跑**(现场两条票是人工手跑回来的)。用户拍板方案 (a):
|
|
6
|
+
这类失败在**上限 + 冷却**约束下自动重试,而不是等人工。
|
|
7
|
+
|
|
8
|
+
⛔ 本脚本**不写表**:真正"重置后重建"的动作全部委托 `scripts/flow_retry.py`
|
|
9
|
+
(走它的**全部**守卫:分类闸门 / 预约闸门 / 次数上限 / 写前整行备份 + 写后读回 + 审计 + 台账 `retry`)。
|
|
10
|
+
本脚本只做**候选筛选 + 每轮限流 + 冷却 + 可见性**。
|
|
11
|
+
|
|
12
|
+
候选(缺任一条 ⇒ **跳过并说明原因**,绝不"差不多就重试"):
|
|
13
|
+
① 台账末条**状态性**事件 ∈ {failed, blocked}(已重试过的票末条是 retry ⇒ 不再筛进来,天然防重入);
|
|
14
|
+
末条若是 `phase`/`wake` 这类**信息性**事件,回退到最近一条状态性事件(与 `flow_triage` 同一口径);
|
|
15
|
+
② 唯一分类器 `scripts/flow_failure.py#classify` 判 **retryable**(⛔ 无 override:
|
|
16
|
+
业务类/门户状态类一律跳过、保持需人工);
|
|
17
|
+
③ 预约闸门 `scripts/flow_retry.py#appointment_gate` 放行(无预约残留 / 无"预约取消失败"留痕);
|
|
18
|
+
④ **尝试**次数 < 上限(台账 `retry` 事件数 + 被委托侧拒绝的次数 —— 后者也要算,
|
|
19
|
+
否则一张"每轮都会被拒"的票会永远每轮白占名额/白刷卡片);
|
|
20
|
+
⑤ 距末条 failed/blocked ≥ 冷却分钟数;
|
|
21
|
+
⑥ 每轮最多处理 N 票(避免风暴)。
|
|
22
|
+
四个量(上限/冷却/每轮上限/开关)全部来自 `scripts/flow_config.py#autoRetry`(唯一读取点),
|
|
23
|
+
可用 `--max` / `--cooldown-min` / `--per-cycle` 覆盖(运维/夹具用)。
|
|
24
|
+
|
|
25
|
+
可见性:审计 JSONL(`<台账同目录>/flow-auto-retry-audit.jsonl`,每**真做**的一次重试一行)
|
|
26
|
+
+ 台账(由 `flow_retry` 写的那条 `retry`,其 `detail` 带 `by=flow-auto-retry`)
|
|
27
|
+
+ **一条**内部群卡片(复用既有通知路径 `scripts/feishu-card.mjs`;按轮汇总,⛔ 一票一条 = 噪音)。
|
|
28
|
+
|
|
29
|
+
⚠️ **为什么本脚本不自己另写一条 `retry` 台账事件**(口径说明,⛔ 勿"顺手改回"):
|
|
30
|
+
`scripts/flow_triage.py` 的"人工放行重跑"闸门(Rule R)把**末条 `retry` 事件的 `by`**
|
|
31
|
+
当作判据之一(必须是 `flow_retry`)。若自动重试再追加一条 `by=flow-auto-retry` 的 retry 事件,
|
|
32
|
+
它就成为**末条** ⇒ 该票下一轮**既进不了 `to_dispatch`**、又落进"分诊未归类"兜底(需人工)
|
|
33
|
+
= 自动重试白做。⇒ 一次重试**只**由 `flow_retry` 记**恰好一条**;本脚本的署名落在
|
|
34
|
+
它的 `--reason`(进台账 `detail`)与自己的审计 JSONL / 卡片 / 日志里。
|
|
35
|
+
|
|
36
|
+
用法:
|
|
37
|
+
python3 scripts/flow-auto-retry.py --flow full # **dry-run**(默认;只打印将重试谁与原因)
|
|
38
|
+
python3 scripts/flow-auto-retry.py --flow full --yes # 真做(走 flow_retry 全部守卫)
|
|
39
|
+
… --max 2 --cooldown-min 20 --per-cycle 2 --only full__<rec> --no-notify --json
|
|
40
|
+
退出码:0 正常(含"无候选 / 全部跳过 / 已关闭");2 前置不可用(配置或判据实现取不到 ⇒ 绝不猜)。
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import datetime
|
|
46
|
+
import json
|
|
47
|
+
import pathlib
|
|
48
|
+
import shutil
|
|
49
|
+
import subprocess
|
|
50
|
+
import sys
|
|
51
|
+
import time
|
|
52
|
+
|
|
53
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
54
|
+
sys.path.insert(0, str(HERE))
|
|
55
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
56
|
+
|
|
57
|
+
import flow_config # noqa: E402 唯一配置读取点(四个量的默认值只在它里面)
|
|
58
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
59
|
+
|
|
60
|
+
DATA_DIR = pathlib.Path(resolve_data_dir()['dir'])
|
|
61
|
+
# 信息性事件(只带说明、不改变票据工作状态)⇒ 判据要回退到最近一条**状态性**事件。
|
|
62
|
+
# 逐条口径与 `scripts/flow_triage.py#INFO_EVENTS` 一致(2026-10-09 P0:末条 `phase` 曾让票静默消失)。
|
|
63
|
+
INFO_EVENTS = ('phase', 'wake', 'policy_apply', 'policy_new')
|
|
64
|
+
FAIL_EVENTS = ('failed', 'blocked')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
# 判据实现:**只**从既有模块取(⛔ 本脚本不自写第二套分类/闸门规则)
|
|
69
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
def load_judges() -> tuple:
|
|
71
|
+
"""取(分类器, 规范原因, 预约闸门, 失败步骤)。任一取不到 ⇒ 抛错(调用方停住)。"""
|
|
72
|
+
try:
|
|
73
|
+
from flow_failure import classify, canonical_reason # noqa: PLC0415
|
|
74
|
+
except Exception as exc: # noqa: BLE001
|
|
75
|
+
raise RuntimeError(f'失败分类器不可用({exc})—— 判据只能有一份,⛔ 本脚本不自写第二套')
|
|
76
|
+
try:
|
|
77
|
+
from flow_retry import appointment_gate, last_failed_step # noqa: PLC0415
|
|
78
|
+
except Exception as exc: # noqa: BLE001
|
|
79
|
+
raise RuntimeError(f'预约闸门不可用({exc})—— 有违约金风险,⛔ 缺闸门时绝不自动重试')
|
|
80
|
+
return classify, canonical_reason, appointment_gate, last_failed_step
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_auto_retry_cfg(flow_id: str):
|
|
84
|
+
"""四个量的**唯一读取点** = `flow_config.load_config(...)['autoRetry']`;读不到 ⇒ None(响亮)。"""
|
|
85
|
+
try:
|
|
86
|
+
cfg = flow_config.load_config(flow_id, strict=False)
|
|
87
|
+
except Exception as exc: # noqa: BLE001
|
|
88
|
+
print(f'❌ 读不到流程配置({exc})⇒ 不猜默认值,本轮不做', file=sys.stderr)
|
|
89
|
+
return None
|
|
90
|
+
ar = cfg.get('autoRetry')
|
|
91
|
+
if not isinstance(ar, dict) or 'enabled' not in ar:
|
|
92
|
+
print('❌ 配置读取点里没有 autoRetry 段(flow_config.py 版本过旧或投递不完整)'
|
|
93
|
+
'⇒ 本轮不做(⛔ 不自带第二份默认值)', file=sys.stderr)
|
|
94
|
+
return None
|
|
95
|
+
return ar, str(cfg.get('flowId') or flow_id)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
# 台账读取(只读)
|
|
100
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
101
|
+
def read_events(path: pathlib.Path) -> list:
|
|
102
|
+
"""读 JSONL 台账(坏行跳过)。读失败 ⇒ 抛 OSError(由 main 收口成 exit 2,⛔ 不当作"无候选")。"""
|
|
103
|
+
out = []
|
|
104
|
+
if not path.exists():
|
|
105
|
+
return out
|
|
106
|
+
for ln in path.read_text(encoding='utf-8').splitlines():
|
|
107
|
+
ln = ln.strip()
|
|
108
|
+
if not ln:
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
e = json.loads(ln)
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
continue
|
|
114
|
+
if isinstance(e, dict) and e.get('caseId'):
|
|
115
|
+
out.append(e)
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def case_prefixes(flow_id: str, canonical: str) -> tuple:
|
|
120
|
+
"""该流程在台账里可能出现的**全部** caseId 前缀(规范 id 优先 + 旧短名)。
|
|
121
|
+
|
|
122
|
+
台账里写的是"当时那条流程叫什么":落位前的短名与落位后的规范 id 并存 ⇒ 只认一个会让
|
|
123
|
+
另一半 case **集体消失**。首选引擎的规范实现(`flow_failure.case_prefixes`,它同时覆盖别名),
|
|
124
|
+
取不到时**保守**用调用方给的两个名字(老克隆兜底)。
|
|
125
|
+
"""
|
|
126
|
+
names = [n for n in (canonical, flow_id) if n]
|
|
127
|
+
try:
|
|
128
|
+
from flow_failure import case_prefixes as _engine_prefixes # noqa: PLC0415
|
|
129
|
+
names.extend(_engine_prefixes(canonical or flow_id))
|
|
130
|
+
except Exception: # noqa: BLE001
|
|
131
|
+
pass
|
|
132
|
+
return tuple(sorted({f'{n}__' for n in names}, key=len, reverse=True))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def effective_last(evs: list) -> tuple:
|
|
136
|
+
"""(有效末条事件, 回退掉的信息性事件名)。信息性事件不改变票据状态 ⇒ 判定回退到状态性事件。"""
|
|
137
|
+
last_state, tail = None, ''
|
|
138
|
+
for e in evs:
|
|
139
|
+
ev = str(e.get('event') or '')
|
|
140
|
+
if ev in INFO_EVENTS:
|
|
141
|
+
tail = ev
|
|
142
|
+
continue
|
|
143
|
+
last_state, tail = e, ''
|
|
144
|
+
return last_state, tail
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def retry_count(events: list, case_id: str) -> int:
|
|
148
|
+
"""该 case 的 `retry` 事件数(= **成功重建过几次**)。
|
|
149
|
+
|
|
150
|
+
⚠️ 判据必须与 `flow_retry.retry_count()` **逐字同口径**(同一条 `event == 'retry'`):
|
|
151
|
+
本脚本先筛、`flow_retry` 再守 —— 两处口径若不同,就会出现"我说该重试、它拒绝"的空转。
|
|
152
|
+
"""
|
|
153
|
+
return sum(1 for e in events
|
|
154
|
+
if str(e.get('caseId') or '') == case_id and str(e.get('event') or '') == 'retry')
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def refused_count(audit_path: pathlib.Path, case_id: str) -> int:
|
|
158
|
+
"""该 case **被委托侧拒绝过几次**(读本脚本自己的审计:`ok:false` 的那些行)。
|
|
159
|
+
|
|
160
|
+
为什么上限要把它算进去:委托侧拒绝时**不会**写台账 `retry` 事件 ⇒ 只看台账事件数的话,
|
|
161
|
+
一张"每轮都会被拒"的票(表里状态已不是失败 / 表备注分类不是可用性类 / 表备注的预约留痕
|
|
162
|
+
与台账不一致…)会**每轮**白占一个每轮名额、白刷一条卡片与一行审计,直到永远。
|
|
163
|
+
把"拒绝"也算作一次尝试 ⇒ 最多 `max` 次之后就明确交人工(日志点名原因)。
|
|
164
|
+
"""
|
|
165
|
+
if not audit_path.exists():
|
|
166
|
+
return 0
|
|
167
|
+
n = 0
|
|
168
|
+
try:
|
|
169
|
+
for ln in audit_path.read_text(encoding='utf-8').splitlines():
|
|
170
|
+
ln = ln.strip()
|
|
171
|
+
if not ln:
|
|
172
|
+
continue
|
|
173
|
+
try:
|
|
174
|
+
e = json.loads(ln)
|
|
175
|
+
except Exception: # noqa: BLE001
|
|
176
|
+
continue
|
|
177
|
+
if str(e.get('caseId') or '') == case_id and e.get('ok') is False:
|
|
178
|
+
n += 1
|
|
179
|
+
except OSError:
|
|
180
|
+
return 0
|
|
181
|
+
return n
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def plain_text(last: dict, canonical_reason) -> str:
|
|
185
|
+
"""候选的失败文本(给分类器与预约闸门):`reason`(规范原因)→ `error` → `detail`,全并起来。"""
|
|
186
|
+
parts = [str(last.get('reason') or '').strip()]
|
|
187
|
+
err = str(last.get('error') or '').strip()
|
|
188
|
+
if err:
|
|
189
|
+
try:
|
|
190
|
+
err = canonical_reason(err) # 剥身份前缀,与表备注同源同文
|
|
191
|
+
except Exception: # noqa: BLE001
|
|
192
|
+
pass
|
|
193
|
+
parts.append(err)
|
|
194
|
+
parts.append(str(last.get('detail') or '').strip())
|
|
195
|
+
return ' '.join([p for p in parts if p]).strip()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
# 四道闸门(逐个独立函数:既便于读,也便于门禁做"**去掉它就必须判红**"的有牙自证)
|
|
200
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
201
|
+
def check_classification(ctx: dict):
|
|
202
|
+
"""② 可用性类判据:**只**认唯一分类器的 `retryable`(⛔ 无 override)。"""
|
|
203
|
+
kind = str((ctx.get('kind') or {}).get('kind') or '')
|
|
204
|
+
if kind != 'retryable':
|
|
205
|
+
return f"分类={kind}({(ctx.get('kind') or {}).get('reason') or ''})⇒ 非可用性类(业务/门户状态类),保持需人工"
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def check_appointment(ctx: dict):
|
|
210
|
+
"""③ 预约闸门:无残留才允许重建(有预约未取消 ⇒ 违约金)。"""
|
|
211
|
+
gate = ctx.get('gate') or {}
|
|
212
|
+
if not gate.get('ok'):
|
|
213
|
+
return f"预约闸门拒绝({gate.get('why') or '证据不足'})"
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def check_upper_bound(ctx: dict):
|
|
218
|
+
"""④ 次数上限:**尝试**次数(台账 `retry` 事件 + 被委托侧拒绝的次数)< 上限。"""
|
|
219
|
+
n = int(ctx.get('retryCount') or 0) + int(ctx.get('refusedCount') or 0)
|
|
220
|
+
mx = int(ctx['cfg']['max'])
|
|
221
|
+
if n >= mx:
|
|
222
|
+
r = int(ctx.get('refusedCount') or 0)
|
|
223
|
+
return f'已达上限({n}/{mx}{f",其中 {r} 次被委托侧拒绝" if r else ""})⇒ 交人工'
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def check_cooldown(ctx: dict):
|
|
228
|
+
"""⑤ 冷却:距末条 failed/blocked 必须 ≥ 冷却分钟数。"""
|
|
229
|
+
left = float(ctx.get('cooldownLeftSec') or 0)
|
|
230
|
+
if left > 0:
|
|
231
|
+
return f'冷却未到(还需约 {int((left + 59) // 60)} 分钟)⇒ 交人工'
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ⛔ `CHECKS` 是**四道闸门的唯一清单**(门禁 `scripts/tests/flow-auto-retry.test.mjs` 靠替换它
|
|
236
|
+
# 做"去掉某道闸门 ⇒ 夹具必须判红"的有牙自证 —— 改这个元组等于改判据强度,必须同时改用例)。
|
|
237
|
+
CHECKS = (check_classification, check_appointment, check_upper_bound, check_cooldown)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
241
|
+
# 动作:委托 flow_retry(**不自己写表**)+ 审计 + 通知
|
|
242
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
243
|
+
def do_retry(flow_id: str, ctx: dict, cfg: dict) -> dict:
|
|
244
|
+
"""委托 `flow_retry.py --yes`(走它全部守卫),返回 `{exit, ok, cmd, out}`。"""
|
|
245
|
+
n = int(ctx.get('retryCount') or 0)
|
|
246
|
+
reason = (f"自动重试(by=flow-auto-retry)第 {n + 1}/{cfg['max']} 次:"
|
|
247
|
+
f"{(ctx.get('kind') or {}).get('reason') or ''}")
|
|
248
|
+
cmd = [sys.executable, str(HERE / 'flow_retry.py'), '--flow', flow_id, '--case', ctx['caseId'],
|
|
249
|
+
'--yes', '--max', str(cfg['max']), '--reason', reason]
|
|
250
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
251
|
+
out = f"{p.stdout or ''}{p.stderr or ''}".strip()
|
|
252
|
+
return {'exit': int(p.returncode), 'ok': int(p.returncode) == 0, 'cmd': cmd, 'out': out}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def audit(audit_path: pathlib.Path, rec: dict) -> None:
|
|
256
|
+
"""追加一行审计(0600 由目录/文件默认权限决定;⛔ 不写业务值以外的敏感内容)。"""
|
|
257
|
+
try:
|
|
258
|
+
audit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
with audit_path.open('a', encoding='utf-8') as fh:
|
|
260
|
+
fh.write(json.dumps(rec, ensure_ascii=False) + '\n')
|
|
261
|
+
except Exception as exc: # noqa: BLE001
|
|
262
|
+
print(f' ⚠️ 审计未落盘({exc})', file=sys.stderr)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def notify_card(flow_id: str, rows: list, cfg: dict) -> bool:
|
|
266
|
+
"""**一条**内部群卡片(按轮汇总)。返回是否真发出。⛔ 通知失败不影响已做的重试(只告警)。"""
|
|
267
|
+
if not rows:
|
|
268
|
+
return False
|
|
269
|
+
script = HERE / 'feishu-card.mjs'
|
|
270
|
+
if not script.exists():
|
|
271
|
+
print(' ⚠️ 通知未发:缺少 scripts/feishu-card.mjs(旧版本)——见日志里的重试记录')
|
|
272
|
+
return False
|
|
273
|
+
node = shutil.which('node') or 'node'
|
|
274
|
+
cases = '、'.join(str(r['caseId']) for r in rows)[:200]
|
|
275
|
+
errs = ';'.join(f"{r['caseId']}:{str(r.get('why') or '')[:60]}" for r in rows)[:300]
|
|
276
|
+
args = [node, str(script), '--template', 'failure-alert', '--to', 'internal',
|
|
277
|
+
'--flow', flow_id, '--level', 'retryable', '--case', cases,
|
|
278
|
+
'--attempt', str(max(int(r.get('attempt') or 1) for r in rows)), '--max', str(cfg['max']),
|
|
279
|
+
'--error', errs or '可用性类失败(分类 retryable)',
|
|
280
|
+
'--action', '已自动重置重建(走 flow_retry 全部守卫);下一轮认领会自动跑,无需人工介入',
|
|
281
|
+
'--send']
|
|
282
|
+
try:
|
|
283
|
+
p = subprocess.run(args, capture_output=True, text=True, timeout=90)
|
|
284
|
+
except Exception as exc: # noqa: BLE001
|
|
285
|
+
print(f' ⚠️ 通知未发({exc})——已做的重试不受影响')
|
|
286
|
+
return False
|
|
287
|
+
if p.returncode != 0:
|
|
288
|
+
print(f" ⚠️ 通知未发(exit={p.returncode}):{(p.stderr or p.stdout or '').strip()[:200]}")
|
|
289
|
+
return False
|
|
290
|
+
print(f" 🔔 已发一条内部群卡片(本轮自动重试 {len(rows)} 票)")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
295
|
+
# 主流程
|
|
296
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
297
|
+
def main(argv=None) -> int:
|
|
298
|
+
ap = argparse.ArgumentParser(description='失败自动重试(可用性类;受守卫、默认 dry-run)')
|
|
299
|
+
ap.add_argument('--flow', default='full', help='流程 id(缺省 full)')
|
|
300
|
+
ap.add_argument('--ledger', default=str(DATA_DIR / 'flow-ledger.jsonl'), help='台账路径(缺省=数据目录)')
|
|
301
|
+
ap.add_argument('--yes', '--apply', dest='yes', action='store_true', help='真做(缺省 dry-run)')
|
|
302
|
+
ap.add_argument('--max', type=int, help='每票上限(覆盖 flow_config autoRetry.max)')
|
|
303
|
+
ap.add_argument('--cooldown-min', type=int, help='冷却分钟(覆盖 autoRetry.cooldownMin)')
|
|
304
|
+
ap.add_argument('--per-cycle', type=int, help='每轮最多处理几票(覆盖 autoRetry.perCycle)')
|
|
305
|
+
ap.add_argument('--only', help='只处理这一个 case(精确匹配 caseId)')
|
|
306
|
+
ap.add_argument('--no-notify', action='store_true', help='不发通知(日志与审计照旧)')
|
|
307
|
+
ap.add_argument('--now', type=float, help='覆盖"当前时间"(epoch 秒;仅测试/排障)')
|
|
308
|
+
ap.add_argument('--json', action='store_true', help='机器可读输出(夹具/巡检用)')
|
|
309
|
+
a = ap.parse_args(argv)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
classify, canonical_reason, appointment_gate, last_failed_step = load_judges()
|
|
313
|
+
except RuntimeError as exc:
|
|
314
|
+
print(f'❌ {exc}', file=sys.stderr)
|
|
315
|
+
return 2
|
|
316
|
+
|
|
317
|
+
loaded = load_auto_retry_cfg(a.flow)
|
|
318
|
+
if loaded is None:
|
|
319
|
+
return 2
|
|
320
|
+
ar, canonical = loaded
|
|
321
|
+
cfg = {
|
|
322
|
+
'enabled': bool(ar.get('enabled')),
|
|
323
|
+
'max': int(a.max if a.max is not None else ar.get('max')),
|
|
324
|
+
'cooldownMin': int(a.cooldown_min if a.cooldown_min is not None else ar.get('cooldownMin')),
|
|
325
|
+
'perCycle': int(a.per_cycle if a.per_cycle is not None else ar.get('perCycle')),
|
|
326
|
+
}
|
|
327
|
+
if cfg['max'] < 1 or cfg['perCycle'] < 1 or cfg['cooldownMin'] < 0:
|
|
328
|
+
print(f'❌ 取值非法: {cfg}(max/perCycle ≥ 1、cooldownMin ≥ 0)', file=sys.stderr)
|
|
329
|
+
return 2
|
|
330
|
+
|
|
331
|
+
ledger = pathlib.Path(a.ledger)
|
|
332
|
+
audit_path = ledger.parent / 'flow-auto-retry-audit.jsonl'
|
|
333
|
+
now = float(a.now if a.now is not None else time.time())
|
|
334
|
+
dry = not a.yes
|
|
335
|
+
|
|
336
|
+
result = {'flow': a.flow, 'canonical': canonical, 'dryRun': bool(dry), 'enabled': cfg['enabled'],
|
|
337
|
+
'limits': {k: cfg[k] for k in ('max', 'cooldownMin', 'perCycle')},
|
|
338
|
+
'ledger': str(ledger), 'audit': str(audit_path),
|
|
339
|
+
'candidates': [], 'retried': [], 'notified': False}
|
|
340
|
+
|
|
341
|
+
if not cfg['enabled']:
|
|
342
|
+
print(f" 流程 {a.flow}:自动重试**已关闭**(flow_config autoRetry.enabled=false)⇒ 本轮不做")
|
|
343
|
+
if a.json:
|
|
344
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
print(f" 流程 {a.flow}(规范 id={canonical})|自动重试:开|每票上限 {cfg['max']}"
|
|
348
|
+
f"|冷却 {cfg['cooldownMin']} 分|每轮上限 {cfg['perCycle']}|台账 {ledger}")
|
|
349
|
+
if ledger.parent != DATA_DIR:
|
|
350
|
+
print(f" ⚠️ 台账父目录({ledger.parent}) ≠ 数据目录({DATA_DIR}):委托的 flow_retry 仍写数据目录那份")
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
events = read_events(ledger)
|
|
354
|
+
except OSError as exc:
|
|
355
|
+
print(f'❌ 台账读不到({exc})⇒ 本轮不做(⛔ 不把"读不到"当作"无候选")', file=sys.stderr)
|
|
356
|
+
return 2
|
|
357
|
+
|
|
358
|
+
prefixes = case_prefixes(a.flow, canonical)
|
|
359
|
+
by_case: dict = {}
|
|
360
|
+
for e in events:
|
|
361
|
+
cid = str(e.get('caseId') or '')
|
|
362
|
+
if any(cid.startswith(p) for p in prefixes):
|
|
363
|
+
by_case.setdefault(cid, []).append(e)
|
|
364
|
+
|
|
365
|
+
# ── 候选筛选(末条**状态性**事件 ∈ {failed, blocked})──────────────────────
|
|
366
|
+
cands = []
|
|
367
|
+
for cid, evs in by_case.items():
|
|
368
|
+
evs.sort(key=lambda e: (e.get('ts') or 0))
|
|
369
|
+
last, tail = effective_last(evs)
|
|
370
|
+
if last is None or str(last.get('event') or '') not in FAIL_EVENTS:
|
|
371
|
+
continue
|
|
372
|
+
if a.only and cid != a.only:
|
|
373
|
+
continue
|
|
374
|
+
cands.append((cid, last, tail))
|
|
375
|
+
# 先到先处理(最久没被处置的排在前面):与"每轮上限"配合时不会让某张票永远排不上
|
|
376
|
+
cands.sort(key=lambda x: float(x[1].get('ts') or 0))
|
|
377
|
+
n_tail = len([c for c in cands if c[2]])
|
|
378
|
+
if n_tail:
|
|
379
|
+
print(f" (其中 {n_tail} 张的末条是信息性事件,已按上一状态性事件判定)")
|
|
380
|
+
print(f" 候选 {len(cands)} 张(末条 failed/blocked)")
|
|
381
|
+
|
|
382
|
+
retried_rows = []
|
|
383
|
+
for cid, last, _tail in cands:
|
|
384
|
+
if len(retried_rows) >= cfg['perCycle']:
|
|
385
|
+
break
|
|
386
|
+
ts = float(last.get('ts') or 0)
|
|
387
|
+
text = plain_text(last, canonical_reason)
|
|
388
|
+
kind = classify(text)
|
|
389
|
+
record = cid.split('__', 1)[1] if '__' in cid else cid
|
|
390
|
+
gate = appointment_gate(text, last_failed_step(record))
|
|
391
|
+
ctx = {
|
|
392
|
+
'caseId': cid, 'cfg': cfg, 'kind': kind, 'gate': gate, 'text': text,
|
|
393
|
+
'retryCount': retry_count(events, cid),
|
|
394
|
+
'refusedCount': refused_count(audit_path, cid),
|
|
395
|
+
'cooldownLeftSec': max(0.0, cfg['cooldownMin'] * 60 - (now - ts)) if ts else 0.0,
|
|
396
|
+
}
|
|
397
|
+
skip = None
|
|
398
|
+
for check in CHECKS:
|
|
399
|
+
skip = check(ctx)
|
|
400
|
+
if skip:
|
|
401
|
+
break
|
|
402
|
+
tried = ctx['retryCount'] + ctx['refusedCount']
|
|
403
|
+
row = {'caseId': cid, 'attempt': tried + 1, 'retryCount': ctx['retryCount'],
|
|
404
|
+
'refusedCount': ctx['refusedCount'],
|
|
405
|
+
'kind': str(kind.get('kind') or ''), 'ts': int(ts),
|
|
406
|
+
'cooldownLeftSec': int(ctx['cooldownLeftSec']), 'gate': str(gate.get('why') or '')}
|
|
407
|
+
if skip:
|
|
408
|
+
row.update({'decision': 'skip', 'why': skip})
|
|
409
|
+
print(f" · {cid} ⏭ 跳过:{skip}")
|
|
410
|
+
result['candidates'].append(row)
|
|
411
|
+
continue
|
|
412
|
+
refused_note = f"(其中 {ctx['refusedCount']} 次被委托侧拒绝)" if ctx['refusedCount'] else ''
|
|
413
|
+
row.update({'decision': 'retry',
|
|
414
|
+
'why': f"分类=retryable|预约闸门放行({gate.get('why')})"
|
|
415
|
+
f"|已尝试 {tried}/{cfg['max']} 次{refused_note}|冷却已过"})
|
|
416
|
+
result['candidates'].append(row)
|
|
417
|
+
if dry:
|
|
418
|
+
print(f" · {cid} ▶ [dry-run] 将自动重试(第 {row['attempt']}/{cfg['max']} 次):{row['why']}")
|
|
419
|
+
continue
|
|
420
|
+
print(f" · {cid} ▶ 自动重试(第 {row['attempt']}/{cfg['max']} 次):{row['why']}")
|
|
421
|
+
got = do_retry(a.flow, ctx, cfg)
|
|
422
|
+
for line in (got['out'] or '').splitlines():
|
|
423
|
+
print(f' {line}')
|
|
424
|
+
row['exit'] = got['exit']
|
|
425
|
+
row['ok'] = got['ok']
|
|
426
|
+
retried_rows.append(row)
|
|
427
|
+
audit(audit_path, {
|
|
428
|
+
'at': datetime.datetime.now().isoformat(timespec='seconds'),
|
|
429
|
+
'flow': a.flow, 'canonical': canonical, 'caseId': cid, 'action': 'auto-retry',
|
|
430
|
+
'by': 'flow-auto-retry', 'attempt': row['attempt'], 'max': cfg['max'],
|
|
431
|
+
'classification': row['kind'], 'appointmentGate': row['gate'],
|
|
432
|
+
'reason': text[:200], 'delegated': 'flow_retry.py', 'exit': got['exit'], 'ok': got['ok'],
|
|
433
|
+
'auditFile': str(audit_path), 'ledger': str(ledger),
|
|
434
|
+
})
|
|
435
|
+
if not got['ok']:
|
|
436
|
+
print(f" ⚠️ 本次未成功(exit={got['exit']})⇒ 该票保持原状,等人工/下一轮(不重试本次)")
|
|
437
|
+
|
|
438
|
+
result['retried'] = retried_rows
|
|
439
|
+
ok_n = len([r for r in retried_rows if r.get('ok')])
|
|
440
|
+
if dry:
|
|
441
|
+
will = len([r for r in result['candidates'] if r['decision'] == 'retry'])
|
|
442
|
+
print(f" [dry-run] 未写入:将自动重试 {will} 票(上限 {cfg['perCycle']});加 --yes 才落库"
|
|
443
|
+
if will else f" [dry-run] 无票可自动重试(跳过 {len(result['candidates'])} 张,原因见上)")
|
|
444
|
+
else:
|
|
445
|
+
print(f" 完成:自动重试 {len(retried_rows)} 票(成功 {ok_n})|审计 {audit_path}")
|
|
446
|
+
if retried_rows and not a.no_notify:
|
|
447
|
+
result['notified'] = bool(notify_card(a.flow, retried_rows, cfg))
|
|
448
|
+
if a.json:
|
|
449
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
450
|
+
return 0
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if __name__ == '__main__':
|
|
454
|
+
sys.exit(main())
|