@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.18
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 +161 -116
- package/README.zh-CN.md +173 -144
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +113 -66
- package/bin/gotry-inner.js +436 -70
- 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 +5 -1
- 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 +1 -1
- package/dist/capabilities/fact-log.js +1 -1
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +1 -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 +1 -1
- 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 +1 -1
- package/dist/capabilities/session/extension-bridge.js +19 -6
- package/dist/capabilities/session/extension-channel.js +3 -3
- 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 +1 -1
- 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 +21 -251
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +11 -5
- package/dist/capabilities/session-search.js +46 -4
- 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 +45 -39
- package/dist/scripts/build-changelog.js +1 -1
- package/dist/scripts/changelog-tests.js +1 -1
- 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 +1 -1
- 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 +128 -23
- package/dist/scripts/fact-gate-tests.js +1 -1
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +1 -1
- 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 +1 -1
- package/dist/scripts/nightly-evidence.js +1 -1
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +21 -53
- 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 +1 -1
- package/dist/scripts/price-drift-watch.js +1 -1
- 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 +79 -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 +1 -1
- 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 +1 -1
- 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 +50 -19
- 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 +1 -1
- 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 +31 -7
- package/package.json +286 -11
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/session/extension-bridge.ts +37 -14
- package/ts/capabilities/session/extension-channel.ts +6 -3
- 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 +1 -1
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +34 -176
- package/ts/capabilities/session-login.ts +15 -5
- package/ts/capabilities/session-search.ts +40 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- 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/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 +64 -11
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
- package/dist/scripts/wizard-bootstrap.js +0 -32
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { openSession } from './session/transport.ts'
|
|
17
|
-
import { extensionCookieNames, extensionOpenLogin, classifyBridgeFailure
|
|
17
|
+
import { extensionCookieNames, extensionOpenLogin, classifyBridgeFailure } from './session/extension-channel.ts'
|
|
18
|
+
import { EXTENSION_STORE_URL } from './session/extension-bridge.ts'
|
|
18
19
|
import { resolveTransportMode } from './session-search.ts'
|
|
19
20
|
|
|
20
21
|
export interface LoginTarget {
|
|
@@ -55,6 +56,9 @@ export interface SessionLoginResult {
|
|
|
55
56
|
/** 已检出的登录票据 cookie 名(名字,非值——本模块永不读取/存储/回传任何 cookie 值) */
|
|
56
57
|
tickets?: string[]
|
|
57
58
|
error?: string
|
|
59
|
+
/** needs-extension 时给出 Chrome Web Store URL —— dsh UI 应直接渲成可点链接(浏览器自己当安装器,gotry 不插手) */
|
|
60
|
+
installUrl?: string
|
|
61
|
+
installAction?: 'add-to-chrome'
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function err(site: string, verdict: SessionLoginResult['verdict'], error: string, started: number, ts: string): SessionLoginResult {
|
|
@@ -158,10 +162,16 @@ export async function sessionLogin(q: SessionLoginQuery = {}): Promise<SessionLo
|
|
|
158
162
|
*/
|
|
159
163
|
async function sessionLoginViaExtension(site: string, target: LoginTarget, q: SessionLoginQuery, started: number, ts: string): Promise<SessionLoginResult> {
|
|
160
164
|
const evidenceTag = `[会话:${site}-login@${ts}]`
|
|
161
|
-
const quick = (verdict: SessionLoginResult['verdict'], error: string): SessionLoginResult =>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
const quick = (verdict: SessionLoginResult['verdict'], error: string): SessionLoginResult => {
|
|
166
|
+
const base: SessionLoginResult = {
|
|
167
|
+
ok: false, via: 'session-login-error', evidence: `[会话:login-error@${ts}] ${error.slice(0, 200)}`,
|
|
168
|
+
latencyMs: Date.now() - started, verdict, site, error: error.slice(0, 400),
|
|
169
|
+
}
|
|
170
|
+
// needs-extension:浏览器自己当安装器,gotry 不弹面板不开剪贴板——只把商店 URL 给 dsh UI
|
|
171
|
+
if (verdict === 'needs-extension') base.installUrl = EXTENSION_STORE_URL
|
|
172
|
+
if (verdict === 'needs-extension') base.installAction = 'add-to-chrome'
|
|
173
|
+
return base
|
|
174
|
+
}
|
|
165
175
|
// 自动检测优先:先只读票据名——已登录则零交互直接确认(与 cdp 车道同一语义,founder「UI 里有自动检测吗」)
|
|
166
176
|
const pre = await extensionCookieNames({ site, domain: target.domain, ticketNames: target.names, timeoutMs: 8_000 })
|
|
167
177
|
if (!pre.ok) return quick(classifyBridgeFailure(pre.kind) === 'needs-extension' ? 'needs-extension' : 'error', pre.summary)
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { openSession } from './session/transport.ts'
|
|
16
|
-
import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure
|
|
16
|
+
import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from './session/extension-channel.ts'
|
|
17
17
|
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
18
18
|
import { dirname } from 'node:path'
|
|
19
19
|
import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN, type SessionFlightOption } from './session/adapters/ctrip-flight.ts'
|
|
20
|
+
import { EXTENSION_STORE_URL } from './session/extension-bridge.ts'
|
|
20
21
|
|
|
21
22
|
export type SessionVerdict = 'hit' | 'miss' | 'error' | 'challenged' | 'cooldown' | 'needs-login' | 'needs-attach' | 'needs-extension'
|
|
22
23
|
|
|
@@ -28,6 +29,9 @@ export interface SessionSearchResult {
|
|
|
28
29
|
verdict: SessionVerdict
|
|
29
30
|
options?: SessionFlightOption[]
|
|
30
31
|
error?: string
|
|
32
|
+
/** needs-extension 时给出 Chrome Web Store URL —— dsh UI 应直接渲成可点链接(浏览器自己当安装器,gotry 不插手) */
|
|
33
|
+
installUrl?: string
|
|
34
|
+
installAction?: 'add-to-chrome'
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface SessionFlightQuery {
|
|
@@ -68,6 +72,33 @@ export function resolveTransportMode(profileDir?: string): 'cdp' | 'persistent'
|
|
|
68
72
|
return (process.env.GOTRY_SESSION_TRANSPORT ?? '').trim().toLowerCase() === 'cdp' ? 'cdp' : 'extension'
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/** Live browser transport is an explicit opt-in for smoke/full-suite callers. */
|
|
76
|
+
export function sessionLiveEnabled(env: Partial<Pick<NodeJS.ProcessEnv, 'GOTRY_SESSION_LIVE'>> = process.env): boolean {
|
|
77
|
+
return env.GOTRY_SESSION_LIVE === '1'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Deterministic offline observation used when the live session gate is closed. */
|
|
81
|
+
export function offlineSessionFlightResult(): SessionSearchResult {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
via: 'session-ctrip-flight-error',
|
|
85
|
+
evidence: '[会话:ctrip-flight@offline] GOTRY_SESSION_LIVE!=1; live transport not invoked',
|
|
86
|
+
latencyMs: 0,
|
|
87
|
+
verdict: 'error',
|
|
88
|
+
error: 'GOTRY_SESSION_LIVE=1 required for live session transport',
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Gate a caller before it can invoke the browser/session transport. */
|
|
93
|
+
export async function gatedSessionFlightSearch(
|
|
94
|
+
q: SessionFlightQuery,
|
|
95
|
+
search: (query: SessionFlightQuery) => Promise<SessionSearchResult> = sessionFlightSearch,
|
|
96
|
+
env: Partial<Pick<NodeJS.ProcessEnv, 'GOTRY_SESSION_LIVE'>> = process.env,
|
|
97
|
+
): Promise<SessionSearchResult> {
|
|
98
|
+
if (!sessionLiveEnabled(env)) return offlineSessionFlightResult()
|
|
99
|
+
return search(q)
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
/** 扩展车道 job 审计(ReadGuard 审计同款 JSONL,kind 区分;auditPath 缺省不落盘) */
|
|
72
103
|
export function appendExtensionAudit(auditPath: string | undefined, entry: { kind: 'extension-session-job'; site: string; url: string; jobId: string; result: string }): void {
|
|
73
104
|
if (!auditPath) return
|
|
@@ -105,7 +136,10 @@ export async function sessionFlightSearch(q: SessionFlightQuery): Promise<Sessio
|
|
|
105
136
|
const login = await extensionCookieNames({ site, domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES })
|
|
106
137
|
if (!login.ok) {
|
|
107
138
|
const verdict = classifyBridgeFailure(login.kind)
|
|
108
|
-
|
|
139
|
+
if (verdict === 'needs-extension') {
|
|
140
|
+
return { ok: false, via: 'session-ctrip-flight-error', evidence: '[会话:ctrip-flight-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: login.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' as const }
|
|
141
|
+
}
|
|
142
|
+
return err(verdict, login.summary)
|
|
109
143
|
}
|
|
110
144
|
// 登录态闸:用户自己的账号;匿名默认拒(allowAnonymous 仅链路自检且证据标自检态)
|
|
111
145
|
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
@@ -119,7 +153,10 @@ export async function sessionFlightSearch(q: SessionFlightQuery): Promise<Sessio
|
|
|
119
153
|
})
|
|
120
154
|
if (!r.ok) {
|
|
121
155
|
const verdict = classifyBridgeFailure(r.kind)
|
|
122
|
-
|
|
156
|
+
if (verdict === 'needs-extension') {
|
|
157
|
+
return { ok: false, via: 'session-ctrip-flight-error', evidence: '[会话:ctrip-flight-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' as const }
|
|
158
|
+
}
|
|
159
|
+
return err(verdict, r.summary)
|
|
123
160
|
}
|
|
124
161
|
const title = r.title
|
|
125
162
|
const head = r.body.slice(0, 5000)
|
|
@@ -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"
|