@danceiny/gotry 0.0.1-rc.10

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/bin/gotry-inner.js +214 -0
  4. package/bin/gotry-stdio-ask.js +59 -0
  5. package/bin/gotry.js +28 -0
  6. package/cordis.gotry-patch.yml +86 -0
  7. package/data/flights_2026.json +218 -0
  8. package/data/golden_erhai.json +92 -0
  9. package/data/golden_trip_2026.json +75 -0
  10. package/data/hotels_2026.json +151 -0
  11. package/data/openflights-skeleton.json +895 -0
  12. package/data/time-slot-eval.json +384 -0
  13. package/data/yunnan-pack.json +202 -0
  14. package/dist/capabilities/agent-reach-bridge.py +90 -0
  15. package/dist/capabilities/agent-reach-deep.js +172 -0
  16. package/dist/capabilities/agent-reach.js +204 -0
  17. package/dist/capabilities/anything.js +139 -0
  18. package/dist/capabilities/flyai.js +131 -0
  19. package/dist/capabilities/hbcli.js +143 -0
  20. package/dist/capabilities/incident-log.js +100 -0
  21. package/dist/capabilities/opensky.js +74 -0
  22. package/dist/capabilities/session/action-cache.js +120 -0
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  24. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  25. package/dist/capabilities/session/extract.js +40 -0
  26. package/dist/capabilities/session/read-guard.js +99 -0
  27. package/dist/capabilities/session/transport.js +101 -0
  28. package/dist/capabilities/session-search.js +93 -0
  29. package/dist/capabilities/weather.js +180 -0
  30. package/dist/scripts/action-cache-tests.js +107 -0
  31. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  32. package/dist/scripts/agent-reach-tests.js +38 -0
  33. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  34. package/dist/scripts/anything-tests.js +95 -0
  35. package/dist/scripts/async-collect.js +37 -0
  36. package/dist/scripts/companion-tests.js +112 -0
  37. package/dist/scripts/diff-test.js +39 -0
  38. package/dist/scripts/engine-run.js +14 -0
  39. package/dist/scripts/engine-tests.js +45 -0
  40. package/dist/scripts/hbcli-tests.js +101 -0
  41. package/dist/scripts/incident-tests.js +96 -0
  42. package/dist/scripts/journey-tests.js +45 -0
  43. package/dist/scripts/ledger-tests.js +365 -0
  44. package/dist/scripts/ledger-workflow-crash.js +42 -0
  45. package/dist/scripts/memory-capture-tests.js +77 -0
  46. package/dist/scripts/memory-decay-tests.js +83 -0
  47. package/dist/scripts/memory-metrics.js +24 -0
  48. package/dist/scripts/nudge-digest.js +83 -0
  49. package/dist/scripts/opensky-check.js +20 -0
  50. package/dist/scripts/opensky-tests.js +52 -0
  51. package/dist/scripts/probe-poi-tests.js +88 -0
  52. package/dist/scripts/publish-preverify.js +68 -0
  53. package/dist/scripts/replay-async.js +44 -0
  54. package/dist/scripts/replay-real.js +38 -0
  55. package/dist/scripts/replay.js +116 -0
  56. package/dist/scripts/session-attach-diagnose.js +27 -0
  57. package/dist/scripts/session-attach-poc.js +65 -0
  58. package/dist/scripts/session-extract-tests.js +81 -0
  59. package/dist/scripts/session-tests.js +162 -0
  60. package/dist/scripts/skeleton-check.js +37 -0
  61. package/dist/scripts/skeleton-integration-test.js +22 -0
  62. package/dist/scripts/skills-contract-tests.js +85 -0
  63. package/dist/scripts/smoke.js +328 -0
  64. package/dist/scripts/state-cli-tests.js +136 -0
  65. package/dist/scripts/state-cli.js +248 -0
  66. package/dist/scripts/time-eval-tests.js +318 -0
  67. package/dist/scripts/travel-timeline-tests.js +123 -0
  68. package/dist/scripts/unified-tests.js +49 -0
  69. package/dist/scripts/weather-tests.js +48 -0
  70. package/dist/src/bridge.js +34 -0
  71. package/dist/src/companions.js +112 -0
  72. package/dist/src/contracts.js +64 -0
  73. package/dist/src/dsh-llm.js +174 -0
  74. package/dist/src/engine.js +331 -0
  75. package/dist/src/index.js +1239 -0
  76. package/dist/src/journey.js +147 -0
  77. package/dist/src/loop.js +360 -0
  78. package/dist/src/memory-capture.js +40 -0
  79. package/dist/src/memory-decay.js +31 -0
  80. package/dist/src/memory-utility.js +55 -0
  81. package/dist/src/mock-llm.js +103 -0
  82. package/dist/src/model.js +134 -0
  83. package/dist/src/slot-spec.js +165 -0
  84. package/dist/src/state-ledger.js +901 -0
  85. package/dist/src/time-anchor.js +100 -0
  86. package/dist/src/tool-packet.js +12 -0
  87. package/dist/src/travel-slots.js +144 -0
  88. package/dist/src/travel-timeline.js +78 -0
  89. package/dist/src/unified.js +538 -0
  90. package/dist/src/wish-pool.js +27 -0
  91. package/package.json +84 -0
  92. package/ts/capabilities/agent-reach-deep.ts +114 -0
  93. package/ts/capabilities/agent-reach.ts +158 -0
  94. package/ts/capabilities/anything.ts +201 -0
  95. package/ts/capabilities/flyai.ts +151 -0
  96. package/ts/capabilities/hbcli.ts +152 -0
  97. package/ts/capabilities/incident-log.ts +148 -0
  98. package/ts/capabilities/opensky.ts +121 -0
  99. package/ts/capabilities/session/action-cache.ts +148 -0
  100. package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
  101. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  102. package/ts/capabilities/session/extract.ts +54 -0
  103. package/ts/capabilities/session/read-guard.ts +141 -0
  104. package/ts/capabilities/session/transport.ts +114 -0
  105. package/ts/capabilities/session-search.ts +129 -0
  106. package/ts/capabilities/weather.ts +176 -0
  107. package/ts/package.json +27 -0
  108. package/ts/scripts/async-collect.ts +50 -0
  109. package/ts/scripts/skeleton-check.ts +49 -0
  110. package/ts/scripts/skeleton-integration-test.ts +23 -0
  111. package/ts/scripts/state-cli.ts +201 -0
  112. package/ts/src/bridge.ts +44 -0
  113. package/ts/src/companions.ts +113 -0
  114. package/ts/src/contracts.ts +165 -0
  115. package/ts/src/dsh-llm.ts +155 -0
  116. package/ts/src/index.ts +968 -0
  117. package/ts/src/loop.ts +411 -0
  118. package/ts/src/memory-capture.ts +59 -0
  119. package/ts/src/memory-decay.ts +55 -0
  120. package/ts/src/memory-utility.ts +70 -0
  121. package/ts/src/mock-llm.ts +100 -0
  122. package/ts/src/model.ts +220 -0
  123. package/ts/src/slot-spec.ts +172 -0
  124. package/ts/src/state-ledger.ts +849 -0
  125. package/ts/src/time-anchor.ts +134 -0
  126. package/ts/src/tool-packet.ts +32 -0
  127. package/ts/src/travel-slots.ts +219 -0
  128. package/ts/src/travel-timeline.ts +83 -0
  129. package/ts/src/unified.ts +582 -0
  130. package/ts/src/wish-pool.ts +66 -0
@@ -0,0 +1,162 @@
1
+ import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { classifyRequest, isSubmitText } from '../capabilities/session/read-guard.js';
5
+ import { buildEntryUrl, parseBatchSearch } from '../capabilities/session/adapters/ctrip-flight.js';
6
+ import { sessionFlightSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
7
+ import { flyaiSearch } from '../capabilities/flyai.js';
8
+ let pass = 0;
9
+ let fail = 0;
10
+ function assert(cond, label, detail) {
11
+ if (cond) {
12
+ pass += 1;
13
+ console.log(` ok - ${label}`);
14
+ } else {
15
+ fail += 1;
16
+ console.log(` FAIL - ${label}${detail !== undefined ? ' :: ' + JSON.stringify(detail)?.slice(0, 300) : ''}`);
17
+ }
18
+ }
19
+ console.log('A. classifyRequest(方法×URL 双因子)');
20
+ assert(classifyRequest('POST', 'https://flights.ctrip.com/international/search/api/search/batchSearch?v=1') === 'allow', '搜索 POST(batchSearch)放行');
21
+ assert(classifyRequest('POST', 'https://m.ctrip.com/restapi/soa2/12345/SubmitOrder') === 'block', 'POST /SubmitOrder 拦截(驼峰复合)');
22
+ assert(classifyRequest('POST', 'https://m.ctrip.com/restapi/soa2/12345/createOrder') === 'block', 'POST /createOrder 拦截(驼峰复合)');
23
+ assert(classifyRequest('GET', 'https://pay.ctrip.com/pay/payment?x=1') === 'block', 'GET 硬支付模式拦截(任意方法)');
24
+ assert(classifyRequest('GET', 'https://m.ctrip.com/restapi/soa2/15095/SearchBoxRecommend') === 'allow', 'GET 搜索建议放行');
25
+ assert(classifyRequest('PUT', 'https://api.example.com/trade/create') === 'block', 'PUT /trade/create 拦截');
26
+ assert(classifyRequest('OPTIONS', 'https://flights.ctrip.com/api/preflight') === 'allow', 'OPTIONS 放行');
27
+ console.log('B. parseBatchSearch(fixture)');
28
+ const fixture = JSON.stringify({
29
+ status: 0,
30
+ data: {
31
+ flightItineraryList: [
32
+ {
33
+ flightSegments: [
34
+ {
35
+ airlineName: '吉祥航空',
36
+ duration: 200,
37
+ flightList: [
38
+ {
39
+ flightNo: 'HO5577',
40
+ departureAirportName: '虹桥国际机场',
41
+ arrivalAirportName: '三义机场',
42
+ departureDateTime: '2026-10-01 07:35:00',
43
+ arrivalDateTime: '2026-10-01 10:55:00',
44
+ aircraftName: '波音737(中)'
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ priceList: [
50
+ {
51
+ adultPrice: 3240
52
+ },
53
+ {
54
+ adultPrice: 2980
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ flightSegments: [
60
+ {}
61
+ ]
62
+ }
63
+ ]
64
+ }
65
+ });
66
+ const parsed = parseBatchSearch(fixture);
67
+ assert(parsed.length === 1, 'malformed 项跳过,1 个有效行程');
68
+ assert(parsed[0]?.flightNo === 'HO5577' && parsed[0]?.price === 2980, 'flightNo + priceList 最小 adultPrice');
69
+ assert(parsed[0]?.depDateTime === '2026-10-01 07:35:00' && parsed[0]?.durationMin === 200, '时刻与时长字段');
70
+ assert(parseBatchSearch('not-json').length === 0, '非 JSON 返空不抛错');
71
+ console.log('C. buildEntryUrl(城市码表)');
72
+ const e1 = buildEntryUrl('上海', '丽江', '2026-10-01');
73
+ assert(e1.ok === true && e1.url === 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-10-01', '上海→丽江 URL 精确');
74
+ const e2 = buildEntryUrl('乌兰巴托', '丽江', '2026-10-01');
75
+ assert(e2.ok === false && e2.unresolved?.[0] === '乌兰巴托', '词表外城市 unresolved 逐字保留(不猜)');
76
+ assert(buildEntryUrl('上海', '丽江', '十月一号').ok === false, '非法日期拒绝');
77
+ console.log('D. isSubmitText(DOM 提交件黑名单)');
78
+ assert(isSubmitText('立即下单') && isSubmitText('去支付') && isSubmitText('提交订单'), '下单/支付/提交 命中');
79
+ assert(!isSubmitText('搜索') && !isSubmitText('筛选'), '搜索/筛选 不误伤');
80
+ console.log('E. 节律闸(≥30s 间隔)');
81
+ __resetRateLimiterForTest();
82
+ const r1 = await sessionFlightSearch({
83
+ from: '乌兰巴托',
84
+ to: '丽江',
85
+ date: '2026-10-01'
86
+ });
87
+ const r2 = await sessionFlightSearch({
88
+ from: '上海',
89
+ to: '丽江',
90
+ date: '2026-10-01'
91
+ });
92
+ assert(r1.verdict === 'error' && /unresolved/.test(r1.error ?? ''), '首次调用:词表外 → error(unresolved)');
93
+ assert(r2.verdict === 'cooldown', '30s 内二次调用 → cooldown,不发起导航');
94
+ __resetRateLimiterForTest();
95
+ console.log('F. flyaiSearch(live,飞猪官方,无 key)');
96
+ const fr = await flyaiSearch({
97
+ kind: 'flight',
98
+ origin: '上海',
99
+ destination: '丽江',
100
+ depDate: '2026-10-01'
101
+ });
102
+ const sentinelBlocked = fr.verdict === 'error' && /sentinel|block/i.test(fr.error ?? '');
103
+ if (sentinelBlocked) {
104
+ console.log(' WARN - 飞猪 Sentinel 限流(2026-08-28 实测,配额未文档化)——降级合同验证通过,跳过 hit 断言');
105
+ assert(fr.ok === false && /\[实时API:flyai@error@/.test(fr.evidence), '限流降级:结构化 error + 证据链错误形');
106
+ } else {
107
+ assert(fr.ok === true && fr.verdict === 'hit', '上海→丽江 hit', fr);
108
+ assert((fr.options?.length ?? 0) >= 1 && (fr.options?.every((o)=>o.price > 0 && /^\d+[A-Z]\d+|^[A-Z]{2}\d+/.test(o.no)) ?? false), '结构化字段齐(price>0,航班号形)', fr.options?.[0]);
109
+ }
110
+ assert(/\[实时API:flyai/.test(fr.evidence), '证据链 [实时API:flyai@*]');
111
+ console.log('G. sessionFlightSearch(live,隔离 profile + ReadGuard)');
112
+ if (process.env.GOTRY_SESSION_LIVE === '0') {
113
+ console.log(' SKIP - GOTRY_SESSION_LIVE=0');
114
+ } else {
115
+ __resetRateLimiterForTest();
116
+ const iso = mkdtempSync(join(tmpdir(), 'gotry-session-test-'));
117
+ const nl = await sessionFlightSearch({
118
+ from: '上海',
119
+ to: '丽江',
120
+ date: '2026-10-01',
121
+ profileDir: join(iso, 'anon-profile'),
122
+ headless: true
123
+ });
124
+ assert(nl.verdict === 'needs-login' && nl.ok === false, '匿名实例 → needs-login(不导航不发请求)', nl);
125
+ __resetRateLimiterForTest();
126
+ const sr = await sessionFlightSearch({
127
+ from: '上海',
128
+ to: '丽江',
129
+ date: '2026-10-01',
130
+ profileDir: join(iso, 'profile'),
131
+ headless: false,
132
+ allowAnonymous: true,
133
+ auditPath: join(iso, 'audit', 'session-incidents.jsonl')
134
+ });
135
+ if (sr.verdict === 'challenged') {
136
+ console.log(` WARN - 风控命中(平台方差,合同路径验证通过):${sr.error}`);
137
+ assert(sr.ok === false && sr.verdict === 'challenged', 'challenged = degraded 语义正确(不重试不绕过)');
138
+ } else if (sr.verdict === 'error' && /chrome launch failed/.test(sr.error ?? '')) {
139
+ console.log(' SKIP - 本机无 Chrome(channel:chrome 不可用)');
140
+ } else {
141
+ assert(sr.ok === true && sr.verdict === 'hit' && (sr.options?.length ?? 0) >= 1, '上海→丽江 会话嗅探 hit', sr);
142
+ assert(sr.options?.every((o)=>o.price > 0 && o.depDateTime.includes('2026-10-01')) ?? false, '班期=查询日,价格>0', sr.options?.[0]);
143
+ assert(/\[会话:ctrip-flight@/.test(sr.evidence) && /blocked=0/.test(sr.evidence), '证据链 [会话:*] + ReadGuard 零拦截(纯只读)');
144
+ assert(!existsSync(join(iso, 'audit', 'session-incidents.jsonl')), '审计文件不出现(零写请求)');
145
+ const flyaiMin = Math.min(...fr.options?.map((o)=>o.price).filter((p)=>p > 0) ?? [
146
+ 0
147
+ ]);
148
+ const sessionMin = Math.min(...sr.options?.map((o)=>o.price).filter((p)=>p > 0) ?? [
149
+ 0
150
+ ]);
151
+ console.log(` 双源对照:FlyAI 最低 ¥${flyaiMin} vs 会话(携程)最低 ¥${sessionMin}(同日同线路,记录不判等)`);
152
+ }
153
+ rmSync(iso, {
154
+ recursive: true,
155
+ force: true
156
+ });
157
+ }
158
+ console.log(`\nSESSION P1: ${pass} pass, ${fail} fail`);
159
+ process.exit(fail > 0 ? 1 : 0);
160
+
161
+
162
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-tests.ts
@@ -0,0 +1,37 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ let cached = null;
4
+ async function load() {
5
+ if (!cached) {
6
+ cached = JSON.parse(await readFile(join(import.meta.dirname, '..', '..', 'data', 'openflights-skeleton.json'), 'utf-8'));
7
+ }
8
+ return cached;
9
+ }
10
+ export async function checkConnectivity(a, b) {
11
+ const skeleton = await load();
12
+ const key = [
13
+ a.toUpperCase(),
14
+ b.toUpperCase()
15
+ ].sort().join('-');
16
+ const airlines = skeleton.pairs[key];
17
+ if (airlines) {
18
+ return {
19
+ connected: true,
20
+ airlines,
21
+ evidence: `[骨架:openflights] ✅ ${a}↔${b} 直飞(${airlines.length}+ 航司:${airlines.join(',')})`
22
+ };
23
+ }
24
+ const inHub = skeleton.meta.hub_filter.includes(a.toUpperCase()) && skeleton.meta.hub_filter.includes(b.toUpperCase());
25
+ return {
26
+ connected: false,
27
+ evidence: inHub ? `[骨架:openflights] ❌ ${a}↔${b} 枢纽间无直飞记录——引擎应将此候选降权或要求中转` : `[骨架:openflights] ○ ${a}或${b}不在枢纽集,骨架不覆盖(不作否定结论)`
28
+ };
29
+ }
30
+ import { pathToFileURL } from 'node:url';
31
+ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
32
+ if (isMain && process.argv[2] && process.argv[3]) {
33
+ console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence);
34
+ }
35
+
36
+
37
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/skeleton-check.ts
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { parseFlightPackToSpec, solveUnified } from '../src/unified.js';
5
+ const pack = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'));
6
+ const spec = parseFlightPackToSpec(pack);
7
+ spec.budgetCny = 9000;
8
+ spec.skeletonHub = true;
9
+ spec.segments.forEach((s)=>{
10
+ if (s.id === 'f5') s.route = 'SZX->DXB';
11
+ if (s.id === 'f1') s.route = 'HKG->HKT';
12
+ });
13
+ const r = await solveUnified(spec);
14
+ assert.equal(r.feasible, true, '骨架否定不排除——EK329 必须仍在');
15
+ const f5 = r.legs.find((l)=>l.leg === 'f5');
16
+ assert.equal(f5?.service, 'EK329', 'EK329 存活(骨架滞后容错)');
17
+ assert.ok((r.skeleton_notes ?? []).some((n)=>n.includes('SZX') && n.includes('❌')), `枢纽否定标注在: ${r.skeleton_notes}`);
18
+ assert.ok((r.skeleton_notes ?? []).some((n)=>n.includes('HKG') && n.includes('✅')), '正向标注在');
19
+ console.log(`骨架集成 OK:EK329 在枢纽否定下存活,标注=[${r.skeleton_notes?.join(' | ')}]`);
20
+
21
+
22
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/skeleton-integration-test.ts
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { apply } from '../src/index.js';
4
+ async function main() {
5
+ const registered = [];
6
+ apply({
7
+ tools: {
8
+ register: (t)=>registered.push(t)
9
+ }
10
+ }, {
11
+ stateRoot: '.',
12
+ timeoutMs: 1000,
13
+ hbcliBin: 'hbcli-not-on-path'
14
+ });
15
+ const desc = (n)=>{
16
+ const t = registered.find((x)=>x.name === n);
17
+ if (!t) return '';
18
+ const params = Object.values(t.parameters?.properties ?? {}).map((p)=>p.description ?? '').join(' ');
19
+ return `${t.description ?? ''} ${params}`;
20
+ };
21
+ const anythingDesc = desc('gotry_anything_search');
22
+ const hotelsDesc = desc('gotry_hotel_search');
23
+ let token = '';
24
+ try {
25
+ const out = execFileSync('git', [
26
+ 'credential-osxkeychain',
27
+ 'get'
28
+ ], {
29
+ input: 'protocol=https\nhost=github.com\n'
30
+ }).toString();
31
+ token = (out.match(/^password=(.+)$/m) ?? [
32
+ '',
33
+ ''
34
+ ])[1].trim();
35
+ } catch {}
36
+ if (!token) {
37
+ console.log('SKIP: 无 GitHub 凭证(契约对齐在有凭证环境跑)');
38
+ return;
39
+ }
40
+ const get = async (path)=>{
41
+ const res = await fetch(`https://api.github.com/repos/Danceiny/hotelbyte-skills/contents/${path}`, {
42
+ headers: {
43
+ Authorization: `Bearer ${token}`,
44
+ Accept: 'application/vnd.github+json'
45
+ },
46
+ signal: AbortSignal.timeout(15_000)
47
+ });
48
+ if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
49
+ const j = await res.json();
50
+ return Buffer.from(j.content ?? '', j.encoding === 'base64' ? 'base64' : 'utf8').toString('utf8');
51
+ };
52
+ let anythingContract = '';
53
+ let hotelsContract = '';
54
+ try {
55
+ ;
56
+ [anythingContract, hotelsContract] = await Promise.all([
57
+ get('contracts/anything.md'),
58
+ get('contracts/hotels.md')
59
+ ]);
60
+ } catch (e) {
61
+ console.log(`SKIP: 契约拉取失败(${e.message})——对齐检查跳过,不红`);
62
+ return;
63
+ }
64
+ assert.ok(anythingContract.includes('contentType'), '契约应声明 contentType');
65
+ assert.ok(anythingDesc.includes('contentType'), 'gotry_anything_search 描述应含 contentType(契约对齐)');
66
+ assert.ok(anythingContract.includes('agent-reach'), '契约应声明与 agent-reach 的域边界');
67
+ assert.ok(/agent[-_]reach/.test(anythingDesc), 'gotry_anything_search 描述应引域边界(agent[-_]reach)');
68
+ for (const p of [
69
+ 'destination',
70
+ 'checkIn',
71
+ 'checkOut'
72
+ ]){
73
+ assert.ok(hotelsContract.includes(p), `契约应声明 ${p}`);
74
+ assert.ok(hotelsDesc.includes(p), `gotry_hotel_search 描述应含 ${p}(契约对齐)`);
75
+ }
76
+ assert.ok(anythingContract.includes('not-installed') || anythingContract.includes('三值'), '契约应声明三值降级');
77
+ console.log('SKILLS CONTRACT TESTS: 2/2 OK(anything/hotels 契约与 gotry 工具描述对齐)');
78
+ }
79
+ main().catch((e)=>{
80
+ console.error(e);
81
+ process.exitCode = 1;
82
+ });
83
+
84
+
85
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/skills-contract-tests.ts
@@ -0,0 +1,328 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { apply } from '../src/index.js';
4
+ async function main() {
5
+ const { mkdtempSync, rmSync } = await import('node:fs');
6
+ const { tmpdir } = await import('node:os');
7
+ const smokeRoot = mkdtempSync(join(tmpdir(), 'gotry-smoke-'));
8
+ const registered = [];
9
+ const variables = {};
10
+ const ctx = {
11
+ tools: {
12
+ register: (t)=>registered.push(t)
13
+ },
14
+ systemPrompt: {
15
+ variable: (name, provider)=>{
16
+ variables[name] = provider;
17
+ }
18
+ }
19
+ };
20
+ apply(ctx, {
21
+ stateRoot: smokeRoot,
22
+ timeoutMs: 30_000,
23
+ hbcliBin: 'hbcli-not-on-path'
24
+ });
25
+ console.log(`registered tools: ${registered.map((t)=>t.name).join(', ')}`);
26
+ const byName = (n)=>{
27
+ const t = registered.find((t)=>t.name === n);
28
+ if (!t) throw new Error(`tool ${n} not registered`);
29
+ return t;
30
+ };
31
+ const motivation = byName('gotry_motivation_save');
32
+ try {
33
+ await motivation.execute({
34
+ profile: {
35
+ weights: {
36
+ escape_rest: 0.7
37
+ }
38
+ }
39
+ }, null);
40
+ throw new Error('FAIL: profile without evidence should have been rejected');
41
+ } catch (e) {
42
+ console.log(`motivation without evidence rejected: ${e.message.slice(0, 60)}...`);
43
+ }
44
+ const saved = await motivation.execute({
45
+ profile: {
46
+ weights: {
47
+ escape_rest: 0.7,
48
+ curiosity: 0.3
49
+ },
50
+ evidence: [
51
+ '用户原话:想去湖边什么都不干(洱海照片)'
52
+ ],
53
+ hard: {
54
+ wake_not_before: '06:30',
55
+ min_arrival_energy_pct: 40
56
+ }
57
+ }
58
+ }, null);
59
+ console.log(`motivation saved -> ${JSON.stringify(saved).slice(0, 80)}...`);
60
+ const payload = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
61
+ const feasibility = byName('gotry_feasibility_check');
62
+ const result = await feasibility.execute({
63
+ payload
64
+ }, null);
65
+ console.log(`\nfeasibility: recommended=${result.recommended}, via=${result.via}, latency=${result.latency_ms}ms\n`);
66
+ console.log(result.answer_md);
67
+ const wish = byName('gotry_wish_pool_add');
68
+ const added = await wish.execute({
69
+ entry: {
70
+ name: '大理·洱海',
71
+ reason: '2 天窗口装不下(冲突:duration)',
72
+ conditions: {
73
+ days: 5,
74
+ budget_cny: 4950,
75
+ best_months: [
76
+ 3,
77
+ 4,
78
+ 5,
79
+ 9,
80
+ 10,
81
+ 11
82
+ ]
83
+ }
84
+ }
85
+ }, null);
86
+ console.log(`\nwish pool -> ${JSON.stringify(added)}`);
87
+ if (result.recommended !== 'qiandao') throw new Error('FAIL: expected qiandao recommended');
88
+ if (typeof feasibility.presentResult !== 'function') throw new Error('FAIL: feasibility 缺 presentResult');
89
+ const view = feasibility.presentResult({
90
+ payload
91
+ }, result);
92
+ if (!view?.title?.includes('qiandao')) throw new Error(`FAIL: 结果卡标题缺推荐,实际 ${view?.title}`);
93
+ const body = view?.content?.[0]?.text ?? '';
94
+ if (!body.includes('✅') || !body.includes('¥')) throw new Error('FAIL: 结果卡缺判定行/成本行');
95
+ console.log(`\nresult card: ${view?.title}\n${body.split('\n').slice(0, 5).join('\n')}`);
96
+ for (const n of [
97
+ 'gotry_feasibility_check',
98
+ 'gotry_hotel_search',
99
+ 'gotry_weather_check',
100
+ 'gotry_anything_search',
101
+ 'gotry_agent_reach'
102
+ ]){
103
+ if (typeof byName(n).presentResult !== 'function') throw new Error(`FAIL: ${n} 缺 presentResult`);
104
+ }
105
+ const ar = byName('gotry_agent_reach');
106
+ const arView = ar.presentResult({
107
+ query: {
108
+ action: 'reach',
109
+ channel: 'v2ex',
110
+ method: 'get_hot_topics'
111
+ }
112
+ }, {
113
+ verdict: 'found',
114
+ summary: '10 topics'
115
+ });
116
+ if (!arView?.title?.includes('✅') || !arView.title.includes('v2ex.get_hot_topics')) throw new Error(`FAIL: agent_reach 结果卡,实际 ${arView?.title}`);
117
+ console.log(`result cards on 5 tools; agent_reach card: ${arView.title}`);
118
+ const ws = byName('gotry_web_search');
119
+ const r1 = await ws.execute({
120
+ query: 'not-a-url'
121
+ }, null);
122
+ const r2 = await ws.execute({
123
+ url: 'not-a-url'
124
+ }, null);
125
+ for (const [name, r] of [
126
+ [
127
+ 'string',
128
+ r1
129
+ ],
130
+ [
131
+ 'bare-obj',
132
+ r2
133
+ ]
134
+ ]){
135
+ if (r.summary === 'url 必填') throw new Error(`FAIL: ${name} 形态未被 unwrapQuery 接住`);
136
+ }
137
+ console.log('unwrapQuery: string + bare-object shapes both accepted');
138
+ const skTool = byName('gotry_skeleton_check');
139
+ const skFlat = await skTool.execute({
140
+ from: 'HKG',
141
+ to: 'BKK'
142
+ }, null);
143
+ if (skFlat.connected !== true) throw new Error(`FAIL: 骨架平铺调用应 connected=true,实际 ${JSON.stringify(skFlat).slice(0, 120)}`);
144
+ const skBad = await skTool.execute({
145
+ from: '',
146
+ to: ''
147
+ }, null);
148
+ if (typeof skBad?.summary === 'string' && skBad.connected === undefined) {
149
+ console.log('skeleton guard-fallback shape survives (loose schema)');
150
+ }
151
+ console.log(`skeleton flat-args: connected=${skFlat.connected}`);
152
+ {
153
+ const hotel = byName('gotry_hotel_search');
154
+ const resolved = await hotel.execute({
155
+ query: {
156
+ destination: '大理',
157
+ checkIn: '2026-9-4',
158
+ checkOut: '2026-09-06'
159
+ }
160
+ }, null);
161
+ if (!resolved.date_notes?.some((n)=>n.includes('2026-9-4 → 2026-09-04'))) {
162
+ throw new Error(`FAIL: 非规整 ISO 应产生 slot-resolved note,实际 ${JSON.stringify(resolved.date_notes)}`);
163
+ }
164
+ const unresolved = await hotel.execute({
165
+ query: {
166
+ destination: '大理',
167
+ checkIn: '近期'
168
+ }
169
+ }, null);
170
+ if (!unresolved.date_notes?.some((n)=>n.includes('日期未解析:近期'))) {
171
+ throw new Error(`FAIL: 词表外表达应产生「日期未解析」note(不猜),实际 ${JSON.stringify(unresolved.date_notes)}`);
172
+ }
173
+ console.log('hotel date slots: verbatim resolved in code layer; unresolved degrades with explicit note');
174
+ }
175
+ {
176
+ const wish = byName('gotry_wish_pool_add');
177
+ const w = await wish.execute({
178
+ entry: {
179
+ name: 'envelope-probe',
180
+ conditions: {
181
+ days: 3
182
+ }
183
+ }
184
+ }, null);
185
+ if (w.ok !== true) throw new Error(`FAIL: wish 添加应 ok:true,实际 ${JSON.stringify(w)}`);
186
+ if (result.ok !== true) throw new Error(`FAIL: feasibility 应 ok:true,实际 ${String(result.ok)}`);
187
+ console.log('observation envelope: success payloads carry flat ok:true (guard fallback = failure branch)');
188
+ }
189
+ {
190
+ const add = byName('gotry_wish_pool_add');
191
+ const list = byName('gotry_wish_pool_list');
192
+ const a = await add.execute({
193
+ entry: {
194
+ name: 'sidecar-probe-洱海',
195
+ reason: '5 天起',
196
+ conditions: {
197
+ days: 5,
198
+ budget_cny: 4950,
199
+ best_months: [
200
+ 3,
201
+ 4,
202
+ 5
203
+ ]
204
+ }
205
+ }
206
+ }, null);
207
+ if (!a.wish_id) throw new Error('FAIL: wish add 应返回稳定 wish_id');
208
+ const recall = await list.execute({
209
+ query: {
210
+ action: 'recall',
211
+ days: 6,
212
+ budgetCny: 6000,
213
+ month: 4
214
+ }
215
+ }, null);
216
+ if (!recall.suggestion?.wish_id) throw new Error(`FAIL: 条件命中应召回恰好 1 条(0..1),实际 ${JSON.stringify(recall.suggestion)}`);
217
+ if ((recall.suggestion.match_score ?? 0) < 3) throw new Error(`FAIL: 召回应按条件匹配评分,实际 ${JSON.stringify(recall.suggestion)}`);
218
+ const confirm = await list.execute({
219
+ query: {
220
+ action: 'confirm-outcome',
221
+ wishId: a.wish_id,
222
+ attribution: 'helpful',
223
+ detail: '用户明说:这条建议成了'
224
+ }
225
+ }, null);
226
+ if (!confirm.ok || confirm.status !== 'helpful') throw new Error(`FAIL: owner 确认归因应落盘,实际 ${JSON.stringify(confirm)}`);
227
+ const noAttr = await list.execute({
228
+ query: {
229
+ action: 'confirm-outcome',
230
+ wishId: a.wish_id
231
+ }
232
+ }, null);
233
+ if (noAttr.ok !== false) throw new Error('FAIL: 无归因的 confirm 应拒绝(归因不许缺省)');
234
+ await add.execute({
235
+ entry: {
236
+ name: 'sidecar-probe-洱海',
237
+ conditions: {
238
+ days: 5,
239
+ budget_cny: 4950,
240
+ best_months: [
241
+ 3,
242
+ 4,
243
+ 5
244
+ ]
245
+ },
246
+ muted: true
247
+ }
248
+ }, null);
249
+ const recall2 = await list.execute({
250
+ query: {
251
+ action: 'recall',
252
+ days: 6,
253
+ budgetCny: 6000,
254
+ month: 4
255
+ }
256
+ }, null);
257
+ if (recall2.suggestion?.wish_id === a.wish_id) throw new Error('FAIL: muted wish 不得召回');
258
+ console.log('wish sidecar: recall 0..1 + muted excluded + owner-confirmed attribution only');
259
+ }
260
+ {
261
+ const brief = variables['motivation_brief'];
262
+ if (typeof brief !== 'function') throw new Error('FAIL: motivation_brief 变量未注册');
263
+ const saved = await byName('gotry_motivation_save').execute({
264
+ profile: {
265
+ weights: {
266
+ brief_probe: 0.6
267
+ },
268
+ evidence: [
269
+ '用户原话:smoke 读回探针'
270
+ ],
271
+ hard: {
272
+ wake_not_before: '07:15'
273
+ }
274
+ }
275
+ }, null);
276
+ if (saved.ok !== true) throw new Error('FAIL: 读回探针画像应保存成功');
277
+ const rendered = brief();
278
+ if (!rendered.includes('brief_probe=') || !rendered.includes('wake_not_before=07:15') || !/证据 [1-9]\d* 条/.test(rendered)) {
279
+ throw new Error(`FAIL: 回访 brief 应含画像字段,实际:${rendered.slice(0, 200)}`);
280
+ }
281
+ console.log('memory read-back: motivation_brief renders profile for returning sessions (empty = first visit)');
282
+ }
283
+ {
284
+ const fa = await byName('gotry_flyai_search').execute({
285
+ query: {
286
+ kind: 'flight',
287
+ from: '上海',
288
+ to: '丽江',
289
+ date: '2026-10-01'
290
+ }
291
+ }, null);
292
+ const faBlocked = fa.verdict === 'error' && /sentinel|block/i.test(fa.error ?? '');
293
+ if (faBlocked) {
294
+ console.log(' WARN - flyai Sentinel 限流中,降级合同通过(hit 断言跳过)');
295
+ } else if (fa.ok !== true || fa.verdict !== 'hit' || (fa.options?.length ?? 0) < 1 || !/\[实时API:flyai@/.test(fa.evidence ?? '')) {
296
+ throw new Error(`FAIL: flyai 工具应 live hit,实际:${JSON.stringify(fa).slice(0, 200)}`);
297
+ }
298
+ const prof = mkdtempSync(join(smokeRoot, 'sess-'));
299
+ const ss = await byName('gotry_session_search').execute({
300
+ query: {
301
+ from: '上海',
302
+ to: '丽江',
303
+ date: '2026-10-01'
304
+ }
305
+ }, null);
306
+ rmSync(prof, {
307
+ recursive: true,
308
+ force: true
309
+ });
310
+ if (!(ss.verdict === 'needs-login' || ss.verdict === 'needs-attach' || ss.verdict === 'hit' || ss.verdict === 'cooldown' || ss.verdict === 'challenged')) {
311
+ throw new Error(`FAIL: session 工具终态应属 {needs-login,hit,cooldown,challenged},实际:${JSON.stringify(ss).slice(0, 200)}`);
312
+ }
313
+ console.log(`session-face tools: flyai ${faBlocked ? 'sentinel-限流降级' : `live hit(${fa.options?.length ?? 0} 条)`}; session 终态=${ss.verdict}(登录态存在前提合同)`);
314
+ }
315
+ rmSync(smokeRoot, {
316
+ recursive: true,
317
+ force: true
318
+ });
319
+ console.log(`smoke state cleaned: ${smokeRoot}`);
320
+ console.log('\nSMOKE OK');
321
+ }
322
+ main().catch((e)=>{
323
+ console.error(e);
324
+ process.exit(1);
325
+ });
326
+
327
+
328
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/smoke.ts