@whyshu/argent-engine 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. 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())