@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.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. 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 { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
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, parseLeftTicketQuery, TRAIN_SITE_HOST } from './session/adapters/rail-12306.js';
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) ? 'needs-attach' : 'error';
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) return 'persistent';
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)) return offlineSessionFlightResult();
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) return;
71
+ if (!auditPath)
72
+ return;
41
73
  try {
42
- const record = {
43
- ts: new Date().toISOString(),
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
- } catch {}
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
- 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)) {
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
- const login = await extensionCookieNames({
98
- site,
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
- const r = await extensionSearchJob({
122
- site,
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
- 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)}`
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
- 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
- });
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) return;
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) return;
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
- } catch {}
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
- } catch (e) {
207
+ }
208
+ catch (e) {
223
209
  return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
224
- } finally{
210
+ }
211
+ finally {
225
212
  await t.close();
226
213
  }
227
214
  }
228
- export function trainStationUnresolvedHint(cities) {
229
- return `城市 ${cities.join('/')} 不在 12306 城市电报码表——web 搜「12306 ${cities[0] ?? ''} 余票」拿到 kyfw 查询页 URL 里的 fs=城市,XXX 三位码,带 fromStationTelecode/toStationTelecode 重试`;
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
- export async function sessionTrainSearch(q) {
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 = '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
- });
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 = buildTrainEntryUrl({
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', trainStationUnresolvedHint(entry.unresolved ?? [
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
- const r = await extensionSearchJob({
264
- site,
265
- url: entry.url,
266
- timeoutMs: q.timeoutMs
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
- 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)}`
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
- const trains = parseLeftTicketQuery(r.body, entry.url);
297
- const verdict = trains.length > 0 ? 'hit' : 'miss';
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-train-12306',
301
- evidence: `[会话:${site}@${ts}] ${trains.length} trains;transport=extension(公开查询面,被动嗅探,零系统弹窗;扩展零写行为=物理只读)`,
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
- trains
360
+ rates,
305
361
  };
306
362
  }
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
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
- const heard = new Promise((resolve)=>{
321
- t.page.on('response', async (res)=>{
322
- if (settled) return;
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)) return;
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
- } catch {}
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
- 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) {
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
- } finally{
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
- ok: false,
368
- via: 'session-ctrip-flight-error',
369
- evidence: `[会话:${site}@error@${ts}] ${error}`,
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
- const login = await extensionCookieNames({
386
- site,
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
- const r = await extensionSearchJob({
410
- site,
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
- site,
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 options = parseBatchSearch(r.body);
443
- const verdict = options.length > 0 ? 'hit' : 'miss';
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
- const t = await openSession({
454
- profileDir: q.profileDir,
455
- headless: q.headless,
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
- const loggedIn = async ()=>{
465
- const cookies = await t.browser.cookies().catch(()=>[]);
466
- return cookies.some((c)=>c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
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) return;
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))) return;
612
+ if (!NETWORK_HINTS.some((re) => re.test(u)))
613
+ return;
478
614
  try {
479
615
  body = await res.text();
480
- } catch {}
616
+ }
617
+ catch { /* 流式/竞态不可读则继续等下一个 */ }
481
618
  if (body) {
482
619
  settled = true;
483
620
  resolve();
484
621
  }
485
622
  });
486
- setTimeout(()=>resolve(), q.timeoutMs ?? 25_000);
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 options = parseBatchSearch(body);
499
- const verdict = options.length > 0 ? 'hit' : 'miss';
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
- } catch (e) {
647
+ }
648
+ catch (e) {
509
649
  return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
510
- } finally{
650
+ }
651
+ finally {
511
652
  await t.close();
512
653
  }
513
654
  }
514
-
515
-
516
- //# sourceURL=ts/capabilities/session-search.ts