@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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +63 -53
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/client/client.js +66 -21
- package/dist/capabilities/doctor.js +7 -8
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/bridge-ledger.js +168 -67
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +148 -47
- package/dist/capabilities/session/gui-flight.js +949 -0
- package/dist/capabilities/session/gui-hotel.js +470 -0
- package/dist/capabilities/session-search.js +102 -14
- 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/bootstrap-tests.js +23 -6
- package/dist/scripts/bridge-ledger-tests.js +20 -18
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +19 -4
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +5 -0
- package/dist/scripts/gotry-web-client-tests.js +20 -1
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +1 -1
- package/dist/scripts/mutation-check.js +153 -0
- package/dist/scripts/nightly-evidence.js +4 -111
- package/dist/scripts/session-tests.js +211 -6
- 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/gotry-web-api.js +64 -2
- package/dist/src/index.js +70 -18
- 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 +5 -1
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/bridge-ledger.ts +192 -78
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +185 -67
- package/ts/capabilities/session/gui-flight.ts +1022 -0
- package/ts/capabilities/session/gui-hotel.ts +505 -0
- package/ts/capabilities/session-search.ts +130 -18
- 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/gotry-web-api.ts +51 -2
- package/ts/src/index.ts +67 -19
- 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
|
@@ -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 {
|
|
21
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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',
|
|
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
|
+
}
|