@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,357 +1,285 @@
1
+ /**
2
+ * Open-Meteo 天气能力层(免费无 key):预报 + 历史气候回退。
3
+ *
4
+ * 产品定位:替代 LLM 的「雨季常识」——季节性建议、窗口内降雨概率、
5
+ * 体感温度范围,全部走真实数据并带证据标注 [实时API:open-meteo@ts]。
6
+ *
7
+ * 契约(与 hbcli.ts 同构,能力层不变量):
8
+ * - 永不抛错:网络失败/超时/解析失败一律降级返回,不穿透到调用方;
9
+ * - 证据链标注:成功带 [实时API:open-meteo@ts],失败带 [实时API:open-meteo@error@ts];
10
+ * - 免费源无配额概念,但仍记录 latency(供 L5 成本工程度量)。
11
+ *
12
+ * 端点(全部免费,无 key):
13
+ * 预报: https://api.open-meteo.com/v1/forecast (≤16 天)
14
+ * 历史气候: https://archive-api.open-meteo.com/v1/archive (1940→,做季节性)
15
+ * 地理编码: https://geocoding-api.open-meteo.com/v1/search (地名→坐标)
16
+ */
1
17
  const FORECAST_BASE = 'https://api.open-meteo.com/v1/forecast';
2
18
  const ARCHIVE_BASE = 'https://archive-api.open-meteo.com/v1/archive';
3
19
  const GEOCODE_BASE = 'https://geocoding-api.open-meteo.com/v1/search';
20
+ /** WMO weather code → 中文描述(节选最常用的码) */
4
21
  const WMO_ZH = {
5
- 0: '晴',
6
- 1: '大致晴',
7
- 2: '多云',
8
- 3: '',
9
- 45: '',
10
- 48: '冻雾',
11
- 51: '毛毛雨',
12
- 53: '毛毛雨',
13
- 55: '密集毛毛雨',
14
- 61: '小雨',
15
- 63: '中雨',
16
- 65: '大雨',
17
- 66: '冻雨',
18
- 67: '强冻雨',
19
- 71: '小雪',
20
- 73: '中雪',
21
- 75: '大雪',
22
- 77: '雪粒',
23
- 80: '阵雨',
24
- 81: '强阵雨',
25
- 82: '暴雨',
26
- 85: '阵雪',
27
- 86: '强阵雪',
28
- 95: '雷暴',
29
- 96: '雷暴伴小冰雹',
30
- 99: '雷暴伴大冰雹'
22
+ 0: '晴', 1: '大致晴', 2: '多云', 3: '阴',
23
+ 45: '', 48: '冻雾',
24
+ 51: '毛毛雨', 53: '毛毛雨', 55: '密集毛毛雨',
25
+ 61: '小雨', 63: '中雨', 65: '大雨',
26
+ 66: '冻雨', 67: '强冻雨',
27
+ 71: '小雪', 73: '中雪', 75: '大雪', 77: '雪粒',
28
+ 80: '阵雨', 81: '强阵雨', 82: '暴雨',
29
+ 85: '阵雪', 86: '强阵雪',
30
+ 95: '雷暴', 96: '雷暴伴小冰雹', 99: '雷暴伴大冰雹',
31
31
  };
32
32
  export function wmoLabel(code) {
33
33
  return WMO_ZH[code] ?? `天气码${code}`;
34
34
  }
35
- const monotonicNow = ()=>performance.now();
35
+ const monotonicNow = () => performance.now();
36
36
  const MIN_TIMER_MS = 1;
37
37
  const TIMEOUT_ERROR = 'timeoutMs must be a finite positive integer';
38
38
  const BUDGET_EXHAUSTED_ERROR = 'total timeout budget exhausted';
39
39
  const REQUEST_ERROR = 'request failed';
40
- const normalizeTimeout = (value)=>Number.isInteger(value) && value > 0 ? value : null;
41
- const normalizeError = (value)=>{
40
+ const normalizeTimeout = (value) => Number.isInteger(value) && value > 0 ? value : null;
41
+ /** Rejection values are untrusted: only a real string message is safe to expose. */
42
+ const normalizeError = (value) => {
42
43
  try {
43
44
  if (value instanceof Error && typeof value.message === 'string' && value.message.length > 0) {
44
45
  return value.message.slice(0, 200);
45
46
  }
46
- if (typeof value === 'string' && value.length > 0) return value.slice(0, 200);
47
- } catch {
47
+ if (typeof value === 'string' && value.length > 0)
48
+ return value.slice(0, 200);
49
+ }
50
+ catch {
48
51
  return REQUEST_ERROR;
49
52
  }
50
53
  return REQUEST_ERROR;
51
54
  };
52
55
  async function fetchJson(url, timeoutMs, headers = {}, fetchImpl = globalThis.fetch) {
53
- if (!Number.isFinite(timeoutMs) || timeoutMs < MIN_TIMER_MS) return {
54
- ok: false,
55
- error: BUDGET_EXHAUSTED_ERROR
56
- };
56
+ if (!Number.isFinite(timeoutMs) || timeoutMs < MIN_TIMER_MS)
57
+ return { ok: false, error: BUDGET_EXHAUSTED_ERROR };
57
58
  const ctrl = new AbortController();
58
59
  const timerDelay = Math.floor(timeoutMs);
59
- if (timerDelay < MIN_TIMER_MS) return {
60
- ok: false,
61
- error: BUDGET_EXHAUSTED_ERROR
62
- };
60
+ if (timerDelay < MIN_TIMER_MS)
61
+ return { ok: false, error: BUDGET_EXHAUSTED_ERROR };
63
62
  let timer;
64
63
  let timedOut = false;
65
- const timeout = new Promise((_, reject)=>{
66
- timer = setTimeout(()=>{
64
+ const timeout = new Promise((_, reject) => {
65
+ timer = setTimeout(() => {
67
66
  timedOut = true;
68
67
  ctrl.abort();
69
68
  reject(new Error(`timeout after ${timeoutMs}ms`));
70
69
  }, timerDelay);
71
70
  });
72
- const operation = (async ()=>{
73
- const res = await fetchImpl(url, {
74
- signal: ctrl.signal,
75
- headers: {
76
- Accept: 'application/json',
77
- ...headers
78
- }
79
- });
80
- if (!res.ok) return {
81
- ok: false,
82
- error: `HTTP ${res.status}`
83
- };
84
- return {
85
- ok: true,
86
- data: await res.json()
87
- };
71
+ const operation = (async () => {
72
+ const res = await fetchImpl(url, { signal: ctrl.signal, headers: { Accept: 'application/json', ...headers } });
73
+ if (!res.ok)
74
+ return { ok: false, error: `HTTP ${res.status}` };
75
+ return { ok: true, data: await res.json() };
88
76
  })();
89
77
  try {
90
- return await Promise.race([
91
- operation,
92
- timeout
93
- ]);
94
- } catch (e) {
95
- return {
96
- ok: false,
97
- error: normalizeError(e),
98
- ...timedOut ? {
99
- timedOut: true
100
- } : {}
101
- };
102
- } finally{
103
- if (timer !== undefined) clearTimeout(timer);
104
- void operation.catch(()=>undefined);
78
+ return await Promise.race([operation, timeout]);
79
+ }
80
+ catch (e) {
81
+ return { ok: false, error: normalizeError(e), ...(timedOut ? { timedOut: true } : {}) };
82
+ }
83
+ finally {
84
+ if (timer !== undefined)
85
+ clearTimeout(timer);
86
+ void operation.catch(() => undefined);
105
87
  }
106
88
  }
107
89
  const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org/search';
90
+ /** 行政级/人口重镇 feature_code(open-meteo 命名惯例):PPLC=首都 PPLA*=首府 PPLB=城镇 PPLX=区片 */
108
91
  const MAJOR_FEATURE = /^(PPLC|PPLA|PPLB|PPLX)/;
109
- const isRecord = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
92
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
110
93
  function stripHits(hits) {
111
- return hits.map(({ name, latitude, longitude, country, admin1 })=>({
112
- ...country ? {
113
- country
114
- } : {},
115
- ...admin1 ? {
116
- admin1
117
- } : {},
118
- name,
119
- latitude,
120
- longitude
121
- }));
94
+ return hits.map(({ name, latitude, longitude, country, admin1 }) => ({ ...(country ? { country } : {}), ...(admin1 ? { admin1 } : {}), name, latitude, longitude }));
122
95
  }
96
+ /** 地名 → 坐标(双源,供 dsh 工具把「大理」转成经纬度):
97
+ * 1. Open-Meteo(主源):命中按人口/行政级降序——「大理」会同时命中云南州府与四川同名乡,不能盲信次序;
98
+ * 2. Nominatim(兜底):open-meteo 的中文名覆盖有洞(实测(issue #24)「普吉岛」0 结果、
99
+ * 裸词「普吉」错配西藏林芝同名村),弱命中/零结果时走 Nominatim 拿正确目标,
100
+ * 兜底仍空则回退 open-meteo 弱结果(不比此前更差)。 */
123
101
  export async function geocodePlace(name, opts = {}) {
124
102
  const ts = new Date().toISOString();
125
103
  const count = opts.count ?? 5;
126
104
  const query = name.trim();
127
105
  const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
128
- if (timeoutMs === null) return {
129
- ok: false,
130
- evidence: `[实时API:open-meteo-geo@error@${ts}]`,
131
- results: [],
132
- error: TIMEOUT_ERROR
133
- };
106
+ if (timeoutMs === null)
107
+ return { ok: false, evidence: `[实时API:open-meteo-geo@error@${ts}]`, results: [], error: TIMEOUT_ERROR };
134
108
  const omUrl = `${GEOCODE_BASE}?name=${encodeURIComponent(query)}&count=${count}&language=zh&format=json`;
109
+ // timeoutMs is a budget for the complete primary→fallback chain, not per request.
135
110
  const deadline = monotonicNow() + timeoutMs;
136
- const remaining = ()=>{
111
+ const remaining = () => {
137
112
  const value = deadline - monotonicNow();
138
113
  return Number.isFinite(value) && value >= MIN_TIMER_MS ? value : null;
139
114
  };
140
115
  const primaryRemaining = remaining();
141
- const r = primaryRemaining === null ? {
142
- ok: false,
143
- error: BUDGET_EXHAUSTED_ERROR
144
- } : await fetchJson(omUrl, primaryRemaining, {}, opts.fetchImpl);
116
+ const r = primaryRemaining === null
117
+ ? { ok: false, error: BUDGET_EXHAUSTED_ERROR }
118
+ : await fetchJson(omUrl, primaryRemaining, {}, opts.fetchImpl);
145
119
  const primaryPayload = r.ok && isRecord(r.data) && Array.isArray(r.data['results']) ? r.data['results'] : null;
146
- const omResults = primaryPayload ? primaryPayload.map((item)=>{
147
- if (!isRecord(item)) return null;
148
- const it = item;
149
- return {
150
- name: String(it['name'] ?? ''),
151
- latitude: Number(it['latitude']),
152
- longitude: Number(it['longitude']),
153
- country: it['country'] ? String(it['country']) : undefined,
154
- admin1: it['admin1'] ? String(it['admin1']) : undefined,
155
- population: Number(it['population'] ?? 0) || 0,
156
- featureCode: String(it['feature_code'] ?? '')
157
- };
158
- }).filter((h)=>!!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude)) : [];
159
- omResults.sort((a, b)=>b.population - a.population || Number(MAJOR_FEATURE.test(b.featureCode)) - Number(MAJOR_FEATURE.test(a.featureCode)));
120
+ const omResults = primaryPayload
121
+ ? primaryPayload.map((item) => {
122
+ if (!isRecord(item))
123
+ return null;
124
+ const it = item;
125
+ return {
126
+ name: String(it['name'] ?? ''),
127
+ latitude: Number(it['latitude']),
128
+ longitude: Number(it['longitude']),
129
+ country: it['country'] ? String(it['country']) : undefined,
130
+ admin1: it['admin1'] ? String(it['admin1']) : undefined,
131
+ population: Number(it['population'] ?? 0) || 0,
132
+ featureCode: String(it['feature_code'] ?? ''),
133
+ };
134
+ }).filter((h) => !!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude))
135
+ : [];
136
+ omResults.sort((a, b) => (b.population - a.population) || (Number(MAJOR_FEATURE.test(b.featureCode)) - Number(MAJOR_FEATURE.test(a.featureCode))));
160
137
  const top = omResults[0];
138
+ // 强命中:有人口规模或主要行政级,直接采信(「深圳市」「曼谷」路径不变)
161
139
  if (top && (top.population > 0 || MAJOR_FEATURE.test(top.featureCode))) {
162
- return {
163
- ok: true,
164
- evidence: `[实时API:open-meteo-geo@${ts}]`,
165
- via: 'open-meteo',
166
- results: stripHits(omResults)
167
- };
140
+ return { ok: true, evidence: `[实时API:open-meteo-geo@${ts}]`, via: 'open-meteo', results: stripHits(omResults) };
168
141
  }
142
+ // 弱命中/零结果/主源请求失败:Nominatim 兜底(免费无 key;中文 accept-language)
169
143
  const nomTs = new Date().toISOString();
170
144
  const nomUrl = `${NOMINATIM_BASE}?q=${encodeURIComponent(query)}&format=jsonv2&limit=${count}&accept-language=zh&addressdetails=1`;
171
145
  const fallbackRemaining = primaryRemaining === null || r.timedOut ? null : remaining();
172
- const nom = fallbackRemaining === null ? {
173
- ok: false,
174
- error: BUDGET_EXHAUSTED_ERROR
175
- } : await fetchJson(nomUrl, fallbackRemaining, {
176
- 'User-Agent': 'gotry-travel-agent/0.1 (+https://github.com/Danceiny/gotry)'
177
- }, opts.fetchImpl);
146
+ const nom = fallbackRemaining === null
147
+ ? { ok: false, error: BUDGET_EXHAUSTED_ERROR }
148
+ : await fetchJson(nomUrl, fallbackRemaining, { 'User-Agent': 'gotry-travel-agent/0.1 (+https://github.com/Danceiny/gotry)' }, opts.fetchImpl);
178
149
  const nomResults = [];
179
150
  if (nom.ok && Array.isArray(nom.data)) {
180
- const parsedNom = nom.data.map((item)=>{
181
- if (!isRecord(item)) return null;
151
+ const parsedNom = nom.data.map((item) => {
152
+ if (!isRecord(item))
153
+ return null;
182
154
  const it = item;
183
155
  const addr = isRecord(it['address']) ? it['address'] : {};
184
156
  const country = typeof addr['country'] === 'string' ? addr['country'] : undefined;
185
- const admin1 = [
186
- addr['province'],
187
- addr['state'],
188
- addr['county']
189
- ].find((v)=>typeof v === 'string');
190
- return {
191
- name: String(it['name'] ?? ''),
192
- latitude: Number(it['lat']),
193
- longitude: Number(it['lon']),
194
- ...country ? {
195
- country
196
- } : {},
197
- ...admin1 ? {
198
- admin1
199
- } : {}
200
- };
201
- }).filter((h)=>!!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude));
157
+ const admin1 = [addr['province'], addr['state'], addr['county']].find(v => typeof v === 'string');
158
+ return { name: String(it['name'] ?? ''), latitude: Number(it['lat']), longitude: Number(it['lon']), ...(country ? { country } : {}), ...(admin1 ? { admin1 } : {}) };
159
+ }).filter((h) => !!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude));
202
160
  nomResults.push(...parsedNom);
203
161
  }
204
162
  if (nomResults.length) {
205
- const omNote = r.ok ? `open-meteo ${omResults.length} 条弱命中(无人口/行政级,不足采信)` : `open-meteo 失败:${r.error ?? 'HTTP error'}`;
206
- return {
207
- ok: true,
208
- evidence: `[实时API:nominatim@${nomTs}](兜底层: ${omNote})`,
209
- via: 'nominatim',
210
- results: nomResults
211
- };
163
+ const omNote = r.ok
164
+ ? `open-meteo ${omResults.length} 条弱命中(无人口/行政级,不足采信)`
165
+ : `open-meteo 失败:${r.error ?? 'HTTP error'}`;
166
+ return { ok: true, evidence: `[实时API:nominatim@${nomTs}](兜底层: ${omNote})`, via: 'nominatim', results: nomResults };
212
167
  }
168
+ // 双源皆空/兜底失败:open-meteo 有弱结果仍返回(同构「降级不阻塞」契约),否则维持失败终态
213
169
  if (omResults.length) {
214
- return {
215
- ok: true,
216
- evidence: `[实时API:open-meteo-geo@${ts}]`,
217
- via: 'open-meteo',
218
- results: stripHits(omResults)
219
- };
170
+ return { ok: true, evidence: `[实时API:open-meteo-geo@${ts}]`, via: 'open-meteo', results: stripHits(omResults) };
220
171
  }
221
172
  return {
222
173
  ok: false,
223
174
  evidence: `[实时API:open-meteo-geo@error@${ts}];[实时API:nominatim@error@${nomTs}]`,
224
175
  results: [],
225
- error: r.ok ? `双源无结果:${nom.error ?? 'nominatim empty'}` : r.error
176
+ error: r.ok ? `双源无结果:${nom.error ?? 'nominatim empty'}` : (r.error),
226
177
  };
227
178
  }
179
+ /** 未来天气预报(≤16 天) */
228
180
  export async function getForecast(point, opts = {}) {
229
181
  const started = monotonicNow();
230
182
  const ts = new Date().toISOString();
231
183
  const requestedDays = opts.days ?? 7;
232
184
  const days = Number.isInteger(requestedDays) && requestedDays > 0 ? Math.min(requestedDays, 16) : 0;
233
185
  const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
234
- if (days < 1 || timeoutMs === null) return {
235
- ok: false,
236
- via: 'open-meteo-error',
237
- evidence: `[实时API:open-meteo@error@${ts}]`,
238
- latencyMs: monotonicNow() - started,
239
- error: days < 1 ? 'days must be valid positive values' : TIMEOUT_ERROR
240
- };
241
- const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode` + `&forecast_days=${days}&timezone=auto`;
186
+ if (days < 1 || timeoutMs === null)
187
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs: monotonicNow() - started, error: days < 1 ? 'days must be valid positive values' : TIMEOUT_ERROR };
188
+ const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
189
+ + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode`
190
+ + `&forecast_days=${days}&timezone=auto`;
242
191
  const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl);
243
192
  const latencyMs = monotonicNow() - started;
244
193
  if (!r.ok) {
245
- return {
246
- ok: false,
247
- via: 'open-meteo-error',
248
- evidence: `[实时API:open-meteo@error@${ts}]`,
249
- latencyMs,
250
- error: r.error
251
- };
194
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: r.error };
252
195
  }
253
196
  const payload = isRecord(r.data) ? r.data : null;
254
197
  const daily = isRecord(payload?.['daily']) ? payload['daily'] : null;
255
- if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max) || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode) || daily.time.length !== days || daily.temperature_2m_max.length !== days || daily.temperature_2m_min.length !== days || daily.weathercode.length !== days || daily.time.some((v)=>typeof v !== 'string') || daily.temperature_2m_max.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.temperature_2m_min.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.weathercode.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.precipitation_probability_max !== undefined && daily.precipitation_probability_max !== null && !Array.isArray(daily.precipitation_probability_max) || Array.isArray(daily.precipitation_probability_max) && (daily.precipitation_probability_max.length !== days || daily.precipitation_probability_max.some((v)=>v !== null && (typeof v !== 'number' || !Number.isFinite(v))))) {
256
- return {
257
- ok: false,
258
- via: 'open-meteo-error',
259
- evidence: `[实时API:open-meteo@error@${ts}]`,
260
- latencyMs,
261
- error: 'empty daily payload'
262
- };
198
+ if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max)
199
+ || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode)
200
+ || daily.time.length !== days || daily.temperature_2m_max.length !== days
201
+ || daily.temperature_2m_min.length !== days || daily.weathercode.length !== days
202
+ || daily.time.some(v => typeof v !== 'string')
203
+ || daily.temperature_2m_max.some(v => typeof v !== 'number' || !Number.isFinite(v))
204
+ || daily.temperature_2m_min.some(v => typeof v !== 'number' || !Number.isFinite(v))
205
+ || daily.weathercode.some(v => typeof v !== 'number' || !Number.isFinite(v))
206
+ || (daily.precipitation_probability_max !== undefined && daily.precipitation_probability_max !== null
207
+ && !Array.isArray(daily.precipitation_probability_max))
208
+ || (Array.isArray(daily.precipitation_probability_max)
209
+ && (daily.precipitation_probability_max.length !== days
210
+ || daily.precipitation_probability_max.some(v => v !== null && (typeof v !== 'number' || !Number.isFinite(v)))))) {
211
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: 'empty daily payload' };
263
212
  }
264
213
  const times = daily.time;
265
214
  const maxes = daily.temperature_2m_max;
266
215
  const mins = daily.temperature_2m_min;
267
216
  const codes = daily.weathercode;
268
- const precip = Array.isArray(daily.precipitation_probability_max) ? daily.precipitation_probability_max : undefined;
269
- const rows = times.map((date, i)=>({
270
- date,
271
- tempMaxC: maxes[i],
272
- tempMinC: mins[i],
273
- precipProbMaxPct: precip?.[i] ?? null,
274
- weatherCode: codes[i]
275
- }));
217
+ const precip = Array.isArray(daily.precipitation_probability_max)
218
+ ? daily.precipitation_probability_max
219
+ : undefined;
220
+ const rows = times.map((date, i) => ({
221
+ date, tempMaxC: maxes[i], tempMinC: mins[i], precipProbMaxPct: precip?.[i] ?? null, weatherCode: codes[i],
222
+ }));
276
223
  return {
277
- ok: true,
278
- via: 'open-meteo',
224
+ ok: true, via: 'open-meteo',
279
225
  evidence: `[实时API:open-meteo@${ts}]`,
280
226
  latencyMs,
281
227
  timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
282
- daily: rows
228
+ daily: rows,
283
229
  };
284
230
  }
231
+ /** 历史气候(季节性推荐底座:去年同期日均温/降雨概率) */
285
232
  export async function getClimate(point, month, opts = {}) {
286
233
  const started = monotonicNow();
287
234
  const ts = new Date().toISOString();
288
235
  const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
289
- if (timeoutMs === null) return {
290
- ok: false,
291
- via: 'open-meteo-error',
292
- evidence: `[实时API:open-meteo-climate@error@${ts}]`,
293
- latencyMs: monotonicNow() - started,
294
- error: TIMEOUT_ERROR
295
- };
296
- const year = opts.year ?? new Date().getUTCFullYear() - 1;
236
+ if (timeoutMs === null)
237
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs: monotonicNow() - started, error: TIMEOUT_ERROR };
238
+ // 默认取去年同月做气候基线
239
+ const year = opts.year ?? (new Date().getUTCFullYear() - 1);
297
240
  if (!Number.isInteger(month) || month < 1 || month > 12 || !Number.isInteger(year) || year < 1940 || year > new Date().getUTCFullYear()) {
298
- return {
299
- ok: false,
300
- via: 'open-meteo-error',
301
- evidence: `[实时API:open-meteo-climate@error@${ts}]`,
302
- latencyMs: monotonicNow() - started,
303
- error: 'month/year must be valid historical calendar values'
304
- };
241
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs: monotonicNow() - started, error: 'month/year must be valid historical calendar values' };
305
242
  }
306
243
  const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
307
- const expectedDates = Array.from({
308
- length: lastDay
309
- }, (_, i)=>`${year}-${String(month).padStart(2, '0')}-${String(i + 1).padStart(2, '0')}`);
244
+ const expectedDates = Array.from({ length: lastDay }, (_, i) => `${year}-${String(month).padStart(2, '0')}-${String(i + 1).padStart(2, '0')}`);
310
245
  const start = `${year}-${String(month).padStart(2, '0')}-01`;
311
246
  const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
312
- const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&start_date=${start}&end_date=${end}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`;
247
+ const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
248
+ + `&start_date=${start}&end_date=${end}`
249
+ + `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`;
313
250
  const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl);
314
251
  const latencyMs = monotonicNow() - started;
315
252
  if (!r.ok) {
316
- return {
317
- ok: false,
318
- via: 'open-meteo-error',
319
- evidence: `[实时API:open-meteo-climate@error@${ts}]`,
320
- latencyMs,
321
- error: r.error
322
- };
253
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: r.error };
323
254
  }
324
255
  const payload = isRecord(r.data) ? r.data : null;
325
256
  const daily = isRecord(payload?.['daily']) ? payload['daily'] : null;
326
- if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max) || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode) || daily.time.length !== lastDay || daily.time.length !== daily.temperature_2m_max.length || daily.time.length !== daily.temperature_2m_min.length || daily.time.length !== daily.weathercode.length || daily.time.some((v)=>typeof v !== 'string') || daily.time.some((v, i)=>v !== expectedDates[i]) || daily.temperature_2m_max.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.temperature_2m_min.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.weathercode.some((v)=>typeof v !== 'number' || !Number.isFinite(v))) {
327
- return {
328
- ok: false,
329
- via: 'open-meteo-error',
330
- evidence: `[实时API:open-meteo-climate@error@${ts}]`,
331
- latencyMs,
332
- error: 'empty daily payload'
333
- };
257
+ if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max)
258
+ || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode)
259
+ || daily.time.length !== lastDay
260
+ || daily.time.length !== daily.temperature_2m_max.length
261
+ || daily.time.length !== daily.temperature_2m_min.length
262
+ || daily.time.length !== daily.weathercode.length
263
+ || daily.time.some(v => typeof v !== 'string')
264
+ || daily.time.some((v, i) => v !== expectedDates[i])
265
+ || daily.temperature_2m_max.some(v => typeof v !== 'number' || !Number.isFinite(v))
266
+ || daily.temperature_2m_min.some(v => typeof v !== 'number' || !Number.isFinite(v))
267
+ || daily.weathercode.some(v => typeof v !== 'number' || !Number.isFinite(v))) {
268
+ return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: 'empty daily payload' };
334
269
  }
335
270
  const times = daily.time;
336
271
  const maxes = daily.temperature_2m_max;
337
272
  const mins = daily.temperature_2m_min;
338
273
  const codes = daily.weathercode;
339
- const rows = times.map((date, i)=>({
340
- date,
341
- tempMaxC: maxes[i],
342
- tempMinC: mins[i],
343
- precipProbMaxPct: null,
344
- weatherCode: codes[i]
345
- }));
274
+ const rows = times.map((date, i) => ({
275
+ date, tempMaxC: maxes[i], tempMinC: mins[i], precipProbMaxPct: null, // archive 端点没有概率,用降水量另行估算
276
+ weatherCode: codes[i],
277
+ }));
346
278
  return {
347
- ok: true,
348
- via: 'open-meteo',
279
+ ok: true, via: 'open-meteo',
349
280
  evidence: `[实时API:open-meteo-climate@${ts}]`,
350
281
  latencyMs,
351
282
  timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
352
- daily: rows
283
+ daily: rows,
353
284
  };
354
285
  }
355
-
356
-
357
- //# sourceURL=ts/capabilities/weather.ts
@@ -1,3 +1,8 @@
1
+ /**
2
+ * action-cache 自愈层确定性测试(RFC P2;run-all §25):
3
+ * 命中/指纹失配/TTL 过期/变量化 key/LRU 淘汰/损坏文件容错/持久化跨实例/幂等回写。
4
+ * 全纯函数 + 临时目录,零浏览器零网络。
5
+ */
1
6
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
7
  import { tmpdir } from 'node:os';
3
8
  import { join } from 'node:path';
@@ -8,7 +13,8 @@ function assert(cond, label, detail) {
8
13
  if (cond) {
9
14
  pass += 1;
10
15
  console.log(` ok - ${label}`);
11
- } else {
16
+ }
17
+ else {
12
18
  fail += 1;
13
19
  console.log(` FAIL - ${label}${detail !== undefined ? ' :: ' + String(detail) : ''}`);
14
20
  }
@@ -17,91 +23,50 @@ const iso = mkdtempSync(join(tmpdir(), 'gotry-actioncache-'));
17
23
  const path = actionCachePath(iso);
18
24
  const T0 = new Date('2026-08-28T10:00:00Z');
19
25
  let clock = T0.getTime();
20
- const now = ()=>new Date(clock);
26
+ const now = () => new Date(clock);
21
27
  const site = 'ctrip-flight';
22
28
  console.log('A. templateKey(参数变量化)');
23
- const k1 = templateKey(site, 'search', {
24
- from: '上海',
25
- to: '丽江',
26
- date: '2026-10-01'
27
- });
28
- const k2 = templateKey(site, 'search', {
29
- from: '上海',
30
- to: '丽江',
31
- date: '2026-11-11'
32
- });
29
+ const k1 = templateKey(site, 'search', { from: '上海', to: '丽江', date: '2026-10-01' });
30
+ const k2 = templateKey(site, 'search', { from: '上海', to: '丽江', date: '2026-11-11' });
33
31
  assert(k1 === k2 && k1 === 'ctrip-flight:search:上海|丽江|%date%', '同型查询共享模板 key(日期变量化),跨日期复用');
34
32
  console.log('B. 命中与指纹失配(被动失效)');
35
- const c = new ActionCache(path, {
36
- now
37
- });
33
+ const c = new ActionCache(path, { now });
38
34
  const fpA = fingerprint('batchSearch v1 shape {"flightItineraryList":[]}');
39
- const fpB = fingerprint('batchSearch v2 shape {"itineraries":[]}');
40
- c.record(k1, fpA, {
41
- entryUrl: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=%date%',
42
- parser: 'batchSearch.v1'
43
- }, site);
35
+ const fpB = fingerprint('batchSearch v2 shape {"itineraries":[]}'); // 接口改版
36
+ c.record(k1, fpA, { entryUrl: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=%date%', parser: 'batchSearch.v1' }, site);
44
37
  clock += 60_000;
45
38
  assert(c.lookup(k1, fpA, site)?.locator.parser === 'batchSearch.v1', '指纹一致 → 命中并回放确定性载荷');
46
39
  assert(c.lookup(k1, fpA, site)?.hits === 2, '命中计 hits');
47
40
  assert(c.lookup(k1, fpB, site) === null, '接口改版(指纹失配)→ miss 不误放(错误的缓存点击比慢更糟)');
48
- assert(c.lookup(templateKey(site, 'search', {
49
- from: '北京',
50
- to: '大理',
51
- date: '2026-10-01'
52
- }), fpA, site) === null, 'key 不同 → miss');
41
+ assert(c.lookup(templateKey(site, 'search', { from: '北京', to: '大理', date: '2026-10-01' }), fpA, site) === null, 'key 不同 → miss');
53
42
  console.log('C. miss 回写与幂等');
54
- c.record(k1, fpB, {
55
- entryUrl: 'oneway-bjs-dlu?depdate=%date%',
56
- parser: 'batchSearch.v2'
57
- }, site);
43
+ c.record(k1, fpB, { entryUrl: 'oneway-bjs-dlu?depdate=%date%', parser: 'batchSearch.v2' }, site);
58
44
  assert(c.lookup(k1, fpB, site)?.locator.parser === 'batchSearch.v2', '改版后回写新载荷,下次命中');
59
45
  const hitsBefore = c.lookup(k1, fpB, site).hits;
60
- c.record(k1, fpB, {
61
- parser: 'batchSearch.v2'
62
- }, site);
46
+ c.record(k1, fpB, { parser: 'batchSearch.v2' }, site);
63
47
  assert(c.lookup(k1, fpB, site).hits === hitsBefore + 1, '重复 record 幂等(hits/createdAt 保留,不重置)');
64
48
  console.log('D. TTL 过期(48h 默认;注入时钟)');
65
49
  clock += 49 * 3600_000;
66
50
  assert(c.lookup(k1, fpB, site) === null && c.size(site) === 0, '超 48h → 过期删除条目');
67
51
  console.log('E. LRU 淘汰(每站点上限)');
68
- const c2 = new ActionCache(path + '.lru', {
69
- now,
70
- maxEntriesPerSite: 3
71
- });
72
- for(let i = 0; i < 5; i++){
52
+ const c2 = new ActionCache(path + '.lru', { now, maxEntriesPerSite: 3 });
53
+ for (let i = 0; i < 5; i++) {
73
54
  const key = `ctrip-flight:search:城${i}|丽江|%date%`;
74
- c2.record(key, fingerprint(`shape-${i}`), {
75
- i: String(i)
76
- }, site);
55
+ c2.record(key, fingerprint(`shape-${i}`), { i: String(i) }, site);
77
56
  clock += 1_000;
78
57
  }
79
58
  assert(c2.size(site) === 3, '超容 LRU 淘汰至 3 条');
80
59
  console.log('F. 持久化跨实例 + 损坏文件容错');
81
- const c3 = new ActionCache(path + '.p', {
82
- now
83
- });
84
- c3.record('meituan:search:大理|民宿|%date%', fingerprint('mt-shape'), {
85
- url: 'https://www.meituan.com/...'
86
- }, 'meituan');
60
+ const c3 = new ActionCache(path + '.p', { now });
61
+ c3.record('meituan:search:大理|民宿|%date%', fingerprint('mt-shape'), { url: 'https://www.meituan.com/...' }, 'meituan');
87
62
  clock += 5_000;
88
- const c4 = new ActionCache(path + '.p', {
89
- now
90
- });
63
+ const c4 = new ActionCache(path + '.p', { now });
91
64
  assert(c4.lookup('meituan:search:大理|民宿|%date%', fingerprint('mt-shape'), 'meituan') !== null, '跨实例:落盘后新实例可命中');
92
65
  writeFileSync(path + '.broken', '{not json', 'utf8');
93
- const c5 = new ActionCache(path + '.broken', {
94
- now
95
- });
66
+ const c5 = new ActionCache(path + '.broken', { now });
96
67
  assert(c5.size() === 0 && c5.lookup('any', 'any', site) === null, '损坏 JSON 按空缓存,不抛错');
97
68
  console.log('G. fingerprint 敏感度');
98
69
  assert(fingerprint('abc') !== fingerprint('abd') && fingerprint('abc') === fingerprint('abc'), '微改即变,同文稳定');
99
- rmSync(iso, {
100
- recursive: true,
101
- force: true
102
- });
70
+ rmSync(iso, { recursive: true, force: true });
103
71
  console.log(`\nACTION-CACHE: ${pass} pass, ${fail} fail`);
104
72
  process.exit(fail > 0 ? 1 : 0);
105
-
106
-
107
- //# sourceURL=ts/scripts/action-cache-tests.ts