@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
@@ -0,0 +1,252 @@
1
+ import { recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
2
+ function timeout(ms) {
3
+ return AbortSignal.timeout(ms);
4
+ }
5
+ async function fetchOk(url, what, ms = 8000) {
6
+ const t0 = Date.now();
7
+ try {
8
+ const res = await fetch(url, {
9
+ signal: timeout(ms),
10
+ headers: {
11
+ Accept: 'application/json'
12
+ }
13
+ });
14
+ if (res.ok) return {
15
+ ok: true,
16
+ ms: Date.now() - t0
17
+ };
18
+ return {
19
+ ok: false,
20
+ reason: `${what}-http-${res.status}`,
21
+ ms: Date.now() - t0
22
+ };
23
+ } catch (err) {
24
+ const msg = err instanceof Error ? err.name : String(err);
25
+ return {
26
+ ok: false,
27
+ reason: msg === 'TimeoutError' ? 'timeout' : 'unreachable',
28
+ ms: Date.now() - t0
29
+ };
30
+ }
31
+ }
32
+ function spawnOk(cmd, args, what, ms = 10000) {
33
+ return async ()=>{
34
+ const t0 = Date.now();
35
+ try {
36
+ const { spawn } = await import('node:child_process');
37
+ const child = spawn(cmd, args, {
38
+ stdio: [
39
+ 'ignore',
40
+ 'ignore',
41
+ 'ignore'
42
+ ]
43
+ });
44
+ const code = await new Promise((resolve, reject)=>{
45
+ const timer = setTimeout(()=>{
46
+ child.kill('SIGKILL');
47
+ reject(new Error('timeout'));
48
+ }, ms);
49
+ child.on('exit', (c)=>{
50
+ clearTimeout(timer);
51
+ resolve(c ?? 1);
52
+ });
53
+ child.on('error', (e)=>{
54
+ clearTimeout(timer);
55
+ reject(e);
56
+ });
57
+ });
58
+ return code === 0 ? {
59
+ ok: true,
60
+ ms: Date.now() - t0
61
+ } : {
62
+ ok: false,
63
+ reason: `${what}-exit-${code}`,
64
+ ms: Date.now() - t0
65
+ };
66
+ } catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ return {
69
+ ok: false,
70
+ reason: msg === 'timeout' ? 'timeout' : 'spawn-failed',
71
+ ms: Date.now() - t0
72
+ };
73
+ }
74
+ };
75
+ }
76
+ export function defaultProbes() {
77
+ return [
78
+ {
79
+ channel: 'hbcli-hotel',
80
+ probeable: true,
81
+ run: spawnOk('hbcli', [
82
+ 'auth',
83
+ 'whoami'
84
+ ], 'auth-denied')
85
+ },
86
+ {
87
+ channel: 'open-meteo',
88
+ probeable: true,
89
+ run: ()=>fetchOk('https://api.open-meteo.com/v1/forecast?latitude=25.2048&longitude=55.2708&current=temperature_2m', 'weather')
90
+ },
91
+ {
92
+ channel: 'opensky',
93
+ probeable: true,
94
+ run: ()=>fetchOk('https://opensky-network.org/api/states/all?lamin=24.0&lomin=53.0&lamax=26.5&lomax=56.5', 'flights', 12000)
95
+ },
96
+ {
97
+ channel: 'flyai',
98
+ probeable: false,
99
+ skipReason: 'anonymous-shared-quota——探针烧额度弊大于利;--only flyai 显式选入',
100
+ run: async ()=>({
101
+ ok: true,
102
+ ms: 0
103
+ })
104
+ },
105
+ {
106
+ channel: 'session:ctrip-flight',
107
+ probeable: false,
108
+ skipReason: '需用户 Chrome 会话,无头不可探',
109
+ run: async ()=>({
110
+ ok: true,
111
+ ms: 0
112
+ })
113
+ },
114
+ {
115
+ channel: 'session:ctrip-hotel',
116
+ probeable: false,
117
+ skipReason: '需用户 Chrome 会话,无头不可探',
118
+ run: async ()=>({
119
+ ok: true,
120
+ ms: 0
121
+ })
122
+ },
123
+ {
124
+ channel: 'session:12306-train',
125
+ probeable: false,
126
+ skipReason: '需用户 Chrome 会话,无头不可探',
127
+ run: async ()=>({
128
+ ok: true,
129
+ ms: 0
130
+ })
131
+ },
132
+ {
133
+ channel: 'anything-geo',
134
+ probeable: false,
135
+ skipReason: '静态包通道,无远端可探',
136
+ run: async ()=>({
137
+ ok: true,
138
+ ms: 0
139
+ })
140
+ },
141
+ {
142
+ channel: 'static-hotel',
143
+ probeable: false,
144
+ skipReason: '静态包通道,无远端可探',
145
+ run: async ()=>({
146
+ ok: true,
147
+ ms: 0
148
+ })
149
+ },
150
+ {
151
+ channel: 'web-read',
152
+ probeable: false,
153
+ skipReason: '依赖 agent-reach 装配态,探测意义低;--only 显式选入可探',
154
+ run: async ()=>({
155
+ ok: true,
156
+ ms: 0
157
+ })
158
+ }
159
+ ];
160
+ }
161
+ export function evaluateProbeResults(results, priorDown, now = new Date()) {
162
+ const events = [];
163
+ for (const { probe, result } of results){
164
+ if (!probe.probeable) continue;
165
+ if (result.ok) {
166
+ if (priorDown.has(probe.channel)) {
167
+ events.push({
168
+ channel: probe.channel,
169
+ state: 'ok',
170
+ at: now.toISOString()
171
+ });
172
+ }
173
+ continue;
174
+ }
175
+ events.push({
176
+ channel: probe.channel,
177
+ state: 'down',
178
+ reason: result.reason,
179
+ at: now.toISOString()
180
+ });
181
+ }
182
+ return events;
183
+ }
184
+ export function parseArgs(argv) {
185
+ let stateRoot;
186
+ let only;
187
+ let dryRun = false;
188
+ let list = false;
189
+ for(let i = 0; i < argv.length; i += 1){
190
+ const a = argv[i];
191
+ if (a === '--state-root') stateRoot = argv[++i];
192
+ else if (a === '--only') only = (argv[++i] ?? '').split(',').map((s)=>s.trim()).filter(Boolean);
193
+ else if (a === '--dry-run') dryRun = true;
194
+ else if (a === '--list') list = true;
195
+ }
196
+ return {
197
+ stateRoot,
198
+ only,
199
+ dryRun,
200
+ list
201
+ };
202
+ }
203
+ export async function main(argv) {
204
+ const args = parseArgs(argv);
205
+ const all = defaultProbes();
206
+ if (args.list) {
207
+ for (const p of all){
208
+ console.log(`${p.probeable ? 'probe' : 'skip '} ${p.channel}${p.skipReason ? ` # ${p.skipReason}` : ''}`);
209
+ }
210
+ return 0;
211
+ }
212
+ const selected = args.only?.length ? all.filter((p)=>args.only.includes(p.channel)) : all.filter((p)=>p.probeable);
213
+ if (selected.length === 0) {
214
+ console.error('[channel-probe] --only 未匹配任何通道;--list 查看探测面');
215
+ return 2;
216
+ }
217
+ const results = [];
218
+ for (const probe of selected){
219
+ const result = await probe.run();
220
+ results.push({
221
+ probe,
222
+ result
223
+ });
224
+ console.log(`[channel-probe] ${result.ok ? 'ok ' : 'FAIL'} ${probe.channel} (${result.ms}ms)${result.ok ? '' : ` reason=${result.reason}`}`);
225
+ }
226
+ const failures = results.filter((r)=>!r.result.ok).length;
227
+ if (args.dryRun || !args.stateRoot) {
228
+ if (!args.stateRoot) console.log('[channel-probe] 未给 --state-root:只探测不落账(等价 --dry-run)');
229
+ return failures > 0 ? 1 : 0;
230
+ }
231
+ let priorDown = new Set();
232
+ try {
233
+ const latest = await readLatestChannelEvents(args.stateRoot);
234
+ priorDown = new Set([
235
+ ...latest.entries()
236
+ ].filter(([, ev])=>ev.state === 'down' || ev.state === 'cooldown').map(([ch])=>ch));
237
+ } catch {}
238
+ for (const ev of evaluateProbeResults(results, priorDown)){
239
+ await recordChannelEvent(args.stateRoot, ev);
240
+ console.log(`[channel-probe] 记录 ${ev.state} ${ev.channel}${ev.reason ? `(${ev.reason})` : ''}`);
241
+ }
242
+ return failures > 0 ? 1 : 0;
243
+ }
244
+ if (import.meta.url === `file://${process.argv[1]}`) {
245
+ main(process.argv.slice(2)).then((code)=>process.exit(code)).catch((err)=>{
246
+ console.error('[channel-probe] fatal:', err instanceof Error ? err.message : err);
247
+ process.exit(1);
248
+ });
249
+ }
250
+
251
+
252
+ //# sourceURL=ts/scripts/channel-probe.ts
@@ -0,0 +1,262 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { CHANNELS, channelsForIntent, routingAdvice, renderRoutingCard, toolRoutingHeadline, INTENT_LABELS } from '../capabilities/channel-registry.js';
6
+ import { noteChannelVerdict, channelState, clearChannel, resetChannelHealth, recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
7
+ import { runDoctorChecks } from '../capabilities/doctor.js';
8
+ import { apply } from '../src/index.js';
9
+ const NOW = 1_700_000_000_000;
10
+ const QUOTA_CLASSES = [
11
+ 'user-session',
12
+ 'user-key',
13
+ 'anonymous-trial',
14
+ 'free-public',
15
+ 'static'
16
+ ];
17
+ const TIERS = [
18
+ 'realtime-api',
19
+ 'session',
20
+ 'best-effort',
21
+ 'static'
22
+ ];
23
+ const INTENTS = Object.keys(INTENT_LABELS);
24
+ const ids = new Set();
25
+ for (const c of CHANNELS){
26
+ assert.ok(!ids.has(c.id), `通道 id 重复:${c.id}`);
27
+ ids.add(c.id);
28
+ assert.ok(QUOTA_CLASSES.includes(c.quotaClass), `${c.id} quotaClass 越界`);
29
+ assert.ok(TIERS.includes(c.tier), `${c.id} tier 越界`);
30
+ assert.ok(c.intents.length > 0 && c.intents.every((i)=>INTENTS.includes(i)), `${c.id} intents 越界`);
31
+ assert.ok(c.tool.startsWith('gotry_'), `${c.id} 工具名形态`);
32
+ if (c.quotaClass === 'static') assert.equal(c.routable, false, '静态包不进模型可选路由');
33
+ }
34
+ assert.ok(CHANNELS.length >= 9, '注册表覆盖面(≥9 通道)');
35
+ console.log('1. 注册表封闭性 OK');
36
+ resetChannelHealth();
37
+ assert.deepEqual(channelsForIntent('search-flight').map((c)=>c.id), [
38
+ 'flyai',
39
+ 'session:ctrip-flight',
40
+ 'web-read'
41
+ ], '机票顺位');
42
+ assert.deepEqual(channelsForIntent('search-train').map((c)=>c.id), [
43
+ 'flyai',
44
+ 'session:12306-train',
45
+ 'web-read'
46
+ ], '火车顺位');
47
+ assert.deepEqual(channelsForIntent('search-hotel').map((c)=>c.id), [
48
+ 'flyai',
49
+ 'hbcli-hotel',
50
+ 'session:ctrip-hotel',
51
+ 'web-read'
52
+ ], '酒店顺位');
53
+ assert.deepEqual(channelsForIntent('weather').map((c)=>c.id), [
54
+ 'open-meteo'
55
+ ], '单通道意图');
56
+ console.log('2. 意图顺位(证据级 > 效率)OK');
57
+ const base = routingAdvice('search-flight');
58
+ assert.equal(base.alternatives[0].channel, 'flyai', '健康时 flyai 首荐(零 setup 摩擦)');
59
+ assert.ok(base.alternatives[0].why.length > 4 && (base.alternatives[0].setup?.length ?? 0) > 0, 'why/setup 齐备');
60
+ noteChannelVerdict('flyai', 'needs-setup', {
61
+ now: NOW
62
+ });
63
+ const afterDown = routingAdvice('search-flight', {
64
+ now: NOW
65
+ });
66
+ assert.ok(!afterDown.alternatives.some((a)=>a.channel === 'flyai'), 'flyai 达限后从建议表排除(#108 病灶)');
67
+ assert.equal(afterDown.alternatives[0].channel, 'session:ctrip-flight', 'session 升首荐(优先级=健康面投影)');
68
+ const excl = routingAdvice('search-flight', {
69
+ excludeChannel: 'session:ctrip-flight',
70
+ now: NOW
71
+ });
72
+ assert.ok(!excl.alternatives.some((a)=>a.channel === 'session:ctrip-flight'), '发起通道被排除');
73
+ assert.equal(routingAdvice('search-flight', {
74
+ limit: 1,
75
+ now: NOW
76
+ }).alternatives.length, 1, 'limit 截断');
77
+ noteChannelVerdict('flyai', 'hit', {
78
+ now: NOW
79
+ });
80
+ assert.equal(routingAdvice('search-flight', {
81
+ now: NOW
82
+ }).alternatives[0].channel, 'flyai', 'hit 即恢复(#107:补 key 不被陈旧状态锁死)');
83
+ console.log('3. routingAdvice(down 排除/发起排除/limit/hit 恢复)OK');
84
+ resetChannelHealth();
85
+ assert.ok(noteChannelVerdict('x', 'needs-setup', {
86
+ now: NOW
87
+ }), 'needs-setup → down 事件');
88
+ assert.equal(channelState('x', NOW)?.state, 'down');
89
+ assert.equal(noteChannelVerdict('x', 'challenged', {
90
+ now: NOW
91
+ })?.reason, 'challenged', 'challenged → down');
92
+ assert.equal(noteChannelVerdict('x', 'needs-login', {
93
+ now: NOW
94
+ })?.reason, 'needs-login', 'needs-login → down');
95
+ assert.equal(noteChannelVerdict('x', 'needs-extension', {
96
+ now: NOW
97
+ })?.reason, 'needs-extension', 'needs-extension → down');
98
+ assert.equal(noteChannelVerdict('x', 'miss', {
99
+ now: NOW
100
+ }), undefined, '业务 miss ≠ 通道故障');
101
+ assert.equal(noteChannelVerdict('x', 'error', {
102
+ now: NOW
103
+ }), undefined, '业务 error ≠ 通道故障');
104
+ clearChannel('x');
105
+ assert.equal(channelState('x', NOW), undefined, '清除后无状态');
106
+ const cd = noteChannelVerdict('y', 'cooldown', {
107
+ now: NOW,
108
+ cooldownMs: 30_000
109
+ });
110
+ assert.equal(cd?.state, 'cooldown', 'cooldown → cooldown 事件');
111
+ assert.equal(channelState('y', NOW + 29_999)?.state, 'cooldown', '冷却未满仍在');
112
+ assert.equal(channelState('y', NOW + 30_001), undefined, '冷却满惰性过期');
113
+ console.log('4. noteChannelVerdict 映射闭集 OK');
114
+ const card1 = renderRoutingCard(new Date(NOW));
115
+ const card2 = renderRoutingCard(new Date(NOW));
116
+ assert.equal(card1, card2, '同刻渲染确定');
117
+ assert.match(card1, /检索通道顺位/, '卡头');
118
+ assert.match(card1, /verdict≠hit/, '改道规则在卡内');
119
+ assert.match(card1, /- 机票: .+gotry_flyai_search.+→.+gotry_session_search/, '机票顺位行');
120
+ assert.match(card1, /- 火车: .+12306 公开查询面.+kind=train/, '火车行带 kind 消歧');
121
+ assert.match(card1, /匿名试用=首次体验导流/, '额度口径脚注(D-7)');
122
+ assert.ok(!card1.includes('- 天气'), '单通道意图不上卡');
123
+ console.log('5. persona 路由卡 OK');
124
+ const stateRoot = await mkdtemp(join(tmpdir(), 'gotry-channel-test-'));
125
+ const realNow = Date.now();
126
+ await recordChannelEvent(stateRoot, {
127
+ channel: 'flyai',
128
+ state: 'down',
129
+ reason: 'needs-setup',
130
+ at: new Date(realNow - 86_400_000).toISOString()
131
+ });
132
+ await recordChannelEvent(stateRoot, {
133
+ channel: 'flyai',
134
+ state: 'down',
135
+ reason: 'needs-setup',
136
+ at: new Date(realNow).toISOString()
137
+ });
138
+ await recordChannelEvent(stateRoot, {
139
+ channel: 'session:ctrip-flight',
140
+ state: 'cooldown',
141
+ at: new Date(realNow).toISOString()
142
+ });
143
+ const latest = await readLatestChannelEvents(stateRoot);
144
+ assert.equal(latest.get('flyai')?.at, new Date(realNow).toISOString(), '每通道取最新');
145
+ assert.equal(latest.get('session:ctrip-flight')?.state, 'cooldown');
146
+ await recordChannelEvent(stateRoot, {
147
+ channel: 'legacy',
148
+ state: 'down',
149
+ at: new Date(realNow - 40 * 86_400_000).toISOString()
150
+ });
151
+ assert.ok(!(await readLatestChannelEvents(stateRoot)).has('legacy'), '40 天前事件被 limitDays 过滤');
152
+ const raw = `${stateRoot}/gotry-state/channel-health.jsonl`;
153
+ await writeFile(raw, `${await readFile(raw, 'utf-8')}not-json\n{"half":true}\n`, 'utf-8');
154
+ assert.equal((await readLatestChannelEvents(stateRoot)).size, 2, '坏行容忍');
155
+ console.log('6. JSONL 持久面 OK');
156
+ const emptyRepo = join(stateRoot, 'repo');
157
+ const emptyHome = join(stateRoot, 'home');
158
+ await mkdir(emptyRepo, {
159
+ recursive: true
160
+ });
161
+ await mkdir(join(emptyHome, '.gotry'), {
162
+ recursive: true
163
+ });
164
+ const calStatePath = join(emptyHome, '.gotry', 'calendar.json');
165
+ const d1 = await runDoctorChecks({
166
+ repoRoot: emptyRepo,
167
+ homeDir: emptyHome,
168
+ env: {},
169
+ stateRoot
170
+ });
171
+ assert.match(d1.items.find((i)=>i.id === 'flyai').detail, /最近一次试用达限/, '最近达限时间进 doctor(#107 可见性)');
172
+ assert.equal(d1.items.find((i)=>i.id === 'calendar').status, 'ok', 'calendar 默认不挂载=ok(D-9)');
173
+ assert.match(d1.items.find((i)=>i.id === 'calendar').detail, /默认未挂载/, '默认态人话');
174
+ await writeFile(calStatePath, JSON.stringify({
175
+ enabled: true
176
+ }), 'utf-8');
177
+ const d2 = await runDoctorChecks({
178
+ repoRoot: emptyRepo,
179
+ homeDir: emptyHome,
180
+ env: {}
181
+ });
182
+ assert.equal(d2.items.find((i)=>i.id === 'calendar').status, 'degraded', 'setup 开启但未配置 username=degraded');
183
+ assert.match(d2.items.find((i)=>i.id === 'calendar').fix ?? '', /npx gotry setup calendar/, 'fix 指向 setup 状态面');
184
+ const profileDir = join(emptyHome, '.dsh/profiles/web');
185
+ await mkdir(profileDir, {
186
+ recursive: true
187
+ });
188
+ await writeFile(join(profileDir, 'cordis.patch.yml'), '- id: dsh-calendar\n config:\n username: someone\n', 'utf-8');
189
+ const d3 = await runDoctorChecks({
190
+ repoRoot: emptyRepo,
191
+ homeDir: emptyHome,
192
+ env: {}
193
+ });
194
+ assert.equal(d3.items.find((i)=>i.id === 'calendar').status, 'ok', 'setup 开启且已配置=ok');
195
+ const d4 = await runDoctorChecks({
196
+ repoRoot: emptyRepo,
197
+ homeDir: emptyHome,
198
+ env: {}
199
+ });
200
+ assert.ok(!/最近一次试用达限/.test(d4.items.find((i)=>i.id === 'flyai').detail), '无 stateRoot 不读持久面(向后兼容)');
201
+ const mapItem = d4.items.find((i)=>i.id === 'map-tools');
202
+ const askItem = d4.items.find((i)=>i.id === 'ask-user');
203
+ assert.equal(mapItem.status, 'missing', '隔离环境 map-tools=missing(静默剔除面可见)');
204
+ assert.match(mapItem.detail, /静默剔除/, 'map-tools detail 说明静默剔除语义');
205
+ assert.equal(askItem.status, 'missing', '隔离环境 ask-user=missing');
206
+ assert.match(askItem.detail, /散文追问/, 'ask-user detail 说明退化面');
207
+ console.log('7. doctor 集成(flyai 配额可见 + calendar setup 状态面三态 + 宿主插件两态)OK');
208
+ resetChannelHealth();
209
+ const variables = {};
210
+ const registered = [];
211
+ const ctx = {
212
+ tools: {
213
+ register: (t)=>registered.push(t)
214
+ },
215
+ systemPrompt: {
216
+ variable: (name, provider)=>{
217
+ variables[name] = provider;
218
+ }
219
+ },
220
+ on: ()=>()=>{}
221
+ };
222
+ apply(ctx, {
223
+ stateRoot,
224
+ timeoutMs: 30_000,
225
+ hbcliBin: 'hbcli-not-on-path',
226
+ sessionAccess: 'ask'
227
+ });
228
+ assert.equal(typeof variables.channel_routing_card, 'function', 'channel_routing_card 变量已注册');
229
+ assert.match(variables.channel_routing_card(), /检索通道顺位/, '卡片可渲染');
230
+ const pastDate = await registered.find((t)=>t.name === 'gotry_flyai_search').execute({
231
+ kind: 'flight',
232
+ from: '深圳',
233
+ to: '普吉',
234
+ date: '2026-01-01'
235
+ }, null);
236
+ assert.ok(!('routing' in pastDate), '参数级预校验拒绝不带 routing(未发起检索,无改道语义)');
237
+ const legacyBlob = await registered.find((t)=>t.name === 'gotry_flyai_search').execute({
238
+ query: {
239
+ kind: 'flight',
240
+ from: '深圳',
241
+ to: '普吉',
242
+ date: '2026-01-01'
243
+ }
244
+ }, null);
245
+ assert.ok(legacyBlob.ok === false && !('routing' in legacyBlob) && !!legacyBlob.evidence, 'legacy blob → 结构化拒绝(ToolFailure,无 routing)');
246
+ const flyaiTool = registered.find((t)=>t.name === 'gotry_flyai_search');
247
+ const doctorToolDesc = registered.find((t)=>t.name === 'gotry_doctor');
248
+ assert.ok(flyaiTool, 'flyai 工具已注册');
249
+ assert.match(toolRoutingHeadline('gotry_flyai_search'), /机票: 飞猪官方 API\(gotry_flyai_search\) → 携程·用户登录态/, 'flyai 首行卡:机票顺位 flyai 在前(证据级)');
250
+ assert.match(toolRoutingHeadline('gotry_session_search'), /火车: .*12306 公开查询面\(gotry_session_search kind=train\)/, 'session 首行卡带 kind 消歧');
251
+ assert.equal(toolRoutingHeadline('gotry_doctor'), '', '非检索工具无首行卡(原样描述)');
252
+ assert.ok(flyaiTool.description?.startsWith('本工具服务的检索意图与通道顺位'), 'flyai 描述已前置首行卡(接线生效)');
253
+ assert.ok(!doctorToolDesc.description?.startsWith('本工具服务的检索意图'), 'doctor 描述无首行卡(接线不误伤)');
254
+ console.log('8. apply 接线 + 描述首行生成 OK');
255
+ await rm(stateRoot, {
256
+ recursive: true,
257
+ force: true
258
+ });
259
+ console.log('channel-registry-tests: 全部通过');
260
+
261
+
262
+ //# sourceURL=ts/scripts/channel-registry-tests.ts
@@ -0,0 +1,121 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { runDoctorChecks, renderDoctorReportMd, nodeOk } from '../capabilities/doctor.js';
6
+ import { apply } from '../src/index.js';
7
+ const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
8
+ const emptyRepo = join(tmp, 'repo-empty');
9
+ const emptyHome = join(tmp, 'home-empty');
10
+ await mkdir(emptyRepo, {
11
+ recursive: true
12
+ });
13
+ await mkdir(emptyHome, {
14
+ recursive: true
15
+ });
16
+ const r1 = await runDoctorChecks({
17
+ repoRoot: emptyRepo,
18
+ homeDir: emptyHome,
19
+ env: {}
20
+ });
21
+ const byId = (id)=>r1.items.find((i)=>i.id === id);
22
+ assert.ok(byId('agent-reach'), 'agent-reach 项存在');
23
+ assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
24
+ assert.match(byId('agent-reach').fix ?? '', /npx gotry doctor --fix/, 'missing 项带精确补装指引');
25
+ assert.equal(byId('extension').status, 'missing', '空 home 扩展=missing');
26
+ assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
27
+ assert.equal(byId('llm-key').status, 'ok', 'LLM key 恒 ok(让渡面)');
28
+ assert.equal(r1.ok, false, '有 missing 项时报告 ok=false');
29
+ assert.match(r1.summary, /待处理/, 'summary 指明待处理项');
30
+ console.log('1. 空 tmp 环境 → missing/degraded 分级正确,LLM key 让渡 OK');
31
+ const repoPartial = join(tmp, 'repo-partial');
32
+ await mkdir(join(repoPartial, '.venv/bin'), {
33
+ recursive: true
34
+ });
35
+ await writeFile(join(repoPartial, '.venv/bin/python'), '#!/bin/sh\n', {
36
+ mode: 0o755
37
+ });
38
+ const r2a = await runDoctorChecks({
39
+ repoRoot: repoPartial,
40
+ homeDir: emptyHome,
41
+ env: {}
42
+ });
43
+ assert.equal(r2a.items.find((i)=>i.id === 'agent-reach').status, 'degraded', '只 python → degraded(半可用显式区分)');
44
+ const repoFull = join(tmp, 'repo-full');
45
+ await mkdir(join(repoFull, '.venv/bin'), {
46
+ recursive: true
47
+ });
48
+ await writeFile(join(repoFull, '.venv/bin/python'), '#!/bin/sh\n', {
49
+ mode: 0o755
50
+ });
51
+ await writeFile(join(repoFull, '.venv/bin/agent-reach'), '#!/bin/sh\n', {
52
+ mode: 0o755
53
+ });
54
+ const r2b = await runDoctorChecks({
55
+ repoRoot: repoFull,
56
+ homeDir: emptyHome,
57
+ env: {}
58
+ });
59
+ assert.equal(r2b.items.find((i)=>i.id === 'agent-reach').status, 'ok', 'python+agent-reach 齐 → ok');
60
+ console.log('2. .venv 三态(missing/degraded/ok)分级 OK');
61
+ const r3 = await runDoctorChecks({
62
+ repoRoot: emptyRepo,
63
+ homeDir: emptyHome,
64
+ env: {
65
+ FLYAI_API_KEY: 'test-key'
66
+ }
67
+ });
68
+ assert.equal(r3.items.find((i)=>i.id === 'flyai').status, 'ok', '有正式 key → flyai=ok');
69
+ console.log('3. FLYAI_API_KEY 注入 → flyai ok OK');
70
+ assert.equal(nodeOk('22.14.0'), false, '22.14 不足');
71
+ assert.equal(nodeOk('22.15.0'), true, '22.15 达标');
72
+ assert.equal(nodeOk('23.0.0'), true, '23.x 达标');
73
+ console.log('4. nodeOk 边界 OK');
74
+ const md = renderDoctorReportMd(r1);
75
+ assert.match(md, /# GoTry 依赖体检报告/, '报告标题');
76
+ assert.match(md, /`npx gotry doctor --fix`/, '命令类 fix 加反引号');
77
+ assert.match(md, /flyai\.open\.fliggy\.com 控制台申请正式 key/, 'prose 类 fix 原样呈现');
78
+ assert.ok(!md.includes('`到 flyai'), 'prose 类 fix 不应整体包反引号');
79
+ assert.match(md, /LLM key/, '让渡面(LLM key)照常入表');
80
+ console.log('5. 报告渲染 OK');
81
+ const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
82
+ const registered = [];
83
+ const ctx = {
84
+ tools: {
85
+ register: (t)=>registered.push(t)
86
+ },
87
+ systemPrompt: {
88
+ variable: ()=>{}
89
+ },
90
+ on: ()=>()=>{}
91
+ };
92
+ apply(ctx, {
93
+ stateRoot: smokeRoot,
94
+ timeoutMs: 30_000,
95
+ hbcliBin: 'hbcli-not-on-path',
96
+ sessionAccess: 'ask'
97
+ });
98
+ const doctorTool = registered.find((t)=>t.name === 'gotry_doctor');
99
+ assert.ok(doctorTool, 'gotry_doctor 已注册');
100
+ const out = await doctorTool.execute({
101
+ query: {}
102
+ }, null);
103
+ assert.equal(out.ok, true, '体检工具 ok=true(体检本身成功,与依赖是否缺失无关)');
104
+ assert.equal(out.verdict, 'needs-attention', '注入环境外仍能出 verdict(本机真实状态,不判 all-clear)');
105
+ assert.ok(Array.isArray(out.items) && out.items.length >= 6, 'items 齐全');
106
+ assert.ok(out.report_path && out.report_path.includes(smokeRoot), `报告落在 isolated stateRoot: ${out.report_path}`);
107
+ const reportMd = await readFile(out.report_path, 'utf-8');
108
+ assert.match(reportMd, /# GoTry 依赖体检报告/, '落盘报告可预览');
109
+ await rm(tmp, {
110
+ recursive: true,
111
+ force: true
112
+ });
113
+ await rm(smokeRoot, {
114
+ recursive: true,
115
+ force: true
116
+ });
117
+ console.log('6. gotry_doctor 工具面 + 报告落盘 OK');
118
+ console.log('doctor-tests: 全部通过');
119
+
120
+
121
+ //# sourceURL=ts/scripts/doctor-tests.ts