@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,27 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话检索编排层(RFC §3.2):transport → adapter entry → networkHint 嗅探 → 解析 → ReadGuard → 证据链。
|
|
3
|
+
*
|
|
4
|
+
* 传输车道(2026-08-29 定案,RFC §2.2):**扩展桥为默认**——一次性安装的 MV3 扩展
|
|
5
|
+
* (GoTry Session Bridge)在自己标签页被动嗅探 batchSearch,零 Chrome 系统弹窗;
|
|
6
|
+
* cdp(attach 日常 Chrome,Chrome 144+ 每连接弹权限框)降为显式后备
|
|
7
|
+
* (`GOTRY_SESSION_TRANSPORT=cdp` opt-in,诊断/测试用);persistent 仅测试。
|
|
8
|
+
*
|
|
9
|
+
* 证据链(L4 增补):[会话:ctrip-flight@ts] / [会话:ctrip-hotel@ts] = 用户本人会话内实时检索,非官方 API;
|
|
10
|
+
* 风控命中(verdict='challenged')= degraded,绝不重试、绝不绕过(合规支柱②)。
|
|
11
|
+
* 节律(§3.4):同站点 ≥30s 间隔 + 单调冷却;超间隔返回 verdict='cooldown'。
|
|
12
|
+
* 永不抛错;扩展车道 fail-closed(桥/扩展不可用即 verdict,零花费);测试/巡检用隔离 profile 与 stateRoot。
|
|
13
|
+
*/
|
|
1
14
|
import { openSession } from './session/transport.js';
|
|
2
15
|
import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from './session/extension-channel.js';
|
|
3
16
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
4
17
|
import { dirname } from 'node:path';
|
|
5
|
-
import {
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { buildEntryUrl, NETWORK_HINTS, parseBatchSearchResult, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
|
|
6
20
|
import { buildHotelEntryUrl, parseCtripHotelList, HOTEL_SITE_HOST } from './session/adapters/ctrip-hotel.js';
|
|
7
|
-
import { buildTrainEntryUrl,
|
|
21
|
+
import { buildTrainEntryUrl, parseLeftTicketQueryResult, resolveTrainQueryTelecodes, validateTrainQueryResponseUrl } from './session/adapters/rail-12306.js';
|
|
22
|
+
import { buildDidaEntryUrl, parseDidaRates, parseDidaRecommendHotels, parseDidaPrices, DIDA_NETWORK_HINTS, DIDA_SITE_DOMAIN, DIDA_LOGIN_COOKIE_NAMES } from './session/adapters/dida-portal.js';
|
|
8
23
|
import { EXTENSION_STORE_URL } from './session/extension-bridge.js';
|
|
24
|
+
/** 节律闸(§3.4):同站点最小间隔;导出仅测试用 */
|
|
9
25
|
const MIN_INTERVAL_MS = 30_000;
|
|
10
26
|
const lastCallAt = new Map();
|
|
11
27
|
export function __resetRateLimiterForTest() {
|
|
12
28
|
lastCallAt.clear();
|
|
13
29
|
}
|
|
30
|
+
let trainSessionTransportForTest;
|
|
31
|
+
/** Isolated CDP collector seam; production uses openSession and never installs this. */
|
|
32
|
+
export function __setTrainSessionTransportForTest(factory) {
|
|
33
|
+
trainSessionTransportForTest = factory ?? undefined;
|
|
34
|
+
}
|
|
14
35
|
const CHALLENGE_RE = /验证|滑块|captcha|verify/i;
|
|
36
|
+
/** 把 transport 文案漂移收敛为稳定的产品 verdict;纯函数供回归覆盖。 */
|
|
15
37
|
export function classifyTransportFailure(summary, cdpMode) {
|
|
16
|
-
return cdpMode && /日常 Chrome 未开调试端口|cdp attach 失败/.test(summary)
|
|
38
|
+
return cdpMode && /日常 Chrome 未开调试端口|cdp attach 失败/.test(summary)
|
|
39
|
+
? 'needs-attach'
|
|
40
|
+
: 'error';
|
|
17
41
|
}
|
|
42
|
+
/** 传输车道解析(纯函数,测试锚点):persistent=隔离 profile(测试自检);cdp=显式 opt-in;扩展=默认 */
|
|
18
43
|
export function resolveTransportMode(profileDir) {
|
|
19
|
-
if (profileDir !== undefined)
|
|
44
|
+
if (profileDir !== undefined)
|
|
45
|
+
return 'persistent';
|
|
20
46
|
return (process.env.GOTRY_SESSION_TRANSPORT ?? '').trim().toLowerCase() === 'cdp' ? 'cdp' : 'extension';
|
|
21
47
|
}
|
|
48
|
+
/** Live browser transport is an explicit opt-in for smoke/full-suite callers. */
|
|
22
49
|
export function sessionLiveEnabled(env = process.env) {
|
|
23
50
|
return env.GOTRY_SESSION_LIVE === '1';
|
|
24
51
|
}
|
|
52
|
+
/** Deterministic offline observation used when the live session gate is closed. */
|
|
25
53
|
export function offlineSessionFlightResult() {
|
|
26
54
|
return {
|
|
27
55
|
ok: false,
|
|
@@ -29,27 +57,27 @@ export function offlineSessionFlightResult() {
|
|
|
29
57
|
evidence: '[会话:ctrip-flight@offline] GOTRY_SESSION_LIVE!=1; live transport not invoked',
|
|
30
58
|
latencyMs: 0,
|
|
31
59
|
verdict: 'error',
|
|
32
|
-
error: 'GOTRY_SESSION_LIVE=1 required for live session transport'
|
|
60
|
+
error: 'GOTRY_SESSION_LIVE=1 required for live session transport',
|
|
33
61
|
};
|
|
34
62
|
}
|
|
63
|
+
/** Gate a caller before it can invoke the browser/session transport. */
|
|
35
64
|
export async function gatedSessionFlightSearch(q, search = sessionFlightSearch, env = process.env) {
|
|
36
|
-
if (!sessionLiveEnabled(env))
|
|
65
|
+
if (!sessionLiveEnabled(env))
|
|
66
|
+
return offlineSessionFlightResult();
|
|
37
67
|
return search(q);
|
|
38
68
|
}
|
|
69
|
+
/** 扩展车道 job 审计(ReadGuard 审计同款 JSONL,kind 区分;auditPath 缺省不落盘) */
|
|
39
70
|
export function appendExtensionAudit(auditPath, entry) {
|
|
40
|
-
if (!auditPath)
|
|
71
|
+
if (!auditPath)
|
|
72
|
+
return;
|
|
41
73
|
try {
|
|
42
|
-
const record = {
|
|
43
|
-
|
|
44
|
-
...entry,
|
|
45
|
-
url: entry.url.slice(0, 400)
|
|
46
|
-
};
|
|
47
|
-
mkdirSync(dirname(auditPath), {
|
|
48
|
-
recursive: true
|
|
49
|
-
});
|
|
74
|
+
const record = { ts: new Date().toISOString(), ...entry, url: entry.url.slice(0, 400) };
|
|
75
|
+
mkdirSync(dirname(auditPath), { recursive: true });
|
|
50
76
|
appendFileSync(auditPath, JSON.stringify(record) + '\n');
|
|
51
|
-
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* 审计失败不阻塞检索 */ }
|
|
52
79
|
}
|
|
80
|
+
/** 酒店城市码表未收录时的人话指引(纯函数,测试锚点) */
|
|
53
81
|
export function hotelCityUnresolvedHint(cities) {
|
|
54
82
|
return `城市 ${cities.join('/')} 不在携程酒店城市码表——先 web 搜「hotels.ctrip.com ${cities[0] ?? ''} 酒店」拿到 list 页 URL 里的 city= 数字,带 cityId 重试`;
|
|
55
83
|
}
|
|
@@ -57,15 +85,11 @@ export async function sessionHotelSearch(q) {
|
|
|
57
85
|
const started = Date.now();
|
|
58
86
|
const ts = new Date().toISOString();
|
|
59
87
|
const site = 'ctrip-hotel';
|
|
60
|
-
const err = (verdict, error)=>({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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)) {
|
|
88
|
+
const err = (verdict, error) => ({
|
|
89
|
+
ok: false, via: 'session-ctrip-hotel-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
|
|
90
|
+
});
|
|
91
|
+
// 日期对闸(与 flyai hotel 同口径):成对且 YYYY-MM-DD;过去日期不发上游
|
|
92
|
+
if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || (q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn))) {
|
|
69
93
|
return err('error', 'checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不带日期)');
|
|
70
94
|
}
|
|
71
95
|
if (q.checkIn) {
|
|
@@ -75,74 +99,40 @@ export async function sessionHotelSearch(q) {
|
|
|
75
99
|
return err('error', `入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${today})——向用户确认日期后再查`);
|
|
76
100
|
}
|
|
77
101
|
}
|
|
102
|
+
// 节律闸:超间隔即拒,不发起导航
|
|
78
103
|
const last = lastCallAt.get(site) ?? 0;
|
|
79
104
|
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
80
105
|
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
81
106
|
}
|
|
82
107
|
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
|
-
});
|
|
108
|
+
const entry = buildHotelEntryUrl({ to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults });
|
|
90
109
|
if (!entry.ok || !entry.url) {
|
|
91
|
-
return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [
|
|
92
|
-
q.to
|
|
93
|
-
]));
|
|
110
|
+
return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [q.to]));
|
|
94
111
|
}
|
|
95
112
|
const mode = resolveTransportMode(q.profileDir);
|
|
96
113
|
if (mode === 'extension') {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
domain: SITE_DOMAIN.replace(/^\./, ''),
|
|
100
|
-
ticketNames: LOGIN_COOKIE_NAMES
|
|
101
|
-
});
|
|
114
|
+
// ① 登录态快查(与机票同账号体系,票据 cookie 名一致)
|
|
115
|
+
const login = await extensionCookieNames({ site, domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES });
|
|
102
116
|
if (!login.ok) {
|
|
103
117
|
const verdict = classifyBridgeFailure(login.kind);
|
|
104
118
|
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
|
-
};
|
|
119
|
+
return { ok: false, via: 'session-ctrip-hotel-error', evidence: '[会话:ctrip-hotel-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: login.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
115
120
|
}
|
|
116
121
|
return err(verdict, login.summary);
|
|
117
122
|
}
|
|
118
123
|
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
119
124
|
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
120
125
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
url: entry.url,
|
|
124
|
-
timeoutMs: q.timeoutMs
|
|
125
|
-
});
|
|
126
|
+
// ② 检索 job:后台标签 + 被动嗅探(URL hint + 形状兜底;扩展零写行为)
|
|
127
|
+
const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs });
|
|
126
128
|
appendExtensionAudit(q.auditPath, {
|
|
127
|
-
kind: 'extension-session-job',
|
|
128
|
-
|
|
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)}`
|
|
129
|
+
kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
|
|
130
|
+
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
131
|
});
|
|
133
132
|
if (!r.ok) {
|
|
134
133
|
const verdict = classifyBridgeFailure(r.kind);
|
|
135
134
|
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
|
-
};
|
|
135
|
+
return { ok: false, via: 'session-ctrip-hotel-error', evidence: '[会话:ctrip-hotel-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
146
136
|
}
|
|
147
137
|
return err(verdict, r.summary);
|
|
148
138
|
}
|
|
@@ -159,35 +149,32 @@ export async function sessionHotelSearch(q) {
|
|
|
159
149
|
evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
160
150
|
latencyMs: Date.now() - started,
|
|
161
151
|
verdict,
|
|
162
|
-
hotels
|
|
152
|
+
hotels,
|
|
163
153
|
};
|
|
164
154
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
headless: q.headless,
|
|
168
|
-
auditPath: q.auditPath,
|
|
169
|
-
mode: mode === 'persistent' ? 'persistent' : 'cdp',
|
|
170
|
-
newPage: true
|
|
171
|
-
});
|
|
155
|
+
// cdp/persistent 车道:与机票同构——挂监听等 NETWORK hint 回包(酒店用 HOTEL hints + 形状兜底)
|
|
156
|
+
const t = await openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true });
|
|
172
157
|
if (!t.ok) {
|
|
173
158
|
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
174
159
|
}
|
|
175
160
|
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));
|
|
161
|
+
const loggedIn = async () => {
|
|
162
|
+
const cookies = await t.browser.cookies().catch(() => []);
|
|
163
|
+
return cookies.some((c) => c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
|
|
179
164
|
};
|
|
180
|
-
if (!await loggedIn() && !q.allowAnonymous) {
|
|
165
|
+
if (!(await loggedIn()) && !q.allowAnonymous) {
|
|
181
166
|
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
182
167
|
}
|
|
183
168
|
let settled = false;
|
|
184
169
|
let body = '';
|
|
185
|
-
const heard = new Promise((resolve)=>{
|
|
186
|
-
t.page.on('response', async (res)=>{
|
|
187
|
-
if (settled)
|
|
170
|
+
const heard = new Promise((resolve) => {
|
|
171
|
+
t.page.on('response', async (res) => {
|
|
172
|
+
if (settled)
|
|
173
|
+
return;
|
|
188
174
|
const u = res.url();
|
|
189
175
|
const hostHit = u.includes(HOTEL_SITE_HOST);
|
|
190
|
-
if (!hostHit)
|
|
176
|
+
if (!hostHit)
|
|
177
|
+
return;
|
|
191
178
|
try {
|
|
192
179
|
const text = await res.text();
|
|
193
180
|
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))) {
|
|
@@ -195,17 +182,15 @@ export async function sessionHotelSearch(q) {
|
|
|
195
182
|
settled = true;
|
|
196
183
|
resolve();
|
|
197
184
|
}
|
|
198
|
-
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* 流式/竞态不可读则继续等下一个 */ }
|
|
199
187
|
});
|
|
200
|
-
setTimeout(()=>resolve(), q.timeoutMs ?? 30_000);
|
|
201
|
-
});
|
|
202
|
-
await t.page.goto(entry.url, {
|
|
203
|
-
waitUntil: 'domcontentloaded',
|
|
204
|
-
timeout: 30_000
|
|
188
|
+
setTimeout(() => resolve(), q.timeoutMs ?? 30_000);
|
|
205
189
|
});
|
|
190
|
+
await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
206
191
|
await heard;
|
|
207
|
-
const title = await t.page.title().catch(()=>'');
|
|
208
|
-
const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
|
|
192
|
+
const title = await t.page.title().catch(() => '');
|
|
193
|
+
const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000);
|
|
209
194
|
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
210
195
|
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
211
196
|
}
|
|
@@ -217,145 +202,320 @@ export async function sessionHotelSearch(q) {
|
|
|
217
202
|
evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
218
203
|
latencyMs: Date.now() - started,
|
|
219
204
|
verdict,
|
|
220
|
-
hotels
|
|
205
|
+
hotels,
|
|
221
206
|
};
|
|
222
|
-
}
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
223
209
|
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
224
|
-
}
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
225
212
|
await t.close();
|
|
226
213
|
}
|
|
227
214
|
}
|
|
228
|
-
|
|
229
|
-
|
|
215
|
+
/** 员工侧登录指引(纯函数,测试锚点):账密永不经 gotry——酒店 platform
|
|
216
|
+
* 凭据在 hotel-be portal「自动登录」跳板完成,dida 侧人只补验证码 */
|
|
217
|
+
export function didaLoginHint() {
|
|
218
|
+
return '未检出你的 dida 登录态——在 hotel-be portal「供应商门户」页对该凭据点「自动登录」完成跳板登录(账密自动填充,你只补验证码);或调用 gotry_session_login(site=dida-portal)打开 dida 登录入口。登录在 dida 官网完成;gotry 永不经手密码/验证码/cookie 值';
|
|
230
219
|
}
|
|
231
|
-
|
|
220
|
+
/** dida 三类回包合并解析(推荐酒店 + 推荐价格 + 实时价;CDP 窗口收集与扩展 multiCollect 双车道同一合并语义) */
|
|
221
|
+
function mergeDidaRatesBodies(bodies) {
|
|
222
|
+
const hotels = parseDidaRecommendHotels(bodies.hotels);
|
|
223
|
+
const recommendPrices = parseDidaPrices(bodies.recommendPrices);
|
|
224
|
+
const realtimeRates = parseDidaRates(bodies.realtime);
|
|
225
|
+
const pricesByHotel = new Map(recommendPrices.map((p) => [p.hotelId, p]));
|
|
226
|
+
const rates = [...realtimeRates];
|
|
227
|
+
for (const h of hotels) {
|
|
228
|
+
const p = pricesByHotel.get(h.hotelId);
|
|
229
|
+
rates.push({
|
|
230
|
+
hotelId: h.hotelId,
|
|
231
|
+
hotelName: h.name,
|
|
232
|
+
price: p?.price ?? 0,
|
|
233
|
+
...(p?.currency ? { currency: p.currency } : {}),
|
|
234
|
+
...(p?.priceDate ? { priceDate: p.priceDate } : {}),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return rates;
|
|
238
|
+
}
|
|
239
|
+
export async function sessionDidaSearch(q) {
|
|
232
240
|
const started = Date.now();
|
|
233
241
|
const ts = new Date().toISOString();
|
|
234
|
-
const site = '
|
|
235
|
-
const err = (verdict, error)=>({
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
latencyMs: Date.now() - started,
|
|
240
|
-
verdict,
|
|
241
|
-
error
|
|
242
|
-
});
|
|
242
|
+
const site = 'dida-portal';
|
|
243
|
+
const err = (verdict, error) => ({
|
|
244
|
+
ok: false, via: 'session-dida-portal-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
|
|
245
|
+
});
|
|
246
|
+
// 节律闸:超间隔即拒,不发起导航
|
|
243
247
|
const last = lastCallAt.get(site) ?? 0;
|
|
244
248
|
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
245
249
|
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
246
250
|
}
|
|
247
251
|
lastCallAt.set(site, Date.now());
|
|
248
|
-
const entry =
|
|
249
|
-
from: q.from,
|
|
250
|
-
to: q.to,
|
|
251
|
-
date: q.date,
|
|
252
|
-
fromStationTelecode: q.fromStationTelecode,
|
|
253
|
-
toStationTelecode: q.toStationTelecode
|
|
254
|
-
});
|
|
252
|
+
const entry = buildDidaEntryUrl({ entryUrl: q.entryUrl });
|
|
255
253
|
if (!entry.ok || !entry.url) {
|
|
256
|
-
return err('error',
|
|
257
|
-
q.from,
|
|
258
|
-
q.to
|
|
259
|
-
]));
|
|
254
|
+
return err('error', 'entry URL 必须落在 https://portal.dida.com/ 域内');
|
|
260
255
|
}
|
|
261
256
|
const mode = resolveTransportMode(q.profileDir);
|
|
262
257
|
if (mode === 'extension') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
258
|
+
// ① 登录态快查:票据 cookie 名存在性(名字级;协议面不存在值字段)。
|
|
259
|
+
// 20s 窗口:桥常在本调用前才拉起,扩展 SW 可能已按 MV3 30s 节律休眠——
|
|
260
|
+
// job 在桥上按 capability 排队等轮询者,窗口覆盖一次 alarm 唤醒周期。
|
|
261
|
+
const login = await extensionCookieNames({ site, domain: DIDA_SITE_DOMAIN, ticketNames: DIDA_LOGIN_COOKIE_NAMES, timeoutMs: 20_000 }, q.bridge);
|
|
262
|
+
if (!login.ok) {
|
|
263
|
+
const verdict = classifyBridgeFailure(login.kind);
|
|
264
|
+
if (verdict === 'needs-extension') {
|
|
265
|
+
return { ok: false, via: 'session-dida-portal-error', evidence: '[会话:dida-portal-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: login.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
266
|
+
}
|
|
267
|
+
return err(verdict, login.summary);
|
|
268
|
+
}
|
|
269
|
+
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
270
|
+
return err('needs-login', didaLoginHint());
|
|
271
|
+
}
|
|
272
|
+
// ② 检索 job:后台标签 + 被动嗅探(URL hint + 形状兜底;扩展零写行为)。
|
|
273
|
+
// multiCollect:find 页自发 hotels+recommendPrices 双流,单首包语义丢一半(2026-09-11)
|
|
274
|
+
const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs, multiCollect: true }, q.bridge);
|
|
268
275
|
appendExtensionAudit(q.auditPath, {
|
|
269
|
-
kind: 'extension-session-job',
|
|
270
|
-
|
|
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)}`
|
|
276
|
+
kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
|
|
277
|
+
result: r.ok ? (r.timedOut ? 'timeout' : r.bodies ? `bodies hotels=${r.bodies.hotels?.length ?? 0}B prices=${r.bodies.recommendPrices?.length ?? 0}B` : `body ${r.body.length}B title="${r.title.slice(0, 60)}"`) : `${r.kind}:${r.summary.slice(0, 120)}`,
|
|
274
278
|
});
|
|
275
279
|
if (!r.ok) {
|
|
276
280
|
const verdict = classifyBridgeFailure(r.kind);
|
|
277
281
|
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
|
-
};
|
|
282
|
+
return { ok: false, via: 'session-dida-portal-error', evidence: '[会话:dida-portal-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
288
283
|
}
|
|
289
284
|
return err(verdict, r.summary);
|
|
290
285
|
}
|
|
291
286
|
const title = r.title;
|
|
292
|
-
const head = r.body.slice(0, 5000);
|
|
287
|
+
const head = (r.bodies?.hotels ?? r.body).slice(0, 5000);
|
|
293
288
|
if (CHALLENGE_RE.test(title + head)) {
|
|
294
289
|
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
295
290
|
}
|
|
296
|
-
|
|
297
|
-
const
|
|
291
|
+
// multiCollect 分桶回包 → 与 CDP 车道同语义合并;旧扩展单首包 → 退化 parseDidaRates 兜底
|
|
292
|
+
const rates = r.bodies
|
|
293
|
+
? mergeDidaRatesBodies({ hotels: r.bodies.hotels ?? '', recommendPrices: r.bodies.recommendPrices ?? '', realtime: r.bodies.realtime ?? '' })
|
|
294
|
+
: parseDidaRates(r.body);
|
|
295
|
+
const verdict = rates.length > 0 ? 'hit' : 'miss';
|
|
298
296
|
return {
|
|
299
297
|
ok: true,
|
|
300
|
-
via: 'session-
|
|
301
|
-
evidence: `[会话:${site}@${ts}] ${
|
|
298
|
+
via: 'session-dida-portal',
|
|
299
|
+
evidence: `[会话:${site}@${ts}] ${rates.length} rates;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
300
|
+
latencyMs: Date.now() - started,
|
|
301
|
+
verdict,
|
|
302
|
+
rates,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// cdp/persistent 车道:与机/酒同构——挂监听等 DIDA hints 回包
|
|
306
|
+
const t = await openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true });
|
|
307
|
+
if (!t.ok) {
|
|
308
|
+
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const loggedIn = async () => {
|
|
312
|
+
const cookies = await t.browser.cookies().catch(() => []);
|
|
313
|
+
return cookies.some((c) => c.domain.includes(DIDA_SITE_DOMAIN) && DIDA_LOGIN_COOKIE_NAMES.includes(c.name));
|
|
314
|
+
};
|
|
315
|
+
if (!(await loggedIn()) && !q.allowAnonymous) {
|
|
316
|
+
return err('needs-login', didaLoginHint());
|
|
317
|
+
}
|
|
318
|
+
// 窗口式收集(2026-09-10 校准):find 页加载自发推荐流(酒店+价格两个接口)
|
|
319
|
+
// 与可能的实时价接口——按接口分类收 body,窗口结束统一合并解析。
|
|
320
|
+
const bodies = { hotels: '', recommendPrices: '', realtime: '' };
|
|
321
|
+
const collect = (text, url) => {
|
|
322
|
+
if (/SearchHomepageRecommendHotels/i.test(url))
|
|
323
|
+
bodies.hotels = text;
|
|
324
|
+
else if (/SearchHomepageRecommendPrices/i.test(url))
|
|
325
|
+
bodies.recommendPrices = text;
|
|
326
|
+
else if (/HotelPriceAPI\/SearchRealTime/i.test(url))
|
|
327
|
+
bodies.realtime = text;
|
|
328
|
+
};
|
|
329
|
+
const heard = new Promise((resolve) => {
|
|
330
|
+
t.page.on('response', async (res) => {
|
|
331
|
+
const u = res.url();
|
|
332
|
+
if (!DIDA_NETWORK_HINTS.some((re) => re.test(u)))
|
|
333
|
+
return;
|
|
334
|
+
try {
|
|
335
|
+
const text = await res.text();
|
|
336
|
+
if (text)
|
|
337
|
+
collect(text, u);
|
|
338
|
+
}
|
|
339
|
+
catch { /* 流式/竞态不可读则继续等下一个 */ }
|
|
340
|
+
});
|
|
341
|
+
setTimeout(resolve, q.timeoutMs ?? 30_000);
|
|
342
|
+
});
|
|
343
|
+
await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
344
|
+
await heard;
|
|
345
|
+
const title = await t.page.title().catch(() => '');
|
|
346
|
+
const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000);
|
|
347
|
+
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
348
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
349
|
+
}
|
|
350
|
+
const rates = mergeDidaRatesBodies(bodies);
|
|
351
|
+
// 有推荐酒店或有实时价即视为通道产出;纯无价酒店如实标注价格待询
|
|
352
|
+
const priced = rates.filter((r) => r.price > 0).length;
|
|
353
|
+
const verdict = rates.length > 0 ? 'hit' : 'miss';
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
via: 'session-dida-portal',
|
|
357
|
+
evidence: `[会话:${site}@${ts}] ${rates.length} hotels(${priced} priced);guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
302
358
|
latencyMs: Date.now() - started,
|
|
303
359
|
verdict,
|
|
304
|
-
|
|
360
|
+
rates,
|
|
305
361
|
};
|
|
306
362
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
363
|
+
catch (e) {
|
|
364
|
+
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
await t.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** 车站电报码表未收录时的人话指引(纯函数,测试锚点) */
|
|
371
|
+
export function trainStationUnresolvedHint(cities) {
|
|
372
|
+
return `城市 ${cities.join('/')} 不在 12306 城市电报码表——web 搜「12306 ${cities[0] ?? ''} 余票」拿到 kyfw 查询页 URL 里的 fs=城市,XXX 三位码,带 fromStationTelecode/toStationTelecode 重试`;
|
|
373
|
+
}
|
|
374
|
+
export async function sessionTrainSearch(q) {
|
|
375
|
+
const started = Date.now();
|
|
376
|
+
const ts = new Date().toISOString();
|
|
377
|
+
const site = 'train-12306';
|
|
378
|
+
const err = (verdict, error) => ({
|
|
379
|
+
ok: false, via: 'session-train-12306-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict,
|
|
380
|
+
outcome: { kind: 'transport-runtime-error', reason: error }, error,
|
|
313
381
|
});
|
|
382
|
+
const parsedResult = (body, responseUrl, evidence, guardSuffix = '') => {
|
|
383
|
+
const outcome = parseLeftTicketQueryResult(body, entry.url, { maxItems: 30 });
|
|
384
|
+
if (outcome.kind === 'malformed') {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
via: 'session-train-12306-error',
|
|
388
|
+
evidence: `${evidence} typed-parse=malformed`,
|
|
389
|
+
latencyMs: Date.now() - started,
|
|
390
|
+
verdict: 'error',
|
|
391
|
+
outcome,
|
|
392
|
+
error: `12306 leftTicket/query 响应未通过 typed contract:${outcome.reason}`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const request = resolveTrainQueryTelecodes({ from: q.from, to: q.to, date: q.date, fromStationTelecode: q.fromStationTelecode, toStationTelecode: q.toStationTelecode });
|
|
396
|
+
if (!request.ok) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
via: 'session-train-12306-error',
|
|
400
|
+
evidence: `${evidence} typed-response-binding=unresolved`,
|
|
401
|
+
latencyMs: Date.now() - started,
|
|
402
|
+
verdict: 'error',
|
|
403
|
+
outcome,
|
|
404
|
+
trains: outcome.trains,
|
|
405
|
+
error: `12306 查询请求电报码未解析:${request.unresolved.join(',')}`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const response = validateTrainQueryResponseUrl(responseUrl, request.telecodes);
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
via: 'session-train-12306-error',
|
|
413
|
+
evidence: `${evidence} typed-response-binding=invalid`,
|
|
414
|
+
latencyMs: Date.now() - started,
|
|
415
|
+
verdict: 'error',
|
|
416
|
+
outcome,
|
|
417
|
+
trains: outcome.trains,
|
|
418
|
+
error: `12306 leftTicket/query 响应未绑定本次请求:${response.reason}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const fetchedAt = new Date().toISOString();
|
|
422
|
+
const batchId = randomUUID();
|
|
423
|
+
const collection = {
|
|
424
|
+
requested: { from: q.from, to: q.to, date: q.date },
|
|
425
|
+
request: request.telecodes,
|
|
426
|
+
response: response.binding,
|
|
427
|
+
batchId,
|
|
428
|
+
queryId: `session:12306-train:${batchId}`,
|
|
429
|
+
fetchedAt,
|
|
430
|
+
};
|
|
431
|
+
const trains = outcome.trains;
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
via: 'session-train-12306',
|
|
435
|
+
evidence: `${evidence} ${guardSuffix}`.trim(),
|
|
436
|
+
latencyMs: Date.now() - started,
|
|
437
|
+
verdict: outcome.kind === 'recognized-nonempty' ? 'hit' : 'miss',
|
|
438
|
+
outcome,
|
|
439
|
+
collection,
|
|
440
|
+
trains,
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
// 节律闸:超间隔即拒,不发起导航
|
|
444
|
+
const last = lastCallAt.get(site) ?? 0;
|
|
445
|
+
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
446
|
+
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
447
|
+
}
|
|
448
|
+
lastCallAt.set(site, Date.now());
|
|
449
|
+
const entry = buildTrainEntryUrl({ from: q.from, to: q.to, date: q.date, fromStationTelecode: q.fromStationTelecode, toStationTelecode: q.toStationTelecode });
|
|
450
|
+
if (!entry.ok || !entry.url) {
|
|
451
|
+
return err('error', trainStationUnresolvedHint(entry.unresolved ?? [q.from, q.to]));
|
|
452
|
+
}
|
|
453
|
+
const mode = resolveTransportMode(q.profileDir);
|
|
454
|
+
if (mode === 'extension') {
|
|
455
|
+
// 无登录闸(公开查询面):直接发起检索 job
|
|
456
|
+
const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs });
|
|
457
|
+
appendExtensionAudit(q.auditPath, {
|
|
458
|
+
kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
|
|
459
|
+
result: r.ok ? (r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"`) : `${r.kind}:${r.summary.slice(0, 120)}`,
|
|
460
|
+
});
|
|
461
|
+
if (!r.ok) {
|
|
462
|
+
const verdict = classifyBridgeFailure(r.kind);
|
|
463
|
+
if (verdict === 'needs-extension') {
|
|
464
|
+
return { ok: false, via: 'session-train-12306-error', evidence: '[会话:train-12306-needs-extension@ts]', latencyMs: Date.now() - started, verdict, outcome: { kind: 'transport-runtime-error', reason: r.summary }, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
465
|
+
}
|
|
466
|
+
return err(verdict, r.summary);
|
|
467
|
+
}
|
|
468
|
+
const title = r.title;
|
|
469
|
+
const head = r.body.slice(0, 5000);
|
|
470
|
+
if (CHALLENGE_RE.test(title + head)) {
|
|
471
|
+
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
472
|
+
}
|
|
473
|
+
return parsedResult(r.body, r.url, `[会话:${site}@${ts}] transport=extension(公开查询面,被动嗅探,零系统弹窗;扩展零写行为=物理只读)`);
|
|
474
|
+
}
|
|
475
|
+
// cdp/persistent 车道:与机/酒同构
|
|
476
|
+
const t = await (trainSessionTransportForTest
|
|
477
|
+
? trainSessionTransportForTest()
|
|
478
|
+
: openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true }));
|
|
314
479
|
if (!t.ok) {
|
|
315
480
|
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
316
481
|
}
|
|
317
482
|
try {
|
|
318
483
|
let settled = false;
|
|
319
484
|
let body = '';
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
485
|
+
let responseUrl = '';
|
|
486
|
+
const heard = new Promise((resolve) => {
|
|
487
|
+
t.page.on('response', async (res) => {
|
|
488
|
+
if (settled)
|
|
489
|
+
return;
|
|
323
490
|
const u = res.url();
|
|
324
|
-
if (!/leftTicket\/query/i.test(u))
|
|
491
|
+
if (!/leftTicket\/query/i.test(u))
|
|
492
|
+
return;
|
|
325
493
|
try {
|
|
326
494
|
const text = await res.text();
|
|
327
495
|
if (text) {
|
|
328
496
|
body = text;
|
|
497
|
+
responseUrl = u;
|
|
329
498
|
settled = true;
|
|
330
499
|
resolve();
|
|
331
500
|
}
|
|
332
|
-
}
|
|
501
|
+
}
|
|
502
|
+
catch { /* 流式/竞态不可读则继续等下一个 */ }
|
|
333
503
|
});
|
|
334
|
-
setTimeout(()=>resolve(), q.timeoutMs ?? 25_000);
|
|
335
|
-
});
|
|
336
|
-
await t.page.goto(entry.url, {
|
|
337
|
-
waitUntil: 'domcontentloaded',
|
|
338
|
-
timeout: 30_000
|
|
504
|
+
setTimeout(() => resolve(), q.timeoutMs ?? 25_000);
|
|
339
505
|
});
|
|
506
|
+
await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
340
507
|
await heard;
|
|
341
|
-
const title = await t.page.title().catch(()=>'');
|
|
342
|
-
const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
|
|
508
|
+
const title = await t.page.title().catch(() => '');
|
|
509
|
+
const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000);
|
|
343
510
|
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
344
511
|
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
345
512
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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) {
|
|
513
|
+
return parsedResult(body, responseUrl, `[会话:${site}@${ts}] 12306 公开查询面`, `guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}`);
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
357
516
|
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
358
|
-
}
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
359
519
|
await t.close();
|
|
360
520
|
}
|
|
361
521
|
}
|
|
@@ -363,14 +523,10 @@ export async function sessionFlightSearch(q) {
|
|
|
363
523
|
const started = Date.now();
|
|
364
524
|
const ts = new Date().toISOString();
|
|
365
525
|
const site = 'ctrip-flight';
|
|
366
|
-
const err = (verdict, error)=>({
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
latencyMs: Date.now() - started,
|
|
371
|
-
verdict,
|
|
372
|
-
error
|
|
373
|
-
});
|
|
526
|
+
const err = (verdict, error) => ({
|
|
527
|
+
ok: false, via: 'session-ctrip-flight-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
|
|
528
|
+
});
|
|
529
|
+
// 节律闸:超间隔即拒,不发起导航
|
|
374
530
|
const last = lastCallAt.get(site) ?? 0;
|
|
375
531
|
if (Date.now() - last < MIN_INTERVAL_MS) {
|
|
376
532
|
return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
|
|
@@ -382,55 +538,29 @@ export async function sessionFlightSearch(q) {
|
|
|
382
538
|
}
|
|
383
539
|
const mode = resolveTransportMode(q.profileDir);
|
|
384
540
|
if (mode === 'extension') {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
domain: SITE_DOMAIN.replace(/^\./, ''),
|
|
388
|
-
ticketNames: LOGIN_COOKIE_NAMES
|
|
389
|
-
});
|
|
541
|
+
// ① 登录态快查:票据 cookie 名存在性(免标签页,秒回;needs-login 不再先付一次导航成本)
|
|
542
|
+
const login = await extensionCookieNames({ site, domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES });
|
|
390
543
|
if (!login.ok) {
|
|
391
544
|
const verdict = classifyBridgeFailure(login.kind);
|
|
392
545
|
if (verdict === 'needs-extension') {
|
|
393
|
-
return {
|
|
394
|
-
ok: false,
|
|
395
|
-
via: 'session-ctrip-flight-error',
|
|
396
|
-
evidence: '[会话:ctrip-flight-needs-extension@ts]',
|
|
397
|
-
latencyMs: Date.now() - started,
|
|
398
|
-
verdict,
|
|
399
|
-
error: login.summary,
|
|
400
|
-
installUrl: EXTENSION_STORE_URL,
|
|
401
|
-
installAction: 'add-to-chrome'
|
|
402
|
-
};
|
|
546
|
+
return { ok: false, via: 'session-ctrip-flight-error', evidence: '[会话:ctrip-flight-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: login.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
403
547
|
}
|
|
404
548
|
return err(verdict, login.summary);
|
|
405
549
|
}
|
|
550
|
+
// 登录态闸:用户自己的账号;匿名默认拒(allowAnonymous 仅链路自检且证据标自检态)
|
|
406
551
|
if (login.tickets.length === 0 && !q.allowAnonymous) {
|
|
407
552
|
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
408
553
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
url: entry.url,
|
|
412
|
-
timeoutMs: q.timeoutMs
|
|
413
|
-
});
|
|
554
|
+
// ② 检索 job:后台标签 + MAIN-world 被动嗅探(检索请求由站点自己发出,扩展零写行为)
|
|
555
|
+
const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs });
|
|
414
556
|
appendExtensionAudit(q.auditPath, {
|
|
415
|
-
kind: 'extension-session-job',
|
|
416
|
-
|
|
417
|
-
url: entry.url,
|
|
418
|
-
jobId: 'search',
|
|
419
|
-
result: r.ok ? r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"` : `${r.kind}:${r.summary.slice(0, 120)}`
|
|
557
|
+
kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
|
|
558
|
+
result: r.ok ? (r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"`) : `${r.kind}:${r.summary.slice(0, 120)}`,
|
|
420
559
|
});
|
|
421
560
|
if (!r.ok) {
|
|
422
561
|
const verdict = classifyBridgeFailure(r.kind);
|
|
423
562
|
if (verdict === 'needs-extension') {
|
|
424
|
-
return {
|
|
425
|
-
ok: false,
|
|
426
|
-
via: 'session-ctrip-flight-error',
|
|
427
|
-
evidence: '[会话:ctrip-flight-needs-extension@ts]',
|
|
428
|
-
latencyMs: Date.now() - started,
|
|
429
|
-
verdict,
|
|
430
|
-
error: r.summary,
|
|
431
|
-
installUrl: EXTENSION_STORE_URL,
|
|
432
|
-
installAction: 'add-to-chrome'
|
|
433
|
-
};
|
|
563
|
+
return { ok: false, via: 'session-ctrip-flight-error', evidence: '[会话:ctrip-flight-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' };
|
|
434
564
|
}
|
|
435
565
|
return err(verdict, r.summary);
|
|
436
566
|
}
|
|
@@ -439,78 +569,86 @@ export async function sessionFlightSearch(q) {
|
|
|
439
569
|
if (CHALLENGE_RE.test(title + head)) {
|
|
440
570
|
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
441
571
|
}
|
|
442
|
-
const
|
|
443
|
-
|
|
572
|
+
const parsed = parseBatchSearchResult(r.body);
|
|
573
|
+
if (parsed.verdict === 'error') {
|
|
574
|
+
return err('error', `batchSearch 响应形状异常(非合法空响应;options=0 不视为 miss):body ${r.body.length}B 头 ${r.body.slice(0, 80)}`);
|
|
575
|
+
}
|
|
576
|
+
const options = parsed.options;
|
|
577
|
+
const verdict = parsed.verdict === 'hit' ? 'hit' : 'miss';
|
|
444
578
|
return {
|
|
445
579
|
ok: true,
|
|
446
580
|
via: 'session-ctrip-flight',
|
|
447
581
|
evidence: `[会话:${site}@${ts}] ${options.length} options;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
448
582
|
latencyMs: Date.now() - started,
|
|
449
583
|
verdict,
|
|
450
|
-
options
|
|
584
|
+
options,
|
|
451
585
|
};
|
|
452
586
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
auditPath: q.auditPath,
|
|
457
|
-
mode: mode === 'persistent' ? 'persistent' : 'cdp',
|
|
458
|
-
newPage: true
|
|
459
|
-
});
|
|
587
|
+
// 人机共治纪律:检索一律开自己的新标签页(绝不劫持用户已有页面),用完关自己的页
|
|
588
|
+
// cdp 车道(显式 GOTRY_SESSION_TRANSPORT=cdp opt-in)与 persistent(测试隔离 profile)
|
|
589
|
+
const t = await openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true });
|
|
460
590
|
if (!t.ok) {
|
|
591
|
+
// cdp 未开端口或握手失败 → needs-attach(一次性用户动作);persistent 启动失败仍走 error。
|
|
592
|
+
// transport 的“端口未开”在连接前返回,文案不含 `cdp attach 失败`,两种形态都要归入同一用户门禁。
|
|
461
593
|
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
462
594
|
}
|
|
463
595
|
try {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
596
|
+
// 登录态闸:用户自己的账号,不是匿名实例——匿名态按 onAnonymous 处置(默认 fail)
|
|
597
|
+
const loggedIn = async () => {
|
|
598
|
+
const cookies = await t.browser.cookies().catch(() => []);
|
|
599
|
+
return cookies.some((c) => c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
|
|
467
600
|
};
|
|
468
|
-
if (!await loggedIn() && !q.allowAnonymous) {
|
|
601
|
+
if (!(await loggedIn()) && !q.allowAnonymous) {
|
|
469
602
|
return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
|
|
470
603
|
}
|
|
604
|
+
// 先挂监听再导航(Playwright network 模式):命中 networkHint 的第一个响应即搜索回包
|
|
471
605
|
let settled = false;
|
|
472
606
|
let body = '';
|
|
473
|
-
const heard = new Promise((resolve)=>{
|
|
474
|
-
t.page.on('response', async (res)=>{
|
|
475
|
-
if (settled)
|
|
607
|
+
const heard = new Promise((resolve) => {
|
|
608
|
+
t.page.on('response', async (res) => {
|
|
609
|
+
if (settled)
|
|
610
|
+
return;
|
|
476
611
|
const u = res.url();
|
|
477
|
-
if (!NETWORK_HINTS.some((re)=>re.test(u)))
|
|
612
|
+
if (!NETWORK_HINTS.some((re) => re.test(u)))
|
|
613
|
+
return;
|
|
478
614
|
try {
|
|
479
615
|
body = await res.text();
|
|
480
|
-
}
|
|
616
|
+
}
|
|
617
|
+
catch { /* 流式/竞态不可读则继续等下一个 */ }
|
|
481
618
|
if (body) {
|
|
482
619
|
settled = true;
|
|
483
620
|
resolve();
|
|
484
621
|
}
|
|
485
622
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
await t.page.goto(entry.url, {
|
|
489
|
-
waitUntil: 'domcontentloaded',
|
|
490
|
-
timeout: 30_000
|
|
623
|
+
// 超时也结算:miss/error 由后续判定
|
|
624
|
+
setTimeout(() => resolve(), q.timeoutMs ?? 25_000);
|
|
491
625
|
});
|
|
626
|
+
await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
492
627
|
await heard;
|
|
493
|
-
const title = await t.page.title().catch(()=>'');
|
|
494
|
-
const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
|
|
628
|
+
const title = await t.page.title().catch(() => '');
|
|
629
|
+
const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000);
|
|
495
630
|
if (CHALLENGE_RE.test(title + headHtml)) {
|
|
496
631
|
return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
|
|
497
632
|
}
|
|
498
|
-
const
|
|
499
|
-
|
|
633
|
+
const parsed = parseBatchSearchResult(body);
|
|
634
|
+
if (parsed.verdict === 'error') {
|
|
635
|
+
return err('error', `batchSearch 响应形状异常(非合法空响应;options=0 不视为 miss):body ${body.length}B 头 ${body.slice(0, 80)}`);
|
|
636
|
+
}
|
|
637
|
+
const options = parsed.options;
|
|
638
|
+
const verdict = parsed.verdict === 'hit' ? 'hit' : 'miss';
|
|
500
639
|
return {
|
|
501
640
|
ok: true,
|
|
502
641
|
via: 'session-ctrip-flight',
|
|
503
642
|
evidence: `[会话:${site}@${ts}] ${options.length} options;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
|
|
504
643
|
latencyMs: Date.now() - started,
|
|
505
644
|
verdict,
|
|
506
|
-
options
|
|
645
|
+
options,
|
|
507
646
|
};
|
|
508
|
-
}
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
509
649
|
return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
|
|
510
|
-
}
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
511
652
|
await t.close();
|
|
512
653
|
}
|
|
513
654
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//# sourceURL=ts/capabilities/session-search.ts
|