@danceiny/gotry 0.0.1-rc.17 → 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 +185 -148
  2. package/README.zh-CN.md +195 -154
  3. package/bin/gotry-bootstrap.js +246 -26
  4. package/bin/gotry-inner.js +68 -38
  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 +115 -44
  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,189 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { listTurnHandoffTickets, settleTurnHandoffTicket, TURN_HANDOFF_SCHEMA } from '../src/turn-deadline.js';
6
+ const TURN_HANDOFF_TERMINAL_SCHEMA = 'gotry_turn_handoff_terminal.v1';
7
+ const ROOT = join(import.meta.dirname, '..', '..');
8
+ function emitTerminal(outcome) {
9
+ console.log(JSON.stringify(outcome));
10
+ process.exit(outcome.status === 'succeeded' ? 0 : 2);
11
+ }
12
+ function loadTicket(stateRoot, ticketId) {
13
+ try {
14
+ const path = join(stateRoot === '.' ? process.cwd() : stateRoot, 'gotry-state', 'turn-handoffs', `${ticketId}.json`);
15
+ const ticket = JSON.parse(readFileSync(path, 'utf-8'));
16
+ return ticket?.schema === TURN_HANDOFF_SCHEMA ? ticket : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function replay(ticket, stateRoot) {
22
+ const dir = join(stateRoot === '.' ? process.cwd() : stateRoot, 'gotry-state', 'turn-handoffs');
23
+ emitTerminal({
24
+ schema: TURN_HANDOFF_TERMINAL_SCHEMA,
25
+ ticket_id: ticket.id,
26
+ status: ticket.status === 'settled' ? 'succeeded' : 'failed',
27
+ ...ticket.deliverableFile ? {
28
+ deliverable_path: join(dir, ticket.deliverableFile)
29
+ } : {},
30
+ ...ticket.error ? {
31
+ error: ticket.error
32
+ } : {}
33
+ });
34
+ }
35
+ async function runPlanner(ticket) {
36
+ const plannerBin = process.env.GOTRY_HANDOFF_PLANNER_BIN ?? join(ROOT, 'bin', 'gotry-inner.js');
37
+ if (!existsSync(plannerBin)) {
38
+ return {
39
+ ok: false,
40
+ output: '',
41
+ error: `planner binary unavailable: ${plannerBin}`
42
+ };
43
+ }
44
+ const timeoutMs = Number.parseInt(process.env.GOTRY_HANDOFF_PLANNER_TIMEOUT_MS ?? '', 10) || 900_000;
45
+ const dshHome = mkdtempSync(join(tmpdir(), 'gotry-handoff-child-dsh-'));
46
+ const childCwd = mkdtempSync(join(tmpdir(), 'gotry-handoff-child-cwd-'));
47
+ try {
48
+ const childEnv = {
49
+ ...process.env
50
+ };
51
+ childEnv.DSH_HOME = dshHome;
52
+ childEnv.GOTRY_HANDOFF_CHILD = '1';
53
+ delete childEnv.GOTRY_TURN_DEADLINE_SOFT_MS;
54
+ delete childEnv.GOTRY_TURN_DEADLINE_HARD_MS;
55
+ delete childEnv.GOTRY_TURN_HANDOFF_ROOT;
56
+ const isJsScript = /\.(js|mjs|cjs)$/.test(plannerBin);
57
+ const child = isJsScript ? spawn(process.execPath, [
58
+ plannerBin,
59
+ ticket.userMessage
60
+ ], {
61
+ cwd: childCwd,
62
+ env: childEnv,
63
+ stdio: [
64
+ 'ignore',
65
+ 'pipe',
66
+ 'pipe'
67
+ ]
68
+ }) : spawn(plannerBin, [
69
+ ticket.userMessage
70
+ ], {
71
+ cwd: childCwd,
72
+ env: childEnv,
73
+ stdio: [
74
+ 'ignore',
75
+ 'pipe',
76
+ 'pipe'
77
+ ]
78
+ });
79
+ let output = '';
80
+ child.stdout.on('data', (chunk)=>{
81
+ output += chunk.toString();
82
+ });
83
+ child.stderr.on('data', (chunk)=>{
84
+ output += chunk.toString();
85
+ });
86
+ const code = await new Promise((resolve)=>{
87
+ const timer = setTimeout(()=>{
88
+ child.kill('SIGKILL');
89
+ resolve(null);
90
+ }, timeoutMs);
91
+ child.once('exit', (c)=>{
92
+ clearTimeout(timer);
93
+ resolve(c);
94
+ });
95
+ });
96
+ if (code === null) return {
97
+ ok: false,
98
+ output: '',
99
+ error: `planner timed out after ${timeoutMs}ms`
100
+ };
101
+ if (code !== 0) return {
102
+ ok: false,
103
+ output: '',
104
+ error: `planner exited ${code}: ${output.trim().slice(-300)}`
105
+ };
106
+ const deliverable = output.trim();
107
+ if (!deliverable) return {
108
+ ok: false,
109
+ output: '',
110
+ error: 'planner produced empty output'
111
+ };
112
+ return {
113
+ ok: true,
114
+ output: deliverable
115
+ };
116
+ } finally{
117
+ await new Promise((resolve)=>setTimeout(resolve, 200));
118
+ rmSync(dshHome, {
119
+ recursive: true,
120
+ force: true
121
+ });
122
+ rmSync(childCwd, {
123
+ recursive: true,
124
+ force: true
125
+ });
126
+ }
127
+ }
128
+ async function collectOne(stateRoot, ticketId) {
129
+ const ticket = loadTicket(stateRoot, ticketId);
130
+ if (!ticket) {
131
+ console.error(`工单 ${ticketId} 不存在(${stateRoot}/gotry-state/turn-handoffs/)`);
132
+ process.exit(1);
133
+ }
134
+ if (ticket.status !== 'open') {
135
+ console.error(`工单 ${ticketId} 已是终态 ${ticket.status},复诵不重算:`);
136
+ replay(ticket, stateRoot);
137
+ }
138
+ const result = await runPlanner(ticket);
139
+ const deliverable = result.ok ? `# 后台深度规划交付:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n${result.output}` : `# 后台深度规划失败:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n未产出交付物:${result.error ?? '未知原因'}。这趟规划没有完成——请重新发起,失败原因如上,无部分结果可交付。`;
140
+ const { deliverablePath } = await settleTurnHandoffTicket(stateRoot, ticket, result.ok ? 'settled' : 'failed', deliverable, result.ok ? undefined : result.error);
141
+ console.error(`工单 ${ticket.id} ${result.ok ? '已交付' : '失败'}:${deliverablePath}`);
142
+ emitTerminal({
143
+ schema: TURN_HANDOFF_TERMINAL_SCHEMA,
144
+ ticket_id: ticket.id,
145
+ status: result.ok ? 'succeeded' : 'failed',
146
+ ...result.ok ? {
147
+ deliverable_path: deliverablePath
148
+ } : {
149
+ error: result.error ?? 'unknown'
150
+ }
151
+ });
152
+ }
153
+ async function collectAll(stateRoot) {
154
+ const open = (await listTurnHandoffTickets(stateRoot)).filter((t)=>t.status === 'open');
155
+ if (open.length === 0) {
156
+ console.error('无待回收的后台规划工单(open=0)');
157
+ process.exit(0);
158
+ }
159
+ let failed = 0;
160
+ for (const view of open){
161
+ const ticket = loadTicket(stateRoot, view.id);
162
+ if (!ticket || ticket.status !== 'open') continue;
163
+ const result = await runPlanner(ticket);
164
+ const deliverable = result.ok ? `# 后台深度规划交付:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n${result.output}` : `# 后台深度规划失败:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n未产出交付物:${result.error ?? '未知原因'}。这趟规划没有完成——请重新发起,失败原因如上,无部分结果可交付。`;
165
+ const { deliverablePath } = await settleTurnHandoffTicket(stateRoot, ticket, result.ok ? 'settled' : 'failed', deliverable, result.ok ? undefined : result.error);
166
+ console.error(`工单 ${ticket.id} ${result.ok ? '已交付' : '失败'}:${deliverablePath}`);
167
+ if (!result.ok) failed += 1;
168
+ }
169
+ console.log(JSON.stringify({
170
+ schema: TURN_HANDOFF_TERMINAL_SCHEMA,
171
+ ticket_id: `--all(${open.length})`,
172
+ status: failed === 0 ? 'succeeded' : 'failed',
173
+ error: failed === 0 ? undefined : `${failed}/${open.length} 张工单回收失败`
174
+ }));
175
+ process.exit(failed === 0 ? 0 : 2);
176
+ }
177
+ const [target, stateRootArg] = process.argv.slice(2);
178
+ const stateRoot = stateRootArg ?? '.';
179
+ if (target === '--all') {
180
+ await collectAll(stateRoot);
181
+ } else if (target) {
182
+ await collectOne(stateRoot, target);
183
+ } else {
184
+ console.error('用法:npx tsx scripts/turn-handoff-collect.ts <ticketId|--all> [stateRoot]');
185
+ process.exit(1);
186
+ }
187
+
188
+
189
+ //# sourceURL=ts/scripts/turn-handoff-collect.ts
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict';
2
+ import { classifyTurn, extractTurnSignals, TURN_POLICIES, turnPolicyFor } from '../src/turn-policy.js';
3
+ const TRAJECTORY_MESSAGE = '2026年我还有6天IRW额度 ,我准备再请几天假,在国内待个十几天。' + '其中10.3要去湖南衡阳参加同学婚礼。其他我没有特别的要求,请根据我的实际情况,偏好,外部环境等因素,给我安排行程';
4
+ assert.equal(classifyTurn(TRAJECTORY_MESSAGE), 'deep-planning');
5
+ assert.equal(classifyTurn('9.26到10.12在国内玩,中间要参加同学婚礼'), 'deep-planning');
6
+ assert.equal(classifyTurn('国庆请假带父母去新疆,大概十几天'), 'deep-planning');
7
+ assert.equal(classifyTurn('帮我查一下明天深圳的天气'), 'quick');
8
+ assert.equal(classifyTurn('你好'), 'quick');
9
+ assert.equal(classifyTurn('帮我看看10.1去杭州的高铁票'), 'sync-planning');
10
+ assert.equal(classifyTurn('3天后出发,帮我查下天气'), 'sync-planning');
11
+ assert.equal(classifyTurn('把这段翻译成英文:会议纪要如下……省略若干字'.repeat(3)), 'sync-planning');
12
+ const full = extractTurnSignals('2026-10-03 出发');
13
+ assert.deepEqual(full.absoluteDates, [
14
+ {
15
+ year: 2026,
16
+ month: 10,
17
+ day: 3
18
+ }
19
+ ]);
20
+ const md = extractTurnSignals('10月3日到衡阳,10.5回深圳');
21
+ assert.equal(md.absoluteDates.length, 2);
22
+ assert.equal(md.spanDays, 2);
23
+ assert.deepEqual(md.absoluteDates[0], {
24
+ year: null,
25
+ month: 10,
26
+ day: 3
27
+ });
28
+ assert.equal(extractTurnSignals('13.45 与 22:40 都是数字').absoluteDates.length, 0);
29
+ assert.equal(extractTurnSignals('待十几天').spanDays, 12);
30
+ assert.equal(extractTurnSignals('待半个月').spanDays, 15);
31
+ assert.equal(extractTurnSignals('待一个月').spanDays, 30);
32
+ assert.equal(extractTurnSignals('大概两周').spanDays, 14);
33
+ assert.equal(extractTurnSignals('大概一周').spanDays, 7);
34
+ assert.equal(extractTurnSignals('还有6天额度').spanDays, 6);
35
+ assert.equal(extractTurnSignals('没有跨度').spanDays, null);
36
+ const both = extractTurnSignals('10.1 到 10.9 玩十几天');
37
+ assert.equal(both.spanDays, 8);
38
+ assert.deepEqual(extractTurnSignals('婚礼加IRW,还要请假').constraintHits, [
39
+ '婚礼',
40
+ 'IRW',
41
+ '请假'
42
+ ]);
43
+ assert.deepEqual(extractTurnSignals('没有任何约束词在这里').constraintHits, []);
44
+ assert.equal(classifyTurn('10.3 婚礼那天衡阳天气怎么样?穿什么'), 'sync-planning');
45
+ assert.equal(TURN_POLICIES['deep-planning'].exit, 'handoff');
46
+ assert.equal(TURN_POLICIES['sync-planning'].exit, 'converge');
47
+ assert.equal(TURN_POLICIES['quick'].exit, 'converge');
48
+ for (const policy of Object.values(TURN_POLICIES)){
49
+ assert.ok(policy.hardMs > policy.softMs, 'hard must exceed soft');
50
+ assert.ok(policy.softMs > 0 && policy.hardMs > 0);
51
+ }
52
+ assert.deepEqual(turnPolicyFor('quick'), TURN_POLICIES['quick']);
53
+ console.log('turn policy tests: OK (routing table incl. trajectory verbatim, date/span extraction, policy shape)');
54
+
55
+
56
+ //# sourceURL=ts/scripts/turn-policy-tests.ts
@@ -0,0 +1,261 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync, existsSync, mkdtempSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { validateJsonSchemaValue } from '@deepseek-ai/dsh-tools';
7
+ import { apply } from '../src/index.js';
8
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
9
+ function loadEnvTriple() {
10
+ const env = process.env;
11
+ let key = env.LLM_API_KEY ?? '';
12
+ let base = env.LLM_BASE_URL ?? '';
13
+ let model = env.LLM_MODEL ?? '';
14
+ const envFile = join(ROOT, '.env');
15
+ if (existsSync(envFile)) {
16
+ for (const line of readFileSync(envFile, 'utf-8').split('\n')){
17
+ const m = /^([A-Z_]+)=(.*)$/.exec(line.trim());
18
+ if (!m) continue;
19
+ const [, k, v] = m;
20
+ if (k === 'LLM_API_KEY' && !key) key = v.trim();
21
+ if (k === 'LLM_BASE_URL' && !base) base = v.trim();
22
+ if (k === 'LLM_MODEL' && !model) model = v.trim();
23
+ }
24
+ }
25
+ assert.ok(key && base && model, 'LLM_API_KEY/LLM_BASE_URL/LLM_MODEL 必须已配(env 或 .env)');
26
+ return {
27
+ key,
28
+ base,
29
+ model
30
+ };
31
+ }
32
+ const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-canary-state-'));
33
+ const registered = [];
34
+ const ctx = {
35
+ tools: {
36
+ register: (t)=>registered.push(t)
37
+ },
38
+ systemPrompt: {
39
+ variable: ()=>()=>''
40
+ },
41
+ on: ()=>()=>{}
42
+ };
43
+ apply(ctx, {
44
+ stateRoot,
45
+ timeoutMs: 30_000,
46
+ hbcliBin: 'hbcli-not-on-path',
47
+ sessionAccess: 'off'
48
+ });
49
+ const byName = (n)=>{
50
+ const t = registered.find((x)=>x.name === n);
51
+ assert.ok(t, `工具 ${n} 应已注册`);
52
+ return t;
53
+ };
54
+ const CASES = [
55
+ {
56
+ label: '机·票(城市对+日期)',
57
+ tool: 'gotry_flyai_search',
58
+ prompt: '帮我查一下 10 月 1 日从上海飞丽江的机票'
59
+ },
60
+ {
61
+ label: '火车票',
62
+ tool: 'gotry_flyai_search',
63
+ prompt: '10 月 1 日上海到昆明的火车票帮我查一下'
64
+ },
65
+ {
66
+ label: '酒店(带档期)',
67
+ tool: 'gotry_flyai_search',
68
+ prompt: '查一下大理 10 月 1 日入住、10 月 3 日退房的酒店'
69
+ },
70
+ {
71
+ label: '酒店(摸底,无档期)',
72
+ tool: 'gotry_flyai_search',
73
+ prompt: '先看看大理有哪些酒店'
74
+ },
75
+ {
76
+ label: '天气预报',
77
+ tool: 'gotry_weather_check',
78
+ prompt: '大理这几天天气怎么样'
79
+ },
80
+ {
81
+ label: '酒店实时源(hbcli)',
82
+ tool: 'gotry_hotel_search',
83
+ prompt: '查一下大理的酒店,入住下周五,住三晚'
84
+ },
85
+ {
86
+ label: '会话面机票(登录态)',
87
+ tool: 'gotry_session_search',
88
+ prompt: '用我的携程账号查 10 月 1 日上海到丽江的机票'
89
+ },
90
+ {
91
+ label: '目的地候选库',
92
+ tool: 'gotry_anything_search',
93
+ prompt: '大连在你们的目的地库里吗?帮我查一下'
94
+ },
95
+ {
96
+ label: 'GitHub 搜索',
97
+ tool: 'gotry_github_search',
98
+ prompt: '在 GitHub 上搜一下 agent-reach 这个项目'
99
+ },
100
+ {
101
+ label: '网页读取',
102
+ tool: 'gotry_web_search',
103
+ prompt: '帮我读一下 https://example.com 这个页面的内容'
104
+ }
105
+ ];
106
+ async function callLlmMessages(cfg, toolName, messages) {
107
+ const tool = byName(toolName);
108
+ const url = `${cfg.base.replace(/\/+$/, '')}/chat/completions`;
109
+ const body = {
110
+ model: cfg.model,
111
+ messages,
112
+ tools: [
113
+ {
114
+ type: 'function',
115
+ function: {
116
+ name: tool.name,
117
+ description: tool.description,
118
+ parameters: tool.parameters
119
+ }
120
+ }
121
+ ],
122
+ tool_choice: {
123
+ type: 'function',
124
+ function: {
125
+ name: tool.name
126
+ }
127
+ },
128
+ temperature: 0
129
+ };
130
+ const res = await fetch(url, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'content-type': 'application/json',
134
+ authorization: `Bearer ${cfg.key}`
135
+ },
136
+ body: JSON.stringify(body),
137
+ signal: AbortSignal.timeout(60_000)
138
+ });
139
+ const bodyText = await res.text();
140
+ assert.equal(res.status, 200, `LLM HTTP ${res.status}: ${bodyText.slice(0, 200)}`);
141
+ const j = JSON.parse(bodyText);
142
+ const msg = j.choices?.[0]?.message;
143
+ const tc = msg.tool_calls?.[0];
144
+ if (!tc?.function?.arguments) {
145
+ const text = String(msg.content ?? '');
146
+ assert.ok(/年份|日期|确认|2024|2025|2026/.test(text), `模型既未调工具也未按拒绝指引向用户确认: ${text.slice(0, 120)}`);
147
+ return {
148
+ args: undefined,
149
+ callId: 'text-reply',
150
+ text,
151
+ textReply: true
152
+ };
153
+ }
154
+ return {
155
+ args: JSON.parse(tc.function.arguments),
156
+ callId: tc.id ?? 'canary-call',
157
+ textReply: false
158
+ };
159
+ }
160
+ async function main() {
161
+ const cfg = loadEnvTriple();
162
+ console.log(`typed-contract canary:model=${cfg.model} 已配(值不回显);用例 ${CASES.length} 条,达标线 ≥90% 一次成型\n`);
163
+ let pass = 0;
164
+ const failures = [];
165
+ for (const c of CASES){
166
+ const tool = byName(c.tool);
167
+ process.stdout.write(`- ${c.label} → ${c.tool} … `);
168
+ try {
169
+ const { args } = await callLlmMessages(cfg, c.tool, [
170
+ {
171
+ role: 'system',
172
+ content: '你是 GoTry 旅行助手。需要查数据时必须调用提供的工具,参数从用户话里取,不要编造。'
173
+ },
174
+ {
175
+ role: 'user',
176
+ content: c.prompt
177
+ }
178
+ ]);
179
+ const violations = validateJsonSchemaValue(tool.parameters, args);
180
+ if (violations.length === 0) {
181
+ pass++;
182
+ console.log(`PASS 一次成型 ${JSON.stringify(args).slice(0, 120)}`);
183
+ } else {
184
+ failures.push(`${c.label}: schema 违例 ${violations.join(';')}`);
185
+ console.log(`FAIL schema 违例: ${violations.join(';').slice(0, 160)}`);
186
+ }
187
+ } catch (e) {
188
+ failures.push(`${c.label}: ${e.message.slice(0, 160)}`);
189
+ console.log(`FAIL ${e.message.slice(0, 140)}`);
190
+ }
191
+ }
192
+ {
193
+ const tool = byName('gotry_flyai_search');
194
+ const today = new Date().toISOString().slice(0, 10);
195
+ try {
196
+ const messages = [
197
+ {
198
+ role: 'system',
199
+ content: '你是 GoTry 旅行助手。需要查数据时必须调用提供的工具,参数从用户话里取,不要编造。'
200
+ },
201
+ {
202
+ role: 'user',
203
+ content: '帮我查一下 10 月 1 日从上海飞丽江的机票'
204
+ },
205
+ {
206
+ role: 'assistant',
207
+ content: null,
208
+ tool_calls: [
209
+ {
210
+ id: 'seed-reject',
211
+ type: 'function',
212
+ function: {
213
+ name: 'gotry_flyai_search',
214
+ arguments: JSON.stringify({
215
+ kind: 'flight',
216
+ from: '上海',
217
+ to: '丽江',
218
+ date: '2024-10-01'
219
+ })
220
+ }
221
+ }
222
+ ]
223
+ },
224
+ {
225
+ role: 'tool',
226
+ tool_call_id: 'seed-reject',
227
+ content: JSON.stringify({
228
+ ok: false,
229
+ verdict: 'error',
230
+ summary: `未发起查询:日期 2024-10-01 已是过去(今天 ${today}),过去不存在在售机票。多为用户时间表达未带年份所致——向用户确认年份(或按未来最近的同月日修正)后再查。`
231
+ })
232
+ }
233
+ ];
234
+ const reply = await callLlmMessages(cfg, 'gotry_flyai_search', messages);
235
+ const violations = reply.args ? validateJsonSchemaValue(tool.parameters, reply.args) : [];
236
+ if (reply.textReply) {
237
+ assert.match(reply.text ?? '', /年份|日期|确认/, '文字形态应向用户确认年份/日期(契约允许的自纠)');
238
+ console.log('自纠回路(验收②):模型按拒绝指引转为向用户确认年份(文字形态,契约允许)');
239
+ } else {
240
+ assert.equal(violations.length, 0, `自纠出参 schema 违例: ${violations.join(';')}`);
241
+ assert.ok(reply.args && reply.args.date && reply.args.date >= today, `自纠后日期应为今天或未来,实际 ${JSON.stringify(reply.args).slice(0, 80)}`);
242
+ console.log(`自纠回路(验收②):模型按拒绝自纠出参 → PASS(${JSON.stringify(reply.args).slice(0, 100)})`);
243
+ }
244
+ } catch (e) {
245
+ failures.push(`验收② 自纠回路异常: ${e.message.slice(0, 140)}`);
246
+ console.log(`FAIL 自纠回路异常(计入失败,不中断报告): ${e.message.slice(0, 140)}`);
247
+ }
248
+ }
249
+ const rate = Math.round(pass / CASES.length * 100);
250
+ console.log(`\n一次成型率:${pass}/${CASES.length} = ${rate}%(达标线 90%)`);
251
+ if (failures.length) console.log('失败明细:\n' + failures.map((f)=>` - ${f}`).join('\n'));
252
+ if (rate < 90) {
253
+ console.error('TYPED CONTRACT CANARY FAIL(不达标不合入)');
254
+ process.exit(1);
255
+ }
256
+ console.log('TYPED CONTRACT CANARY OK');
257
+ }
258
+ await main();
259
+
260
+
261
+ //# sourceURL=ts/scripts/typed-contract-canary.ts
@@ -0,0 +1,75 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { splitPolicyEntries, continentListUrl, countryPageUrl, extractVisaSection, listCountryPaths, policyFactsFromMfa } from '../capabilities/visa-policy.js';
6
+ import { makeProductionInterpreter, __resetEffectBreakersForTest } from '../capabilities/effect.js';
7
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
8
+ const FIXTURE = join(ROOT, 'data', 'fixtures', 'visa-policy-tg-sample.html');
9
+ let passed = 0;
10
+ function check(cond, msg) {
11
+ if (cond) {
12
+ passed++;
13
+ console.log(` ok - ${msg}`);
14
+ } else {
15
+ console.error(` FAIL - ${msg}`);
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ const html = readFileSync(FIXTURE, 'utf-8');
20
+ const section = extractVisaSection(html);
21
+ check(!!section && section.includes('免签'), '签证入境章节应被抽取(免签条款在案)');
22
+ check(section.includes('2024'), '生效日期应保留(逐段溯源)');
23
+ const entries = splitPolicyEntries(section);
24
+ check(entries.length >= 2, `中文序号段落切分(实际 ${entries.length} 条)`);
25
+ check(entries.every((e)=>e.length > 30), '条目长度下限(短段过滤)');
26
+ const fetchedAt = '2026-09-05T00:00:00.000Z';
27
+ const facts = policyFactsFromMfa({
28
+ countryLabel: '泰国',
29
+ countryPath: 'tg_647570',
30
+ section: section,
31
+ fetchedAt
32
+ });
33
+ check(facts.length === entries.length, '事实数=条目数');
34
+ check(facts.every((f)=>f.kind === 'policy' && f.schema === 'gotry_bookable_fact.v1'), 'PolicyFact 形态');
35
+ check(facts.every((f)=>f.as_of === '2026-09-05' && f.review_by === '2026-10-05'), 'as_of 抓取日 + review_by=D+30');
36
+ check(facts.every((f)=>f.source.startsWith('cs-mfa@') && f.query_id.startsWith('visa-policy:cs-mfa:')), '来源证据链 cs.mfa.gov.cn');
37
+ const facts2 = policyFactsFromMfa({
38
+ countryLabel: '泰国',
39
+ countryPath: 'tg_647570',
40
+ section: section,
41
+ fetchedAt
42
+ });
43
+ check(facts.every((f, i)=>f.fact_id === facts2[i].fact_id), 'fact_id 幂等');
44
+ assert.equal(countryPageUrl('yz_645708', 'tg_647570'), 'https://cs.mfa.gov.cn/zggmcg/ljmdd/yz_645708/tg_647570/', 'URL 形态');
45
+ assert.equal(countryPageUrl('..', 'x'), null, '路径穿越拒绝');
46
+ assert.equal(continentListUrl('..'), null, '洲路径穿越拒绝');
47
+ __resetEffectBreakersForTest();
48
+ const interp = makeProductionInterpreter({
49
+ sleep: async ()=>{},
50
+ breakers: new Map(),
51
+ handlers: {
52
+ VISA_POLICY_FETCH: async ()=>({
53
+ ok: true,
54
+ via: 'cs-mfa',
55
+ evidence: 'e',
56
+ latencyMs: 1,
57
+ facts: [],
58
+ countries: [
59
+ 'tg_647570'
60
+ ]
61
+ })
62
+ }
63
+ });
64
+ const out = await interp({
65
+ effect: 'VISA_POLICY_FETCH',
66
+ params: {
67
+ country: 'tg_647570'
68
+ }
69
+ });
70
+ assert.equal(out.trace.channel, 'api', 'trace.channel=api(策略表行在案)');
71
+ assert.equal(out.trace.attempts, 1, '礼貌抓取:单次执行(策略表 retry=null 生效)');
72
+ console.log(`\nVISA POLICY TESTS: ${passed} pass${process.exitCode ? ', FAIL' : ' (全绿)'}`);
73
+
74
+
75
+ //# sourceURL=ts/scripts/visa-policy-tests.ts
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { scoreWishMatch, pickNudgeWish } from '../src/wish-pool.js';
3
+ const base = {
4
+ wish_id: 'w-dubai',
5
+ name: '迪拜',
6
+ added_at: '2026-09-01T00:00:00Z',
7
+ conditions: {
8
+ days: 5,
9
+ budget_cny: 8000,
10
+ best_months: [
11
+ 10,
12
+ 11
13
+ ],
14
+ channels: [
15
+ 'hbcli-hotel'
16
+ ]
17
+ }
18
+ };
19
+ const vetoed = scoreWishMatch(base, {
20
+ days: 10,
21
+ budgetCny: 20000,
22
+ month: 10,
23
+ channelDown: new Set([
24
+ 'hbcli-hotel'
25
+ ])
26
+ });
27
+ assert.equal(vetoed, null, 'days/budget/month 全命中但依赖通道 down → 否证返回 null');
28
+ const healthy = scoreWishMatch(base, {
29
+ days: 10,
30
+ budgetCny: 20000,
31
+ month: 10,
32
+ channelDown: new Set()
33
+ });
34
+ assert.ok(healthy && healthy.score === 3 && healthy.hits.join('+') === 'days≥5+budget≥8000+month=10', '健康时评分与旧口径一致(score=3)');
35
+ const legacy = scoreWishMatch(base, {
36
+ days: 10,
37
+ budgetCny: 20000,
38
+ month: 10
39
+ });
40
+ assert.ok(legacy && legacy.score === 3, '不传 channelDown 的旧调用行为不变');
41
+ const malformed = scoreWishMatch({
42
+ ...base,
43
+ conditions: {
44
+ days: 5,
45
+ channels: 'hbcli-hotel'
46
+ }
47
+ }, {
48
+ days: 10,
49
+ month: 10,
50
+ channelDown: new Set([
51
+ 'hbcli-hotel'
52
+ ])
53
+ });
54
+ assert.ok(malformed && malformed.score === 1, '非数组 channels 被忽略,不误杀');
55
+ const mixed = scoreWishMatch({
56
+ ...base,
57
+ conditions: {
58
+ days: 5,
59
+ channels: [
60
+ 42,
61
+ null,
62
+ 'hbcli-hotel'
63
+ ]
64
+ }
65
+ }, {
66
+ days: 10,
67
+ month: 10,
68
+ channelDown: new Set([
69
+ 'hbcli-hotel'
70
+ ])
71
+ });
72
+ assert.equal(mixed, null, '数组内字符串元素仍参与否证(非字符串忽略)');
73
+ const pool = [
74
+ {
75
+ ...base,
76
+ added_at: '2026-09-01T00:00:00Z'
77
+ },
78
+ {
79
+ wish_id: 'w-osaka',
80
+ name: '大阪',
81
+ added_at: '2026-09-02T00:00:00Z',
82
+ conditions: {
83
+ days: 5,
84
+ best_months: [
85
+ 10
86
+ ]
87
+ }
88
+ }
89
+ ];
90
+ const picked = pickNudgeWish(pool, {
91
+ days: 10,
92
+ month: 10,
93
+ channelDown: new Set([
94
+ 'hbcli-hotel'
95
+ ])
96
+ });
97
+ assert.ok(picked && picked.wishId === 'w-osaka', `被否证的 w-dubai 退出,次高 w-osaka 顶上,实际 ${picked?.wishId}`);
98
+ const none = pickNudgeWish([
99
+ base
100
+ ], {
101
+ days: 10,
102
+ month: 10,
103
+ channelDown: new Set([
104
+ 'hbcli-hotel'
105
+ ])
106
+ });
107
+ assert.equal(none, null, '全部被否证 → null(不硬推纪律保持)');
108
+ console.log('WISH CHANNEL GATE TESTS: 5/5 OK(否证 / 健康不加分 / 旧调用零破坏 / 畸形忽略 / 0..1 集成)');
109
+
110
+
111
+ //# sourceURL=ts/scripts/wish-channel-gate-tests.ts