@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export const DIST_ASSET_TARBALL = 'gotry-session-bridge.tar.gz';
|
|
6
|
+
export const DIST_ASSET_STORE_ZIP = 'gotry-session-bridge-store.zip';
|
|
7
|
+
export const DIST_ASSET_MANIFEST = 'extension-dist-manifest.json';
|
|
8
|
+
export const DEFAULT_RELEASE_BASE = 'https://github.com/Danceiny/gotry/releases';
|
|
9
|
+
export function compareVersions(a, b) {
|
|
10
|
+
const pa = String(a).split('.').map((s)=>Number.parseInt(s, 10) || 0);
|
|
11
|
+
const pb = String(b).split('.').map((s)=>Number.parseInt(s, 10) || 0);
|
|
12
|
+
const n = Math.max(pa.length, pb.length);
|
|
13
|
+
for(let i = 0; i < n; i += 1){
|
|
14
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
15
|
+
if (d !== 0) return d;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
export function distAssetUrls(releaseBase = DEFAULT_RELEASE_BASE) {
|
|
20
|
+
const base = releaseBase.replace(/\/+$/, '');
|
|
21
|
+
return {
|
|
22
|
+
tarball: `${base}/latest/download/${DIST_ASSET_TARBALL}`,
|
|
23
|
+
storeZip: `${base}/latest/download/${DIST_ASSET_STORE_ZIP}`,
|
|
24
|
+
manifest: `${base}/latest/download/${DIST_ASSET_MANIFEST}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
28
|
+
export function parseDistManifest(raw) {
|
|
29
|
+
let obj;
|
|
30
|
+
try {
|
|
31
|
+
obj = JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error('dist-manifest 不是合法 JSON');
|
|
34
|
+
}
|
|
35
|
+
const version = obj.version;
|
|
36
|
+
const tarball = obj.tarball;
|
|
37
|
+
const tarballSha256 = obj.tarballSha256;
|
|
38
|
+
const zip = obj.zip;
|
|
39
|
+
const zipSha256 = obj.zipSha256;
|
|
40
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error('dist-manifest.version 缺失或非 x.y.z');
|
|
41
|
+
if (tarball !== DIST_ASSET_TARBALL) throw new Error(`dist-manifest.tarball 资产名漂移(期望 ${DIST_ASSET_TARBALL})`);
|
|
42
|
+
if (zip !== DIST_ASSET_STORE_ZIP) throw new Error(`dist-manifest.zip 资产名漂移(期望 ${DIST_ASSET_STORE_ZIP})`);
|
|
43
|
+
if (typeof tarballSha256 !== 'string' || !SHA256_RE.test(tarballSha256)) throw new Error('dist-manifest.tarballSha256 非 64-hex');
|
|
44
|
+
if (typeof zipSha256 !== 'string' || !SHA256_RE.test(zipSha256)) throw new Error('dist-manifest.zipSha256 非 64-hex');
|
|
45
|
+
return {
|
|
46
|
+
version,
|
|
47
|
+
tarball,
|
|
48
|
+
tarballSha256,
|
|
49
|
+
zip,
|
|
50
|
+
zipSha256,
|
|
51
|
+
builtFromCommit: typeof obj.builtFromCommit === 'string' ? obj.builtFromCommit : undefined
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function sha256Hex(bytes) {
|
|
55
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
56
|
+
}
|
|
57
|
+
export function readExtensionKey(extensionDir) {
|
|
58
|
+
const p = join(extensionDir, 'manifest.json');
|
|
59
|
+
if (!existsSync(p)) return undefined;
|
|
60
|
+
try {
|
|
61
|
+
const m = JSON.parse(readFileSync(p, 'utf8'));
|
|
62
|
+
return typeof m.key === 'string' ? m.key : undefined;
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function readExtensionVersion(extensionDir) {
|
|
68
|
+
const p = join(extensionDir, 'manifest.json');
|
|
69
|
+
if (!existsSync(p)) return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const m = JSON.parse(readFileSync(p, 'utf8'));
|
|
72
|
+
return typeof m.version === 'string' ? m.version : undefined;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function runTar(args, spawnFn, timeoutMs) {
|
|
78
|
+
return new Promise((resolve)=>{
|
|
79
|
+
const child = spawnFn('tar', args, {
|
|
80
|
+
stdio: 'ignore'
|
|
81
|
+
});
|
|
82
|
+
let done = false;
|
|
83
|
+
const timer = setTimeout(()=>{
|
|
84
|
+
if (!done) {
|
|
85
|
+
done = true;
|
|
86
|
+
try {
|
|
87
|
+
child.kill('SIGKILL');
|
|
88
|
+
} catch {}
|
|
89
|
+
resolve({
|
|
90
|
+
ok: false,
|
|
91
|
+
error: `tar 超时(>${timeoutMs}ms)`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
child.on('error', (e)=>{
|
|
96
|
+
if (!done) {
|
|
97
|
+
done = true;
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve({
|
|
100
|
+
ok: false,
|
|
101
|
+
error: `tar 不可用:${e.message}`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
child.on('close', (code)=>{
|
|
106
|
+
if (!done) {
|
|
107
|
+
done = true;
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
resolve(code === 0 ? {
|
|
110
|
+
ok: true
|
|
111
|
+
} : {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `tar exit ${code}`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function fetchBytes(url, fetchFn, timeoutMs) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetchFn(url, {
|
|
122
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) return {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: `HTTP ${res.status} ${url}`
|
|
127
|
+
};
|
|
128
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
bytes: buf
|
|
132
|
+
};
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `下载失败 ${e.message}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function installExtensionFromGithub(opts) {
|
|
141
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
142
|
+
const fetchFn = opts.fetch ?? fetch;
|
|
143
|
+
const spawnFn = opts.spawnFn ?? spawn;
|
|
144
|
+
const urls = distAssetUrls(opts.releaseBase);
|
|
145
|
+
const fail = (error)=>({
|
|
146
|
+
ok: false,
|
|
147
|
+
action: 'fallback-bundled',
|
|
148
|
+
error
|
|
149
|
+
});
|
|
150
|
+
const mRes = await fetchBytes(urls.manifest, fetchFn, timeoutMs);
|
|
151
|
+
if (!mRes.ok) return fail(mRes.error);
|
|
152
|
+
let manifest;
|
|
153
|
+
try {
|
|
154
|
+
manifest = parseDistManifest(new TextDecoder().decode(mRes.bytes));
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return fail(`dist-manifest 校验失败:${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
const previousVersion = readExtensionVersion(opts.destDir);
|
|
159
|
+
if (previousVersion != null && compareVersions(manifest.version, previousVersion) <= 0) {
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
action: 'up-to-date',
|
|
163
|
+
version: manifest.version,
|
|
164
|
+
previousVersion
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (opts.checkOnly) {
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
action: 'up-to-date',
|
|
171
|
+
version: manifest.version,
|
|
172
|
+
previousVersion
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const tRes = await fetchBytes(urls.tarball, fetchFn, timeoutMs);
|
|
176
|
+
if (!tRes.ok) return fail(tRes.error);
|
|
177
|
+
const actual = sha256Hex(tRes.bytes);
|
|
178
|
+
if (actual !== manifest.tarballSha256) return fail(`SHA256 不符(期望 ${manifest.tarballSha256.slice(0, 12)}… 实得 ${actual.slice(0, 12)}…)`);
|
|
179
|
+
const incoming = `${opts.destDir}.incoming-${process.pid}-${Date.now()}`;
|
|
180
|
+
const tarPath = `${incoming}.tar.gz`;
|
|
181
|
+
try {
|
|
182
|
+
rmSync(incoming, {
|
|
183
|
+
recursive: true,
|
|
184
|
+
force: true
|
|
185
|
+
});
|
|
186
|
+
mkdirSync(incoming, {
|
|
187
|
+
recursive: true
|
|
188
|
+
});
|
|
189
|
+
writeFileSync(tarPath, tRes.bytes);
|
|
190
|
+
const ext = await runTar([
|
|
191
|
+
'-xzf',
|
|
192
|
+
tarPath,
|
|
193
|
+
'-C',
|
|
194
|
+
incoming,
|
|
195
|
+
'--strip-components',
|
|
196
|
+
'1'
|
|
197
|
+
], spawnFn, timeoutMs);
|
|
198
|
+
if (!ext.ok) return fail(ext.error);
|
|
199
|
+
const incomingKey = readExtensionKey(incoming);
|
|
200
|
+
if (incomingKey == null) return fail('解压产物缺 manifest.json/key(非法包)');
|
|
201
|
+
if (opts.pinnedSourceDir) {
|
|
202
|
+
const pinnedKey = readExtensionKey(opts.pinnedSourceDir);
|
|
203
|
+
if (pinnedKey != null && incomingKey !== pinnedKey) return fail('扩展 key 漂移(通道产物与 bundled 不同 ID——拒绝安装,防端口池/host 权限漂移)');
|
|
204
|
+
}
|
|
205
|
+
const incomingVersion = readExtensionVersion(incoming);
|
|
206
|
+
if (incomingVersion !== manifest.version) return fail(`解压产物版本(${incomingVersion})与 dist-manifest(${manifest.version})不符`);
|
|
207
|
+
rmSync(opts.destDir, {
|
|
208
|
+
recursive: true,
|
|
209
|
+
force: true
|
|
210
|
+
});
|
|
211
|
+
cpSync(incoming, opts.destDir, {
|
|
212
|
+
recursive: true
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
action: 'installed',
|
|
217
|
+
version: manifest.version,
|
|
218
|
+
previousVersion
|
|
219
|
+
};
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return fail(`落位失败 ${e.message}`);
|
|
222
|
+
} finally{
|
|
223
|
+
rmSync(incoming, {
|
|
224
|
+
recursive: true,
|
|
225
|
+
force: true
|
|
226
|
+
});
|
|
227
|
+
rmSync(tarPath, {
|
|
228
|
+
force: true
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
//# sourceURL=ts/capabilities/session/extension-distribution.ts
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SESSION_FIELD_ACCURACY_THRESHOLD } from './benchmark.js';
|
|
2
|
+
function parseHHmm(value) {
|
|
3
|
+
const [hours, minutes] = value.split(':');
|
|
4
|
+
return parseInt(hours, 10) * 60 + parseInt(minutes, 10);
|
|
5
|
+
}
|
|
6
|
+
function parseLocalMinutes(value) {
|
|
7
|
+
const match = value.match(/(\d{2}):(\d{2})/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
return parseInt(match[1], 10) * 60 + parseInt(match[2], 10);
|
|
10
|
+
}
|
|
11
|
+
function parseLocalDate(value) {
|
|
12
|
+
return value.match(/^(\d{4}-\d{2}-\d{2})[T ]/)?.[1] ?? null;
|
|
13
|
+
}
|
|
14
|
+
export function scoreSessionAgainstGoldenBand(session, band, goldenSource) {
|
|
15
|
+
const missing = [];
|
|
16
|
+
const hardIncorrect = [];
|
|
17
|
+
const segment = session.route_segments[0];
|
|
18
|
+
if (!session.query_id) missing.push('query_id');
|
|
19
|
+
else if (session.query_id !== band.query_id) hardIncorrect.push('query_id');
|
|
20
|
+
if (!session.currency) missing.push('currency');
|
|
21
|
+
else if (session.currency !== 'CNY') hardIncorrect.push('currency');
|
|
22
|
+
if (!session.source) missing.push('source');
|
|
23
|
+
else if (session.source !== 'ctrip-flight') hardIncorrect.push('source');
|
|
24
|
+
if (!session.verdict) missing.push('verdict');
|
|
25
|
+
else if (session.verdict !== 'hit') hardIncorrect.push('verdict');
|
|
26
|
+
if (!segment) {
|
|
27
|
+
missing.push('route_segments[0].from', 'route_segments[0].to', 'route_segments[0].departure_at', 'route_segments[0].arrival_at', 'route_segments[0].transport_number');
|
|
28
|
+
} else {
|
|
29
|
+
if (!segment.from) missing.push('route_segments[0].from');
|
|
30
|
+
else if (segment.from !== band.from) hardIncorrect.push('route_segments[0].from');
|
|
31
|
+
if (!segment.to) missing.push('route_segments[0].to');
|
|
32
|
+
else if (segment.to !== band.to) hardIncorrect.push('route_segments[0].to');
|
|
33
|
+
if (!segment.departure_at) missing.push('route_segments[0].departure_at');
|
|
34
|
+
else if (parseLocalDate(segment.departure_at) !== band.date) {
|
|
35
|
+
hardIncorrect.push('route_segments[0].departure_at.date');
|
|
36
|
+
}
|
|
37
|
+
if (!segment.arrival_at) missing.push('route_segments[0].arrival_at');
|
|
38
|
+
else if (parseLocalDate(segment.arrival_at) !== band.date) {
|
|
39
|
+
hardIncorrect.push('route_segments[0].arrival_at.date');
|
|
40
|
+
}
|
|
41
|
+
if (!segment.transport_number) missing.push('route_segments[0].transport_number');
|
|
42
|
+
}
|
|
43
|
+
const softChecks = [];
|
|
44
|
+
if (segment?.departure_at) {
|
|
45
|
+
const minutes = parseLocalMinutes(segment.departure_at);
|
|
46
|
+
softChecks.push({
|
|
47
|
+
path: 'route_segments[0].departure_at',
|
|
48
|
+
ok: minutes !== null && minutes >= parseHHmm(band.window_dep_local.earliest) && minutes <= parseHHmm(band.window_dep_local.latest)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (segment?.arrival_at) {
|
|
52
|
+
const minutes = parseLocalMinutes(segment.arrival_at);
|
|
53
|
+
softChecks.push({
|
|
54
|
+
path: 'route_segments[0].arrival_at',
|
|
55
|
+
ok: minutes !== null && minutes >= parseHHmm(band.window_arr_local.earliest) && minutes <= parseHHmm(band.window_arr_local.latest)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (segment?.transport_number) {
|
|
59
|
+
const actual = segment.transport_number.toUpperCase();
|
|
60
|
+
softChecks.push({
|
|
61
|
+
path: 'route_segments[0].transport_number',
|
|
62
|
+
ok: band.known_flights.some((flight)=>actual.startsWith(flight.toUpperCase())) || band.known_flights.some((flight)=>actual.includes(flight.toUpperCase().slice(0, 2)))
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (Number.isFinite(session.price) && session.price > 0) {
|
|
66
|
+
const margin = band.price_cny.max * 0.15;
|
|
67
|
+
softChecks.push({
|
|
68
|
+
path: 'price',
|
|
69
|
+
ok: session.price >= band.price_cny.min - margin && session.price <= band.price_cny.max + margin
|
|
70
|
+
});
|
|
71
|
+
} else missing.push('price');
|
|
72
|
+
const softIncorrect = softChecks.filter((check)=>!check.ok).map((check)=>check.path);
|
|
73
|
+
const incorrect = [
|
|
74
|
+
...hardIncorrect,
|
|
75
|
+
...softIncorrect
|
|
76
|
+
];
|
|
77
|
+
const total = 13;
|
|
78
|
+
const correct = Math.max(0, total - new Set(missing).size - incorrect.length);
|
|
79
|
+
const accuracy = total > 0 ? correct / total : 0;
|
|
80
|
+
return {
|
|
81
|
+
pass: missing.length === 0 && hardIncorrect.length === 0 && accuracy >= SESSION_FIELD_ACCURACY_THRESHOLD,
|
|
82
|
+
total,
|
|
83
|
+
correct,
|
|
84
|
+
accuracy,
|
|
85
|
+
missing: Array.from(new Set(missing)),
|
|
86
|
+
incorrect,
|
|
87
|
+
golden_source: goldenSource
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
//# sourceURL=ts/capabilities/session/golden-score.ts
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { request } from 'node:http';
|
|
2
|
+
import { BRIDGE_PORTS, EXTENSION_CONNECTED_WINDOW_MS } from './extension-bridge.js';
|
|
3
|
+
export async function combinedProbe(opts, businessProbe) {
|
|
4
|
+
if (!await defaultProbe(opts)) return false;
|
|
5
|
+
if (!businessProbe) return true;
|
|
6
|
+
try {
|
|
7
|
+
const channel = await import('./extension-channel.js');
|
|
8
|
+
const bridge = await import('./extension-bridge.js');
|
|
9
|
+
bridge.getOrCreateSessionBridge({
|
|
10
|
+
keepBridge: opts.keepBridge === true
|
|
11
|
+
});
|
|
12
|
+
const cookie = await channel.extensionCookieNames({
|
|
13
|
+
site: 'ctrip-flight',
|
|
14
|
+
domain: 'ctrip.com',
|
|
15
|
+
ticketNames: [
|
|
16
|
+
'cticket',
|
|
17
|
+
'uid'
|
|
18
|
+
],
|
|
19
|
+
timeoutMs: 8_000
|
|
20
|
+
});
|
|
21
|
+
return cookie.ok;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function defaultProbe(opts) {
|
|
27
|
+
const ports = opts.ports ?? [
|
|
28
|
+
...BRIDGE_PORTS
|
|
29
|
+
];
|
|
30
|
+
for (const port of ports){
|
|
31
|
+
const ok = await new Promise((resolve)=>{
|
|
32
|
+
const req = request({
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
port,
|
|
35
|
+
path: '/status',
|
|
36
|
+
method: 'GET',
|
|
37
|
+
timeout: 2_000
|
|
38
|
+
}, (res)=>{
|
|
39
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
40
|
+
resolve(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let buf = '';
|
|
44
|
+
res.setEncoding('utf8');
|
|
45
|
+
res.on('data', (c)=>{
|
|
46
|
+
buf += c;
|
|
47
|
+
});
|
|
48
|
+
res.on('end', ()=>{
|
|
49
|
+
try {
|
|
50
|
+
const payload = JSON.parse(buf);
|
|
51
|
+
resolve(payload.extensionConnected === true);
|
|
52
|
+
} catch {
|
|
53
|
+
resolve(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
res.on('error', ()=>resolve(false));
|
|
57
|
+
});
|
|
58
|
+
req.on('error', ()=>resolve(false));
|
|
59
|
+
req.on('timeout', ()=>{
|
|
60
|
+
req.destroy();
|
|
61
|
+
resolve(false);
|
|
62
|
+
});
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
if (ok) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export function startExtensionHealthWatch(opts = {}) {
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
71
|
+
const intervalMs = opts.intervalMs ?? 5_000;
|
|
72
|
+
const maxRetries = opts.maxRetries ?? 1;
|
|
73
|
+
const businessProbe = opts.businessProbe ?? true;
|
|
74
|
+
const probe = opts.probe ?? (async ()=>{
|
|
75
|
+
const bridge = await import('./extension-bridge.js');
|
|
76
|
+
await bridge.getOrCreateSessionBridge({
|
|
77
|
+
keepBridge: opts.keepBridge === true
|
|
78
|
+
});
|
|
79
|
+
return combinedProbe(opts, businessProbe);
|
|
80
|
+
});
|
|
81
|
+
const now = opts.now ?? Date.now;
|
|
82
|
+
const state = {
|
|
83
|
+
cancelled: false,
|
|
84
|
+
readyCount: 0,
|
|
85
|
+
resolveOutcome: null
|
|
86
|
+
};
|
|
87
|
+
const finish = (outcome)=>{
|
|
88
|
+
if (state.resolveOutcome) {
|
|
89
|
+
const resolve = state.resolveOutcome;
|
|
90
|
+
state.resolveOutcome = null;
|
|
91
|
+
resolve(outcome);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onAbort = ()=>{
|
|
95
|
+
state.cancelled = true;
|
|
96
|
+
finish({
|
|
97
|
+
ready: false,
|
|
98
|
+
reason: 'cancelled',
|
|
99
|
+
attempts: state.readyCount,
|
|
100
|
+
waitedMs: 0
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
opts.signal?.addEventListener('abort', onAbort, {
|
|
104
|
+
once: true
|
|
105
|
+
});
|
|
106
|
+
const waitReady = ()=>new Promise((resolve)=>{
|
|
107
|
+
if (state.cancelled) {
|
|
108
|
+
resolve({
|
|
109
|
+
ready: false,
|
|
110
|
+
reason: 'cancelled',
|
|
111
|
+
attempts: 0,
|
|
112
|
+
waitedMs: 0
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (timeoutMs === 0) {
|
|
117
|
+
resolve({
|
|
118
|
+
ready: false,
|
|
119
|
+
reason: 'timeout',
|
|
120
|
+
attempts: 0,
|
|
121
|
+
waitedMs: 0
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
state.resolveOutcome = resolve;
|
|
126
|
+
const deadline = now() + timeoutMs;
|
|
127
|
+
let attempts = 0;
|
|
128
|
+
let waitedMs = 0;
|
|
129
|
+
const tick = async ()=>{
|
|
130
|
+
if (state.cancelled || state.resolveOutcome === null) return;
|
|
131
|
+
attempts += 1;
|
|
132
|
+
try {
|
|
133
|
+
const ready = await probe();
|
|
134
|
+
if (ready && !state.cancelled) {
|
|
135
|
+
state.readyCount += 1;
|
|
136
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
137
|
+
if (state.readyCount <= maxRetries) {
|
|
138
|
+
finish({
|
|
139
|
+
ready: true,
|
|
140
|
+
attempts,
|
|
141
|
+
waitedMs: waitedMs > 0 ? waitedMs : 0
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
148
|
+
if (now() >= deadline) {
|
|
149
|
+
finish({
|
|
150
|
+
ready: false,
|
|
151
|
+
reason: 'timeout',
|
|
152
|
+
attempts,
|
|
153
|
+
waitedMs
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const remaining = deadline - now();
|
|
158
|
+
const delay = Math.min(intervalMs, Math.max(0, remaining));
|
|
159
|
+
if (delay <= 0) {
|
|
160
|
+
finish({
|
|
161
|
+
ready: false,
|
|
162
|
+
reason: 'timeout',
|
|
163
|
+
attempts,
|
|
164
|
+
waitedMs
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setTimeout(tick, delay);
|
|
169
|
+
};
|
|
170
|
+
void tick();
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
waitReady,
|
|
174
|
+
cancel: ()=>onAbort(),
|
|
175
|
+
get readyCount () {
|
|
176
|
+
return state.readyCount;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function withAutoRetryOnExtension(args) {
|
|
181
|
+
const first = await args.fetch();
|
|
182
|
+
if (!args.isExtensionNeeded(first)) {
|
|
183
|
+
return {
|
|
184
|
+
result: first,
|
|
185
|
+
retried: false,
|
|
186
|
+
attempts: 1,
|
|
187
|
+
outcome: {
|
|
188
|
+
ready: true,
|
|
189
|
+
attempts: 0,
|
|
190
|
+
waitedMs: 0
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const watch = startExtensionHealthWatch(args.watch ?? {});
|
|
195
|
+
const outcome = await watch.waitReady();
|
|
196
|
+
if (!outcome.ready) {
|
|
197
|
+
return {
|
|
198
|
+
result: first,
|
|
199
|
+
retried: false,
|
|
200
|
+
attempts: 1,
|
|
201
|
+
outcome
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const retriedResult = await (args.retry ?? args.fetch)();
|
|
205
|
+
return {
|
|
206
|
+
result: retriedResult,
|
|
207
|
+
retried: true,
|
|
208
|
+
attempts: 2,
|
|
209
|
+
outcome
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export const HEALTH_WATCH_INTERVAL_FLOOR_MS = 1_000;
|
|
213
|
+
export const HEALTH_WATCH_INTERVAL_CEIL_MS = EXTENSION_CONNECTED_WINDOW_MS - 1_000;
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
//# sourceURL=ts/capabilities/session/health-watch.ts
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function parseGoldenSource(args) {
|
|
4
|
+
const raw = args.find((arg)=>arg.startsWith('--golden='))?.slice('--golden='.length) ?? 'manual';
|
|
5
|
+
if (raw === 'manual' || raw === 'flyai' || raw === 'static') return raw;
|
|
6
|
+
throw new Error(`不支持的 golden vendor: ${raw}`);
|
|
7
|
+
}
|
|
8
|
+
export function loadStaticFlightSnapshot(path = join(import.meta.dirname, '..', '..', 'data', 'sf-static-routes.json')) {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
10
|
+
if (!isRecord(parsed) || parsed.schema_version !== 'sf-static-routes.v1') {
|
|
11
|
+
throw new Error(`invalid static snapshot schema_version: ${String(isRecord(parsed) ? parsed.schema_version : undefined)}`);
|
|
12
|
+
}
|
|
13
|
+
const source = parsed.source;
|
|
14
|
+
if (!isRecord(source) || ![
|
|
15
|
+
'name',
|
|
16
|
+
'url',
|
|
17
|
+
'revision',
|
|
18
|
+
'license',
|
|
19
|
+
'retrieved_at'
|
|
20
|
+
].every((key)=>typeof source[key] === 'string') || !Array.isArray(source.limitations) || !source.limitations.every((item)=>typeof item === 'string')) {
|
|
21
|
+
throw new Error('invalid static snapshot source');
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(parsed.routes) || !parsed.routes.every(isStaticFlightRoute)) {
|
|
24
|
+
throw new Error('invalid static snapshot routes');
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function isStaticFlightRoute(value) {
|
|
32
|
+
if (!isRecord(value) || typeof value.query_id !== 'string' || typeof value.from_city !== 'string' || typeof value.to_city !== 'string' || value.route_kind !== 'direct' && value.route_kind !== 'transfer' || !Array.isArray(value.legs) || value.legs.length === 0) return false;
|
|
33
|
+
return value.legs.every((leg)=>isRecord(leg) && typeof leg.from_iata === 'string' && typeof leg.to_iata === 'string' && Array.isArray(leg.carrier_codes) && leg.carrier_codes.every((carrier)=>typeof carrier === 'string'));
|
|
34
|
+
}
|
|
35
|
+
function provenance(snapshot) {
|
|
36
|
+
return {
|
|
37
|
+
route_source: snapshot?.source.url ?? '',
|
|
38
|
+
route_revision: snapshot?.source.revision ?? '',
|
|
39
|
+
route_license: snapshot?.source.license ?? '',
|
|
40
|
+
band_source: 'sf-golden-manifest.json'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function formatWithOffset(timestampMs, offset) {
|
|
44
|
+
const sign = offset.startsWith('-') ? -1 : 1;
|
|
45
|
+
const [hours, minutes] = offset.slice(1).split(':').map(Number);
|
|
46
|
+
const offsetMinutes = sign * ((hours ?? 0) * 60 + (minutes ?? 0));
|
|
47
|
+
const local = new Date(timestampMs + offsetMinutes * 60_000).toISOString().slice(0, 19);
|
|
48
|
+
return `${local}${offset}`;
|
|
49
|
+
}
|
|
50
|
+
function buildSegments(query, route, manualSegment) {
|
|
51
|
+
if (route.route_kind === 'direct' && route.legs.length === 1) {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
from: query.from,
|
|
55
|
+
to: query.to,
|
|
56
|
+
departure_at: manualSegment.departure_at,
|
|
57
|
+
arrival_at: manualSegment.arrival_at,
|
|
58
|
+
transport_number: `${route.legs[0].carrier_codes[0]}-STATIC`
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
if (route.route_kind !== 'transfer' || route.legs.length !== 2) return null;
|
|
63
|
+
const departureMs = Date.parse(manualSegment.departure_at);
|
|
64
|
+
const arrivalMs = Date.parse(manualSegment.arrival_at);
|
|
65
|
+
if (!Number.isFinite(departureMs) || !Number.isFinite(arrivalMs) || arrivalMs <= departureMs) return null;
|
|
66
|
+
const offset = manualSegment.departure_at.match(/([+-]\d{2}:\d{2})$/)?.[1] ?? '+08:00';
|
|
67
|
+
const totalMinutes = Math.floor((arrivalMs - departureMs) / 60_000);
|
|
68
|
+
if (totalMinutes < 90) return null;
|
|
69
|
+
const layoverMinutes = Math.min(60, Math.floor(totalMinutes / 3));
|
|
70
|
+
const travelMinutes = totalMinutes - layoverMinutes;
|
|
71
|
+
const firstTravelMinutes = Math.floor(travelMinutes / 2);
|
|
72
|
+
const secondDepartureMs = arrivalMs - (travelMinutes - firstTravelMinutes) * 60_000;
|
|
73
|
+
const middle = route.legs[0].to_iata;
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
from: query.from,
|
|
77
|
+
to: middle,
|
|
78
|
+
departure_at: manualSegment.departure_at,
|
|
79
|
+
arrival_at: formatWithOffset(departureMs + firstTravelMinutes * 60_000, offset),
|
|
80
|
+
transport_number: `${route.legs[0].carrier_codes[0]}-STATIC`
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
from: middle,
|
|
84
|
+
to: query.to,
|
|
85
|
+
departure_at: formatWithOffset(secondDepartureMs, offset),
|
|
86
|
+
arrival_at: manualSegment.arrival_at,
|
|
87
|
+
transport_number: `${route.legs[1].carrier_codes[0]}-STATIC`
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
export function resolveStaticGolden(input) {
|
|
92
|
+
const route = input.snapshot?.routes.find((candidate)=>candidate.query_id === input.query.id);
|
|
93
|
+
let fallbackReason;
|
|
94
|
+
if (input.snapshotError) {
|
|
95
|
+
fallbackReason = `static snapshot unavailable: ${input.snapshotError}`;
|
|
96
|
+
} else if (!route) {
|
|
97
|
+
fallbackReason = `static route missing for ${input.query.id}`;
|
|
98
|
+
} else if (route.from_city !== input.query.from || route.to_city !== input.query.to) {
|
|
99
|
+
fallbackReason = `static route mismatch for ${input.query.id}`;
|
|
100
|
+
} else if (route.legs.some((leg)=>leg.carrier_codes.length === 0)) {
|
|
101
|
+
fallbackReason = `static carrier coverage missing for ${input.query.id}`;
|
|
102
|
+
}
|
|
103
|
+
const manualSegment = input.manualRecord.route_segments[0];
|
|
104
|
+
const segments = !fallbackReason && route && manualSegment ? buildSegments(input.query, route, manualSegment) : null;
|
|
105
|
+
if (!fallbackReason && !segments) fallbackReason = `static route cannot form comparable record for ${input.query.id}`;
|
|
106
|
+
if (fallbackReason) {
|
|
107
|
+
input.warn?.(`[sf-live-benchmark] static vendor failed for ${input.query.id}: ${fallbackReason}; fallback=manual-golden`);
|
|
108
|
+
return {
|
|
109
|
+
requested_source: 'static',
|
|
110
|
+
effective_source: 'manual-golden',
|
|
111
|
+
record: input.manualRecord,
|
|
112
|
+
fallback_reason: fallbackReason,
|
|
113
|
+
estimated_fields: [],
|
|
114
|
+
provenance: provenance(input.snapshot)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
requested_source: 'static',
|
|
119
|
+
effective_source: 'static-openflights+manual-band',
|
|
120
|
+
record: {
|
|
121
|
+
...input.manualRecord,
|
|
122
|
+
route_segments: segments,
|
|
123
|
+
journey_type: route.route_kind,
|
|
124
|
+
source: 'static-openflights+manual-band'
|
|
125
|
+
},
|
|
126
|
+
estimated_fields: [
|
|
127
|
+
'route_segments[].departure_at',
|
|
128
|
+
'route_segments[].arrival_at',
|
|
129
|
+
'route_segments[].transport_number',
|
|
130
|
+
'price'
|
|
131
|
+
],
|
|
132
|
+
provenance: provenance(input.snapshot)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
//# sourceURL=ts/capabilities/session/static-flight-golden.ts
|