@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.
Files changed (89) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/capabilities/session/bridge-ledger.js +168 -67
  4. package/dist/capabilities/session/extension-bridge.js +140 -46
  5. package/dist/capabilities/session/gui-flight.js +281 -69
  6. package/dist/capabilities/session/gui-hotel.js +74 -17
  7. package/dist/capabilities/session-search.js +6 -5
  8. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
  9. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
  10. package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
  11. package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
  12. package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
  13. package/dist/scripts/bridge-ledger-tests.js +20 -18
  14. package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
  15. package/dist/scripts/mutation-check.js +153 -0
  16. package/dist/scripts/nightly-evidence.js +4 -111
  17. package/dist/scripts/session-tests.js +42 -8
  18. package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
  19. package/dist/scripts/tenant-capacity-probe.js +263 -0
  20. package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
  21. package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
  22. package/dist/scripts/tenant-executor-binding-tests.js +203 -0
  23. package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
  24. package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
  25. package/dist/scripts/tenant-seam-quota-tests.js +383 -0
  26. package/dist/scripts/tenant-seam-test-kit.js +292 -0
  27. package/dist/scripts/tenant-state-copies-tests.js +607 -0
  28. package/dist/scripts/tenant-state-drill-tests.js +298 -0
  29. package/dist/scripts/tenant-state-drill-worker.js +164 -0
  30. package/dist/scripts/tenant-state-durable-conformance.js +101 -0
  31. package/dist/scripts/tenant-state-handoff-tests.js +854 -0
  32. package/dist/scripts/tenant-state-lease-conformance.js +142 -0
  33. package/dist/scripts/tenant-state-lease-worker.js +28 -0
  34. package/dist/scripts/tenant-state-router-tests.js +464 -0
  35. package/dist/scripts/tenant-state-test-kit.js +92 -0
  36. package/dist/scripts/tenant-state-tests.js +564 -0
  37. package/dist/src/backend/modules/booking-executor.js +61 -6
  38. package/dist/src/backend/modules/session-search.js +136 -39
  39. package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  40. package/dist/src/booking-surface/dsh-planner.js +150 -19
  41. package/dist/src/booking-surface/index.js +1 -0
  42. package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
  43. package/dist/src/booking-surface/server.js +193 -80
  44. package/dist/src/booking-surface/startup.js +153 -0
  45. package/dist/src/booking-surface/tenancy.js +188 -0
  46. package/dist/src/index.js +31 -1
  47. package/dist/src/llm-price-table.js +148 -0
  48. package/dist/src/tenant-seam/index.js +8 -0
  49. package/dist/src/tenant-seam/llm-broker.js +508 -0
  50. package/dist/src/tenant-seam/process-governor.js +281 -0
  51. package/dist/src/tenant-seam/quotas.js +360 -0
  52. package/dist/src/tenant-seam/seam.js +92 -0
  53. package/dist/src/tenant-state/durable-store.js +158 -0
  54. package/dist/src/tenant-state/epoch-copies.js +422 -0
  55. package/dist/src/tenant-state/errors.js +108 -0
  56. package/dist/src/tenant-state/ids.js +101 -0
  57. package/dist/src/tenant-state/index.js +11 -0
  58. package/dist/src/tenant-state/lease-store.js +203 -0
  59. package/dist/src/tenant-state/tenant-lease.js +150 -0
  60. package/dist/src/tenant-state/tenant-router.js +650 -0
  61. package/extension/manifest.json +2 -2
  62. package/package.json +4 -1
  63. package/ts/capabilities/session/bridge-ledger.ts +192 -78
  64. package/ts/capabilities/session/extension-bridge.ts +176 -65
  65. package/ts/capabilities/session/gui-flight.ts +318 -88
  66. package/ts/capabilities/session/gui-hotel.ts +72 -18
  67. package/ts/capabilities/session-search.ts +17 -5
  68. package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  69. package/ts/src/booking-surface/dsh-planner.ts +169 -19
  70. package/ts/src/booking-surface/index.ts +1 -0
  71. package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
  72. package/ts/src/booking-surface/server.ts +215 -84
  73. package/ts/src/booking-surface/startup.ts +155 -0
  74. package/ts/src/booking-surface/tenancy.ts +242 -0
  75. package/ts/src/index.ts +28 -2
  76. package/ts/src/llm-price-table.ts +180 -0
  77. package/ts/src/tenant-seam/index.ts +9 -0
  78. package/ts/src/tenant-seam/llm-broker.ts +520 -0
  79. package/ts/src/tenant-seam/process-governor.ts +343 -0
  80. package/ts/src/tenant-seam/quotas.ts +433 -0
  81. package/ts/src/tenant-seam/seam.ts +142 -0
  82. package/ts/src/tenant-state/durable-store.ts +171 -0
  83. package/ts/src/tenant-state/epoch-copies.ts +491 -0
  84. package/ts/src/tenant-state/errors.ts +128 -0
  85. package/ts/src/tenant-state/ids.ts +108 -0
  86. package/ts/src/tenant-state/index.ts +12 -0
  87. package/ts/src/tenant-state/lease-store.ts +266 -0
  88. package/ts/src/tenant-state/tenant-lease.ts +175 -0
  89. 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
- function assertPriceEntry(label, value) {
68
- const entry = typeof value === 'object' && value !== null ? value : null;
69
- if (!entry)
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
- const itinerary = opts.apiBody === undefined
915
- ? { url: 'https://flights.ctrip.com/international/search/api/search/batchSearch', body: FIXTURE }
916
- : opts.apiBody ? { url: 'x', body: opts.apiBody } : null;
917
- const calendar = opts.calendarBody ? { url: 'https://m.ctrip.com/restapi/soa2/15380/bjjson/FlightIntlAndInlandLowestPriceSearch', body: opts.calendarBody } : null;
918
- return (itinerary || calendar) ? { ...(itinerary ? { itinerary } : {}), ...(calendar ? { calendar } : {}) } : null;
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 && /出发城市框未确认/.test(g2b.error ?? '') && cityDriver.state.clickedSearch === false, 'readback 逐字段:城市框错位(全局框假阳)→ 终止', g2b);
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;