@danceiny/gotry 0.2.0-rc.32 → 0.2.0-rc.33
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/capabilities/session/bridge-ledger.js +168 -67
- package/dist/capabilities/session/extension-bridge.js +140 -46
- package/dist/capabilities/session/gui-flight.js +281 -69
- package/dist/capabilities/session/gui-hotel.js +74 -17
- package/dist/capabilities/session-search.js +6 -5
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
- package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
- package/dist/scripts/bridge-ledger-tests.js +20 -18
- package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
- package/dist/scripts/mutation-check.js +153 -0
- package/dist/scripts/nightly-evidence.js +4 -111
- package/dist/scripts/session-tests.js +42 -8
- package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
- package/dist/scripts/tenant-capacity-probe.js +263 -0
- package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
- package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
- package/dist/scripts/tenant-executor-binding-tests.js +203 -0
- package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
- package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
- package/dist/scripts/tenant-seam-quota-tests.js +383 -0
- package/dist/scripts/tenant-seam-test-kit.js +292 -0
- package/dist/scripts/tenant-state-copies-tests.js +607 -0
- package/dist/scripts/tenant-state-drill-tests.js +298 -0
- package/dist/scripts/tenant-state-drill-worker.js +164 -0
- package/dist/scripts/tenant-state-durable-conformance.js +101 -0
- package/dist/scripts/tenant-state-handoff-tests.js +854 -0
- package/dist/scripts/tenant-state-lease-conformance.js +142 -0
- package/dist/scripts/tenant-state-lease-worker.js +28 -0
- package/dist/scripts/tenant-state-router-tests.js +464 -0
- package/dist/scripts/tenant-state-test-kit.js +92 -0
- package/dist/scripts/tenant-state-tests.js +564 -0
- package/dist/src/backend/modules/booking-executor.js +61 -6
- package/dist/src/backend/modules/session-search.js +136 -39
- package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/dist/src/booking-surface/dsh-planner.js +150 -19
- package/dist/src/booking-surface/index.js +1 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
- package/dist/src/booking-surface/server.js +193 -80
- package/dist/src/booking-surface/startup.js +153 -0
- package/dist/src/booking-surface/tenancy.js +188 -0
- package/dist/src/index.js +31 -1
- package/dist/src/llm-price-table.js +148 -0
- package/dist/src/tenant-seam/index.js +8 -0
- package/dist/src/tenant-seam/llm-broker.js +508 -0
- package/dist/src/tenant-seam/process-governor.js +281 -0
- package/dist/src/tenant-seam/quotas.js +360 -0
- package/dist/src/tenant-seam/seam.js +92 -0
- package/dist/src/tenant-state/durable-store.js +158 -0
- package/dist/src/tenant-state/epoch-copies.js +422 -0
- package/dist/src/tenant-state/errors.js +108 -0
- package/dist/src/tenant-state/ids.js +101 -0
- package/dist/src/tenant-state/index.js +11 -0
- package/dist/src/tenant-state/lease-store.js +203 -0
- package/dist/src/tenant-state/tenant-lease.js +150 -0
- package/dist/src/tenant-state/tenant-router.js +650 -0
- package/extension/manifest.json +2 -2
- package/package.json +4 -1
- package/ts/capabilities/session/bridge-ledger.ts +192 -78
- package/ts/capabilities/session/extension-bridge.ts +176 -65
- package/ts/capabilities/session/gui-flight.ts +318 -88
- package/ts/capabilities/session/gui-hotel.ts +72 -18
- package/ts/capabilities/session-search.ts +17 -5
- package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/ts/src/booking-surface/dsh-planner.ts +169 -19
- package/ts/src/booking-surface/index.ts +1 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
- package/ts/src/booking-surface/server.ts +215 -84
- package/ts/src/booking-surface/startup.ts +155 -0
- package/ts/src/booking-surface/tenancy.ts +242 -0
- package/ts/src/index.ts +28 -2
- package/ts/src/llm-price-table.ts +180 -0
- package/ts/src/tenant-seam/index.ts +9 -0
- package/ts/src/tenant-seam/llm-broker.ts +520 -0
- package/ts/src/tenant-seam/process-governor.ts +343 -0
- package/ts/src/tenant-seam/quotas.ts +433 -0
- package/ts/src/tenant-seam/seam.ts +142 -0
- package/ts/src/tenant-state/durable-store.ts +171 -0
- package/ts/src/tenant-state/epoch-copies.ts +491 -0
- package/ts/src/tenant-state/errors.ts +128 -0
- package/ts/src/tenant-state/ids.ts +108 -0
- package/ts/src/tenant-state/index.ts +12 -0
- package/ts/src/tenant-state/lease-store.ts +266 -0
- package/ts/src/tenant-state/tenant-lease.ts +175 -0
- package/ts/src/tenant-state/tenant-router.ts +743 -0
|
@@ -32,6 +32,7 @@ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { createMockLlm } from '../src/mock-llm.js';
|
|
34
34
|
import { createOpenAICompatLlm } from '../src/dsh-llm.js';
|
|
35
|
+
import { loadPriceTable, priceRunCost } from '../src/llm-price-table.js';
|
|
35
36
|
import { newState, runTurn } from '../src/loop.js';
|
|
36
37
|
import { solveUnified } from '../src/unified.js';
|
|
37
38
|
import { realtimeSolvePort } from '../src/realtime-pricing.js';
|
|
@@ -50,9 +51,6 @@ function canonical(value) {
|
|
|
50
51
|
function sha256Of(value) {
|
|
51
52
|
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
52
53
|
}
|
|
53
|
-
function round6(value) {
|
|
54
|
-
return Number(value.toFixed(6));
|
|
55
|
-
}
|
|
56
54
|
export function loadPromptSet(path) {
|
|
57
55
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
58
56
|
if (raw['schema_version'] !== 'gotry_m3_nightly_prompt_set_v1') {
|
|
@@ -64,114 +62,9 @@ export function loadPromptSet(path) {
|
|
|
64
62
|
}
|
|
65
63
|
return { schema_version: 'gotry_m3_nightly_prompt_set_v1', description: raw['description'], turns: turns };
|
|
66
64
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw new Error(`${label} must be an object`);
|
|
71
|
-
for (const field of ['input_cache_miss_usd_per_1m_peak', 'input_cache_hit_usd_per_1m_peak', 'output_usd_per_1m_peak']) {
|
|
72
|
-
const num = entry[field];
|
|
73
|
-
if (typeof num !== 'number' || !Number.isFinite(num) || num < 0) {
|
|
74
|
-
throw new Error(`${label}.${field} must be a non-negative finite number`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
}
|
|
79
|
-
export function loadPriceTable(path) {
|
|
80
|
-
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
81
|
-
const schemaVersion = raw['schema_version'];
|
|
82
|
-
if (schemaVersion === 'gotry_llm_price_table_v2') {
|
|
83
|
-
return loadPriceTableV2(raw);
|
|
84
|
-
}
|
|
85
|
-
if (schemaVersion === 'gotry_llm_price_table_v1') {
|
|
86
|
-
return loadPriceTableV1(raw);
|
|
87
|
-
}
|
|
88
|
-
throw new Error(`price table schema_version must be gotry_llm_price_table_v1 or _v2, got: ${String(schemaVersion)}`);
|
|
89
|
-
}
|
|
90
|
-
function loadPriceTableV1(raw) {
|
|
91
|
-
const aliases = typeof raw['aliases'] === 'object' && raw['aliases'] !== null ? raw['aliases'] : {};
|
|
92
|
-
for (const [from, to] of Object.entries(aliases)) {
|
|
93
|
-
if (typeof to !== 'string')
|
|
94
|
-
throw new Error(`price table alias ${from} must map to a model name`);
|
|
95
|
-
}
|
|
96
|
-
const prices = {};
|
|
97
|
-
for (const [name, entry] of Object.entries(typeof raw['prices'] === 'object' && raw['prices'] !== null ? raw['prices'] : {})) {
|
|
98
|
-
prices[name] = assertPriceEntry(`price table entry ${name}`, entry);
|
|
99
|
-
}
|
|
100
|
-
return { schema_version: 'gotry_llm_price_table_v1', aliases, prices };
|
|
101
|
-
}
|
|
102
|
-
/** v2 reader:providers.<id>.models.<model> 平铺为统一的 aliases/prices 双表,外暴露 API 不变。
|
|
103
|
-
* lookup 规则:给定 model 名 →
|
|
104
|
-
* 1. 若在 aliases 表中,跳到别名目标(qualified `providerId:model`);
|
|
105
|
-
* 2. 否则直接以 qualified `providerId:model` 在 prices 中查找;
|
|
106
|
-
* 3. 否则尝试裸 `model` 键(单 provider 时透明,如 MiniMax-M3 在 minimax:models 下唯一);
|
|
107
|
-
* 4. 仍找不到 → fail-closed。 */
|
|
108
|
-
function loadPriceTableV2(raw) {
|
|
109
|
-
const providersObj = typeof raw['providers'] === 'object' && raw['providers'] !== null ? raw['providers'] : null;
|
|
110
|
-
if (!providersObj)
|
|
111
|
-
throw new Error('price table v2 requires providers object');
|
|
112
|
-
const aliases = {};
|
|
113
|
-
const prices = {};
|
|
114
|
-
for (const [providerId, rawProvider] of Object.entries(providersObj)) {
|
|
115
|
-
const provider = assertProviderEntry(`price table provider ${providerId}`, rawProvider);
|
|
116
|
-
for (const [aliasName, target] of Object.entries(provider.aliases)) {
|
|
117
|
-
if (typeof target !== 'string')
|
|
118
|
-
throw new Error(`provider ${providerId} alias ${aliasName} must map to a model name`);
|
|
119
|
-
if (!provider.models[target]) {
|
|
120
|
-
throw new Error(`provider ${providerId} alias ${aliasName} -> ${target} has no entry; update data/llm-price-table.json via PR`);
|
|
121
|
-
}
|
|
122
|
-
aliases[aliasName] = target;
|
|
123
|
-
}
|
|
124
|
-
for (const [modelName, entry] of Object.entries(provider.models)) {
|
|
125
|
-
const qualified = `${providerId}:${modelName}`;
|
|
126
|
-
const validated = assertPriceEntry(`price table provider ${providerId} model ${modelName}`, entry);
|
|
127
|
-
if (prices[qualified])
|
|
128
|
-
throw new Error(`duplicate price table key ${qualified}`);
|
|
129
|
-
prices[qualified] = validated;
|
|
130
|
-
if (prices[modelName])
|
|
131
|
-
throw new Error(`duplicate price table key ${modelName} (across providers, disambiguate via aliases or qualified lookup)`);
|
|
132
|
-
prices[modelName] = validated;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return { schema_version: 'gotry_llm_price_table_v2', aliases, prices };
|
|
136
|
-
}
|
|
137
|
-
function assertProviderEntry(label, value) {
|
|
138
|
-
const entry = typeof value === 'object' && value !== null ? value : null;
|
|
139
|
-
if (!entry)
|
|
140
|
-
throw new Error(`${label} must be an object`);
|
|
141
|
-
const family = entry['family'];
|
|
142
|
-
if (typeof family !== 'string' || family.length === 0)
|
|
143
|
-
throw new Error(`${label}.family must be a non-empty string`);
|
|
144
|
-
const strategy = entry['price_strategy'];
|
|
145
|
-
if (strategy !== 'tiered_peak_offpeak' && strategy !== 'flat_no_offpeak') {
|
|
146
|
-
throw new Error(`${label}.price_strategy must be 'tiered_peak_offpeak' or 'flat_no_offpeak', got: ${String(strategy)}`);
|
|
147
|
-
}
|
|
148
|
-
const aliases = typeof entry['aliases'] === 'object' && entry['aliases'] !== null ? entry['aliases'] : {};
|
|
149
|
-
const models = typeof entry['models'] === 'object' && entry['models'] !== null ? entry['models'] : {};
|
|
150
|
-
return {
|
|
151
|
-
family,
|
|
152
|
-
price_strategy: strategy,
|
|
153
|
-
api_base: typeof entry['api_base'] === 'string' ? entry['api_base'] : undefined,
|
|
154
|
-
source_url: typeof entry['source_url'] === 'string' ? entry['source_url'] : undefined,
|
|
155
|
-
aliases,
|
|
156
|
-
models,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
/** 换算单次运行成本(peak 保守上界:全部输入按 cache miss 峰价计,只高不低)。 */
|
|
160
|
-
export function priceRunCost(model, usage, table) {
|
|
161
|
-
if (usage.responsesMissingUsage > 0) {
|
|
162
|
-
throw new Error(`usage provider omitted usage on ${usage.responsesMissingUsage} response(s) — cost unprovable, refusing to price (fail-closed)`);
|
|
163
|
-
}
|
|
164
|
-
const alias = table.aliases[model];
|
|
165
|
-
if (alias && !table.prices[alias])
|
|
166
|
-
throw new Error(`price table alias ${model} -> ${alias} has no entry; update data/llm-price-table.json via PR`);
|
|
167
|
-
const entry = table.prices[alias ?? model];
|
|
168
|
-
if (!entry)
|
|
169
|
-
throw new Error(`price table has no entry for model ${model}; update data/llm-price-table.json via PR (fail-closed, no guessed price)`);
|
|
170
|
-
const cost = (usage.inputCacheMissTokens * entry.input_cache_miss_usd_per_1m_peak
|
|
171
|
-
+ usage.inputCacheHitTokens * entry.input_cache_hit_usd_per_1m_peak
|
|
172
|
-
+ usage.outputTokens * entry.output_usd_per_1m_peak) / 1_000_000;
|
|
173
|
-
return round6(cost);
|
|
174
|
-
}
|
|
65
|
+
// The frozen price table (ADR-20) and its pricing function live in src/llm-price-table.ts so the AaaS quota meter prices
|
|
66
|
+
// through the same code; they are re-exported here for the nightly producer and its callers.
|
|
67
|
+
export { loadPriceTable, priceRunCost };
|
|
175
68
|
/** 记录构建(确定性:同内容同 executed_at => 同 run_key)。写入前由消费方 parseNightlyRun 校验。 */
|
|
176
69
|
export function buildNightlyRunRecord(input) {
|
|
177
70
|
const { schema_version, executed_at, real_llm, prompt_set_sha256, output_sha256, cost_usd } = input;
|
|
@@ -877,7 +877,7 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
877
877
|
const planRt = planFlightGuiActions({ from: '香港', to: '迪拜', date: '2026-12-06', backDate: '2026-12-13' });
|
|
878
878
|
assert(planRt.some(a => a.t === 'clickTripTab' && a.trip === 'roundtrip') && planRt.some(a => a.t === 'setDate' && a.which === 'back'), '往返规划:往返 tab + 回程日期(不知 URL 形状也能搜)');
|
|
879
879
|
const rb = planRt.find(a => a.t === 'readback');
|
|
880
|
-
assert(!!rb && rb.t === 'readback' && rb.expect.back === '2026-12-13', 'readback 期望含回程日期');
|
|
880
|
+
assert(!!rb && rb.t === 'readback' && rb.expect.legs[0]?.back === '2026-12-13', 'readback 期望含回程日期(legs 形状)');
|
|
881
881
|
// 安全白名单:闭集外/提交件文本/非固定入口/坏日期/坏按键全拒
|
|
882
882
|
assert(!assertGuiPlanSafe([{ t: 'goto', url: 'https://evil.example.com/' }]).ok, '安全闸:非固定入口 URL 拒绝');
|
|
883
883
|
assert(!assertGuiPlanSafe([{ t: 'typeCity', which: 'from', text: '立即下单' }]).ok, '安全闸:城市位提交件文本拒绝(写面红线)');
|
|
@@ -897,7 +897,7 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
897
897
|
{ departDate: '/Date(1792252800000+0800)/', price: 760 },
|
|
898
898
|
] });
|
|
899
899
|
const makeFakeDriver = (opts = {}) => {
|
|
900
|
-
const state = { clickedSearch: false, form: { from: '', to: '', dep: '', back: '' } };
|
|
900
|
+
const state = { clickedSearch: false, drives: 0, form: { from: '', to: '', dep: '', back: '' } };
|
|
901
901
|
const page = {
|
|
902
902
|
async goto() { },
|
|
903
903
|
async clickTripTab() { },
|
|
@@ -910,12 +910,16 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
910
910
|
async cityFieldValues() { const f = { ...state.form, ...opts.formOverride }; return { from: f.from ?? '', to: f.to ?? '' }; },
|
|
911
911
|
async pageText() { return opts.challenge ? '请输入验证码 拖动滑块' : '机票预订'; },
|
|
912
912
|
async currentUrl() { return opts.listUrl ?? 'https://flights.ctrip.com/online/channel/domestic'; },
|
|
913
|
+
async waitForHumanLogin() { return opts.loginResult ?? false; },
|
|
913
914
|
async waitForApiCapture() {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
915
|
+
state.drives += 1;
|
|
916
|
+
const first = state.drives === 1;
|
|
917
|
+
const itinerary = first
|
|
918
|
+
? (opts.apiBody === undefined ? { url: 'https://flights.ctrip.com/international/search/api/search/batchSearch', body: FIXTURE } : opts.apiBody ? { url: 'x', body: opts.apiBody } : null)
|
|
919
|
+
: (opts.afterLoginBody ? { url: 'x', body: opts.afterLoginBody } : null);
|
|
920
|
+
const calendar = first && opts.calendarBody ? { url: 'https://m.ctrip.com/restapi/soa2/15380/bjjson/FlightIntlAndInlandLowestPriceSearch', body: opts.calendarBody } : null;
|
|
921
|
+
const probe = first && opts.loginProbe ? { url: 'x', body: '{"data":null,"needUserLogin":true}' } : undefined;
|
|
922
|
+
return (itinerary || calendar || probe) ? { ...(itinerary ? { itinerary } : {}), ...(calendar ? { calendar } : {}), ...(probe ? { loginProbe: probe } : {}) } : null;
|
|
919
923
|
},
|
|
920
924
|
};
|
|
921
925
|
return { driver: { open: async () => page, close: async () => { } }, state };
|
|
@@ -950,7 +954,7 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
950
954
|
const cityDriver = makeFakeDriver({ formOverride: { from: '北京(BJS)' } }); // 城市打进别的框(live 实测形态)
|
|
951
955
|
__setGuiDriverForTest(() => cityDriver.driver);
|
|
952
956
|
const g2b = await runFlightGuiSearch({ from: '上海', to: '丽江', date: '2027-05-02' });
|
|
953
|
-
assert(g2b.ok === false &&
|
|
957
|
+
assert(g2b.ok === false && /城市框未确认/.test(g2b.error ?? '') && cityDriver.state.clickedSearch === false, 'readback 逐字段:城市框错位(全局框假阳)→ 终止', g2b);
|
|
954
958
|
__setGuiDriverForTest(() => makeFakeDriver({ challenge: true }).driver);
|
|
955
959
|
const g3 = await runFlightGuiSearch({ from: '上海', to: '丽江', date: '2027-05-02' });
|
|
956
960
|
assert(g3.ok === false && g3.verdict === 'challenged', '风控页 → challenged(不重试不绕过)', g3);
|
|
@@ -974,6 +978,35 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
974
978
|
const rtUrl = buildEntryUrl('香港', '迪拜', '2026-11-13', {}, '2026-11-17');
|
|
975
979
|
assert(rtUrl.ok === true && rtUrl.url === 'https://flights.ctrip.com/online/list/round-hkg0-dxb0?depdate=2026-11-13_2026-11-17&cabin=y_s&adult=1&child=0&infant=0', '往返 URL=GUI 教师实证形状(round-{code}0)', rtUrl);
|
|
976
980
|
__setGuiDriverForTest(null);
|
|
981
|
+
// N. GUI 登录态配对 + 多程(2026-10-15,#661/#663/#664 余尾收口)
|
|
982
|
+
{
|
|
983
|
+
console.log('N. 登录态配对(interactiveLogin)+ 多程规划');
|
|
984
|
+
// 多程规划:multi tab + 逐段(城市×2+日期)动作 + legs readback
|
|
985
|
+
const planMc = planFlightGuiActions({ from: '', to: '', date: '', legs: [
|
|
986
|
+
{ from: '香港', to: '迪拜', date: '2026-12-06' },
|
|
987
|
+
{ from: '迪拜', to: '上海', date: '2026-12-10' },
|
|
988
|
+
] });
|
|
989
|
+
assert(planMc.some(a => a.t === 'clickTripTab' && a.trip === 'multi'), '多程规划:点 multi tab');
|
|
990
|
+
assert(planMc.filter(a => a.t === 'setDate').length === 2 && planMc.some(a => a.t === 'setDate' && a.seg === 2), '多程规划:逐段日期(seg 维度)');
|
|
991
|
+
assert(!assertGuiPlanSafe([{ t: 'typeCity', which: 'from', text: '香港', seg: 4 }]).ok, '安全闸:seg 闭集(1–3)外拒绝');
|
|
992
|
+
// 登录配对(机票):首驾只有日历+needUserLogin 探测 → 等人登录 → 重驾拿行程 → anonymous=false
|
|
993
|
+
const loginDriver = makeFakeDriver({ apiBody: null, calendarBody: CAL_FIXTURE, loginProbe: true, loginResult: true, afterLoginBody: FIXTURE });
|
|
994
|
+
__setGuiDriverForTest(() => loginDriver.driver);
|
|
995
|
+
const n1 = await runFlightGuiSearch({ from: '上海', to: '丽江', date: '2026-11-13', interactiveLoginMs: 60_000 });
|
|
996
|
+
assert(n1.ok === true && (n1.options?.length ?? 0) === 1 && n1.anonymous === false, '登录配对(机票):探测→等人登录→重驾→行程明细 hit,anonymous=false', n1);
|
|
997
|
+
assert(/本人已在本窗口官方页登录/.test(n1.evidence), '登录态证据链标注');
|
|
998
|
+
// 登录等待超时:needs-login 终态(不冒充成功)
|
|
999
|
+
const timeoutDriver = makeFakeDriver({ apiBody: null, calendarBody: CAL_FIXTURE, loginProbe: true, loginResult: false });
|
|
1000
|
+
__setGuiDriverForTest(() => timeoutDriver.driver);
|
|
1001
|
+
const n2 = await runFlightGuiSearch({ from: '上海', to: '丽江', date: '2026-11-13', interactiveLoginMs: 60_000 });
|
|
1002
|
+
assert(n2.ok === false && n2.verdict === 'needs-login' && /登录等待超时/.test(n2.error ?? ''), '登录等待超时 → needs-login 终态', n2);
|
|
1003
|
+
// 未授权等待(interactiveLoginMs 缺省):保持匿名日历面,不弹登录
|
|
1004
|
+
const anonDriver = makeFakeDriver({ apiBody: null, calendarBody: CAL_FIXTURE, loginProbe: true });
|
|
1005
|
+
__setGuiDriverForTest(() => anonDriver.driver);
|
|
1006
|
+
const n3 = await runFlightGuiSearch({ from: '上海', to: '丽江', date: '2026-11-13' });
|
|
1007
|
+
assert(n3.ok === true && (n3.calendar?.length ?? 0) === 3 && n3.anonymous === true, '未授权等待:保持匿名日历面(默认行为不变)', n3);
|
|
1008
|
+
__setGuiDriverForTest(null);
|
|
1009
|
+
}
|
|
977
1010
|
}
|
|
978
1011
|
// M. 携程酒店 GUI 车道(2026-10-14,#664 首片):规划/安全闸/readback/needs-login 边界
|
|
979
1012
|
{
|
|
@@ -1003,6 +1036,7 @@ console.log('M. Dida 门户(entry 守域 + 信封走形 + 闸面)');
|
|
|
1003
1036
|
async destFieldValue() { return (opts.formOverride?.to ?? state.form.to) + '(测试)'; },
|
|
1004
1037
|
async pageText() { return '酒店预订'; },
|
|
1005
1038
|
async currentUrl() { return state.url; },
|
|
1039
|
+
async waitForHumanLogin() { return false; },
|
|
1006
1040
|
async waitForApiBody() {
|
|
1007
1041
|
if (opts.passportAfterSearch)
|
|
1008
1042
|
return null;
|