@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17

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 (238) hide show
  1. package/README.md +51 -13
  2. package/README.zh-CN.md +26 -12
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +357 -8
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +15 -1
  10. package/data/airline-airports.json +39 -0
  11. package/dist/capabilities/agent-reach-deep.js +1 -1
  12. package/dist/capabilities/agent-reach.js +1 -1
  13. package/dist/capabilities/anything.js +1 -1
  14. package/dist/capabilities/artifacts.js +1 -1
  15. package/dist/capabilities/effect.js +293 -0
  16. package/dist/capabilities/fact-log.js +41 -0
  17. package/dist/capabilities/flyai.js +20 -6
  18. package/dist/capabilities/hbcli.js +43 -1
  19. package/dist/capabilities/incident-log.js +1 -1
  20. package/dist/capabilities/model-override.js +18 -0
  21. package/dist/capabilities/opensky.js +1 -1
  22. package/dist/capabilities/resilience.js +90 -0
  23. package/dist/capabilities/session/action-cache.js +1 -1
  24. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  25. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  26. package/dist/capabilities/session/benchmark.js +2 -1
  27. package/dist/capabilities/session/extension-bridge.js +358 -0
  28. package/dist/capabilities/session/extension-channel.js +93 -0
  29. package/dist/capabilities/session/extension-distribution.js +234 -0
  30. package/dist/capabilities/session/extract.js +1 -1
  31. package/dist/capabilities/session/golden-score.js +92 -0
  32. package/dist/capabilities/session/health-watch.js +216 -0
  33. package/dist/capabilities/session/read-guard.js +1 -1
  34. package/dist/capabilities/session/static-flight-golden.js +137 -0
  35. package/dist/capabilities/session/transport.js +1 -1
  36. package/dist/capabilities/session/wizard.js +147 -0
  37. package/dist/capabilities/session-consent.js +1 -1
  38. package/dist/capabilities/session-login.js +83 -1
  39. package/dist/capabilities/session-search.js +111 -2
  40. package/dist/capabilities/weather.js +168 -46
  41. package/dist/data/session-golden-20.json +25 -0
  42. package/dist/data/sf-golden-manifest.json +102 -0
  43. package/dist/data/sf-static-routes.json +91 -0
  44. package/dist/scripts/action-cache-tests.js +1 -1
  45. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  46. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  47. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  48. package/dist/scripts/agent-reach-tests.js +1 -1
  49. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  50. package/dist/scripts/anything-tests.js +1 -1
  51. package/dist/scripts/async-collect.js +1 -1
  52. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  53. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  54. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  55. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  56. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  57. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  58. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  59. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  60. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  61. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  62. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  63. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  64. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  65. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  66. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  67. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  68. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  69. package/dist/scripts/booking-saga-tests.js +1 -1
  70. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  71. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  72. package/dist/scripts/bootstrap-tests.js +59 -3
  73. package/dist/scripts/build-changelog.js +279 -0
  74. package/dist/scripts/changelog-tests.js +165 -0
  75. package/dist/scripts/companion-tests.js +1 -1
  76. package/dist/scripts/diff-test.js +1 -1
  77. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  78. package/dist/scripts/dsh-runtime-closure.js +150 -0
  79. package/dist/scripts/effect-tests.js +474 -0
  80. package/dist/scripts/engine-run.js +1 -1
  81. package/dist/scripts/engine-tests.js +1 -1
  82. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  83. package/dist/scripts/evaluation-contract-tests.js +574 -0
  84. package/dist/scripts/extension-distribution-cli.js +35 -0
  85. package/dist/scripts/extension-distribution-tests.js +341 -0
  86. package/dist/scripts/extension-tests.js +573 -0
  87. package/dist/scripts/fact-gate-tests.js +318 -0
  88. package/dist/scripts/flyai-tests.js +1 -1
  89. package/dist/scripts/hbcli-e2e-tests.js +180 -0
  90. package/dist/scripts/hbcli-tests.js +1 -1
  91. package/dist/scripts/health-watch-cli.js +51 -0
  92. package/dist/scripts/i18n-tests.js +1 -1
  93. package/dist/scripts/incident-tests.js +1 -1
  94. package/dist/scripts/journey-tests.js +1 -1
  95. package/dist/scripts/ledger-tests.js +1 -1
  96. package/dist/scripts/ledger-workflow-crash.js +1 -1
  97. package/dist/scripts/memory-capture-tests.js +1 -1
  98. package/dist/scripts/memory-decay-tests.js +1 -1
  99. package/dist/scripts/memory-metrics.js +1 -1
  100. package/dist/scripts/memory-value-report.js +1 -1
  101. package/dist/scripts/model-override-e2e.js +176 -0
  102. package/dist/scripts/nightly-evidence-tests.js +67 -4
  103. package/dist/scripts/nightly-evidence.js +59 -3
  104. package/dist/scripts/nudge-digest.js +1 -1
  105. package/dist/scripts/onboarding-tests.js +186 -0
  106. package/dist/scripts/opensky-check.js +1 -1
  107. package/dist/scripts/opensky-tests.js +1 -1
  108. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  109. package/dist/scripts/price-drift-tests.js +441 -0
  110. package/dist/scripts/price-drift-watch.js +491 -0
  111. package/dist/scripts/probe-poi-tests.js +1 -1
  112. package/dist/scripts/product-metrics.js +1 -1
  113. package/dist/scripts/publish-preverify.js +40 -4
  114. package/dist/scripts/realtime-pricing-tests.js +1 -1
  115. package/dist/scripts/replay-async.js +1 -1
  116. package/dist/scripts/replay-real.js +1 -1
  117. package/dist/scripts/replay.js +1 -1
  118. package/dist/scripts/session-attach-diagnose.js +1 -1
  119. package/dist/scripts/session-attach-poc.js +1 -1
  120. package/dist/scripts/session-benchmark.js +1 -1
  121. package/dist/scripts/session-extract-tests.js +1 -1
  122. package/dist/scripts/session-login.js +1 -1
  123. package/dist/scripts/session-tests.js +70 -20
  124. package/dist/scripts/sf-live-benchmark.js +338 -0
  125. package/dist/scripts/sf-live-cli-tests.js +21 -0
  126. package/dist/scripts/sf-soft-score-tests.js +108 -0
  127. package/dist/scripts/sf-summary.js +93 -0
  128. package/dist/scripts/skeleton-check.js +1 -1
  129. package/dist/scripts/skeleton-integration-test.js +1 -1
  130. package/dist/scripts/skills-contract-tests.js +1 -1
  131. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  132. package/dist/scripts/smoke.js +154 -33
  133. package/dist/scripts/state-cli-tests.js +1 -1
  134. package/dist/scripts/state-cli.js +1 -1
  135. package/dist/scripts/static-golden-tests.js +299 -0
  136. package/dist/scripts/time-eval-tests.js +1 -1
  137. package/dist/scripts/travel-timeline-tests.js +1 -1
  138. package/dist/scripts/unified-tests.js +1 -1
  139. package/dist/scripts/weather-tests.js +694 -44
  140. package/dist/scripts/z3-race-tests.js +1 -1
  141. package/dist/src/artifact-gate.js +310 -0
  142. package/dist/src/benchmark-agent-conformance.js +370 -0
  143. package/dist/src/benchmark-environment-bridge.js +384 -0
  144. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  145. package/dist/src/benchmark-tool-isolation.js +124 -0
  146. package/dist/src/bookable-facts.js +349 -0
  147. package/dist/src/booking-saga.js +1 -1
  148. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  149. package/dist/src/booking-surface/canonical-schema.js +113 -0
  150. package/dist/src/booking-surface/contracts-v2.js +89 -0
  151. package/dist/src/booking-surface/contracts.js +46 -0
  152. package/dist/src/booking-surface/dsh-planner.js +453 -0
  153. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  154. package/dist/src/booking-surface/error-codes.js +94 -0
  155. package/dist/src/booking-surface/index.js +15 -0
  156. package/dist/src/booking-surface/profile.js +68 -0
  157. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  158. package/dist/src/booking-surface/runtime.js +351 -0
  159. package/dist/src/booking-surface/server-v2.js +334 -0
  160. package/dist/src/booking-surface/server.js +302 -0
  161. package/dist/src/booking-surface/startup.js +159 -0
  162. package/dist/src/booking-surface/validation-v2.js +319 -0
  163. package/dist/src/booking-surface/validation.js +809 -0
  164. package/dist/src/bridge.js +1 -1
  165. package/dist/src/companions.js +1 -1
  166. package/dist/src/contracts.js +1 -1
  167. package/dist/src/dsh-llm.js +1 -1
  168. package/dist/src/engine.js +1 -1
  169. package/dist/src/evaluation-cadence.js +234 -0
  170. package/dist/src/evaluation-contracts.js +906 -0
  171. package/dist/src/i18n.js +1 -1
  172. package/dist/src/index.js +234 -60
  173. package/dist/src/journey.js +1 -1
  174. package/dist/src/loop.js +1 -1
  175. package/dist/src/memory-capture.js +1 -1
  176. package/dist/src/memory-decay.js +1 -1
  177. package/dist/src/memory-utility.js +1 -1
  178. package/dist/src/mock-llm.js +1 -1
  179. package/dist/src/model.js +1 -1
  180. package/dist/src/realtime-pricing.js +14 -7
  181. package/dist/src/slot-spec.js +1 -1
  182. package/dist/src/state-ledger.js +2 -1
  183. package/dist/src/time-anchor.js +1 -1
  184. package/dist/src/tool-budget.js +136 -0
  185. package/dist/src/tool-packet.js +1 -1
  186. package/dist/src/travel-slots.js +1 -1
  187. package/dist/src/travel-timeline.js +1 -1
  188. package/dist/src/unified.js +1 -1
  189. package/dist/src/wish-pool.js +1 -1
  190. package/dist/src/z3-shared.js +1 -1
  191. package/extension/README.md +50 -0
  192. package/extension/background.js +160 -0
  193. package/extension/content-bridge.js +41 -0
  194. package/extension/content-main.js +62 -0
  195. package/extension/manifest.json +32 -0
  196. package/package.json +291 -12
  197. package/schemas/booking.surface.v1.schema.json +927 -0
  198. package/schemas/booking.surface.v2.schema.json +61 -0
  199. package/ts/capabilities/fact-log.ts +54 -0
  200. package/ts/capabilities/flyai.ts +16 -3
  201. package/ts/capabilities/hbcli.ts +46 -0
  202. package/ts/capabilities/session/benchmark.ts +4 -1
  203. package/ts/capabilities/session/extension-bridge.ts +377 -0
  204. package/ts/capabilities/session/extension-channel.ts +95 -0
  205. package/ts/capabilities/session/extension-distribution.ts +264 -0
  206. package/ts/capabilities/session/golden-score.ts +139 -0
  207. package/ts/capabilities/session/health-watch.ts +257 -0
  208. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  209. package/ts/capabilities/session/wizard.ts +140 -0
  210. package/ts/capabilities/session-login.ts +63 -2
  211. package/ts/capabilities/session-search.ts +105 -3
  212. package/ts/capabilities/weather.ts +141 -52
  213. package/ts/package.json +3 -3
  214. package/ts/src/artifact-gate.ts +365 -0
  215. package/ts/src/benchmark-agent-conformance.ts +448 -0
  216. package/ts/src/benchmark-environment-bridge.ts +348 -0
  217. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  218. package/ts/src/benchmark-tool-isolation.ts +166 -0
  219. package/ts/src/bookable-facts.ts +557 -0
  220. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  221. package/ts/src/booking-surface/canonical-schema.js +113 -0
  222. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  223. package/ts/src/booking-surface/contracts.ts +380 -0
  224. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  225. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  226. package/ts/src/booking-surface/error-codes.ts +101 -0
  227. package/ts/src/booking-surface/index.ts +12 -0
  228. package/ts/src/booking-surface/profile.ts +42 -0
  229. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  230. package/ts/src/booking-surface/runtime.ts +483 -0
  231. package/ts/src/booking-surface/server-v2.ts +247 -0
  232. package/ts/src/booking-surface/server.ts +324 -0
  233. package/ts/src/booking-surface/startup.ts +196 -0
  234. package/ts/src/booking-surface/validation-v2.ts +205 -0
  235. package/ts/src/booking-surface/validation.ts +453 -0
  236. package/ts/src/index.ts +161 -14
  237. package/ts/src/state-ledger.ts +1 -0
  238. package/ts/src/tool-budget.ts +165 -0
@@ -59,17 +59,53 @@ export function wmoLabel(code: number): string {
59
59
  return WMO_ZH[code] ?? `天气码${code}`
60
60
  }
61
61
 
62
- async function fetchJson(url: string, timeoutMs: number, headers: Record<string, string> = {}): Promise<{ ok: boolean; data?: unknown; error?: string }> {
63
- const ctrl = new AbortController()
64
- const timer = setTimeout(() => ctrl.abort(), timeoutMs)
62
+ type FetchImpl = typeof fetch
63
+ const monotonicNow = (): number => performance.now()
64
+ const MIN_TIMER_MS = 1
65
+ const TIMEOUT_ERROR = 'timeoutMs must be a finite positive integer'
66
+ const BUDGET_EXHAUSTED_ERROR = 'total timeout budget exhausted'
67
+ const REQUEST_ERROR = 'request failed'
68
+ const normalizeTimeout = (value: number): number | null => Number.isInteger(value) && value > 0 ? value : null
69
+
70
+ /** Rejection values are untrusted: only a real string message is safe to expose. */
71
+ const normalizeError = (value: unknown): string => {
65
72
  try {
66
- const res = await fetch(url, { signal: ctrl.signal, headers: { Accept: 'application/json', ...headers } })
73
+ if (value instanceof Error && typeof value.message === 'string' && value.message.length > 0) {
74
+ return value.message.slice(0, 200)
75
+ }
76
+ if (typeof value === 'string' && value.length > 0) return value.slice(0, 200)
77
+ } catch {
78
+ return REQUEST_ERROR
79
+ }
80
+ return REQUEST_ERROR
81
+ }
82
+
83
+ async function fetchJson(url: string, timeoutMs: number, headers: Record<string, string> = {}, fetchImpl: FetchImpl = globalThis.fetch): Promise<{ ok: boolean; data?: unknown; error?: string; timedOut?: boolean }> {
84
+ if (!Number.isFinite(timeoutMs) || timeoutMs < MIN_TIMER_MS) return { ok: false, error: BUDGET_EXHAUSTED_ERROR }
85
+ const ctrl = new AbortController()
86
+ const timerDelay = Math.floor(timeoutMs)
87
+ if (timerDelay < MIN_TIMER_MS) return { ok: false, error: BUDGET_EXHAUSTED_ERROR }
88
+ let timer: ReturnType<typeof setTimeout> | undefined
89
+ let timedOut = false
90
+ const timeout = new Promise<never>((_, reject) => {
91
+ timer = setTimeout(() => {
92
+ timedOut = true
93
+ ctrl.abort()
94
+ reject(new Error(`timeout after ${timeoutMs}ms`))
95
+ }, timerDelay)
96
+ })
97
+ const operation = (async () => {
98
+ const res = await fetchImpl(url, { signal: ctrl.signal, headers: { Accept: 'application/json', ...headers } })
67
99
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
68
100
  return { ok: true, data: await res.json() }
101
+ })()
102
+ try {
103
+ return await Promise.race([operation, timeout])
69
104
  } catch (e) {
70
- return { ok: false, error: (e as Error).message.slice(0, 200) }
105
+ return { ok: false, error: normalizeError(e), ...(timedOut ? { timedOut: true } : {}) }
71
106
  } finally {
72
- clearTimeout(timer)
107
+ if (timer !== undefined) clearTimeout(timer)
108
+ void operation.catch(() => undefined)
73
109
  }
74
110
  }
75
111
 
@@ -77,6 +113,7 @@ const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org/search'
77
113
 
78
114
  /** 行政级/人口重镇 feature_code(open-meteo 命名惯例):PPLC=首都 PPLA*=首府 PPLB=城镇 PPLX=区片 */
79
115
  const MAJOR_FEATURE = /^(PPLC|PPLA|PPLB|PPLX)/
116
+ const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value)
80
117
 
81
118
  interface GeoHitInternal {
82
119
  name: string
@@ -101,15 +138,28 @@ function stripHits(hits: GeoHitInternal[]): Array<{ name: string; latitude: numb
101
138
  * 兜底仍空则回退 open-meteo 弱结果(不比此前更差)。 */
102
139
  export async function geocodePlace(
103
140
  name: string,
104
- opts: { timeoutMs?: number; count?: number } = {},
141
+ opts: { timeoutMs?: number; count?: number; fetchImpl?: FetchImpl } = {},
105
142
  ): Promise<{ ok: boolean; evidence: string; via?: 'open-meteo' | 'nominatim'; results: Array<{ name: string; latitude: number; longitude: number; country?: string; admin1?: string }>; error?: string }> {
106
143
  const ts = new Date().toISOString()
107
144
  const count = opts.count ?? 5
108
145
  const query = name.trim()
146
+ const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000)
147
+ if (timeoutMs === null) return { ok: false, evidence: `[实时API:open-meteo-geo@error@${ts}]`, results: [], error: TIMEOUT_ERROR }
109
148
  const omUrl = `${GEOCODE_BASE}?name=${encodeURIComponent(query)}&count=${count}&language=zh&format=json`
110
- const r = await fetchJson(omUrl, opts.timeoutMs ?? 15_000)
111
- const omResults: GeoHitInternal[] = r.ok
112
- ? ((r.data as { results?: unknown[] })?.results ?? []).map((item) => {
149
+ // timeoutMs is a budget for the complete primary→fallback chain, not per request.
150
+ const deadline = monotonicNow() + timeoutMs
151
+ const remaining = (): number | null => {
152
+ const value = deadline - monotonicNow()
153
+ return Number.isFinite(value) && value >= MIN_TIMER_MS ? value : null
154
+ }
155
+ const primaryRemaining = remaining()
156
+ const r = primaryRemaining === null
157
+ ? { ok: false, error: BUDGET_EXHAUSTED_ERROR }
158
+ : await fetchJson(omUrl, primaryRemaining, {}, opts.fetchImpl)
159
+ const primaryPayload = r.ok && isRecord(r.data) && Array.isArray(r.data['results']) ? r.data['results'] : null
160
+ const omResults: GeoHitInternal[] = primaryPayload
161
+ ? primaryPayload.map((item) => {
162
+ if (!isRecord(item)) return null
113
163
  const it = item as Record<string, unknown>
114
164
  return {
115
165
  name: String(it['name'] ?? ''),
@@ -120,7 +170,7 @@ export async function geocodePlace(
120
170
  population: Number(it['population'] ?? 0) || 0,
121
171
  featureCode: String(it['feature_code'] ?? ''),
122
172
  }
123
- }).filter(h => h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude))
173
+ }).filter((h) => !!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude)) as GeoHitInternal[]
124
174
  : []
125
175
  omResults.sort((a, b) => (b.population - a.population) || (Number(MAJOR_FEATURE.test(b.featureCode)) - Number(MAJOR_FEATURE.test(a.featureCode))))
126
176
  const top = omResults[0]
@@ -131,20 +181,22 @@ export async function geocodePlace(
131
181
  // 弱命中/零结果/主源请求失败:Nominatim 兜底(免费无 key;中文 accept-language)
132
182
  const nomTs = new Date().toISOString()
133
183
  const nomUrl = `${NOMINATIM_BASE}?q=${encodeURIComponent(query)}&format=jsonv2&limit=${count}&accept-language=zh&addressdetails=1`
134
- const nom = await fetchJson(nomUrl, opts.timeoutMs ?? 15_000, { 'User-Agent': 'gotry-travel-agent/0.1 (+https://github.com/Danceiny/gotry)' })
135
- const nomResults: Array<{ name: string; latitude: number; longitude: number; country?: string; admin1?: string }> = nom.ok
136
- ? ((nom.data as unknown[]) ?? []).map((item) => {
184
+ const fallbackRemaining = primaryRemaining === null || r.timedOut ? null : remaining()
185
+ const nom = fallbackRemaining === null
186
+ ? { ok: false, error: BUDGET_EXHAUSTED_ERROR }
187
+ : await fetchJson(nomUrl, fallbackRemaining, { 'User-Agent': 'gotry-travel-agent/0.1 (+https://github.com/Danceiny/gotry)' }, opts.fetchImpl)
188
+ const nomResults: Array<{ name: string; latitude: number; longitude: number; country?: string; admin1?: string }> = []
189
+ if (nom.ok && Array.isArray(nom.data)) {
190
+ const parsedNom = nom.data.map((item) => {
191
+ if (!isRecord(item)) return null
137
192
  const it = item as Record<string, unknown>
138
- const addr = (it['address'] ?? {}) as Record<string, string>
139
- return {
140
- name: String(it['name'] ?? ''),
141
- latitude: Number(it['lat']),
142
- longitude: Number(it['lon']),
143
- country: addr['country'],
144
- admin1: addr['province'] ?? addr['state'] ?? addr['county'],
145
- }
146
- }).filter(h => h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude))
147
- : []
193
+ const addr = isRecord(it['address']) ? it['address'] : {}
194
+ const country = typeof addr['country'] === 'string' ? addr['country'] : undefined
195
+ const admin1 = [addr['province'], addr['state'], addr['county']].find(v => typeof v === 'string') as string | undefined
196
+ return { name: String(it['name'] ?? ''), latitude: Number(it['lat']), longitude: Number(it['lon']), ...(country ? { country } : {}), ...(admin1 ? { admin1 } : {}) }
197
+ }).filter((h) => !!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude)) as Array<{ name: string; latitude: number; longitude: number; country?: string; admin1?: string }>
198
+ nomResults.push(...parsedNom)
199
+ }
148
200
  if (nomResults.length) {
149
201
  const omNote = r.ok
150
202
  ? `open-meteo ${omResults.length} 条弱命中(无人口/行政级,不足采信)`
@@ -166,35 +218,54 @@ export async function geocodePlace(
166
218
  /** 未来天气预报(≤16 天) */
167
219
  export async function getForecast(
168
220
  point: WeatherPoint,
169
- opts: { days?: number; timeoutMs?: number } = {},
221
+ opts: { days?: number; timeoutMs?: number; fetchImpl?: FetchImpl } = {},
170
222
  ): Promise<WeatherResult> {
171
- const started = Date.now()
223
+ const started = monotonicNow()
172
224
  const ts = new Date().toISOString()
173
- const days = Math.min(opts.days ?? 7, 16)
225
+ const requestedDays = opts.days ?? 7
226
+ const days = Number.isInteger(requestedDays) && requestedDays > 0 ? Math.min(requestedDays, 16) : 0
227
+ const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000)
228
+ if (days < 1 || timeoutMs === null) 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 }
174
229
  const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
175
230
  + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode`
176
231
  + `&forecast_days=${days}&timezone=auto`
177
- const r = await fetchJson(url, opts.timeoutMs ?? 15_000)
178
- const latencyMs = Date.now() - started
232
+ const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl)
233
+ const latencyMs = monotonicNow() - started
179
234
  if (!r.ok) {
180
235
  return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: r.error }
181
236
  }
182
- const daily = (r.data as { daily?: { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; precipitation_probability_max?: (number | null)[]; weathercode: number[] }; timezone?: string }).daily
183
- if (!daily) {
237
+ const payload = isRecord(r.data) ? r.data : null
238
+ const daily = isRecord(payload?.['daily']) ? payload['daily'] as { time?: unknown[]; temperature_2m_max?: unknown[]; temperature_2m_min?: unknown[]; precipitation_probability_max?: unknown[]; weathercode?: unknown[] } : null
239
+ if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max)
240
+ || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode)
241
+ || daily.time.length !== days || daily.temperature_2m_max.length !== days
242
+ || daily.temperature_2m_min.length !== days || daily.weathercode.length !== days
243
+ || daily.time.some(v => typeof v !== 'string')
244
+ || daily.temperature_2m_max.some(v => typeof v !== 'number' || !Number.isFinite(v))
245
+ || daily.temperature_2m_min.some(v => typeof v !== 'number' || !Number.isFinite(v))
246
+ || daily.weathercode.some(v => typeof v !== 'number' || !Number.isFinite(v))
247
+ || (daily.precipitation_probability_max !== undefined && daily.precipitation_probability_max !== null
248
+ && !Array.isArray(daily.precipitation_probability_max))
249
+ || (Array.isArray(daily.precipitation_probability_max)
250
+ && (daily.precipitation_probability_max.length !== days
251
+ || daily.precipitation_probability_max.some(v => v !== null && (typeof v !== 'number' || !Number.isFinite(v)))))) {
184
252
  return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: 'empty daily payload' }
185
253
  }
186
- const rows: WeatherDaily[] = daily.time.map((date, i) => ({
187
- date,
188
- tempMaxC: daily.temperature_2m_max[i] ?? NaN,
189
- tempMinC: daily.temperature_2m_min[i] ?? NaN,
190
- precipProbMaxPct: daily.precipitation_probability_max?.[i] ?? null,
191
- weatherCode: daily.weathercode[i] ?? -1,
254
+ const times = daily.time as string[]
255
+ const maxes = daily.temperature_2m_max as number[]
256
+ const mins = daily.temperature_2m_min as number[]
257
+ const codes = daily.weathercode as number[]
258
+ const precip = Array.isArray(daily.precipitation_probability_max)
259
+ ? daily.precipitation_probability_max as Array<number | null>
260
+ : undefined
261
+ const rows: WeatherDaily[] = times.map((date, i) => ({
262
+ date, tempMaxC: maxes[i]!, tempMinC: mins[i]!, precipProbMaxPct: precip?.[i] ?? null, weatherCode: codes[i]!,
192
263
  }))
193
264
  return {
194
265
  ok: true, via: 'open-meteo',
195
266
  evidence: `[实时API:open-meteo@${ts}]`,
196
267
  latencyMs,
197
- timezone: (r.data as { timezone?: string }).timezone,
268
+ timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
198
269
  daily: rows,
199
270
  }
200
271
  }
@@ -203,39 +274,57 @@ export async function getForecast(
203
274
  export async function getClimate(
204
275
  point: WeatherPoint,
205
276
  month: number,
206
- opts: { year?: number; timeoutMs?: number } = {},
277
+ opts: { year?: number; timeoutMs?: number; fetchImpl?: FetchImpl } = {},
207
278
  ): Promise<WeatherResult> {
208
- const started = Date.now()
279
+ const started = monotonicNow()
209
280
  const ts = new Date().toISOString()
281
+ const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000)
282
+ if (timeoutMs === null) return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs: monotonicNow() - started, error: TIMEOUT_ERROR }
210
283
  // 默认取去年同月做气候基线
211
- const year = opts.year ?? (new Date().getFullYear() - 1)
284
+ const year = opts.year ?? (new Date().getUTCFullYear() - 1)
285
+ if (!Number.isInteger(month) || month < 1 || month > 12 || !Number.isInteger(year) || year < 1940 || year > new Date().getUTCFullYear()) {
286
+ 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' }
287
+ }
212
288
  const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate()
289
+ const expectedDates = Array.from({ length: lastDay }, (_, i) => `${year}-${String(month).padStart(2, '0')}-${String(i + 1).padStart(2, '0')}`)
213
290
  const start = `${year}-${String(month).padStart(2, '0')}-01`
214
291
  const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`
215
292
  const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
216
293
  + `&start_date=${start}&end_date=${end}`
217
294
  + `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`
218
- const r = await fetchJson(url, opts.timeoutMs ?? 15_000)
219
- const latencyMs = Date.now() - started
295
+ const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl)
296
+ const latencyMs = monotonicNow() - started
220
297
  if (!r.ok) {
221
298
  return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: r.error }
222
299
  }
223
- const daily = (r.data as { daily?: { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; weathercode: number[] } }).daily
224
- if (!daily) {
300
+ const payload = isRecord(r.data) ? r.data : null
301
+ const daily = isRecord(payload?.['daily']) ? payload['daily'] as { time?: unknown[]; temperature_2m_max?: unknown[]; temperature_2m_min?: unknown[]; weathercode?: unknown[] } : null
302
+ if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max)
303
+ || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode)
304
+ || daily.time.length !== lastDay
305
+ || daily.time.length !== daily.temperature_2m_max.length
306
+ || daily.time.length !== daily.temperature_2m_min.length
307
+ || daily.time.length !== daily.weathercode.length
308
+ || daily.time.some(v => typeof v !== 'string')
309
+ || daily.time.some((v, i) => v !== expectedDates[i])
310
+ || daily.temperature_2m_max.some(v => typeof v !== 'number' || !Number.isFinite(v))
311
+ || daily.temperature_2m_min.some(v => typeof v !== 'number' || !Number.isFinite(v))
312
+ || daily.weathercode.some(v => typeof v !== 'number' || !Number.isFinite(v))) {
225
313
  return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: 'empty daily payload' }
226
314
  }
227
- const rows: WeatherDaily[] = daily.time.map((date, i) => ({
228
- date,
229
- tempMaxC: daily.temperature_2m_max[i] ?? NaN,
230
- tempMinC: daily.temperature_2m_min[i] ?? NaN,
231
- precipProbMaxPct: null, // archive 端点没有概率,用降水量另行估算
232
- weatherCode: daily.weathercode[i] ?? -1,
315
+ const times = daily.time as string[]
316
+ const maxes = daily.temperature_2m_max as number[]
317
+ const mins = daily.temperature_2m_min as number[]
318
+ const codes = daily.weathercode as number[]
319
+ const rows: WeatherDaily[] = times.map((date, i) => ({
320
+ date, tempMaxC: maxes[i]!, tempMinC: mins[i]!, precipProbMaxPct: null, // archive 端点没有概率,用降水量另行估算
321
+ weatherCode: codes[i]!,
233
322
  }))
234
323
  return {
235
324
  ok: true, via: 'open-meteo',
236
325
  evidence: `[实时API:open-meteo-climate@${ts}]`,
237
326
  latencyMs,
238
- timezone: (r.data as { timezone?: string }).timezone,
327
+ timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
239
328
  daily: rows,
240
329
  }
241
330
  }
package/ts/package.json CHANGED
@@ -10,9 +10,9 @@
10
10
  "smoke": "tsx scripts/smoke.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@deepseek-ai/cordis": "*",
14
- "@deepseek-ai/dsh-tools": "*",
15
- "@deepseek-ai/schemastery": "*",
13
+ "@deepseek-ai/cordis": "4.0.2",
14
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
15
+ "@deepseek-ai/schemastery": "^3.18.2",
16
16
  "@types/better-sqlite3": "^9.6.0",
17
17
  "better-sqlite3": "^13.0.3",
18
18
  "z3-solver": "^5.2.0"