@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { openSession } from './session/transport.js';
|
|
2
|
-
import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure
|
|
2
|
+
import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from './session/extension-channel.js';
|
|
3
3
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
|
|
6
|
+
import { EXTENSION_STORE_URL } from './session/extension-bridge.js';
|
|
6
7
|
const MIN_INTERVAL_MS = 30_000;
|
|
7
8
|
const lastCallAt = new Map();
|
|
8
9
|
export function __resetRateLimiterForTest() {
|
|
@@ -16,6 +17,23 @@ export function resolveTransportMode(profileDir) {
|
|
|
16
17
|
if (profileDir !== undefined) return 'persistent';
|
|
17
18
|
return (process.env.GOTRY_SESSION_TRANSPORT ?? '').trim().toLowerCase() === 'cdp' ? 'cdp' : 'extension';
|
|
18
19
|
}
|
|
20
|
+
export function sessionLiveEnabled(env = process.env) {
|
|
21
|
+
return env.GOTRY_SESSION_LIVE === '1';
|
|
22
|
+
}
|
|
23
|
+
export function offlineSessionFlightResult() {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
via: 'session-ctrip-flight-error',
|
|
27
|
+
evidence: '[会话:ctrip-flight@offline] GOTRY_SESSION_LIVE!=1; live transport not invoked',
|
|
28
|
+
latencyMs: 0,
|
|
29
|
+
verdict: 'error',
|
|
30
|
+
error: 'GOTRY_SESSION_LIVE=1 required for live session transport'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function gatedSessionFlightSearch(q, search = sessionFlightSearch, env = process.env) {
|
|
34
|
+
if (!sessionLiveEnabled(env)) return offlineSessionFlightResult();
|
|
35
|
+
return search(q);
|
|
36
|
+
}
|
|
19
37
|
export function appendExtensionAudit(auditPath, entry) {
|
|
20
38
|
if (!auditPath) return;
|
|
21
39
|
try {
|
|
@@ -60,7 +78,19 @@ export async function sessionFlightSearch(q) {
|
|
|
60
78
|
});
|
|
61
79
|
if (!login.ok) {
|
|
62
80
|
const verdict = classifyBridgeFailure(login.kind);
|
|
63
|
-
|
|
81
|
+
if (verdict === 'needs-extension') {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
via: 'session-ctrip-flight-error',
|
|
85
|
+
evidence: '[会话:ctrip-flight-needs-extension@ts]',
|
|
86
|
+
latencyMs: Date.now() - started,
|
|
87
|
+
verdict,
|
|
88
|
+
error: login.summary,
|
|
89
|
+
installUrl: EXTENSION_STORE_URL,
|
|
90
|
+
installAction: 'add-to-chrome'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return err(verdict, login.summary);
|
|
64
94
|
}
|
|
65
95
|
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
66
96
|
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
@@ -79,7 +109,19 @@ export async function sessionFlightSearch(q) {
|
|
|
79
109
|
});
|
|
80
110
|
if (!r.ok) {
|
|
81
111
|
const verdict = classifyBridgeFailure(r.kind);
|
|
82
|
-
|
|
112
|
+
if (verdict === 'needs-extension') {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
via: 'session-ctrip-flight-error',
|
|
116
|
+
evidence: '[会话:ctrip-flight-needs-extension@ts]',
|
|
117
|
+
latencyMs: Date.now() - started,
|
|
118
|
+
verdict,
|
|
119
|
+
error: r.summary,
|
|
120
|
+
installUrl: EXTENSION_STORE_URL,
|
|
121
|
+
installAction: 'add-to-chrome'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return err(verdict, r.summary);
|
|
83
125
|
}
|
|
84
126
|
const title = r.title;
|
|
85
127
|
const head = r.body.slice(0, 5000);
|
|
@@ -160,4 +202,4 @@ export async function sessionFlightSearch(q) {
|
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
|
|
163
|
-
//# sourceURL
|
|
205
|
+
//# sourceURL=ts/capabilities/session-search.ts
|
|
@@ -32,11 +32,45 @@ const WMO_ZH = {
|
|
|
32
32
|
export function wmoLabel(code) {
|
|
33
33
|
return WMO_ZH[code] ?? `天气码${code}`;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const monotonicNow = ()=>performance.now();
|
|
36
|
+
const MIN_TIMER_MS = 1;
|
|
37
|
+
const TIMEOUT_ERROR = 'timeoutMs must be a finite positive integer';
|
|
38
|
+
const BUDGET_EXHAUSTED_ERROR = 'total timeout budget exhausted';
|
|
39
|
+
const REQUEST_ERROR = 'request failed';
|
|
40
|
+
const normalizeTimeout = (value)=>Number.isInteger(value) && value > 0 ? value : null;
|
|
41
|
+
const normalizeError = (value)=>{
|
|
38
42
|
try {
|
|
39
|
-
|
|
43
|
+
if (value instanceof Error && typeof value.message === 'string' && value.message.length > 0) {
|
|
44
|
+
return value.message.slice(0, 200);
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === 'string' && value.length > 0) return value.slice(0, 200);
|
|
47
|
+
} catch {
|
|
48
|
+
return REQUEST_ERROR;
|
|
49
|
+
}
|
|
50
|
+
return REQUEST_ERROR;
|
|
51
|
+
};
|
|
52
|
+
async function fetchJson(url, timeoutMs, headers = {}, fetchImpl = globalThis.fetch) {
|
|
53
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < MIN_TIMER_MS) return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: BUDGET_EXHAUSTED_ERROR
|
|
56
|
+
};
|
|
57
|
+
const ctrl = new AbortController();
|
|
58
|
+
const timerDelay = Math.floor(timeoutMs);
|
|
59
|
+
if (timerDelay < MIN_TIMER_MS) return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: BUDGET_EXHAUSTED_ERROR
|
|
62
|
+
};
|
|
63
|
+
let timer;
|
|
64
|
+
let timedOut = false;
|
|
65
|
+
const timeout = new Promise((_, reject)=>{
|
|
66
|
+
timer = setTimeout(()=>{
|
|
67
|
+
timedOut = true;
|
|
68
|
+
ctrl.abort();
|
|
69
|
+
reject(new Error(`timeout after ${timeoutMs}ms`));
|
|
70
|
+
}, timerDelay);
|
|
71
|
+
});
|
|
72
|
+
const operation = (async ()=>{
|
|
73
|
+
const res = await fetchImpl(url, {
|
|
40
74
|
signal: ctrl.signal,
|
|
41
75
|
headers: {
|
|
42
76
|
Accept: 'application/json',
|
|
@@ -51,17 +85,28 @@ async function fetchJson(url, timeoutMs, headers = {}) {
|
|
|
51
85
|
ok: true,
|
|
52
86
|
data: await res.json()
|
|
53
87
|
};
|
|
88
|
+
})();
|
|
89
|
+
try {
|
|
90
|
+
return await Promise.race([
|
|
91
|
+
operation,
|
|
92
|
+
timeout
|
|
93
|
+
]);
|
|
54
94
|
} catch (e) {
|
|
55
95
|
return {
|
|
56
96
|
ok: false,
|
|
57
|
-
error: e
|
|
97
|
+
error: normalizeError(e),
|
|
98
|
+
...timedOut ? {
|
|
99
|
+
timedOut: true
|
|
100
|
+
} : {}
|
|
58
101
|
};
|
|
59
102
|
} finally{
|
|
60
|
-
clearTimeout(timer);
|
|
103
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
104
|
+
void operation.catch(()=>undefined);
|
|
61
105
|
}
|
|
62
106
|
}
|
|
63
107
|
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org/search';
|
|
64
108
|
const MAJOR_FEATURE = /^(PPLC|PPLA|PPLB|PPLX)/;
|
|
109
|
+
const isRecord = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
65
110
|
function stripHits(hits) {
|
|
66
111
|
return hits.map(({ name, latitude, longitude, country, admin1 })=>({
|
|
67
112
|
...country ? {
|
|
@@ -79,9 +124,27 @@ export async function geocodePlace(name, opts = {}) {
|
|
|
79
124
|
const ts = new Date().toISOString();
|
|
80
125
|
const count = opts.count ?? 5;
|
|
81
126
|
const query = name.trim();
|
|
127
|
+
const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
|
|
128
|
+
if (timeoutMs === null) return {
|
|
129
|
+
ok: false,
|
|
130
|
+
evidence: `[实时API:open-meteo-geo@error@${ts}]`,
|
|
131
|
+
results: [],
|
|
132
|
+
error: TIMEOUT_ERROR
|
|
133
|
+
};
|
|
82
134
|
const omUrl = `${GEOCODE_BASE}?name=${encodeURIComponent(query)}&count=${count}&language=zh&format=json`;
|
|
83
|
-
const
|
|
84
|
-
const
|
|
135
|
+
const deadline = monotonicNow() + timeoutMs;
|
|
136
|
+
const remaining = ()=>{
|
|
137
|
+
const value = deadline - monotonicNow();
|
|
138
|
+
return Number.isFinite(value) && value >= MIN_TIMER_MS ? value : null;
|
|
139
|
+
};
|
|
140
|
+
const primaryRemaining = remaining();
|
|
141
|
+
const r = primaryRemaining === null ? {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: BUDGET_EXHAUSTED_ERROR
|
|
144
|
+
} : await fetchJson(omUrl, primaryRemaining, {}, opts.fetchImpl);
|
|
145
|
+
const primaryPayload = r.ok && isRecord(r.data) && Array.isArray(r.data['results']) ? r.data['results'] : null;
|
|
146
|
+
const omResults = primaryPayload ? primaryPayload.map((item)=>{
|
|
147
|
+
if (!isRecord(item)) return null;
|
|
85
148
|
const it = item;
|
|
86
149
|
return {
|
|
87
150
|
name: String(it['name'] ?? ''),
|
|
@@ -92,7 +155,7 @@ export async function geocodePlace(name, opts = {}) {
|
|
|
92
155
|
population: Number(it['population'] ?? 0) || 0,
|
|
93
156
|
featureCode: String(it['feature_code'] ?? '')
|
|
94
157
|
};
|
|
95
|
-
}).filter((h)
|
|
158
|
+
}).filter((h)=>!!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude)) : [];
|
|
96
159
|
omResults.sort((a, b)=>b.population - a.population || Number(MAJOR_FEATURE.test(b.featureCode)) - Number(MAJOR_FEATURE.test(a.featureCode)));
|
|
97
160
|
const top = omResults[0];
|
|
98
161
|
if (top && (top.population > 0 || MAJOR_FEATURE.test(top.featureCode))) {
|
|
@@ -105,20 +168,39 @@ export async function geocodePlace(name, opts = {}) {
|
|
|
105
168
|
}
|
|
106
169
|
const nomTs = new Date().toISOString();
|
|
107
170
|
const nomUrl = `${NOMINATIM_BASE}?q=${encodeURIComponent(query)}&format=jsonv2&limit=${count}&accept-language=zh&addressdetails=1`;
|
|
108
|
-
const
|
|
171
|
+
const fallbackRemaining = primaryRemaining === null || r.timedOut ? null : remaining();
|
|
172
|
+
const nom = fallbackRemaining === null ? {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: BUDGET_EXHAUSTED_ERROR
|
|
175
|
+
} : await fetchJson(nomUrl, fallbackRemaining, {
|
|
109
176
|
'User-Agent': 'gotry-travel-agent/0.1 (+https://github.com/Danceiny/gotry)'
|
|
110
|
-
});
|
|
111
|
-
const nomResults =
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
177
|
+
}, opts.fetchImpl);
|
|
178
|
+
const nomResults = [];
|
|
179
|
+
if (nom.ok && Array.isArray(nom.data)) {
|
|
180
|
+
const parsedNom = nom.data.map((item)=>{
|
|
181
|
+
if (!isRecord(item)) return null;
|
|
182
|
+
const it = item;
|
|
183
|
+
const addr = isRecord(it['address']) ? it['address'] : {};
|
|
184
|
+
const country = typeof addr['country'] === 'string' ? addr['country'] : undefined;
|
|
185
|
+
const admin1 = [
|
|
186
|
+
addr['province'],
|
|
187
|
+
addr['state'],
|
|
188
|
+
addr['county']
|
|
189
|
+
].find((v)=>typeof v === 'string');
|
|
190
|
+
return {
|
|
191
|
+
name: String(it['name'] ?? ''),
|
|
192
|
+
latitude: Number(it['lat']),
|
|
193
|
+
longitude: Number(it['lon']),
|
|
194
|
+
...country ? {
|
|
195
|
+
country
|
|
196
|
+
} : {},
|
|
197
|
+
...admin1 ? {
|
|
198
|
+
admin1
|
|
199
|
+
} : {}
|
|
200
|
+
};
|
|
201
|
+
}).filter((h)=>!!h && !!h.name && Number.isFinite(h.latitude) && Number.isFinite(h.longitude));
|
|
202
|
+
nomResults.push(...parsedNom);
|
|
203
|
+
}
|
|
122
204
|
if (nomResults.length) {
|
|
123
205
|
const omNote = r.ok ? `open-meteo ${omResults.length} 条弱命中(无人口/行政级,不足采信)` : `open-meteo 失败:${r.error ?? 'HTTP error'}`;
|
|
124
206
|
return {
|
|
@@ -144,12 +226,21 @@ export async function geocodePlace(name, opts = {}) {
|
|
|
144
226
|
};
|
|
145
227
|
}
|
|
146
228
|
export async function getForecast(point, opts = {}) {
|
|
147
|
-
const started =
|
|
229
|
+
const started = monotonicNow();
|
|
148
230
|
const ts = new Date().toISOString();
|
|
149
|
-
const
|
|
231
|
+
const requestedDays = opts.days ?? 7;
|
|
232
|
+
const days = Number.isInteger(requestedDays) && requestedDays > 0 ? Math.min(requestedDays, 16) : 0;
|
|
233
|
+
const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
|
|
234
|
+
if (days < 1 || timeoutMs === null) return {
|
|
235
|
+
ok: false,
|
|
236
|
+
via: 'open-meteo-error',
|
|
237
|
+
evidence: `[实时API:open-meteo@error@${ts}]`,
|
|
238
|
+
latencyMs: monotonicNow() - started,
|
|
239
|
+
error: days < 1 ? 'days must be valid positive values' : TIMEOUT_ERROR
|
|
240
|
+
};
|
|
150
241
|
const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode` + `&forecast_days=${days}&timezone=auto`;
|
|
151
|
-
const r = await fetchJson(url,
|
|
152
|
-
const latencyMs =
|
|
242
|
+
const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl);
|
|
243
|
+
const latencyMs = monotonicNow() - started;
|
|
153
244
|
if (!r.ok) {
|
|
154
245
|
return {
|
|
155
246
|
ok: false,
|
|
@@ -159,8 +250,9 @@ export async function getForecast(point, opts = {}) {
|
|
|
159
250
|
error: r.error
|
|
160
251
|
};
|
|
161
252
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
253
|
+
const payload = isRecord(r.data) ? r.data : null;
|
|
254
|
+
const daily = isRecord(payload?.['daily']) ? payload['daily'] : null;
|
|
255
|
+
if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max) || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode) || daily.time.length !== days || daily.temperature_2m_max.length !== days || daily.temperature_2m_min.length !== days || daily.weathercode.length !== days || daily.time.some((v)=>typeof v !== 'string') || daily.temperature_2m_max.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.temperature_2m_min.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.weathercode.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.precipitation_probability_max !== undefined && daily.precipitation_probability_max !== null && !Array.isArray(daily.precipitation_probability_max) || Array.isArray(daily.precipitation_probability_max) && (daily.precipitation_probability_max.length !== days || daily.precipitation_probability_max.some((v)=>v !== null && (typeof v !== 'number' || !Number.isFinite(v))))) {
|
|
164
256
|
return {
|
|
165
257
|
ok: false,
|
|
166
258
|
via: 'open-meteo-error',
|
|
@@ -169,32 +261,57 @@ export async function getForecast(point, opts = {}) {
|
|
|
169
261
|
error: 'empty daily payload'
|
|
170
262
|
};
|
|
171
263
|
}
|
|
172
|
-
const
|
|
264
|
+
const times = daily.time;
|
|
265
|
+
const maxes = daily.temperature_2m_max;
|
|
266
|
+
const mins = daily.temperature_2m_min;
|
|
267
|
+
const codes = daily.weathercode;
|
|
268
|
+
const precip = Array.isArray(daily.precipitation_probability_max) ? daily.precipitation_probability_max : undefined;
|
|
269
|
+
const rows = times.map((date, i)=>({
|
|
173
270
|
date,
|
|
174
|
-
tempMaxC:
|
|
175
|
-
tempMinC:
|
|
176
|
-
precipProbMaxPct:
|
|
177
|
-
weatherCode:
|
|
271
|
+
tempMaxC: maxes[i],
|
|
272
|
+
tempMinC: mins[i],
|
|
273
|
+
precipProbMaxPct: precip?.[i] ?? null,
|
|
274
|
+
weatherCode: codes[i]
|
|
178
275
|
}));
|
|
179
276
|
return {
|
|
180
277
|
ok: true,
|
|
181
278
|
via: 'open-meteo',
|
|
182
279
|
evidence: `[实时API:open-meteo@${ts}]`,
|
|
183
280
|
latencyMs,
|
|
184
|
-
timezone:
|
|
281
|
+
timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
|
|
185
282
|
daily: rows
|
|
186
283
|
};
|
|
187
284
|
}
|
|
188
285
|
export async function getClimate(point, month, opts = {}) {
|
|
189
|
-
const started =
|
|
286
|
+
const started = monotonicNow();
|
|
190
287
|
const ts = new Date().toISOString();
|
|
191
|
-
const
|
|
288
|
+
const timeoutMs = normalizeTimeout(opts.timeoutMs ?? 15_000);
|
|
289
|
+
if (timeoutMs === null) return {
|
|
290
|
+
ok: false,
|
|
291
|
+
via: 'open-meteo-error',
|
|
292
|
+
evidence: `[实时API:open-meteo-climate@error@${ts}]`,
|
|
293
|
+
latencyMs: monotonicNow() - started,
|
|
294
|
+
error: TIMEOUT_ERROR
|
|
295
|
+
};
|
|
296
|
+
const year = opts.year ?? new Date().getUTCFullYear() - 1;
|
|
297
|
+
if (!Number.isInteger(month) || month < 1 || month > 12 || !Number.isInteger(year) || year < 1940 || year > new Date().getUTCFullYear()) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
via: 'open-meteo-error',
|
|
301
|
+
evidence: `[实时API:open-meteo-climate@error@${ts}]`,
|
|
302
|
+
latencyMs: monotonicNow() - started,
|
|
303
|
+
error: 'month/year must be valid historical calendar values'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
192
306
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
307
|
+
const expectedDates = Array.from({
|
|
308
|
+
length: lastDay
|
|
309
|
+
}, (_, i)=>`${year}-${String(month).padStart(2, '0')}-${String(i + 1).padStart(2, '0')}`);
|
|
193
310
|
const start = `${year}-${String(month).padStart(2, '0')}-01`;
|
|
194
311
|
const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
195
312
|
const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&start_date=${start}&end_date=${end}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`;
|
|
196
|
-
const r = await fetchJson(url,
|
|
197
|
-
const latencyMs =
|
|
313
|
+
const r = await fetchJson(url, timeoutMs, {}, opts.fetchImpl);
|
|
314
|
+
const latencyMs = monotonicNow() - started;
|
|
198
315
|
if (!r.ok) {
|
|
199
316
|
return {
|
|
200
317
|
ok: false,
|
|
@@ -204,8 +321,9 @@ export async function getClimate(point, month, opts = {}) {
|
|
|
204
321
|
error: r.error
|
|
205
322
|
};
|
|
206
323
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
324
|
+
const payload = isRecord(r.data) ? r.data : null;
|
|
325
|
+
const daily = isRecord(payload?.['daily']) ? payload['daily'] : null;
|
|
326
|
+
if (!daily || !Array.isArray(daily.time) || !Array.isArray(daily.temperature_2m_max) || !Array.isArray(daily.temperature_2m_min) || !Array.isArray(daily.weathercode) || daily.time.length !== lastDay || daily.time.length !== daily.temperature_2m_max.length || daily.time.length !== daily.temperature_2m_min.length || daily.time.length !== daily.weathercode.length || daily.time.some((v)=>typeof v !== 'string') || daily.time.some((v, i)=>v !== expectedDates[i]) || daily.temperature_2m_max.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.temperature_2m_min.some((v)=>typeof v !== 'number' || !Number.isFinite(v)) || daily.weathercode.some((v)=>typeof v !== 'number' || !Number.isFinite(v))) {
|
|
209
327
|
return {
|
|
210
328
|
ok: false,
|
|
211
329
|
via: 'open-meteo-error',
|
|
@@ -214,22 +332,26 @@ export async function getClimate(point, month, opts = {}) {
|
|
|
214
332
|
error: 'empty daily payload'
|
|
215
333
|
};
|
|
216
334
|
}
|
|
217
|
-
const
|
|
335
|
+
const times = daily.time;
|
|
336
|
+
const maxes = daily.temperature_2m_max;
|
|
337
|
+
const mins = daily.temperature_2m_min;
|
|
338
|
+
const codes = daily.weathercode;
|
|
339
|
+
const rows = times.map((date, i)=>({
|
|
218
340
|
date,
|
|
219
|
-
tempMaxC:
|
|
220
|
-
tempMinC:
|
|
341
|
+
tempMaxC: maxes[i],
|
|
342
|
+
tempMinC: mins[i],
|
|
221
343
|
precipProbMaxPct: null,
|
|
222
|
-
weatherCode:
|
|
344
|
+
weatherCode: codes[i]
|
|
223
345
|
}));
|
|
224
346
|
return {
|
|
225
347
|
ok: true,
|
|
226
348
|
via: 'open-meteo',
|
|
227
349
|
evidence: `[实时API:open-meteo-climate@${ts}]`,
|
|
228
350
|
latencyMs,
|
|
229
|
-
timezone:
|
|
351
|
+
timezone: typeof payload?.['timezone'] === 'string' ? payload['timezone'] : undefined,
|
|
230
352
|
daily: rows
|
|
231
353
|
};
|
|
232
354
|
}
|
|
233
355
|
|
|
234
356
|
|
|
235
|
-
//# sourceURL
|
|
357
|
+
//# sourceURL=ts/capabilities/weather.ts
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "会话数据面金标准查询集(RFC P2 监控面输入;只增不改语义,对齐 time-slot-eval 惯例)。city 字段约束:from/to 必须同时命中 ctrip 城市码表与美团拼音表(双适配器通用)。",
|
|
3
|
+
"queries": [
|
|
4
|
+
{ "id": "sf-01", "kind": "flight", "from": "上海", "to": "丽江", "date": "2026-10-01" },
|
|
5
|
+
{ "id": "sf-02", "kind": "flight", "from": "北京", "to": "大理", "date": "2026-10-02" },
|
|
6
|
+
{ "id": "sf-03", "kind": "flight", "from": "上海", "to": "三亚", "date": "2026-11-11" },
|
|
7
|
+
{ "id": "sf-04", "kind": "flight", "from": "广州", "to": "昆明", "date": "2026-12-20" },
|
|
8
|
+
{ "id": "sf-05", "kind": "flight", "from": "深圳", "to": "成都", "date": "2026-10-05" },
|
|
9
|
+
{ "id": "sf-06", "kind": "flight", "from": "杭州", "to": "厦门", "date": "2026-10-06" },
|
|
10
|
+
{ "id": "sf-07", "kind": "flight", "from": "西安", "to": "桂林", "date": "2026-10-07" },
|
|
11
|
+
{ "id": "sf-08", "kind": "flight", "from": "重庆", "to": "贵阳", "date": "2026-10-08" },
|
|
12
|
+
{ "id": "mt-01", "kind": "meituan-hotel", "city": "上海", "keyword": "陆家嘴" },
|
|
13
|
+
{ "id": "mt-02", "kind": "meituan-hotel", "city": "大理", "keyword": "洱海" },
|
|
14
|
+
{ "id": "mt-03", "kind": "meituan-hotel", "city": "成都", "keyword": "太古里" },
|
|
15
|
+
{ "id": "mt-04", "kind": "meituan-hotel", "city": "北京", "keyword": "故宫" },
|
|
16
|
+
{ "id": "mt-05", "kind": "meituan-minsu", "city": "丽江", "keyword": "古城" },
|
|
17
|
+
{ "id": "mt-06", "kind": "meituan-minsu", "city": "厦门", "keyword": "鼓浪屿" },
|
|
18
|
+
{ "id": "mt-07", "kind": "meituan-minsu", "city": "杭州", "keyword": "西湖" },
|
|
19
|
+
{ "id": "mt-08", "kind": "meituan-minsu", "city": "三亚", "keyword": "亚龙湾" },
|
|
20
|
+
{ "id": "fa-01", "kind": "flyai-flight", "from": "上海", "to": "丽江", "date": "2026-10-01" },
|
|
21
|
+
{ "id": "fa-02", "kind": "flyai-flight", "from": "北京", "to": "大理", "date": "2026-10-02" },
|
|
22
|
+
{ "id": "fa-03", "kind": "flyai-train", "from": "上海", "to": "大理", "date": "2026-10-01" },
|
|
23
|
+
{ "id": "fa-04", "kind": "flyai-train", "from": "北京", "to": "昆明", "date": "2026-10-03" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "sf-live-benchmark golden(issue #21 验收「字段准确率 ≥90%」对照源)。本表=公开班期 + 价格区间,不绑任何 vendor;字段评分子集硬命中(query_id/from/to/currency/source/verdict)+ 时间/价格/班次软命中(±60min/±15%/子串包含)。sf-01 fixture 来自 ts/scripts/session-benchmark.ts:26-42 已知期望;其余 7 条以公开班期知识写时间窗口与价格带(具体班次由 session 真实落,匹配子串算 correct)。",
|
|
3
|
+
"threshold": 0.9,
|
|
4
|
+
"matches": [
|
|
5
|
+
{
|
|
6
|
+
"query_id": "sf-01",
|
|
7
|
+
"from": "上海",
|
|
8
|
+
"to": "丽江",
|
|
9
|
+
"date": "2026-10-01",
|
|
10
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
11
|
+
"window_arr_local": { "earliest": "10:00", "latest": "23:59" },
|
|
12
|
+
"duration_min": { "min": 180, "max": 720 },
|
|
13
|
+
"price_cny": { "min": 800, "max": 4000 },
|
|
14
|
+
"transport_hint": "直飞或 1 段中转,承运含 MU/HO/9C/CA/CZ 之一",
|
|
15
|
+
"known_flights": ["HO5577", "MU6145", "9C8779"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"query_id": "sf-02",
|
|
19
|
+
"from": "北京",
|
|
20
|
+
"to": "大理",
|
|
21
|
+
"date": "2026-10-02",
|
|
22
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
23
|
+
"window_arr_local": { "earliest": "10:00", "latest": "23:59" },
|
|
24
|
+
"duration_min": { "min": 240, "max": 900 },
|
|
25
|
+
"price_cny": { "min": 800, "max": 4500 },
|
|
26
|
+
"transport_hint": "直飞稀,多经昆明/成都中转 1-2 段,承运含 CA/MU/3U/CZ",
|
|
27
|
+
"known_flights": ["CA1441", "MU5712", "3U8831"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"query_id": "sf-03",
|
|
31
|
+
"from": "上海",
|
|
32
|
+
"to": "三亚",
|
|
33
|
+
"date": "2026-11-11",
|
|
34
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
35
|
+
"window_arr_local": { "earliest": "10:00", "latest": "23:59" },
|
|
36
|
+
"duration_min": { "min": 180, "max": 600 },
|
|
37
|
+
"price_cny": { "min": 500, "max": 3000 },
|
|
38
|
+
"transport_hint": "旺季直飞多,承运含 9C/MU/HU/CZ/CA",
|
|
39
|
+
"known_flights": ["9C8779", "MU5377", "HU7177", "CZ6766"]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"query_id": "sf-04",
|
|
43
|
+
"from": "广州",
|
|
44
|
+
"to": "昆明",
|
|
45
|
+
"date": "2026-12-20",
|
|
46
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
47
|
+
"window_arr_local": { "earliest": "09:00", "latest": "23:59" },
|
|
48
|
+
"duration_min": { "min": 150, "max": 600 },
|
|
49
|
+
"price_cny": { "min": 400, "max": 2500 },
|
|
50
|
+
"transport_hint": "直飞密集,承运含 CZ/MU/3U/8L",
|
|
51
|
+
"known_flights": ["CZ3497", "MU5738", "3U8805", "8L9628"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"query_id": "sf-05",
|
|
55
|
+
"from": "深圳",
|
|
56
|
+
"to": "成都",
|
|
57
|
+
"date": "2026-10-05",
|
|
58
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
59
|
+
"window_arr_local": { "earliest": "09:00", "latest": "23:59" },
|
|
60
|
+
"duration_min": { "min": 150, "max": 600 },
|
|
61
|
+
"price_cny": { "min": 400, "max": 2500 },
|
|
62
|
+
"transport_hint": "直飞密集,承运含 CZ/MU/CA/3U/ZH",
|
|
63
|
+
"known_flights": ["CZ3453", "MU5402", "CA4314", "3U8744"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"query_id": "sf-06",
|
|
67
|
+
"from": "杭州",
|
|
68
|
+
"to": "厦门",
|
|
69
|
+
"date": "2026-10-06",
|
|
70
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
71
|
+
"window_arr_local": { "earliest": "09:00", "latest": "23:59" },
|
|
72
|
+
"duration_min": { "min": 90, "max": 360 },
|
|
73
|
+
"price_cny": { "min": 300, "max": 2000 },
|
|
74
|
+
"transport_hint": "短途直飞,承运含 GJ/MU/CZ/CA",
|
|
75
|
+
"known_flights": ["GJ7153", "MU5520", "CZ6955"]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"query_id": "sf-07",
|
|
79
|
+
"from": "西安",
|
|
80
|
+
"to": "桂林",
|
|
81
|
+
"date": "2026-10-07",
|
|
82
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
83
|
+
"window_arr_local": { "earliest": "09:00", "latest": "23:59" },
|
|
84
|
+
"duration_min": { "min": 150, "max": 600 },
|
|
85
|
+
"price_cny": { "min": 400, "max": 2500 },
|
|
86
|
+
"transport_hint": "直飞较稀,多中转 1 段,承运含 MU/CZ/9C/HU",
|
|
87
|
+
"known_flights": ["JD5143", "MU2176", "CZ6319"]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"query_id": "sf-08",
|
|
91
|
+
"from": "重庆",
|
|
92
|
+
"to": "贵阳",
|
|
93
|
+
"date": "2026-10-08",
|
|
94
|
+
"window_dep_local": { "earliest": "06:00", "latest": "22:00" },
|
|
95
|
+
"window_arr_local": { "earliest": "09:00", "latest": "23:59" },
|
|
96
|
+
"duration_min": { "min": 60, "max": 300 },
|
|
97
|
+
"price_cny": { "min": 250, "max": 1800 },
|
|
98
|
+
"transport_hint": "极短途直飞,承运含 G5/CZ/MU/GY",
|
|
99
|
+
"known_flights": ["G52667", "CZ5817", "MU2146", "GY7122"]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "sf-static-routes.v1",
|
|
3
|
+
"source": {
|
|
4
|
+
"name": "OpenFlights routes.dat",
|
|
5
|
+
"url": "https://raw.githubusercontent.com/jpatokal/openflights/4b969f8e91eb800c45f0e0e2355a0fbb93de27e4/data/routes.dat",
|
|
6
|
+
"revision": "4b969f8e91eb800c45f0e0e2355a0fbb93de27e4",
|
|
7
|
+
"license": "ODbL-1.0",
|
|
8
|
+
"retrieved_at": "2026-08-30",
|
|
9
|
+
"limitations": [
|
|
10
|
+
"route/carrier coverage only",
|
|
11
|
+
"no schedule",
|
|
12
|
+
"no price",
|
|
13
|
+
"no availability"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"routes": [
|
|
17
|
+
{
|
|
18
|
+
"query_id": "sf-01",
|
|
19
|
+
"from_city": "上海",
|
|
20
|
+
"to_city": "丽江",
|
|
21
|
+
"route_kind": "direct",
|
|
22
|
+
"legs": [
|
|
23
|
+
{ "from_iata": "SHA", "to_iata": "LJG", "carrier_codes": ["MU"] }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query_id": "sf-02",
|
|
28
|
+
"from_city": "北京",
|
|
29
|
+
"to_city": "大理",
|
|
30
|
+
"route_kind": "transfer",
|
|
31
|
+
"legs": [
|
|
32
|
+
{ "from_iata": "PEK", "to_iata": "KMG", "carrier_codes": ["8L", "CA", "CZ", "HU", "MU", "ZH"] },
|
|
33
|
+
{ "from_iata": "KMG", "to_iata": "DLU", "carrier_codes": ["8L", "CZ", "MU"] }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"query_id": "sf-03",
|
|
38
|
+
"from_city": "上海",
|
|
39
|
+
"to_city": "三亚",
|
|
40
|
+
"route_kind": "direct",
|
|
41
|
+
"legs": [
|
|
42
|
+
{ "from_iata": "PVG", "to_iata": "SYX", "carrier_codes": ["9C", "CZ", "FM", "HO", "MU"] }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"query_id": "sf-04",
|
|
47
|
+
"from_city": "广州",
|
|
48
|
+
"to_city": "昆明",
|
|
49
|
+
"route_kind": "direct",
|
|
50
|
+
"legs": [
|
|
51
|
+
{ "from_iata": "CAN", "to_iata": "KMG", "carrier_codes": ["CA", "CZ", "KY", "MU", "ZH"] }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"query_id": "sf-05",
|
|
56
|
+
"from_city": "深圳",
|
|
57
|
+
"to_city": "成都",
|
|
58
|
+
"route_kind": "direct",
|
|
59
|
+
"legs": [
|
|
60
|
+
{ "from_iata": "SZX", "to_iata": "CTU", "carrier_codes": ["3U", "CA", "CZ", "EU", "HU", "TV", "ZH"] }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"query_id": "sf-06",
|
|
65
|
+
"from_city": "杭州",
|
|
66
|
+
"to_city": "厦门",
|
|
67
|
+
"route_kind": "direct",
|
|
68
|
+
"legs": [
|
|
69
|
+
{ "from_iata": "HGH", "to_iata": "XMN", "carrier_codes": ["CA", "CZ", "MF", "MU", "SC"] }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"query_id": "sf-07",
|
|
74
|
+
"from_city": "西安",
|
|
75
|
+
"to_city": "桂林",
|
|
76
|
+
"route_kind": "direct",
|
|
77
|
+
"legs": [
|
|
78
|
+
{ "from_iata": "XIY", "to_iata": "KWL", "carrier_codes": ["CA", "CZ", "GS", "JD", "MU", "NS", "ZH"] }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"query_id": "sf-08",
|
|
83
|
+
"from_city": "重庆",
|
|
84
|
+
"to_city": "贵阳",
|
|
85
|
+
"route_kind": "direct",
|
|
86
|
+
"legs": [
|
|
87
|
+
{ "from_iata": "CKG", "to_iata": "KWE", "carrier_codes": ["CA", "CZ", "G5", "GS", "SC"] }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|