@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.9
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 +29 -74
- package/bin/gotry-inner.js +50 -1
- package/bin/gotry-stdio-ask.js +59 -0
- package/cordis.gotry-patch.yml +63 -5
- package/data/flights_2026.json +7 -4
- package/data/hotels_2026.json +102 -15
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +2 -1
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/anything.js +2 -2
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +11 -6
- package/dist/capabilities/incident-log.js +2 -1
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +56 -0
- package/dist/capabilities/session/transport.js +95 -0
- package/dist/capabilities/session-search.js +95 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/async-collect.js +26 -7
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/hbcli-tests.js +18 -2
- package/dist/scripts/ledger-tests.js +344 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/probe-poi-tests.js +9 -0
- package/dist/scripts/replay.js +73 -1
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-attach-wait.js +41 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-login.js +48 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +3 -1
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +242 -4
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +234 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/src/companions.js +112 -0
- package/dist/src/dsh-llm.js +27 -7
- package/dist/src/index.js +600 -121
- package/dist/src/loop.js +89 -25
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +6 -1
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +848 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +3 -1
- package/dist/src/wish-pool.js +27 -0
- package/package.json +6 -2
- package/ts/capabilities/hbcli.ts +6 -6
- package/ts/capabilities/incident-log.ts +6 -3
- package/ts/package.json +3 -0
- package/ts/scripts/skeleton-check.ts +5 -1
- package/ts/src/dsh-llm.ts +27 -7
- package/ts/src/index.ts +439 -91
- package/ts/src/loop.ts +109 -34
- package/ts/src/mock-llm.ts +13 -1
- package/ts/src/unified.ts +6 -1
- package/ts/cordis.gotry-patch.yml +0 -36
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const WEEKDAYS_ZH = [
|
|
2
|
+
'日',
|
|
3
|
+
'一',
|
|
4
|
+
'二',
|
|
5
|
+
'三',
|
|
6
|
+
'四',
|
|
7
|
+
'五',
|
|
8
|
+
'六'
|
|
9
|
+
];
|
|
10
|
+
export function ymd(d) {
|
|
11
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
12
|
+
}
|
|
13
|
+
function addDays(d, n) {
|
|
14
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
15
|
+
}
|
|
16
|
+
function weekMonday(d) {
|
|
17
|
+
return addDays(d, -((d.getDay() + 6) % 7));
|
|
18
|
+
}
|
|
19
|
+
const SPRING_FESTIVAL = {
|
|
20
|
+
2026: '2026-02-17',
|
|
21
|
+
2027: '2027-02-06',
|
|
22
|
+
2028: '2028-01-26',
|
|
23
|
+
2029: '2029-02-13',
|
|
24
|
+
2030: '2030-02-03',
|
|
25
|
+
2031: '2031-01-23'
|
|
26
|
+
};
|
|
27
|
+
function tzLabelOf(d) {
|
|
28
|
+
const offsetMin = -d.getTimezoneOffset();
|
|
29
|
+
const sign = offsetMin >= 0 ? '+' : '-';
|
|
30
|
+
const abs = Math.abs(offsetMin);
|
|
31
|
+
const h = Math.floor(abs / 60);
|
|
32
|
+
const m = abs % 60;
|
|
33
|
+
return `UTC${sign}${m === 0 ? h : `${h}:${String(m).padStart(2, '0')}`}`;
|
|
34
|
+
}
|
|
35
|
+
function weekdayOf(d) {
|
|
36
|
+
return `周${WEEKDAYS_ZH[d.getDay()]}`;
|
|
37
|
+
}
|
|
38
|
+
function weekRow(label, monday) {
|
|
39
|
+
const days = Array.from({
|
|
40
|
+
length: 7
|
|
41
|
+
}, (_, i)=>{
|
|
42
|
+
const d = addDays(monday, i);
|
|
43
|
+
return `${weekdayOf(d)} ${ymd(d)}`;
|
|
44
|
+
});
|
|
45
|
+
return `${label}:${days.join('|')}`;
|
|
46
|
+
}
|
|
47
|
+
function nextOccurrence(todayYmd, year, monthDay) {
|
|
48
|
+
const thisYear = `${year}-${monthDay}`;
|
|
49
|
+
return thisYear >= todayYmd ? thisYear : `${year + 1}-${monthDay}`;
|
|
50
|
+
}
|
|
51
|
+
export function buildTimeAnchor(now = new Date()) {
|
|
52
|
+
const today = ymd(now);
|
|
53
|
+
const year = now.getFullYear();
|
|
54
|
+
const monday = weekMonday(now);
|
|
55
|
+
const tomorrow = addDays(now, 1);
|
|
56
|
+
const dayAfter = addDays(now, 2);
|
|
57
|
+
const inThreeDays = addDays(now, 3);
|
|
58
|
+
const nm = new Date(year, now.getMonth() + 1, 1);
|
|
59
|
+
const nmYear = nm.getFullYear();
|
|
60
|
+
const nmMonth = nm.getMonth();
|
|
61
|
+
const nmLastDay = new Date(nmYear, nmMonth + 1, 0).getDate();
|
|
62
|
+
const mm = String(nmMonth + 1).padStart(2, '0');
|
|
63
|
+
const nextQuarterMonth0 = Math.floor(now.getMonth() / 3) * 3 + 3;
|
|
64
|
+
const nq = new Date(nextQuarterMonth0 >= 12 ? year + 1 : year, nextQuarterMonth0 % 12, 1);
|
|
65
|
+
const festivals = [
|
|
66
|
+
`国庆 ${nextOccurrence(today, year, '10-01')}`,
|
|
67
|
+
`元旦 ${nextOccurrence(today, year, '01-01')}`
|
|
68
|
+
];
|
|
69
|
+
const spring = Object.values(SPRING_FESTIVAL).filter((d)=>d >= today).sort()[0];
|
|
70
|
+
if (spring) festivals.push(`春节 ${spring}`);
|
|
71
|
+
const card = [
|
|
72
|
+
`今天 ${today} ${weekdayOf(now)}(时区 ${tzLabelOf(now)})`,
|
|
73
|
+
`明天 ${ymd(tomorrow)} ${weekdayOf(tomorrow)}|后天 ${ymd(dayAfter)} ${weekdayOf(dayAfter)}|大后天 ${ymd(inThreeDays)} ${weekdayOf(inThreeDays)}`,
|
|
74
|
+
weekRow('本周', monday),
|
|
75
|
+
weekRow('下周', addDays(monday, 7)),
|
|
76
|
+
weekRow('下下周', addDays(monday, 14)),
|
|
77
|
+
`下个月 ${nmYear}-${mm}:初 ${mm}-01~${mm}-10|中旬 ${mm}-11~${mm}-20|下旬 ${mm}-21~${mm}-${nmLastDay}`,
|
|
78
|
+
`下个季度初:${ymd(nq)} 起`,
|
|
79
|
+
`节日锚点:${festivals.join('|')}(「国庆前」= 该日之前,以此类推)`
|
|
80
|
+
].join('\n');
|
|
81
|
+
return {
|
|
82
|
+
today,
|
|
83
|
+
todayWeekdayZh: weekdayOf(now),
|
|
84
|
+
tzLabel: tzLabelOf(now),
|
|
85
|
+
card
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function parseAbsoluteDate(expr, anchorYear) {
|
|
89
|
+
const t = expr.trim();
|
|
90
|
+
let m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
91
|
+
if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`;
|
|
92
|
+
m = t.match(/^(\d{1,2})[./](\d{1,2})(?:日|号)?$/);
|
|
93
|
+
if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`;
|
|
94
|
+
m = t.match(/^(\d{1,2})月(\d{1,2})[日号]?$/);
|
|
95
|
+
if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/time-anchor.ts
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function interpretArgs(args, stringKey) {
|
|
2
|
+
if (args.query && typeof args.query === 'object') return args.query;
|
|
3
|
+
const { query, ...rest } = args;
|
|
4
|
+
if (Object.keys(rest).length > 0) return rest;
|
|
5
|
+
if (typeof query === 'string' && stringKey) return {
|
|
6
|
+
[stringKey]: query
|
|
7
|
+
};
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/tool-packet.ts
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { parseAbsoluteDate } from './time-anchor.js';
|
|
2
|
+
const PRIMARY_DATE_FIELD = {
|
|
3
|
+
requisition: 'start_date',
|
|
4
|
+
flight: 'departure_date',
|
|
5
|
+
hotel: 'check_in_date'
|
|
6
|
+
};
|
|
7
|
+
export function flagExpiredSlots(ext, anchor) {
|
|
8
|
+
const anchorYear = Number(anchor.today.slice(0, 4));
|
|
9
|
+
const missing = [
|
|
10
|
+
...ext.missing_slots
|
|
11
|
+
];
|
|
12
|
+
for (const domain of ext.domains){
|
|
13
|
+
const slots = ext.slots[domain];
|
|
14
|
+
const field = PRIMARY_DATE_FIELD[domain];
|
|
15
|
+
const value = slots?.[field];
|
|
16
|
+
if (typeof value !== 'string') continue;
|
|
17
|
+
const resolved = parseAbsoluteDate(value, anchorYear);
|
|
18
|
+
if (resolved && resolved < anchor.today) {
|
|
19
|
+
const flag = `${domain}.${field} (date is expired)`;
|
|
20
|
+
if (!missing.includes(flag)) missing.push(flag);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
...ext,
|
|
25
|
+
missing_slots: missing
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function buildSlotSystem(anchor) {
|
|
29
|
+
return `你是差旅需求的槽位抽取器。从用户原话抽取槽位,只输出 JSON:
|
|
30
|
+
{"schema_version":"travel_slot_extraction.v1","language":"zh|en|mixed","domains":["requisition"|"flight"|"hotel",...],"slots":{...},"missing_slots":[...]}
|
|
31
|
+
|
|
32
|
+
## 时间锚点卡
|
|
33
|
+
${anchor.card}
|
|
34
|
+
相对日期(明天/下周X/下个月中旬/国庆前…)对应的绝对日期都查锚点卡,不要自己心算。
|
|
35
|
+
|
|
36
|
+
## 铁律
|
|
37
|
+
1. 时间表达逐字保留、不翻译:用户说「下周一」就写「下周一」,说「next Thursday」就写 "next Thursday"——绝不换算成绝对日期、绝不切换语言(换算归下游工具)。模糊时间词(过几天/近期/下周初/隔周/下个月)同样是合法槽位值,逐字保留,**不算缺失**。
|
|
38
|
+
2. language 指**用户原话**的语言(与槽位值用什么语言无关):全中文 zh;全英文 en;中英混排 mixed。即使槽位值按规则 10 含中文城市名,只要原话全英文,language 仍为 en。
|
|
39
|
+
3. 域判定——看用户的**动作动词**,一句话可落多个域:
|
|
40
|
+
- 安排行程/行程推荐/帮我安排 → requisition:{"mode":"create","destination","start_date","end_date"?,"trip_type"}
|
|
41
|
+
- 看/查/订机票、航班、票价 → flight:{"action":"search","origin"?,"destination","departure_date","return_date"?,"trip_type"}
|
|
42
|
+
- 看/订酒店、入住、订房 → hotel:{"action":"search","city","check_in_date","check_out_date"?};「待/住 N 天」只是时长语义,不单独构成 hotel 域
|
|
43
|
+
- 「出差/去 X」本身不构成 requisition;没有上述动词的单城出行意图按回程形态分流:
|
|
44
|
+
「去 X,当天往返」→ 仅 requisition(round_trip);「去 X,周日回/X 号回」跨天回程 → 仅 flight(round_trip)。
|
|
45
|
+
4. trip_type 必填(requisition 与 flight;hotel 不要):出现「往返/回程/当天往返/X 号回」→ "round_trip";否则显式 "one_way"。
|
|
46
|
+
5. 停留天数:「出差/待/住 N 天」→ requisition 用 "duration_days":N;hotel 用 "check_out_date":"<入住表达>+N" 原样拼接。
|
|
47
|
+
6. 回程机票(「住 N 天回 X」「待到周日回 X」):flight.origin=旅行城市, flight.destination=X, flight.departure_date="<入住/到达表达>+N" 或逐字回程表达,只有回程时 trip_type="one_way"。
|
|
48
|
+
7. 价格**排序**偏好只在找低价时加(「哪天便宜/比较便宜/最便宜」→ flight "sort_name":"PRICE","sort_type":"ASC");单纯「看价格/多少钱」不加。
|
|
49
|
+
8. 出发时段偏好(「早上七点后出发」)→ flight 加 "departure_time_pref":"07:00后"(HH:MM后 格式)。
|
|
50
|
+
9. 多目的地:
|
|
51
|
+
- 国内多城链路(≥3 城):requisition.destination 与 flight.destination 都写**全程城市数组**(含首站),flight 不写 origin,departure_date=首段出发表达,加 "multi_destination":true;不为链路每段单独建 hotel/子段。
|
|
52
|
+
- 国际开口程(open-jaw):flight.origin=首站(单值), flight.destination=后续链路数组, 加 "multi_destination":true。
|
|
53
|
+
10. 城市名:纯国内行程用中文标准名(北京/上海/广州/深圳…,即使原文是拼音);国际或跨国多城链路的城市名保留用户原文。
|
|
54
|
+
11. 关键槽位缺失(如有机票意图但无目的地)→ missing_slots 加 "<domain>.<field>";不缺则空数组。
|
|
55
|
+
12. 过期判定不归你:绝对日期是否已过由下游确定性层处理,你只管逐字抽取。
|
|
56
|
+
|
|
57
|
+
## 示例(学习约定,勿照抄内容)
|
|
58
|
+
用户:下周五入住上海,住三天回杭州
|
|
59
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["hotel","flight"],"slots":{"hotel":{"action":"search","city":"上海","check_in_date":"下周五","check_out_date":"下周五+3"},"flight":{"action":"search","origin":"上海","destination":"杭州","departure_date":"下周五+3","trip_type":"one_way"}},"missing_slots":[]}
|
|
60
|
+
|
|
61
|
+
用户:next Monday 去上海,订酒店
|
|
62
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"mixed","domains":["hotel"],"slots":{"hotel":{"action":"search","city":"上海","check_in_date":"next Monday"}},"missing_slots":[]}
|
|
63
|
+
|
|
64
|
+
用户:这阵子想去广州,帮我看下深圳到广州的机票
|
|
65
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["flight"],"slots":{"flight":{"action":"search","origin":"深圳","destination":"广州","departure_date":"这阵子","trip_type":"one_way"}},"missing_slots":[]}
|
|
66
|
+
|
|
67
|
+
用户:8.20 日去杭州,当天往返
|
|
68
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["requisition"],"slots":{"requisition":{"mode":"create","destination":"杭州","start_date":"8.20","trip_type":"round_trip"}},"missing_slots":[]}
|
|
69
|
+
|
|
70
|
+
用户:下周三去杭州,周六回
|
|
71
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["flight"],"slots":{"flight":{"action":"search","destination":"杭州","departure_date":"下周三","return_date":"周六","trip_type":"round_trip"}},"missing_slots":[]}
|
|
72
|
+
|
|
73
|
+
用户:下周三去成都,待到周六,周六去西安待一天然后回杭州
|
|
74
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["requisition","flight"],"slots":{"requisition":{"mode":"create","destination":["成都","西安","杭州"],"start_date":"下周三","trip_type":"round_trip"},"flight":{"action":"search","destination":["成都","西安","杭州"],"departure_date":"下周三","multi_destination":true,"trip_type":"round_trip"}},"missing_slots":[]}
|
|
75
|
+
|
|
76
|
+
用户:open-jaw please: Shanghai to Tokyo next Monday, back from Osaka to Shanghai next Friday
|
|
77
|
+
输出:{"schema_version":"travel_slot_extraction.v1","language":"en","domains":["flight"],"slots":{"flight":{"action":"search","origin":"Shanghai","destination":["Tokyo","Osaka","Shanghai"],"departure_date":"next Monday","return_date":"next Friday","multi_destination":true,"trip_type":"round_trip"}},"missing_slots":[]}
|
|
78
|
+
|
|
79
|
+
只输出 JSON,不要多余文本。`;
|
|
80
|
+
}
|
|
81
|
+
export function detectLanguage(utterance) {
|
|
82
|
+
const hasCjk = /[一-鿿]/.test(utterance);
|
|
83
|
+
const hasLatin = /[A-Za-z]/.test(utterance);
|
|
84
|
+
if (hasCjk && hasLatin) return 'mixed';
|
|
85
|
+
if (hasLatin) return 'en';
|
|
86
|
+
return 'zh';
|
|
87
|
+
}
|
|
88
|
+
export function normalizeExtraction(obj, utterance) {
|
|
89
|
+
const domains = obj['domains'];
|
|
90
|
+
if (!Array.isArray(domains) || domains.length === 0) return null;
|
|
91
|
+
const valid = domains.filter((d)=>d === 'requisition' || d === 'flight' || d === 'hotel');
|
|
92
|
+
if (valid.length === 0) return null;
|
|
93
|
+
const lang = obj['language'];
|
|
94
|
+
const slots = obj['slots'] ?? {};
|
|
95
|
+
const missing = obj['missing_slots'];
|
|
96
|
+
return {
|
|
97
|
+
schema_version: 'travel_slot_extraction.v1',
|
|
98
|
+
language: utterance !== undefined ? detectLanguage(utterance) : lang === 'en' || lang === 'mixed' ? lang : 'zh',
|
|
99
|
+
domains: valid,
|
|
100
|
+
slots,
|
|
101
|
+
missing_slots: Array.isArray(missing) ? missing.map(String) : []
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const sortedSet = (xs)=>[
|
|
105
|
+
...xs
|
|
106
|
+
].sort();
|
|
107
|
+
export function scoreExtraction(expected, actual) {
|
|
108
|
+
const diffs = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
if (expected.language !== actual.language) {
|
|
111
|
+
diffs.push(`language: 期望 ${expected.language},实际 ${actual.language}`);
|
|
112
|
+
}
|
|
113
|
+
if (JSON.stringify(sortedSet(expected.domains)) !== JSON.stringify(sortedSet(actual.domains))) {
|
|
114
|
+
diffs.push(`domains: 期望 [${sortedSet(expected.domains)}],实际 [${sortedSet(actual.domains)}]`);
|
|
115
|
+
}
|
|
116
|
+
for (const domain of expected.domains){
|
|
117
|
+
const exp = expected.slots[domain];
|
|
118
|
+
const act = actual.slots[domain];
|
|
119
|
+
if (!exp) continue;
|
|
120
|
+
if (!act) {
|
|
121
|
+
diffs.push(`slots.${domain}: 实际缺失整个域`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const [k, v] of Object.entries(exp)){
|
|
125
|
+
if (JSON.stringify(act[k]) !== JSON.stringify(v)) {
|
|
126
|
+
diffs.push(`slots.${domain}.${k}: 期望 ${JSON.stringify(v)},实际 ${JSON.stringify(act[k])}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const k of Object.keys(act)){
|
|
130
|
+
if (!(k in exp)) warnings.push(`slots.${domain}.${k}: 实际多出键 ${JSON.stringify(act[k])}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (JSON.stringify(sortedSet(expected.missing_slots)) !== JSON.stringify(sortedSet(actual.missing_slots))) {
|
|
134
|
+
diffs.push(`missing_slots: 期望 [${expected.missing_slots}],实际 [${actual.missing_slots}]`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
pass: diffs.length === 0,
|
|
138
|
+
diffs,
|
|
139
|
+
warnings
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/travel-slots.ts
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseAbsoluteDate, ymd } from './time-anchor.js';
|
|
2
|
+
import { resolveSlotDate } from './slot-spec.js';
|
|
3
|
+
export function resolveTimelineDate(expr, anchor) {
|
|
4
|
+
return resolveSlotDate(expr, anchor).date;
|
|
5
|
+
}
|
|
6
|
+
export function makeTripId(ev) {
|
|
7
|
+
return `${ev.destination}|${ev.start}|${ev.source}`.replace(/\s+/g, '_');
|
|
8
|
+
}
|
|
9
|
+
export function appendTrip(events, ev) {
|
|
10
|
+
if (!ev.destination?.trim()) return {
|
|
11
|
+
events,
|
|
12
|
+
appended: false,
|
|
13
|
+
reason: 'destination 必填'
|
|
14
|
+
};
|
|
15
|
+
if (!ev.evidence?.trim()) return {
|
|
16
|
+
events,
|
|
17
|
+
appended: false,
|
|
18
|
+
reason: 'evidence 必填(用户原话或 wish 确认指针)'
|
|
19
|
+
};
|
|
20
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(ev.start)) return {
|
|
21
|
+
events,
|
|
22
|
+
appended: false,
|
|
23
|
+
reason: 'start 必须为 YYYY-MM-DD(词表外日期由上游解析,不猜)'
|
|
24
|
+
};
|
|
25
|
+
if (ev.end && ev.end < ev.start) return {
|
|
26
|
+
events,
|
|
27
|
+
appended: false,
|
|
28
|
+
reason: 'end 早于 start'
|
|
29
|
+
};
|
|
30
|
+
const tripId = makeTripId(ev);
|
|
31
|
+
if (events.some((e)=>e.trip_id === tripId)) return {
|
|
32
|
+
events,
|
|
33
|
+
appended: false,
|
|
34
|
+
tripId
|
|
35
|
+
};
|
|
36
|
+
const overlap = events.find((e)=>e.destination === ev.destination && !(e.end && e.end < ev.start) && !(ev.end && ev.end < e.start));
|
|
37
|
+
if (overlap) return {
|
|
38
|
+
events,
|
|
39
|
+
appended: false,
|
|
40
|
+
tripId: overlap.trip_id,
|
|
41
|
+
reason: `与已有行程 ${overlap.trip_id} 日期重叠,冲突即停`
|
|
42
|
+
};
|
|
43
|
+
const full = {
|
|
44
|
+
schema: 'travel_timeline.v1',
|
|
45
|
+
trip_id: tripId,
|
|
46
|
+
ts: new Date().toISOString(),
|
|
47
|
+
...ev
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
events: [
|
|
51
|
+
...events,
|
|
52
|
+
full
|
|
53
|
+
],
|
|
54
|
+
appended: true,
|
|
55
|
+
tripId
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function projectTimeline(events) {
|
|
59
|
+
return [
|
|
60
|
+
...events
|
|
61
|
+
].sort((a, b)=>a.start < b.start ? 1 : -1).map((e)=>({
|
|
62
|
+
tripId: e.trip_id,
|
|
63
|
+
destination: e.destination,
|
|
64
|
+
start: e.start,
|
|
65
|
+
end: e.end,
|
|
66
|
+
companions: e.companions
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
export function timelineGapsForVerified(timeline, verifiedWishes) {
|
|
70
|
+
const dests = new Set(timeline.map((t)=>t.destination));
|
|
71
|
+
return verifiedWishes.filter((w)=>w.destination && !dests.has(w.destination)).map((w)=>({
|
|
72
|
+
wishId: w.wishId
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
export { ymd };
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/travel-timeline.ts
|
package/dist/src/unified.js
CHANGED
|
@@ -179,7 +179,9 @@ function evaluateOptionMove(segId, mv) {
|
|
|
179
179
|
let energy;
|
|
180
180
|
if (mv.redEye && (mv.redEyeDurationMin ?? 0) > 0) {
|
|
181
181
|
const sleepH = ((mv.redEyeDurationMin ?? 0) - 60) / 60;
|
|
182
|
-
|
|
182
|
+
const groundH = (mv.groundRecoveryMin ?? mv.destTransferMin ?? 0) / 60;
|
|
183
|
+
const recovery = Math.min(5, 5 * groundH);
|
|
184
|
+
energy = Math.max(30, Math.min(75 + recovery, 80, 30 + 8 * sleepH + recovery));
|
|
183
185
|
} else {
|
|
184
186
|
energy = 100 - 2 * 8;
|
|
185
187
|
if (wake < 5 * 60) energy -= 30;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function scoreWishMatch(entry, ctx) {
|
|
2
|
+
const c = entry.conditions ?? {};
|
|
3
|
+
const hits = [];
|
|
4
|
+
if (typeof ctx.days === 'number' && typeof c.days === 'number' && ctx.days >= c.days) hits.push(`days≥${c.days}`);
|
|
5
|
+
if (typeof ctx.budgetCny === 'number' && typeof c.budget_cny === 'number' && ctx.budgetCny >= c.budget_cny) hits.push(`budget≥${c.budget_cny}`);
|
|
6
|
+
if (Array.isArray(c.best_months) && typeof ctx.month === 'number' && c.best_months.map(Number).includes(ctx.month)) hits.push(`month=${ctx.month}`);
|
|
7
|
+
if (hits.length === 0) return null;
|
|
8
|
+
return {
|
|
9
|
+
entry,
|
|
10
|
+
wishId: String(entry.wish_id ?? ''),
|
|
11
|
+
score: hits.length,
|
|
12
|
+
hits
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function pickNudgeWish(pool, ctx) {
|
|
16
|
+
const scored = pool.filter((e)=>!e.muted && typeof e.wish_id === 'string').map((e)=>scoreWishMatch(e, ctx)).filter((m)=>m !== null).sort((a, b)=>{
|
|
17
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
18
|
+
const ta = String(a.entry.added_at ?? '');
|
|
19
|
+
const tb = String(b.entry.added_at ?? '');
|
|
20
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
21
|
+
return a.wishId < b.wishId ? -1 : 1;
|
|
22
|
+
});
|
|
23
|
+
return scored[0] ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/wish-pool.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danceiny/gotry",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.9",
|
|
4
4
|
"description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm 包入口 + vendored dsh runtime + 5 行 README 安装路径。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ts/src/index.ts",
|
|
@@ -37,7 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@deepseek-ai/dsh": "^0.1.1-rc.1",
|
|
40
|
-
"z3-solver": "^5.2.0"
|
|
40
|
+
"z3-solver": "^5.2.0",
|
|
41
|
+
"dsh-map-tools": "^0.4.4",
|
|
42
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
44
|
+
"dsh-calendar": "^0.3.2"
|
|
41
45
|
},
|
|
42
46
|
"repository": {
|
|
43
47
|
"type": "git",
|
package/ts/capabilities/hbcli.ts
CHANGED
|
@@ -120,14 +120,14 @@ export async function searchHotels(
|
|
|
120
120
|
query: { destination: string; checkIn?: string; checkOut?: string; adults?: number },
|
|
121
121
|
opts: HbcliCallOptions & { fallbackPath?: string } = {},
|
|
122
122
|
): Promise<HbcliCallResult & { hotels?: unknown; summary: string }> {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (query.
|
|
127
|
-
if (query.adults) hbArgs.push('--
|
|
123
|
+
// 旗标对齐上游 CLI v0.3.0(命令树扁平化后):--destination-name + --room-occupancies;
|
|
124
|
+
// hotel-list 无日期旗标(房价按上游当前窗口),checkIn/checkOut 留给 hotel-rates 跟进
|
|
125
|
+
const hbArgs = ['search', 'hotel-list', '--json', '--page-size', '10']
|
|
126
|
+
if (query.destination) hbArgs.push('--destination-name', query.destination)
|
|
127
|
+
if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]))
|
|
128
128
|
const live = await callHbcliJson(hbArgs, opts)
|
|
129
129
|
if (live.via === 'hbcli-realtime') {
|
|
130
|
-
return { ...live, hotels: live.result, summary: `${query.destination}:hbcli
|
|
130
|
+
return { ...live, hotels: live.result, summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}` }
|
|
131
131
|
}
|
|
132
132
|
// 降级:读静态包
|
|
133
133
|
const fallback = opts.fallbackPath
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
16
16
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import type { ToolFailure } from '../src/tool-packet.ts'
|
|
18
19
|
|
|
19
20
|
export type IncidentKind = 'uncaughtException' | 'unhandledRejection' | 'plugin_error' | 'tool_execute_error'
|
|
20
21
|
|
|
@@ -74,7 +75,7 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
|
|
|
74
75
|
installed = true
|
|
75
76
|
logPath = resolveIncidentsPath(stateRoot)
|
|
76
77
|
|
|
77
|
-
const uncaughtHandler = (err: Error, origin: NodeJS.
|
|
78
|
+
const uncaughtHandler = (err: Error, origin: NodeJS.UncaughtExceptionOrigin | 'uncaughtException') => {
|
|
78
79
|
recordIncident({
|
|
79
80
|
ts: new Date().toISOString(),
|
|
80
81
|
kind: 'uncaughtException',
|
|
@@ -111,6 +112,7 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
|
|
|
111
112
|
* 工具执行面异常隔离(D-NEW gotry 侧收尾):dsh 的一个工具 execute 抛错/拒绝
|
|
112
113
|
* 会沿 cordis 传到主循环,拖垮整个会话。包装后:降级为结构化错误返回给 LLM、
|
|
113
114
|
* 事故落盘,永不向上抛。落盘失败也不抛(双保险,仍返回结构化错误)。
|
|
115
|
+
* 失败形状 = ToolFailure(RFC S1 observation envelope 的失败分支,编译期对齐)。
|
|
114
116
|
*/
|
|
115
117
|
export function guardToolExecute<A, R>(name: string, stateRoot: string, execute: (args: A, exec: unknown) => R | Promise<R>): (args: A, exec: unknown) => Promise<R> {
|
|
116
118
|
return async (args: A, exec: unknown): Promise<R> => {
|
|
@@ -125,11 +127,12 @@ export function guardToolExecute<A, R>(name: string, stateRoot: string, execute:
|
|
|
125
127
|
stack: (err.stack ?? '').slice(0, 4000),
|
|
126
128
|
source: name,
|
|
127
129
|
}, stateRoot)
|
|
128
|
-
|
|
130
|
+
const failure: ToolFailure = {
|
|
129
131
|
ok: false,
|
|
130
132
|
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${err.message.slice(0, 300)}`,
|
|
131
133
|
evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`,
|
|
132
|
-
}
|
|
134
|
+
}
|
|
135
|
+
return failure as R
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
package/ts/package.json
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
"@deepseek-ai/cordis": "*",
|
|
14
14
|
"@deepseek-ai/dsh-tools": "*",
|
|
15
15
|
"@deepseek-ai/schemastery": "*",
|
|
16
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
17
|
+
"better-sqlite3": "^13.0.3",
|
|
16
18
|
"z3-solver": "^5.2.0"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
19
21
|
"@types/node": "^22.0.0",
|
|
22
|
+
"playwright-core": "^1.62.1",
|
|
20
23
|
"tsx": "^4.19.0",
|
|
21
24
|
"typescript": "^5.6.0"
|
|
22
25
|
}
|
|
@@ -40,6 +40,10 @@ export async function checkConnectivity(a: string, b: string): Promise<{ connect
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// CLI 直跑
|
|
43
|
-
|
|
43
|
+
// 仅作为直接 CLI 入口时执行(被 import 时不触发:此前 dsh 子进程的
|
|
44
|
+
// argv[2]/[3](--profile headless)恰好真值,每次启动白跑一次探测还污染 stdout)
|
|
45
|
+
import { pathToFileURL } from 'node:url'
|
|
46
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
47
|
+
if (isMain && process.argv[2] && process.argv[3]) {
|
|
44
48
|
console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence)
|
|
45
49
|
}
|
package/ts/src/dsh-llm.ts
CHANGED
|
@@ -10,18 +10,22 @@
|
|
|
10
10
|
import type { LlmPort } from './loop.ts'
|
|
11
11
|
import type { InterviewQuestion, TravelerProfile, TripState, Turn, CalendarState } from './contracts.ts'
|
|
12
12
|
import { parseFlightPackToSpec, type JourneySpecTS } from './unified.ts'
|
|
13
|
+
import { buildTimeAnchor } from './time-anchor.ts'
|
|
14
|
+
import { buildSlotSystem, flagExpiredSlots, normalizeExtraction, type TravelSlotExtraction } from './travel-slots.ts'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// 惰性读取(env 在调用时取值):模块顶常量会在 .env 加载前冻结(ESM import 提升),
|
|
17
|
+
// 脚本先 loadEnv 再 import 也救不了——401 错配(key 发往默认端点)的存量隐患由此根除。
|
|
18
|
+
const model = () => process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2'
|
|
19
|
+
const base = () => (process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '')
|
|
16
20
|
|
|
17
21
|
async function chat(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>, json: boolean): Promise<string> {
|
|
18
22
|
const key = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY']
|
|
19
23
|
if (!key) throw new Error('LLM_API_KEY 未设置(兼容 DEEPSEEK_API_KEY 别名)——真 LLM 路径不可用,请回退 mock(ADR-8)')
|
|
20
|
-
const res = await fetch(`${
|
|
24
|
+
const res = await fetch(`${base()}/chat/completions`, {
|
|
21
25
|
method: 'POST',
|
|
22
26
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
|
|
23
27
|
body: JSON.stringify({
|
|
24
|
-
model:
|
|
28
|
+
model: model(),
|
|
25
29
|
messages,
|
|
26
30
|
...(json ? { response_format: { type: 'json_object' } } : {}),
|
|
27
31
|
temperature: json ? 0 : 0.7,
|
|
@@ -58,13 +62,16 @@ const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程
|
|
|
58
62
|
规则:每个跨城移动一段;锚点只放用户明说或必然的(如"当天到"→arriveByMin 23:59=1439);时刻用当日分钟。
|
|
59
63
|
scenario 判定:「洱海/大理/千岛湖/太湖+选目的地」→erhai(候选集);「普吉/workation/远程办公+多城链」→workation(五段链);「云南/大理丽江」→yunnan;不确定→generic。只输出 JSON。`
|
|
60
64
|
|
|
61
|
-
export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
65
|
+
export function createOpenAICompatLlm(flightPackPath?: string, clock: () => Date = () => new Date()): LlmPort {
|
|
62
66
|
const pack = flightPackPath
|
|
63
67
|
const historyText = (h: Turn[]) => h.map(t => `${t.role === 'user' ? '用户' : '助手'}: ${t.text}`).join('\n')
|
|
68
|
+
// 时间锚点卡:每条抽取链路都带上「今天」——legacy 路径此前无时间注入,
|
|
69
|
+
// 过期/相对日期语义全靠它(算术在 time-anchor 层,LLM 只查卡)。
|
|
70
|
+
const anchorContext = () => `时间锚点卡:\n${buildTimeAnchor(clock()).card}`
|
|
64
71
|
return {
|
|
65
72
|
async extractFacts(history) {
|
|
66
73
|
const out = await chat(
|
|
67
|
-
[{ role: 'system', content: FACTS_SYSTEM }, { role: 'user', content: historyText(history) }],
|
|
74
|
+
[{ role: 'system', content: FACTS_SYSTEM }, { role: 'user', content: `${anchorContext()}\n\n${historyText(history)}` }],
|
|
68
75
|
true,
|
|
69
76
|
)
|
|
70
77
|
const obj = parseJsonBlock(out)
|
|
@@ -76,7 +83,7 @@ export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
|
76
83
|
},
|
|
77
84
|
async extractSpec(history, state) {
|
|
78
85
|
// 架构(ADR-10):LLM 只产骨架与锚点;班次数据永远来自能力层(数据包/未来实时API)
|
|
79
|
-
const context =
|
|
86
|
+
const context = `${anchorContext()}\n已断言日历:${JSON.stringify(state.calendar.assertedWeekdays)}\nprofile:${JSON.stringify(state.profile)}`
|
|
80
87
|
const out = await chat(
|
|
81
88
|
[{ role: 'system', content: SKELETON_SYSTEM }, { role: 'user', content: `${context}\n\n${historyText(history)}` }],
|
|
82
89
|
true,
|
|
@@ -123,6 +130,19 @@ export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
|
123
130
|
packSpec.budgetCny = 9000
|
|
124
131
|
return packSpec
|
|
125
132
|
},
|
|
133
|
+
async extractSlots(history, now?: Date): Promise<TravelSlotExtraction | null> {
|
|
134
|
+
// now 可注入(评测固定锚点);过期判定在 flagExpiredSlots(代码层),模型只管逐字抽取
|
|
135
|
+
const anchor = buildTimeAnchor(now ?? clock())
|
|
136
|
+
const out = await chat(
|
|
137
|
+
[{ role: 'system', content: buildSlotSystem(anchor) }, { role: 'user', content: historyText(history) }],
|
|
138
|
+
true,
|
|
139
|
+
)
|
|
140
|
+
const obj = parseJsonBlock(out)
|
|
141
|
+
if (!obj) return null
|
|
142
|
+
// language 判定归代码层(detectLanguage),模型输出仅供参考;过期判定同在代码层
|
|
143
|
+
const ext = normalizeExtraction(obj, history.map(t => t.text).join('\n'))
|
|
144
|
+
return ext ? flagExpiredSlots(ext, anchor) : null
|
|
145
|
+
},
|
|
126
146
|
async polishQuestion(q: InterviewQuestion) {
|
|
127
147
|
const out = await chat(
|
|
128
148
|
[{ role: 'system', content: '把旅行规划的追问润色得更自然,保留全部信息(含为什么问),一两句话,不要加表情。' },
|