@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +236 -3
- package/bin/gotry-inner.js +66 -20
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +87 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +121 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +735 -149
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/package.json +1 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -3,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 {
|
|
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
|
}
|