@whyshu/argent-engine 0.0.0-stage → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.json +307 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +377 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +134 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +46 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +260 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1024 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2275 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +194 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent_datadir.py +262 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/run-tests.sh +256 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +136 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +73 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +34 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +706 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""飞书 Base 数据面实现(P4,2026-09-29):从 `flow_source.py` 原样搬出。
|
|
2
|
+
|
|
3
|
+
接口:`KIND` + `pull(spec, args) -> [(record_id, fields), …]`。
|
|
4
|
+
窄投影收窄把候选清空时,把说明写进 `common.LAST_NARROW` / `common.LAST_PICK_NOTES`
|
|
5
|
+
(引擎的 JSON 输出与日志读它们 —— 迁移期两个模块引用的是**同一批对象**)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json, os, sys
|
|
10
|
+
|
|
11
|
+
from flow_sources.common import (
|
|
12
|
+
LAST_NARROW,
|
|
13
|
+
LAST_PICK_NOTES,
|
|
14
|
+
_flat,
|
|
15
|
+
apply_pick,
|
|
16
|
+
required_projection,
|
|
17
|
+
run,
|
|
18
|
+
ticket_key,
|
|
19
|
+
wanted_pick_columns,
|
|
20
|
+
wanted_upstream_columns,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
KIND = 'feishu-base'
|
|
24
|
+
|
|
25
|
+
def _field_map(profile: str, base: str, table: str) -> dict:
|
|
26
|
+
"""取 字段名 ↔ id 双向映射(实测形状:data.fields[{id,name,type}])。"""
|
|
27
|
+
code, out, err = run(['lark-cli', '--profile', profile, 'base', '+field-list',
|
|
28
|
+
'--base-token', base, '--table-id', table, '--as', 'bot', '--format', 'json'])
|
|
29
|
+
if code != 0:
|
|
30
|
+
print(f'字段清单读取失败(exit {code}):{(err or out).strip()[:300]}', file=sys.stderr)
|
|
31
|
+
sys.exit(2)
|
|
32
|
+
try:
|
|
33
|
+
fields = (json.loads(out).get('data') or {}).get('fields') or []
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f'字段清单不是 JSON({e})', file=sys.stderr)
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
by_id = {f.get('id'): f.get('name') for f in fields if f.get('id')}
|
|
38
|
+
by_name = {f.get('name'): f.get('id') for f in fields if f.get('name')}
|
|
39
|
+
return {'by_id': by_id, 'by_name': by_name, 'count': len(fields)}
|
|
40
|
+
|
|
41
|
+
def _resolved_source(flow_id: str, spec: dict) -> dict:
|
|
42
|
+
"""把流程声明的源解析成**当前生效的一份**:变体选择 + `envFile`/`baseEnv`/`tableEnv`/`*Ref`。
|
|
43
|
+
|
|
44
|
+
复用 `flow_config`(唯一实现)—— 引擎里绝不再抄第二套解析逻辑。否则像 `full` 这种
|
|
45
|
+
"两套变体 + 表 id 来自 fulfillment.env"的流程在别处就会读不了表
|
|
46
|
+
(2026-09-17 实测:直接读 spec['source'] ⇒ 报"缺少 base/table/identity")。
|
|
47
|
+
"""
|
|
48
|
+
raw = dict((spec or {}).get('source') or {})
|
|
49
|
+
if flow_id:
|
|
50
|
+
try:
|
|
51
|
+
from flow_config import load_config
|
|
52
|
+
return {**raw, **dict((load_config(flow_id, strict=False).get('source') or {}))}
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
# 兜底(flow_config 不可用时):自己按声明里的 env 名解析
|
|
56
|
+
if raw.get('envFile'):
|
|
57
|
+
load_env_file(str(raw['envFile']))
|
|
58
|
+
out = dict(raw)
|
|
59
|
+
for key in ('base', 'table'):
|
|
60
|
+
if not out.get(key) and raw.get(f'{key}Env'):
|
|
61
|
+
out[key] = os.environ.get(str(raw[f'{key}Env']), '')
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
def pull(spec: dict, args) -> list[tuple[str, dict]]:
|
|
65
|
+
"""只读拉取飞书表 → [(ticketKey, fields)]。
|
|
66
|
+
|
|
67
|
+
实测要点(2026-09-15,lip 表):
|
|
68
|
+
· `+field-list` 形状 data.fields[{id,name}];
|
|
69
|
+
· `+record-list --format json --field-id …` 返回**位置数组** data.data=[[值,…],…],
|
|
70
|
+
**不带表头名** ⇒ 必须按传入 --field-id 的**顺序**映射回字段名;
|
|
71
|
+
· 不支持 --record-id/--page-size ⇒ 单条过滤与条数上限在客户端做。
|
|
72
|
+
"""
|
|
73
|
+
# 源解析走 flow_config(变体/envFile/env 名/引用 全都在那里处理,这里不再抄一套)
|
|
74
|
+
src = _resolved_source(str(getattr(args, 'flow', '') or ''), spec)
|
|
75
|
+
base = args.base or src.get('base')
|
|
76
|
+
table = args.table or src.get('table')
|
|
77
|
+
profile = args.profile or src.get('identity')
|
|
78
|
+
if not (base and table and profile):
|
|
79
|
+
miss = [src[k] for k in src if str(k).endswith('Unresolved')]
|
|
80
|
+
tip = f"(未解析:{'、'.join(str(x) for x in miss)})" if miss else ''
|
|
81
|
+
print(f'缺少 base/table/identity:请在流程声明里配置,或用 --base/--table/--profile 传入{tip}',
|
|
82
|
+
file=sys.stderr)
|
|
83
|
+
sys.exit(2)
|
|
84
|
+
|
|
85
|
+
fmap = _field_map(profile, base, table)
|
|
86
|
+
|
|
87
|
+
# 选投影列:显式 --field-ids(id 或名)优先;否则用流程必需字段里的**非系统字段**前 6 个
|
|
88
|
+
pick, seen = [], set()
|
|
89
|
+
if args.field_ids:
|
|
90
|
+
for raw in args.field_ids.split(','):
|
|
91
|
+
t = raw.strip()
|
|
92
|
+
name = fmap['by_id'].get(t) or t
|
|
93
|
+
fid = t if t in fmap['by_id'] else fmap['by_name'].get(t)
|
|
94
|
+
if not fid or fid in seen:
|
|
95
|
+
continue
|
|
96
|
+
seen.add(fid); pick.append((name, fid))
|
|
97
|
+
else:
|
|
98
|
+
unknown: list[str] = []
|
|
99
|
+
# 用**当前变体**的字段要求(flow_config 已合并;见 load_config 的 requiredSource)
|
|
100
|
+
# 判据与兜底逻辑统一在 required_projection()(可单测;见 scripts/tests/flow-required-role.test.mjs)
|
|
101
|
+
for name in required_projection(spec):
|
|
102
|
+
fid = fmap['by_name'].get(name)
|
|
103
|
+
if not fid:
|
|
104
|
+
# 2026-09-17 实测(FULL):required 里写了表里不存在的列名(店铺/装箱数,真名是
|
|
105
|
+
# 店铺名称/箱数)⇒ 旧实现**静默跳过**,投影少列,最终"未取到任何行",
|
|
106
|
+
# 排查成本极高。这里改为**大声失败**并列出可用列名。
|
|
107
|
+
unknown.append(str(name))
|
|
108
|
+
continue
|
|
109
|
+
if fid not in seen: # 去重(required 里可能有多条指向同一列)
|
|
110
|
+
seen.add(fid); pick.append((name, fid))
|
|
111
|
+
# 2026-09-15 修正(文鳐实跑发现):投影必须**覆盖 required 的全部非系统字段**,
|
|
112
|
+
# 否则缺口判断永远显示缺一堆。JSON 返回是**位置数组**(非 markdown),不存在
|
|
113
|
+
# AGENTS.md 说的宽投影错位问题 ⇒ 按需取全,但仍不取无关列。
|
|
114
|
+
if len(pick) >= 24: # 上限保护
|
|
115
|
+
break
|
|
116
|
+
if unknown:
|
|
117
|
+
avail = '、'.join(sorted(fmap['by_name'])[:30])
|
|
118
|
+
print(f"❌ 流程声明的必需字段在表里不存在:{'、'.join(unknown)}\n"
|
|
119
|
+
f" 表 {table}({profile})可用列(前 30):{avail}\n"
|
|
120
|
+
f" ⇒ 列名不对会让投影失败并**静默返回 0 行**,请修正 flow-requirements.json 的 required",
|
|
121
|
+
file=sys.stderr)
|
|
122
|
+
sys.exit(2)
|
|
123
|
+
# 2026-09-15:投影里**必须含路由列**(routeBy),否则无法按货代过滤候选行
|
|
124
|
+
rt = spec.get('routing') or {}
|
|
125
|
+
route_by = rt.get('routeBy')
|
|
126
|
+
if route_by and route_by in fmap['by_name'] and fmap['by_name'][route_by] not in seen:
|
|
127
|
+
seen.add(fmap['by_name'][route_by]); pick.append((route_by, fmap['by_name'][route_by]))
|
|
128
|
+
# 2026-09-17(FULL 实测):投影里**必须含票号列**,否则位置数组里没有票号 ⇒ 每行票号为空
|
|
129
|
+
# ⇒ 全被跳过 ⇒ "未取到任何行"(静默)。与上面"必须含路由列"是同一类坑。
|
|
130
|
+
tf_name = args.ticket_field or src.get('ticketField') or '货件号'
|
|
131
|
+
if tf_name in fmap['by_name'] and fmap['by_name'][tf_name] not in seen:
|
|
132
|
+
seen.add(fmap['by_name'][tf_name]); pick.append((tf_name, fmap['by_name'][tf_name]))
|
|
133
|
+
# 2026-09-17(FULL 切到 lip 表时实测):投影里**必须含 `pick` 用到的列**。
|
|
134
|
+
# 事故形态:lip 变体的 `pick.status=FULL状态`、`pick.ready=就绪`,这两列既不在 required 里、
|
|
135
|
+
# 也不是路由/票号列 ⇒ 没进投影 ⇒ 过滤时读到空值 ⇒ **候选静默收窄到 0 行**
|
|
136
|
+
# (日志只说"候选收窄 100 → 0 行",看起来像"今天没活干",实际 FULL状态=待执行 有 96 行)。
|
|
137
|
+
# 判据:`source.fields.*`(领域名 → 真列名)里被 pick 引用的那几个领域。
|
|
138
|
+
src_fields = src.get('fields') or {}
|
|
139
|
+
pick_rules = src.get('pick') or {}
|
|
140
|
+
for nm in wanted_pick_columns(src):
|
|
141
|
+
fid = fmap['by_name'].get(nm)
|
|
142
|
+
if fid and fid not in seen:
|
|
143
|
+
seen.add(fid); pick.append((nm, fid))
|
|
144
|
+
# 2026-10-13:**上游变化观察列**(`pick.releaseReDispatch.upstreamHash.watch`)也进投影 ——
|
|
145
|
+
# 「上游数据变了 ⇒ 放行额度重置」的判据只看得见投影到的列,而 case 里没有的列**永远
|
|
146
|
+
# 不会被算作"变"**(实测口径:发票流程的 `产品图` 就是这种列:运营补了它、流程也依赖它)。
|
|
147
|
+
# 与 pick 列的区别:表里没有这一列 ⇒ **静默跳过**(观察列可选;pick 列缺失才是硬故障)。
|
|
148
|
+
for nm in wanted_upstream_columns(src):
|
|
149
|
+
fid = fmap['by_name'].get(nm)
|
|
150
|
+
if fid and fid not in seen:
|
|
151
|
+
seen.add(fid); pick.append((nm, fid))
|
|
152
|
+
if not pick:
|
|
153
|
+
print('无法确定投影列:请传 --field-ids,或在流程 required 里声明字段名', file=sys.stderr)
|
|
154
|
+
sys.exit(2)
|
|
155
|
+
|
|
156
|
+
cmd = ['lark-cli', '--profile', profile, 'base', '+record-list',
|
|
157
|
+
'--base-token', base, '--table-id', table, '--as', 'bot', '--format', 'json']
|
|
158
|
+
for _, fid in pick:
|
|
159
|
+
cmd += ['--field-id', fid]
|
|
160
|
+
# 2026-09-15 修正 B(文鳐实跑发现):`+record-list` 默认只返回 100 行 ⇒ 必须翻页,
|
|
161
|
+
# 否则 100 行以外的票看不见。
|
|
162
|
+
page_size = 100
|
|
163
|
+
data, rows_all, ids_all, offset, guard = {}, [], [], 0, 0
|
|
164
|
+
while True:
|
|
165
|
+
guard += 1
|
|
166
|
+
if guard > 50: # 保险丝:最多 5000 行
|
|
167
|
+
break
|
|
168
|
+
page = cmd + ['--limit', str(page_size)]
|
|
169
|
+
if offset:
|
|
170
|
+
page += ['--offset', str(offset)]
|
|
171
|
+
code, out, err = run(page)
|
|
172
|
+
if code != 0:
|
|
173
|
+
print(f'飞书表读取失败(exit {code}):{(err or out).strip()[:300]}', file=sys.stderr)
|
|
174
|
+
sys.exit(2)
|
|
175
|
+
try:
|
|
176
|
+
data = (json.loads(out).get('data') or {})
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f'飞书返回不是 JSON({e})', file=sys.stderr)
|
|
179
|
+
sys.exit(2)
|
|
180
|
+
got = data.get('data') or data.get('records') or data.get('items') or []
|
|
181
|
+
if not got:
|
|
182
|
+
break
|
|
183
|
+
rows_all.extend(got if isinstance(got, list) else [])
|
|
184
|
+
# 2026-09-17(FULL 实测):位置数组形态**没有 record_id 字段**,但响应里有
|
|
185
|
+
# `data.record_id_list`(与 data.data 同序)⇒ 必须一起累积,否则拿不到"单元 id",
|
|
186
|
+
# 而 FULL 的单元就是 record_id(货件号此时是空的、是流程产物)。
|
|
187
|
+
ids_all.extend(data.get('record_id_list') or [])
|
|
188
|
+
if len(got) < page_size:
|
|
189
|
+
break
|
|
190
|
+
offset += len(got)
|
|
191
|
+
if args.limit and len(rows_all) >= args.limit:
|
|
192
|
+
break
|
|
193
|
+
if args.record: # 单条查询无需翻页
|
|
194
|
+
break
|
|
195
|
+
data = {'data': rows_all} if isinstance(rows_all, list) and rows_all and isinstance(rows_all[0], list) else {'records': rows_all}
|
|
196
|
+
|
|
197
|
+
rows = []
|
|
198
|
+
positional = data.get('data')
|
|
199
|
+
if isinstance(positional, list): # 位置数组形态
|
|
200
|
+
for idx, arr in enumerate(positional):
|
|
201
|
+
fields = {pick[i][0]: _flat(v) for i, v in enumerate(arr) if i < len(pick)}
|
|
202
|
+
if idx < len(ids_all): # 补单元 id(见上:位置数组不含 record_id)
|
|
203
|
+
fields['_record_id'] = ids_all[idx]
|
|
204
|
+
rows.append(fields)
|
|
205
|
+
else: # 对象形态(含 record_id)
|
|
206
|
+
for r in (data.get('records') or data.get('items') or []):
|
|
207
|
+
rec = r.get('record_id') or r.get('recordId') or ''
|
|
208
|
+
fields = {k: _flat(v) for k, v in (r.get('fields') or {}).items() if k != '_record_id'}
|
|
209
|
+
fields['_record_id'] = rec
|
|
210
|
+
rows.append(fields)
|
|
211
|
+
|
|
212
|
+
ticket_field = args.ticket_field or '货件号'
|
|
213
|
+
# 2026-09-15:**按 routing 收窄候选行** —— 实测教训:76135791/76163607 的「货代公司」
|
|
214
|
+
# 不是「上海文鳐」,被 wenyao_run 的 P1 守卫正确拒绝;此前 triage 却把它们列为"可派发"。
|
|
215
|
+
if (not getattr(args, 'no_route_filter', False)) and route_by and rt.get('match'):
|
|
216
|
+
before = len(rows)
|
|
217
|
+
rows = [r for r in rows if str(r.get(route_by) or '').strip() == str(rt['match']).strip()]
|
|
218
|
+
if before != len(rows):
|
|
219
|
+
print(f' · 路由过滤({route_by}=={rt["match"]}):{before} → {len(rows)} 行', file=sys.stderr)
|
|
220
|
+
|
|
221
|
+
# ── 候选收窄(pick):把"流程声明里写的该动哪些行"真正落到候选集上 ─────────────
|
|
222
|
+
# 2026-09-17(FULL 实测):注册表早就声明了 pick(legacy: 状态∈{Pending,运行中} + 就绪;
|
|
223
|
+
# lip: FULL状态=待执行 + 就绪 + 货件号空),但引擎没应用 ⇒ 180 行**全部**被当成"可派发"
|
|
224
|
+
# (含已完成的行)。不应用 pick 就等于把整张表当待办。
|
|
225
|
+
pick = src.get('pick') or {}
|
|
226
|
+
rows, phases = apply_pick(rows, src, no_pick_filter=getattr(args, 'no_pick_filter', False))
|
|
227
|
+
# "已排除 N 张(人工/既往已完成)"要进 JSON 的 notes:分诊把它显示在摘要里、认领把它带进
|
|
228
|
+
# 触发器日志的 reason ⇒ 运维一眼能看出"没票"不等于"没数据",而是历史数据被正确分辨掉了。
|
|
229
|
+
pick_notes = []
|
|
230
|
+
if phases.get('droppedDone'):
|
|
231
|
+
pick_notes.append(f'已排除 {phases["droppedDone"]} 张已完成(人工/既往,判据见 pick.exclude*)')
|
|
232
|
+
LAST_PICK_NOTES.clear()
|
|
233
|
+
LAST_PICK_NOTES.extend(pick_notes)
|
|
234
|
+
if pick and not getattr(args, 'no_pick_filter', False):
|
|
235
|
+
if phases.get('before') and not rows:
|
|
236
|
+
LAST_NARROW['emptied'] = True # 收窄到 0 = 合法结论(当前没有该动的行)
|
|
237
|
+
LAST_NARROW['info'] = {k: v for k, v in phases.items() if k != 'rule'}
|
|
238
|
+
if phases.get('before') != len(rows):
|
|
239
|
+
print(f" · 候选收窄({phases['rule']}):{phases['before']} → {len(rows)} 行"
|
|
240
|
+
+ (f"({phases['detail']})" if phases.get('detail') else ''), file=sys.stderr)
|
|
241
|
+
del phases['rule'], phases['before'], phases['detail']
|
|
242
|
+
|
|
243
|
+
out_rows = []
|
|
244
|
+
skipped_no_ticket = 0
|
|
245
|
+
# 2026-09-15:位置数组形态**没有 record_id** ⇒ `--record` 无法过滤;
|
|
246
|
+
# 此时要求用 `--ticket`(按票号过滤),避免"以为只取一条、实际拉全表"。
|
|
247
|
+
if args.record and rows and not any('_record_id' in r for r in rows):
|
|
248
|
+
if not args.ticket:
|
|
249
|
+
print('该表返回位置数组(无 record_id)⇒ --record 无法过滤;请改用 --ticket <货件号>', file=sys.stderr)
|
|
250
|
+
sys.exit(2)
|
|
251
|
+
for fields in rows:
|
|
252
|
+
rec = fields.get('_record_id') or ''
|
|
253
|
+
if args.record and rec and rec != args.record:
|
|
254
|
+
continue
|
|
255
|
+
if args.ticket and str(fields.get(ticket_field) or '') != str(args.ticket):
|
|
256
|
+
continue
|
|
257
|
+
# 身份走 ticket_key(),**模式逐流程来自注册表 source.ticketKey**(缺省=历史行为「票号列优先」)。
|
|
258
|
+
# 见 ticket_key() 注释:FULL 必须 record_id(货件号中途才有);发票流程必须货件号(行天然已有)。
|
|
259
|
+
ticket = ticket_key(rec, fields, ticket_field, src.get('ticketKey'))
|
|
260
|
+
if not ticket:
|
|
261
|
+
skipped_no_ticket += 1
|
|
262
|
+
continue
|
|
263
|
+
fields.pop('_record_id', None)
|
|
264
|
+
out_rows.append((ticket, fields))
|
|
265
|
+
if args.limit and len(out_rows) >= args.limit:
|
|
266
|
+
break
|
|
267
|
+
if not out_rows and skipped_no_ticket:
|
|
268
|
+
print(f"❌ 取到 {skipped_no_ticket} 行但**都无法确定票号**(票号列「{tf_name}」为空、且位置数组无 record_id)\n"
|
|
269
|
+
f" ⇒ 通常是:① 票号列没进投影(已自动补,若仍失败请检查列名)② 该流程的单元不是票号列\n"
|
|
270
|
+
f" ⇒ 对策:把票号列加进 required,或声明 source.ticketField 指向真正的单元列",
|
|
271
|
+
file=sys.stderr)
|
|
272
|
+
sys.exit(2)
|
|
273
|
+
return out_rows
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""flow_stale —— **停滞票自动回收**(认领租约到期、从未被派发 ⇒ 重排回候选)。
|
|
3
|
+
|
|
4
|
+
## 为什么要有它(2026-09-23 实测)
|
|
5
|
+
triage 把"最近事件是认领/派发但已超窗口"的 case 归为 `stale`,理由写的是
|
|
6
|
+
**"认领租约到期未派发(可回收重派)"** —— 但**没有任何代码回收它**:
|
|
7
|
+
· `flow_claim.py` 只从 `plan.to_dispatch` 取票(停滞票不在其中);
|
|
8
|
+
· `flow_retry.py` 没有回收选项;`flow-trigger.sh` 也没有相关动作。
|
|
9
|
+
⇒ 环境故障(那天是紫鸟未登录)→ 角色 blocked → 租约过期变停滞 → **票静静躺着**,
|
|
10
|
+
还占着同店铺的批次("可派发 6 却连续 4 轮无票可认领"的直接原因)。
|
|
11
|
+
|
|
12
|
+
## 语义(保守,可配)
|
|
13
|
+
· 只回收 `ageMin >= staleAfterMin`(默认 60 分钟)的停滞票
|
|
14
|
+
· 每轮最多 `maxPerRound` 张(默认 3),避免风暴
|
|
15
|
+
· `requireEnv=true` 时先跑环境预检(env_probe):环境不可用 ⇒ **不回收**(否则回收了也白跑)
|
|
16
|
+
· 回收动作 = 写一条 `requeued` 台账事件(triage 认它 ⇒ 回到 to_dispatch)
|
|
17
|
+
· 幂等:同一 case 在 `cooldownMin` 内不重复回收(避免每 5 分钟刷一轮)
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
python3 scripts/flow_stale.py --flow full # dry-run:只看会回收什么
|
|
21
|
+
python3 scripts/flow_stale.py --flow full --requeue # 真写(trigger 用这个)
|
|
22
|
+
退出码:0 正常(含无事可做);2 配置/读取失败
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
import argparse, json, pathlib, subprocess, sys, time
|
|
26
|
+
|
|
27
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
28
|
+
sys.path.insert(0, str(HERE))
|
|
29
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
30
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
31
|
+
PY = sys.executable
|
|
32
|
+
DEFAULTS = {'enabled': True, 'staleAfterMin': 60, 'maxPerRound': 3, 'requireEnv': True, 'cooldownMin': 30}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cfg(flow: str, key: str):
|
|
36
|
+
p = subprocess.run([PY, str(HERE / 'flow_config.py'), '--flow', flow, '--get', key],
|
|
37
|
+
capture_output=True, text=True)
|
|
38
|
+
try:
|
|
39
|
+
return json.loads(p.stdout)
|
|
40
|
+
except Exception: # noqa: BLE001
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def spec_of(flow: str) -> dict:
|
|
45
|
+
raw = cfg(flow, 'staleReclaim') or {}
|
|
46
|
+
if isinstance(raw, dict) and raw.get('enabled') is False:
|
|
47
|
+
return {'enabled': False}
|
|
48
|
+
out = {**DEFAULTS, **(raw if isinstance(raw, dict) else {})}
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def last_event_ts(ledger: pathlib.Path, case_id: str) -> float:
|
|
53
|
+
"""该 case 最后一次事件的 ts(用于补算停滞时长)。"""
|
|
54
|
+
ts = 0.0
|
|
55
|
+
try:
|
|
56
|
+
for line in ledger.read_text(encoding='utf-8').splitlines():
|
|
57
|
+
if not line.strip() or case_id not in line:
|
|
58
|
+
continue
|
|
59
|
+
e = json.loads(line)
|
|
60
|
+
if e.get('caseId') == case_id:
|
|
61
|
+
ts = max(ts, float(e.get('ts') or 0))
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
return ts
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def age_minutes(case: dict, ledger: pathlib.Path, now: float) -> float:
|
|
68
|
+
"""停滞时长(分钟)。
|
|
69
|
+
|
|
70
|
+
⚠️ 不能只看 `case['ageMin']`:triage 对"认领了但从未派发(租约到期)"这类停滞
|
|
71
|
+
**不带 ageMin**(只给 reason)—— 2026-09-23 实测:只读 ageMin 会把它们当 0 岁,
|
|
72
|
+
于是阈值判断永远不过、**回收形同虚设**。缺失时用台账里最后一次事件的时间补算。
|
|
73
|
+
"""
|
|
74
|
+
raw = case.get('ageMin')
|
|
75
|
+
if raw is not None:
|
|
76
|
+
try:
|
|
77
|
+
return float(raw)
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
pass
|
|
80
|
+
ts = last_event_ts(ledger, str(case.get('caseId') or ''))
|
|
81
|
+
return (now - ts) / 60 if ts else 1e9 # 台账里也查不到 ⇒ 视为很久(该回收)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def last_requeue_ts(ledger: pathlib.Path, case_id: str) -> float:
|
|
85
|
+
ts = 0.0
|
|
86
|
+
try:
|
|
87
|
+
for line in ledger.read_text(encoding='utf-8').splitlines():
|
|
88
|
+
if not line.strip() or case_id not in line:
|
|
89
|
+
continue
|
|
90
|
+
e = json.loads(line)
|
|
91
|
+
if e.get('caseId') == case_id and e.get('event') == 'requeued':
|
|
92
|
+
ts = max(ts, float(e.get('ts') or 0))
|
|
93
|
+
except OSError:
|
|
94
|
+
pass
|
|
95
|
+
return ts
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def pick_stale(plan: dict, spec: dict, ledger: pathlib.Path, now: float = None) -> tuple:
|
|
99
|
+
"""→ (要回收的 case 列表, 说明)。纯函数(便于回归),不写任何东西。"""
|
|
100
|
+
now = now or time.time()
|
|
101
|
+
stale = list(((plan.get('plan') or {}).get('stale')) or [])
|
|
102
|
+
why = []
|
|
103
|
+
if not stale:
|
|
104
|
+
return [], '无停滞票'
|
|
105
|
+
aged = [c for c in stale if age_minutes(c, ledger, now) >= float(spec['staleAfterMin'])]
|
|
106
|
+
why.append(f"停滞 {len(stale)} 张 → 超 {spec['staleAfterMin']} 分钟 {len(aged)} 张")
|
|
107
|
+
cool = float(spec.get('cooldownMin') or 0) * 60
|
|
108
|
+
fresh, held = [], 0
|
|
109
|
+
for c in aged:
|
|
110
|
+
ts = last_requeue_ts(ledger, str(c.get('caseId') or ''))
|
|
111
|
+
if cool and ts and (now - ts) < cool:
|
|
112
|
+
held += 1
|
|
113
|
+
continue
|
|
114
|
+
fresh.append(c)
|
|
115
|
+
if held:
|
|
116
|
+
why.append(f"{held} 张在冷却期内({spec['cooldownMin']} 分钟)")
|
|
117
|
+
cap = int(spec.get('maxPerRound') or 3)
|
|
118
|
+
picked = fresh[:cap]
|
|
119
|
+
if len(fresh) > cap:
|
|
120
|
+
why.append(f"本轮上限 {cap} 张,其余 {len(fresh) - cap} 张下一轮")
|
|
121
|
+
return picked, ';'.join(why) or '无可回收'
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def env_ok(flow: str) -> tuple:
|
|
125
|
+
p = subprocess.run([PY, str(HERE / 'env_probe.py'), '--flow', flow, '--json'],
|
|
126
|
+
capture_output=True, text=True, timeout=90)
|
|
127
|
+
try:
|
|
128
|
+
d = json.loads(p.stdout or '{}')
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
d = {}
|
|
131
|
+
if p.returncode == 3: # 流程未声明探针 ⇒ 不拦
|
|
132
|
+
return True, '未声明环境探针'
|
|
133
|
+
return p.returncode == 0, d.get('detail') or (p.stderr or '')[:120]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main(argv=None) -> int:
|
|
137
|
+
ap = argparse.ArgumentParser(description='停滞票自动回收')
|
|
138
|
+
ap.add_argument('--flow', required=True)
|
|
139
|
+
ap.add_argument('--requeue', action='store_true', help='真写 requeued 事件(默认 dry-run)')
|
|
140
|
+
ap.add_argument('--json', action='store_true')
|
|
141
|
+
a = ap.parse_args(argv)
|
|
142
|
+
|
|
143
|
+
spec = spec_of(a.flow)
|
|
144
|
+
if not spec.get('enabled'):
|
|
145
|
+
print(f" · 流程 {a.flow} 未启用停滞回收(staleReclaim.enabled=false)")
|
|
146
|
+
return 0
|
|
147
|
+
ledger = pathlib.Path(cfg(a.flow, 'paths.ledger')
|
|
148
|
+
or (pathlib.Path(resolve_data_dir()['dir']) / 'flow-ledger.jsonl'))
|
|
149
|
+
p = subprocess.run([PY, str(HERE / 'flow_triage.py'), '--flow', a.flow, '--json'],
|
|
150
|
+
capture_output=True, text=True, timeout=600)
|
|
151
|
+
if not (p.stdout or '').strip().startswith('{'):
|
|
152
|
+
print(f"❌ 取分诊失败:{(p.stderr or p.stdout).strip()[:200]}", file=sys.stderr)
|
|
153
|
+
return 2
|
|
154
|
+
plan = json.loads(p.stdout)
|
|
155
|
+
picked, why = pick_stale(plan, spec, ledger)
|
|
156
|
+
if not picked:
|
|
157
|
+
print(f" · 停滞回收:无需动作({why})")
|
|
158
|
+
return 0
|
|
159
|
+
if spec.get('requireEnv'):
|
|
160
|
+
ok, detail = env_ok(a.flow)
|
|
161
|
+
if not ok:
|
|
162
|
+
print(f" ⏸ 停滞回收跳过:环境不可用({detail})——回收了也会再 blocked")
|
|
163
|
+
return 0
|
|
164
|
+
ids = [str(c.get('caseId')) for c in picked]
|
|
165
|
+
if not a.requeue:
|
|
166
|
+
print(f" [dry-run] 将回收 {len(ids)} 张:{', '.join(ids)}({why})")
|
|
167
|
+
return 0
|
|
168
|
+
done = []
|
|
169
|
+
for cid in ids:
|
|
170
|
+
r = subprocess.run([PY, str(HERE / 'flow_ledger.py'), '--case-id', cid, '--event', 'requeued',
|
|
171
|
+
'--by', 'flow-stale', '--detail', f"停滞回收:{why}"],
|
|
172
|
+
capture_output=True, text=True)
|
|
173
|
+
if r.returncode == 0:
|
|
174
|
+
done.append(cid)
|
|
175
|
+
else:
|
|
176
|
+
print(f" ⚠️ {cid} 回收失败:{(r.stderr or r.stdout).strip()[:120]}", file=sys.stderr)
|
|
177
|
+
if a.json:
|
|
178
|
+
print(json.dumps({'flow': a.flow, 'requeued': done, 'reason': why}, ensure_ascii=False))
|
|
179
|
+
else:
|
|
180
|
+
print(f" ✅ 停滞回收 {len(done)} 张:{', '.join(done)}({why})")
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
raise SystemExit(main())
|