@whyshu/argent-engine 0.0.0-stage → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""发票流程·缺口回填「退回运营」(用户口径 2026-09-17)。
|
|
3
|
+
|
|
4
|
+
规则(两个发票流程一致:文鳐 + 鑫中航,同一张 lip 表):
|
|
5
|
+
· **只有需要运营填的字段缺口**(owner 不含「系统」)才动作:
|
|
6
|
+
① 写 `发票错误信息` = **结论+动作**的运营文案(`flow_failure.ops_message()` 产出,
|
|
7
|
+
⛔ 不在本文件里另写一份;2026-10-09 缺陷 B 收口);
|
|
8
|
+
② 把 `发票就绪` 置 **false**(取消放行 —— 运营补齐后需自己重勾,那是他的确认动作)。
|
|
9
|
+
· **我方/系统类缺口**:`发票错误信息` 是**给运营看的**字段 ⇒ **不写**;我方缺口自行解决
|
|
10
|
+
(记台账/发卡片/补录),绝不污染运营那一列,也不取消勾选。
|
|
11
|
+
· **同源同文**(2026-10-09):表字段那串 == 台账 `blocked.detail` 那串(逐字相同,
|
|
12
|
+
`blocked.error` 放**规范原因**供分类器/取数口用)——⛔ 不许两处各写一套。
|
|
13
|
+
|
|
14
|
+
守卫(写生产表):
|
|
15
|
+
① 字段白名单:只许 `发票错误信息` / `发票就绪`;
|
|
16
|
+
② **单向**:`发票就绪` 只允许置 false —— 勾选权永远归运营,流程永不自己勾上;
|
|
17
|
+
③ 记录白名单:必须显式 `--case`/`--record`,一次一条;
|
|
18
|
+
④ 前提校验:该行必须是已开启的行(`FULL状态=已完成 ∧ 发票就绪=true`),否则不动;
|
|
19
|
+
⑤ 写前整行备份 + 写后读回 + 审计 JSONL + 台账 `blocked`;
|
|
20
|
+
⑥ 默认 dry-run,真写要 `--yes`。
|
|
21
|
+
|
|
22
|
+
用法:
|
|
23
|
+
python3 scripts/flow_return_to_ops.py --flow wenyao-invoice --case wenyao-invoice__rec1 # 预览
|
|
24
|
+
python3 scripts/flow_return_to_ops.py --flow wenyao-invoice --case wenyao-invoice__rec1 --yes # 真写
|
|
25
|
+
# 测试/离线:--fields-from-json <行字段JSON>(跳过读表)
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import datetime
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import pathlib
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
38
|
+
sys.path.insert(0, str(HERE))
|
|
39
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
40
|
+
import flow_config # noqa: E402 唯一配置读取点
|
|
41
|
+
import flow_failure as F # noqa: E402 失败/缺口文案的**唯一来源**(结论+动作,与台账同源同文)
|
|
42
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
43
|
+
|
|
44
|
+
DATA_DIR = pathlib.Path(resolve_data_dir()['dir'])
|
|
45
|
+
AUDIT = DATA_DIR / "flow-return-audit.jsonl"
|
|
46
|
+
BACKUP_DIR = DATA_DIR / "flow-return-backup"
|
|
47
|
+
LEDGER = DATA_DIR / "flow-ledger.jsonl"
|
|
48
|
+
WRITE_FIELDS = ("发票错误信息", "发票就绪")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def field_ids(profile: str, base: str, table: str) -> dict:
|
|
52
|
+
p = subprocess.run(["lark-cli", "--profile", profile, "base", "+field-list",
|
|
53
|
+
"--base-token", base, "--table-id", table, "--as", "bot", "--format", "json"],
|
|
54
|
+
capture_output=True, text=True,
|
|
55
|
+
timeout=int(os.environ.get('ARGENT_CMD_TIMEOUT') or 120))
|
|
56
|
+
if p.returncode != 0:
|
|
57
|
+
raise SystemExit(f"❌ 取字段表失败:{(p.stderr or p.stdout).strip()[:200]}")
|
|
58
|
+
d = json.loads(p.stdout or "{}")
|
|
59
|
+
return {str(f.get("field_name") or f.get("name")): str(f.get("field_id") or f.get("id"))
|
|
60
|
+
for f in (((d.get("data") or {}).get("fields")) or [])}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_row(profile: str, base: str, table: str, record: str) -> dict:
|
|
64
|
+
p = subprocess.run(["lark-cli", "--profile", profile, "base", "+record-get", "--base-token", base,
|
|
65
|
+
"--table-id", table, "--record-id", record, "--as", "bot", "--format", "json"],
|
|
66
|
+
capture_output=True, text=True,
|
|
67
|
+
timeout=int(os.environ.get('ARGENT_CMD_TIMEOUT') or 120))
|
|
68
|
+
if p.returncode != 0:
|
|
69
|
+
raise SystemExit(f"❌ 读记录失败:{(p.stderr or p.stdout).strip()[:200]}")
|
|
70
|
+
data = (json.loads(p.stdout or "{}").get("data") or {})
|
|
71
|
+
for c in (((data.get("record") or {}).get("fields")), data.get("fields")):
|
|
72
|
+
if isinstance(c, dict) and c:
|
|
73
|
+
return c
|
|
74
|
+
vals, names = data.get("data"), data.get("fields")
|
|
75
|
+
if isinstance(vals, list) and vals and isinstance(vals[0], list) and isinstance(names, list):
|
|
76
|
+
return {str(n): v for n, v in zip(names, vals[0])}
|
|
77
|
+
raise SystemExit(f"❌ 读不到记录 {record} 的字段(形状异常:{list(data)[:6]})")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def human_gaps(flow: str, fields: dict) -> list[str]:
|
|
81
|
+
"""算出**需要运营填**的缺口(owner 不含「系统」)。复用 ml_schema_check 的判据,避免第二套实现。"""
|
|
82
|
+
req = flow_config.load_config(flow).get("required") or []
|
|
83
|
+
owner = {str(r.get("name")): str(r.get("owner") or "") for r in req}
|
|
84
|
+
# ⚠️ ml_schema_check 的 --fields-from-json 只接受**文件路径**(不支持 `-`)——
|
|
85
|
+
# 第一版传 `-` 导致子进程失败、异常被吞 ⇒ 缺口恒为空("静默空"老毛病)⇒ 落临时文件 + 失败要响亮。
|
|
86
|
+
import tempfile
|
|
87
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as tf:
|
|
88
|
+
json.dump(fields, tf, ensure_ascii=False)
|
|
89
|
+
tmp = tf.name
|
|
90
|
+
try:
|
|
91
|
+
p = subprocess.run([sys.executable, str(HERE / "ml_schema_check.py"), "--flow", flow,
|
|
92
|
+
"--fields-from-json", tmp, "--json"], capture_output=True, text=True)
|
|
93
|
+
finally:
|
|
94
|
+
pathlib.Path(tmp).unlink(missing_ok=True)
|
|
95
|
+
if not (p.stdout or "").strip().startswith("{"):
|
|
96
|
+
raise SystemExit(f"❌ 缺口检查失败(不当作无缺口):{(p.stderr or p.stdout).strip()[:200]}")
|
|
97
|
+
missing = [str(m.get("name")) for m in (json.loads(p.stdout).get("missing") or [])]
|
|
98
|
+
return [m for m in missing if "系统" not in owner.get(m, "")]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def truthy(v) -> bool:
|
|
102
|
+
if isinstance(v, list):
|
|
103
|
+
return truthy(v[0]) if v else False
|
|
104
|
+
if isinstance(v, bool):
|
|
105
|
+
return v
|
|
106
|
+
return str(v or "").strip().lower() not in ("", "false", "0", "no", "否")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def one(v):
|
|
110
|
+
return v[0] if isinstance(v, list) and v else v
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv=None) -> int:
|
|
114
|
+
ap = argparse.ArgumentParser(description="发票缺口回填:写「发票错误信息」+ 取消「发票就绪」(仅运营缺口)")
|
|
115
|
+
ap.add_argument("--flow", required=True)
|
|
116
|
+
ap.add_argument("--case", help="台账 case id(<flow>__<record_id>)")
|
|
117
|
+
ap.add_argument("--record", help="record_id")
|
|
118
|
+
ap.add_argument("--fields-from-json", help="离线:直接给该行字段 JSON(测试用,跳过读表)")
|
|
119
|
+
ap.add_argument("--yes", action="store_true", help="真写(默认 dry-run)")
|
|
120
|
+
a = ap.parse_args(argv)
|
|
121
|
+
|
|
122
|
+
cfg = flow_config.load_config(a.flow)
|
|
123
|
+
src = cfg.get("source") or {}
|
|
124
|
+
base, table, profile = str(src.get("base") or ""), str(src.get("table") or ""), str(src.get("identity") or "")
|
|
125
|
+
record = a.record or (a.case.split("__", 1)[1] if a.case and "__" in a.case else "")
|
|
126
|
+
if not record:
|
|
127
|
+
print("❌ 需要 --case <flow>__<record_id> 或 --record <record_id>(记录白名单,一次一条)", file=sys.stderr)
|
|
128
|
+
return 2
|
|
129
|
+
|
|
130
|
+
if a.fields_from_json:
|
|
131
|
+
row = json.loads(pathlib.Path(a.fields_from_json).read_text(encoding="utf-8"))
|
|
132
|
+
fmap = {k: k for k in WRITE_FIELDS} # 离线模式按字段名写(仅预览用)
|
|
133
|
+
else:
|
|
134
|
+
fmap = field_ids(profile, base, table)
|
|
135
|
+
row = read_row(profile, base, table, record)
|
|
136
|
+
|
|
137
|
+
gate_full, gate_ready = one(row.get("FULL状态")), one(row.get("发票就绪"))
|
|
138
|
+
gaps = human_gaps(a.flow, row)
|
|
139
|
+
print("── 现场 ──")
|
|
140
|
+
print(f" 流程/表 : {a.flow}|{table or '(离线)'}")
|
|
141
|
+
print(f" 记录 : {record}")
|
|
142
|
+
print(f" 开启前提: FULL状态={gate_full!r}|发票就绪={gate_ready!r}")
|
|
143
|
+
print(f" 运营缺口: {gaps or '(无)'}")
|
|
144
|
+
|
|
145
|
+
if not gaps:
|
|
146
|
+
print("\n ✅ 无运营缺口 ⇒ **不写任何字段**(`发票错误信息` 是给运营看的;我方缺口自行解决)")
|
|
147
|
+
return 0
|
|
148
|
+
if not a.fields_from_json and not (str(gate_full).strip() == "已完成" and truthy(gate_ready)):
|
|
149
|
+
print(f"\n ⚠️ 该行未处于已开启状态(需 FULL状态=已完成 ∧ 发票就绪=true)⇒ 不回填(避免误改未开启的行)")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
msg = F.ops_message(a.flow, f"缺 {'、'.join(gaps)}", missing=gaps)
|
|
153
|
+
patch = {fmap.get("发票错误信息", "发票错误信息"): msg,
|
|
154
|
+
fmap.get("发票就绪", "发票就绪"): False} # 单向:只允许 false
|
|
155
|
+
print("── 计划(退回运营)──")
|
|
156
|
+
for k, v in patch.items():
|
|
157
|
+
print(f" {k} → {v!r}")
|
|
158
|
+
if not a.yes:
|
|
159
|
+
print("\n[dry-run] 未写入。加 --yes 才落库。")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
at = datetime.datetime.now().isoformat(timespec="seconds")
|
|
164
|
+
bk = BACKUP_DIR / f"{a.flow}__{record}__{at.replace(':', '')}.json"
|
|
165
|
+
bk.write_text(json.dumps({"flow": a.flow, "record": record, "at": at, "fields": row},
|
|
166
|
+
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
167
|
+
p = subprocess.run(["lark-cli", "--profile", profile, "base", "+record-batch-update", "--base-token", base,
|
|
168
|
+
"--table-id", table, "--json",
|
|
169
|
+
json.dumps({"update_records": {record: patch}}, ensure_ascii=False),
|
|
170
|
+
"--as", "bot", "--format", "json"], capture_output=True, text=True,
|
|
171
|
+
timeout=int(os.environ.get('ARGENT_CMD_TIMEOUT') or 120))
|
|
172
|
+
if p.returncode != 0:
|
|
173
|
+
print(f"❌ 写入失败:{(p.stderr or p.stdout).strip()[:250]}", file=sys.stderr)
|
|
174
|
+
return 3
|
|
175
|
+
after = read_row(profile, base, table, record)
|
|
176
|
+
ok = (not truthy(one(after.get("发票就绪")))) and str(one(after.get("发票错误信息")) or "").strip() != ""
|
|
177
|
+
with AUDIT.open("a", encoding="utf-8") as fh:
|
|
178
|
+
fh.write(json.dumps({"at": at, "flow": a.flow, "record": record, "gaps": gaps,
|
|
179
|
+
"wrote": {"发票错误信息": msg, "发票就绪": False}, "backup": str(bk), "ok": bool(ok)},
|
|
180
|
+
ensure_ascii=False) + "\n")
|
|
181
|
+
subprocess.run([sys.executable, str(HERE / "flow_ledger.py"), "--case-id", a.case or f"{a.flow}__{record}",
|
|
182
|
+
"--event", "blocked", "--by", "flow_return_to_ops",
|
|
183
|
+
"--error", f"缺 {'、'.join(gaps)}", # 规范原因(分类器/取数口用)
|
|
184
|
+
"--detail", msg], check=False) # **与表里那一串逐字相同**(同源同文)
|
|
185
|
+
print(f" ✅ 已回填并读回{'一致' if ok else '**不一致**'}|备份 {bk.name}|审计 flow-return-audit.jsonl")
|
|
186
|
+
print(f" 表「发票错误信息」= 台账 blocked.detail = {msg}")
|
|
187
|
+
return 0 if ok else 4
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
sys.exit(main())
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""flow_source —— 流程「源适配器」:把**任意来源**的字段归一化成 case,并给出字段缺口。
|
|
3
|
+
|
|
4
|
+
设计(见 docs/argent-architecture.md「流程引擎」+ docs/流程能力分层与迭代计划.md):要求侧固定(flow-requirements.json),
|
|
5
|
+
供给侧可插拔(本脚本的 --source),两者之间用 **case** 对接,下游只消费 case。
|
|
6
|
+
|
|
7
|
+
来源:
|
|
8
|
+
cli-json 字段来自 JSON 文件/stdin(人工、夹具、其它系统导出,**单张票**)
|
|
9
|
+
cases-json **批量**待处理清单(`[{ticketKey, fields}]`;日报这类"单元不在表里"的流程用)
|
|
10
|
+
conversation 会话里由用户/角色提供的字段(同为 JSON,但记录 providedBy=用户)
|
|
11
|
+
feishu-base 只读拉取飞书表行(**窄投影**,按表头名映射;身份/表来自流程声明或参数)
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
python3 scripts/flow_source.py --flow wenyao-invoice --source cli-json \
|
|
15
|
+
--ticket 76637122 --fields-from-json /tmp/row.json --provided-by 运营 [--json]
|
|
16
|
+
python3 scripts/flow_source.py --flow wenyao-invoice --source feishu-base \
|
|
17
|
+
--record recXXXX [--field-ids 货件号,店铺,HS编码] [--out-dir DIR] [--json]
|
|
18
|
+
|
|
19
|
+
退出码:0 = 建案且字段齐备;1 = 建案但**存在缺口**(据此落卡/提示补录);2 = 参数或源读取失败
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
import argparse, json, os, pathlib, subprocess, sys, tempfile, time
|
|
23
|
+
|
|
24
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
25
|
+
REQ = HERE / 'flow-requirements.json'
|
|
26
|
+
sys.path.insert(0, str(HERE))
|
|
27
|
+
from flow_config import load_env_file # noqa: E402 部署环境文件(唯一实现)
|
|
28
|
+
try:
|
|
29
|
+
from flow_registry import consumer_doc as _registry_doc
|
|
30
|
+
except Exception: # noqa: BLE001 —— 半坏机器上退回单文件
|
|
31
|
+
def _registry_doc(path=None):
|
|
32
|
+
_p = pathlib.Path(path) if path else REQ
|
|
33
|
+
return json.loads(_p.read_text(encoding="utf-8"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
FLOW_CASE = HERE / 'flow_case.py'
|
|
37
|
+
SCHEMA_CHECK = HERE / 'ml_schema_check.py'
|
|
38
|
+
|
|
39
|
+
# ── P4:实现已按端口拆出去(纯搬家,行为不变)─────────────────────────
|
|
40
|
+
# 共享原语从这里**再导出**:`flow_triage` 的 `_truthy`、`flow_retry` 的 `load_allowed_stores`、
|
|
41
|
+
# 测试里的 `ticket_key`/`wanted_pick_columns`/`apply_pick` 都还按老路径 import。
|
|
42
|
+
sys.path.insert(0, str(HERE))
|
|
43
|
+
from flow_sources.common import ( # noqa: E402,F401 (F401:再导出,供历史调用点)
|
|
44
|
+
CMD_RETRIES, CMD_TIMEOUT_SEC, LAST_NARROW, LAST_PICK_NOTES, PHASES, TRANSIENT_PAT,
|
|
45
|
+
_flat, _has_value, _is_transient, _truthy, apply_pick, load_allowed_stores,
|
|
46
|
+
read_cases_batch, read_cases_notes, read_fields, required_projection, run,
|
|
47
|
+
ticket_key, wanted_pick_columns, wanted_upstream_columns,
|
|
48
|
+
)
|
|
49
|
+
from flow_sources import kinds as _source_kinds, load as _load_source # noqa: E402
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── Source 端口表(P4 第一步,2026-09-29)─────────────────────────
|
|
54
|
+
# 为什么要有它:`--source` 此前只是**命令行参数**,业务线的数据面无法在注册表里声明,
|
|
55
|
+
# 也无法被门禁校验。现在:**注册表 `source.kind` 声明该流程的数据面**,本表声明引擎实现了哪些。
|
|
56
|
+
# kind :注册表里的取值
|
|
57
|
+
# needs_args :必须由命令行补齐的输入(cli-json/conversation 要 --ticket + --fields-from-json)
|
|
58
|
+
# registry_driven:是否支持"完全按注册表声明跑"(= 定时轮能用),否则只作手工/夹具通道
|
|
59
|
+
SOURCES: dict = {
|
|
60
|
+
'feishu-base': {'needs_args': (), 'registry_driven': True, 'module': 'flow_sources.feishu_base',
|
|
61
|
+
'note': '飞书 Base 行(窄投影 + 表头名映射)—— 当前唯一的定时轮数据面'},
|
|
62
|
+
'cases-json': {'needs_args': ('--from-json 或 source.file',), 'registry_driven': True,
|
|
63
|
+
'module': 'flow_sources.cases_json',
|
|
64
|
+
'note': '批量清单文件(生成器产出的待处理单元;日报走这条)'},
|
|
65
|
+
'cli-json': {'needs_args': ('--ticket', '--fields-from-json'), 'registry_driven': False, 'inline': True,
|
|
66
|
+
'note': '手工/外部喂字段(会话与排障用)'},
|
|
67
|
+
'conversation': {'needs_args': ('--ticket', '--fields-from-json'), 'registry_driven': False, 'inline': True,
|
|
68
|
+
'note': '会话来源(同 cli-json,provenance 记为 conversation)'},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def source_kinds() -> list:
|
|
73
|
+
"""已实现的 source kind(供门禁/文档/CLI 消费)。"""
|
|
74
|
+
return sorted(SOURCES)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _assert_kinds_aligned() -> None:
|
|
78
|
+
"""**声明了 module 的 kind 必须有对应模块**(两处漂移会让"声明了但没实现"溜过去)。
|
|
79
|
+
|
|
80
|
+
注意区分两件事(P4 迁移中的真实状态):
|
|
81
|
+
· `SOURCES` = 引擎**已实现**的数据面(含三个仍内联在 main() 里的手工/夹具通道);
|
|
82
|
+
· `MODULES` = 已抽成端口模块的实现(目前只有 feishu-base)。
|
|
83
|
+
等三个内联通道也抽成模块后,这里的判据收紧为"两者相等"。
|
|
84
|
+
"""
|
|
85
|
+
claim_module = {k for k, v in SOURCES.items() if v.get('module')}
|
|
86
|
+
implemented = set(_source_kinds())
|
|
87
|
+
# 每个已实现的数据面必须**要么**登记了 module(端口实现),**要么**显式标 inline(引擎内联:手工/夹具通道)
|
|
88
|
+
inline = {k for k, v in SOURCES.items() if v.get('inline')}
|
|
89
|
+
covered = claim_module | inline
|
|
90
|
+
if covered != set(SOURCES):
|
|
91
|
+
raise SystemExit(f'source 端口表有未归类项:{sorted(set(SOURCES) - covered)}'
|
|
92
|
+
f'(要么给它 module,要么标 inline —— 别让它含糊地"存在但没交代")')
|
|
93
|
+
if claim_module != implemented:
|
|
94
|
+
raise SystemExit(f'source 端口表不一致:声明了 module 但没实现={sorted(claim_module - implemented)};'
|
|
95
|
+
f'实现了但没登记={sorted(implemented - claim_module)}')
|
|
96
|
+
# 逐个校验契约(KIND 一致 + 恰好一个 pull/pull_cases)——半迁移的模块启动即失败
|
|
97
|
+
from flow_sources import validate_module # noqa: PLC0415
|
|
98
|
+
for k in sorted(implemented):
|
|
99
|
+
validate_module(k)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def declared_kind(spec: dict) -> str:
|
|
103
|
+
"""该流程在注册表里声明的数据面(`source.kind`),缺省 feishu-base(历史行为)。"""
|
|
104
|
+
return str((spec.get('source') or {}).get('kind') or 'feishu-base')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_flow(flow_id: str, registry: str | None = None) -> dict:
|
|
108
|
+
req = pathlib.Path(registry) if registry else REQ
|
|
109
|
+
# ⚠️ 两个坑(2026-09-29 P2a 实测):
|
|
110
|
+
# ① 只有**调用方显式给了 registry** 才按单文件读;否则必须走合并
|
|
111
|
+
# (`req` 恒为真 ⇒ 原写法把合并退化成"只读 base",业务线流程全部"未知");
|
|
112
|
+
# ② 旧 id(`full`)要经别名解析成规范 id(`<vertical>.<flow>`)后再查。
|
|
113
|
+
doc = _registry_doc(str(registry) if registry else None)
|
|
114
|
+
canonical = (doc.get('aliases') or {}).get(flow_id, flow_id)
|
|
115
|
+
spec = (doc.get('flows') or {}).get(canonical)
|
|
116
|
+
# 字段要求按**当前变体**取(FULL:legacy 用「店铺名称/品名」,lip 用「店铺/产品名称」)。
|
|
117
|
+
# 这里是投影列的唯一来源 ⇒ 失败时退回流程级,宁可退也不能炸。
|
|
118
|
+
try:
|
|
119
|
+
import flow_config as _fc
|
|
120
|
+
# ⚠️ 不能把 base 路径当 registry_path 传(那会强制单文件读 ⇒ 业务线流程全部"未知",
|
|
121
|
+
# 且 flow_config.die 抛的是 SystemExit,不被 except Exception 捕获 —— 2026-09-29 实测)。
|
|
122
|
+
_req = _fc.load_config(flow_id, registry_path=registry, strict=False).get('required')
|
|
123
|
+
if _req:
|
|
124
|
+
spec = {**spec, 'required': _req}
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
if not spec:
|
|
128
|
+
print(f'未知流程: {flow_id}(可用: {", ".join((doc.get("flows") or {}).keys())})', file=sys.stderr)
|
|
129
|
+
sys.exit(2)
|
|
130
|
+
return spec
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# 瞬时故障特征(飞书 API / 网关):命中就重试,其余错误立即返回(不掩盖真问题)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv=None) -> int:
|
|
171
|
+
_assert_kinds_aligned()
|
|
172
|
+
ap = argparse.ArgumentParser(description='流程源适配器:来源 → case → 字段缺口')
|
|
173
|
+
ap.add_argument('--flow', help='流程 id(--list-sources 时可省)')
|
|
174
|
+
ap.add_argument('--registry', help='覆盖流程注册表(测试/多部署用,同 flow_config --registry)')
|
|
175
|
+
ap.add_argument('--source', choices=source_kinds(),
|
|
176
|
+
help='数据面种类(默认取注册表 source.kind;choices 从 SOURCES 表派生)')
|
|
177
|
+
ap.add_argument('--list-sources', action='store_true',
|
|
178
|
+
help='列出引擎实现的 source kind(P4 端口表,供门禁/文档消费)')
|
|
179
|
+
ap.add_argument('--list-phases', action='store_true',
|
|
180
|
+
help='列出引擎**实际产出**的相位(唯一真源 = flow_sources/common.py#PHASES;'
|
|
181
|
+
'check-sop-consistency.mjs 的 C2 问这里,不再解析源码文本)')
|
|
182
|
+
ap.add_argument('--no-regenerate', action='store_true',
|
|
183
|
+
help='不跑 source.regenerate、也不查过期(仅夹具/排障:读别处的临时清单时用)')
|
|
184
|
+
ap.add_argument('--from-json', dest='from_json',
|
|
185
|
+
help='cases-json 的批量输入文件(- = stdin;省略则用流程声明的 source.file)')
|
|
186
|
+
ap.add_argument('--ticket', help='票据键(cli-json/conversation 必填;feishu 可作按票号过滤)')
|
|
187
|
+
ap.add_argument('--fields-from-json', help='字段 JSON 文件路径或 -(stdin)')
|
|
188
|
+
ap.add_argument('--provided-by', default='运营', help='字段提供者(provenance)')
|
|
189
|
+
ap.add_argument('--base'); ap.add_argument('--table'); ap.add_argument('--profile')
|
|
190
|
+
ap.add_argument('--record', help='只取指定 record(feishu-base)')
|
|
191
|
+
ap.add_argument('--field-ids', help='窄投影列(**字段 id 或名**,逗号分隔;省略则用流程必需字段前 6 个非系统字段)')
|
|
192
|
+
ap.add_argument('--ticket-field', default='货件号', help='用哪一列作为票据键(默认 货件号)')
|
|
193
|
+
ap.add_argument('--no-pick-filter', action='store_true',
|
|
194
|
+
help='不按流程声明的 pick(候选收窄)过滤;默认按声明过滤')
|
|
195
|
+
ap.add_argument('--no-route-filter', action='store_true',
|
|
196
|
+
help='关闭按 routing.routeBy==match 的候选行收窄(默认开启)')
|
|
197
|
+
ap.add_argument('--limit', type=int, help='最多取多少行(feishu-base)')
|
|
198
|
+
ap.add_argument('--out-dir', help='case 落盘目录(默认 ~/.dsh/argent/flow-cases)')
|
|
199
|
+
ap.add_argument('--skip-gap', action='store_true', help='只建案,不做字段缺口检查')
|
|
200
|
+
ap.add_argument('--json', action='store_true')
|
|
201
|
+
args = ap.parse_args(argv)
|
|
202
|
+
if args.list_sources:
|
|
203
|
+
print(json.dumps(SOURCES, ensure_ascii=False, indent=2))
|
|
204
|
+
return 0
|
|
205
|
+
if args.list_phases:
|
|
206
|
+
# 一行 JSON(门禁按行解析):相位的**唯一真源**在 common.py,这里只做转达
|
|
207
|
+
print(json.dumps({'phases': list(PHASES)}, ensure_ascii=False))
|
|
208
|
+
return 0
|
|
209
|
+
if not args.flow:
|
|
210
|
+
ap.error('--flow 必填(除非用 --list-sources)')
|
|
211
|
+
|
|
212
|
+
spec = load_flow(args.flow, args.registry)
|
|
213
|
+
# P4:数据面由**注册表声明**(`source.kind`),命令行只作覆盖(测试/排障)。
|
|
214
|
+
if not args.source:
|
|
215
|
+
args.source = declared_kind(spec)
|
|
216
|
+
if args.source not in SOURCES:
|
|
217
|
+
print(f'❌ 未实现的 source kind:{args.source}(已实现:{", ".join(source_kinds())})', file=sys.stderr)
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
# ① 取字段
|
|
221
|
+
if args.source == 'feishu-base':
|
|
222
|
+
# P4:实现按端口载入(`flow_sources.MODULES` 是 kind → 模块的唯一映射)
|
|
223
|
+
rows = _load_source('feishu-base').pull(spec, args)
|
|
224
|
+
if not rows:
|
|
225
|
+
if LAST_NARROW.get('emptied'):
|
|
226
|
+
# 读到了行、但按流程声明的 pick 收窄后为空 ⇒ **合法结论**(本轮无事可做)
|
|
227
|
+
info = dict(LAST_NARROW.get('info') or {})
|
|
228
|
+
notes = list(info.pop('notes', []) or [])
|
|
229
|
+
if info.get('droppedDone'):
|
|
230
|
+
notes.append(f'已排除 {info["droppedDone"]} 张已完成'
|
|
231
|
+
f'(人工/既往做过的行,判据 pick.exclude*)')
|
|
232
|
+
if info.get('before'):
|
|
233
|
+
notes.append(f'读取 {info["before"]} 行 → 门内 0 行(说明是历史/未放行数据,不是遗漏)')
|
|
234
|
+
if args.json:
|
|
235
|
+
print(json.dumps({'flow': args.flow, 'source': args.source, 'cases': [], 'exit': 0,
|
|
236
|
+
'narrowedToEmpty': True, 'pick': info, 'notes': notes},
|
|
237
|
+
ensure_ascii=False, indent=2))
|
|
238
|
+
else:
|
|
239
|
+
print(' · 候选收窄后为空(当前没有该动的行)'
|
|
240
|
+
+ (f':{";".join(notes)}' if notes else ''))
|
|
241
|
+
return 0
|
|
242
|
+
print('⚠️ 未取到任何行(检查 --record/筛选条件)', file=sys.stderr)
|
|
243
|
+
return 2
|
|
244
|
+
elif args.source == 'cases-json':
|
|
245
|
+
# P4:实现已抽到 flow_sources.cases_json(契约:(exit_code|None, rows))
|
|
246
|
+
code, rows = _load_source('cases-json').pull_cases(spec, args)
|
|
247
|
+
if code is not None:
|
|
248
|
+
return code
|
|
249
|
+
else:
|
|
250
|
+
if not args.ticket or not args.fields_from_json:
|
|
251
|
+
print('❌ cli-json/conversation 需要 --ticket 与 --fields-from-json', file=sys.stderr)
|
|
252
|
+
return 2
|
|
253
|
+
try:
|
|
254
|
+
rows = [(str(args.ticket), read_fields(args.fields_from_json))]
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print(f'❌ 读取字段失败: {e}', file=sys.stderr)
|
|
257
|
+
return 2
|
|
258
|
+
|
|
259
|
+
kind = ('conversation' if args.source == 'conversation'
|
|
260
|
+
else 'feishu-base' if args.source == 'feishu-base'
|
|
261
|
+
else 'cases-json' if args.source == 'cases-json' else 'cli-json')
|
|
262
|
+
source_id = (f'chat-{args.profile or spec.get("source", {}).get("identity", "session")}'
|
|
263
|
+
if kind == 'conversation' else f'{args.flow}-{kind}')
|
|
264
|
+
results, worst = [], 0
|
|
265
|
+
for ticket, fields in rows:
|
|
266
|
+
# ② 建/合并 case
|
|
267
|
+
case_cmd = [sys.executable, str(FLOW_CASE), '--flow', args.flow, '--ticket', ticket,
|
|
268
|
+
'--source-id', source_id, '--source-kind', kind, '--provided-by', args.provided_by,
|
|
269
|
+
'--fields-from-json', '-']
|
|
270
|
+
if args.out_dir:
|
|
271
|
+
case_cmd += ['--out-dir', args.out_dir]
|
|
272
|
+
p = subprocess.run(case_cmd, input=json.dumps(fields, ensure_ascii=False),
|
|
273
|
+
capture_output=True, text=True)
|
|
274
|
+
if p.returncode not in (0, 1):
|
|
275
|
+
print(f'❌ 建案失败 {ticket}: {(p.stderr or p.stdout).strip()[:300]}', file=sys.stderr)
|
|
276
|
+
return 2
|
|
277
|
+
worst = max(worst, p.returncode)
|
|
278
|
+
|
|
279
|
+
item = {'ticket': ticket, 'caseExit': p.returncode, 'caseLine': (p.stdout or '').strip().splitlines()[:1]}
|
|
280
|
+
# ③ 字段缺口
|
|
281
|
+
if not args.skip_gap:
|
|
282
|
+
# **只把"真有值"的字段算作已提供** —— 见 `_has_value()` 的说明:
|
|
283
|
+
# 投影覆盖的列即使为空也会出现在 fields 里,不过滤 ⇒ 放行开关(发票就绪)形同虚设。
|
|
284
|
+
provided = sorted(k for k, v in (fields or {}).items() if _has_value(v))
|
|
285
|
+
with tempfile.NamedTemporaryFile('w', suffix='.json', delete=False, encoding='utf-8') as tf:
|
|
286
|
+
json.dump(provided, tf, ensure_ascii=False); tmp = tf.name
|
|
287
|
+
g = subprocess.run([sys.executable, str(SCHEMA_CHECK), '--flow', args.flow,
|
|
288
|
+
'--fields-from-json', tmp, '--json'], capture_output=True, text=True)
|
|
289
|
+
pathlib.Path(tmp).unlink(missing_ok=True)
|
|
290
|
+
if g.returncode in (0, 1) and g.stdout.strip().startswith('{'):
|
|
291
|
+
gj = json.loads(g.stdout)
|
|
292
|
+
item['ok'] = bool(gj.get('ok'))
|
|
293
|
+
item['missing'] = [m.get('name') for m in (gj.get('missing') or [])]
|
|
294
|
+
if not gj.get('ok'):
|
|
295
|
+
worst = max(worst, 1)
|
|
296
|
+
else:
|
|
297
|
+
item['gapError'] = (g.stderr or g.stdout).strip()[:200]
|
|
298
|
+
worst = max(worst, 1) # 缺口检查失败 ⇒ 不低于"有缺口"的严重度
|
|
299
|
+
results.append(item)
|
|
300
|
+
|
|
301
|
+
if args.json:
|
|
302
|
+
print(json.dumps({'flow': args.flow, 'source': args.source, 'cases': results,
|
|
303
|
+
'exit': worst, 'notes': list(LAST_PICK_NOTES)}, ensure_ascii=False, indent=2))
|
|
304
|
+
else:
|
|
305
|
+
for r in results:
|
|
306
|
+
if r.get('ok'):
|
|
307
|
+
print(f' ✅ {r["ticket"]}:字段齐备(case 退出码 {r["caseExit"]})')
|
|
308
|
+
else:
|
|
309
|
+
miss = '、'.join((r.get('missing') or [])[:8]) or '(未知)'
|
|
310
|
+
print(f' ⚠️ {r["ticket"]}:缺 {len(r.get("missing") or [])} 个字段 → {miss}')
|
|
311
|
+
return worst
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == '__main__':
|
|
315
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Source 端口实现(P4,2026-09-29)。
|
|
2
|
+
|
|
3
|
+
一个数据面 = 本目录下**一个模块**:模块导出 `KIND` 与 `pull(spec, args)`(返回
|
|
4
|
+
`[(record_id, fields), …]`),引擎只负责调度与输出形状(见 `scripts/flow_source.py`)。
|
|
5
|
+
加一种数据面 = 加一个文件 + 在注册表 `source.kind` 里声明 ⇒ **不改引擎**。
|
|
6
|
+
|
|
7
|
+
为什么这么切:`flow_source.py` 曾是"引擎 + 飞书实现"糊在一起(897 行),
|
|
8
|
+
业务线想接自己的数据面只能改引擎 —— 那正是"底座被单一通道绑架"的形态。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
|
|
14
|
+
# kind → 实现模块(模块必须导出 KIND 与 pull)
|
|
15
|
+
MODULES: dict = {
|
|
16
|
+
'feishu-base': 'flow_sources.feishu_base',
|
|
17
|
+
'cases-json': 'flow_sources.cases_json',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def kinds() -> list:
|
|
22
|
+
"""已实现的数据面种类(与 flow_source.SOURCES 的键必须一致,门禁会校验)。"""
|
|
23
|
+
return sorted(MODULES)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_module(kind: str) -> str:
|
|
27
|
+
"""校验某 kind 的实现模块符合端口契约,返回它的**形态**(`rows` | `cases`)。
|
|
28
|
+
|
|
29
|
+
两种形态(现实里确实有两种数据面,别硬凑成一种):
|
|
30
|
+
· `rows` :外部数据源给"记录 + 字段",引擎负责建案/缺口检查 —— 导出 `pull(spec, args)`;
|
|
31
|
+
· `cases`:上游已产出**待处理单元**(如日报的店铺×日期×幂等标记差集)—— 导出 `pull_cases(spec, args)`。
|
|
32
|
+
|
|
33
|
+
契约自校验放在这里(而不是各实现里)⇒ 半迁移的模块会在**启动时**响亮失败,
|
|
34
|
+
而不是运行到一半才炸。`flow_source._assert_kinds_aligned()` 会逐个调用它。
|
|
35
|
+
"""
|
|
36
|
+
mod = load(kind)
|
|
37
|
+
declared = getattr(mod, 'KIND', None)
|
|
38
|
+
if declared != kind:
|
|
39
|
+
raise SystemExit(f'source 实现 {MODULES[kind]} 的 KIND={declared!r} 与登记不一致(应为 {kind!r})')
|
|
40
|
+
has_pull = callable(getattr(mod, 'pull', None))
|
|
41
|
+
has_cases = callable(getattr(mod, 'pull_cases', None))
|
|
42
|
+
if has_pull == has_cases:
|
|
43
|
+
raise SystemExit(f'source 实现 {MODULES[kind]} 必须**恰好**导出 pull() 或 pull_cases() 之一'
|
|
44
|
+
f'(当前 pull={has_pull} pull_cases={has_cases})')
|
|
45
|
+
return 'rows' if has_pull else 'cases'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load(kind: str):
|
|
49
|
+
"""按 kind 载入实现模块(惰性 import:没装/没用的实现不该拖慢每次分诊)。"""
|
|
50
|
+
mod_name = MODULES.get(kind)
|
|
51
|
+
if not mod_name:
|
|
52
|
+
raise KeyError(kind)
|
|
53
|
+
return importlib.import_module(mod_name)
|