@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,531 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""usage_report —— **每票真实模型成本**(定价锚点,2026-09-22 落地)。
|
|
3
|
+
|
|
4
|
+
## 为什么需要它
|
|
5
|
+
定价不能靠猜。2026-09-22 实测发现:服务端扣费公式里有**最低 1 积分封顶**
|
|
6
|
+
(`cost = max(1, ceil(cost元 × 10 × 1.2 × 100)/100)`),历史 1841 次消耗里 **95.2% 都被封顶**
|
|
7
|
+
⇒ 只看"扣了多少积分"**根本推不出真实成本**(之前"¥0.11/请求"其实是我们的**售价**,不是成本)。
|
|
8
|
+
所以锚点分两段:
|
|
9
|
+
|
|
10
|
+
- **从现在起(精确)**:服务端把计价依据一并落库(`costYuan` + `usage{prompt,cacheHit,cacheMiss,output}`
|
|
11
|
+
+ `rate` 单价快照 + `margin/peak`,2026-09-22 上线,见 whyshu-svr `src/routes/argent.js`);
|
|
12
|
+
本脚本直接读 `costYuan` ⇒ 每票成本**精确**。
|
|
13
|
+
- **历史(只有区间)**:非封顶行可由金额精确反推(`cost = |金额| / (cnyToPoints × margin)`);
|
|
14
|
+
封顶行只能给**上界** `1/(cny×margin)` 元。报告里用 `¥lo ~ ¥hi` 表达,并标 `exact=False`。
|
|
15
|
+
|
|
16
|
+
## 归因(诚实近似)
|
|
17
|
+
把每一次请求按时间落进 case 的 `planned → done/failed` 窗口(前后各外扩 90s/300s);
|
|
18
|
+
窗口重叠的票(一店一批、一个 subagent 跑 3 张)**整批求和再按票数摊**,标 `batched=true`。
|
|
19
|
+
所以输出里的"每票成本"是**该批次摊到的量**,是定价下限的稳健锚,不是逐次调用的精确归属。
|
|
20
|
+
|
|
21
|
+
用法:
|
|
22
|
+
python3 scripts/usage_report.py --pull # 拉流水(增量去重)
|
|
23
|
+
python3 scripts/usage_report.py --report [--month 2026-09] [--json]
|
|
24
|
+
退出码:0 正常;1 拉取失败;2 参数错误
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
import argparse, datetime as dt, json, os, pathlib, re, sys, time, urllib.error, urllib.request
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from flow_registry import consumer_doc as _registry_doc # P2a:base + 业务线合并(唯一真源)
|
|
31
|
+
except Exception: # noqa: BLE001 —— 半坏机器上退回单文件(升级链路要能跑)
|
|
32
|
+
def _registry_doc(path=None):
|
|
33
|
+
_p = pathlib.Path(path) if path else (HERE / "flow-requirements.json")
|
|
34
|
+
return json.loads(_p.read_text(encoding="utf-8"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
38
|
+
sys.path.insert(0, str(HERE / 'lib'))
|
|
39
|
+
from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
|
|
40
|
+
DATA = pathlib.Path(resolve_data_dir()['dir'])
|
|
41
|
+
USAGE = DATA / "usage"
|
|
42
|
+
HISTORY = USAGE / "points-history.jsonl" # 原始流水(按 _id 去重)
|
|
43
|
+
LOCAL = USAGE / "local-requests.jsonl" # 本机会话级用量(走官方直连也有)
|
|
44
|
+
RATES = USAGE / "rate-table.json" # 权威费率缓存(来自服务端 points-rules)
|
|
45
|
+
DEFAULT_LEDGER = DATA / "flow-ledger.jsonl"
|
|
46
|
+
POINTS_PER_YUAN = 10.0 # 1 元 = 10 积分(售价侧)
|
|
47
|
+
DEFAULT_MARGIN, DEFAULT_CNY_TO_POINTS = 1.2, 10.0
|
|
48
|
+
# 内置兜底费率(与服务端 config.points.models 保持一致;正常应经 --pull-rates 取权威值)
|
|
49
|
+
DEFAULT_RATES = {"margin": DEFAULT_MARGIN, "cnyToPoints": DEFAULT_CNY_TO_POINTS,
|
|
50
|
+
"peakHours": [[9, 12], [14, 18]],
|
|
51
|
+
"models": {"deepseek-v4-flash": {"cacheHit": 0.1, "input": 3.0, "output": 9.0},
|
|
52
|
+
"deepseek-v4-pro": {"cacheHit": 0.3, "input": 9.0, "output": 27.0}}}
|
|
53
|
+
|
|
54
|
+
LEAD_SEC, TAIL_SEC = 90, 300 # 窗口外扩(派发前思考 + 跑完复核)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_registry() -> dict:
|
|
58
|
+
try:
|
|
59
|
+
return _registry_doc()
|
|
60
|
+
except Exception: # noqa: BLE001
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def api_creds() -> tuple:
|
|
65
|
+
base = (os.environ.get("WHYSHU_API_URL") or "https://whyshu.com").rstrip("/")
|
|
66
|
+
tok = os.environ.get("WHYSHU_API_KEY") or ""
|
|
67
|
+
if not tok:
|
|
68
|
+
for name in ("whyshu.env", "fulfillment.env"):
|
|
69
|
+
try:
|
|
70
|
+
for line in (DATA / name).read_text(encoding="utf-8").splitlines():
|
|
71
|
+
t = line.strip()
|
|
72
|
+
if t.startswith("WHYSHU_API_KEY="):
|
|
73
|
+
tok = t.split("=", 1)[1].strip().strip('"').strip("'")
|
|
74
|
+
except Exception: # noqa: BLE001
|
|
75
|
+
continue
|
|
76
|
+
return base, tok
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def pull(pages: int = 20, limit: int = 200) -> int:
|
|
80
|
+
"""增量拉取积分流水(按 _id 去重)。返回新增条数;-1 = 失败。"""
|
|
81
|
+
base, tok = api_creds()
|
|
82
|
+
if not tok:
|
|
83
|
+
print("❌ 缺 WHYSHU_API_KEY(进程环境或 ~/.dsh/argent/*.env)", file=sys.stderr)
|
|
84
|
+
return -1
|
|
85
|
+
seen = set()
|
|
86
|
+
if HISTORY.exists():
|
|
87
|
+
for line in HISTORY.read_text(encoding="utf-8").splitlines():
|
|
88
|
+
try:
|
|
89
|
+
seen.add(json.loads(line)["_id"])
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
continue
|
|
92
|
+
USAGE.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
HISTORY.parent.mkdir(parents=True, exist_ok=True) # 首次运行目录还不存在时也要能写
|
|
94
|
+
new = 0
|
|
95
|
+
with HISTORY.open("a", encoding="utf-8") as fh:
|
|
96
|
+
for page in range(1, pages + 1):
|
|
97
|
+
url = f"{base}/api/argent/points-history?limit={limit}&page={page}"
|
|
98
|
+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {tok}"})
|
|
99
|
+
try:
|
|
100
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
101
|
+
doc = json.loads(r.read().decode("utf-8") or "{}")
|
|
102
|
+
except urllib.error.HTTPError as e:
|
|
103
|
+
print(f"❌ 拉取失败 HTTP {e.code}(page {page})", file=sys.stderr)
|
|
104
|
+
return -1
|
|
105
|
+
except Exception as e: # noqa: BLE001
|
|
106
|
+
print(f"❌ 拉取失败:{e}", file=sys.stderr)
|
|
107
|
+
return -1
|
|
108
|
+
items = doc.get("transactions") or doc.get("items") or []
|
|
109
|
+
if not items:
|
|
110
|
+
break
|
|
111
|
+
for t in items:
|
|
112
|
+
if t.get("_id") in seen:
|
|
113
|
+
continue
|
|
114
|
+
seen.add(t["_id"])
|
|
115
|
+
fh.write(json.dumps(t, ensure_ascii=False) + "\n")
|
|
116
|
+
new += 1
|
|
117
|
+
if len(items) < limit:
|
|
118
|
+
break
|
|
119
|
+
print(f" ✅ 流水:新增 {new} 条(累计 {len(seen)} 条)→ {HISTORY}")
|
|
120
|
+
return new
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def pull_rates() -> dict:
|
|
124
|
+
"""拉权威费率表(公开端点,无需登录)并缓存。失败时保留旧缓存/兜底值。"""
|
|
125
|
+
base, _ = api_creds()
|
|
126
|
+
try:
|
|
127
|
+
with urllib.request.urlopen(f"{base}/api/argent/points-rules", timeout=20) as r:
|
|
128
|
+
doc = json.loads(r.read().decode("utf-8") or "{}")
|
|
129
|
+
table = {"margin": float(doc.get("margin") or DEFAULT_MARGIN),
|
|
130
|
+
"cnyToPoints": float(doc.get("cnyToPoints") or DEFAULT_CNY_TO_POINTS),
|
|
131
|
+
"peakHours": doc.get("peakHours") or DEFAULT_RATES["peakHours"],
|
|
132
|
+
"models": {m["model"]: {"cacheHit": float(m["perMillion"]["cacheHit"]),
|
|
133
|
+
"input": float(m["perMillion"]["input"]),
|
|
134
|
+
"output": float(m["perMillion"]["output"])}
|
|
135
|
+
for m in (doc.get("models") or []) if m.get("perMillion")}}
|
|
136
|
+
if not table["models"]:
|
|
137
|
+
raise ValueError("服务端未返回 models")
|
|
138
|
+
USAGE.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
RATES.write_text(json.dumps(table, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
140
|
+
print(f" ✅ 费率表已更新({len(table['models'])} 个模型,margin {table['margin']})→ {RATES}")
|
|
141
|
+
return table
|
|
142
|
+
except Exception as e: # noqa: BLE001
|
|
143
|
+
print(f" ⚠️ 费率表拉取失败(沿用缓存/兜底):{e}", file=sys.stderr)
|
|
144
|
+
return load_rates()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_rates() -> dict:
|
|
148
|
+
try:
|
|
149
|
+
t = json.loads(RATES.read_text(encoding="utf-8"))
|
|
150
|
+
if t.get("models"):
|
|
151
|
+
return t
|
|
152
|
+
except Exception: # noqa: BLE001
|
|
153
|
+
pass
|
|
154
|
+
return DEFAULT_RATES
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_peak(ts: float, rates: dict) -> bool:
|
|
158
|
+
"""峰谷判定:按**北京时间**(服务端 TZ 即 Asia/Shanghai,与文档一致)。"""
|
|
159
|
+
h = dt.datetime.fromtimestamp(ts, dt.timezone(dt.timedelta(hours=8))).hour
|
|
160
|
+
return any(a <= h < b for a, b in (rates.get("peakHours") or []))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def price_row(row: dict, rates: dict) -> float:
|
|
164
|
+
"""本机用量 → 元(与服务端 computeCostYuan 同一公式:命中/未命中/输出 × 单价 × 峰谷)。
|
|
165
|
+
|
|
166
|
+
⚠️ `row["ts"]` 是**毫秒**(会话日志 `time` 字段),折算成秒后才能判峰谷。
|
|
167
|
+
"""
|
|
168
|
+
m = (rates.get("models") or {}).get(row.get("model") or "") or DEFAULT_RATES["models"]["deepseek-v4-flash"]
|
|
169
|
+
yuan = (int(row.get("cacheRead") or 0) * m["cacheHit"] + int(row.get("input") or 0) * m["input"]
|
|
170
|
+
+ int(row.get("output") or 0) * m["output"]) / 1_000_000
|
|
171
|
+
ts_sec = float(row.get("ts") or 0) / 1000.0
|
|
172
|
+
return yuan * (1.0 if is_peak(ts_sec, rates) else 0.5)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def load_local(rates: dict, path: pathlib.Path = None) -> list:
|
|
176
|
+
"""本机会话用量(`local-usage.mjs` 产出)→ 统一行结构(成本精确,无需封顶估计)。"""
|
|
177
|
+
path = path or LOCAL
|
|
178
|
+
out = []
|
|
179
|
+
if not path.exists():
|
|
180
|
+
return out
|
|
181
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
182
|
+
try:
|
|
183
|
+
r = json.loads(line)
|
|
184
|
+
except Exception: # noqa: BLE001
|
|
185
|
+
continue
|
|
186
|
+
cy = price_row(r, rates)
|
|
187
|
+
out.append({"id": None, "ts": float(r.get("ts") or 0) / 1000.0, "points": cy,
|
|
188
|
+
"spend": True, "type": "local_usage", "source": "local",
|
|
189
|
+
"kind": r.get("kind") or "chat", "session": r.get("session"),
|
|
190
|
+
"model": r.get("model") or "", "peak": is_peak(float(r.get("ts") or 0) / 1000.0, rates),
|
|
191
|
+
"costLo": cy, "costHi": cy, "exact": True, "charged": 0.0,
|
|
192
|
+
"tokens": int(r.get("input") or 0) + int(r.get("cacheRead") or 0) + int(r.get("output") or 0),
|
|
193
|
+
"prompt": int(r.get("input") or 0) + int(r.get("cacheRead") or 0),
|
|
194
|
+
"cacheHit": r.get("cacheRead"), "cacheMiss": r.get("input"), "output": r.get("output")})
|
|
195
|
+
out.sort(key=lambda x: x["ts"])
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def scan_local() -> int:
|
|
200
|
+
"""调用 local-usage.mjs 增量扫描本机会话日志。返回新增请求数(-1 = 失败)。"""
|
|
201
|
+
import subprocess
|
|
202
|
+
try:
|
|
203
|
+
r = subprocess.run(["node", str(HERE / "local-usage.mjs"), "--json"],
|
|
204
|
+
capture_output=True, text=True, timeout=600)
|
|
205
|
+
if r.returncode != 0:
|
|
206
|
+
print(f" ⚠️ 本机扫描失败:{(r.stderr or r.stdout or '').strip()[:200]}", file=sys.stderr)
|
|
207
|
+
return -1
|
|
208
|
+
n = int(json.loads(r.stdout).get("newRequests") or 0)
|
|
209
|
+
print(f" ✅ 本机会话:新增 {n} 次请求 → {LOCAL}")
|
|
210
|
+
return n
|
|
211
|
+
except Exception as e: # noqa: BLE001
|
|
212
|
+
print(f" ⚠️ 本机扫描失败:{e}", file=sys.stderr)
|
|
213
|
+
return -1
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def parse_desc(desc) -> dict:
|
|
217
|
+
"""从 description 取 tokens/模型(服务端模板:`AI对话消耗 133 tokens(deepseek-v4-flash,高峰时段)`)。"""
|
|
218
|
+
d = str(desc or "")
|
|
219
|
+
m = re.search(r"(\d+)\s*tokens", d)
|
|
220
|
+
mm = re.search(r"(([^,,]+)[,,]", d)
|
|
221
|
+
return {"tokens": int(m.group(1)) if m else 0, "modelDesc": (mm.group(1) if mm else ""),
|
|
222
|
+
"peakDesc": "高峰" in d}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def row_cost(t: dict) -> tuple:
|
|
226
|
+
"""一次请求的真实成本 (lo, hi, exact, charged)——见文件头"两段锚点"。
|
|
227
|
+
|
|
228
|
+
精确:新数据带 costYuan;历史的非封顶行可由金额反推(误差 <0.01 元,来自 ceil 取整)。
|
|
229
|
+
上界:历史封顶行(金额恰为 1 积分)只知道 cost ≤ 1/(cny×margin) 元。
|
|
230
|
+
"""
|
|
231
|
+
cny = float(t.get("cnyToPoints") or DEFAULT_CNY_TO_POINTS)
|
|
232
|
+
mg = float(t.get("margin") or DEFAULT_MARGIN)
|
|
233
|
+
raw = float(t.get("amount") or 0)
|
|
234
|
+
if raw >= 0: # 发放/充值/管理调整:不是消耗,既无成本也无"收费"
|
|
235
|
+
return 0.0, 0.0, True, 0.0
|
|
236
|
+
amt = abs(raw)
|
|
237
|
+
charged = amt / cny
|
|
238
|
+
if t.get("costYuan") is not None:
|
|
239
|
+
cy = float(t["costYuan"])
|
|
240
|
+
return cy, cy, True, charged
|
|
241
|
+
if amt > 1.0: # 未封顶 ⇒ 由金额反推(ceil 取整,误差 ≤ 0.01/(cny×margin) 元)
|
|
242
|
+
return amt / (cny * mg), amt / (cny * mg), True, charged
|
|
243
|
+
return 0.0, 1.0 / (cny * mg), False, charged # 封顶 ⇒ 仅上界
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def load_history(path: pathlib.Path) -> list:
|
|
247
|
+
out = []
|
|
248
|
+
if not path.exists():
|
|
249
|
+
return out
|
|
250
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
251
|
+
try:
|
|
252
|
+
t = json.loads(line)
|
|
253
|
+
except Exception: # noqa: BLE001
|
|
254
|
+
continue
|
|
255
|
+
try:
|
|
256
|
+
epoch = dt.datetime.fromisoformat(str(t.get("created_at") or t.get("createdAt") or "")
|
|
257
|
+
.replace("Z", "+00:00")).timestamp()
|
|
258
|
+
except Exception: # noqa: BLE001
|
|
259
|
+
continue
|
|
260
|
+
info = parse_desc(t.get("description"))
|
|
261
|
+
amt = float(t.get("amount") or 0)
|
|
262
|
+
lo, hi, exact, charged = row_cost(t)
|
|
263
|
+
u = t.get("usage") or {}
|
|
264
|
+
out.append({"id": t.get("_id"), "ts": epoch, "points": -amt if amt < 0 else 0.0,
|
|
265
|
+
"spend": amt < 0, "type": t.get("type"), "source": "proxy", "kind": "proxy",
|
|
266
|
+
"model": t.get("model") or info["modelDesc"],
|
|
267
|
+
"peak": t.get("peak") if t.get("peak") is not None else info["peakDesc"],
|
|
268
|
+
"costLo": lo, "costHi": hi, "exact": exact, "charged": charged,
|
|
269
|
+
"tokens": u.get("total") or info["tokens"],
|
|
270
|
+
"prompt": u.get("prompt"), "cacheHit": u.get("cacheHit"),
|
|
271
|
+
"cacheMiss": u.get("cacheMiss"), "output": u.get("output")})
|
|
272
|
+
out.sort(key=lambda x: x["ts"])
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def load_cases(ledger: pathlib.Path, doc: dict = None) -> dict:
|
|
277
|
+
"""台账 → caseId: {start, end, flow, terminal}(窗口归因输入)。
|
|
278
|
+
|
|
279
|
+
只收**已注册流程**的 case(`doc['flows']`);缺 doc 时按前缀黑名单兜底。
|
|
280
|
+
否则 `claim__<x>` / `preflight__<x>` / `lease` 这类事件会变成幽灵票,把每票成本摊薄。
|
|
281
|
+
"""
|
|
282
|
+
cases: dict = {}
|
|
283
|
+
known = set((doc or {}).get("flows") or {})
|
|
284
|
+
skip = {"claim", "preflight", "lease", "lease-remind", "heartbeat", "notify", "invoice-ready-leftover"}
|
|
285
|
+
if not ledger.exists():
|
|
286
|
+
return cases
|
|
287
|
+
for line in ledger.read_text(encoding="utf-8").splitlines():
|
|
288
|
+
try:
|
|
289
|
+
e = json.loads(line)
|
|
290
|
+
except Exception: # noqa: BLE001
|
|
291
|
+
continue
|
|
292
|
+
cid = str(e.get("caseId") or "")
|
|
293
|
+
if not cid or "__" not in cid:
|
|
294
|
+
continue
|
|
295
|
+
flow = cid.split("__")[0]
|
|
296
|
+
if (known and flow not in known) or (not known and flow in skip):
|
|
297
|
+
continue
|
|
298
|
+
ts = float(e.get("ts") or 0)
|
|
299
|
+
c = cases.setdefault(cid, {"caseId": cid, "flow": flow, "start": ts, "end": ts, "terminal": ""})
|
|
300
|
+
c["start"] = min(c["start"], ts) if c["start"] else ts
|
|
301
|
+
c["end"] = max(c["end"], ts)
|
|
302
|
+
if str(e.get("event")) in ("done", "failed", "blocked"):
|
|
303
|
+
c["terminal"] = str(e.get("event"))
|
|
304
|
+
return cases
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def attribute(cases: dict, hist: list, doc: dict) -> dict:
|
|
308
|
+
"""窗口归因 + 批次摊分(纯函数)。成本按 lo/hi 双轨累计。"""
|
|
309
|
+
flow_cap = {fid: str((f or {}).get("capability") or "") for fid, f in (doc.get("flows") or {}).items()}
|
|
310
|
+
# 票据成本只能由**流程角色会话**产生:本机对话/开发会话(kind=chat)不进窗口,
|
|
311
|
+
# 否则我自己的长对话会把每票成本算成天价(2026-09-22 实测 chat 侧 43 亿 cacheRead)。
|
|
312
|
+
events = [h for h in hist if h.get("spend") and (h.get("source") != "local" or h.get("kind") == "flow")]
|
|
313
|
+
wins = []
|
|
314
|
+
for c in cases.values():
|
|
315
|
+
cap = flow_cap.get(c["flow"], "")
|
|
316
|
+
if not cap:
|
|
317
|
+
continue
|
|
318
|
+
wins.append({**c, "capability": cap, "w0": c["start"] - LEAD_SEC, "w1": c["end"] + TAIL_SEC,
|
|
319
|
+
"costLo": 0.0, "costHi": 0.0, "tokens": 0, "reqs": 0.0, "exact": True})
|
|
320
|
+
wins.sort(key=lambda x: x["w0"])
|
|
321
|
+
batches = []
|
|
322
|
+
for w in wins:
|
|
323
|
+
if batches and w["w0"] <= batches[-1]["w1"]:
|
|
324
|
+
batches[-1]["items"].append(w)
|
|
325
|
+
batches[-1]["w1"] = max(batches[-1]["w1"], w["w1"])
|
|
326
|
+
else:
|
|
327
|
+
batches.append({"w0": w["w0"], "w1": w["w1"], "items": [w]})
|
|
328
|
+
for b in batches:
|
|
329
|
+
inside = [h for h in events if b["w0"] <= h["ts"] <= b["w1"]]
|
|
330
|
+
b["costLo"] = sum(h["costLo"] for h in inside)
|
|
331
|
+
b["costHi"] = sum(h["costHi"] for h in inside)
|
|
332
|
+
b["tokens"] = sum(h["tokens"] for h in inside)
|
|
333
|
+
b["reqs"] = len(inside)
|
|
334
|
+
b["exact"] = all(h["exact"] for h in inside)
|
|
335
|
+
n = max(1, len(b["items"]))
|
|
336
|
+
for w in b["items"]:
|
|
337
|
+
w["costLo"] = b["costLo"] / n
|
|
338
|
+
w["costHi"] = b["costHi"] / n
|
|
339
|
+
w["tokens"] = b["tokens"] // n
|
|
340
|
+
w["reqs"] = b["reqs"] / n
|
|
341
|
+
w["exact"] = b["exact"]
|
|
342
|
+
w["batched"] = n > 1
|
|
343
|
+
w["batchSize"] = n
|
|
344
|
+
return {"byCase": {w["caseId"]: w for w in wins}, "batches": batches, "hist": hist}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def summary(attr: dict, month: str, doc: dict) -> dict:
|
|
348
|
+
hist = [h for h in attr["hist"]
|
|
349
|
+
if h.get("spend") and time.strftime("%Y-%m", time.localtime(h["ts"])) == month]
|
|
350
|
+
total_lo = sum(h["costLo"] for h in hist)
|
|
351
|
+
total_hi = sum(h["costHi"] for h in hist)
|
|
352
|
+
charged = sum(h["charged"] for h in hist) # 本机行 charged=0(未走代理计费)
|
|
353
|
+
local_hist = [h for h in hist if h.get("source") == "local"]
|
|
354
|
+
proxy_hist = [h for h in hist if h.get("source") != "local"]
|
|
355
|
+
local_flow = [h for h in local_hist if h.get("kind") == "flow"]
|
|
356
|
+
by_source = {
|
|
357
|
+
"local": {"requests": len(local_hist), "tokens": sum(h["tokens"] for h in local_hist),
|
|
358
|
+
"costYuan": round(sum(h["costHi"] for h in local_hist), 4),
|
|
359
|
+
"flowRequests": len(local_flow),
|
|
360
|
+
"flowCostYuan": round(sum(h["costHi"] for h in local_flow), 4),
|
|
361
|
+
"chatRequests": len(local_hist) - len(local_flow),
|
|
362
|
+
"chatCostYuan": round(sum(h["costHi"] for h in local_hist) - sum(h["costHi"] for h in local_flow), 4)},
|
|
363
|
+
"proxy": {"requests": len(proxy_hist), "tokens": sum(h["tokens"] for h in proxy_hist),
|
|
364
|
+
"costYuanLo": round(sum(h["costLo"] for h in proxy_hist), 4),
|
|
365
|
+
"costYuanHi": round(sum(h["costHi"] for h in proxy_hist), 4),
|
|
366
|
+
"chargedYuan": round(charged, 4)},
|
|
367
|
+
}
|
|
368
|
+
proxy_lo = sum(h["costLo"] for h in proxy_hist)
|
|
369
|
+
proxy_hi = sum(h["costHi"] for h in proxy_hist)
|
|
370
|
+
by_model: dict = {}
|
|
371
|
+
for h in hist:
|
|
372
|
+
k = h["model"] or "(未知)"
|
|
373
|
+
e = by_model.setdefault(k, {"reqs": 0, "tokens": 0, "costLo": 0.0, "costHi": 0.0,
|
|
374
|
+
"charged": 0.0, "exactReqs": 0, "cacheHit": 0, "cacheMiss": 0, "output": 0})
|
|
375
|
+
e["reqs"] += 1
|
|
376
|
+
e["tokens"] += h["tokens"]
|
|
377
|
+
e["costLo"] += h["costLo"]
|
|
378
|
+
e["costHi"] += h["costHi"]
|
|
379
|
+
e["charged"] += h["charged"]
|
|
380
|
+
e["exactReqs"] += 1 if h["exact"] else 0
|
|
381
|
+
for f in ("cacheHit", "cacheMiss", "output"):
|
|
382
|
+
if h.get(f) is not None:
|
|
383
|
+
e[f] += int(h[f])
|
|
384
|
+
cases = [w for w in attr["byCase"].values()
|
|
385
|
+
if time.strftime("%Y-%m", time.localtime(w["end"])) == month]
|
|
386
|
+
per_cap: dict = {}
|
|
387
|
+
for w in cases:
|
|
388
|
+
e = per_cap.setdefault(w["capability"], {"tickets": 0, "costLo": 0.0, "costHi": 0.0, "tokens": 0,
|
|
389
|
+
"reqs": 0.0, "batched": 0, "exact": True, "terminal": {}})
|
|
390
|
+
e["tickets"] += 1
|
|
391
|
+
e["costLo"] += w["costLo"]
|
|
392
|
+
e["costHi"] += w["costHi"]
|
|
393
|
+
e["tokens"] += w["tokens"]
|
|
394
|
+
e["reqs"] += w["reqs"]
|
|
395
|
+
e["batched"] += 1 if w.get("batched") else 0
|
|
396
|
+
e["exact"] = e["exact"] and w.get("exact", True)
|
|
397
|
+
k = w["terminal"] or "(在飞)"
|
|
398
|
+
e["terminal"][k] = e["terminal"].get(k, 0) + 1
|
|
399
|
+
attributed = sum(w["costHi"] for w in cases)
|
|
400
|
+
attributed_lo = sum(w["costLo"] for w in cases)
|
|
401
|
+
# 可归因池 = 本机流程角色会话 + 代理侧(这些才可能是"为客户干的活")
|
|
402
|
+
pool_lo = by_source["local"]["flowCostYuan"] + by_source["proxy"]["costYuanLo"]
|
|
403
|
+
pool_hi = by_source["local"]["flowCostYuan"] + by_source["proxy"]["costYuanHi"]
|
|
404
|
+
for cap, e in per_cap.items():
|
|
405
|
+
n = max(1, e["tickets"])
|
|
406
|
+
e["yuanLoPerTicket"] = round(e["costLo"] / n, 4)
|
|
407
|
+
e["yuanHiPerTicket"] = round(e["costHi"] / n, 4)
|
|
408
|
+
e["avgTokensPerTicket"] = e["tokens"] // n
|
|
409
|
+
e["avgReqsPerTicket"] = round(e["reqs"] / n, 1)
|
|
410
|
+
return {
|
|
411
|
+
"month": month, "pointsPerYuan": POINTS_PER_YUAN,
|
|
412
|
+
"requests": len(hist), "tokens": sum(h["tokens"] for h in hist),
|
|
413
|
+
"costYuanLo": round(total_lo, 4), "costYuanHi": round(total_hi, 4),
|
|
414
|
+
"chargedYuan": round(charged, 4), "chargedPoints": round(charged * POINTS_PER_YUAN, 2),
|
|
415
|
+
"bySource": by_source,
|
|
416
|
+
# 票据成本 vs 角色开销:稳定运行后 overhead 应远小于 attributed;若反了,说明
|
|
417
|
+
# 要么票窗口没覆盖到真实工作会话,要么角色在票据之外空转(两种都要查)。
|
|
418
|
+
"attributedYuanLo": round(attributed_lo, 4), "attributedYuanHi": round(attributed, 4),
|
|
419
|
+
"flowPoolYuanLo": round(pool_lo, 4), "flowPoolYuanHi": round(pool_hi, 4),
|
|
420
|
+
"overheadYuanLo": round(max(0.0, pool_lo - attributed_lo), 4),
|
|
421
|
+
"overheadYuanHi": round(max(0.0, pool_hi - attributed), 4),
|
|
422
|
+
"marginLo": round(charged / proxy_hi, 3) if proxy_hi else None, # 最保守:成本取上界
|
|
423
|
+
"marginHi": round(charged / proxy_lo, 3) if proxy_lo else None, # 最乐观:成本取下界
|
|
424
|
+
"exactRequests": sum(1 for h in hist if h["exact"]),
|
|
425
|
+
"instrumentedRequests": sum(1 for h in hist if h["cacheHit"] is not None),
|
|
426
|
+
"byModel": by_model, "perCapability": per_cap, "ticketsCounted": len(cases),
|
|
427
|
+
"note": ("两条腿:**本机**(会话日志逐次用量 × 权威费率,精确)+**代理**(服务端 costYuan,"
|
|
428
|
+
"2026-09-22 起精确;更早的封顶行只有上界)。每票成本 = 该票窗口内的**流程角色会话**"
|
|
429
|
+
"消耗按票摊分(对话/开发会话不计入),窗口重叠按批摊分。"),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def render_report(s: dict) -> str:
|
|
434
|
+
loc = s.get("bySource", {}).get("local", {})
|
|
435
|
+
px = s.get("bySource", {}).get("proxy", {})
|
|
436
|
+
L = [f" 模型成本锚点({s['month']})",
|
|
437
|
+
f" 真实成本合计:¥{s['costYuanLo']:.2f} ~ ¥{s['costYuanHi']:.2f}"
|
|
438
|
+
f"|请求 {s['requests']} 次|tokens {s['tokens']:,}",
|
|
439
|
+
f" 本机(会话日志,精确):¥{loc.get('costYuan', 0):.2f}"
|
|
440
|
+
f"|{loc.get('requests', 0)} 次(流程角色 {loc.get('flowRequests', 0)} / 对话 {loc.get('chatRequests', 0)})",
|
|
441
|
+
f" 代理(服务端 costYuan):¥{px.get('costYuanLo', 0):.2f} ~ ¥{px.get('costYuanHi', 0):.2f}"
|
|
442
|
+
f"|{px.get('requests', 0)} 次|收费 ¥{px.get('chargedYuan', 0):.2f}"
|
|
443
|
+
f"(毛利 {(s['marginLo'] or 0):.2f}× ~ {(s['marginHi'] or 0):.2f}×)",
|
|
444
|
+
f" 票据窗口内 {s.get('attributedYuanLo', 0):.2f} ~ {s.get('attributedYuanHi', 0):.2f}"
|
|
445
|
+
f"|窗口外角色开销(调度/巡检/开发){s.get('overheadYuanLo', 0):.2f} ~ {s.get('overheadYuanHi', 0):.2f}",
|
|
446
|
+
" " + "-" * 96]
|
|
447
|
+
for m, v in sorted(s["byModel"].items(), key=lambda x: -x[1]["costHi"]):
|
|
448
|
+
L.append(f" {m:20} {v['reqs']:>5} 次|{v['tokens']:>11,} tokens"
|
|
449
|
+
f"|成本 ¥{v['costLo']:.2f}~{v['costHi']:.2f}|收费 ¥{v['charged']:.2f}"
|
|
450
|
+
f"|精确 {v['exactReqs']}/{v['reqs']}")
|
|
451
|
+
if v["cacheHit"] or v["cacheMiss"] or v["output"]:
|
|
452
|
+
L.append(f" {'':20} 缓存命中 {v['cacheHit']:,}|未命中 {v['cacheMiss']:,}|输出 {v['output']:,}")
|
|
453
|
+
if loc:
|
|
454
|
+
L.append(f" (本机流程角色会话成本 ¥{loc.get('flowCostYuan', 0):.2f};"
|
|
455
|
+
f"对话/开发会话 ¥{loc.get('chatCostYuan', 0):.2f} 不计入票据成本)")
|
|
456
|
+
L.append(" " + "-" * 96)
|
|
457
|
+
L.append(" 按能力的**每票真实成本**(= 定价下限锚;含失败票,失败也烧 token):")
|
|
458
|
+
for cap, v in sorted(s["perCapability"].items(), key=lambda x: -x[1]["tickets"]):
|
|
459
|
+
rng = (f"¥{v['yuanLoPerTicket']:.3f}" if v["exact"] else f"≤¥{v['yuanHiPerTicket']:.3f}")
|
|
460
|
+
L.append(f" {cap:16} 票 {v['tickets']:>3}|每票成本 {rng:>9}"
|
|
461
|
+
f"|{v['avgTokensPerTicket']:>9,} tokens|{v['avgReqsPerTicket']:>5} 请求/票"
|
|
462
|
+
f"|同批 {v['batched']}|终态 {v['terminal']}")
|
|
463
|
+
L.append("")
|
|
464
|
+
L.append(" ⚠️ " + s["note"])
|
|
465
|
+
return "\n".join(L)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def main(argv=None) -> int:
|
|
469
|
+
ap = argparse.ArgumentParser(description="每票/每日真实模型成本(定价锚点)")
|
|
470
|
+
ap.add_argument("--pull", action="store_true", help="从服务端拉积分流水(增量去重)")
|
|
471
|
+
ap.add_argument("--report", action="store_true")
|
|
472
|
+
ap.add_argument("--month", help="YYYY-MM(缺省=当月)")
|
|
473
|
+
ap.add_argument("--ledger", default=str(DEFAULT_LEDGER))
|
|
474
|
+
ap.add_argument("--pages", type=int, default=20, help="--pull 最多翻多少页(每页 200 条)")
|
|
475
|
+
ap.add_argument("--json", action="store_true")
|
|
476
|
+
ap.add_argument("--pull-rates", action="store_true", help="只刷新权威费率表")
|
|
477
|
+
ap.add_argument("--no-local", action="store_true", help="忽略本机会话用量(只看代理侧)")
|
|
478
|
+
ap.add_argument("--snapshot", action="store_true", help="把当日快照追加到 usage/daily.jsonl(供定时器积累趋势)")
|
|
479
|
+
a = ap.parse_args(argv)
|
|
480
|
+
|
|
481
|
+
if a.pull_rates:
|
|
482
|
+
pull_rates()
|
|
483
|
+
return 0
|
|
484
|
+
if a.pull:
|
|
485
|
+
pull_rates()
|
|
486
|
+
n = pull(a.pages)
|
|
487
|
+
scan_local()
|
|
488
|
+
if n < 0:
|
|
489
|
+
return 1
|
|
490
|
+
# ⚠️ 采集完必须继续走下面的"算 + 落快照":早期版本在这里直接 return,
|
|
491
|
+
# 于是定时器每天只采集、**从不写快照**(daily.jsonl 永远停在手动那一次)。
|
|
492
|
+
if not a.snapshot:
|
|
493
|
+
return 0
|
|
494
|
+
month = a.month or time.strftime("%Y-%m")
|
|
495
|
+
doc = load_registry()
|
|
496
|
+
rates = load_rates()
|
|
497
|
+
hist = load_history(HISTORY)
|
|
498
|
+
if not a.no_local:
|
|
499
|
+
hist = hist + load_local(rates)
|
|
500
|
+
hist.sort(key=lambda x: x["ts"])
|
|
501
|
+
attr = attribute(load_cases(pathlib.Path(a.ledger).expanduser(), doc), hist, doc)
|
|
502
|
+
s = summary(attr, month, doc)
|
|
503
|
+
USAGE.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
(USAGE / f"{month}-cost.json").write_text(json.dumps(s, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
505
|
+
if a.snapshot:
|
|
506
|
+
# 逐日追加(同一天只保留最后一条),供"跑一段时间"后看趋势/回归
|
|
507
|
+
line = {"date": time.strftime("%Y-%m-%d"), "month": month, "requests": s["requests"],
|
|
508
|
+
"costYuanLo": s["costYuanLo"], "costYuanHi": s["costYuanHi"],
|
|
509
|
+
"costYuan": s["bySource"]["local"]["costYuan"] + s["bySource"]["proxy"]["costYuanHi"],
|
|
510
|
+
"localFlowCostYuan": s["bySource"]["local"]["flowCostYuan"],
|
|
511
|
+
"chargedYuan": s["chargedYuan"], "attributedYuanHi": s["attributedYuanHi"],
|
|
512
|
+
"overheadYuanHi": s["overheadYuanHi"], "tickets": s["ticketsCounted"],
|
|
513
|
+
"perCapability": {k: {"tickets": v["tickets"], "yuanHiPerTicket": v["yuanHiPerTicket"]}
|
|
514
|
+
for k, v in s["perCapability"].items()}}
|
|
515
|
+
daily = USAGE / "daily.jsonl"
|
|
516
|
+
keep = []
|
|
517
|
+
if daily.exists():
|
|
518
|
+
keep = [l for l in daily.read_text(encoding="utf-8").splitlines()
|
|
519
|
+
if l.strip() and json.loads(l).get("date") != line["date"]]
|
|
520
|
+
daily.write_text("\n".join(keep + [json.dumps(line, ensure_ascii=False)]) + "\n", encoding="utf-8")
|
|
521
|
+
print(f" (快照追加:{daily})")
|
|
522
|
+
if a.json:
|
|
523
|
+
print(json.dumps(s, ensure_ascii=False, indent=2))
|
|
524
|
+
else:
|
|
525
|
+
print(render_report(s))
|
|
526
|
+
print(f" (快照:{USAGE / (month + '-cost.json')})")
|
|
527
|
+
return 0
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
if __name__ == "__main__":
|
|
531
|
+
raise SystemExit(main())
|