@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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 与机票 GUI 车道(gui-flight.ts)同构:动作闭集 + 提交件黑名单 + readback 硬闸 +
|
|
7
7
|
* 可注入 driver;复用 ctrip-hotel 适配器的三态分类器(classifyCtripHotelList:
|
|
8
8
|
* parsed/recognized-empty/unrecognized——只有显式空列表才算 miss,绝不猜)。
|
|
9
|
-
* 证据链 [会话:ctrip-hotel-gui@ts]
|
|
9
|
+
* 证据链 [会话:ctrip-hotel-gui@ts];anonymous 标注(登录态配对后为 false)。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
@@ -19,6 +19,8 @@ import { HOTEL_NETWORK_HINTS, looksLikeHotelListBody, classifyCtripHotelList, ty
|
|
|
19
19
|
export const HOTEL_GUI_HOME = 'https://hotels.ctrip.com/'
|
|
20
20
|
const YMD = /^\d{4}-\d{2}-\d{2}$/
|
|
21
21
|
const GUI_CHALLENGE_RE = /验证|校验|拖动滑块|请输入验证码/
|
|
22
|
+
/** 登录票据 cookie 名单(与 bridge 车道 LOGIN_COOKIE_NAMES 同源;只读名字) */
|
|
23
|
+
const TICKET_COOKIE_NAMES = ['cticket', 'uid', 'uname', 'passport']
|
|
22
24
|
|
|
23
25
|
export interface GuiHotelQuery {
|
|
24
26
|
/** 目的地中文(站点自动补全解析,码表不在主链路) */
|
|
@@ -27,6 +29,9 @@ export interface GuiHotelQuery {
|
|
|
27
29
|
checkIn: string
|
|
28
30
|
/** 退房 YYYY-MM-DD(须晚于入住) */
|
|
29
31
|
checkOut: string
|
|
32
|
+
/** 登录态配对(2026-10-15,#664 余尾):匿名列表强制跳 passport 时,窗口停在官方登录页
|
|
33
|
+
* 等用户本人完成登录(cookie 只读名字),随后原窗重驾拿登录态真实价。 */
|
|
34
|
+
interactiveLoginMs?: number
|
|
30
35
|
timeoutMs?: number
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -102,6 +107,8 @@ export interface GuiHotelPage {
|
|
|
102
107
|
pageText(): Promise<string>
|
|
103
108
|
/** 当前页 URL(list 页真实形状回传) */
|
|
104
109
|
currentUrl(): Promise<string>
|
|
110
|
+
/** 登录态配对:窗口停在 passport 登录页等人完成登录(cookie 只读名字);超时 false */
|
|
111
|
+
waitForHumanLogin(timeoutMs: number): Promise<boolean>
|
|
105
112
|
waitForApiBody(timeoutMs: number): Promise<{ url: string; body: string } | null>
|
|
106
113
|
recentApiUrls?(): Promise<string[]>
|
|
107
114
|
}
|
|
@@ -121,7 +128,7 @@ export interface GuiHotelResult {
|
|
|
121
128
|
verdict: GuiHotelVerdict
|
|
122
129
|
hotels?: SessionHotelOption[]
|
|
123
130
|
error?: string
|
|
124
|
-
anonymous:
|
|
131
|
+
anonymous: boolean
|
|
125
132
|
/** 检索后站点落到的 list 页 URL(真实形状回传) */
|
|
126
133
|
listUrl?: string
|
|
127
134
|
}
|
|
@@ -134,10 +141,13 @@ export function __setHotelDriverForTest(factory: (() => GuiHotelDriver) | null):
|
|
|
134
141
|
export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResult> {
|
|
135
142
|
const started = Date.now()
|
|
136
143
|
const ts = new Date().toISOString()
|
|
144
|
+
let humanLoggedIn = false
|
|
137
145
|
const fail = (error: string, verdict: GuiHotelVerdict = 'error'): GuiHotelResult => ({
|
|
138
146
|
ok: false, via: 'session-ctrip-hotel-gui-error', latencyMs: Date.now() - started, verdict, error,
|
|
139
|
-
evidence: `[会话:ctrip-hotel-gui@error@${ts}] ${error.slice(0, 120)}`, anonymous:
|
|
147
|
+
evidence: `[会话:ctrip-hotel-gui@error@${ts}] ${error.slice(0, 120)}`, anonymous: !humanLoggedIn,
|
|
140
148
|
})
|
|
149
|
+
const needsLoginMessage = (): string =>
|
|
150
|
+
'酒店列表需登录态:匿名窗口被重定向到携程登录页(红线:不自动登录)——interactiveLogin=true 可在窗口内本人登录后自动重查,或 gotry_session_login 后走 bridge 车道'
|
|
141
151
|
|
|
142
152
|
if (!(q.checkIn < q.checkOut)) return fail(`退房日期(${q.checkOut})须晚于入住(${q.checkIn})`)
|
|
143
153
|
|
|
@@ -157,10 +167,11 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
157
167
|
|
|
158
168
|
let api: { url: string; body: string } | null = null
|
|
159
169
|
let listUrl = ''
|
|
160
|
-
|
|
170
|
+
class HotelFlowAbort { constructor(public result: GuiHotelResult) {} }
|
|
171
|
+
const driveOnce = async (): Promise<void> => {
|
|
161
172
|
for (const a of plan) {
|
|
162
173
|
const remain = budget - (Date.now() - started)
|
|
163
|
-
if (remain <= 0)
|
|
174
|
+
if (remain <= 0) throw new HotelFlowAbort(fail(`GUI 预算耗尽(${budget}ms)`))
|
|
164
175
|
const step = <T,>(p: Promise<T>, what: string): Promise<T> =>
|
|
165
176
|
withDeadline(p, Math.min(remain, 20_000), `GUI 步骤失败(${what})`)
|
|
166
177
|
switch (a.t) {
|
|
@@ -176,7 +187,7 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
176
187
|
{
|
|
177
188
|
const after = await page.currentUrl().catch(() => '')
|
|
178
189
|
if (/passport\.ctrip\.com\/.*login/i.test(after)) {
|
|
179
|
-
|
|
190
|
+
throw new HotelFlowAbort(fail('__NEEDS_LOGIN__', 'needs-login'))
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
break
|
|
@@ -184,7 +195,7 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
184
195
|
const dest = await step(page.destFieldValue(), '读回目的地框')
|
|
185
196
|
const form = await step(page.visibleFormText(), '读回表单')
|
|
186
197
|
if (!dest.includes(a.expect.to)) {
|
|
187
|
-
|
|
198
|
+
throw new HotelFlowAbort(fail(`readback 硬闸:目的地框未确认「${a.expect.to}」(实际「${dest.slice(0, 30)}」)——已终止,未发起搜索`))
|
|
188
199
|
}
|
|
189
200
|
// live 校准:酒店表单日期显示为本地化格式(如 11月13日(周五))而非 YMD——双格式核对
|
|
190
201
|
const dateShown = (ymd: string): boolean => {
|
|
@@ -193,7 +204,7 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
193
204
|
}
|
|
194
205
|
for (const [label, value] of [['入住日期', a.expect.checkIn], ['退房日期', a.expect.checkOut]] as const) {
|
|
195
206
|
if (!dateShown(value)) {
|
|
196
|
-
|
|
207
|
+
throw new HotelFlowAbort(fail(`readback 硬闸:${label}「${value}」未在表单确认(表单摘要:${form.slice(0, 200)})——已终止,未发起搜索`))
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
210
|
break
|
|
@@ -201,32 +212,65 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
201
212
|
case 'awaitApi': {
|
|
202
213
|
const text = await step(page.pageText(), '检查页面')
|
|
203
214
|
if (GUI_CHALLENGE_RE.test(text)) {
|
|
204
|
-
|
|
215
|
+
throw new HotelFlowAbort(fail('风控/验证码命中;按红线不重试不绕过,交还用户', 'challenged'))
|
|
205
216
|
}
|
|
206
217
|
api = await step(page.waitForApiBody(Math.min(remain - 500, 18_000)), '等待酒店列表回包')
|
|
207
218
|
if (!api) {
|
|
208
219
|
const recent = page.recentApiUrls ? (await page.recentApiUrls().catch(() => [])) : []
|
|
209
220
|
const nowUrl = await page.currentUrl().catch(() => '?')
|
|
210
221
|
if (/passport\.ctrip\.com\/.*login/i.test(nowUrl)) {
|
|
211
|
-
|
|
222
|
+
throw new HotelFlowAbort(fail('__NEEDS_LOGIN__', 'needs-login'))
|
|
212
223
|
}
|
|
213
|
-
|
|
224
|
+
throw new HotelFlowAbort(fail(`酒店列表回包嗅探超时(href=${nowUrl.slice(0, 120)})${recent.length ? ` ‖recent-apis: ${recent.slice(-8).join(' ; ').slice(0, 500)}` : ''}`))
|
|
214
225
|
}
|
|
215
226
|
break
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
}
|
|
219
|
-
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await driveOnce()
|
|
220
235
|
} catch (e) {
|
|
221
|
-
|
|
236
|
+
// 登录态配对(#664 余尾,2026-10-15):needs-login 且调用方授权等待时,窗口停在官方
|
|
237
|
+
// 登录页等用户本人完成登录(cookie 只读名字),随后原窗重驾一次拿登录态真实价。
|
|
238
|
+
if (e instanceof HotelFlowAbort && e.result.verdict === 'needs-login' && q.interactiveLoginMs && q.interactiveLoginMs > 0) {
|
|
239
|
+
const loggedIn = await withDeadline(page.waitForHumanLogin(q.interactiveLoginMs), q.interactiveLoginMs + 5_000, '等待人工登录').catch(() => false)
|
|
240
|
+
if (!loggedIn) {
|
|
241
|
+
return fail(`登录等待超时(${q.interactiveLoginMs}ms):用户未在 GUI 窗口完成登录——可稍后带 interactiveLogin 重试,或登录后走 bridge 车道`, 'needs-login')
|
|
242
|
+
}
|
|
243
|
+
humanLoggedIn = true
|
|
244
|
+
api = null
|
|
245
|
+
try {
|
|
246
|
+
await driveOnce()
|
|
247
|
+
} catch (e2) {
|
|
248
|
+
if (e2 instanceof HotelFlowAbort) {
|
|
249
|
+
return e2.result.error === '__NEEDS_LOGIN__'
|
|
250
|
+
? fail(needsLoginMessage(), 'needs-login')
|
|
251
|
+
: e2.result
|
|
252
|
+
}
|
|
253
|
+
return fail((e2 as Error).message.slice(0, 400))
|
|
254
|
+
}
|
|
255
|
+
} else if (e instanceof HotelFlowAbort) {
|
|
256
|
+
return e.result.error === '__NEEDS_LOGIN__'
|
|
257
|
+
? fail(needsLoginMessage(), 'needs-login')
|
|
258
|
+
: e.result
|
|
259
|
+
} else {
|
|
260
|
+
return fail((e as Error).message.slice(0, 400))
|
|
261
|
+
}
|
|
222
262
|
} finally {
|
|
223
263
|
await driver.close().catch(() => {})
|
|
224
264
|
}
|
|
265
|
+
try {
|
|
266
|
+
listUrl = await page.currentUrl().catch(() => '')
|
|
267
|
+
} catch { /* 已关窗口 */ }
|
|
225
268
|
|
|
226
|
-
|
|
227
|
-
|
|
269
|
+
const captured = api as { url: string; body: string } | null
|
|
270
|
+
if (captured === null) return fail('内部不变量失败:api 未结算(awaitApi 已过)')
|
|
271
|
+
const classified = classifyCtripHotelList(captured.body)
|
|
228
272
|
if (classified.shape === 'unrecognized') {
|
|
229
|
-
return fail(`酒店列表回包形状未识别(body头:${
|
|
273
|
+
return fail(`酒店列表回包形状未识别(body头:${captured.body.replace(/\s+/g, ' ').slice(0, 240)})`)
|
|
230
274
|
}
|
|
231
275
|
if (classified.shape === 'malformed') {
|
|
232
276
|
return fail('酒店列表回包非 JSON(接口改版?)')
|
|
@@ -237,8 +281,8 @@ export async function runHotelGuiSearch(q: GuiHotelQuery): Promise<GuiHotelResul
|
|
|
237
281
|
ok: verdict === 'hit', via: 'session-ctrip-hotel-gui', latencyMs: Date.now() - started, verdict,
|
|
238
282
|
...(hotels.length > 0 ? { hotels: hotels.slice(0, 20) } : {}),
|
|
239
283
|
...(listUrl ? { listUrl } : {}),
|
|
240
|
-
evidence: `[会话:ctrip-hotel-gui@${ts}]
|
|
241
|
-
anonymous:
|
|
284
|
+
evidence: `[会话:ctrip-hotel-gui@${ts}] GUI 面检索:${hotels.length} 家(shape=${classified.shape};${humanLoggedIn ? '用户本人已在本窗口官方页登录,登录态真实价' : '匿名公开价,非登录态价'})`,
|
|
285
|
+
anonymous: !humanLoggedIn,
|
|
242
286
|
}
|
|
243
287
|
}
|
|
244
288
|
|
|
@@ -431,6 +475,16 @@ async function createHotelPlaywrightDriver(): Promise<GuiHotelDriver> {
|
|
|
431
475
|
return title + text
|
|
432
476
|
},
|
|
433
477
|
async currentUrl() { return page.url() },
|
|
478
|
+
async waitForHumanLogin(timeoutMs: number) {
|
|
479
|
+
// 红线:人自己完成登录;只轮询票据 cookie 名字(值即弃)。窗口此刻通常已在 passport 页。
|
|
480
|
+
const deadline = Date.now() + timeoutMs
|
|
481
|
+
while (Date.now() < deadline) {
|
|
482
|
+
const names = await ctx.cookies().then(cs => cs.map(c => c.name)).catch(() => [] as string[])
|
|
483
|
+
if (names.some(n => TICKET_COOKIE_NAMES.includes(n))) return true
|
|
484
|
+
await page.waitForTimeout(5_000)
|
|
485
|
+
}
|
|
486
|
+
return false
|
|
487
|
+
},
|
|
434
488
|
async recentApiUrls() { return [...recentUrls] },
|
|
435
489
|
async waitForApiBody(timeoutMs) {
|
|
436
490
|
return new Promise(resolve => {
|
|
@@ -66,6 +66,10 @@ export interface SessionFlightQuery {
|
|
|
66
66
|
date: string
|
|
67
67
|
/** 回程日期:存在即往返检索——bridge 车道不知往返 URL 形状(诚实边界,报错指路 GUI);GUI 车道原生支持 */
|
|
68
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
|
|
69
73
|
/** 隔离 profile 目录(测试必传;默认 /tmp 专用目录) */
|
|
70
74
|
profileDir?: string
|
|
71
75
|
headless?: boolean
|
|
@@ -149,6 +153,8 @@ export function appendExtensionAudit(auditPath: string | undefined, entry: { kin
|
|
|
149
153
|
// ---------------------------------------------------------------------------
|
|
150
154
|
|
|
151
155
|
export interface SessionHotelQuery {
|
|
156
|
+
/** 登录态配对等待上限 ms(仅 ui=gui;passport 命中时窗口停官方登录页等人) */
|
|
157
|
+
interactiveLoginMs?: number
|
|
152
158
|
/** 目的地(中文);城市码表未收录时须带 cityId */
|
|
153
159
|
to: string
|
|
154
160
|
/** 显式城市 id(携程/trip.com 酒店 list 页 URL 的 city= 数字;覆盖码表) */
|
|
@@ -216,7 +222,7 @@ export async function sessionHotelGuiSearch(q: SessionHotelQuery): Promise<Sessi
|
|
|
216
222
|
if (!q.checkIn || !q.checkOut) {
|
|
217
223
|
return err('error', 'GUI 车道需要 checkIn/checkOut(YYYY-MM-DD,成对)')
|
|
218
224
|
}
|
|
219
|
-
const r = await runHotelGuiSearch({ to: q.to, checkIn: q.checkIn, checkOut: q.checkOut, ...(q.timeoutMs ? { timeoutMs: q.timeoutMs } : {}) })
|
|
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) } : {}) })
|
|
220
226
|
return {
|
|
221
227
|
ok: r.ok, via: r.via, evidence: r.evidence, latencyMs: r.latencyMs, verdict: r.verdict,
|
|
222
228
|
...(r.hotels ? { hotels: r.hotels } : {}),
|
|
@@ -410,6 +416,11 @@ export interface SessionDidaQuery {
|
|
|
410
416
|
* 远程扩展经鉴权面连入;缺省走 loopback 懒单例(桌面形态)
|
|
411
417
|
*/
|
|
412
418
|
bridge?: SessionJobHandle
|
|
419
|
+
/**
|
|
420
|
+
* 进程内节律闸的键(AaaS A2):缺省 = 站点,单车道行为不变。多组织形态的挂载模块传 `<组织>/<站点>`,
|
|
421
|
+
* 一个组织的检索不给另一个组织定节律,另一个组织也读不到「上一次是多久前」。
|
|
422
|
+
*/
|
|
423
|
+
pacingKey?: string
|
|
413
424
|
}
|
|
414
425
|
|
|
415
426
|
export interface SessionDidaResult {
|
|
@@ -465,12 +476,13 @@ export async function sessionDidaSearch(q: SessionDidaQuery): Promise<SessionDid
|
|
|
465
476
|
ok: false, via: 'session-dida-portal-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
|
|
466
477
|
})
|
|
467
478
|
|
|
468
|
-
//
|
|
469
|
-
const
|
|
479
|
+
// 节律闸:超间隔即拒,不发起导航。键缺省是站点;多组织形态按组织分键(见 SessionDidaQuery.pacingKey)
|
|
480
|
+
const paceKey = q.pacingKey ?? site
|
|
481
|
+
const last = lastCallAt.get(paceKey) ?? 0
|
|
470
482
|
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
471
483
|
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
|
|
472
484
|
}
|
|
473
|
-
lastCallAt.set(
|
|
485
|
+
lastCallAt.set(paceKey, Date.now())
|
|
474
486
|
|
|
475
487
|
const entry = buildDidaEntryUrl({ entryUrl: q.entryUrl })
|
|
476
488
|
if (!entry.ok || !entry.url) {
|
|
@@ -981,7 +993,7 @@ export async function sessionFlightGuiSearch(q: SessionFlightQuery): Promise<Ses
|
|
|
981
993
|
return err('error', `回程日期(${q.backDate})须晚于去程(${q.date})`)
|
|
982
994
|
}
|
|
983
995
|
|
|
984
|
-
const r = await runFlightGuiSearch({ from: q.from, to: q.to, date: q.date, ...(q.backDate ? { backDate: q.backDate } : {}), ...(q.timeoutMs ? { timeoutMs: q.timeoutMs } : {}) })
|
|
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) } : {}) })
|
|
985
997
|
return {
|
|
986
998
|
ok: r.ok, via: r.via, evidence: r.evidence, latencyMs: r.latencyMs,
|
|
987
999
|
verdict: r.verdict,
|