@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,181 +1,212 @@
1
+ /**
2
+ * LLM 官方定价漂移监测(issue #49 长效机制)。
3
+ *
4
+ * 设计纪律(与 ADR-11 / issue #49 一致):
5
+ * - 永不自动 apply 价格变更到 llm-price-table.json——价格调整必须走 PR,
6
+ * 由人 review;founder 2026-08-26 实测「自动 apply 容易把 down 5% 的官方新价错认为我的 bug」;
7
+ * - 本脚本只产出 PR-就绪的 Markdown diff,供人粘贴到 PR 描述;
8
+ * - 联网取数(--fetch 模式)有超时 + 离线 SKIP,默认行为零网络,适配 CI 纪律;
9
+ * - 解析规则保守:任何一行解析失败 → 整家 SKIP + warn,不写未知数据;
10
+ * - 比较口径:与 fixture 缓存的上次官方价对照——若 fixture 不存在,fetch 模式写入新 fixture,
11
+ * diff 模式直接报"首次运行,无 baseline"。
12
+ *
13
+ * 运行(在 ts/ 下):
14
+ * npx tsx scripts/price-drift-watch.ts # 离线模式:对照 fixture 算 diff(零网络)
15
+ * npx tsx scripts/price-drift-watch.ts --fetch # 联网模式:fetch 官方页 + 写 fixture + 算 diff
16
+ * npx tsx scripts/price-drift-watch.ts --format json
17
+ *
18
+ * 输出:stdout Markdown(默认)/JSON(--format json),心形:
19
+ * {
20
+ * schema_version: 'gotry_llm_price_drift_v1',
21
+ * observed_at: '<ISO>',
22
+ * providers: {
23
+ * deepseek: { status: 'unchanged' | 'drift' | 'skipped_no_baseline' | 'skipped_parse_error' | 'skipped_no_network', drift: [...] },
24
+ * minimax: { ... },
25
+ * openai: { ... },
26
+ * anthropic: { ... }
27
+ * },
28
+ * suggested_pr_paragraph: '<可直接粘贴到 PR 描述的一段>'
29
+ * }
30
+ *
31
+ * 心智模型:
32
+ * - provider="deepseek": fixture 文件 data/llm-price-baseline-deepseek.json,
33
+ * 上次 fetch 的官方价(rows 数组 + as_of);若不存在 → "skipped_no_baseline",
34
+ * 提示:首次跑 --fetch 一次入 fixture。
35
+ * - provider="minimax": 同上,data/llm-price-baseline-minimax.json。
36
+ * - provider="openai": 同上,data/llm-price-baseline-openai.json。
37
+ * - provider="anthropic": 同上,data/llm-price-baseline-anthropic.json。
38
+ * - 已入价表的 provider(当前为 deepseek/minimax)才有 baseline 期望;
39
+ * 未入价表的 provider(openai/anthropic)fetch 模式采得就当 narrative 记录,
40
+ * 仍不进 llm-price-table.json(那是未来单独的 PR)。
41
+ */
1
42
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
43
  const FETCH_TIMEOUT_MS = 8000;
4
44
  const DEFAULT_PROVIDERS = [
5
- {
6
- id: 'deepseek',
7
- source_url: 'https://api-docs.deepseek.com/quick_start/pricing',
8
- baseline_path: 'data/llm-price-baseline-deepseek.json',
9
- parse: parseDeepseekHtml
10
- },
11
- {
12
- id: 'minimax',
13
- source_url: 'https://platform.minimax.io/docs/guides/pricing-paygo',
14
- baseline_path: 'data/llm-price-baseline-minimax.json',
15
- parse: parseMinimaxHtml
16
- },
17
- {
18
- id: 'openai',
19
- source_url: 'https://openai.com/api/pricing/',
20
- baseline_path: 'data/llm-price-baseline-openai.json',
21
- snapshot_path: 'data/llm-pricing-snapshot-openai.html',
22
- parse: parseOpenaiHtml
23
- },
24
- {
25
- id: 'anthropic',
26
- source_url: 'https://www.anthropic.com/pricing',
27
- baseline_path: 'data/llm-price-baseline-anthropic.json',
28
- snapshot_path: 'data/llm-pricing-snapshot-anthropic.html',
29
- parse: parseAnthropicHtml
30
- }
45
+ { id: 'deepseek', source_url: 'https://api-docs.deepseek.com/quick_start/pricing', baseline_path: 'data/llm-price-baseline-deepseek.json', parse: parseDeepseekHtml },
46
+ { id: 'minimax', source_url: 'https://platform.minimax.io/docs/guides/pricing-paygo', baseline_path: 'data/llm-price-baseline-minimax.json', parse: parseMinimaxHtml },
47
+ { id: 'openai', source_url: 'https://openai.com/api/pricing/', baseline_path: 'data/llm-price-baseline-openai.json', snapshot_path: 'data/llm-pricing-snapshot-openai.html', parse: parseOpenaiHtml },
48
+ { id: 'anthropic', source_url: 'https://www.anthropic.com/pricing', baseline_path: 'data/llm-price-baseline-anthropic.json', snapshot_path: 'data/llm-pricing-snapshot-anthropic.html', parse: parseAnthropicHtml },
31
49
  ];
32
50
  async function fetchWithTimeout(url, timeoutMs) {
33
51
  const controller = new AbortController();
34
- const timer = setTimeout(()=>controller.abort(), timeoutMs);
52
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
35
53
  try {
36
- const res = await fetch(url, {
37
- signal: controller.signal,
38
- headers: {
39
- 'User-Agent': 'GoTry-price-drift-watch/1 (issue #49)'
40
- }
41
- });
42
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
54
+ const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'GoTry-price-drift-watch/1 (issue #49)' } });
55
+ if (!res.ok)
56
+ throw new Error(`HTTP ${res.status}`);
43
57
  return await res.text();
44
- } finally{
58
+ }
59
+ finally {
45
60
  clearTimeout(timer);
46
61
  }
47
62
  }
63
+ /** DeepSeek 官方定价页是简单 HTML 表格;粗解析:美元数字 $/M token 表行。
64
+ * 失败时不抛:返回 null,watcher 标 skipped_parse_error。 */
48
65
  function parseDeepseekHtml(html) {
66
+ // DeepSeek 表格行形如:
67
+ // <td>deepseek-v4-flash</td><td>0.44</td><td>0.014</td><td>1.32</td> (cache_miss/cache_hit/output per 1M)
49
68
  const rows = [];
50
69
  const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
51
70
  const cellRe = /<td[^>]*>([\s\S]*?)<\/td>/g;
52
71
  let tr;
53
- while((tr = rowRe.exec(html)) !== null){
72
+ while ((tr = rowRe.exec(html)) !== null) {
54
73
  const cells = [];
55
74
  let m;
56
- while((m = cellRe.exec(tr[1])) !== null)cells.push(m[1].replace(/<[^>]+>/g, '').trim());
57
- if (cells.length < 4) continue;
75
+ while ((m = cellRe.exec(tr[1])) !== null)
76
+ cells.push(m[1].replace(/<[^>]+>/g, '').trim());
77
+ if (cells.length < 4)
78
+ continue;
58
79
  const model = cells[0];
59
- if (!/^deepseek-v[34]/.test(model)) continue;
80
+ if (!/^deepseek-v[34]/.test(model))
81
+ continue;
60
82
  const miss = Number(cells[1]);
61
83
  const hit = Number(cells[2]);
62
84
  const out = Number(cells[3]);
63
- if (!Number.isFinite(miss) || !Number.isFinite(out)) continue;
64
- rows.push({
65
- model,
66
- input_cache_miss_usd_per_1m: miss,
67
- input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null,
68
- output_usd_per_1m: out
69
- });
85
+ if (!Number.isFinite(miss) || !Number.isFinite(out))
86
+ continue;
87
+ rows.push({ model, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null, output_usd_per_1m: out });
70
88
  }
71
89
  return rows.length > 0 ? rows : null;
72
90
  }
91
+ /** MiniMax Pay-as-you-go 页面是 markdown 化 HTML 文档,粗解析:model + $/M tokens 行。 */
73
92
  function parseMinimaxHtml(html) {
74
93
  const rows = [];
94
+ // 行形如: **MiniMax-M3** … $0.30 / M tokens (Input, ≤512k) … $1.20 / M tokens (Output, ≤512k) … $0.06 / M tokens (Cache Read)
75
95
  const lineRe = /<p[^>]*>([\s\S]*?)<\/p>/g;
76
96
  let p;
77
97
  const seen = new Set();
78
- while((p = lineRe.exec(html)) !== null){
98
+ while ((p = lineRe.exec(html)) !== null) {
79
99
  const text = p[1].replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
80
100
  const modelMatch = text.match(/(MiniMax-M2(?:\.1)?(?:-highspeed)?|MiniMax-M3)/);
81
- if (!modelMatch) continue;
101
+ if (!modelMatch)
102
+ continue;
82
103
  const model = modelMatch[1];
83
- const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*M\s*tokens?/g)).map((m)=>Number(m[1]));
84
- if (numbers.length < 2) continue;
104
+ // 取输入/输出/缓存读取三个数;按出现顺序:Input, Output, Cache Read
105
+ const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*M\s*tokens?/g)).map(m => Number(m[1]));
106
+ if (numbers.length < 2)
107
+ continue;
108
+ // 输入/输出 两个必备;Cache Read 可选
85
109
  const [miss, out] = numbers;
86
110
  const hit = numbers.length >= 3 ? numbers[2] : null;
87
- if (seen.has(model)) continue;
111
+ if (seen.has(model))
112
+ continue;
88
113
  seen.add(model);
89
- rows.push({
90
- model,
91
- input_cache_miss_usd_per_1m: miss,
92
- input_cache_hit_usd_per_1m: hit,
93
- output_usd_per_1m: out
94
- });
114
+ rows.push({ model, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: hit, output_usd_per_1m: out });
95
115
  }
96
116
  return rows.length > 0 ? rows : null;
97
117
  }
118
+ /** OpenAI 公开页是 React SPA(服务端只返壳 JSON 与内嵌 <script>)。
119
+ * 解析策略:`data/llm-pricing-snapshot-openai.html` snapshot 优先(入 git,CI 零联网);
120
+ * `--fetch` 模式覆盖 snapshot;解析取自 Next.js 嵌入的 `__NEXT_DATA__` JSON dump
121
+ * (其 `props.pageProps` 含 model price 数组),失败回退到段落文字粗解析。 */
98
122
  export function parseOpenaiHtml(html) {
99
123
  const fromNextData = parseOpenaiNextData(html);
100
- if (fromNextData) return fromNextData;
124
+ if (fromNextData)
125
+ return fromNextData;
101
126
  return parseOpenaiParagraphFallback(html);
102
127
  }
103
128
  function parseOpenaiNextData(html) {
104
129
  const m = html.match(/<script[^>]*id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
105
- if (!m) return null;
130
+ if (!m)
131
+ return null;
106
132
  try {
107
133
  const data = JSON.parse(m[1]);
108
134
  const models = data?.props?.pageProps?.models;
109
- if (!Array.isArray(models) || models.length === 0) return null;
135
+ if (!Array.isArray(models) || models.length === 0)
136
+ return null;
110
137
  const rows = [];
111
- for (const m of models){
138
+ for (const m of models) {
112
139
  const name = String(m['name'] ?? m['model'] ?? '').trim();
113
- if (!name) continue;
140
+ if (!name)
141
+ continue;
142
+ // 启发式:per-token 数字通常极小(<1),per-1M 数字通常 ≥ 0.1;前者要 ×1_000_000
114
143
  const rawMiss = Number(m['input_cost_per_token'] ?? m['input_per_million'] ?? m['input']);
115
144
  const rawOut = Number(m['output_cost_per_token'] ?? m['output_per_million'] ?? m['output']);
116
145
  const rawHit = Number(m['cached_input_cost_per_token'] ?? m['cached_input_per_million'] ?? m['cache_read']);
117
- if (!Number.isFinite(rawMiss) || !Number.isFinite(rawOut)) continue;
146
+ if (!Number.isFinite(rawMiss) || !Number.isFinite(rawOut))
147
+ continue;
118
148
  rows.push({
119
149
  model: name,
120
150
  input_cache_miss_usd_per_1m: rawMiss < 1 ? rawMiss * 1_000_000 : rawMiss,
121
- input_cache_hit_usd_per_1m: Number.isFinite(rawHit) ? rawHit < 1 ? rawHit * 1_000_000 : rawHit : null,
122
- output_usd_per_1m: rawOut < 1 ? rawOut * 1_000_000 : rawOut
151
+ input_cache_hit_usd_per_1m: Number.isFinite(rawHit) ? (rawHit < 1 ? rawHit * 1_000_000 : rawHit) : null,
152
+ output_usd_per_1m: rawOut < 1 ? rawOut * 1_000_000 : rawOut,
123
153
  });
124
154
  }
125
155
  return rows.length > 0 ? rows : null;
126
- } catch {
156
+ }
157
+ catch {
127
158
  return null;
128
159
  }
129
160
  }
130
161
  function parseOpenaiParagraphFallback(html) {
162
+ // 兜底:段落文字形如 "gpt-4o … $2.50 / 1M input … $10.00 / 1M output"
131
163
  const rows = [];
132
164
  const lineRe = /<p[^>]*>([\s\S]*?)<\/p>|<li[^>]*>([\s\S]*?)<\/li>/g;
133
165
  let m;
134
- while((m = lineRe.exec(html)) !== null){
166
+ while ((m = lineRe.exec(html)) !== null) {
135
167
  const text = (m[1] ?? m[2] ?? '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
136
168
  const modelMatch = text.match(/(gpt-4o(?:-mini)?|gpt-4\.1(?:-mini)?|gpt-5(?:-mini)?|o[1-9](?:-mini|-pro)?)/);
137
- if (!modelMatch) continue;
138
- const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M)/g)).map((x)=>Number(x[1]));
139
- if (numbers.length < 2) continue;
169
+ if (!modelMatch)
170
+ continue;
171
+ const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M)/g)).map(x => Number(x[1]));
172
+ if (numbers.length < 2)
173
+ continue;
140
174
  const [miss, out] = numbers;
141
- rows.push({
142
- model: modelMatch[1],
143
- input_cache_miss_usd_per_1m: miss,
144
- input_cache_hit_usd_per_1m: null,
145
- output_usd_per_1m: out
146
- });
175
+ rows.push({ model: modelMatch[1], input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: null, output_usd_per_1m: out });
147
176
  }
148
177
  return rows.length > 0 ? rows : null;
149
178
  }
179
+ /** Anthropic pricing 页同样 SPA,内嵌 `__NEXT_DATA__` 形态可能变;双路径同 OpenAI。 */
150
180
  function parseAnthropicHtml(html) {
151
181
  const fromNextData = parseAnthropicNextData(html);
152
- if (fromNextData) return fromNextData;
182
+ if (fromNextData)
183
+ return fromNextData;
153
184
  return parseAnthropicParagraphFallback(html);
154
185
  }
155
186
  function parseAnthropicNextData(html) {
156
187
  const m = html.match(/<script[^>]*id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
157
- if (!m) return null;
188
+ if (!m)
189
+ return null;
158
190
  try {
159
191
  const data = JSON.parse(m[1]);
160
192
  const pricing = data?.props?.pageProps?.pricing;
161
- if (!Array.isArray(pricing) || pricing.length === 0) return null;
193
+ if (!Array.isArray(pricing) || pricing.length === 0)
194
+ return null;
162
195
  const rows = [];
163
- for (const p of pricing){
196
+ for (const p of pricing) {
164
197
  const name = String(p['model'] ?? p['name'] ?? '').trim();
165
- if (!name) continue;
198
+ if (!name)
199
+ continue;
166
200
  const miss = Number(p['input_cost_per_mtok'] ?? p['input']);
167
201
  const out = Number(p['output_cost_per_mtok'] ?? p['output']);
168
202
  const hit = Number(p['cache_read_cost_per_mtok'] ?? p['cache_read']);
169
- if (!Number.isFinite(miss) || !Number.isFinite(out)) continue;
170
- rows.push({
171
- model: name,
172
- input_cache_miss_usd_per_1m: miss,
173
- input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null,
174
- output_usd_per_1m: out
175
- });
203
+ if (!Number.isFinite(miss) || !Number.isFinite(out))
204
+ continue;
205
+ rows.push({ model: name, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null, output_usd_per_1m: out });
176
206
  }
177
207
  return rows.length > 0 ? rows : null;
178
- } catch {
208
+ }
209
+ catch {
179
210
  return null;
180
211
  }
181
212
  }
@@ -183,103 +214,71 @@ function parseAnthropicParagraphFallback(html) {
183
214
  const rows = [];
184
215
  const lineRe = /<p[^>]*>([\s\S]*?)<\/p>|<li[^>]*>([\s\S]*?)<\/li>/g;
185
216
  let m;
186
- while((m = lineRe.exec(html)) !== null){
217
+ while ((m = lineRe.exec(html)) !== null) {
187
218
  const text = (m[1] ?? m[2] ?? '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
188
219
  const modelMatch = text.match(/(claude-(?:opus|sonnet|haiku)-[0-9](?:\.[0-9])?(?:-[0-9]+k)?)/);
189
- if (!modelMatch) continue;
190
- const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M|MT)/g)).map((x)=>Number(x[1]));
191
- if (numbers.length < 2) continue;
220
+ if (!modelMatch)
221
+ continue;
222
+ const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M|MT)/g)).map(x => Number(x[1]));
223
+ if (numbers.length < 2)
224
+ continue;
192
225
  const [miss, out] = numbers;
193
- rows.push({
194
- model: modelMatch[1],
195
- input_cache_miss_usd_per_1m: miss,
196
- input_cache_hit_usd_per_1m: null,
197
- output_usd_per_1m: out
198
- });
226
+ rows.push({ model: modelMatch[1], input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: null, output_usd_per_1m: out });
199
227
  }
200
228
  return rows.length > 0 ? rows : null;
201
229
  }
202
230
  function diffRows(baseline, current) {
203
- const baseMap = new Map(baseline.map((r)=>[
204
- r.model,
205
- r
206
- ]));
207
- const currMap = new Map(current.map((r)=>[
208
- r.model,
209
- r
210
- ]));
231
+ const baseMap = new Map(baseline.map(r => [r.model, r]));
232
+ const currMap = new Map(current.map(r => [r.model, r]));
211
233
  const drift = [];
212
- for (const [model, curr] of currMap){
234
+ for (const [model, curr] of currMap) {
213
235
  const base = baseMap.get(model);
214
236
  if (!base) {
215
- drift.push({
216
- model,
217
- field: 'input_cache_miss_usd_per_1m',
218
- from: null,
219
- to: curr.input_cache_miss_usd_per_1m,
220
- direction: 'new'
221
- });
237
+ drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: null, to: curr.input_cache_miss_usd_per_1m, direction: 'new' });
222
238
  continue;
223
239
  }
224
240
  if (base.input_cache_miss_usd_per_1m !== curr.input_cache_miss_usd_per_1m) {
225
- drift.push({
226
- model,
227
- field: 'input_cache_miss_usd_per_1m',
228
- from: base.input_cache_miss_usd_per_1m,
229
- to: curr.input_cache_miss_usd_per_1m,
230
- direction: curr.input_cache_miss_usd_per_1m > base.input_cache_miss_usd_per_1m ? 'up' : 'down'
231
- });
241
+ drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: base.input_cache_miss_usd_per_1m, to: curr.input_cache_miss_usd_per_1m, direction: curr.input_cache_miss_usd_per_1m > base.input_cache_miss_usd_per_1m ? 'up' : 'down' });
232
242
  }
233
243
  if (base.input_cache_hit_usd_per_1m !== curr.input_cache_hit_usd_per_1m) {
234
- drift.push({
235
- model,
236
- field: 'input_cache_hit_usd_per_1m',
237
- from: base.input_cache_hit_usd_per_1m,
238
- to: curr.input_cache_hit_usd_per_1m,
239
- direction: (curr.input_cache_hit_usd_per_1m ?? 0) > (base.input_cache_hit_usd_per_1m ?? 0) ? 'up' : 'down'
240
- });
244
+ drift.push({ model, field: 'input_cache_hit_usd_per_1m', from: base.input_cache_hit_usd_per_1m, to: curr.input_cache_hit_usd_per_1m, direction: (curr.input_cache_hit_usd_per_1m ?? 0) > (base.input_cache_hit_usd_per_1m ?? 0) ? 'up' : 'down' });
241
245
  }
242
246
  if (base.output_usd_per_1m !== curr.output_usd_per_1m) {
243
- drift.push({
244
- model,
245
- field: 'output_usd_per_1m',
246
- from: base.output_usd_per_1m,
247
- to: curr.output_usd_per_1m,
248
- direction: curr.output_usd_per_1m > base.output_usd_per_1m ? 'up' : 'down'
249
- });
247
+ drift.push({ model, field: 'output_usd_per_1m', from: base.output_usd_per_1m, to: curr.output_usd_per_1m, direction: curr.output_usd_per_1m > base.output_usd_per_1m ? 'up' : 'down' });
250
248
  }
251
249
  }
252
- for (const [model, base] of baseMap){
250
+ for (const [model, base] of baseMap) {
253
251
  if (!currMap.has(model)) {
254
- drift.push({
255
- model,
256
- field: 'input_cache_miss_usd_per_1m',
257
- from: base.input_cache_miss_usd_per_1m,
258
- to: null,
259
- direction: 'removed'
260
- });
252
+ drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: base.input_cache_miss_usd_per_1m, to: null, direction: 'removed' });
261
253
  }
262
254
  }
263
255
  return drift;
264
256
  }
265
257
  function loadBaseline(path) {
266
- if (!existsSync(path)) return null;
258
+ if (!existsSync(path))
259
+ return null;
267
260
  try {
268
261
  const raw = JSON.parse(readFileSync(path, 'utf8'));
269
- if (raw['schema_version'] !== 'gotry_llm_price_baseline_v1') throw new Error(`baseline ${path} schema_version must be gotry_llm_price_baseline_v1, got ${String(raw['schema_version'])}`);
262
+ if (raw['schema_version'] !== 'gotry_llm_price_baseline_v1')
263
+ throw new Error(`baseline ${path} schema_version must be gotry_llm_price_baseline_v1, got ${String(raw['schema_version'])}`);
270
264
  const rows = raw['rows'];
271
- if (!Array.isArray(rows)) throw new Error(`baseline ${path} rows must be array`);
265
+ if (!Array.isArray(rows))
266
+ throw new Error(`baseline ${path} rows must be array`);
272
267
  return raw;
273
- } catch (error) {
268
+ }
269
+ catch (error) {
274
270
  return null;
275
271
  }
276
272
  }
273
+ /** 读 snapshot HTML 文件;不存在或空 → null。snapshot 由 `--fetch` 模式首次写盘(founder 手动 PR)。 */
277
274
  function loadSnapshot(path) {
278
- if (!existsSync(path)) return null;
275
+ if (!existsSync(path))
276
+ return null;
279
277
  try {
280
278
  const text = readFileSync(path, 'utf8');
281
279
  return text.length > 0 ? text : null;
282
- } catch {
280
+ }
281
+ catch {
283
282
  return null;
284
283
  }
285
284
  }
@@ -291,7 +290,7 @@ function writeBaseline(path, rows, sourceUrl) {
291
290
  schema_version: 'gotry_llm_price_baseline_v1',
292
291
  as_of: new Date().toISOString(),
293
292
  source_url: sourceUrl,
294
- rows
293
+ rows,
295
294
  };
296
295
  writeFileSync(path, JSON.stringify(payload, null, 2) + '\n', 'utf8');
297
296
  }
@@ -299,107 +298,70 @@ export async function runPriceDriftWatch(options) {
299
298
  const observedAt = options.observedAt().toISOString();
300
299
  const providers = {};
301
300
  const providerSpecs = options.providers ?? DEFAULT_PROVIDERS;
302
- const fetcher = options.fetcher ?? ((url)=>fetchWithTimeout(url, FETCH_TIMEOUT_MS));
303
- for (const spec of providerSpecs){
301
+ const fetcher = options.fetcher ?? ((url) => fetchWithTimeout(url, FETCH_TIMEOUT_MS));
302
+ for (const spec of providerSpecs) {
304
303
  const baseline = loadBaseline(spec.baseline_path);
305
304
  const snapshot = spec.snapshot_path ? loadSnapshot(spec.snapshot_path) : null;
306
305
  if (options.mode === 'offline') {
306
+ // 离线模式两种来源:
307
+ // (a) 有 snapshot → 走 snapshot.html parse 出 current,与 baseline diff(零联网,CI 心形)
308
+ // (b) 无 snapshot → 只能报 baseline 状态(无东西可 diff,标 unchanged + 提示)
307
309
  if (!baseline) {
308
- providers[spec.id] = {
309
- status: 'skipped_no_baseline',
310
- source_url: spec.source_url,
311
- reason: `no baseline fixture at ${spec.baseline_path}; run --fetch once to seed`
312
- };
310
+ providers[spec.id] = { status: 'skipped_no_baseline', source_url: spec.source_url, reason: `no baseline fixture at ${spec.baseline_path}; run --fetch once to seed` };
313
311
  continue;
314
312
  }
315
313
  if (snapshot === null) {
316
314
  if (spec.snapshot_path) {
317
- providers[spec.id] = {
318
- status: 'skipped_no_snapshot',
319
- source_url: spec.source_url,
320
- reason: `no snapshot at ${spec.snapshot_path}; run --fetch once to seed (or: founder manually save official pricing page HTML as the snapshot)`
321
- };
322
- } else {
323
- providers[spec.id] = {
324
- status: 'unchanged',
325
- source_url: spec.source_url,
326
- reason: 'offline mode; baseline present, run --fetch to compare against current official prices'
327
- };
315
+ providers[spec.id] = { status: 'skipped_no_snapshot', source_url: spec.source_url, reason: `no snapshot at ${spec.snapshot_path}; run --fetch once to seed (or: founder manually save official pricing page HTML as the snapshot)` };
316
+ }
317
+ else {
318
+ providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, reason: 'offline mode; baseline present, run --fetch to compare against current official prices' };
328
319
  }
329
320
  continue;
330
321
  }
331
322
  const currentFromSnapshot = spec.parse(snapshot);
332
323
  if (!currentFromSnapshot) {
333
- providers[spec.id] = {
334
- status: 'skipped_parse_error',
335
- source_url: spec.source_url,
336
- reason: 'snapshot present but parser failed to extract rows; refresh snapshot or fix parser'
337
- };
324
+ providers[spec.id] = { status: 'skipped_parse_error', source_url: spec.source_url, reason: 'snapshot present but parser failed to extract rows; refresh snapshot or fix parser' };
338
325
  continue;
339
326
  }
340
327
  const driftFromSnapshot = diffRows(baseline.rows, currentFromSnapshot) ?? [];
341
328
  if (driftFromSnapshot.length === 0) {
342
- providers[spec.id] = {
343
- status: 'unchanged',
344
- source_url: spec.source_url,
345
- reason: 'offline; baseline == snapshot-derived current'
346
- };
347
- } else {
348
- providers[spec.id] = {
349
- status: 'drift',
350
- source_url: spec.source_url,
351
- fetched_at: observedAt,
352
- drift: driftFromSnapshot,
353
- reason: 'offline diff baseline vs snapshot'
354
- };
329
+ providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, reason: 'offline; baseline == snapshot-derived current' };
330
+ }
331
+ else {
332
+ providers[spec.id] = { status: 'drift', source_url: spec.source_url, fetched_at: observedAt, drift: driftFromSnapshot, reason: 'offline diff baseline vs snapshot' };
355
333
  }
356
334
  continue;
357
335
  }
336
+ // fetch mode
358
337
  let html;
359
338
  try {
360
339
  html = await fetcher(spec.source_url);
361
- } catch (error) {
362
- providers[spec.id] = {
363
- status: 'skipped_no_network',
364
- source_url: spec.source_url,
365
- reason: `fetch failed: ${error.message}`
366
- };
340
+ }
341
+ catch (error) {
342
+ providers[spec.id] = { status: 'skipped_no_network', source_url: spec.source_url, reason: `fetch failed: ${error.message}` };
367
343
  continue;
368
344
  }
369
- if (spec.snapshot_path) writeSnapshot(spec.snapshot_path, html);
345
+ // snapshot 写盘:OpenAI/Anthropic SPA 的真实状态在 snapshot 文件里(founder 手动 PR)
346
+ if (spec.snapshot_path)
347
+ writeSnapshot(spec.snapshot_path, html);
370
348
  const current = spec.parse(html);
371
349
  if (!current) {
372
- providers[spec.id] = {
373
- status: 'skipped_parse_error',
374
- source_url: spec.source_url,
375
- reason: 'parser did not extract any rows; snapshot was written anyway (manual review of fixture + parser required)'
376
- };
350
+ providers[spec.id] = { status: 'skipped_parse_error', source_url: spec.source_url, reason: 'parser did not extract any rows; snapshot was written anyway (manual review of fixture + parser required)' };
377
351
  continue;
378
352
  }
379
353
  if (!baseline) {
354
+ // 首次 fetch 写 fixture
380
355
  writeBaseline(spec.baseline_path, current, spec.source_url);
381
- providers[spec.id] = {
382
- status: 'fetched_no_table_entry',
383
- source_url: spec.source_url,
384
- fetched_at: observedAt,
385
- reason: 'first fetch; baseline fixture written; no current llm-price-table.json entry (add a PR if you want this provider in nightly-evidence cost math)'
386
- };
356
+ providers[spec.id] = { status: 'fetched_no_table_entry', source_url: spec.source_url, fetched_at: observedAt, reason: 'first fetch; baseline fixture written; no current llm-price-table.json entry (add a PR if you want this provider in nightly-evidence cost math)' };
387
357
  continue;
388
358
  }
389
359
  const drift = diffRows(baseline.rows, current) ?? [];
390
360
  if (drift.length === 0) {
391
- providers[spec.id] = {
392
- status: 'unchanged',
393
- source_url: spec.source_url,
394
- fetched_at: observedAt
395
- };
396
- } else {
397
- providers[spec.id] = {
398
- status: 'drift',
399
- source_url: spec.source_url,
400
- fetched_at: observedAt,
401
- drift
402
- };
361
+ providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, fetched_at: observedAt };
362
+ }
363
+ else {
364
+ providers[spec.id] = { status: 'drift', source_url: spec.source_url, fetched_at: observedAt, drift };
403
365
  }
404
366
  }
405
367
  const suggestedPrParagraph = renderSuggestedPr(providers, observedAt);
@@ -408,26 +370,27 @@ export async function runPriceDriftWatch(options) {
408
370
  observed_at: observedAt,
409
371
  mode: options.mode,
410
372
  providers,
411
- suggested_pr_paragraph: suggestedPrParagraph
373
+ suggested_pr_paragraph: suggestedPrParagraph,
412
374
  };
413
375
  }
414
376
  function renderSuggestedPr(providers, observedAt) {
415
- const lines = [
416
- `## LLM 价格漂移监测(${observedAt})`
417
- ];
418
- for (const [id, report] of Object.entries(providers)){
377
+ const lines = [`## LLM 价格漂移监测(${observedAt})`];
378
+ for (const [id, report] of Object.entries(providers)) {
419
379
  if (report.status === 'drift' && report.drift && report.drift.length > 0) {
420
380
  const driftItems = report.drift;
421
381
  lines.push(`- **${id}**(${report.source_url}):${driftItems.length} 项变动`);
422
- for (const d of driftItems){
382
+ for (const d of driftItems) {
423
383
  lines.push(` - \`${d.model}\` ${d.field}: ${d.from ?? '∅'} → ${d.to ?? '∅'} (${d.direction})`);
424
384
  }
425
385
  lines.push(' - 建议:按 ADR-11 复核,人工提 PR 改 `data/llm-price-table.json`。');
426
- } else if (report.status === 'unchanged') {
386
+ }
387
+ else if (report.status === 'unchanged') {
427
388
  lines.push(`- **${id}**:无变动`);
428
- } else if (report.status === 'fetched_no_table_entry') {
389
+ }
390
+ else if (report.status === 'fetched_no_table_entry') {
429
391
  lines.push(`- **${id}**:首次 fetch,baseline 已写入(${report.reason ?? ''})`);
430
- } else {
392
+ }
393
+ else {
431
394
  lines.push(`- **${id}** [${report.status}]:${report.reason ?? ''}`);
432
395
  }
433
396
  }
@@ -436,18 +399,17 @@ function renderSuggestedPr(providers, observedAt) {
436
399
  return lines.join('\n');
437
400
  }
438
401
  function formatMarkdown(report) {
439
- const lines = [
440
- `# LLM 价格漂移监测(${report.observed_at},mode=${report.mode})`,
441
- ''
442
- ];
443
- for (const [id, r] of Object.entries(report.providers)){
402
+ const lines = [`# LLM 价格漂移监测(${report.observed_at},mode=${report.mode})`, ''];
403
+ for (const [id, r] of Object.entries(report.providers)) {
444
404
  lines.push(`## ${id} [${r.status}]`);
445
405
  lines.push(`source: ${r.source_url}`);
446
- if (r.reason) lines.push(`reason: ${r.reason}`);
406
+ if (r.reason)
407
+ lines.push(`reason: ${r.reason}`);
447
408
  if (r.drift && r.drift.length > 0) {
448
409
  lines.push('| model | field | from | to | direction |');
449
410
  lines.push('|---|---|---|---|---|');
450
- for (const d of r.drift)lines.push(`| \`${d.model}\` | ${d.field} | ${d.from ?? '∅'} | ${d.to ?? '∅'} | ${d.direction} |`);
411
+ for (const d of r.drift)
412
+ lines.push(`| \`${d.model}\` | ${d.field} | ${d.from ?? '∅'} | ${d.to ?? '∅'} | ${d.direction} |`);
451
413
  }
452
414
  lines.push('');
453
415
  }
@@ -463,29 +425,23 @@ function arg(name) {
463
425
  async function main() {
464
426
  const asJson = (arg('--format') ?? 'markdown') === 'json';
465
427
  const mode = process.argv.includes('--fetch') ? 'fetch' : 'offline';
466
- mkdirSync('gotry-state', {
467
- recursive: true
468
- });
428
+ mkdirSync('gotry-state', { recursive: true });
469
429
  try {
470
- const report = await runPriceDriftWatch({
471
- mode,
472
- observedAt: ()=>new Date()
473
- });
474
- if (asJson) console.log(JSON.stringify(report, null, 2));
475
- else console.log(formatMarkdown(report));
476
- } catch (error) {
477
- const payload = {
478
- state: 'error',
479
- error: error.message
480
- };
481
- if (asJson) console.log(JSON.stringify(payload, null, 2));
482
- else console.error(`price-drift-watch error: ${error.message}`);
430
+ const report = await runPriceDriftWatch({ mode, observedAt: () => new Date() });
431
+ if (asJson)
432
+ console.log(JSON.stringify(report, null, 2));
433
+ else
434
+ console.log(formatMarkdown(report));
435
+ }
436
+ catch (error) {
437
+ const payload = { state: 'error', error: error.message };
438
+ if (asJson)
439
+ console.log(JSON.stringify(payload, null, 2));
440
+ else
441
+ console.error(`price-drift-watch error: ${error.message}`);
483
442
  process.exitCode = 1;
484
443
  }
485
444
  }
486
445
  if (process.argv[1]?.endsWith('price-drift-watch.ts')) {
487
446
  await main();
488
447
  }
489
-
490
-
491
- //# sourceURL=ts/scripts/price-drift-watch.ts