@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20
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 +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +309 -30
- package/bin/gotry-inner.js +74 -23
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +22 -8
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +307 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/hbcli.js +28 -2
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/anything-tests.js +70 -21
- package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
- package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +113 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +175 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/hbcli-tests.js +38 -1
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +39 -16
- package/dist/src/benchmark-environment-bridge.js +316 -52
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +739 -150
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +7 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/hbcli.ts +53 -4
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/dsh-runtime/vendor/README.md +60 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
- package/ts/package.json +9 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +54 -15
- package/ts/src/benchmark-environment-bridge.ts +234 -46
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +492 -172
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +365 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -3,6 +3,8 @@ import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from
|
|
|
3
3
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
|
|
6
|
+
import { buildHotelEntryUrl, parseCtripHotelList, HOTEL_SITE_HOST } from './session/adapters/ctrip-hotel.js';
|
|
7
|
+
import { buildTrainEntryUrl, parseLeftTicketQuery, TRAIN_SITE_HOST } from './session/adapters/rail-12306.js';
|
|
6
8
|
import { EXTENSION_STORE_URL } from './session/extension-bridge.js';
|
|
7
9
|
const MIN_INTERVAL_MS = 30_000;
|
|
8
10
|
const lastCallAt = new Map();
|
|
@@ -48,6 +50,315 @@ export function appendExtensionAudit(auditPath, entry) {
|
|
|
48
50
|
appendFileSync(auditPath, JSON.stringify(record) + '\n');
|
|
49
51
|
} catch {}
|
|
50
52
|
}
|
|
53
|
+
export function hotelCityUnresolvedHint(cities) {
|
|
54
|
+
return `城市 ${cities.join('/')} 不在携程酒店城市码表——先 web 搜「hotels.ctrip.com ${cities[0] ?? ''} 酒店」拿到 list 页 URL 里的 city= 数字,带 cityId 重试`;
|
|
55
|
+
}
|
|
56
|
+
export async function sessionHotelSearch(q) {
|
|
57
|
+
const started = Date.now();
|
|
58
|
+
const ts = new Date().toISOString();
|
|
59
|
+
const site = 'ctrip-hotel';
|
|
60
|
+
const err = (verdict, error)=>({
|
|
61
|
+
ok: false,
|
|
62
|
+
via: 'session-ctrip-hotel-error',
|
|
63
|
+
evidence: `[会话:${site}@error@${ts}] ${error}`,
|
|
64
|
+
latencyMs: Date.now() - started,
|
|
65
|
+
verdict,
|
|
66
|
+
error
|
|
67
|
+
});
|
|
68
|
+
if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn)) {
|
|
69
|
+
return err('error', 'checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不带日期)');
|
|
70
|
+
}
|
|
71
|
+
if (q.checkIn) {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
74
|
+
if (q.checkIn < today || (q.checkOut ?? '') < q.checkIn) {
|
|
75
|
+
return err('error', `入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${today})——向用户确认日期后再查`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const last = lastCallAt.get(site) ?? 0;
|
|
79
|
+
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
80
|
+
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
81
|
+
}
|
|
82
|
+
lastCallAt.set(site, Date.now());
|
|
83
|
+
const entry = buildHotelEntryUrl({
|
|
84
|
+
to: q.to,
|
|
85
|
+
cityId: q.cityId,
|
|
86
|
+
checkIn: q.checkIn,
|
|
87
|
+
checkOut: q.checkOut,
|
|
88
|
+
adults: q.adults
|
|
89
|
+
});
|
|
90
|
+
if (!entry.ok || !entry.url) {
|
|
91
|
+
return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [
|
|
92
|
+
q.to
|
|
93
|
+
]));
|
|
94
|
+
}
|
|
95
|
+
const mode = resolveTransportMode(q.profileDir);
|
|
96
|
+
if (mode === 'extension') {
|
|
97
|
+
const login = await extensionCookieNames({
|
|
98
|
+
site,
|
|
99
|
+
domain: SITE_DOMAIN.replace(/^\./, ''),
|
|
100
|
+
ticketNames: LOGIN_COOKIE_NAMES
|
|
101
|
+
});
|
|
102
|
+
if (!login.ok) {
|
|
103
|
+
const verdict = classifyBridgeFailure(login.kind);
|
|
104
|
+
if (verdict === 'needs-extension') {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
via: 'session-ctrip-hotel-error',
|
|
108
|
+
evidence: '[会话:ctrip-hotel-needs-extension@ts]',
|
|
109
|
+
latencyMs: Date.now() - started,
|
|
110
|
+
verdict,
|
|
111
|
+
error: login.summary,
|
|
112
|
+
installUrl: EXTENSION_STORE_URL,
|
|
113
|
+
installAction: 'add-to-chrome'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return err(verdict, login.summary);
|
|
117
|
+
}
|
|
118
|
+
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
119
|
+
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
120
|
+
}
|
|
121
|
+
const r = await extensionSearchJob({
|
|
122
|
+
site,
|
|
123
|
+
url: entry.url,
|
|
124
|
+
timeoutMs: q.timeoutMs
|
|
125
|
+
});
|
|
126
|
+
appendExtensionAudit(q.auditPath, {
|
|
127
|
+
kind: 'extension-session-job',
|
|
128
|
+
site,
|
|
129
|
+
url: entry.url,
|
|
130
|
+
jobId: 'search',
|
|
131
|
+
result: r.ok ? r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"` : `${r.kind}:${r.summary.slice(0, 120)}`
|
|
132
|
+
});
|
|
133
|
+
if (!r.ok) {
|
|
134
|
+
const verdict = classifyBridgeFailure(r.kind);
|
|
135
|
+
if (verdict === 'needs-extension') {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
via: 'session-ctrip-hotel-error',
|
|
139
|
+
evidence: '[会话:ctrip-hotel-needs-extension@ts]',
|
|
140
|
+
latencyMs: Date.now() - started,
|
|
141
|
+
verdict,
|
|
142
|
+
error: r.summary,
|
|
143
|
+
installUrl: EXTENSION_STORE_URL,
|
|
144
|
+
installAction: 'add-to-chrome'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return err(verdict, r.summary);
|
|
148
|
+
}
|
|
149
|
+
const title = r.title;
|
|
150
|
+
const head = r.body.slice(0, 5000);
|
|
151
|
+
if (CHALLENGE_RE.test(title + head)) {
|
|
152
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
153
|
+
}
|
|
154
|
+
const hotels = parseCtripHotelList(r.body);
|
|
155
|
+
const verdict = hotels.length > 0 ? 'hit' : 'miss';
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
via: 'session-ctrip-hotel',
|
|
159
|
+
evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
160
|
+
latencyMs: Date.now() - started,
|
|
161
|
+
verdict,
|
|
162
|
+
hotels
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const t = await openSession({
|
|
166
|
+
profileDir: q.profileDir,
|
|
167
|
+
headless: q.headless,
|
|
168
|
+
auditPath: q.auditPath,
|
|
169
|
+
mode: mode === 'persistent' ? 'persistent' : 'cdp',
|
|
170
|
+
newPage: true
|
|
171
|
+
});
|
|
172
|
+
if (!t.ok) {
|
|
173
|
+
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const loggedIn = async ()=>{
|
|
177
|
+
const cookies = await t.browser.cookies().catch(()=>[]);
|
|
178
|
+
return cookies.some((c)=>c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
|
|
179
|
+
};
|
|
180
|
+
if (!await loggedIn() && !q.allowAnonymous) {
|
|
181
|
+
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
182
|
+
}
|
|
183
|
+
let settled = false;
|
|
184
|
+
let body = '';
|
|
185
|
+
const heard = new Promise((resolve)=>{
|
|
186
|
+
t.page.on('response', async (res)=>{
|
|
187
|
+
if (settled) return;
|
|
188
|
+
const u = res.url();
|
|
189
|
+
const hostHit = u.includes(HOTEL_SITE_HOST);
|
|
190
|
+
if (!hostHit) return;
|
|
191
|
+
try {
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
if (text && (hostHit && /hotels\.ctrip\.com\/(hotels\/api|domestic\/pc\/api)|GetHotelListBySOA|GetHotelListByCity|HotelSearch|hotelsearch/i.test(u) || text.length <= 2_000_000 && /"hotelList"|"hotelMatchInfos"|"hotelName"/.test(text))) {
|
|
194
|
+
body = text;
|
|
195
|
+
settled = true;
|
|
196
|
+
resolve();
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
});
|
|
200
|
+
setTimeout(()=>resolve(), q.timeoutMs ?? 30_000);
|
|
201
|
+
});
|
|
202
|
+
await t.page.goto(entry.url, {
|
|
203
|
+
waitUntil: 'domcontentloaded',
|
|
204
|
+
timeout: 30_000
|
|
205
|
+
});
|
|
206
|
+
await heard;
|
|
207
|
+
const title = await t.page.title().catch(()=>'');
|
|
208
|
+
const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
|
|
209
|
+
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
210
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
211
|
+
}
|
|
212
|
+
const hotels = parseCtripHotelList(body);
|
|
213
|
+
const verdict = hotels.length > 0 ? 'hit' : 'miss';
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
via: 'session-ctrip-hotel',
|
|
217
|
+
evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
218
|
+
latencyMs: Date.now() - started,
|
|
219
|
+
verdict,
|
|
220
|
+
hotels
|
|
221
|
+
};
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
224
|
+
} finally{
|
|
225
|
+
await t.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export function trainStationUnresolvedHint(cities) {
|
|
229
|
+
return `城市 ${cities.join('/')} 不在 12306 城市电报码表——web 搜「12306 ${cities[0] ?? ''} 余票」拿到 kyfw 查询页 URL 里的 fs=城市,XXX 三位码,带 fromStationTelecode/toStationTelecode 重试`;
|
|
230
|
+
}
|
|
231
|
+
export async function sessionTrainSearch(q) {
|
|
232
|
+
const started = Date.now();
|
|
233
|
+
const ts = new Date().toISOString();
|
|
234
|
+
const site = 'train-12306';
|
|
235
|
+
const err = (verdict, error)=>({
|
|
236
|
+
ok: false,
|
|
237
|
+
via: 'session-train-12306-error',
|
|
238
|
+
evidence: `[会话:${site}@error@${ts}] ${error}`,
|
|
239
|
+
latencyMs: Date.now() - started,
|
|
240
|
+
verdict,
|
|
241
|
+
error
|
|
242
|
+
});
|
|
243
|
+
const last = lastCallAt.get(site) ?? 0;
|
|
244
|
+
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
245
|
+
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
246
|
+
}
|
|
247
|
+
lastCallAt.set(site, Date.now());
|
|
248
|
+
const entry = buildTrainEntryUrl({
|
|
249
|
+
from: q.from,
|
|
250
|
+
to: q.to,
|
|
251
|
+
date: q.date,
|
|
252
|
+
fromStationTelecode: q.fromStationTelecode,
|
|
253
|
+
toStationTelecode: q.toStationTelecode
|
|
254
|
+
});
|
|
255
|
+
if (!entry.ok || !entry.url) {
|
|
256
|
+
return err('error', trainStationUnresolvedHint(entry.unresolved ?? [
|
|
257
|
+
q.from,
|
|
258
|
+
q.to
|
|
259
|
+
]));
|
|
260
|
+
}
|
|
261
|
+
const mode = resolveTransportMode(q.profileDir);
|
|
262
|
+
if (mode === 'extension') {
|
|
263
|
+
const r = await extensionSearchJob({
|
|
264
|
+
site,
|
|
265
|
+
url: entry.url,
|
|
266
|
+
timeoutMs: q.timeoutMs
|
|
267
|
+
});
|
|
268
|
+
appendExtensionAudit(q.auditPath, {
|
|
269
|
+
kind: 'extension-session-job',
|
|
270
|
+
site,
|
|
271
|
+
url: entry.url,
|
|
272
|
+
jobId: 'search',
|
|
273
|
+
result: r.ok ? r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"` : `${r.kind}:${r.summary.slice(0, 120)}`
|
|
274
|
+
});
|
|
275
|
+
if (!r.ok) {
|
|
276
|
+
const verdict = classifyBridgeFailure(r.kind);
|
|
277
|
+
if (verdict === 'needs-extension') {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
via: 'session-train-12306-error',
|
|
281
|
+
evidence: '[会话:train-12306-needs-extension@ts]',
|
|
282
|
+
latencyMs: Date.now() - started,
|
|
283
|
+
verdict,
|
|
284
|
+
error: r.summary,
|
|
285
|
+
installUrl: EXTENSION_STORE_URL,
|
|
286
|
+
installAction: 'add-to-chrome'
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return err(verdict, r.summary);
|
|
290
|
+
}
|
|
291
|
+
const title = r.title;
|
|
292
|
+
const head = r.body.slice(0, 5000);
|
|
293
|
+
if (CHALLENGE_RE.test(title + head)) {
|
|
294
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
295
|
+
}
|
|
296
|
+
const trains = parseLeftTicketQuery(r.body, entry.url);
|
|
297
|
+
const verdict = trains.length > 0 ? 'hit' : 'miss';
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
via: 'session-train-12306',
|
|
301
|
+
evidence: `[会话:${site}@${ts}] ${trains.length} trains;transport=extension(公开查询面,被动嗅探,零系统弹窗;扩展零写行为=物理只读)`,
|
|
302
|
+
latencyMs: Date.now() - started,
|
|
303
|
+
verdict,
|
|
304
|
+
trains
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const t = await openSession({
|
|
308
|
+
profileDir: q.profileDir,
|
|
309
|
+
headless: q.headless,
|
|
310
|
+
auditPath: q.auditPath,
|
|
311
|
+
mode: mode === 'persistent' ? 'persistent' : 'cdp',
|
|
312
|
+
newPage: true
|
|
313
|
+
});
|
|
314
|
+
if (!t.ok) {
|
|
315
|
+
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
let settled = false;
|
|
319
|
+
let body = '';
|
|
320
|
+
const heard = new Promise((resolve)=>{
|
|
321
|
+
t.page.on('response', async (res)=>{
|
|
322
|
+
if (settled) return;
|
|
323
|
+
const u = res.url();
|
|
324
|
+
if (!/leftTicket\/query/i.test(u)) return;
|
|
325
|
+
try {
|
|
326
|
+
const text = await res.text();
|
|
327
|
+
if (text) {
|
|
328
|
+
body = text;
|
|
329
|
+
settled = true;
|
|
330
|
+
resolve();
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
});
|
|
334
|
+
setTimeout(()=>resolve(), q.timeoutMs ?? 25_000);
|
|
335
|
+
});
|
|
336
|
+
await t.page.goto(entry.url, {
|
|
337
|
+
waitUntil: 'domcontentloaded',
|
|
338
|
+
timeout: 30_000
|
|
339
|
+
});
|
|
340
|
+
await heard;
|
|
341
|
+
const title = await t.page.title().catch(()=>'');
|
|
342
|
+
const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
|
|
343
|
+
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
344
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
345
|
+
}
|
|
346
|
+
const trains = parseLeftTicketQuery(body, entry.url);
|
|
347
|
+
const verdict = trains.length > 0 ? 'hit' : 'miss';
|
|
348
|
+
return {
|
|
349
|
+
ok: true,
|
|
350
|
+
via: 'session-train-12306',
|
|
351
|
+
evidence: `[会话:${site}@${ts}] ${trains.length} trains(公开查询面);guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}`,
|
|
352
|
+
latencyMs: Date.now() - started,
|
|
353
|
+
verdict,
|
|
354
|
+
trains
|
|
355
|
+
};
|
|
356
|
+
} catch (e) {
|
|
357
|
+
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
358
|
+
} finally{
|
|
359
|
+
await t.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
51
362
|
export async function sessionFlightSearch(q) {
|
|
52
363
|
const started = Date.now();
|
|
53
364
|
const ts = new Date().toISOString();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { BOOKABLE_FACT_SCHEMA, makeFactId } from '../src/bookable-facts.js';
|
|
3
|
+
const SOURCE_HOST = 'https://cs.mfa.gov.cn';
|
|
4
|
+
const SOURCE_ID = 'cs-mfa';
|
|
5
|
+
const USER_AGENT = 'GoTry/0.0.1 (+https://github.com/Danceiny/gotry; polite policy reader)';
|
|
6
|
+
const CONTINENTS = [
|
|
7
|
+
'yz_645708',
|
|
8
|
+
'feizhou_645714',
|
|
9
|
+
'ouzhou_645711',
|
|
10
|
+
'bz_645718',
|
|
11
|
+
'dyz_645717'
|
|
12
|
+
];
|
|
13
|
+
export function countryPageUrl(continent, country) {
|
|
14
|
+
if (!/^[a-z0-9_]+$/.test(continent) || !/^[a-z0-9_]+$/.test(country)) return null;
|
|
15
|
+
return `${SOURCE_HOST}/zggmcg/ljmdd/${continent}/${country}/`;
|
|
16
|
+
}
|
|
17
|
+
function stripTags(html) {
|
|
18
|
+
return html.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ');
|
|
19
|
+
}
|
|
20
|
+
export function extractVisaSection(pageText) {
|
|
21
|
+
const text = stripTags(pageText);
|
|
22
|
+
const start = text.indexOf('签证入境');
|
|
23
|
+
if (start < 0) return null;
|
|
24
|
+
const rest = text.slice(start + '签证入境'.length);
|
|
25
|
+
const sectionEnds = [
|
|
26
|
+
'海关检查',
|
|
27
|
+
'海关申报',
|
|
28
|
+
'医疗条件',
|
|
29
|
+
'交通出行',
|
|
30
|
+
'实用信息',
|
|
31
|
+
'领事保护',
|
|
32
|
+
'国家安全',
|
|
33
|
+
'特别提醒'
|
|
34
|
+
].map((kw)=>rest.indexOf(kw)).filter((i)=>i > 0);
|
|
35
|
+
const numHeaders = Array.from(rest.matchAll(/[一二三四五六七八九十]+、(?!免签|落地签|签证|入境规定)/g)).map((m)=>m.index).filter((i)=>i > 0);
|
|
36
|
+
const allEnds = [
|
|
37
|
+
...sectionEnds,
|
|
38
|
+
...numHeaders
|
|
39
|
+
].sort((a, b)=>a - b);
|
|
40
|
+
const end = allEnds.length > 0 ? allEnds[0] : Math.min(rest.length, 4_000);
|
|
41
|
+
const section = rest.slice(0, end).trim();
|
|
42
|
+
return section.length > 20 ? section : null;
|
|
43
|
+
}
|
|
44
|
+
export function splitPolicyEntries(section) {
|
|
45
|
+
const parts = section.split(/(?:[一二三四五六七八九十]+、|[((]\s*[一二三四五六七八九十]+\s*[))])\s*/).map((p)=>p.trim());
|
|
46
|
+
return parts.filter((p)=>p.length > 30);
|
|
47
|
+
}
|
|
48
|
+
export function policyFactsFromMfa(input) {
|
|
49
|
+
const entries = splitPolicyEntries(input.section);
|
|
50
|
+
const asOf = input.fetchedAt.slice(0, 10);
|
|
51
|
+
const queryId = `visa-policy:${SOURCE_ID}:${input.countryPath}:${asOf}`;
|
|
52
|
+
return entries.map((entry, i)=>({
|
|
53
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
54
|
+
fact_id: makeFactId([
|
|
55
|
+
input.countryPath,
|
|
56
|
+
String(i),
|
|
57
|
+
entry.slice(0, 64),
|
|
58
|
+
queryId
|
|
59
|
+
]),
|
|
60
|
+
kind: 'policy',
|
|
61
|
+
subject: `${input.countryLabel} 签证入境(条款 ${i + 1})`,
|
|
62
|
+
statement: entry,
|
|
63
|
+
source: `${SOURCE_ID}@${input.fetchedAt}`,
|
|
64
|
+
query_id: queryId,
|
|
65
|
+
fetched_at: input.fetchedAt,
|
|
66
|
+
as_of: asOf,
|
|
67
|
+
review_by: defaultReviewBy(input.fetchedAt)
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
export function defaultReviewBy(fetchedAt) {
|
|
71
|
+
const d = new Date(fetchedAt);
|
|
72
|
+
d.setDate(d.getDate() + 30);
|
|
73
|
+
return d.toISOString().slice(0, 10);
|
|
74
|
+
}
|
|
75
|
+
export function listCountryPaths(continentListHtml, continent) {
|
|
76
|
+
const hits = continentListHtml.matchAll(new RegExp(`href="(\\./([a-z0-9_]+)/)"[^>]*>([^<]{2,20})<`, 'g'));
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const m of hits){
|
|
79
|
+
if (!m[2] || !m[3]) continue;
|
|
80
|
+
out.push({
|
|
81
|
+
path: m[2],
|
|
82
|
+
label: m[3].trim()
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out.map((x)=>x.path).filter((p)=>/^[a-z0-9_]+$/.test(p));
|
|
86
|
+
}
|
|
87
|
+
export function continentListUrl(continent) {
|
|
88
|
+
if (!/^[a-z0-9_]+$/.test(continent)) return null;
|
|
89
|
+
return `${SOURCE_HOST}/zggmcg/ljmdd/${continent}/`;
|
|
90
|
+
}
|
|
91
|
+
export function readPageFixture(path) {
|
|
92
|
+
return readFileSync(path, 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
export async function fetchVisaPolicyPage(url, timeoutMs = 20_000) {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(url, {
|
|
99
|
+
headers: {
|
|
100
|
+
'user-agent': USER_AGENT,
|
|
101
|
+
'accept-language': 'zh-CN,zh'
|
|
102
|
+
},
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
redirect: 'follow'
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: `HTTP ${res.status}`
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
html: await res.text()
|
|
113
|
+
};
|
|
114
|
+
} catch (e) {
|
|
115
|
+
const msg = controller.signal.aborted ? `timeout after ${timeoutMs}ms` : String(e.message ?? e);
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: msg
|
|
119
|
+
};
|
|
120
|
+
} finally{
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
//# sourceURL=ts/capabilities/visa-policy.ts
|