@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +309 -30
  4. package/bin/gotry-inner.js +74 -23
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +22 -8
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/anything.js +62 -29
  11. package/dist/capabilities/channel-health.js +103 -0
  12. package/dist/capabilities/channel-registry.js +213 -0
  13. package/dist/capabilities/doctor.js +307 -0
  14. package/dist/capabilities/effect.js +178 -5
  15. package/dist/capabilities/flyai.js +13 -0
  16. package/dist/capabilities/hbcli.js +28 -2
  17. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  18. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  19. package/dist/capabilities/session/extension-bridge.js +14 -1
  20. package/dist/capabilities/session/extension-channel.js +4 -2
  21. package/dist/capabilities/session/extension-distribution.js +1 -1
  22. package/dist/capabilities/session-search.js +311 -0
  23. package/dist/capabilities/visa-policy.js +126 -0
  24. package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
  25. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  26. package/dist/scripts/anything-tests.js +70 -21
  27. package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
  28. package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
  29. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  30. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  31. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  32. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  33. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  34. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  35. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  36. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  37. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  38. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  39. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  40. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  41. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  42. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  43. package/dist/scripts/bootstrap-tests.js +113 -2
  44. package/dist/scripts/build-metrics-report.js +339 -0
  45. package/dist/scripts/channel-probe-tests.js +148 -0
  46. package/dist/scripts/channel-probe.js +252 -0
  47. package/dist/scripts/channel-registry-tests.js +262 -0
  48. package/dist/scripts/doctor-tests.js +175 -0
  49. package/dist/scripts/effect-tests.js +274 -1
  50. package/dist/scripts/extension-tests.js +129 -11
  51. package/dist/scripts/fact-gate-tests.js +91 -2
  52. package/dist/scripts/flyai-tests.js +17 -1
  53. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  54. package/dist/scripts/hbcli-tests.js +38 -1
  55. package/dist/scripts/metrics-report-tests.js +288 -0
  56. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  57. package/dist/scripts/session-tests.js +401 -1
  58. package/dist/scripts/smoke.js +100 -83
  59. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  60. package/dist/scripts/turn-handoff-collect.js +189 -0
  61. package/dist/scripts/turn-policy-tests.js +56 -0
  62. package/dist/scripts/typed-contract-canary.js +261 -0
  63. package/dist/scripts/visa-policy-tests.js +75 -0
  64. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  65. package/dist/src/artifact-gate.js +91 -3
  66. package/dist/src/benchmark-agent-conformance.js +39 -16
  67. package/dist/src/benchmark-environment-bridge.js +316 -52
  68. package/dist/src/bookable-facts.js +62 -3
  69. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  70. package/dist/src/booking-surface/canonical-schema.js +8 -22
  71. package/dist/src/booking-surface/contracts.js +70 -19
  72. package/dist/src/booking-surface/dsh-planner.js +310 -177
  73. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  74. package/dist/src/booking-surface/index.js +1 -5
  75. package/dist/src/booking-surface/profile.js +1 -1
  76. package/dist/src/booking-surface/runtime.js +1682 -247
  77. package/dist/src/booking-surface/server.js +371 -185
  78. package/dist/src/booking-surface/startup.js +17 -27
  79. package/dist/src/booking-surface/validation.js +270 -760
  80. package/dist/src/index.js +739 -150
  81. package/dist/src/turn-deadline.js +292 -0
  82. package/dist/src/turn-policy.js +146 -0
  83. package/dist/src/wish-pool.js +5 -0
  84. package/extension/background.js +23 -4
  85. package/extension/content-main.js +47 -9
  86. package/extension/manifest.json +27 -9
  87. package/package.json +7 -26
  88. package/schemas/booking.surface.schema.json +2593 -0
  89. package/ts/capabilities/agent-reach.ts +4 -4
  90. package/ts/capabilities/anything.ts +89 -38
  91. package/ts/capabilities/flyai.ts +21 -3
  92. package/ts/capabilities/hbcli.ts +53 -4
  93. package/ts/capabilities/session/action-cache.ts +1 -1
  94. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  95. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  96. package/ts/capabilities/session/extension-bridge.ts +32 -2
  97. package/ts/capabilities/session/extension-channel.ts +9 -8
  98. package/ts/capabilities/session/extension-distribution.ts +3 -1
  99. package/ts/capabilities/session/health-watch.ts +1 -1
  100. package/ts/capabilities/session/read-guard.ts +1 -1
  101. package/ts/capabilities/session/wizard.ts +1 -1
  102. package/ts/capabilities/session-search.ts +323 -1
  103. package/ts/dsh-runtime/vendor/README.md +60 -0
  104. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  105. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  106. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  107. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  108. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  109. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  110. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  111. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  112. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  113. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  114. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  115. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  116. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  117. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  118. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  119. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  120. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  121. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  122. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  123. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  124. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  125. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  126. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  127. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  128. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  129. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  130. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  131. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  132. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  133. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  134. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  135. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  136. package/ts/package.json +9 -0
  137. package/ts/scripts/turn-handoff-collect.ts +178 -0
  138. package/ts/src/artifact-gate.ts +82 -2
  139. package/ts/src/benchmark-agent-conformance.ts +54 -15
  140. package/ts/src/benchmark-environment-bridge.ts +234 -46
  141. package/ts/src/bookable-facts.ts +109 -5
  142. package/ts/src/booking-saga.ts +1 -1
  143. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  144. package/ts/src/booking-surface/canonical-schema.js +8 -22
  145. package/ts/src/booking-surface/contracts.ts +278 -239
  146. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  147. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  148. package/ts/src/booking-surface/index.ts +1 -5
  149. package/ts/src/booking-surface/profile.ts +11 -11
  150. package/ts/src/booking-surface/runtime.ts +1351 -351
  151. package/ts/src/booking-surface/server.ts +282 -175
  152. package/ts/src/booking-surface/startup.ts +36 -48
  153. package/ts/src/booking-surface/validation.ts +194 -442
  154. package/ts/src/contracts.ts +1 -1
  155. package/ts/src/index.ts +492 -172
  156. package/ts/src/loop.ts +1 -1
  157. package/ts/src/state-ledger.ts +1 -1
  158. package/ts/src/turn-deadline.ts +365 -0
  159. package/ts/src/turn-policy.ts +154 -0
  160. package/ts/src/wish-pool.ts +17 -4
  161. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  162. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  163. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  164. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  165. package/dist/src/booking-surface/contracts-v2.js +0 -89
  166. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  167. package/dist/src/booking-surface/server-v2.js +0 -334
  168. package/dist/src/booking-surface/validation-v2.js +0 -319
  169. package/dist/src/tool-budget.js +0 -136
  170. package/schemas/booking.surface.v1.schema.json +0 -927
  171. package/schemas/booking.surface.v2.schema.json +0 -61
  172. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  173. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  174. package/ts/src/booking-surface/server-v2.ts +0 -247
  175. package/ts/src/booking-surface/validation-v2.ts +0 -205
  176. package/ts/src/tool-budget.ts +0 -165
@@ -3,7 +3,9 @@ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { classifyRequest, isSubmitText } from '../capabilities/session/read-guard.js';
5
5
  import { buildEntryUrl, parseBatchSearch } from '../capabilities/session/adapters/ctrip-flight.js';
6
- import { sessionFlightSearch, __resetRateLimiterForTest, classifyTransportFailure } from '../capabilities/session-search.js';
6
+ import { buildHotelEntryUrl, parseCtripHotelList, looksLikeHotelListBody } from '../capabilities/session/adapters/ctrip-hotel.js';
7
+ import { buildTrainEntryUrl, parseLeftTicketQuery, STATION_TELECODES } from '../capabilities/session/adapters/rail-12306.js';
8
+ import { sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, trainStationUnresolvedHint, hotelCityUnresolvedHint, __resetRateLimiterForTest, classifyTransportFailure } from '../capabilities/session-search.js';
7
9
  import { flyaiSearch } from '../capabilities/flyai.js';
8
10
  import { createConsentGate } from '../capabilities/session-consent.js';
9
11
  import { sessionLogin, pollTicketNames, LOGIN_TARGETS } from '../capabilities/session-login.js';
@@ -445,6 +447,404 @@ console.log('H. flyaiSearch hotel(飞猪官方 search-hotel)');
445
447
  } else {
446
448
  console.log(' SKIP - 登录 live 探针默认关(GOTRY_SESSION_LIVE=1 opt-in;工具面/桌面入口才真调)');
447
449
  }
450
+ }console.log('K. 酒店适配器(buildHotelEntryUrl/走形解析/形状嗅探/城市码指引/节律与日期闸)');
451
+ {
452
+ const h1 = buildHotelEntryUrl({
453
+ to: '迪拜',
454
+ cityId: 220,
455
+ checkIn: '2026-12-01',
456
+ checkOut: '2026-12-03',
457
+ adults: 2
458
+ });
459
+ assert(h1.ok && h1.url === 'https://hotels.ctrip.com/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03&adult=2', 'cityId 显式覆盖 + 参数序', h1);
460
+ const h2 = buildHotelEntryUrl({
461
+ to: '上海'
462
+ });
463
+ assert(h2.ok && h2.url === 'https://hotels.ctrip.com/hotels/list?city=2', '码表内城市(上海=2,实测校准)', h2);
464
+ const h3 = buildHotelEntryUrl({
465
+ to: '不在码表的城市'
466
+ });
467
+ assert(!h3.ok && h3.unresolved?.includes('不在码表的城市') === true, '未收录城市 unresolved(不猜 id,不造数)', h3);
468
+ assert(/city=/.test(hotelCityUnresolvedHint([
469
+ '迪拜'
470
+ ])), '未收录指引带 cityId 发现路径(web 搜 list 页 URL)');
471
+ const hDubai = buildHotelEntryUrl({
472
+ to: '迪拜'
473
+ });
474
+ assert(hDubai.ok && hDubai.url.includes('city=220'), '迪拜=220(实测,轨迹里 agent 构造的 id 正确)', hDubai);
475
+ const hLijiang = buildHotelEntryUrl({
476
+ to: '丽江'
477
+ });
478
+ assert(hLijiang.ok && hLijiang.url.includes('city=37'), '丽江=37(实测)', hLijiang);
479
+ const realShape = JSON.stringify({
480
+ initListData: {
481
+ hotelList: [
482
+ {
483
+ hotelInfo: {
484
+ summary: {
485
+ hotelId: '3732301',
486
+ nameInfo: {
487
+ name: 'Ibis Styles Dubai Jumeira',
488
+ names: [
489
+ 'Ibis Styles Dubai Jumeira'
490
+ ]
491
+ },
492
+ hotelStar: {
493
+ star: 3
494
+ },
495
+ positionInfo: {
496
+ address: 'Al Mina Road - Jumeirah 1'
497
+ }
498
+ },
499
+ commentInfo: {
500
+ commentScore: '4.4'
501
+ },
502
+ roomInfo: [
503
+ {
504
+ priceInfo: {
505
+ price: 520
506
+ }
507
+ },
508
+ {
509
+ priceInfo: {
510
+ price: 468,
511
+ displayPrice: '¥468起'
512
+ }
513
+ }
514
+ ]
515
+ }
516
+ }
517
+ ]
518
+ }
519
+ });
520
+ const realHotels = parseCtripHotelList(realShape);
521
+ assert(realHotels.length === 1, '真实形态(hotelInfo 包裹)解析命中', realHotels);
522
+ assert(realHotels[0].name === 'Ibis Styles Dubai Jumeira' && realHotels[0].hotelId === '3732301' && realHotels[0].star === 3 && realHotels[0].score === '4.4', '结构化字段(名/id/星/评分)', realHotels[0]);
523
+ assert(realHotels[0].price === 468, '多房型取最低价(roomInfo[].priceInfo.price 官方路径)', realHotels[0]);
524
+ assert(realHotels[0].jumpUrl === 'https://hotels.ctrip.com/hotel/3732301', '真实形态 jumpUrl 构造', realHotels[0]);
525
+ const encryptedShape = JSON.stringify({
526
+ hotelList: [
527
+ {
528
+ hotelInfo: {
529
+ summary: {
530
+ hotelId: '999',
531
+ nameInfo: {
532
+ names: [
533
+ 'Atlantis The Palm'
534
+ ]
535
+ },
536
+ hotelStar: {
537
+ star: 5
538
+ }
539
+ },
540
+ commentInfo: {
541
+ commentScore: '4.8'
542
+ },
543
+ roomInfo: [
544
+ {
545
+ priceInfo: {
546
+ priceToken: 'ENCRYPTED_TOKEN',
547
+ isHiddenPrice: false
548
+ }
549
+ }
550
+ ]
551
+ }
552
+ }
553
+ ]
554
+ });
555
+ const encHotels = parseCtripHotelList(encryptedShape);
556
+ assert(encHotels.length === 1, '加密价条目不丢弃(名/星/评分仍可呈现)', encHotels);
557
+ assert(encHotels[0].price === 0 && !encHotels[0].priceRaw, '加密价不伪造价格(价以 jumpUrl 落地页为准)', encHotels[0]);
558
+ const displayShape = JSON.stringify({
559
+ hotelList: [
560
+ {
561
+ hotelInfo: {
562
+ summary: {
563
+ hotelId: '888',
564
+ nameInfo: {
565
+ names: [
566
+ 'Rove Downtown'
567
+ ]
568
+ }
569
+ },
570
+ roomInfo: [
571
+ {
572
+ priceInfo: {
573
+ displayPrice: '¥7xx'
574
+ }
575
+ }
576
+ ]
577
+ }
578
+ }
579
+ ]
580
+ });
581
+ const dispHotels = parseCtripHotelList(displayShape);
582
+ assert(dispHotels.length === 1 && dispHotels[0].priceRaw === '¥7xx' && dispHotels[0].price === 0, 'displayPrice 字符串价原样保留(打码口径)', dispHotels[0]);
583
+ const fixture = JSON.stringify({
584
+ data: {
585
+ hotelList: [
586
+ {
587
+ hotelId: 442516,
588
+ hotelName: 'Dubai Marriott',
589
+ star: 5,
590
+ commentScore: 4.7,
591
+ position: {
592
+ address: 'Sheikh Zayed Rd'
593
+ },
594
+ priceInfo: {
595
+ avgPrice: 680,
596
+ total: 1360
597
+ }
598
+ },
599
+ {
600
+ hotelId: '999',
601
+ hotelName: 'Rove Downtown',
602
+ priceInfo: {
603
+ priceDisplay: '¥7xx'
604
+ }
605
+ }
606
+ ]
607
+ }
608
+ });
609
+ const hotels1 = parseCtripHotelList(fixture);
610
+ assert(hotels1.length === 2, '走形解析命中 hotelList 数组', hotels1);
611
+ assert(hotels1[0].name === 'Dubai Marriott' && hotels1[0].price === 680 && hotels1[0].star === 5 && hotels1[0].score === '4.7', '归一化字段(名/价/星/评分)', hotels1[0]);
612
+ assert(hotels1[0].jumpUrl === 'https://hotels.ctrip.com/hotel/442516', 'jumpUrl 由 hotelId 构造(预订由人完成)', hotels1[0]);
613
+ assert(hotels1[1].price === 0 && hotels1[1].priceRaw === '¥7xx', '打码价原样保留不数值化(不伪装真价)', hotels1[1]);
614
+ const hotels2 = parseCtripHotelList(JSON.stringify({
615
+ hotelMatchInfos: [
616
+ {
617
+ name: 'Atlantis',
618
+ price: 2100,
619
+ score: '4.8',
620
+ hotelId: '123'
621
+ }
622
+ ]
623
+ }));
624
+ assert(hotels2.length === 1 && hotels2[0].price === 2100, '扁平形态(顶层 price 数字)同样命中', hotels2);
625
+ assert(parseCtripHotelList('not json{').length === 0 && parseCtripHotelList('{"a":1}').length === 0, 'malformed/无签名 一律返空(不抛错)');
626
+ assert(looksLikeHotelListBody('{"data":{"hotelList":[]}}') === true, '签名命中 hotelList');
627
+ assert(looksLikeHotelListBody('{"data":{"userList":[]}}') === false, '无签名不转发(防无关响应误投)');
628
+ assert(looksLikeHotelListBody('x'.repeat(2_000_001)) === false, '超大响应体不嗅探(上限把关)');
629
+ const dPast = await sessionHotelSearch({
630
+ to: '上海',
631
+ checkIn: '2026-01-01',
632
+ checkOut: '2026-01-03'
633
+ });
634
+ assert(dPast.ok === false && dPast.verdict === 'error' && /不是未来合法区间/.test(dPast.error ?? ''), '过去入住日代码层拒绝,不发上游', dPast);
635
+ const dPair = await sessionHotelSearch({
636
+ to: '上海',
637
+ checkIn: '2026-12-01'
638
+ });
639
+ assert(dPair.ok === false && /成对/.test(dPair.error ?? ''), 'checkIn/checkOut 须成对', dPair);
640
+ __resetRateLimiterForTest();
641
+ const e1 = await sessionHotelSearch({
642
+ to: '不在码表的城市'
643
+ });
644
+ assert(e1.ok === false && e1.verdict === 'error' && /city=/.test(e1.error ?? ''), '未收录城市 → error + cityId 指引', e1);
645
+ const e2 = await sessionHotelSearch({
646
+ to: '上海'
647
+ });
648
+ assert(e2.ok === false && e2.verdict === 'cooldown', '节律闸:同站点 30s 内第二调 → cooldown(不发起导航)', e2);
649
+ }console.log('L. 火车适配器(buildTrainEntryUrl/parseLeftTicketQuery/电报码指引/节律闸)');
650
+ {
651
+ const t1 = buildTrainEntryUrl({
652
+ from: '上海',
653
+ to: '昆明',
654
+ date: '2026-12-01'
655
+ });
656
+ assert(t1.ok && t1.url.includes('fs=%E4%B8%8A%E6%B5%B7%2CSHH') && t1.url.includes('ts=%E6%98%86%E6%98%8E%2CKMM') && t1.url.includes('date=2026-12-01'), '城市电报码(fs=城市名,SHH 形态)', t1);
657
+ const t2 = buildTrainEntryUrl({
658
+ from: '丽江',
659
+ to: '大理',
660
+ date: '2026-12-01',
661
+ fromStationTelecode: 'EHM',
662
+ toStationTelecode: 'KDM'
663
+ });
664
+ assert(t2.ok && t2.url.includes('%2CEHM') && t2.url.includes('%2CKDM'), '显式电报码覆盖(码表外城市)', t2);
665
+ const t3 = buildTrainEntryUrl({
666
+ from: '敦煌',
667
+ to: '西双版纳',
668
+ date: '2026-12-01'
669
+ });
670
+ assert(!t3.ok && t3.unresolved?.includes('敦煌') === true, '码表外城市 unresolved(官方站表亦无城市组,不猜码)', t3);
671
+ assert(/fromStationTelecode/.test(trainStationUnresolvedHint([
672
+ '敦煌'
673
+ ])), '未收录指引带电报码发现路径(web 搜 kyfw 查询页 URL)');
674
+ {
675
+ const snap = JSON.parse(readFileSync(join(import.meta.dirname, '..', '..', 'data', 'stations-12306-verify.json'), 'utf-8'));
676
+ const names = Object.keys(snap.cities);
677
+ assert(names.length >= 129, `快照覆盖 ${names.length} 城`);
678
+ let drift = 0;
679
+ for (const [city, v] of Object.entries(snap.cities)){
680
+ if (STATION_TELECODES[city] !== v.telecode) {
681
+ drift += 1;
682
+ console.log(` FAIL-detail - 电报码漂移: ${city} 表=${STATION_TELECODES[city]} 快照=${v.telecode}`);
683
+ }
684
+ }
685
+ assert(drift === 0, `电报码表 129 城与官方快照零漂移(漂移 ${drift} 处)`);
686
+ assert(Object.keys(STATION_TELECODES).length === names.length, '表与快照城市集合一致(无表外多城)');
687
+ }
688
+ const t4 = buildTrainEntryUrl({
689
+ from: '南宁',
690
+ to: '丽江',
691
+ date: '2026-12-01'
692
+ });
693
+ assert(t4.ok && t4.url.includes('%2CNNZ'), '南宁=NNZ(官方站表校准,曾错 NIZ)且丽江已入表=LHM', t4);
694
+ const makeRow = (seats)=>{
695
+ const c = new Array(56).fill('');
696
+ c[2] = '24000000G1375';
697
+ c[3] = 'G1375';
698
+ c[4] = 'SHH';
699
+ c[5] = 'KMM';
700
+ c[6] = 'SHH';
701
+ c[7] = 'KMM';
702
+ c[8] = '07:35';
703
+ c[9] = '15:27';
704
+ c[10] = '07:52';
705
+ c[11] = 'Y';
706
+ c[12] = 'yp';
707
+ c[13] = '20261201';
708
+ c[14] = 'x';
709
+ c[15] = 'loc';
710
+ c[16] = '01';
711
+ c[17] = '02';
712
+ c[18] = 'Y';
713
+ c[19] = '0';
714
+ for (const [k, v] of Object.entries(seats))c[Number(k)] = v;
715
+ return c.join('|');
716
+ };
717
+ const entry = buildTrainEntryUrl({
718
+ from: '上海',
719
+ to: '昆明',
720
+ date: '2026-12-01'
721
+ });
722
+ assert(entry.ok, 'entry 构造成功');
723
+ const probeSeats = {};
724
+ for(let i = 20; i <= 33; i++)probeSeats[i] = String(i);
725
+ const probeRow = makeRow(probeSeats);
726
+ const probe = parseLeftTicketQuery(JSON.stringify({
727
+ data: {
728
+ result: [
729
+ probeRow
730
+ ],
731
+ map: {
732
+ SHH: '上海南',
733
+ KMM: '昆明'
734
+ }
735
+ }
736
+ }), entry.url ?? '');
737
+ assert(probe.length === 1, '探针行解析', probe);
738
+ const expectSeats = [
739
+ [
740
+ '其他(通勤)',
741
+ '20'
742
+ ],
743
+ [
744
+ '高级软卧',
745
+ '21'
746
+ ],
747
+ [
748
+ '其他',
749
+ '22'
750
+ ],
751
+ [
752
+ '软卧(一等卧)',
753
+ '23'
754
+ ],
755
+ [
756
+ '软座',
757
+ '24'
758
+ ],
759
+ [
760
+ '特等座',
761
+ '25'
762
+ ],
763
+ [
764
+ '无座',
765
+ '26'
766
+ ],
767
+ [
768
+ 'yp_b(上游席别)',
769
+ '27'
770
+ ],
771
+ [
772
+ '硬卧(二等卧)',
773
+ '28'
774
+ ],
775
+ [
776
+ '硬座',
777
+ '29'
778
+ ],
779
+ [
780
+ '二等座',
781
+ '30'
782
+ ],
783
+ [
784
+ '一等座',
785
+ '31'
786
+ ],
787
+ [
788
+ '商务座',
789
+ '32'
790
+ ],
791
+ [
792
+ '动卧',
793
+ '33'
794
+ ]
795
+ ];
796
+ for (const [label, idxVal] of expectSeats){
797
+ assert(probe[0].seats[label] === idxVal, `座位桶 ${label} 下标=${idxVal}(官方 cN)`, probe[0].seats);
798
+ }
799
+ assert(probe[0].fromStation === '上海南' && probe[0].toStation === '昆明', '站名= data.map[电报码](官方 cN 口径,非行内索引)', probe[0]);
800
+ const trains1 = parseLeftTicketQuery(JSON.stringify({
801
+ data: {
802
+ result: [
803
+ makeRow({
804
+ 25: '有',
805
+ 26: '有',
806
+ 28: '有',
807
+ 29: '有',
808
+ 30: '有',
809
+ 31: '有',
810
+ 32: '有'
811
+ })
812
+ ],
813
+ map: {
814
+ SHH: '上海南',
815
+ KMM: '昆明'
816
+ }
817
+ }
818
+ }), entry.url ?? '');
819
+ assert(trains1.length === 1, '管道行解析命中', trains1);
820
+ assert(trains1[0].trainCode === 'G1375' && trains1[0].depTime === '07:35' && trains1[0].arrTime === '15:27' && trains1[0].durationMin === 472, '车次/时刻/历时归一化', trains1[0]);
821
+ assert(trains1[0].seats['商务座'] === '有' && trains1[0].seats['二等座'] === '有' && trains1[0].seats['特等座'] === '有' && trains1[0].seats['硬卧(二等卧)'] === '有', '余票分桶(官方索引)', trains1[0].seats);
822
+ assert(!('price' in trains1[0]), '列表接口无票价——不伪装价格(诚实面)');
823
+ const shifted = makeRow({
824
+ 25: '有'
825
+ }).split('|').slice(1).join('|');
826
+ assert(parseLeftTicketQuery(JSON.stringify({
827
+ data: {
828
+ result: [
829
+ shifted
830
+ ],
831
+ map: {}
832
+ }
833
+ }), entry.url ?? '').length === 0, '索引漂移行(签名失配)整行跳过,fail-visible');
834
+ assert(parseLeftTicketQuery('not json', entry.url ?? '').length === 0 && parseLeftTicketQuery('{"data":{"result":[]}}', entry.url ?? '').length === 0, 'malformed/空 一律返空(不抛错)');
835
+ __resetRateLimiterForTest();
836
+ const e1 = await sessionTrainSearch({
837
+ from: '不在码表',
838
+ to: '也不在',
839
+ date: '2026-12-01'
840
+ });
841
+ assert(e1.ok === false && e1.verdict === 'error' && /fromStationTelecode/.test(e1.error ?? ''), '未收录电报码 → error + 发现路径指引', e1);
842
+ const e2 = await sessionTrainSearch({
843
+ from: '上海',
844
+ to: '昆明',
845
+ date: '2026-12-01'
846
+ });
847
+ assert(e2.ok === false && e2.verdict === 'cooldown', '节律闸:同站点 30s 内第二调 → cooldown(不发起导航)', e2);
448
848
  }if (process.env.GOTRY_SESSION_TEST_FORCE_FAILURE === '1') {
449
849
  assert(false, 'forced session failure propagation proof');
450
850
  }