@danceiny/gotry 0.0.1-rc.5 → 0.0.1-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/gotry-inner.js +73 -32
- package/cordis.gotry-patch.yml +2 -2
- package/data/flights_2026.json +215 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +64 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/yunnan-pack.json +201 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/hbcli.js +138 -0
- package/dist/capabilities/incident-log.js +99 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +18 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +85 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +79 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +44 -0
- package/dist/scripts/skeleton-check.js +35 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/smoke.js +90 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +154 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +760 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +296 -0
- package/dist/src/mock-llm.js +98 -0
- package/dist/src/model.js +134 -0
- package/dist/src/unified.js +536 -0
- package/package.json +3 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { probePoi } from '../src/loop.js';
|
|
3
|
+
function pass(name, body) {
|
|
4
|
+
body();
|
|
5
|
+
console.log(` ${name} OK`);
|
|
6
|
+
}
|
|
7
|
+
pass('1. 不抛错 + 空串/纯空白返 null', ()=>{
|
|
8
|
+
assert.doesNotThrow(()=>probePoi(''));
|
|
9
|
+
assert.equal(probePoi(''), null);
|
|
10
|
+
assert.equal(probePoi(' '), null);
|
|
11
|
+
});
|
|
12
|
+
pass('2. 短纯地名(<24 字符非问句)触发', ()=>{
|
|
13
|
+
for (const t of [
|
|
14
|
+
'普吉',
|
|
15
|
+
'清迈',
|
|
16
|
+
'大理',
|
|
17
|
+
'北京 酒店',
|
|
18
|
+
'丽江市'
|
|
19
|
+
]){
|
|
20
|
+
const r = probePoi(t);
|
|
21
|
+
if (r === null) throw new Error(`${JSON.stringify(t)} 应触发,得 null`);
|
|
22
|
+
if (typeof r !== 'string') throw new Error(`返类型错: ${typeof r}`);
|
|
23
|
+
if (r.length < 2 || r.length > 24) throw new Error(`长度越界: ${r.length}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
pass('3. 结构触发 — 含"查/搜/找/看看/推荐/告诉我/检索"', ()=>{
|
|
27
|
+
for (const t of [
|
|
28
|
+
'查一下 大理',
|
|
29
|
+
'搜 普吉',
|
|
30
|
+
'找清迈',
|
|
31
|
+
'看看曼谷',
|
|
32
|
+
'推荐 沙巴'
|
|
33
|
+
]){
|
|
34
|
+
const r = probePoi(t);
|
|
35
|
+
if (r === null) throw new Error(`${JSON.stringify(t)} 应触发,得 null`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
pass('4. 内容触发 — 含"酒店/民宿/客栈/有什么/玩什么/有哪些"', ()=>{
|
|
39
|
+
for (const t of [
|
|
40
|
+
'大理酒店',
|
|
41
|
+
'清迈民宿',
|
|
42
|
+
'曼谷有什么',
|
|
43
|
+
'丽江有哪些'
|
|
44
|
+
]){
|
|
45
|
+
const r = probePoi(t);
|
|
46
|
+
if (r === null) throw new Error(`${JSON.stringify(t)} 应触发,得 null`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
pass('5. 不触发 — 长问句(>24 字符 触发上限 isShort 拒)', ()=>{
|
|
50
|
+
for (const t of [
|
|
51
|
+
'lorem ipsum dolor sit amet consect',
|
|
52
|
+
'我想查询从北京到上海的高铁票价格'
|
|
53
|
+
]){
|
|
54
|
+
const r = probePoi(t);
|
|
55
|
+
if (t.length > 24) {
|
|
56
|
+
if (r !== null) throw new Error(`>24 字符 ${JSON.stringify(t)} 应不触发,得 ${JSON.stringify(r)}`);
|
|
57
|
+
} else {
|
|
58
|
+
if (r === null) throw new Error(`<=24 字符 ${JSON.stringify(t)} 应触发,得 null`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
pass('6. 不抛错 + 返回类型是 string 或 null(边界)', ()=>{
|
|
63
|
+
for (const t of [
|
|
64
|
+
'a'.repeat(200),
|
|
65
|
+
'a?'.repeat(50),
|
|
66
|
+
'!@#',
|
|
67
|
+
'中文' + 'a'.repeat(100),
|
|
68
|
+
' 前后空格 '
|
|
69
|
+
]){
|
|
70
|
+
const r = probePoi(t);
|
|
71
|
+
if (r !== null && typeof r !== 'string') {
|
|
72
|
+
throw new Error(`返类型错: ${typeof r}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
console.log('\nprobePoi TESTS: 6 类 OK(不崩 + 5 类触发 + 1 类不触发返 null)');
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/probe-poi-tests.ts
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createMockLlm } from '../src/mock-llm.js';
|
|
3
|
+
import { collectDeepPlanning, isComplex, newState, persistAsyncTicket, requestDeepPlanning, runTurn } from '../src/loop.js';
|
|
4
|
+
import { solveUnified } from '../src/unified.js';
|
|
5
|
+
const llm = createMockLlm(join('..', 'data', 'flights_2026.json'));
|
|
6
|
+
const state = newState();
|
|
7
|
+
const history = [];
|
|
8
|
+
const turns = [
|
|
9
|
+
'7.17周五22:40落地深圳……8.10周一凌晨从深圳起飞周一上班前到迪拜。请给我做机票和酒店的行程规划和推荐。',
|
|
10
|
+
'我的工作时间是UTC+4的早上10点到下午7点',
|
|
11
|
+
'我订了酒店:The Title East Wing Rawai,7.18入住 7.23 退房'
|
|
12
|
+
];
|
|
13
|
+
let lastReply = '';
|
|
14
|
+
for (const t of turns){
|
|
15
|
+
const { reply } = await runTurn(state, t, llm, [
|
|
16
|
+
...history
|
|
17
|
+
], solveUnified);
|
|
18
|
+
history.push({
|
|
19
|
+
role: 'user',
|
|
20
|
+
text: t
|
|
21
|
+
}, {
|
|
22
|
+
role: 'assistant',
|
|
23
|
+
text: reply
|
|
24
|
+
});
|
|
25
|
+
lastReply = reply;
|
|
26
|
+
}
|
|
27
|
+
if (!isComplex(state)) throw new Error('复杂度判据未命中,重放脚本与判据不一致');
|
|
28
|
+
const { reply: asyncReply, ticket } = await requestDeepPlanning(state);
|
|
29
|
+
const saved = await persistAsyncTicket(ticket, state);
|
|
30
|
+
console.log('用户> (约束齐备,行程复杂)\n');
|
|
31
|
+
console.log(`GoTry> ${asyncReply}\n`);
|
|
32
|
+
console.log(`(工单已持久化:${saved}——任意后续进程可执行回收)\n`);
|
|
33
|
+
if (process.argv.includes('--request-only')) {
|
|
34
|
+
console.log(`(request-only 模式结束;回收命令:npx tsx scripts/async-collect.ts ${ticket.id})`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log('……(一小时后,另一个进程执行回收)……\n');
|
|
37
|
+
const { reply: deliverable } = await collectDeepPlanning(state, ticket, solveUnified);
|
|
38
|
+
console.log(`GoTry> ${deliverable}`);
|
|
39
|
+
if (!deliverable.includes('4/4')) throw new Error('FAIL: 不失望四条未全过');
|
|
40
|
+
console.log(`\n(真实形态命令:npx tsx scripts/async-collect.ts ${ticket.id})`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/replay-async.ts
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
try {
|
|
4
|
+
for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')){
|
|
5
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
6
|
+
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
7
|
+
}
|
|
8
|
+
} catch {}
|
|
9
|
+
import { createMockLlm } from '../src/mock-llm.js';
|
|
10
|
+
import { createOpenAICompatLlm } from '../src/dsh-llm.js';
|
|
11
|
+
import { newState, runTurn } from '../src/loop.js';
|
|
12
|
+
import { solveUnified } from '../src/unified.js';
|
|
13
|
+
const useReal = Boolean(process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY']);
|
|
14
|
+
const llm = useReal ? createOpenAICompatLlm(join('..', 'data', 'flights_2026.json')) : createMockLlm(join('..', 'data', 'flights_2026.json'));
|
|
15
|
+
console.log(useReal ? `=== 真 LLM 模式(${process.env['LLM_MODEL'] ?? 'MiniMax-M2'})===` : '=== 无 LLM_API_KEY,回退 mock(ADR-8)===\n');
|
|
16
|
+
const state = newState();
|
|
17
|
+
const history = [];
|
|
18
|
+
const turns = [
|
|
19
|
+
'7.17周五22:40落地深圳,7.18早上去香港办银行开户&保险签约;争取7.18当天飞泰国普吉岛……8.10周一凌晨从深圳起飞,周一上班前到迪拜。请给我做机票和酒店的行程规划和推荐。',
|
|
20
|
+
'我的工作时间是UTC+4的早上10点到下午7点',
|
|
21
|
+
'我订了酒店:The Title East Wing Rawai,7.18入住 7.23 退房'
|
|
22
|
+
];
|
|
23
|
+
for (const t of turns){
|
|
24
|
+
const { reply } = await runTurn(state, t, llm, [
|
|
25
|
+
...history
|
|
26
|
+
], solveUnified);
|
|
27
|
+
history.push({
|
|
28
|
+
role: 'user',
|
|
29
|
+
text: t
|
|
30
|
+
}, {
|
|
31
|
+
role: 'assistant',
|
|
32
|
+
text: reply
|
|
33
|
+
});
|
|
34
|
+
console.log(`\n用户> ${t.slice(0, 46)}…\nGoTry> ${reply}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/replay-real.ts
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createMockLlm } from '../src/mock-llm.js';
|
|
2
|
+
import { interviewNext, newState, runTurn } from '../src/loop.js';
|
|
3
|
+
import { solveUnified } from '../src/unified.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const llm = createMockLlm(join('..', 'data', 'flights_2026.json'));
|
|
6
|
+
let state = newState();
|
|
7
|
+
const userTurns = [
|
|
8
|
+
'7.17周五22:40落地深圳,7.18早上去香港办银行开户&保险签约;争取7.18当天飞泰国普吉岛……8.10周一凌晨从深圳起飞,周一上班前到迪拜。请给我做机票和酒店的行程规划和推荐。',
|
|
9
|
+
'我的工作时间是UTC+4的早上10点到下午7点,晚上不一定有时间(至少得带电脑保持在线)',
|
|
10
|
+
'我订了酒店:The Title East Wing Rawai,7.18入住 7.23 退房'
|
|
11
|
+
];
|
|
12
|
+
const history = [];
|
|
13
|
+
for (const turn of userTurns){
|
|
14
|
+
const { reply } = await runTurn(state, turn, llm, [
|
|
15
|
+
...history
|
|
16
|
+
], (spec)=>{
|
|
17
|
+
spec.skeletonHub = true;
|
|
18
|
+
return solveUnified(spec);
|
|
19
|
+
});
|
|
20
|
+
history.push({
|
|
21
|
+
role: 'user',
|
|
22
|
+
text: turn
|
|
23
|
+
}, {
|
|
24
|
+
role: 'assistant',
|
|
25
|
+
text: reply
|
|
26
|
+
});
|
|
27
|
+
console.log(`\n用户> ${turn.slice(0, 50)}…`);
|
|
28
|
+
console.log(`GoTry> ${reply}`);
|
|
29
|
+
}
|
|
30
|
+
console.log('\n===== 终态校验 =====');
|
|
31
|
+
console.log('日历断言数:', Object.keys(state.calendar.assertedWeekdays).length, '| 2026-07-17 =', state.calendar.assertedWeekdays['2026-07-17']);
|
|
32
|
+
console.log('workWindow:', state.profile.workWindow ? `${state.profile.workWindow.startMin / 60}:00-${state.profile.workWindow.endMin / 60}:00 UTC+${state.profile.workWindow.homeTzOffsetMin / 60}(证据:${state.profile.workWindow.evidence.slice(0, 24)}…)` : '缺失');
|
|
33
|
+
console.log('bookedResources:', state.profile.bookedResources?.map((b)=>b.ref).join('; ') ?? '缺失');
|
|
34
|
+
const { missing } = interviewNext(state);
|
|
35
|
+
console.log('剩余待问:', missing.length === 0 ? '无(除预算档未答,见下)' : missing.join(','));
|
|
36
|
+
const weekday = state.calendar.assertedWeekdays['2026-07-17'];
|
|
37
|
+
if (weekday !== 'fri') throw new Error(`FAIL: 2026-07-17 应为 fri,实为 ${weekday}`);
|
|
38
|
+
if (!state.profile.workWindow?.evidence) throw new Error('FAIL: workWindow 缺失或无证据');
|
|
39
|
+
if (!state.profile.bookedResources?.length) throw new Error('FAIL: bookedResources 缺失');
|
|
40
|
+
if (!(missing.length === 1 && missing[0] === 'budgetTier')) throw new Error(`FAIL: 剩余待问应为 [budgetTier],实为 [${missing.join(',')}]`);
|
|
41
|
+
console.log('REPLAY ASSERTS OK');
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/replay.ts
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
if (process.argv[2] && process.argv[3]) {
|
|
31
|
+
console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
//# 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,90 @@
|
|
|
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 registered = [];
|
|
6
|
+
const ctx = {
|
|
7
|
+
tools: {
|
|
8
|
+
register: (t)=>registered.push(t)
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
apply(ctx, {
|
|
12
|
+
pythonBin: '../.venv/bin/python',
|
|
13
|
+
pythonPath: '../py',
|
|
14
|
+
stateRoot: '.',
|
|
15
|
+
timeoutMs: 30_000,
|
|
16
|
+
preferInProcess: true,
|
|
17
|
+
hbcliBin: 'hbcli-not-on-path'
|
|
18
|
+
});
|
|
19
|
+
console.log(`registered tools: ${registered.map((t)=>t.name).join(', ')}`);
|
|
20
|
+
const byName = (n)=>{
|
|
21
|
+
const t = registered.find((t)=>t.name === n);
|
|
22
|
+
if (!t) throw new Error(`tool ${n} not registered`);
|
|
23
|
+
return t;
|
|
24
|
+
};
|
|
25
|
+
const motivation = byName('gotry_motivation_save');
|
|
26
|
+
try {
|
|
27
|
+
await motivation.execute({
|
|
28
|
+
profile: {
|
|
29
|
+
weights: {
|
|
30
|
+
escape_rest: 0.7
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, null);
|
|
34
|
+
throw new Error('FAIL: profile without evidence should have been rejected');
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.log(`motivation without evidence rejected: ${e.message.slice(0, 60)}...`);
|
|
37
|
+
}
|
|
38
|
+
const saved = await motivation.execute({
|
|
39
|
+
profile: {
|
|
40
|
+
weights: {
|
|
41
|
+
escape_rest: 0.7,
|
|
42
|
+
curiosity: 0.3
|
|
43
|
+
},
|
|
44
|
+
evidence: [
|
|
45
|
+
'用户原话:想去湖边什么都不干(洱海照片)'
|
|
46
|
+
],
|
|
47
|
+
hard: {
|
|
48
|
+
wake_not_before: '06:30',
|
|
49
|
+
min_arrival_energy_pct: 40
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, null);
|
|
53
|
+
console.log(`motivation saved -> ${JSON.stringify(saved).slice(0, 80)}...`);
|
|
54
|
+
const payload = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
|
|
55
|
+
const feasibility = byName('gotry_feasibility_check');
|
|
56
|
+
const result = await feasibility.execute({
|
|
57
|
+
payload
|
|
58
|
+
}, null);
|
|
59
|
+
console.log(`\nfeasibility: recommended=${result.recommended}, via=${result.via}, latency=${result.latency_ms}ms\n`);
|
|
60
|
+
console.log(result.answer_md);
|
|
61
|
+
const wish = byName('gotry_wish_pool_add');
|
|
62
|
+
const added = await wish.execute({
|
|
63
|
+
entry: {
|
|
64
|
+
name: '大理·洱海',
|
|
65
|
+
reason: '2 天窗口装不下(冲突:duration)',
|
|
66
|
+
conditions: {
|
|
67
|
+
days: 5,
|
|
68
|
+
budget_cny: 4950,
|
|
69
|
+
best_months: [
|
|
70
|
+
3,
|
|
71
|
+
4,
|
|
72
|
+
5,
|
|
73
|
+
9,
|
|
74
|
+
10,
|
|
75
|
+
11
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, null);
|
|
80
|
+
console.log(`\nwish pool -> ${JSON.stringify(added)}`);
|
|
81
|
+
if (result.recommended !== 'qiandao') throw new Error('FAIL: expected qiandao recommended');
|
|
82
|
+
console.log('\nSMOKE OK');
|
|
83
|
+
}
|
|
84
|
+
main().catch((e)=>{
|
|
85
|
+
console.error(e);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/smoke.ts
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import { hhmmToMin } from '../src/model.js';
|
|
6
|
+
const pack = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'));
|
|
7
|
+
const spec = parseFlightPackToSpec(pack);
|
|
8
|
+
spec.budgetCny = 9000;
|
|
9
|
+
const r1 = await solveUnified(spec);
|
|
10
|
+
assert.equal(r1.feasible, true);
|
|
11
|
+
assert.equal(r1.money_cny, r1.legs.reduce((a, l)=>a + l.price_cny, 0));
|
|
12
|
+
assert.ok(7280 <= r1.money_cny && r1.money_cny <= 8350, `money=${r1.money_cny}`);
|
|
13
|
+
const byLeg = Object.fromEntries(r1.legs.map((l)=>[
|
|
14
|
+
l.leg,
|
|
15
|
+
l
|
|
16
|
+
]));
|
|
17
|
+
assert.notEqual(byLeg['f4'].service, 'DZ6252');
|
|
18
|
+
assert.equal(byLeg['f5'].service, 'EK329');
|
|
19
|
+
assert.equal(byLeg['f5'].energy_pct, 75);
|
|
20
|
+
assert.ok(byLeg['f5'].wake.includes('前一日'));
|
|
21
|
+
assert.equal(byLeg['f5'].door_to_door, '11h20m');
|
|
22
|
+
if (byLeg['f3'].service === 'MU6088') {
|
|
23
|
+
assert.equal(byLeg['f3'].door_to_door, '6h15m');
|
|
24
|
+
}
|
|
25
|
+
const tight = parseFlightPackToSpec(pack);
|
|
26
|
+
tight.segments[0].anchors.arriveByMin = hhmmToMin('15:00');
|
|
27
|
+
const r2 = await solveUnified(tight);
|
|
28
|
+
assert.equal(r2.feasible, false);
|
|
29
|
+
assert.ok(r2.unsat_core.includes('f1:arrive_by'), `core=${JSON.stringify(r2.unsat_core)}`);
|
|
30
|
+
assert.ok(r2.unsat_core.every((c)=>!c.includes('|')), 'core 无竖线残留');
|
|
31
|
+
assert.ok(r2.suggestions.some((sg)=>sg.relax === 'f1:arrive_by'));
|
|
32
|
+
const poor = parseFlightPackToSpec(pack);
|
|
33
|
+
poor.budgetCny = 1000;
|
|
34
|
+
const r3 = await solveUnified(poor);
|
|
35
|
+
assert.equal(r3.feasible, false);
|
|
36
|
+
assert.ok(r3.unsat_core.includes('total:budget'));
|
|
37
|
+
const excluded = r1.work_window_exclusions ?? [];
|
|
38
|
+
assert.ok(excluded.some((e)=>e.segment === 'f2' && e.option === 'TG216'), JSON.stringify(excluded));
|
|
39
|
+
assert.ok(excluded.some((e)=>e.segment === 'f2' && e.option === 'TG218'));
|
|
40
|
+
assert.equal(byLeg['f2'].service, 'VZ303');
|
|
41
|
+
const off = parseFlightPackToSpec(pack);
|
|
42
|
+
off.budgetCny = 9000;
|
|
43
|
+
off.workWindow = undefined;
|
|
44
|
+
const r4 = await solveUnified(off);
|
|
45
|
+
assert.deepEqual(r4.work_window_exclusions, []);
|
|
46
|
+
console.log(`TS UNIFIED TESTS: 4/4 OK(工作窗口生效,f2=VZ303,¥${r1.money_cny})`);
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/unified-tests.ts
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
|
|
3
|
+
const geo = await geocodePlace('大理市', {
|
|
4
|
+
count: 10
|
|
5
|
+
});
|
|
6
|
+
assert.equal(geo.ok, true, `geocode ok: ${geo.error ?? ''}`);
|
|
7
|
+
assert.ok(geo.results.length > 0, '应有结果');
|
|
8
|
+
const dali = geo.results.find((r)=>(r.admin1 ?? '').includes('云南')) ?? geo.results[0];
|
|
9
|
+
assert.ok(Math.abs(dali.latitude - 25.6) < 0.5, `纬度应≈25.6,实际 ${dali.latitude}(${dali.name},${dali.admin1})`);
|
|
10
|
+
assert.match(geo.evidence, /open-meteo-geo@2/, '证据链带时间戳');
|
|
11
|
+
console.log(`1. geocode 大理市 → ${dali.latitude},${dali.longitude} (${dali.name},${dali.admin1}) OK`);
|
|
12
|
+
const fc = await getForecast({
|
|
13
|
+
latitude: dali.latitude,
|
|
14
|
+
longitude: dali.longitude
|
|
15
|
+
}, {
|
|
16
|
+
days: 7
|
|
17
|
+
});
|
|
18
|
+
assert.equal(fc.ok, true, `forecast ok: ${fc.error ?? ''}`);
|
|
19
|
+
assert.equal(fc.daily.length, 7, '7 天');
|
|
20
|
+
assert.ok(fc.daily[0].tempMaxC > -50 && fc.daily[0].tempMaxC < 60, '温度合理范围');
|
|
21
|
+
assert.match(fc.evidence, /\[实时API:open-meteo@2/, '证据链');
|
|
22
|
+
assert.ok(fc.latencyMs < 5000, `延迟应 <5s,实际 ${fc.latencyMs}ms`);
|
|
23
|
+
console.log(`2. forecast 大理 7 天:${fc.daily[0].tempMinC}–${fc.daily[0].tempMaxC}°C ${wmoLabel(fc.daily[0].weatherCode)} OK`);
|
|
24
|
+
const cl = await getClimate({
|
|
25
|
+
latitude: dali.latitude,
|
|
26
|
+
longitude: dali.longitude
|
|
27
|
+
}, 8);
|
|
28
|
+
assert.equal(cl.ok, true, `climate ok: ${cl.error ?? ''}`);
|
|
29
|
+
assert.ok(cl.daily.length >= 28, '8 月应有 ≥28 天');
|
|
30
|
+
assert.match(cl.evidence, /open-meteo-climate@/, '气候证据链');
|
|
31
|
+
console.log(`3. climate 大理去年 8 月:${cl.daily.length} 天 OK`);
|
|
32
|
+
const bad = await getForecast({
|
|
33
|
+
latitude: 999,
|
|
34
|
+
longitude: 0
|
|
35
|
+
}, {
|
|
36
|
+
timeoutMs: 10_000
|
|
37
|
+
});
|
|
38
|
+
assert.equal(bad.ok, false, '非法输入应返回 ok=false');
|
|
39
|
+
assert.ok(bad.evidence.includes('error'), '降级证据标注');
|
|
40
|
+
console.log(`4. 降级 ok=false 不抛错 OK`);
|
|
41
|
+
assert.equal(wmoLabel(0), '晴');
|
|
42
|
+
assert.equal(wmoLabel(95), '雷暴');
|
|
43
|
+
assert.match(wmoLabel(999), /天气码999/, '未知码回退');
|
|
44
|
+
console.log('5. WMO 码映射 OK');
|
|
45
|
+
console.log('\nWEATHER TESTS: 5/5 OK(Open-Meteo 真实 API,免费无 key)');
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/weather-tests.ts
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function ensureStateDir(root) {
|
|
4
|
+
const dir = join(root, 'gotry-state');
|
|
5
|
+
await mkdir(dir, {
|
|
6
|
+
recursive: true
|
|
7
|
+
});
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
export async function readJson(path, fallback) {
|
|
11
|
+
try {
|
|
12
|
+
const text = await readFile(path, 'utf-8');
|
|
13
|
+
return JSON.parse(text);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code === 'ENOENT') return fallback;
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function writeJson(path, value) {
|
|
20
|
+
const tmp = `${path}.tmp`;
|
|
21
|
+
await writeFile(tmp, JSON.stringify(value, null, 2), 'utf-8');
|
|
22
|
+
const { rename } = await import('node:fs/promises');
|
|
23
|
+
await rename(tmp, path);
|
|
24
|
+
}
|
|
25
|
+
export async function recordLatency(logPath, latencyMs, kind) {
|
|
26
|
+
await appendFile(logPath, JSON.stringify({
|
|
27
|
+
ts: new Date().toISOString(),
|
|
28
|
+
kind,
|
|
29
|
+
latencyMs
|
|
30
|
+
}) + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/bridge.ts
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const TOOL_CONTRACTS = {
|
|
2
|
+
'gotry_interview_next': {},
|
|
3
|
+
'gotry_spec_extract': {},
|
|
4
|
+
'gotry_solve': {},
|
|
5
|
+
'gotry_render': {},
|
|
6
|
+
'gotry_wish_pool_add': {}
|
|
7
|
+
};
|
|
8
|
+
export const TRIP_STATE_JSON_SCHEMA = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
properties: {
|
|
12
|
+
calendar: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
description: '年份与已断言的星期映射;一次断言终身使用,冲突必须显式指出'
|
|
16
|
+
},
|
|
17
|
+
profile: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
additionalProperties: true,
|
|
20
|
+
description: 'workWindow(带 evidence)/companions/budgetTier/bookedResources/motivation'
|
|
21
|
+
},
|
|
22
|
+
spec: {
|
|
23
|
+
type: 'json',
|
|
24
|
+
description: '统一行程模型 JourneySpec(见 unified.ts)'
|
|
25
|
+
},
|
|
26
|
+
solve: {
|
|
27
|
+
type: 'json',
|
|
28
|
+
description: '求解结果(锚点/排除/红旗/建议)'
|
|
29
|
+
},
|
|
30
|
+
gates: {
|
|
31
|
+
type: 'array',
|
|
32
|
+
description: '待决问题,只能是选择题',
|
|
33
|
+
items: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
additionalProperties: true,
|
|
36
|
+
properties: {
|
|
37
|
+
id: {
|
|
38
|
+
type: 'string'
|
|
39
|
+
},
|
|
40
|
+
question: {
|
|
41
|
+
type: 'string'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
wishes: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: '「下一次出发」清单',
|
|
49
|
+
items: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
additionalProperties: true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
required: [
|
|
56
|
+
'calendar',
|
|
57
|
+
'profile',
|
|
58
|
+
'gates',
|
|
59
|
+
'wishes'
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/contracts.ts
|