@danceiny/gotry 0.2.0-rc.31 → 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 (128) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +63 -53
  4. package/bin/gotry-hbcli-release.d.ts +3 -0
  5. package/bin/gotry-hbcli-release.js +4 -0
  6. package/client/client.js +66 -21
  7. package/dist/capabilities/doctor.js +7 -8
  8. package/dist/capabilities/effect.js +45 -12
  9. package/dist/capabilities/flyai-config.js +12 -10
  10. package/dist/capabilities/flyai.js +87 -56
  11. package/dist/capabilities/hbcli-config.js +156 -0
  12. package/dist/capabilities/hbcli.js +29 -2
  13. package/dist/capabilities/resilience.js +6 -6
  14. package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
  16. package/dist/capabilities/session/bridge-ledger.js +168 -67
  17. package/dist/capabilities/session/ctrip-city.js +186 -0
  18. package/dist/capabilities/session/extension-bridge.js +148 -47
  19. package/dist/capabilities/session/gui-flight.js +949 -0
  20. package/dist/capabilities/session/gui-hotel.js +470 -0
  21. package/dist/capabilities/session-search.js +102 -14
  22. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
  23. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
  24. package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
  25. package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
  26. package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
  27. package/dist/scripts/bootstrap-tests.js +23 -6
  28. package/dist/scripts/bridge-ledger-tests.js +20 -18
  29. package/dist/scripts/ctrip-city-tests.js +80 -0
  30. package/dist/scripts/effect-tests.js +602 -504
  31. package/dist/scripts/extension-tests.js +14 -8
  32. package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
  33. package/dist/scripts/flight-malformed-tests.js +1 -1
  34. package/dist/scripts/flyai-setup-tests.js +19 -4
  35. package/dist/scripts/flyai-tests.js +768 -653
  36. package/dist/scripts/gotry-web-api-tests.js +5 -0
  37. package/dist/scripts/gotry-web-client-tests.js +20 -1
  38. package/dist/scripts/gui-flight-live.js +27 -0
  39. package/dist/scripts/hbcli-tests.js +1 -1
  40. package/dist/scripts/hbcli-upgrade-tests.js +107 -0
  41. package/dist/scripts/hbcli-web-setup-tests.js +114 -0
  42. package/dist/scripts/hotel-date-gate-tests.js +1 -1
  43. package/dist/scripts/hotel-result-tests.js +169 -0
  44. package/dist/scripts/issue-271-liveness-tests.js +1 -1
  45. package/dist/scripts/mutation-check.js +153 -0
  46. package/dist/scripts/nightly-evidence.js +4 -111
  47. package/dist/scripts/session-tests.js +211 -6
  48. package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
  49. package/dist/scripts/tenant-capacity-probe.js +263 -0
  50. package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
  51. package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
  52. package/dist/scripts/tenant-executor-binding-tests.js +203 -0
  53. package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
  54. package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
  55. package/dist/scripts/tenant-seam-quota-tests.js +383 -0
  56. package/dist/scripts/tenant-seam-test-kit.js +292 -0
  57. package/dist/scripts/tenant-state-copies-tests.js +607 -0
  58. package/dist/scripts/tenant-state-drill-tests.js +298 -0
  59. package/dist/scripts/tenant-state-drill-worker.js +164 -0
  60. package/dist/scripts/tenant-state-durable-conformance.js +101 -0
  61. package/dist/scripts/tenant-state-handoff-tests.js +854 -0
  62. package/dist/scripts/tenant-state-lease-conformance.js +142 -0
  63. package/dist/scripts/tenant-state-lease-worker.js +28 -0
  64. package/dist/scripts/tenant-state-router-tests.js +464 -0
  65. package/dist/scripts/tenant-state-test-kit.js +92 -0
  66. package/dist/scripts/tenant-state-tests.js +564 -0
  67. package/dist/src/backend/modules/booking-executor.js +61 -6
  68. package/dist/src/backend/modules/session-search.js +136 -39
  69. package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  70. package/dist/src/booking-surface/dsh-planner.js +150 -19
  71. package/dist/src/booking-surface/index.js +1 -0
  72. package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
  73. package/dist/src/booking-surface/server.js +193 -80
  74. package/dist/src/booking-surface/startup.js +153 -0
  75. package/dist/src/booking-surface/tenancy.js +188 -0
  76. package/dist/src/gotry-web-api.js +64 -2
  77. package/dist/src/index.js +70 -18
  78. package/dist/src/llm-price-table.js +148 -0
  79. package/dist/src/tenant-seam/index.js +8 -0
  80. package/dist/src/tenant-seam/llm-broker.js +508 -0
  81. package/dist/src/tenant-seam/process-governor.js +281 -0
  82. package/dist/src/tenant-seam/quotas.js +360 -0
  83. package/dist/src/tenant-seam/seam.js +92 -0
  84. package/dist/src/tenant-state/durable-store.js +158 -0
  85. package/dist/src/tenant-state/epoch-copies.js +422 -0
  86. package/dist/src/tenant-state/errors.js +108 -0
  87. package/dist/src/tenant-state/ids.js +101 -0
  88. package/dist/src/tenant-state/index.js +11 -0
  89. package/dist/src/tenant-state/lease-store.js +203 -0
  90. package/dist/src/tenant-state/tenant-lease.js +150 -0
  91. package/dist/src/tenant-state/tenant-router.js +650 -0
  92. package/extension/manifest.json +2 -2
  93. package/package.json +5 -1
  94. package/ts/capabilities/flyai-config.ts +12 -10
  95. package/ts/capabilities/flyai.ts +91 -50
  96. package/ts/capabilities/hbcli-config.ts +128 -0
  97. package/ts/capabilities/hbcli.ts +30 -3
  98. package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
  99. package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
  100. package/ts/capabilities/session/bridge-ledger.ts +192 -78
  101. package/ts/capabilities/session/ctrip-city.ts +158 -0
  102. package/ts/capabilities/session/extension-bridge.ts +185 -67
  103. package/ts/capabilities/session/gui-flight.ts +1022 -0
  104. package/ts/capabilities/session/gui-hotel.ts +505 -0
  105. package/ts/capabilities/session-search.ts +130 -18
  106. package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  107. package/ts/src/booking-surface/dsh-planner.ts +169 -19
  108. package/ts/src/booking-surface/index.ts +1 -0
  109. package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
  110. package/ts/src/booking-surface/server.ts +215 -84
  111. package/ts/src/booking-surface/startup.ts +155 -0
  112. package/ts/src/booking-surface/tenancy.ts +242 -0
  113. package/ts/src/gotry-web-api.ts +51 -2
  114. package/ts/src/index.ts +67 -19
  115. package/ts/src/llm-price-table.ts +180 -0
  116. package/ts/src/tenant-seam/index.ts +9 -0
  117. package/ts/src/tenant-seam/llm-broker.ts +520 -0
  118. package/ts/src/tenant-seam/process-governor.ts +343 -0
  119. package/ts/src/tenant-seam/quotas.ts +433 -0
  120. package/ts/src/tenant-seam/seam.ts +142 -0
  121. package/ts/src/tenant-state/durable-store.ts +171 -0
  122. package/ts/src/tenant-state/epoch-copies.ts +491 -0
  123. package/ts/src/tenant-state/errors.ts +128 -0
  124. package/ts/src/tenant-state/ids.ts +108 -0
  125. package/ts/src/tenant-state/index.ts +12 -0
  126. package/ts/src/tenant-state/lease-store.ts +266 -0
  127. package/ts/src/tenant-state/tenant-lease.ts +175 -0
  128. package/ts/src/tenant-state/tenant-router.ts +743 -0
@@ -17,11 +17,13 @@ import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure, needsE
17
17
  import { appendFileSync, mkdirSync } from 'node:fs'
18
18
  import { dirname } from 'node:path'
19
19
  import { randomUUID } from 'node:crypto'
20
- import { buildEntryUrl, NETWORK_HINTS, parseBatchSearchResult, LOGIN_COOKIE_NAMES, SITE_DOMAIN, type BatchSearchShape, type SessionFlightOption } from './session/adapters/ctrip-flight.ts'
21
- import { buildHotelEntryUrl, parseCtripHotelList, HOTEL_SITE_HOST, type SessionHotelOption } from './session/adapters/ctrip-hotel.ts'
20
+ import { resolveFlightEntryUrl, NETWORK_HINTS, parseBatchSearchResult, LOGIN_COOKIE_NAMES, SITE_DOMAIN, type BatchSearchShape, type SessionFlightOption } from './session/adapters/ctrip-flight.ts'
21
+ import { resolveHotelEntryUrl, classifyCtripHotelList, HOTEL_SITE_HOST, type SessionHotelOption } from './session/adapters/ctrip-hotel.ts'
22
22
  import { buildTrainEntryUrl, parseLeftTicketQueryResult, resolveTrainQueryTelecodes, validateTrainQueryResponseUrl, TRAIN_SITE_HOST, type SessionTrainOption, type TrainQueryParseOutcome, type TrainResponseBinding } from './session/adapters/rail-12306.ts'
23
23
  import { buildDidaEntryUrl, parseDidaRates, parseDidaSearchCache, parseDidaRecommendHotels, parseDidaPrices, DIDA_NETWORK_HINTS, DIDA_SITE_HOST, DIDA_SITE_DOMAIN, DIDA_LOGIN_COOKIE_NAMES, type SessionDidaRateOption } from './session/adapters/dida-portal.ts'
24
24
  import { EXTENSION_STORE_URL, type SessionJobHandle, type SniffBodies } from './session/extension-bridge.ts'
25
+ import { runFlightGuiSearch, type GuiCalendarDay } from './session/gui-flight.ts'
26
+ import { runHotelGuiSearch } from './session/gui-hotel.ts'
25
27
 
26
28
  export type SessionVerdict = 'hit' | 'miss' | 'error' | 'challenged' | 'cooldown' | 'needs-login' | 'needs-attach' | 'needs-extension'
27
29
  export type SessionFlightTransportShape = BatchSearchShape | 'sniff_timeout' | 'challenge_page' | 'precheck_unavailable' | 'needs_login' | 'search_job_unavailable'
@@ -38,7 +40,7 @@ function flightBridgeFailureSummary(kind: BridgeFailureKind): string {
38
40
 
39
41
  export interface SessionSearchResult {
40
42
  ok: boolean
41
- via: 'session-ctrip-flight' | 'session-ctrip-flight-error'
43
+ via: 'session-ctrip-flight' | 'session-ctrip-flight-error' | 'session-ctrip-flight-gui' | 'session-ctrip-flight-gui-error'
42
44
  evidence: string
43
45
  latencyMs: number
44
46
  verdict: SessionVerdict
@@ -49,13 +51,25 @@ export interface SessionSearchResult {
49
51
  /** needs-extension 时给出 Chrome Web Store URL —— dsh UI 应直接渲成可点链接(浏览器自己当安装器,gotry 不插手) */
50
52
  installUrl?: string
51
53
  installAction?: 'add-to-chrome'
54
+ /** 低价日历(GUI 车道匿名可用;#662:该航线哪些日期有票/低价) */
55
+ calendar?: GuiCalendarDay[]
56
+ /** GUI 检索后站点落到的 list 页真实 URL(#663 URL 教师) */
57
+ listUrl?: string
52
58
  }
53
59
 
54
60
  export interface SessionFlightQuery {
55
61
  from: string
56
62
  to: string
63
+ fromCityCode?: string
64
+ toCityCode?: string
57
65
  /** YYYY-MM-DD */
58
66
  date: string
67
+ /** 回程日期:存在即往返检索——bridge 车道不知往返 URL 形状(诚实边界,报错指路 GUI);GUI 车道原生支持 */
68
+ backDate?: string
69
+ /** 多程段(2–3 段;仅 ui=gui;存在时忽略顶层 from/to/date/backDate) */
70
+ legs?: Array<{ from: string; to: string; date: string }>
71
+ /** 登录态配对等待上限 ms(仅 ui=gui;needUserLogin/passport 命中时窗口停官方登录页等人) */
72
+ interactiveLoginMs?: number
59
73
  /** 隔离 profile 目录(测试必传;默认 /tmp 专用目录) */
60
74
  profileDir?: string
61
75
  headless?: boolean
@@ -139,10 +153,13 @@ export function appendExtensionAudit(auditPath: string | undefined, entry: { kin
139
153
  // ---------------------------------------------------------------------------
140
154
 
141
155
  export interface SessionHotelQuery {
156
+ /** 登录态配对等待上限 ms(仅 ui=gui;passport 命中时窗口停官方登录页等人) */
157
+ interactiveLoginMs?: number
142
158
  /** 目的地(中文);城市码表未收录时须带 cityId */
143
159
  to: string
144
160
  /** 显式城市 id(携程/trip.com 酒店 list 页 URL 的 city= 数字;覆盖码表) */
145
161
  cityId?: number | string
162
+ country?: string
146
163
  /** YYYY-MM-DD */
147
164
  checkIn?: string
148
165
  /** YYYY-MM-DD(与 checkIn 成对) */
@@ -161,11 +178,13 @@ export interface SessionHotelQuery {
161
178
 
162
179
  export interface SessionHotelResult {
163
180
  ok: boolean
164
- via: 'session-ctrip-hotel' | 'session-ctrip-hotel-error'
181
+ via: 'session-ctrip-hotel' | 'session-ctrip-hotel-error' | 'session-ctrip-hotel-gui' | 'session-ctrip-hotel-gui-error'
165
182
  evidence: string
166
183
  latencyMs: number
167
184
  verdict: SessionVerdict
168
185
  hotels?: SessionHotelOption[]
186
+ /** GUI 检索后站点落到的 list 页真实 URL */
187
+ listUrl?: string
169
188
  error?: string
170
189
  /** needs-extension 时给出 Chrome Web Store URL(dsh UI 渲成可点链接) */
171
190
  installUrl?: string
@@ -177,6 +196,41 @@ export function hotelCityUnresolvedHint(cities: string[]): string {
177
196
  return `城市 ${cities.join('/')} 不在携程酒店城市码表——先 web 搜「hotels.ctrip.com ${cities[0] ?? ''} 酒店」拿到 list 页 URL 里的 city= 数字,带 cityId 重试`
178
197
  }
179
198
 
199
+ /** 机票城市三字码未收录时的人话指引(纯函数,测试锚点;码表只是缓存,主链路=显式码逃生口) */
200
+ export function flightCityUnresolvedHint(cities: string[]): string {
201
+ return `城市 ${cities.join('/')} 不在携程机票三字码表(缓存)——web 搜「flights.ctrip.com ${cities[0] ?? ''} 机票」从 list 页 URL 取三字码(形如 oneway-hkg-dxb),带 fromCityCode/toCityCode 重试;或改走 gotry_web_search`
202
+ }
203
+
204
+ /**
205
+ * 携程酒店 GUI 车道(2026-10-14,#664 首片):gotry 隔离匿名窗口驱动 hotels.ctrip.com
206
+ * 表单(目的地=站点自动补全,cityId 码表退出主链路),结果复用 ctrip-hotel 三态分类器。
207
+ * 节律闸与 bridge 酒店道同站同桶('ctrip-hotel');匿名公开价,恒标 anonymous。
208
+ */
209
+ export async function sessionHotelGuiSearch(q: SessionHotelQuery): Promise<SessionHotelResult> {
210
+ const started = Date.now()
211
+ const ts = new Date().toISOString()
212
+ const err = (verdict: SessionVerdict, error: string): SessionHotelResult => ({
213
+ ok: false, via: 'session-ctrip-hotel-error', evidence: `[会话:ctrip-hotel-gui@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
214
+ })
215
+
216
+ const last = lastCallAt.get('ctrip-hotel') ?? 0
217
+ if (Date.now() - last < MIN_INTERVAL_MS) {
218
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
219
+ }
220
+ lastCallAt.set('ctrip-hotel', Date.now())
221
+
222
+ if (!q.checkIn || !q.checkOut) {
223
+ return err('error', 'GUI 车道需要 checkIn/checkOut(YYYY-MM-DD,成对)')
224
+ }
225
+ const r = await runHotelGuiSearch({ to: q.to, checkIn: q.checkIn, checkOut: q.checkOut, ...(q.interactiveLoginMs ? { interactiveLoginMs: q.interactiveLoginMs } : {}), ...(q.timeoutMs ? { timeoutMs: Math.max(q.timeoutMs, q.interactiveLoginMs ? q.interactiveLoginMs + 120_000 : 0) } : {}) })
226
+ return {
227
+ ok: r.ok, via: r.via, evidence: r.evidence, latencyMs: r.latencyMs, verdict: r.verdict,
228
+ ...(r.hotels ? { hotels: r.hotels } : {}),
229
+ ...(r.listUrl ? { listUrl: r.listUrl } : {}),
230
+ ...(r.error ? { error: r.error } : {}),
231
+ }
232
+ }
233
+
180
234
  export async function sessionHotelSearch(q: SessionHotelQuery): Promise<SessionHotelResult> {
181
235
  const started = Date.now()
182
236
  const ts = new Date().toISOString()
@@ -202,13 +256,14 @@ export async function sessionHotelSearch(q: SessionHotelQuery): Promise<SessionH
202
256
  if (Date.now() - last < MIN_INTERVAL_MS) {
203
257
  return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
204
258
  }
205
- lastCallAt.set(site, Date.now())
206
-
207
- const entry = buildHotelEntryUrl({ to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults })
259
+ const entry = await resolveHotelEntryUrl({ to: q.to, country: q.country, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults })
208
260
  if (!entry.ok || !entry.url) {
209
- return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [q.to]))
261
+ return err('error', entry.error ?? hotelCityUnresolvedHint(entry.unresolved ?? [q.to]))
210
262
  }
211
263
 
264
+ const resolvedLast = lastCallAt.get(site) ?? 0
265
+ if (Date.now() - resolvedLast < MIN_INTERVAL_MS) return err('cooldown', `rate limit: min ${MIN_INTERVAL_MS}ms`)
266
+ lastCallAt.set(site, Date.now())
212
267
  const mode = resolveTransportMode(q.profileDir)
213
268
 
214
269
  if (mode === 'extension') {
@@ -239,10 +294,15 @@ export async function sessionHotelSearch(q: SessionHotelQuery): Promise<SessionH
239
294
  }
240
295
  const title = r.title
241
296
  const head = r.body.slice(0, 5000)
242
- if (CHALLENGE_RE.test(title + head)) {
297
+ if (r.challenge === true || CHALLENGE_RE.test(title + head)) {
243
298
  return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
244
299
  }
245
- const hotels = parseCtripHotelList(r.body)
300
+ if (r.timedOut || !r.body) return err('error', '酒店页嗅探超时,未取得报价;本次不能判断无房,请换用其他酒店通道')
301
+ const parsed = classifyCtripHotelList(r.body)
302
+ if (parsed.shape === 'malformed' || parsed.shape === 'unrecognized') {
303
+ return err('error', `酒店回包无法识别(${parsed.shape}),库存查询未完成;本次不能判断无房`)
304
+ }
305
+ const hotels = parsed.hotels
246
306
  const verdict: SessionVerdict = hotels.length > 0 ? 'hit' : 'miss'
247
307
  return {
248
308
  ok: true,
@@ -293,7 +353,12 @@ export async function sessionHotelSearch(q: SessionHotelQuery): Promise<SessionH
293
353
  if (CHALLENGE_RE.test(title + headHtml)) {
294
354
  return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
295
355
  }
296
- const hotels = parseCtripHotelList(body)
356
+ if (!body) return err('error', '酒店页嗅探超时,未取得报价;本次不能判断无房,请换用其他酒店通道')
357
+ const parsed = classifyCtripHotelList(body)
358
+ if (parsed.shape === 'malformed' || parsed.shape === 'unrecognized') {
359
+ return err('error', `酒店回包无法识别(${parsed.shape}),库存查询未完成;本次不能判断无房`)
360
+ }
361
+ const hotels = parsed.hotels
297
362
  const verdict: SessionVerdict = hotels.length > 0 ? 'hit' : 'miss'
298
363
  return {
299
364
  ok: true,
@@ -351,6 +416,11 @@ export interface SessionDidaQuery {
351
416
  * 远程扩展经鉴权面连入;缺省走 loopback 懒单例(桌面形态)
352
417
  */
353
418
  bridge?: SessionJobHandle
419
+ /**
420
+ * 进程内节律闸的键(AaaS A2):缺省 = 站点,单车道行为不变。多组织形态的挂载模块传 `<组织>/<站点>`,
421
+ * 一个组织的检索不给另一个组织定节律,另一个组织也读不到「上一次是多久前」。
422
+ */
423
+ pacingKey?: string
354
424
  }
355
425
 
356
426
  export interface SessionDidaResult {
@@ -406,12 +476,13 @@ export async function sessionDidaSearch(q: SessionDidaQuery): Promise<SessionDid
406
476
  ok: false, via: 'session-dida-portal-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
407
477
  })
408
478
 
409
- // 节律闸:超间隔即拒,不发起导航
410
- const last = lastCallAt.get(site) ?? 0
479
+ // 节律闸:超间隔即拒,不发起导航。键缺省是站点;多组织形态按组织分键(见 SessionDidaQuery.pacingKey)
480
+ const paceKey = q.pacingKey ?? site
481
+ const last = lastCallAt.get(paceKey) ?? 0
411
482
  if (Date.now() - last < MIN_INTERVAL_MS) {
412
483
  return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
413
484
  }
414
- lastCallAt.set(site, Date.now())
485
+ lastCallAt.set(paceKey, Date.now())
415
486
 
416
487
  const entry = buildDidaEntryUrl({ entryUrl: q.entryUrl })
417
488
  if (!entry.ok || !entry.url) {
@@ -757,13 +828,20 @@ export async function sessionFlightSearch(q: SessionFlightQuery): Promise<Sessio
757
828
  if (Date.now() - last < MIN_INTERVAL_MS) {
758
829
  return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
759
830
  }
760
- lastCallAt.set(site, Date.now())
761
-
762
- const entry = buildEntryUrl(q.from, q.to, q.date)
831
+ // 往返(2026-10-14 解锁,#663):URL 形状由 GUI 车道教师实测定稿——
832
+ // round-{code}0-{code}0?depdate=去_回&cabin=y_s(curl 200+往返 title 实证)。
833
+ if (q.backDate && !(q.date < q.backDate)) {
834
+ return err('error', `回程日期(${q.backDate})须晚于去程(${q.date})`)
835
+ }
836
+ const entry = await resolveFlightEntryUrl(q.from, q.to, q.date, { fromCode: q.fromCityCode, toCode: q.toCityCode }, undefined, q.backDate)
763
837
  if (!entry.ok || !entry.url) {
764
- return err('error', `unresolved entry: ${(entry.unresolved ?? []).join('/')} 不在城市码表`)
838
+ return err('error', entry.error ?? '城市解析失败,请确认城市或机场码;不能据此判断航线覆盖')
765
839
  }
766
840
 
841
+ // Resolution never consumes inventory pacing; recheck after asynchronous lookup.
842
+ const resolvedLast = lastCallAt.get(site) ?? 0
843
+ if (Date.now() - resolvedLast < MIN_INTERVAL_MS) return err('cooldown', `rate limit: min ${MIN_INTERVAL_MS}ms`)
844
+ lastCallAt.set(site, Date.now())
767
845
  const mode = resolveTransportMode(q.profileDir)
768
846
 
769
847
  if (mode === 'extension') {
@@ -891,3 +969,37 @@ export async function sessionFlightSearch(q: SessionFlightQuery): Promise<Sessio
891
969
  await t.close()
892
970
  }
893
971
  }
972
+
973
+ /**
974
+ * 携程机票 GUI 车道(2026-10-14,创始人指示):gotry 自己的隔离匿名 headful 窗口,
975
+ * 驱动 PC 检索表单(往返/任意城市原生支持——城市解析=站点自动补全,码表退居缓存),
976
+ * 结果从站点自己的 batchSearch 回包解析。节律闸与 bridge 同站同桶('ctrip-flight')。
977
+ * 写面红线:动作白名单+提交件黑名单+readback 硬闸(gui-flight.ts 契约)。
978
+ */
979
+ export async function sessionFlightGuiSearch(q: SessionFlightQuery): Promise<SessionSearchResult> {
980
+ const started = Date.now()
981
+ const ts = new Date().toISOString()
982
+ const err = (verdict: SessionVerdict, error: string): SessionSearchResult => ({
983
+ ok: false, via: 'session-ctrip-flight-error', evidence: `[会话:ctrip-flight-gui@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
984
+ })
985
+
986
+ const last = lastCallAt.get('ctrip-flight') ?? 0
987
+ if (Date.now() - last < MIN_INTERVAL_MS) {
988
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
989
+ }
990
+ lastCallAt.set('ctrip-flight', Date.now())
991
+
992
+ if (q.backDate && !(q.date < q.backDate)) {
993
+ return err('error', `回程日期(${q.backDate})须晚于去程(${q.date})`)
994
+ }
995
+
996
+ const r = await runFlightGuiSearch({ from: q.from, to: q.to, date: q.date, ...(q.backDate ? { backDate: q.backDate } : {}), ...(q.legs ? { legs: q.legs } : {}), ...(q.interactiveLoginMs ? { interactiveLoginMs: q.interactiveLoginMs } : {}), ...(q.timeoutMs ? { timeoutMs: Math.max(q.timeoutMs, q.interactiveLoginMs ? q.interactiveLoginMs + 120_000 : 0) } : {}) })
997
+ return {
998
+ ok: r.ok, via: r.via, evidence: r.evidence, latencyMs: r.latencyMs,
999
+ verdict: r.verdict,
1000
+ ...(r.options ? { options: r.options } : {}),
1001
+ ...(r.calendar && r.calendar.length > 0 ? { calendar: r.calendar } : {}),
1002
+ ...(r.listUrl ? { listUrl: r.listUrl } : {}),
1003
+ ...(r.error ? { error: r.error } : {}),
1004
+ }
1005
+ }