@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,106 @@
|
|
|
1
|
+
"""cases-json 数据面(P4,2026-09-29):从 `flow_source.py` **逻辑搬家**(行为不变)。
|
|
2
|
+
|
|
3
|
+
契约(见 `flow_sources.validate_module`):`KIND` + `pull_cases(spec, args) -> (exit_code | None, rows)`。
|
|
4
|
+
· `exit_code is None` ⇒ 拿到了 `rows`(`(票据键, 字段)` 列表,可能为空),**引擎**继续建案 / 缺口检查 / 输出;
|
|
5
|
+
· `exit_code` 为整数 ⇒ 该数据面自己已给结论并**打印过原因**(再生成失败 / 清单过期 / 读失败 / 空清单)。
|
|
6
|
+
|
|
7
|
+
⚠️ 这里的"再生成 + 过期守卫"是 **2026-09-18 / 09-20 两次日报静默停摆事故**的修复,
|
|
8
|
+
注释一字未改 —— 它们解释了"为什么读流程声明的清单就必须查过期、不能因为传了 --from-json 就绕过"。
|
|
9
|
+
|
|
10
|
+
⚠️ 形态说明:它和 feishu-base 一样产出 **rows**(所以 `pull_cases` 与 `pull` 只差"来源"),
|
|
11
|
+
但语义上属于"上游已产出待处理单元",故用 cases 命名以区别于"外部数据源现读"。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from flow_sources.common import read_cases_batch, read_cases_notes
|
|
23
|
+
|
|
24
|
+
# 引擎脚本目录(本模块在 scripts/flow_sources/ 下)——搬家前这里用的是 flow_source.HERE
|
|
25
|
+
SCRIPTS = pathlib.Path(__file__).resolve().parent.parent
|
|
26
|
+
sys.path.insert(0, str(SCRIPTS / 'lib'))
|
|
27
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
28
|
+
|
|
29
|
+
KIND = 'cases-json'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pull_cases(spec: dict, args):
|
|
33
|
+
"""见模块 docstring 的契约。"""
|
|
34
|
+
src = spec.get('source') or {}
|
|
35
|
+
batch = args.from_json or str(src.get('file') or '')
|
|
36
|
+
if batch and batch != '-':
|
|
37
|
+
batch = os.path.expanduser(batch)
|
|
38
|
+
if not os.path.isabs(batch):
|
|
39
|
+
batch = str(pathlib.Path(resolve_data_dir()['dir']) / batch)
|
|
40
|
+
# ⚠️ 2026-09-20 二次故障(同一事故复发):上面的"再生成 + 过期守卫"曾被
|
|
41
|
+
# `--from-json` **整体绕过** —— 而 flow_triage 生产路径恰恰总是传 `--from-json`,
|
|
42
|
+
# 于是修复只在"直接调 flow_source"时生效,真实链路里清单**冻结 43 小时**没人发现:
|
|
43
|
+
# 日报 09-19 14:05(站点本地 00:05)该推的那天静默没推,且因为清单里还写着
|
|
44
|
+
# "09-17 已推送",每轮都显示"无票可认领(已推送)"——看起来一切正常。
|
|
45
|
+
# 定则改为:**读的是流程声明的那个文件 ⇒ 该再生成就再生成、该查过期就查过期**;
|
|
46
|
+
# 只有读"别处的临时清单"(测试夹具/排障)才跳过 —— 那才是 --from-json 的本意。
|
|
47
|
+
def _resolved(p: str) -> str:
|
|
48
|
+
pp = os.path.expanduser(str(p))
|
|
49
|
+
if not os.path.isabs(pp):
|
|
50
|
+
pp = str(pathlib.Path(resolve_data_dir()['dir']) / pp)
|
|
51
|
+
return os.path.abspath(pp)
|
|
52
|
+
|
|
53
|
+
declared = str(src.get('file') or '')
|
|
54
|
+
reading_declared = bool(declared) and batch != '-' and _resolved(batch) == _resolved(declared)
|
|
55
|
+
|
|
56
|
+
# ── 源的"再生成"(2026-09-18 真实故障:日报静默停摆一整天)────────────────
|
|
57
|
+
# 日报的待办单元不是静态文件,而是「店铺 × 站点本地日期 × 幂等标记」的差集,
|
|
58
|
+
# 由 daily-report-inbox.py 算出。此前**没有任何调用方**(不在触发器、不在插件、
|
|
59
|
+
# cron 已清空)⇒ 清单冻结在 09-17 10:57 的旧内容,其唯一单元早已被影子轮消费
|
|
60
|
+
# (台账 done)⇒ 之后每轮都是"无票可认领"(看起来一切正常,实际当天永不推送)。
|
|
61
|
+
# 约定:流程在 `source.regenerate` 里声明生成器,源适配器读之前先跑它;
|
|
62
|
+
# 生成失败**响亮停住**(exit 2),绝不退化成"0 票"。
|
|
63
|
+
regen = src.get('regenerate') or {}
|
|
64
|
+
if regen and reading_declared and not getattr(args, 'no_regenerate', False):
|
|
65
|
+
script = str(regen.get('script') or '')
|
|
66
|
+
if script:
|
|
67
|
+
cmd = [sys.executable, str(SCRIPTS / script)] + [str(x) for x in (regen.get('args') or [])]
|
|
68
|
+
env = dict(os.environ)
|
|
69
|
+
env.setdefault('ARGENT_DATA_DIR', resolve_data_dir()['dir']) # 传给再生成器(唯一判据解析出来的)
|
|
70
|
+
try:
|
|
71
|
+
r = subprocess.run(cmd, capture_output=True, text=True, env=env,
|
|
72
|
+
timeout=int(regen.get('timeoutSec') or 120))
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f'❌ 源再生成异常({script}): {e}', file=sys.stderr)
|
|
75
|
+
return 2, []
|
|
76
|
+
if r.returncode != 0:
|
|
77
|
+
print(f'❌ 源再生成失败({script},exit={r.returncode}):'
|
|
78
|
+
f'{(r.stderr or r.stdout).strip()[:300]}', file=sys.stderr)
|
|
79
|
+
return 2, []
|
|
80
|
+
# 过期守卫:生成器没在跑 ⇒ 清单会一直"合法地空" ⇒ 必须响亮暴露(不许静默 0 票)
|
|
81
|
+
max_age = int(src.get('maxAgeSec') or 0)
|
|
82
|
+
if (max_age and batch and batch != '-' and reading_declared
|
|
83
|
+
and not getattr(args, 'no_regenerate', False) and pathlib.Path(batch).exists()):
|
|
84
|
+
age_sec = time.time() - pathlib.Path(batch).stat().st_mtime
|
|
85
|
+
if age_sec > max_age:
|
|
86
|
+
print(f'❌ 批量清单过期({int(age_sec / 60)} 分钟未更新,上限 {max_age // 60} 分钟):{batch}'
|
|
87
|
+
f' —— 上游生成器未在运行,本轮不派发', file=sys.stderr)
|
|
88
|
+
return 2, []
|
|
89
|
+
try:
|
|
90
|
+
rows = read_cases_batch(batch)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f'❌ 读取批量清单失败({batch or "stdin"}): {e}', file=sys.stderr)
|
|
93
|
+
return 2, []
|
|
94
|
+
notes = read_cases_notes(batch)
|
|
95
|
+
if not rows:
|
|
96
|
+
# 空清单是**合法结论**(当前没有待处理单元)⇒ exit 0,
|
|
97
|
+
# 让分诊给出"无票可派"而不是报错(否则定时轮每小时刷一条假错误);
|
|
98
|
+
# 但**必须带上原因**(未到点/已推送/窗口已过),否则运维看到的是"无票可认领"
|
|
99
|
+
# 这种没有信息量的结论 —— 2026-09-18 用户实测正是被它误导。
|
|
100
|
+
if args.json:
|
|
101
|
+
print(json.dumps({'flow': args.flow, 'source': args.source, 'cases': [], 'exit': 0,
|
|
102
|
+
'notes': notes}, ensure_ascii=False, indent=2))
|
|
103
|
+
else:
|
|
104
|
+
print(' · 批量清单为空(当前没有待处理单元)' + (f':{";".join(notes)}' if notes else ''))
|
|
105
|
+
return 0, []
|
|
106
|
+
return None, rows
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""数据面共享原语(P4):读表/收窄/票据键/字段缺口 —— 引擎与各实现共用。
|
|
2
|
+
|
|
3
|
+
从 `flow_source.py` **原样搬出**(纯搬家,不改行为);`flow_source.py` 继续再导出这些名字,
|
|
4
|
+
历史调用点(`flow_triage` 的 `_truthy`、`flow_retry` 的 `load_allowed_stores`、测试的
|
|
5
|
+
`ticket_key`/`wanted_pick_columns`/`apply_pick`)不感知这次搬迁。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
TRANSIENT_PAT = (
|
|
17
|
+
'context deadline exceeded', '"subtype":"transport"', '"subtype":"timeout"',
|
|
18
|
+
'subtype": "transport', 'subtype": "timeout', 'socket hang up', 'ETIMEDOUT',
|
|
19
|
+
'ECONNRESET', 'EPIPE', 'accounts.feishu.cn/oauth', 'network',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
CMD_TIMEOUT_SEC = int(os.environ.get('ARGENT_CMD_TIMEOUT') or 120) # 单次外部命令硬超时
|
|
23
|
+
|
|
24
|
+
CMD_RETRIES = int(os.environ.get('ARGENT_CMD_RETRIES') or 3)
|
|
25
|
+
|
|
26
|
+
# ── 相位(**唯一真源**)────────────────────────────────────────────────────────
|
|
27
|
+
# `apply_pick()` 给每行打的 `_phase` **只从这一份取**;门禁 `check-sop-consistency.mjs` 的 C2
|
|
28
|
+
# (提示词里提到的相位 ⊆ 引擎实际产出的相位)经 `flow_source.py --list-phases` **问这里**。
|
|
29
|
+
# ⛔ 不要在别处再抄一份相位名单:2026-10-08 修的正是这个形态 —— 门禁在 Node 侧另抄了一份,
|
|
30
|
+
# 又用正则去解析**源码文本**;P4(2026-09-29)把赋值从 `flow_source.py` 搬进本文件后,
|
|
31
|
+
# 那次解析恒得**空集**,C2 变成"空集 ⊆ 任何集合"的**静默空转**(输出里 `引擎产出()`)。
|
|
32
|
+
PHASES = ('identify', 'execute', 'await_release', 'retry', 'needs_human')
|
|
33
|
+
# 赋值点用这组名字(与 PHASES 同源),避免字面量在代码里再散一份
|
|
34
|
+
PHASE_IDENTIFY, PHASE_EXECUTE, PHASE_AWAIT_RELEASE, PHASE_RETRY, PHASE_NEEDS_HUMAN = PHASES
|
|
35
|
+
|
|
36
|
+
def _is_transient(text: str) -> bool:
|
|
37
|
+
t = (text or '').lower()
|
|
38
|
+
return any(pat.lower() in t for pat in TRANSIENT_PAT)
|
|
39
|
+
|
|
40
|
+
def run(cmd: list[str], timeout: int = None, retries: int = None) -> tuple[int, str, str]:
|
|
41
|
+
"""跑外部命令(lark-cli 读表等)——**必须带超时**。
|
|
42
|
+
|
|
43
|
+
为什么(2026-09-22 事故):飞书 API 出现半开连接时 `lark-cli` **永不返回**,而这里原本
|
|
44
|
+
没有超时 ⇒ `flow_triage.py` 卡死,文鳐/鑫中航两个定时器各自冻结 **4 小时 51 分**:
|
|
45
|
+
日志停在那一刻、没有任何报错、认领锁一直被占,整条发票流程静默停摆。
|
|
46
|
+
超时按 124(GNU timeout 约定)返回,调用方据此走"响亮失败"分支。
|
|
47
|
+
瞬时网络错误(网关超时/连接重置)按指数退避重试,避免一次抖动就记一条人工处理。
|
|
48
|
+
"""
|
|
49
|
+
timeout = timeout or CMD_TIMEOUT_SEC
|
|
50
|
+
retries = retries if retries is not None else CMD_RETRIES
|
|
51
|
+
last = (124, '', f'命令超时 {timeout}s({cmd[0]} …)')
|
|
52
|
+
for attempt in range(max(1, retries)):
|
|
53
|
+
try:
|
|
54
|
+
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
55
|
+
except subprocess.TimeoutExpired:
|
|
56
|
+
last = (124, '', f'命令超时 {timeout}s({cmd[0]} …)')
|
|
57
|
+
if attempt + 1 < retries:
|
|
58
|
+
time.sleep(min(2 ** attempt, 8))
|
|
59
|
+
continue
|
|
60
|
+
if p.returncode != 0 and _is_transient(p.stderr) and attempt + 1 < retries:
|
|
61
|
+
last = (p.returncode, p.stdout, p.stderr)
|
|
62
|
+
time.sleep(min(2 ** attempt, 8))
|
|
63
|
+
continue
|
|
64
|
+
return p.returncode, p.stdout, p.stderr
|
|
65
|
+
return last
|
|
66
|
+
|
|
67
|
+
def read_cases_batch(path: str) -> list:
|
|
68
|
+
"""读取**批量**待处理清单(cases-json)。
|
|
69
|
+
|
|
70
|
+
接受三种形状(宽松,便于各系统导出直接用):
|
|
71
|
+
[{"ticketKey": "站点-日期-id", "fields": {...}}, ...]
|
|
72
|
+
{"cases": [ ...同上... ]}
|
|
73
|
+
{"<ticketKey>": { ...直接是字段... }, ...}
|
|
74
|
+
返回 [(ticketKey, fields)];形状不对的条目跳过(不静默造假数据)。
|
|
75
|
+
"""
|
|
76
|
+
raw = sys.stdin.read() if (not path or path == '-') else pathlib.Path(path).read_text(encoding='utf-8')
|
|
77
|
+
doc = json.loads(raw)
|
|
78
|
+
items = None
|
|
79
|
+
if isinstance(doc, dict) and isinstance(doc.get('cases'), list):
|
|
80
|
+
items = doc['cases']
|
|
81
|
+
elif isinstance(doc, list):
|
|
82
|
+
items = doc
|
|
83
|
+
elif isinstance(doc, dict):
|
|
84
|
+
items = [{'ticketKey': k, 'fields': v} for k, v in doc.items()]
|
|
85
|
+
out = []
|
|
86
|
+
for it in items or []:
|
|
87
|
+
if not isinstance(it, dict):
|
|
88
|
+
continue
|
|
89
|
+
tk = it.get('ticketKey') or it.get('ticket') or it.get('key')
|
|
90
|
+
fields = it.get('fields')
|
|
91
|
+
if fields is None:
|
|
92
|
+
fields = {k: v for k, v in it.items() if k not in ('ticketKey', 'ticket', 'key')}
|
|
93
|
+
if tk and isinstance(fields, dict):
|
|
94
|
+
out.append((str(tk), fields))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
def read_cases_notes(path: str) -> list:
|
|
98
|
+
"""批量清单里的"为什么没有待处理单元"说明(`notes` / `skipped`)。
|
|
99
|
+
|
|
100
|
+
日报这类流程的清单由生成器算出:空清单可能是"今天已推送"、"还没到推送点"、
|
|
101
|
+
"错过补推窗口"——三种结论对运维的意义完全不同 ⇒ 必须一路带到触发器日志里。
|
|
102
|
+
"""
|
|
103
|
+
if not path or path == '-':
|
|
104
|
+
return []
|
|
105
|
+
try:
|
|
106
|
+
doc = json.loads(pathlib.Path(path).read_text(encoding='utf-8'))
|
|
107
|
+
except Exception:
|
|
108
|
+
return []
|
|
109
|
+
if not isinstance(doc, dict):
|
|
110
|
+
return []
|
|
111
|
+
out = []
|
|
112
|
+
for n in (doc.get('notes') or []):
|
|
113
|
+
if isinstance(n, str) and n.strip():
|
|
114
|
+
out.append(n.strip())
|
|
115
|
+
if out:
|
|
116
|
+
return out[:6] # 生成器已给出原因 ⇒ 不再从 skipped 重复推导
|
|
117
|
+
for it in (doc.get('skipped') or []):
|
|
118
|
+
if isinstance(it, dict):
|
|
119
|
+
store = it.get('store') or it.get('storeKey') or ''
|
|
120
|
+
why = it.get('reason') or it.get('why') or ''
|
|
121
|
+
if store or why:
|
|
122
|
+
out.append(f'{store} {why}'.strip())
|
|
123
|
+
elif isinstance(it, str) and it.strip():
|
|
124
|
+
out.append(it.strip())
|
|
125
|
+
return out[:6]
|
|
126
|
+
|
|
127
|
+
def read_fields(path: str) -> dict:
|
|
128
|
+
raw = sys.stdin.read() if path == '-' else pathlib.Path(path).read_text(encoding='utf-8')
|
|
129
|
+
v = json.loads(raw)
|
|
130
|
+
if isinstance(v, dict) and 'fields' in v and isinstance(v['fields'], dict):
|
|
131
|
+
v = v['fields'] # 兼容直接传 case 文件
|
|
132
|
+
if not isinstance(v, dict):
|
|
133
|
+
raise ValueError('字段必须是 JSON 对象')
|
|
134
|
+
return v
|
|
135
|
+
|
|
136
|
+
def _flat(v):
|
|
137
|
+
"""把飞书单元格值归一化:单元素数组展开、多元素拼接、None → 空串。"""
|
|
138
|
+
if v is None:
|
|
139
|
+
return ''
|
|
140
|
+
if isinstance(v, list):
|
|
141
|
+
parts = [_flat(x) for x in v]
|
|
142
|
+
parts = [x for x in parts if x != '']
|
|
143
|
+
return parts[0] if len(parts) == 1 else ' | '.join(parts)
|
|
144
|
+
if isinstance(v, dict):
|
|
145
|
+
for k in ('text', 'name', 'value', 'file_name'):
|
|
146
|
+
if k in v:
|
|
147
|
+
return _flat(v[k])
|
|
148
|
+
return json.dumps(v, ensure_ascii=False)[:200]
|
|
149
|
+
return str(v)
|
|
150
|
+
|
|
151
|
+
def _has_value(v) -> bool:
|
|
152
|
+
"""字段**真有值**吗(缺口判据用,2026-09-17 P4 实测修正)。
|
|
153
|
+
|
|
154
|
+
为什么需要它:`ml_schema_check` 离线模式的判据是"这个字段名在不在提供的名单里",
|
|
155
|
+
而 `fields` 里**投影到的每一列都存在**(空单元格 = `''`)⇒ 若不过滤空值,
|
|
156
|
+
「发票就绪」(运营放行 checkbox,未勾选时接口返回空)会被算作**已提供** ⇒
|
|
157
|
+
全表只要有 `货代公司=上海文鳐` 就判"字段齐备",分诊直接 `to_dispatch`
|
|
158
|
+
(2026-09-17 实测:lip 表 535 行 → 文鳐 90 行 → 分诊报可派发 83,而人工核对只有个位数真候选)。
|
|
159
|
+
|
|
160
|
+
判据:`None`/空白串/空容器 ⇒ 未提供;**`False` 与字符串 `False`/`false` 也视为未提供**
|
|
161
|
+
(飞书 checkbox 未勾选在部分接口形态下就返回这两种值,放行开关必须因此挡住);
|
|
162
|
+
数字 0、字符串 `'0'`、`'否'` 等**算有值**(是真取值,不是缺省)。
|
|
163
|
+
"""
|
|
164
|
+
if v is None or v is False:
|
|
165
|
+
return False
|
|
166
|
+
if isinstance(v, str):
|
|
167
|
+
return v.strip() not in ('', 'False', 'false')
|
|
168
|
+
if isinstance(v, (list, tuple, dict, set)):
|
|
169
|
+
return len(v) > 0
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
LAST_NARROW: dict = {} # 记录「候选收窄把行清空」这一合法情形(见 pull_feishu)
|
|
173
|
+
|
|
174
|
+
LAST_PICK_NOTES: list = [] # 候选收窄的说明(跨函数传:pull_feishu → main 的 JSON 输出)
|
|
175
|
+
|
|
176
|
+
def _truthy(v) -> bool:
|
|
177
|
+
"""checkbox 类字段的**真值**判断(2026-09-17 实测补)。
|
|
178
|
+
|
|
179
|
+
坑:`_flat()` 把飞书布尔值转成字符串 ⇒ 未勾选是 `'False'`,而它是**非空字符串**,
|
|
180
|
+
直接 `if v` 会判成"真" ⇒ 放行门(就绪)/就绪类开关会被绕过,未放行的行照样被派发。
|
|
181
|
+
判据:`None`/`False`/空白/`false`/`0`/`no`/`否`/`未`/`空` ⇒ 假;其余非空 ⇒ 真。
|
|
182
|
+
"""
|
|
183
|
+
if v is None or v is False:
|
|
184
|
+
return False
|
|
185
|
+
if isinstance(v, str):
|
|
186
|
+
return v.strip().lower() not in ('', 'false', '0', 'no', '否', '未', '空')
|
|
187
|
+
if isinstance(v, (list, tuple, dict, set)):
|
|
188
|
+
return len(v) > 0
|
|
189
|
+
return bool(v)
|
|
190
|
+
|
|
191
|
+
def load_allowed_stores(path: str) -> set:
|
|
192
|
+
"""读 `stores.json`(本机紫鸟已授权的店:名 → {store_id, cdp_port})取**可驱动的店名集合**。
|
|
193
|
+
|
|
194
|
+
用途(`pick.onlyStoresFrom`):本机只授权了客户 6 家里的 3 家 ⇒ 其余行的店铺我们**驱动不了**。
|
|
195
|
+
旧行为下识别层会把它们判成"未授权"并**把「跳过」写进生产表**(写脏数据);加了这层收窄后,
|
|
196
|
+
不属于本机授权店的行**根本不进候选**——不派发、也不写任何字段,等本机授权补齐再说。
|
|
197
|
+
"""
|
|
198
|
+
import json as _json
|
|
199
|
+
if not path:
|
|
200
|
+
return set()
|
|
201
|
+
p = pathlib.Path(os.path.expanduser(str(path)))
|
|
202
|
+
try:
|
|
203
|
+
doc = _json.loads(p.read_text(encoding='utf-8'))
|
|
204
|
+
except Exception:
|
|
205
|
+
return set()
|
|
206
|
+
if isinstance(doc, dict) and isinstance(doc.get('stores'), dict):
|
|
207
|
+
doc = doc['stores'] # 兼容 {stores:{…}} 包装
|
|
208
|
+
return set(doc.keys()) if isinstance(doc, dict) else set()
|
|
209
|
+
|
|
210
|
+
def wanted_upstream_columns(src: dict) -> list[str]:
|
|
211
|
+
"""**上游变化观察列**(`pick.releaseReDispatch.upstreamHash.watch`,真列名)—— 必须进投影。
|
|
212
|
+
|
|
213
|
+
2026-10-13(放行额度随上游数据变化重置):判据是"该票上游行走流程读取的字段变了没有",
|
|
214
|
+
而 case 文件只装**投影到**的列 ⇒ 不在投影里的列(如发票流程的 `产品图`:运营补了它、
|
|
215
|
+
流程也需要它,却从不出现在 case 里)**永远不会被算作"变"**。
|
|
216
|
+
与 `wanted_pick_columns` 同一形状(都返回真列名,由 feishu_base 拼进投影),
|
|
217
|
+
但**语义不同**:pick 列缺失是硬故障(过滤读空 ⇒ 静默收窄到 0 行),
|
|
218
|
+
观察列缺失只是判据更窄 ⇒ 调用方**静默跳过该列**(⛔ 不硬失败)。
|
|
219
|
+
"""
|
|
220
|
+
rd = ((src.get('pick') or {}).get('releaseReDispatch') or {})
|
|
221
|
+
up = rd.get('upstreamHash') or {}
|
|
222
|
+
out, seen = [], set()
|
|
223
|
+
for nm in (up.get('watch') or []):
|
|
224
|
+
nm = str(nm).strip()
|
|
225
|
+
if nm and nm not in seen:
|
|
226
|
+
seen.add(nm)
|
|
227
|
+
out.append(nm)
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def wanted_pick_columns(src: dict) -> list[str]:
|
|
232
|
+
"""`pick` 规则要用到哪些**真列名**(投影必须覆盖它们,否则过滤读到空值 → 静默收窄到 0 行)。
|
|
233
|
+
|
|
234
|
+
2026-09-17 实测事故:FULL lip 变体的 `pick.status=FULL状态`、`pick.ready=就绪` 两列既不在
|
|
235
|
+
`required` 里、也不是路由/票号列 ⇒ 没进投影 ⇒ 每行这两列都是空 ⇒ 530 行全部被"收窄"掉,
|
|
236
|
+
日志只显示「候选收窄 100 → 0 行」,看起来像"今天没活干"。判据来源:
|
|
237
|
+
· `pick.status` → `source.fields.status`(领域名→真列名,缺省按领域名)
|
|
238
|
+
· `pick.ready`(只要声明了,无论真假)→ `source.fields.ready`
|
|
239
|
+
· `pick.equals` 的键、`pick.requireEmpty` 的元素 —— 这两处写的**已经是真列名**
|
|
240
|
+
"""
|
|
241
|
+
sf = src.get('fields') or {}
|
|
242
|
+
pk = src.get('pick') or {}
|
|
243
|
+
doms: list[str] = []
|
|
244
|
+
if pk.get('status'):
|
|
245
|
+
doms.append('status')
|
|
246
|
+
if pk.get('ready') is not None:
|
|
247
|
+
doms.append('ready')
|
|
248
|
+
doms += list((pk.get('equals') or {}).keys())
|
|
249
|
+
doms += list(pk.get('requireEmpty') or [])
|
|
250
|
+
rel = pk.get('release') or {}
|
|
251
|
+
if rel.get('field'):
|
|
252
|
+
doms.append(str(rel['field'])) # release.field 写的是真列名(如「就绪」)
|
|
253
|
+
if pk.get('onlyStoresFrom'):
|
|
254
|
+
doms.append('store') # 店铺收窄要用到店铺列(fields.store → 真列名)
|
|
255
|
+
if pk.get('retryFrom'):
|
|
256
|
+
doms.append('note') # 失败分类要读备注(fields.note → 真列名)
|
|
257
|
+
out, seen = [], set()
|
|
258
|
+
for d in doms:
|
|
259
|
+
nm = str(sf.get(d) or d)
|
|
260
|
+
if nm not in seen:
|
|
261
|
+
seen.add(nm); out.append(nm)
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
def apply_pick(rows: list[dict], src: dict, *, no_pick_filter: bool = False):
|
|
265
|
+
"""按流程声明的 `pick` 收窄候选行,并标出每行的**阶段**(纯函数,便于离线回归)。
|
|
266
|
+
|
|
267
|
+
阶段(2026-09-17 用户口径 F5「triage 即识别层」):
|
|
268
|
+
· `identify` —— 状态为空 = 未识别的新行(角色只做识别:写「待执行」/「跳过」+备注)
|
|
269
|
+
· `execute` —— 已识别 且 放行门通过(`pick.release`/`pick.ready`)
|
|
270
|
+
· `await_release` —— 已识别 但运营还没勾放行(分诊只报数、不派发、不落卡)
|
|
271
|
+
|
|
272
|
+
放行门只对**已识别**的行生效;真值判断走 `_truthy()`(`'False'` 不算放行)。
|
|
273
|
+
返回 `(rows, meta)`,meta 含 before/rule/detail 供上层打日志与判「收窄到 0」。
|
|
274
|
+
"""
|
|
275
|
+
pick = src.get('pick') or {}
|
|
276
|
+
if not pick or no_pick_filter:
|
|
277
|
+
return rows, {'before': len(rows)}
|
|
278
|
+
names = src.get('fields') or {}
|
|
279
|
+
want_status = [str(x).strip().lower() for x in (pick.get('status') or [])]
|
|
280
|
+
equals = {str(k): str(v).strip().lower() for k, v in (pick.get('equals') or {}).items()}
|
|
281
|
+
status_col = names.get('status')
|
|
282
|
+
ready_col = names.get('ready')
|
|
283
|
+
require_empty = list(pick.get('requireEmpty') or [])
|
|
284
|
+
# 已(人工/自动)完成的排除规则(2026-09-22 用户口径:lip 表里大量行是客户运营**人工完成**的
|
|
285
|
+
# 历史数据,必须分辨清楚):命中即不进候选,也不写任何字段,只计数上报。
|
|
286
|
+
# · excludeEquals {列: [值,…]} 任一命中 ⇒ 排除
|
|
287
|
+
# · excludeAllPresent [[列,…],…] 某一组**全部非空** ⇒ 排除("两个产物都在"才算做完)
|
|
288
|
+
excl_equals = {str(k): [str(x).strip().lower() for x in (v if isinstance(v, list) else [v])]
|
|
289
|
+
for k, v in (pick.get('excludeEquals') or {}).items()}
|
|
290
|
+
excl_all = [[str(c) for c in grp] for grp in (pick.get('excludeAllPresent') or []) if grp]
|
|
291
|
+
dropped_done = 0
|
|
292
|
+
release = pick.get('release') or {}
|
|
293
|
+
rel_col = release.get('field') or ready_col
|
|
294
|
+
rel_want = release.get('equals', True)
|
|
295
|
+
# 店铺收窄(2026-09-17):只处理**本机紫鸟已授权**的店;其余行不进候选、也不写任何字段。
|
|
296
|
+
store_col = names.get('store') or pick.get('storeField') or ''
|
|
297
|
+
allowed = load_allowed_stores(pick.get('onlyStoresFrom') or '') if pick.get('onlyStoresFrom') else set()
|
|
298
|
+
dropped_store = 0
|
|
299
|
+
keep = []
|
|
300
|
+
# 计数字典的相位键**从 PHASES 派生**(唯一真源);`excludedDone` 不是相位,是"已完成排除"的计数
|
|
301
|
+
phases = {p: 0 for p in PHASES}
|
|
302
|
+
phases['excludedDone'] = 0
|
|
303
|
+
for r in rows:
|
|
304
|
+
if equals and any(str(r.get(col) or '').strip().lower() != want for col, want in equals.items()):
|
|
305
|
+
continue
|
|
306
|
+
if excl_equals and any(str(r.get(col) or '').strip().lower() in vals
|
|
307
|
+
for col, vals in excl_equals.items()):
|
|
308
|
+
dropped_done += 1
|
|
309
|
+
continue
|
|
310
|
+
if excl_all and any(all(str(r.get(c) or '').strip() for c in grp) for grp in excl_all):
|
|
311
|
+
dropped_done += 1
|
|
312
|
+
continue
|
|
313
|
+
if allowed and store_col:
|
|
314
|
+
st = str(r.get(store_col) or '').strip()
|
|
315
|
+
# 只有**写了店名但本机没授权**的行才剔除(我们驱动不了,且不该写「跳过」);
|
|
316
|
+
# **店铺为空**的行要留下 —— 那是"缺字段",由 triage 归入 need_fields 让人补,
|
|
317
|
+
# 直接吞掉会让这批行彻底从视野里消失(2026-09-17 实测:91 行差一点被吞)。
|
|
318
|
+
if st and st not in allowed:
|
|
319
|
+
dropped_store += 1
|
|
320
|
+
continue
|
|
321
|
+
got = str(r.get(status_col) or '').strip().lower() if status_col else ''
|
|
322
|
+
identified = bool(got) # ⚠️ 必须在 retry 分支**之前**定义(否则会用到上一行的值)
|
|
323
|
+
# 2026-09-18 实测修:**失败行的重跑分支必须先于状态门**。
|
|
324
|
+
# 上一版把 retry 分支放在 requireEmpty 之前(对),但仍放在 `want_status` 之后(错)——
|
|
325
|
+
# 而 `pick.status` 只含 ["", "待执行"],`失败` 根本不在其中 ⇒ 行在状态门就被
|
|
326
|
+
# `continue` 掉,**retryFrom 分支实际不可达**(实测:77445732 / recvvgPQgoB4qL
|
|
327
|
+
# `FULL状态=失败` 在分诊结果里彻底不出现,plan.retry 恒为 0)。
|
|
328
|
+
# 这正是注释里说的"第一版踩的坑"——只是差了**一步**位置。失败行天然带非空状态,
|
|
329
|
+
# 所以必须独立于"候选状态集合"判定。
|
|
330
|
+
retry_from = [str(x).strip().lower() for x in (pick.get('retryFrom') or [])]
|
|
331
|
+
if identified and status_col and got in retry_from:
|
|
332
|
+
note_col = names.get('note') or ''
|
|
333
|
+
text = str(r.get(note_col) or '') if note_col else ''
|
|
334
|
+
try:
|
|
335
|
+
from flow_failure import classify as _classify
|
|
336
|
+
res = _classify(text, attempt=int((pick.get('retry') or {}).get('attempt', 1)),
|
|
337
|
+
max_attempts=int((pick.get('retry') or {}).get('maxAttempts', 3)))
|
|
338
|
+
# classify() 返回 dict({kind, reason, error})—— 第一版按字符串比,结果全落到 needs_human
|
|
339
|
+
kind = str((res or {}).get('kind') or 'needs_agent') if isinstance(res, dict) else str(res)
|
|
340
|
+
except Exception:
|
|
341
|
+
kind = 'needs_agent'
|
|
342
|
+
r['_phase'] = PHASE_RETRY if kind == 'retryable' else PHASE_NEEDS_HUMAN
|
|
343
|
+
r['_failureKind'] = kind
|
|
344
|
+
phases[r['_phase']] = phases.get(r['_phase'], 0) + 1
|
|
345
|
+
keep.append(r)
|
|
346
|
+
continue
|
|
347
|
+
if want_status and status_col and got not in want_status:
|
|
348
|
+
continue
|
|
349
|
+
if require_empty and any(str(r.get(c) or '').strip() for c in require_empty):
|
|
350
|
+
continue
|
|
351
|
+
released = True
|
|
352
|
+
if identified and rel_col:
|
|
353
|
+
curr = r.get(rel_col)
|
|
354
|
+
released = (_truthy(curr) if rel_want is True
|
|
355
|
+
else str(curr or '').strip().lower() == str(rel_want).strip().lower())
|
|
356
|
+
r['_phase'] = (PHASE_IDENTIFY if not identified
|
|
357
|
+
else (PHASE_EXECUTE if released else PHASE_AWAIT_RELEASE))
|
|
358
|
+
phases[r['_phase']] += 1
|
|
359
|
+
keep.append(r)
|
|
360
|
+
rule = ('、'.join(f'{k}={v}' for k, v in (pick.get('equals') or {}).items())
|
|
361
|
+
or f'状态∈{pick.get("status") or "-"}')
|
|
362
|
+
if rel_col:
|
|
363
|
+
rule += f' ∧(已识别的行){rel_col}=放行'
|
|
364
|
+
if require_empty:
|
|
365
|
+
rule += f' ∧ {"、".join(require_empty)}为空'
|
|
366
|
+
# 2026-09-18:失败行的重跑旁路必须写进规则文案 —— 否则日志只显示「货件号为空」,
|
|
367
|
+
# 运维/角色会以为"失败的行(货件号非空)根本不会被拾取",与实测相反(实测 77445732 会进)。
|
|
368
|
+
if (pick.get('retryFrom') or []):
|
|
369
|
+
rule += (f';**例外**:状态∈{"、".join(str(x) for x in pick["retryFrom"])} 的行'
|
|
370
|
+
f'**不受上面任何一条限制**(按失败分类器分流 retry/needs_human)')
|
|
371
|
+
if allowed:
|
|
372
|
+
rule += f' ∧ {store_col or "店铺"}∈本机已授权 {len(allowed)} 店'
|
|
373
|
+
if excl_equals or excl_all:
|
|
374
|
+
bits = [f'{k}∈{"、".join(v)}' for k, v in excl_equals.items()]
|
|
375
|
+
bits += ['且'.join(grp) + '都已填' for grp in excl_all]
|
|
376
|
+
rule += f';**排除**:{";".join(bits)} ⇒ 视为已完成,不进候选'
|
|
377
|
+
detail = '、'.join(f'{k} {v}' for k, v in phases.items() if v)
|
|
378
|
+
if dropped_store:
|
|
379
|
+
detail += ((',' if detail else '') + f'店未授权跳过 {dropped_store}')
|
|
380
|
+
if dropped_done:
|
|
381
|
+
detail += ((',' if detail else '') + f'已完成排除 {dropped_done}')
|
|
382
|
+
phases['excludedDone'] = dropped_done
|
|
383
|
+
return keep, {'before': len(rows), 'rule': rule, 'phases': phases, 'detail': detail,
|
|
384
|
+
'allowedStores': len(allowed), 'droppedStore': dropped_store,
|
|
385
|
+
'droppedDone': dropped_done}
|
|
386
|
+
|
|
387
|
+
def ticket_key(rec: str, fields: dict, ticket_field: str, mode: str = '') -> str:
|
|
388
|
+
"""行的**身份**(caseId 的后半段)。
|
|
389
|
+
|
|
390
|
+
2026-09-18 二次修正(第一次改错了,见下):身份**必须逐流程选**,不能一刀切。
|
|
391
|
+
各流程的既有约定(实测 flow-ledger.jsonl 去重计数):
|
|
392
|
+
· `full` → **record_id**(19/19)。因为 FULL 是**流程中途才建出货件号**:
|
|
393
|
+
派发时「货件号」为空、身份只能是 record_id;若改成"票号列优先",同一条票在
|
|
394
|
+
建单失败后会**换成第二个身份**(full__<货件号>),于是与失败台账 caseId 对不上
|
|
395
|
+
(查不到事件 ⇒ blocked 的票被当成"全新票"自动重派,绕过"needs_human 不许自动派发")。
|
|
396
|
+
· `wenyao-invoice` / `xzh-invoice` → **货件号**(10+5 条)。这两个流程的行**天然已有**
|
|
397
|
+
货件号(开启前提是 FULL状态=已完成),历史身份一直是数字;改成 record_id 会让
|
|
398
|
+
"已完成"的行换个身份重新被认领(实测 23:17 立刻重复认领了 77447894,多亏角色
|
|
399
|
+
判 blocked 才没重复写生产表)。
|
|
400
|
+
⇒ 由注册表 `source.ticketKey` 显式声明('record_id' 或 'ticketField'),缺省保持历史行为。
|
|
401
|
+
"""
|
|
402
|
+
m = str(mode or '').strip()
|
|
403
|
+
if m == 'record_id':
|
|
404
|
+
return str(rec or fields.get(ticket_field) or '')
|
|
405
|
+
return str(fields.get(ticket_field) or rec or '') # 历史默认:票号列优先
|
|
406
|
+
|
|
407
|
+
def required_projection(spec: dict) -> list[str]:
|
|
408
|
+
"""`required` 里**该从数据源拉取**的字段名(= 投影的 required 部分)。
|
|
409
|
+
|
|
410
|
+
判据=显式 `role`(2026-09-20 迭代1-步骤2 起),不再是 owner 的中文子串:
|
|
411
|
+
· `input` 运营提供的数据 → 拉
|
|
412
|
+
· `state` 运行中读写的状态位 → **拉**(规则与 runner 都可能读它,如发票状态/发票就绪)
|
|
413
|
+
· `output` 流程产物 → 不拉(不是数据源的输入)
|
|
414
|
+
· `derived`上游生成器提供 → 不拉(不是表列)
|
|
415
|
+
旧判据 `'系统' in owner` 把 `发票状态`(owner=系统/运营)误排除 ⇒ 依赖该列的规则会静默拿到空值。
|
|
416
|
+
"""
|
|
417
|
+
out = []
|
|
418
|
+
for r in (spec.get('required') or []):
|
|
419
|
+
name = r.get('name')
|
|
420
|
+
if not name:
|
|
421
|
+
continue
|
|
422
|
+
role = str(r.get('role') or '')
|
|
423
|
+
if not role: # 兜底:裸调(未经 flow_config 校验)时按旧判据推理
|
|
424
|
+
role = 'output' if '系统' in str(r.get('owner') or '') else 'input'
|
|
425
|
+
if role in ('output', 'derived'):
|
|
426
|
+
continue
|
|
427
|
+
out.append(str(name))
|
|
428
|
+
return out
|