@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.
Files changed (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  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 +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -468,7 +468,280 @@ assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
468
468
  assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
469
469
  assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
470
470
  console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
471
- console.log('EFFECT INTERPRETER TESTS: 10/10 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应,纯离线)');
471
+ __resetEffectBreakersForTest();
472
+ let flakyCalls = 0;
473
+ const ipTimeout = makeProductionInterpreter({
474
+ sleep: sleep0,
475
+ breakers: new Map(),
476
+ handlers: {
477
+ HBCLI_HOTEL_SEARCH: async ()=>{
478
+ flakyCalls += 1;
479
+ if (flakyCalls === 1) return {
480
+ via: 'hbcli-error',
481
+ exitCode: -1,
482
+ result: null,
483
+ evidence: '[实时API:hbcli@timeout@ts]',
484
+ latencyMs: 1,
485
+ error: 'timeout after 30000ms'
486
+ };
487
+ return {
488
+ via: 'hbcli-realtime',
489
+ exitCode: 0,
490
+ result: {
491
+ hotels: []
492
+ },
493
+ evidence: '[实时API:hbcli@ts]',
494
+ latencyMs: 1
495
+ };
496
+ }
497
+ }
498
+ });
499
+ const itp11 = await ipTimeout({
500
+ effect: 'HBCLI_HOTEL_SEARCH',
501
+ params: {
502
+ destination: '迪拜'
503
+ }
504
+ });
505
+ assert.equal(itp11.trace.attempts, 2, `timeout 重试一次(attempts=2),实际 ${itp11.trace.attempts}`);
506
+ assert.equal(itp11.result?.via, 'hbcli-realtime', '重试后恢复实时');
507
+ assert.equal(itp11.trace.breaker, 'closed', '恢复即成功,断路不计数');
508
+ let hardCalls = 0;
509
+ const ipEnoent = makeProductionInterpreter({
510
+ sleep: sleep0,
511
+ breakers: new Map(),
512
+ handlers: {
513
+ HBCLI_HOTEL_SEARCH: async ()=>{
514
+ hardCalls += 1;
515
+ return {
516
+ via: 'hbcli-error',
517
+ exitCode: -1,
518
+ result: null,
519
+ evidence: '[实时API:hbcli@spawn_error@ts]',
520
+ latencyMs: 1,
521
+ error: 'spawn hbcli ENOENT'
522
+ };
523
+ }
524
+ }
525
+ });
526
+ const itp11b = await ipEnoent({
527
+ effect: 'HBCLI_HOTEL_SEARCH',
528
+ params: {
529
+ destination: '迪拜'
530
+ }
531
+ });
532
+ assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)');
533
+ console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK');
534
+ __resetEffectBreakersForTest();
535
+ const sixChannels = [
536
+ [
537
+ 'ANYTHING_SEARCH',
538
+ 'cli',
539
+ {
540
+ ok: true,
541
+ via: 'hbcli-anything',
542
+ verdict: 'hit',
543
+ evidence: 'e',
544
+ latencyMs: 1
545
+ }
546
+ ],
547
+ [
548
+ 'WEB_READ',
549
+ 'api',
550
+ {
551
+ ok: true,
552
+ via: 'r.jina.ai',
553
+ evidence: 'e',
554
+ latencyMs: 1
555
+ }
556
+ ],
557
+ [
558
+ 'GITHUB_SEARCH',
559
+ 'cli',
560
+ {
561
+ ok: true,
562
+ verdict: 'found',
563
+ evidence: 'e',
564
+ latencyMs: 1
565
+ }
566
+ ],
567
+ [
568
+ 'VIDEO_SUBTITLE',
569
+ 'cli',
570
+ {
571
+ ok: true,
572
+ verdict: 'found',
573
+ evidence: 'e',
574
+ latencyMs: 1
575
+ }
576
+ ],
577
+ [
578
+ 'AGENT_REACH',
579
+ 'cli',
580
+ {
581
+ ok: true,
582
+ verdict: 'found',
583
+ channel: 'v2ex',
584
+ method: 'read',
585
+ evidence: 'e',
586
+ latencyMs: 1
587
+ }
588
+ ],
589
+ [
590
+ 'SESSION_LOGIN',
591
+ 'browser',
592
+ {
593
+ ok: true,
594
+ via: 'session-login',
595
+ verdict: 'pending',
596
+ evidence: 'e',
597
+ latencyMs: 1
598
+ }
599
+ ]
600
+ ];
601
+ for (const [fx, channel, obs] of sixChannels){
602
+ const ip = makeProductionInterpreter({
603
+ sleep: sleep0,
604
+ breakers: new Map(),
605
+ handlers: {
606
+ [fx]: async ()=>obs
607
+ }
608
+ });
609
+ const out = await ip({
610
+ effect: fx,
611
+ params: {}
612
+ });
613
+ assert.equal(out.result, obs, `${fx} 透传原样`);
614
+ assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`);
615
+ assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`);
616
+ }
617
+ console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK');
618
+ for (const fx of [
619
+ 'AGENT_REACH',
620
+ 'SESSION_LOGIN'
621
+ ]){
622
+ const ip = makeProductionInterpreter({
623
+ sleep: sleep0,
624
+ breakers: new Map(),
625
+ handlers: {
626
+ [fx]: async ()=>{
627
+ throw new Error('upstream says no');
628
+ }
629
+ }
630
+ });
631
+ const out = await ip({
632
+ effect: fx,
633
+ params: {}
634
+ });
635
+ assert.equal(out.result, null, `${fx} 抛错 → result=null`);
636
+ assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`);
637
+ }
638
+ console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK');
639
+ __resetEffectBreakersForTest();
640
+ let webCalls = 0;
641
+ const ipWeb = makeProductionInterpreter({
642
+ sleep: sleep0,
643
+ breakers: new Map(),
644
+ handlers: {
645
+ WEB_READ: async ()=>{
646
+ webCalls += 1;
647
+ if (webCalls === 1) throw new Error('socket hang up');
648
+ return {
649
+ ok: true,
650
+ via: 'r.jina.ai',
651
+ evidence: 'e',
652
+ latencyMs: 1
653
+ };
654
+ }
655
+ }
656
+ });
657
+ const outWeb = await ipWeb({
658
+ effect: 'WEB_READ',
659
+ params: {
660
+ url: 'https://example.com'
661
+ }
662
+ });
663
+ assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`);
664
+ let ghTimeoutCalls = 0;
665
+ const ipGhTimeout = makeProductionInterpreter({
666
+ sleep: sleep0,
667
+ breakers: new Map(),
668
+ handlers: {
669
+ GITHUB_SEARCH: async ()=>{
670
+ ghTimeoutCalls += 1;
671
+ if (ghTimeoutCalls === 1) return {
672
+ ok: false,
673
+ verdict: 'timeout',
674
+ evidence: 'e',
675
+ latencyMs: 1,
676
+ stdout: '',
677
+ stderr: ''
678
+ };
679
+ return {
680
+ ok: true,
681
+ verdict: 'found',
682
+ evidence: 'e',
683
+ latencyMs: 1
684
+ };
685
+ }
686
+ }
687
+ });
688
+ const outGhT = await ipGhTimeout({
689
+ effect: 'GITHUB_SEARCH',
690
+ params: {
691
+ query: 'x'
692
+ }
693
+ });
694
+ assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)');
695
+ let ghNiCalls = 0;
696
+ const ipGhNi = makeProductionInterpreter({
697
+ sleep: sleep0,
698
+ breakers: new Map(),
699
+ handlers: {
700
+ GITHUB_SEARCH: async ()=>{
701
+ ghNiCalls += 1;
702
+ return {
703
+ ok: false,
704
+ verdict: 'not-installed',
705
+ evidence: 'e',
706
+ latencyMs: 0
707
+ };
708
+ }
709
+ }
710
+ });
711
+ const outGhNi = await ipGhNi({
712
+ effect: 'GITHUB_SEARCH',
713
+ params: {
714
+ query: 'x'
715
+ }
716
+ });
717
+ assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试');
718
+ console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK');
719
+ __resetEffectBreakersForTest();
720
+ const mockIp = makeMockInterpreter({
721
+ ANYTHING_SEARCH: {
722
+ ok: true,
723
+ via: 'hbcli-anything',
724
+ verdict: 'hit',
725
+ evidence: '[实时API:hbcli-anything@ts]',
726
+ latencyMs: 1
727
+ }
728
+ });
729
+ const outMock = await mockIp({
730
+ effect: 'ANYTHING_SEARCH',
731
+ params: {
732
+ keyword: '大理'
733
+ }
734
+ });
735
+ assert.equal(outMock.result.verdict, 'hit', 'mock 夹具回放原样');
736
+ assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注');
737
+ const outMockBad = await mockIp({
738
+ effect: 'SESSION_LOGIN',
739
+ params: {}
740
+ });
741
+ assert.equal(outMockBad.result, null, '未登记夹具 → result=null');
742
+ assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝');
743
+ console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK');
744
+ console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)');
472
745
 
473
746
 
474
747
  //# sourceURL=ts/scripts/effect-tests.ts
@@ -1,15 +1,17 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { fork } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { BRIDGE_PORTS, EXTENSION_ID, EXTENSION_ID_STORE, EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE, EXTENSION_ORIGINS, EXTENSION_STORE_URL, createSessionBridge, __resetSessionBridgeForTest, __setSessionBridgeForTest } from '../capabilities/session/extension-bridge.js';
8
+ import { BRIDGE_PORTS, EXTENSION_ID, EXTENSION_ID_STORE, EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE, EXTENSION_ORIGINS, EXTENSION_STORE_URL, createSessionBridge, localExtensionInstalled, needsExtensionSummary, __resetSessionBridgeForTest, __setSessionBridgeForTest } from '../capabilities/session/extension-bridge.js';
9
9
  import { classifyBridgeFailure, extensionCookieNames, extensionSearchJob } from '../capabilities/session/extension-channel.js';
10
- import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch } from '../capabilities/session-search.js';
10
+ import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
11
11
  import { sessionLogin } from '../capabilities/session-login.js';
12
12
  import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
13
+ import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST, buildHotelEntryUrl } from '../capabilities/session/adapters/ctrip-hotel.js';
14
+ import { TRAIN_NETWORK_HINTS, TRAIN_SITE_HOST } from '../capabilities/session/adapters/rail-12306.js';
13
15
  import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
14
16
  const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
15
17
  const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
@@ -161,11 +163,14 @@ async function main() {
161
163
  ].sort((a, b)=>a - b));
162
164
  assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
163
165
  });
164
- await check('防漂移:content_scripts 双 world 只挂 flights.ctrip.com(MAIN 嗅探 + ISOLATED 桥)', ()=>{
166
+ await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306(MAIN 嗅探 + ISOLATED 桥;2026-09-03 酒/火实装)', ()=>{
165
167
  assert.equal(manifest.content_scripts.length, 2);
166
168
  for (const cs of manifest.content_scripts){
167
169
  assert.deepEqual(cs.matches, [
168
- 'https://flights.ctrip.com/*'
170
+ 'https://flights.ctrip.com/*',
171
+ `https://${HOTEL_SITE_HOST}/*`,
172
+ `https://${TRAIN_SITE_HOST}/*`,
173
+ 'https://www.12306.cn/*'
169
174
  ]);
170
175
  assert.equal(cs.run_at, 'document_start');
171
176
  }
@@ -182,19 +187,42 @@ async function main() {
182
187
  const backgroundJs = read('background.js');
183
188
  const contentMainJs = read('content-main.js');
184
189
  const contentBridgeJs = read('content-bridge.js');
190
+ await check('版本跟随主版本:manifest version_name = package.json version;version = 四段投影(0.0.1-rc.N → 0.0.1.N,founder 2026-09-03 拍板)', ()=>{
191
+ const pkgVersion = JSON.parse(readFileSync(join(EXT_DIR, '..', 'package.json'), 'utf8')).version;
192
+ assert.equal(manifest.version_name, pkgVersion, 'version_name 应与 gotry 主版本逐字一致');
193
+ const m = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(pkgVersion);
194
+ assert.ok(m, `主版本形态应为 x.y.z-rc.N,实际 ${pkgVersion}`);
195
+ assert.equal(manifest.version, `${m[1]}.${m[2]}.${m[3]}.${m[4]}`, 'version 应为主版本的四段投影(Chrome manifest 只收点分整数)');
196
+ assert.equal(manifest.content_scripts.length, 2);
197
+ });
185
198
  await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', ()=>{
186
199
  for (const name of LOGIN_COOKIE_NAMES){
187
200
  assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
188
201
  }
189
202
  assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
190
203
  });
191
- await check('防漂移:NETWORK_HINTS(Node)= content-main HINT_RE(MAIN-world 嗅探面)', ()=>{
204
+ await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', ()=>{
192
205
  for (const hint of NETWORK_HINTS){
193
206
  assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
194
207
  }
208
+ for (const hint of HOTEL_NETWORK_HINTS){
209
+ assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺酒店 hint ${hint.source}`);
210
+ }
211
+ for (const hint of TRAIN_NETWORK_HINTS){
212
+ assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺火车 hint ${hint.source}`);
213
+ }
214
+ assert.ok(contentMainJs.includes('12306'), 'content-main.js 应感知 12306 页域(火车嗅探作用域)');
215
+ assert.ok(contentMainJs.includes('hotels\\.ctrip\\.com'), 'content-main.js 应感知酒店页域(形状嗅探兜底的作用域)');
216
+ });
217
+ await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', ()=>{
218
+ assert.ok(stripRe(contentMainJs).includes('"hotelList"|"hotelMatchInfos"|"hotelName"'), '形状嗅探签名两侧必须逐字一致');
195
219
  });
196
- await check('防漂移:检索/登录 URL 面(background 只允许 flights.ctrip.com;ISOLATED 桥转发两类事件)', ()=>{
197
- assert.ok(backgroundJs.includes('https://flights.ctrip.com/'));
220
+ await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', ()=>{
221
+ assert.ok(backgroundJs.includes('https://flights.ctrip.com/'), 'background 应含机票检索白名单');
222
+ assert.ok(backgroundJs.includes(`https://${HOTEL_SITE_HOST}/`), 'background 应含酒店检索白名单(2026-09-03 实装)');
223
+ assert.ok(backgroundJs.includes('https://kyfw.12306.cn/'), 'background 应含火车检索白名单(2026-09-03 实装)');
224
+ assert.ok(backgroundJs.includes("'ctrip-hotel'"), 'background SITES 应注册 ctrip-hotel');
225
+ assert.ok(backgroundJs.includes("'train-12306'"), 'background SITES 应注册 train-12306');
198
226
  assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
199
227
  assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
200
228
  assert.ok(contentBridgeJs.includes('gotry-page'));
@@ -453,16 +481,43 @@ async function main() {
453
481
  ]
454
482
  };
455
483
  if (job.kind === 'search') {
456
- assert.ok(String(job.url).startsWith('https://flights.ctrip.com/'));
484
+ const u = String(job.url);
485
+ const isHotel = u.startsWith(`https://${HOTEL_SITE_HOST}/`);
486
+ const isTrain = u.startsWith('https://kyfw.12306.cn/');
487
+ assert.ok(u.startsWith('https://flights.ctrip.com/') || isHotel || isTrain, `search job 只允许已注册站点域,实际 ${u}`);
457
488
  return {
458
489
  ok: true,
459
490
  kind: 'search',
460
- body: JSON.stringify({
491
+ body: isHotel ? JSON.stringify({
492
+ data: {
493
+ hotelList: [
494
+ {
495
+ hotelId: 442516,
496
+ hotelName: 'Hotel X',
497
+ star: 5,
498
+ commentScore: 4.7,
499
+ priceInfo: {
500
+ avgPrice: 680
501
+ }
502
+ }
503
+ ]
504
+ }
505
+ }) : isTrain ? JSON.stringify({
506
+ data: {
507
+ result: [
508
+ '|预订|24000000G1375|G1375|SHH|KMM|SHH|KMM|07:35|15:27|07:52|Y|yp|x|x|loc|01|02|Y|0|--|--|--|--|--|--|有|--|--|有|有|有|有|--|ex|st|'
509
+ ],
510
+ map: {
511
+ SHH: '上海南',
512
+ KMM: '昆明'
513
+ }
514
+ }
515
+ }) : JSON.stringify({
461
516
  data: {
462
517
  flightItineraryList: []
463
518
  }
464
519
  }),
465
- title: '机票列表'
520
+ title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表'
466
521
  };
467
522
  }
468
523
  return {
@@ -491,6 +546,40 @@ async function main() {
491
546
  assert.ok(search.ok);
492
547
  assert.equal(search.ok ? search.timedOut : true, false);
493
548
  assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
549
+ const hotelLane = await extensionSearchJob({
550
+ site: 'ctrip-hotel',
551
+ url: `https://${HOTEL_SITE_HOST}/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03`,
552
+ timeoutMs: 3_000
553
+ });
554
+ assert.ok(hotelLane.ok, '酒店 search job 应放行(per-site 白名单)');
555
+ assert.ok(hotelLane.ok ? hotelLane.body.includes('hotelList') : false);
556
+ __resetRateLimiterForTest();
557
+ const hotelSession = await sessionHotelSearch({
558
+ to: '迪拜',
559
+ cityId: 220,
560
+ checkIn: '2026-12-01',
561
+ checkOut: '2026-12-03',
562
+ timeoutMs: 3_000
563
+ });
564
+ assert.equal(hotelSession.verdict, 'hit', `酒店会话检索应 hit,实际 ${hotelSession.verdict}:${hotelSession.error ?? ''}`);
565
+ assert.ok(hotelSession.evidence.includes('[会话:ctrip-hotel@'), '酒店证据链 [会话:ctrip-hotel@ts]');
566
+ assert.equal(hotelSession.hotels?.[0]?.name, 'Hotel X');
567
+ assert.equal(hotelSession.hotels?.[0]?.price, 680);
568
+ assert.ok(hotelSession.hotels?.[0]?.jumpUrl?.includes('/hotel/442516'), 'jumpUrl 由 hotelId 构造(预订由人在落地页完成)');
569
+ __resetRateLimiterForTest();
570
+ const trainSession = await sessionTrainSearch({
571
+ from: '上海',
572
+ to: '昆明',
573
+ date: '2026-12-01',
574
+ timeoutMs: 3_000
575
+ });
576
+ assert.equal(trainSession.verdict, 'hit', `火车会话检索应 hit,实际 ${trainSession.verdict}:${trainSession.error ?? ''}`);
577
+ assert.ok(trainSession.evidence.includes('[会话:train-12306@'), '火车证据链 [会话:train-12306@ts]');
578
+ assert.ok(trainSession.evidence.includes('公开查询面'), '证据链标注公开查询面(无登录闸的诚实口径)');
579
+ assert.equal(trainSession.trains?.[0]?.trainCode, 'G1375');
580
+ assert.equal(trainSession.trains?.[0]?.depTime, '07:35');
581
+ assert.equal(trainSession.trains?.[0]?.durationMin, 472);
582
+ assert.ok(trainSession.trains?.[0]?.jumpUrl?.includes('kyfw.12306.cn'), 'jumpUrl=查询落地页(人选车完成预订)');
494
583
  const login = await sessionLogin({
495
584
  site: 'ctrip-flight'
496
585
  });
@@ -558,6 +647,35 @@ async function main() {
558
647
  assert.equal(e.quota_disposition, 'no_spend_waiting_user');
559
648
  assert.equal(e.retry_allowed, false);
560
649
  });
650
+ await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', ()=>{
651
+ const s = needsExtensionSummary({
652
+ localInstalled: true
653
+ });
654
+ assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引');
655
+ assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在');
656
+ });
657
+ await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', ()=>{
658
+ const s = needsExtensionSummary({
659
+ localInstalled: false
660
+ });
661
+ assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)');
662
+ assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样');
663
+ assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)');
664
+ assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引');
665
+ });
666
+ await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', ()=>{
667
+ const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-'));
668
+ assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位');
669
+ mkdirSync(join(fakeHome, '.gotry', 'extension'), {
670
+ recursive: true
671
+ });
672
+ writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8');
673
+ assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true');
674
+ rmSync(fakeHome, {
675
+ recursive: true,
676
+ force: true
677
+ });
678
+ });
561
679
  await b.close();
562
680
  console.log(`\n§38 extension bridge: ${passed} 段全绿`);
563
681
  }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { BOOKABLE_FACT_SCHEMA, checkSameDayDeparture, dedupeFacts, detectBacktrack, factsFromFlyai, flightClaimVerdict, itineraryInvariants, latestFactsForRouteDate, negativeFact, renderConnection, renderFlightFact, renderNightsLine, renderPolicyFact } from '../src/bookable-facts.js';
3
+ import { BOOKABLE_FACT_SCHEMA, checkSameDayDeparture, dedupeFacts, detectBacktrack, factsFromFlyai, factsFromHotel, flightClaimVerdict, itineraryInvariants, latestFactsForRouteDate, negativeFact, renderConnection, renderFlightFact, renderHotelFact, renderNightsLine, renderPolicyFact } from '../src/bookable-facts.js';
4
4
  import { ARTIFACT_GATE_SCHEMA, gateArtifact } from '../src/artifact-gate.js';
5
5
  let pass = 0;
6
6
  let fail = 0;
@@ -311,7 +311,96 @@ const goodReport = gateArtifact(goodArtifact, registry, map, {
311
311
  });
312
312
  assert(goodReport.verdict === 'pass' && goodReport.presentation === 'verified_itinerary_allowed' && goodReport.traceable === 6 && goodReport.violations.length === 0, `good artifact:6/6 航班 claim 全回溯,闸 pass(违例 ${goodReport.violations.length})`);
313
313
  assert(goodFlights.every((f)=>f.bookability === 'bookable_exact_date' && f.query_id.includes('flyai:flight:')), 'good artifact 的每个可下单事实均可回溯到 tool result/query id(验收⑧)');
314
- console.log(`\nFACT GATE TESTS: ${pass} pass, ${fail} fail`);
314
+ {
315
+ const fetchedAt = '2026-09-04T00:00:00.000Z';
316
+ assert(factsFromHotel({
317
+ source: 'flyai-hotel',
318
+ destination: '大理',
319
+ verdict: 'hit',
320
+ options: 9,
321
+ evidence: 'e',
322
+ fetchedAt
323
+ }).length === 0, '摸底(无档期)酒店检索不落账(无 exact-date 语义)');
324
+ assert(factsFromHotel({
325
+ source: 'flyai-hotel',
326
+ destination: '大理',
327
+ checkIn: '2026-10-01',
328
+ checkOut: '2026-10-03',
329
+ verdict: 'needs-setup',
330
+ options: 0,
331
+ evidence: 'e',
332
+ fetchedAt
333
+ }).length === 0 && factsFromHotel({
334
+ source: 'flyai-hotel',
335
+ destination: '大理',
336
+ checkIn: '2026-10-01',
337
+ checkOut: '2026-10-03',
338
+ verdict: 'error',
339
+ options: 0,
340
+ evidence: 'e',
341
+ fetchedAt
342
+ }).length === 0, 'needs-setup/error 传输失败永不落负事实(ADR-19 不变量)');
343
+ const hitFacts = factsFromHotel({
344
+ source: 'flyai-hotel',
345
+ destination: '大理',
346
+ checkIn: '2026-10-01',
347
+ checkOut: '2026-10-03',
348
+ verdict: 'hit',
349
+ options: 9,
350
+ evidence: 'e',
351
+ fetchedAt
352
+ });
353
+ const missFacts = factsFromHotel({
354
+ source: 'session:ctrip-hotel',
355
+ destination: '大理',
356
+ checkIn: '2026-10-01',
357
+ checkOut: '2026-10-03',
358
+ verdict: 'miss',
359
+ options: 0,
360
+ evidence: 'e',
361
+ fetchedAt
362
+ });
363
+ assert(hitFacts.length === 1 && hitFacts[0].bookability === 'bookable_exact_date' && hitFacts[0].options_masked === 9, 'exact-date hit → 正事实(在架家数,无数字价字段)');
364
+ assert(!('price' in hitFacts[0]), '酒店事实无 price 字段(打码价保真,不落数字价)');
365
+ assert(missFacts.length === 1 && missFacts[0].bookability === 'unavailable_exact_date', 'exact-date miss → 负事实');
366
+ const hotelLine = renderHotelFact(hitFacts[0]);
367
+ assert(hotelLine.includes(`fact:${hitFacts[0].fact_id}`), 'renderHotelFact 行内嵌 fact 锚点');
368
+ const anchoredArtifact = [
369
+ '## 住宿',
370
+ hotelLine
371
+ ].join('\n');
372
+ const anchoredReport = gateArtifact(anchoredArtifact, [
373
+ ...hitFacts
374
+ ], map, {
375
+ trip_year: 2026
376
+ });
377
+ assert(anchoredReport.verdict === 'pass' && anchoredReport.traceable === 1, `锚点酒店行 → 锚点确定性回溯 pass(实际 ${anchoredReport.verdict}/traceable=${anchoredReport.traceable})`);
378
+ const forged = anchoredArtifact.replace(hitFacts[0].fact_id, '0123456789abcdef');
379
+ const forgedReport = gateArtifact(forged, [
380
+ ...hitFacts
381
+ ], map, {
382
+ trip_year: 2026
383
+ });
384
+ assert(forgedReport.violations.some((v)=>v.kind === 'fact_anchor_unknown'), '手改/伪造锚点 → fact_anchor_unknown');
385
+ const claimHit = gateArtifact('## 住宿\n- 大理 2026-10-01→2026-10-03 洱海民宿有房可订', [
386
+ ...hitFacts
387
+ ], map, {
388
+ trip_year: 2026
389
+ });
390
+ assert(claimHit.verdict === 'pass' && claimHit.traceable === 1, '启发式酒店 claim 命中在册 hit → traceable');
391
+ const claimMiss = gateArtifact('## 住宿\n- 大理 2026-10-01→2026-10-03 洱海民宿有房可订 ✓', [
392
+ ...missFacts
393
+ ], map, {
394
+ trip_year: 2026
395
+ });
396
+ assert(claimMiss.violations.some((v)=>v.kind === 'not_in_source'), 'exact-date miss 在册却写有房 → not_in_source');
397
+ assert(claimMiss.violations.some((v)=>v.kind === 'unconditional_check'), '对未核验酒店用 ✓ → unconditional_check');
398
+ const noCtx = gateArtifact('## 住宿\n- 住宿:湖景房有房可订 ✓', [], map, {
399
+ trip_year: 2026
400
+ });
401
+ assert(noCtx.violations.some((v)=>v.kind === 'unverifiable_hotel_claim'), '缺目的地上下文 → unverifiable_hotel_claim(fail closed)');
402
+ console.log(` ok - §10 酒店事实闸(D-26)八断言完成`);
403
+ }console.log(`\nFACT GATE TESTS: ${pass} pass, ${fail} fail`);
315
404
  if (fail > 0) process.exit(1);
316
405
 
317
406
 
@@ -85,11 +85,27 @@ const f = await flyaiSearch({
85
85
  assert.equal(f.ok, false);
86
86
  assert.equal(f.verdict, 'error', '非零退出应判 error');
87
87
  console.log('4. 非零退出 → error OK');
88
+ const trialBin = join(tmp, 'flyai-trial');
89
+ await writeFile(trialBin, `#!/bin/sh\necho 'search-hotel: MCP HTTP 429: Body: {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Trial limit reached. Please visit the console at flyai.open.fliggy.com to get a formal API Key"}}' >&2\nexit 1\n`, {
90
+ mode: 0o755
91
+ });
92
+ const t = await flyaiSearch({
93
+ ...base,
94
+ cliBin: trialBin,
95
+ timeoutMs: 5000
96
+ });
97
+ assert.equal(t.ok, false);
98
+ assert.equal(t.verdict, 'needs-setup', `429 达限应判 needs-setup,实际 ${t.verdict}`);
99
+ assert.match(t.setup ?? '', /FLYAI_API_KEY/, 'setup 指引带 FLYAI_API_KEY 配置路径');
100
+ assert.match(t.setup ?? '', /flyai\.open\.fliggy\.com/, 'setup 指引带控制台入口');
101
+ assert.match(t.setup ?? '', /请勿重试|勿重试|不要重试/, 'setup 明示本会话勿重试');
102
+ assert.match(t.error ?? '', /429|Trial limit/i, 'error 保留上游 429 原话');
103
+ assert.match(t.evidence, /\[实时API:flyai@error@/, '证据链标注');
88
104
  await rm(tmp, {
89
105
  recursive: true,
90
106
  force: true
91
107
  });
92
- console.log('FLYAI TESTS: 4/4 OK(离线假 CLI:Sentinel→error / 空 itemList→miss / 命中→hit / exit≠0→error)');
108
+ console.log('FLYAI TESTS: 5/5 OK(离线假 CLI:Sentinel→error / 空 itemList→miss / 命中→hit / exit≠0→error / 429 达限→needs-setup)');
93
109
 
94
110
 
95
111
  //# sourceURL=ts/scripts/flyai-tests.ts
@@ -144,9 +144,7 @@ async function main() {
144
144
  const tool = registered.find((t)=>t.name === 'gotry_hotel_search');
145
145
  assert.ok(tool, 'gotry_hotel_search 应已注册');
146
146
  const obs = await tool.execute({
147
- query: {
148
- destination: '深圳'
149
- }
147
+ destination: '深圳'
150
148
  }, null);
151
149
  assert.equal(obs.ok, true, '工具面应返回 ok 包络(永不抛错契约)');
152
150
  assert.ok(obs.via === 'hbcli-error' || obs.via === 'hbcli-realtime', `工具面 via 应与渠道一致,实际 ${obs.via}`);