@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.
- package/README.md +51 -13
- package/README.zh-CN.md +26 -12
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +357 -8
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +15 -1
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +358 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +147 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +83 -1
- package/dist/capabilities/session-search.js +111 -2
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +59 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +573 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +186 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +154 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +234 -60
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +50 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +291 -12
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +377 -0
- package/ts/capabilities/session/extension-channel.ts +95 -0
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-login.ts +63 -2
- package/ts/capabilities/session-search.ts +105 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +161 -14
- package/ts/src/state-ledger.ts +1 -0
- 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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
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
|
|
135
|
-
const
|
|
136
|
-
?
|
|
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']
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 =
|
|
223
|
+
const started = monotonicNow()
|
|
172
224
|
const ts = new Date().toISOString()
|
|
173
|
-
const
|
|
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,
|
|
178
|
-
const latencyMs =
|
|
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
|
|
183
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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:
|
|
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 =
|
|
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().
|
|
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,
|
|
219
|
-
const latencyMs =
|
|
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
|
|
224
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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:
|
|
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"
|