@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,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* license-gate —— 生产闸门的**离线许可 / 租约二选一**判定(2026-09-30)
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么单独一个脚本
|
|
6
|
+
* `flow-trigger.sh` 的租约闸门是 bash 内联的,但"许可"这一侧要做四件事:
|
|
7
|
+
* ① 读 JSON(`check-license.mjs --json`)② 三态 + 能力/业务线判定 ③ 追加一行 JSONL 证据
|
|
8
|
+
* ④ 把结论翻译成人类可读一行。全塞进 bash 就得靠 grep 中文/手拼 JSON —— 那是**不可测**的。
|
|
9
|
+
* 所以判定与取证放这里(纯 Node、可单测、可注入 --now),bash 只负责"要不要停派"。
|
|
10
|
+
*
|
|
11
|
+
* ## 模式(`ARGENT_LICENSE_ENFORCE`,与 flow-trigger.sh 同一套取值)
|
|
12
|
+
* `0` / `off` 逃生阀:**不读许可**、不停派(连判定都跳过)
|
|
13
|
+
* `observe` **默认**:照常计算"能不能用",但**无论结论如何都不停派**;必须留证据
|
|
14
|
+
* `enforce` 不可用 ⇒ 退出码 1(调用方沿用"停派 + 台账 + 人工指南"分支)
|
|
15
|
+
*
|
|
16
|
+
* ## 判定顺序(与 flow-trigger.sh 一致,**租约优先**)
|
|
17
|
+
* ① 有可用租约(`--lease-ok 1`)⇒ 直接放行,**根本不读许可**(最大程度不改变既有行为)
|
|
18
|
+
* ② 否则回落到离线许可:`check-license.mjs --cap <cap> [--vertical <v>] --json`
|
|
19
|
+
* · exit 0(valid)|3(grace:放行但告警)⇒ 可用;1(denied)⇒ 不可用
|
|
20
|
+
*
|
|
21
|
+
* ## 退出码 = **真实判定**(映射的唯一真源;调用方按 mode 决定要不要据此停派)
|
|
22
|
+
* 0 可用(放行)|3 宽限(可用但必须告警)|1 不可用(拒绝)|2 用法错误
|
|
23
|
+
* ⚠️ observe 下**照样**返回上面的真实码,但**绝不据此停派** —— 若让 observe 恒返回 0,
|
|
24
|
+
* 调用方就再也分不清"许可有效"与"许可无效但只是告警"(实测踩到:日志会把过期许可写成
|
|
25
|
+
* "离线许可有效 ⇒ 照常派发",正是最该看见的那条信息被抹掉)。
|
|
26
|
+
*
|
|
27
|
+
* ## 证据(验收核心)
|
|
28
|
+
* `$ARGENT_DATA_DIR/license-gate.jsonl`(缺省 `~/.dsh/argent/license-gate.jsonl`,0600)**每次判定追加一行**:
|
|
29
|
+
* ts / flow / cap / mode / leaseOk / licenseState / allowed / reason / decision
|
|
30
|
+
* `decision` ∈ `dispatch`(真放行)|`would_block`(enforce 会停派)|`bypassed`(逃生阀)。
|
|
31
|
+
* **每次判定都写**(含"租约可用 ⇒ 放行",此时 `licenseState=not_checked`、`reason=lease_ok`)——
|
|
32
|
+
* 观察期要能回答"闸门到底跑没跑、走的哪条路"。文件超过 `GATE_LOG_MAX_BYTES`(默认 5MB)就轮转一次
|
|
33
|
+
* (`license-gate.jsonl` → `license-gate.jsonl.1`,只留一份),避免客户机上无限增长。
|
|
34
|
+
* stdout:一行人类可读结论,由 flow-trigger.sh 追加进 `flow-trigger-<flow>.out.log`。
|
|
35
|
+
*
|
|
36
|
+
* 用法:
|
|
37
|
+
* node scripts/license-gate.mjs --flow full --cap full [--vertical <业务线id>] \
|
|
38
|
+
* [--mode observe] [--lease-ok 0] [--log <jsonl>] [--now <ISO>]
|
|
39
|
+
*/
|
|
40
|
+
import { spawnSync } from 'node:child_process'
|
|
41
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync } from 'node:fs'
|
|
42
|
+
import { homedir } from 'node:os'
|
|
43
|
+
import { dirname, join, resolve } from 'node:path'
|
|
44
|
+
import { fileURLToPath } from 'node:url'
|
|
45
|
+
import { resolveDataDir } from './lib/argent-datadir.mjs'
|
|
46
|
+
|
|
47
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
48
|
+
export const REPO = resolve(HERE, '..')
|
|
49
|
+
export const CHECK = join(HERE, 'check-license.mjs')
|
|
50
|
+
export const GATE_LOG_NAME = 'license-gate.jsonl'
|
|
51
|
+
/** 证据文件上限(超过就轮转一次,只保留一份 .1):观察期每轮一行,不设上限会一直长。 */
|
|
52
|
+
export const GATE_LOG_MAX_BYTES = Number(process.env.ARGENT_LICENSE_GATE_MAX_BYTES || 5 * 1024 * 1024)
|
|
53
|
+
|
|
54
|
+
/** 模式归一:`0`/`off`/`false`/`no` ⇒ off;`enforce`/`1`/`true` ⇒ enforce;其余(含未设)⇒ observe。 */
|
|
55
|
+
export function normalizeMode(raw) {
|
|
56
|
+
const v = String(raw ?? '').trim().toLowerCase()
|
|
57
|
+
if (['0', 'off', 'false', 'no', 'skip'].includes(v)) return 'off'
|
|
58
|
+
if (['enforce', '1', 'true', 'yes', 'on'].includes(v)) return 'enforce'
|
|
59
|
+
return 'observe'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultGateLog(env = process.env) {
|
|
63
|
+
return join(resolveDataDir({ env }).dir, GATE_LOG_NAME) // 内核三级链(唯一判据)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* "租约或离线许可是否可用"(**纯函数**,唯一判定入口)。
|
|
68
|
+
* @param {object} o
|
|
69
|
+
* @param {boolean} o.leaseOk 租约是否可用(调用方已判;true ⇒ **不读许可**,现有逻辑不变)
|
|
70
|
+
* @param {object} o.license `check-license.mjs --json` 的输出(leaseOk=false 时必需)
|
|
71
|
+
* @param {string} o.mode normalizeMode 的结果
|
|
72
|
+
* @param {string} o.flow 流程名(仅用于证据/文案)
|
|
73
|
+
* @param {string} o.cap 能力 id(仅用于文案)
|
|
74
|
+
* @param {string} [o.licenseFailedReason] 许可检查**跑不起来**时的原因(脚本缺失/输出不是 JSON)
|
|
75
|
+
* @returns {{allowed:boolean, decision:'dispatch'|'would_block'|'bypassed', licenseState:string,
|
|
76
|
+
* licenseOk:boolean, reason:string, human:string}}
|
|
77
|
+
*/
|
|
78
|
+
export function evaluate({ leaseOk, license, mode, flow = '?', cap = '', licenseFailedReason = null }) {
|
|
79
|
+
if (mode === 'off') {
|
|
80
|
+
return {
|
|
81
|
+
allowed: true, decision: 'bypassed', licenseState: 'skipped', licenseOk: true, reason: 'enforce_off',
|
|
82
|
+
human: `许可闸门[off]:逃生阀已开(ARGENT_LICENSE_ENFORCE=off)⇒ 跳过许可与租约判定,照常派发`,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (leaseOk) {
|
|
86
|
+
return {
|
|
87
|
+
allowed: true, decision: 'dispatch', licenseState: 'not_checked', licenseOk: true, reason: 'lease_ok',
|
|
88
|
+
human: `许可闸门[${mode}]:能力租约可用 ⇒ 放行(未读离线许可;现有租约逻辑不变)`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 租约不可用 ⇒ 回落离线许可(这是**唯一**的许可读取点)
|
|
92
|
+
const lic = license && typeof license === 'object' ? license : null
|
|
93
|
+
const state = lic && typeof lic.state === 'string' ? lic.state : 'denied'
|
|
94
|
+
const licOk = !licenseFailedReason && !!(lic && lic.allowed === true)
|
|
95
|
+
const baseReason = licenseFailedReason || (lic && lic.reason) || 'unreadable'
|
|
96
|
+
const gate = lic && lic.gateReason ? String(lic.gateReason) : null
|
|
97
|
+
const reason = licOk ? baseReason : (gate || baseReason)
|
|
98
|
+
const grace = licOk && lic.inGrace === true
|
|
99
|
+
|
|
100
|
+
const why = licenseFailedReason
|
|
101
|
+
? `许可检查跑不起来(${licenseFailedReason})`
|
|
102
|
+
: gate === 'cap_not_allowed'
|
|
103
|
+
? `离线许可**没有这个能力**(要 ${cap},许可只有 ${(lic.capabilities || []).join(',') || '(空)'})`
|
|
104
|
+
: gate === 'vertical_not_allowed'
|
|
105
|
+
? `离线许可不含业务线 ${lic.requestedVertical}`
|
|
106
|
+
: {
|
|
107
|
+
missing: '本机没有离线许可文件',
|
|
108
|
+
no_secret: '本机没有许可验签密钥(无法确认授权)',
|
|
109
|
+
bad_signature: '离线许可**验签失败**(文件被改过或密钥不对)',
|
|
110
|
+
expired: '离线许可已过期且超出宽限期',
|
|
111
|
+
malformed: '离线许可文件不合法',
|
|
112
|
+
bad_version: '离线许可版本不支持',
|
|
113
|
+
unreadable: '离线许可读不出来',
|
|
114
|
+
}[baseReason] || `离线许可不可用(${baseReason})`
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
allowed: licOk,
|
|
118
|
+
decision: licOk ? 'dispatch' : 'would_block',
|
|
119
|
+
licenseState: state,
|
|
120
|
+
licenseOk: licOk,
|
|
121
|
+
reason,
|
|
122
|
+
human: licOk
|
|
123
|
+
? `许可闸门[${mode}]:离线许可${grace ? '在宽限期内(已过期,请尽快续期)' : '有效'}`
|
|
124
|
+
+ `(cap=${cap}${lic && lic.expiresAt ? `,至 ${lic.expiresAt}` : ''})⇒ 放行(enforce 下也会放行)`
|
|
125
|
+
: `许可闸门[${mode}]:${why}(reason=${reason})⇒ `
|
|
126
|
+
+ (mode === 'enforce' ? '本轮不派发' : 'enforce 下会停派,当前仅告警'),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 追加一行 JSONL 证据(目录自动建、文件 0600、超限轮转;写失败**不影响判定**)。 */
|
|
131
|
+
export function appendEvidence(path, row, { maxBytes = GATE_LOG_MAX_BYTES } = {}) {
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
134
|
+
// 轮转:只在超限那一刻做一次(rename 是原子的;旧的 .1 直接覆盖,只留一份历史)
|
|
135
|
+
try {
|
|
136
|
+
if (maxBytes > 0 && statSync(path).size > maxBytes) renameSync(path, `${path}.1`)
|
|
137
|
+
} catch { /* 文件不存在 / 权限问题 ⇒ 照常追加 */ }
|
|
138
|
+
appendFileSync(path, JSON.stringify(row) + '\n', { mode: 0o600 })
|
|
139
|
+
try { if ((statSync(path).mode & 0o077) !== 0) chmodSync(path, 0o600) } catch { /* 权限收不紧不致命 */ }
|
|
140
|
+
return true
|
|
141
|
+
} catch { return false }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 取 `check-license.mjs --json` 的输出。
|
|
146
|
+
* 刻意把"跑不起来"(脚本缺失/输出不是 JSON)与"判定为拒绝"分开 —— 前者是我们自己的故障,
|
|
147
|
+
* 后者是客户的授权状态,两种事的处置完全不同(证据里的 reason 一眼能分)。
|
|
148
|
+
*/
|
|
149
|
+
export function readLicense({ file, cap, vertical, now, env, timeout = 20000 } = {}) {
|
|
150
|
+
const args = [CHECK, '--json']
|
|
151
|
+
if (file) args.push('--file', file)
|
|
152
|
+
if (cap) args.push('--cap', cap)
|
|
153
|
+
if (vertical) args.push('--vertical', vertical)
|
|
154
|
+
if (now) args.push('--now', now)
|
|
155
|
+
const r = spawnSync(process.execPath, args, { encoding: 'utf8', env: env || process.env, timeout })
|
|
156
|
+
let json = null
|
|
157
|
+
try { json = JSON.parse(String(r.stdout || '')) } catch { json = null }
|
|
158
|
+
if (json) return { license: json, failedReason: null, code: r.status ?? 1 }
|
|
159
|
+
const err = String(r.stderr || '').trim().split('\n').filter(Boolean).pop() || ''
|
|
160
|
+
return {
|
|
161
|
+
license: null,
|
|
162
|
+
failedReason: r.error
|
|
163
|
+
? `checker_error:${r.error.code || r.error.message}`
|
|
164
|
+
: `checker_exit_${r.status ?? '?'}${err ? `:${err.slice(0, 120)}` : ''}`,
|
|
165
|
+
code: r.status ?? 1,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function parseArgs(argv, env = process.env) {
|
|
170
|
+
const has = (n) => argv.includes(n)
|
|
171
|
+
const of = (n, d = null) => { const i = argv.indexOf(n); return i > -1 ? (argv[i + 1] ?? d) : d }
|
|
172
|
+
if (has('-h') || has('--help')) return { help: true }
|
|
173
|
+
return {
|
|
174
|
+
flow: of('--flow') || '?',
|
|
175
|
+
cap: of('--cap') || '',
|
|
176
|
+
vertical: of('--vertical'),
|
|
177
|
+
mode: normalizeMode(of('--mode') ?? env.ARGENT_LICENSE_ENFORCE),
|
|
178
|
+
leaseOk: ['1', 'true', 'yes', 'ok'].includes(String(of('--lease-ok', '0')).toLowerCase()),
|
|
179
|
+
log: of('--log') || defaultGateLog(env),
|
|
180
|
+
file: of('--file'),
|
|
181
|
+
now: of('--now'),
|
|
182
|
+
quiet: has('--quiet'),
|
|
183
|
+
// `--json`(2026-10-03 P1 第二批加):只给**程序消费**用(`core/lib/license-guard.mjs` 读它拿判定,
|
|
184
|
+
// 不再自己算一遍)。**不改判定、不改证据**:证据行与退出码与不带 --json 时逐字相同。
|
|
185
|
+
json: has('--json'),
|
|
186
|
+
// `--force` 或 `ARGENT_LICENSE_GATE_FORCE=1`:即使租约可用也去读一次许可并留证。
|
|
187
|
+
// 用途:客户机想**独立核对**"离线许可本身还有效吗"(不依赖租约那条路径),
|
|
188
|
+
// 或运维想在证据里看到每轮的"闸门确实跑过"心跳。
|
|
189
|
+
force: has('--force') || ['1', 'true', 'yes', 'on'].includes(String(env.ARGENT_LICENSE_GATE_FORCE || '').toLowerCase()),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function die(m) { console.error(`✗ ${m}`); process.exit(2) }
|
|
194
|
+
|
|
195
|
+
function main() {
|
|
196
|
+
const argv = process.argv.slice(2)
|
|
197
|
+
const a = parseArgs(argv)
|
|
198
|
+
if (a.help) {
|
|
199
|
+
console.log('用法:node scripts/license-gate.mjs --flow <flow> --cap <cap> [--vertical <v>] \\')
|
|
200
|
+
console.log(' [--mode 0|off|observe|enforce] [--lease-ok 0|1] [--log <jsonl>] [--now <ISO>] [--quiet] [--force] [--json]')
|
|
201
|
+
console.log('退出码 = 真实判定:0 可用 | 3 宽限(可用但告警)| 1 不可用 | 2 用法错误')
|
|
202
|
+
console.log(' 注意:停不停派由调用方按 mode 决定 —— observe 下无论返回什么码都必须继续派发。')
|
|
203
|
+
console.log(' --force / ARGENT_LICENSE_GATE_FORCE=1:即使租约可用也去读许可并留证(默认走快速路径)')
|
|
204
|
+
process.exit(0)
|
|
205
|
+
}
|
|
206
|
+
if (!argv.length) die('缺参数:至少需要 --flow 与 --cap')
|
|
207
|
+
if (a.now && !Number.isFinite(Date.parse(a.now))) die(`--now 不是可解析的时间:${a.now}`)
|
|
208
|
+
|
|
209
|
+
// 租约可用且非 force ⇒ 走快速路径:不读许可、不落判定(客户机每 5 分钟一轮,开销接近 0)。
|
|
210
|
+
// force 时把 leaseOk 视为 false ⇒ 一定要读一次许可(这也是 `--lease-ok 1 --force` 的意义)。
|
|
211
|
+
const leaseOk = a.leaseOk && !a.force
|
|
212
|
+
if (a.mode !== 'off' && !leaseOk) {
|
|
213
|
+
const { license, failedReason } = readLicense({
|
|
214
|
+
file: a.file, cap: a.cap, vertical: a.vertical, now: a.now, env: process.env,
|
|
215
|
+
})
|
|
216
|
+
var verdict = evaluate({ leaseOk, license, mode: a.mode, flow: a.flow, cap: a.cap, licenseFailedReason: failedReason })
|
|
217
|
+
} else {
|
|
218
|
+
var verdict = evaluate({ leaseOk, license: null, mode: a.mode, flow: a.flow, cap: a.cap })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 证据:**每次判定都留一行**(含"租约可用 ⇒ 放行、未读许可")——
|
|
222
|
+
// 观察期要能回答"闸门跑没跑、走的哪条路",缺了这条最常见的记录,文件里只剩异常样本。
|
|
223
|
+
// 增长由 appendEvidence 的轮转兜住(>5MB ⇒ .1,只留一份)。
|
|
224
|
+
appendEvidence(a.log, {
|
|
225
|
+
ts: new Date().toISOString(),
|
|
226
|
+
flow: a.flow,
|
|
227
|
+
cap: a.cap,
|
|
228
|
+
mode: a.mode,
|
|
229
|
+
leaseOk: a.leaseOk,
|
|
230
|
+
licenseState: verdict.licenseState,
|
|
231
|
+
allowed: verdict.allowed,
|
|
232
|
+
reason: verdict.reason,
|
|
233
|
+
decision: verdict.decision,
|
|
234
|
+
})
|
|
235
|
+
// 退出码 = 真实判定(见文件头):0 可用 | 3 宽限 | 1 不可用。
|
|
236
|
+
// **停不停派由调用方按 mode 决定**:observe 下无论这个码是什么都必须继续派发。
|
|
237
|
+
const verdictCode = !verdict.allowed ? 1 : (verdict.licenseState === 'grace' ? 3 : 0)
|
|
238
|
+
if (a.json) {
|
|
239
|
+
// 一行 JSON(stdout 只有它 ⇒ 调用方直接 JSON.parse)。`human` 一并带上,日志里仍有人话。
|
|
240
|
+
console.log(JSON.stringify({ ...verdict, mode: a.mode, flow: a.flow, cap: a.cap, vertical: a.vertical || null, log: a.log, exit: verdictCode }))
|
|
241
|
+
} else if (!a.quiet) console.log(verdict.human)
|
|
242
|
+
process.exit(verdictCode)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) main()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lip_backlog —— **只读对账**:lip 表里那些行,到底哪些是"人工已完成的历史数据"、
|
|
3
|
+
哪些是真待处理、哪些只是没到放行条件。
|
|
4
|
+
|
|
5
|
+
## 为什么需要它(2026-09-22 用户口径)
|
|
6
|
+
> "lip 表中有很多行都是之前客户运营人员手动完成的数据,所以要分辨清楚了。"
|
|
7
|
+
|
|
8
|
+
流程每轮只报"可派发 0|等人补字段 0",看不出**为什么**没有票:是表里没数据?还是数据都做完了?
|
|
9
|
+
还是运营没勾放行?这三件事的处置完全不同(补录 / 什么都不用做 / 催勾选)。
|
|
10
|
+
本命令把"路由到的全部行"逐行分类(**只读**,不写任何字段、不派发、不落卡),给出可核对的数字。
|
|
11
|
+
|
|
12
|
+
分类**按注册表规则判定**(`source.pick`:`equals` 门 + `excludeEquals`/`excludeAllPresent` 排除),
|
|
13
|
+
不硬编码列名 ⇒ 两个发票流程、FULL、日报都能用同一套口径。
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
python3 scripts/lip_backlog.py --flow xzh-invoice [--flow wenyao-invoice] [--examples 3] [--json]
|
|
17
|
+
退出码:0 正常;2 源读取失败
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import argparse, json, pathlib, subprocess, sys
|
|
21
|
+
|
|
22
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
23
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
24
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
25
|
+
DATA = pathlib.Path(resolve_data_dir()['dir'])
|
|
26
|
+
CASES = DATA / 'flow-cases'
|
|
27
|
+
PY = sys.executable
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cfg(flow: str, key: str):
|
|
31
|
+
p = subprocess.run([PY, str(HERE / 'flow_config.py'), '--flow', flow, '--get', key],
|
|
32
|
+
capture_output=True, text=True)
|
|
33
|
+
try:
|
|
34
|
+
return json.loads(p.stdout)
|
|
35
|
+
except Exception: # noqa: BLE001
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_flow_fields(flow: str) -> dict:
|
|
40
|
+
"""流程声明的栏位名(`source.fields.status/ready/store`)——判定"状态/放行"门要用。"""
|
|
41
|
+
return cfg(flow, 'source.fields') or {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_flow_rows(flow: str) -> tuple:
|
|
45
|
+
"""拿**路由后的全部行**(不应用 pick 门)⇒ (rows, err)。只读。"""
|
|
46
|
+
kind = cfg(flow, 'source.kind') or 'feishu-base'
|
|
47
|
+
p = subprocess.run([PY, str(HERE / 'flow_source.py'), '--flow', flow, '--source', kind,
|
|
48
|
+
'--no-pick-filter', '--json'], capture_output=True, text=True, timeout=600)
|
|
49
|
+
if not p.stdout.strip().startswith('{'):
|
|
50
|
+
return [], (p.stderr or p.stdout).strip()[:200]
|
|
51
|
+
doc = json.loads(p.stdout)
|
|
52
|
+
rows = []
|
|
53
|
+
for item in doc.get('cases') or []:
|
|
54
|
+
f = CASES / f"{flow}__{item.get('ticket')}.json"
|
|
55
|
+
fields = {}
|
|
56
|
+
if f.exists():
|
|
57
|
+
try:
|
|
58
|
+
fields = json.loads(f.read_text(encoding='utf-8')).get('fields') or {}
|
|
59
|
+
except Exception: # noqa: BLE001
|
|
60
|
+
fields = {}
|
|
61
|
+
rows.append({'ticket': item.get('ticket'), 'fields': fields, 'missing': item.get('missing') or []})
|
|
62
|
+
return rows, ''
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _truthy(v) -> bool:
|
|
66
|
+
return str(v).strip().lower() in ('true', '1', 'yes', '是', '已勾选', '✓')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def classify(rows: list, pick: dict, fields: dict = None) -> dict:
|
|
70
|
+
"""按注册表规则把每行归桶(纯函数,便于回归测试)。
|
|
71
|
+
|
|
72
|
+
镜像 `flow_source.apply_pick` 的判定顺序:排除 → 状态门 → equals 门 → 必填空 → 放行勾。
|
|
73
|
+
分桶口径(**只描述、不改数据**):
|
|
74
|
+
done 已完成(人工/既往):命中 excludeEquals / excludeAllPresent
|
|
75
|
+
gate_not_met 未到放行条件:状态门或 equals 门未过(等运营/等上游)
|
|
76
|
+
await_release 已识别但放行勾未勾(运营勾一下就进候选)
|
|
77
|
+
gate_met_missing 门内但缺字段(等人补)
|
|
78
|
+
gate_met_ready 真待处理(门内、字段齐)
|
|
79
|
+
"""
|
|
80
|
+
names = fields or {}
|
|
81
|
+
status_col = names.get('status') or ''
|
|
82
|
+
want_status = [str(x).strip().lower() for x in (pick.get('status') or [])]
|
|
83
|
+
equals = {str(k): str(v).strip().lower() for k, v in (pick.get('equals') or {}).items()}
|
|
84
|
+
ex_eq = {str(k): [str(x).strip().lower() for x in (v if isinstance(v, list) else [v])]
|
|
85
|
+
for k, v in (pick.get('excludeEquals') or {}).items()}
|
|
86
|
+
ex_all = [[str(c) for c in grp] for grp in (pick.get('excludeAllPresent') or []) if grp]
|
|
87
|
+
require_empty = list(pick.get('requireEmpty') or [])
|
|
88
|
+
release = pick.get('release') or {}
|
|
89
|
+
rel_col = release.get('field') or names.get('ready') or ''
|
|
90
|
+
rel_want = release.get('equals', True)
|
|
91
|
+
|
|
92
|
+
def truthy(v) -> bool:
|
|
93
|
+
return str(v).strip().lower() in ('true', '1', 'yes', '是', '已勾选', '✓')
|
|
94
|
+
|
|
95
|
+
buckets, reasons, examples, miss = {}, {}, {}, {}
|
|
96
|
+
for r in rows:
|
|
97
|
+
f = r.get('fields') or {}
|
|
98
|
+
|
|
99
|
+
def val(k):
|
|
100
|
+
return str(f.get(k) or '').strip()
|
|
101
|
+
|
|
102
|
+
if (ex_eq and any(val(k).lower() in v for k, v in ex_eq.items())) or \
|
|
103
|
+
(ex_all and any(all(val(c) for c in grp) for grp in ex_all)):
|
|
104
|
+
b, why = 'done', '人工/既往已完成'
|
|
105
|
+
else:
|
|
106
|
+
unmet = [k for k, want in equals.items() if val(k).lower() != want]
|
|
107
|
+
got = val(status_col).lower() if status_col else ''
|
|
108
|
+
if status_col and want_status and got not in want_status:
|
|
109
|
+
b, why = 'gate_not_met', f'{status_col}={val(status_col) or "空"} 未识别'
|
|
110
|
+
elif unmet:
|
|
111
|
+
b, why = 'gate_not_met', '、'.join(f'{k}≠{(pick.get("equals") or {}).get(k)}' for k in unmet)
|
|
112
|
+
elif require_empty and any(val(c) for c in require_empty):
|
|
113
|
+
b, why = 'gate_not_met', f'{"、".join(require_empty)} 非空'
|
|
114
|
+
elif rel_col and not (truthy(f.get(rel_col)) if rel_want is True
|
|
115
|
+
else val(rel_col).lower() == str(rel_want).strip().lower()):
|
|
116
|
+
b, why = 'await_release', f'{rel_col} 未勾'
|
|
117
|
+
elif r.get('missing'):
|
|
118
|
+
b, why = 'gate_met_missing', '缺 ' + '、'.join(str(x) for x in r['missing'][:6])
|
|
119
|
+
for m in r['missing']:
|
|
120
|
+
miss[m] = miss.get(m, 0) + 1
|
|
121
|
+
else:
|
|
122
|
+
b, why = 'gate_met_ready', '门内字段齐'
|
|
123
|
+
buckets[b] = buckets.get(b, 0) + 1
|
|
124
|
+
reasons[why] = reasons.get(why, 0) + 1
|
|
125
|
+
examples.setdefault(b, []).append(r.get('ticket'))
|
|
126
|
+
return {'total': len(rows), 'buckets': buckets, 'reasons': reasons,
|
|
127
|
+
'missingInGate': miss, 'examples': examples}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
LABELS = {'done': '已完成(人工/既往,不该动)', 'gate_not_met': '未到放行条件(等运营/等上游)',
|
|
131
|
+
'await_release': '已识别但放行勾未勾', 'gate_met_ready': '真待处理(门内、字段齐)',
|
|
132
|
+
'gate_met_missing': '门内但缺字段(等人补)'}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv=None) -> int:
|
|
136
|
+
ap = argparse.ArgumentParser(description='lip 表只读对账:分辨人工已完成 vs 真待处理')
|
|
137
|
+
ap.add_argument('--flow', action='append', required=True)
|
|
138
|
+
ap.add_argument('--examples', type=int, default=3)
|
|
139
|
+
ap.add_argument('--json', action='store_true')
|
|
140
|
+
a = ap.parse_args(argv)
|
|
141
|
+
out = {}
|
|
142
|
+
for flow in a.flow:
|
|
143
|
+
pick = cfg(flow, 'source.pick') or {}
|
|
144
|
+
rows, err = read_flow_rows(flow)
|
|
145
|
+
if err:
|
|
146
|
+
print(f"❌ {flow}:源读取失败:{err}", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
res = classify(rows, pick, read_flow_fields(flow))
|
|
149
|
+
out[flow] = res
|
|
150
|
+
if a.json:
|
|
151
|
+
continue
|
|
152
|
+
print(f"\n── {flow}:路由到 {res['total']} 行")
|
|
153
|
+
for b in ('done', 'gate_not_met', 'await_release', 'gate_met_ready', 'gate_met_missing'):
|
|
154
|
+
n = res['buckets'].get(b, 0)
|
|
155
|
+
if not n:
|
|
156
|
+
continue
|
|
157
|
+
print(f" {LABELS[b]:28} {n:>4} 张"
|
|
158
|
+
+ (f"|例:{', '.join(str(x) for x in res['examples'][b][:a.examples])}" if a.examples else ''))
|
|
159
|
+
if res['reasons']:
|
|
160
|
+
print(" 原因分布:" + "|".join(f"{k} {n} 张"
|
|
161
|
+
for k, n in sorted(res['reasons'].items(), key=lambda x: -x[1])[:6]))
|
|
162
|
+
if res['missingInGate']:
|
|
163
|
+
print(" 门内缺字段:" + "|".join(f"{k} {n} 张"
|
|
164
|
+
for k, n in sorted(res['missingInGate'].items(), key=lambda x: -x[1])))
|
|
165
|
+
if a.json:
|
|
166
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == '__main__':
|
|
171
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lip 表目标与字段 id 的**唯一解析入口**(2026-09-20 迭代1-残留项)。
|
|
3
|
+
|
|
4
|
+
## 为什么要有它
|
|
5
|
+
`wenyao_run.py` 里曾写死 `BASE/TABLE/PROFILE` + **21 条字段 id**(`fldV3Q64fj`…),
|
|
6
|
+
`invoice_lip_runner.py` 里还残留 1 条。后果:
|
|
7
|
+
· **换客户/换表 = 改代码**(与"能力层 + 集成层"的目标相反);
|
|
8
|
+
· 字段 id 是**表相关**的:客户另建一张同名字段的表,id 全不同 ⇒ 写回/上传会打到错的列;
|
|
9
|
+
· 与 FULL/发票流程"表 id 走 env"的口径不一致(同一仓库两种写法)。
|
|
10
|
+
|
|
11
|
+
## 口径
|
|
12
|
+
- **表/身份**:经 `flow_config.py`(唯一配置读取点)解析 ⇒ 走部署 env(`fulfillment.env` 的
|
|
13
|
+
`FULL_BASE_TOKEN/FULL_TABLE_ID/FULL_LARK_PROFILE`),解析失败**响亮失败**(不猜表)。
|
|
14
|
+
- **字段 id**:运行时用 `lark-cli base +field-list` 拉一次,建 `名 → id` 映射并进程内缓存;
|
|
15
|
+
查不到的字段名**原样返回**(lark-cli 允许按名传,但按名解析偶发 `20008` ⇒ 优先用 id)。
|
|
16
|
+
|
|
17
|
+
用法:
|
|
18
|
+
import lip_table
|
|
19
|
+
t = lip_table.target("wenyao-invoice") # {'flow','base','table','profile'}
|
|
20
|
+
fid = lip_table.field_id("发票文件", t) # 'fldvmra3Rp'(运行时解析,不写死)
|
|
21
|
+
names = lip_table.field_names(t) # 反向映射(id → 名),排查用
|
|
22
|
+
lip_table.clear_cache() # 测试用
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import pathlib
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
33
|
+
|
|
34
|
+
# 进程内缓存:{ (flow): {'target': {...}, 'by_name': {...}, 'by_id': {...}} }
|
|
35
|
+
_CACHE: dict = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def target(flow: str = "wenyao-invoice", *, base: str = "", table: str = "", profile: str = "") -> dict:
|
|
39
|
+
"""解析流程的表/身份(走 flow_config ⇒ 部署 env)。显式传入的参数优先(测试/排障)。"""
|
|
40
|
+
if base and table and profile:
|
|
41
|
+
return {"flow": flow, "base": base, "table": table, "profile": profile}
|
|
42
|
+
key = f"target::{flow}"
|
|
43
|
+
if key in _CACHE:
|
|
44
|
+
return dict(_CACHE[key])
|
|
45
|
+
sys.path.insert(0, str(HERE))
|
|
46
|
+
import flow_config # noqa: PLC0415 同目录模块,延迟导入避免循环
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
cfg = flow_config.load_config(flow) # strict:缺 env 直接 exit 2
|
|
50
|
+
except SystemExit as exc: # pragma: no cover - 由 flow_config 打印原因
|
|
51
|
+
raise RuntimeError(f"流程 {flow} 配置不可解析(缺部署 env?):{exc}") from exc
|
|
52
|
+
src = cfg.get("source") or {}
|
|
53
|
+
got = {k: str(src.get(k) or "") for k in ("base", "table", "identity")}
|
|
54
|
+
if not (got["base"] and got["table"] and got["identity"]):
|
|
55
|
+
raise RuntimeError(f"流程 {flow} 未解析出 base/table/identity(检查注册表 source.*Env 与部署 env)")
|
|
56
|
+
out = {"flow": flow, "base": got["base"], "table": got["table"], "profile": got["identity"]}
|
|
57
|
+
_CACHE[key] = dict(out)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def field_map(t: dict | None = None, flow: str = "wenyao-invoice") -> dict:
|
|
62
|
+
"""运行时拉 `+field-list` → {'by_name': {名: id}, 'by_id': {id: 名}}(进程内缓存)。
|
|
63
|
+
|
|
64
|
+
失败时抛 RuntimeError(**不静默**):字段 id 拿不到就等于写回目标不确定,
|
|
65
|
+
宁可不跑,也不要用"按名写"去赌偶发的 20008。
|
|
66
|
+
"""
|
|
67
|
+
t = t or target(flow)
|
|
68
|
+
key = f"fields::{t['flow']}::{t['base']}::{t['table']}"
|
|
69
|
+
if key in _CACHE:
|
|
70
|
+
return _CACHE[key]
|
|
71
|
+
cmd = ["lark-cli", "--profile", t["profile"], "base", "+field-list",
|
|
72
|
+
"--base-token", t["base"], "--table-id", t["table"], "--as", "bot", "--format", "json"]
|
|
73
|
+
try:
|
|
74
|
+
r = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=120)
|
|
75
|
+
d = json.loads(r.stdout)
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
77
|
+
raise RuntimeError(f"拉取字段清单失败({t['flow']}):{exc}") from exc
|
|
78
|
+
fields = ((d.get("data") or {}).get("fields")) or []
|
|
79
|
+
by_name, by_id = {}, {}
|
|
80
|
+
for f in fields:
|
|
81
|
+
nm, fid = str(f.get("name") or ""), str(f.get("id") or "")
|
|
82
|
+
if nm and fid:
|
|
83
|
+
by_name[nm] = fid
|
|
84
|
+
by_id[fid] = nm
|
|
85
|
+
if not by_name:
|
|
86
|
+
raise RuntimeError(f"字段清单为空({t['flow']} {t['table']})——检查该 bot 是否有表权限")
|
|
87
|
+
got = {"by_name": by_name, "by_id": by_id}
|
|
88
|
+
_CACHE[key] = got
|
|
89
|
+
return got
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def field_id(name: str, t: dict | None = None, flow: str = "wenyao-invoice") -> str:
|
|
93
|
+
"""字段名 → id;查不到就**原样返回**(lark-cli 允许按名,但优先 id,避免偶发 20008)。"""
|
|
94
|
+
if not name:
|
|
95
|
+
return name
|
|
96
|
+
try:
|
|
97
|
+
return field_map(t, flow)["by_name"].get(name, name)
|
|
98
|
+
except RuntimeError:
|
|
99
|
+
return name # 拉不到清单时降级为按名(调用方的读回验收会暴露问题)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def field_names(t: dict | None = None, flow: str = "wenyao-invoice") -> dict:
|
|
103
|
+
"""id → 名(排查用)。"""
|
|
104
|
+
return dict(field_map(t, flow)["by_id"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def clear_cache() -> None:
|
|
108
|
+
_CACHE.clear()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv=None) -> int:
|
|
112
|
+
import argparse
|
|
113
|
+
ap = argparse.ArgumentParser(description="lip 表目标与字段 id 解析(唯一入口)")
|
|
114
|
+
ap.add_argument("--flow", default="wenyao-invoice")
|
|
115
|
+
ap.add_argument("--field", help="查某个字段名的 id")
|
|
116
|
+
ap.add_argument("--json", action="store_true")
|
|
117
|
+
a = ap.parse_args(argv)
|
|
118
|
+
try:
|
|
119
|
+
t = target(a.flow)
|
|
120
|
+
if a.field:
|
|
121
|
+
fid = field_map(t, a.flow)["by_name"].get(a.field)
|
|
122
|
+
print(json.dumps({"flow": a.flow, "field": a.field, "id": fid}, ensure_ascii=False) if a.json
|
|
123
|
+
else f" {a.field} → {fid or '(表里没有这个字段名)'}")
|
|
124
|
+
return 0 if fid else 1
|
|
125
|
+
fm = field_map(t, a.flow)
|
|
126
|
+
if a.json:
|
|
127
|
+
print(json.dumps({"target": t, "field_count": len(fm["by_name"]), "by_name": fm["by_name"]},
|
|
128
|
+
ensure_ascii=False, indent=2))
|
|
129
|
+
else:
|
|
130
|
+
print(f" 流程 {a.flow}|base {t['base'][:12]}…|table {t['table']}|身份 {t['profile']}"
|
|
131
|
+
f"|字段 {len(fm['by_name'])} 个(运行时解析,无硬编码)")
|
|
132
|
+
return 0
|
|
133
|
+
except RuntimeError as exc:
|
|
134
|
+
print(f"❌ {exc}", file=sys.stderr)
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
raise SystemExit(main())
|
|
140
|
+
|
|
141
|
+
# ── 全表读取(2026-09-23 事故:分页漏行)──────────────────────────────
|
|
142
|
+
# 事故:`+record-list --format json` 的 `--limit` **上限只有 200**,而表有 531 行 ⇒
|
|
143
|
+
# 任何"只读前 200 行"的对账/扫描都会**静默漏行**(用户指出"就绪=真 ∧ 待执行"的 6 条没被看到,
|
|
144
|
+
# 就是因为我看的是前 200 行)。正确读法:
|
|
145
|
+
# · `--format ndjson --limit 2000`:一次拿全表(ndjson 上限 2000)
|
|
146
|
+
# · `--output` 必须是**相对路径**(绝对路径被 CLI 拒:unsafe file path)
|
|
147
|
+
# · ndjson 行是**扁平**结构:字段在顶层 + `record_id`(不是嵌套 fields)
|
|
148
|
+
# · 超过 2000 行才需要分页(本表 531 行,一次到位)
|
|
149
|
+
def all_rows(profile: str = None, base: str = None, table: str = None, limit: int = 2000,
|
|
150
|
+
filter_json: str = "") -> list:
|
|
151
|
+
"""读**全表**(ndjson)→ [{字段…, _id}]。绝不能退回 json 格式(会截断到 200 行)。"""
|
|
152
|
+
import json as _json, os as _os, subprocess as _sp, tempfile as _tf
|
|
153
|
+
prof = profile or _os.environ.get("LARK_PROFILE") or _os.environ.get("FULL_LARK_PROFILE") or "lip-customer"
|
|
154
|
+
b = base or _os.environ.get("FULL_BASE_TOKEN") or ""
|
|
155
|
+
t = table or _os.environ.get("FULL_TABLE_ID") or ""
|
|
156
|
+
tmpdir = _tf.mkdtemp(prefix="lip-rows-")
|
|
157
|
+
out = "rows.ndjson"
|
|
158
|
+
cmd = ["lark-cli", "--profile", prof, "base", "+record-list", "--base-token", b,
|
|
159
|
+
"--table-id", t, "--as", "bot", "--format", "ndjson",
|
|
160
|
+
"--limit", str(limit), "--output", out]
|
|
161
|
+
if filter_json:
|
|
162
|
+
cmd += ["--filter-json", filter_json] # 服务端过滤(与 ndjson 兼容,省流量)
|
|
163
|
+
r = _sp.run(cmd, capture_output=True, text=True, timeout=300, cwd=tmpdir)
|
|
164
|
+
path = _os.path.join(tmpdir, out)
|
|
165
|
+
rows = []
|
|
166
|
+
try:
|
|
167
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
168
|
+
for line in fh:
|
|
169
|
+
line = line.strip()
|
|
170
|
+
if not line:
|
|
171
|
+
continue
|
|
172
|
+
try:
|
|
173
|
+
d = _json.loads(line)
|
|
174
|
+
except Exception:
|
|
175
|
+
continue
|
|
176
|
+
if isinstance(d, dict):
|
|
177
|
+
d = dict(d)
|
|
178
|
+
d["_id"] = d.get("record_id") or d.get("_id")
|
|
179
|
+
rows.append(d)
|
|
180
|
+
except OSError:
|
|
181
|
+
pass
|
|
182
|
+
if not rows:
|
|
183
|
+
raise RuntimeError(f"全表读取失败({len(rows)} 行;stderr={(r.stderr or '')[:150]})——"
|
|
184
|
+
f"不要退回 --format json(会被截断到 200 行)")
|
|
185
|
+
return rows
|