@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.8
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 +27 -74
- package/bin/gotry-inner.js +50 -1
- package/bin/gotry-stdio-ask.js +59 -0
- package/cordis.gotry-patch.yml +56 -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/hbcli.js +11 -6
- package/dist/capabilities/incident-log.js +2 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +18 -2
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-metrics.js +38 -0
- package/dist/scripts/nudge-digest.js +93 -0
- package/dist/scripts/probe-poi-tests.js +9 -0
- package/dist/scripts/replay.js +73 -1
- package/dist/scripts/skeleton-check.js +3 -1
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +201 -3
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/src/dsh-llm.js +27 -7
- package/dist/src/index.js +342 -92
- package/dist/src/loop.js +37 -10
- package/dist/src/memory-capture.js +40 -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/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- 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/scripts/skeleton-check.ts +5 -1
- package/ts/src/dsh-llm.ts +27 -7
- package/ts/src/index.ts +292 -77
- package/ts/src/loop.ts +52 -17
- package/ts/src/mock-llm.ts +13 -1
- package/ts/cordis.gotry-patch.yml +0 -36
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
2
4
|
import z from '@deepseek-ai/schemastery';
|
|
3
5
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
6
|
import { ensureStateDir, readJson, recordLatency, writeJson } from './bridge.js';
|
|
@@ -7,6 +9,12 @@ import { checkConnectivity } from '../scripts/skeleton-check.js';
|
|
|
7
9
|
import { parseCandidate, parseRequest } from './model.js';
|
|
8
10
|
import { searchHotels as hbcliSearchHotels } from '../capabilities/hbcli.js';
|
|
9
11
|
import { installProcessGuards, guardToolExecute } from '../capabilities/incident-log.js';
|
|
12
|
+
import { interpretArgs } from './tool-packet.js';
|
|
13
|
+
import { appendEvent, projectUtility } from './memory-utility.js';
|
|
14
|
+
import { pickNudgeWish } from './wish-pool.js';
|
|
15
|
+
import { mergeProfile } from './memory-capture.js';
|
|
16
|
+
import { buildTimeAnchor } from './time-anchor.js';
|
|
17
|
+
import { resolveSlotDate } from './slot-spec.js';
|
|
10
18
|
import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
|
|
11
19
|
import { verifyFlight } from '../capabilities/opensky.js';
|
|
12
20
|
import { anythingSearch } from '../capabilities/anything.js';
|
|
@@ -22,6 +30,25 @@ export const Config = z.object({
|
|
|
22
30
|
timeoutMs: z.number().default(30_000),
|
|
23
31
|
hbcliBin: z.string().default('hbcli')
|
|
24
32
|
});
|
|
33
|
+
const unwrapQuery = interpretArgs;
|
|
34
|
+
function renderMotivationBrief(stateRoot) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(join(stateRoot, 'gotry-state', 'motivation-profile.json'), 'utf-8');
|
|
37
|
+
const p = JSON.parse(raw);
|
|
38
|
+
const lines = [
|
|
39
|
+
'## 用户记忆(跨会话画像;与用户当轮说法冲突时以用户为准,更新经 gotry_motivation_save)'
|
|
40
|
+
];
|
|
41
|
+
const weights = Object.entries(p.weights ?? {});
|
|
42
|
+
if (weights.length) lines.push(`- 动机权重: ${weights.map(([k, v])=>`${k}=${v}`).join(', ')}(证据 ${p.evidence?.length ?? 0} 条)`);
|
|
43
|
+
const hard = Object.entries(p.hard ?? {});
|
|
44
|
+
if (hard.length) lines.push(`- 硬约束: ${hard.map(([k, v])=>`${k}=${String(v)}`).join(', ')}`);
|
|
45
|
+
lines.push(`- 愿望池: 用 gotry_wish_pool_list 按条件召回(0..1),勿直接堆砌`);
|
|
46
|
+
if (p.updated_at) lines.push(`- 更新于: ${p.updated_at}`);
|
|
47
|
+
return weights.length || hard.length ? lines.join('\n') : '';
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
25
52
|
export function apply(ctx, config) {
|
|
26
53
|
const sp = ctx['systemPrompt'];
|
|
27
54
|
sp?.variable?.('current_date', ()=>{
|
|
@@ -38,6 +65,8 @@ export function apply(ctx, config) {
|
|
|
38
65
|
];
|
|
39
66
|
return `${ymd} 周${weekdays[d.getDay()]}`;
|
|
40
67
|
});
|
|
68
|
+
sp?.variable?.('time_anchor_card', ()=>buildTimeAnchor(new Date()).card);
|
|
69
|
+
sp?.variable?.('motivation_brief', ()=>renderMotivationBrief(config.stateRoot ?? '.'));
|
|
41
70
|
installProcessGuards(config.stateRoot ?? '.', {
|
|
42
71
|
uncaughtException: 'gotry-tools',
|
|
43
72
|
unhandledRejection: 'gotry-tools'
|
|
@@ -53,7 +82,7 @@ export function apply(ctx, config) {
|
|
|
53
82
|
};
|
|
54
83
|
registerGuarded(defineTool({
|
|
55
84
|
name: 'gotry_feasibility_check',
|
|
56
|
-
description: 'Check travel candidates against the user\'s motivation and hard constraints using the ' + 'door-to-door true-cost engine (wake time, arrival state, energy, usable hours, money). ' + 'Input is the structured request (motivation weights + hard constraints + window + budget + home hubs) ' + 'and the candidate list (services/transfers/stay costs/min days),
|
|
85
|
+
description: 'Check travel candidates against the user\'s motivation and hard constraints using the ' + 'door-to-door true-cost engine (wake time, arrival state, energy, usable hours, money). ' + 'Input is the structured request (motivation weights + hard constraints + window + budget + home hubs) ' + 'and the candidate list (services/transfers/stay costs/min days): ' + 'structure { request: { motivation weights, hard constraints, window, budget, home hubs }, candidates: [ { id, label, services, transfers, stay, minDays } ] }. ' + 'Returns per-candidate verdicts, unsat cores with minimal-modification suggestions, ' + 'a wish-pool entry for infeasible aspirations, and a ready-to-show markdown answer.',
|
|
57
86
|
parameters: {
|
|
58
87
|
payload: {
|
|
59
88
|
type: 'json',
|
|
@@ -82,6 +111,7 @@ export function apply(ctx, config) {
|
|
|
82
111
|
const dir = await ensureStateDir(config.stateRoot);
|
|
83
112
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(()=>{});
|
|
84
113
|
return {
|
|
114
|
+
ok: true,
|
|
85
115
|
...result,
|
|
86
116
|
latency_ms: Date.now() - started,
|
|
87
117
|
via: 'in-process-unified'
|
|
@@ -90,13 +120,32 @@ export function apply(ctx, config) {
|
|
|
90
120
|
presentCall: (args)=>({
|
|
91
121
|
card: 'generic',
|
|
92
122
|
title: 'GoTry 可行性检查(门到门全成本)',
|
|
93
|
-
kind: '
|
|
123
|
+
kind: 'execute',
|
|
94
124
|
rawInput: args.payload
|
|
95
|
-
})
|
|
125
|
+
}),
|
|
126
|
+
presentResult: (args, value)=>{
|
|
127
|
+
const r = value;
|
|
128
|
+
const budget = args.payload?.request?.budget_cny;
|
|
129
|
+
const lines = (r.verdicts ?? []).map((v)=>{
|
|
130
|
+
const tc = v.true_cost ?? {};
|
|
131
|
+
const money = typeof tc.money_cny === 'number' ? ` ¥${tc.money_cny}/人` + (typeof budget === 'number' ? `(预算 ¥${budget},${tc.money_cny <= budget ? '余' : '超'} ¥${Math.abs(budget - tc.money_cny)})` : '') : '';
|
|
132
|
+
return v.feasible ? `✅ ${String(v.name ?? v.candidate_id)}${money}${v.candidate_id === r.recommended ? ' ← 推荐' : ''}` : `❌ ${String(v.name ?? v.candidate_id)} — ${Array.isArray(v.unsat_core) ? v.unsat_core.join(',') : '不可行'}`;
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
card: 'generic',
|
|
136
|
+
title: `可行性:${r.recommended ? `推荐 ${r.recommended}` : '全部不可行'}`,
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: (lines.length ? lines.join('\n') + '\n\n' : '') + String(r.answer_md ?? '').slice(0, 1500)
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
96
145
|
}));
|
|
97
146
|
registerGuarded(defineTool({
|
|
98
147
|
name: 'gotry_motivation_save',
|
|
99
|
-
description: 'Persist the traveler\'s motivation profile (the "why depart" contract object). ' + 'This is the B2B reuse seam: downstream plugins consume only MotivationProfile + constraints, ' + 'never the principal/sponsor distinction.
|
|
148
|
+
description: 'Persist the traveler\'s motivation profile (the "why depart" contract object). ' + 'This is the B2B reuse seam: downstream plugins consume only MotivationProfile + constraints, ' + 'never the principal/sponsor distinction. MERGE semantics (T1): call again with just the NEW ' + 'facts learned this turn (weights delta optional but MUST bring fresh evidence; evidence = user quotes); ' + 'existing history is never deleted. Requires evidence on every call (P0 anti-fabrication rule).',
|
|
100
149
|
parameters: {
|
|
101
150
|
profile: {
|
|
102
151
|
type: 'json',
|
|
@@ -106,19 +155,7 @@ export function apply(ctx, config) {
|
|
|
106
155
|
},
|
|
107
156
|
output: {
|
|
108
157
|
schema: {
|
|
109
|
-
type: '
|
|
110
|
-
additionalProperties: false,
|
|
111
|
-
properties: {
|
|
112
|
-
saved: {
|
|
113
|
-
type: 'boolean'
|
|
114
|
-
},
|
|
115
|
-
path: {
|
|
116
|
-
type: 'string'
|
|
117
|
-
},
|
|
118
|
-
profile: {
|
|
119
|
-
type: 'json'
|
|
120
|
-
}
|
|
121
|
-
}
|
|
158
|
+
type: 'json'
|
|
122
159
|
},
|
|
123
160
|
render: (_args, value)=>[
|
|
124
161
|
{
|
|
@@ -128,55 +165,67 @@ export function apply(ctx, config) {
|
|
|
128
165
|
]
|
|
129
166
|
},
|
|
130
167
|
async execute (args, _exec) {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
168
|
+
const incoming = args.profile ?? {};
|
|
169
|
+
if (!incoming.evidence?.length) {
|
|
133
170
|
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)');
|
|
134
171
|
}
|
|
135
172
|
const dir = await ensureStateDir(config.stateRoot);
|
|
136
173
|
const path = join(dir, 'motivation-profile.json');
|
|
174
|
+
let existing = null;
|
|
175
|
+
try {
|
|
176
|
+
existing = await readJson(path, null);
|
|
177
|
+
} catch {}
|
|
178
|
+
const merged = mergeProfile(existing, {
|
|
179
|
+
weights: incoming.weights,
|
|
180
|
+
evidence: incoming.evidence,
|
|
181
|
+
hard: incoming.hard
|
|
182
|
+
});
|
|
183
|
+
if (!merged) {
|
|
184
|
+
const currentJson = JSON.parse(JSON.stringify({
|
|
185
|
+
...existing ?? {},
|
|
186
|
+
updated_at: new Date().toISOString()
|
|
187
|
+
}));
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
saved: false,
|
|
191
|
+
path,
|
|
192
|
+
profile: currentJson,
|
|
193
|
+
summary: '无新内容(幂等跳过)'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
137
196
|
const saved = JSON.parse(JSON.stringify({
|
|
138
|
-
...
|
|
197
|
+
...merged,
|
|
139
198
|
updated_at: new Date().toISOString()
|
|
140
199
|
}));
|
|
141
200
|
await writeJson(path, saved);
|
|
142
201
|
return {
|
|
202
|
+
ok: true,
|
|
143
203
|
saved: true,
|
|
144
204
|
path,
|
|
145
|
-
profile: saved
|
|
205
|
+
profile: saved,
|
|
206
|
+
summary: '画像已合并落盘'
|
|
146
207
|
};
|
|
147
208
|
},
|
|
148
209
|
presentCall: (args)=>({
|
|
149
210
|
card: 'generic',
|
|
150
211
|
title: '保存动机画像',
|
|
151
|
-
kind: '
|
|
212
|
+
kind: 'edit',
|
|
152
213
|
rawInput: args.profile
|
|
153
214
|
})
|
|
154
215
|
}));
|
|
155
216
|
registerGuarded(defineTool({
|
|
156
217
|
name: 'gotry_wish_pool_add',
|
|
157
|
-
description: 'Add an aspiration to the "next departure" wish pool — the graceful home for infeasible dreams. ' + 'An entry carries its fulfilment conditions (days needed, budget, best months) so a future ' + '"next departure" nudge can fire when the window matches. 憧憬不被拒绝。',
|
|
218
|
+
description: 'Add an aspiration to the "next departure" wish pool — the graceful home for infeasible dreams. ' + 'An entry carries its fulfilment conditions (days needed, budget, best months) so a future ' + '"next departure" nudge can fire when the window matches. Each entry gets a stable wish_id ' + '(the memory-utility sidecar keys on it); muted:true puts a wish dormant (永不删除,只是不再召回). 憧憬不被拒绝。',
|
|
158
219
|
parameters: {
|
|
159
220
|
entry: {
|
|
160
221
|
type: 'json',
|
|
161
222
|
required: true,
|
|
162
|
-
description: '{ name, reason
|
|
223
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months }, muted?: boolean }'
|
|
163
224
|
}
|
|
164
225
|
},
|
|
165
226
|
output: {
|
|
166
227
|
schema: {
|
|
167
|
-
type: '
|
|
168
|
-
additionalProperties: false,
|
|
169
|
-
properties: {
|
|
170
|
-
added: {
|
|
171
|
-
type: 'boolean'
|
|
172
|
-
},
|
|
173
|
-
total: {
|
|
174
|
-
type: 'integer'
|
|
175
|
-
},
|
|
176
|
-
path: {
|
|
177
|
-
type: 'string'
|
|
178
|
-
}
|
|
179
|
-
}
|
|
228
|
+
type: 'json'
|
|
180
229
|
},
|
|
181
230
|
render: (_args, value)=>{
|
|
182
231
|
const v = value;
|
|
@@ -198,26 +247,37 @@ export function apply(ctx, config) {
|
|
|
198
247
|
const pool = await readJson(path, []);
|
|
199
248
|
const existing = pool.findIndex((e)=>e['name'] === entry.name);
|
|
200
249
|
if (existing >= 0) {
|
|
250
|
+
const prev = pool[existing] ?? {};
|
|
201
251
|
pool[existing] = {
|
|
202
|
-
...
|
|
203
|
-
|
|
204
|
-
|
|
252
|
+
...prev,
|
|
253
|
+
wish_id: prev['wish_id'] ?? `w${Date.now().toString(36)}`,
|
|
254
|
+
reason: entry.reason ?? prev['reason'],
|
|
255
|
+
conditions: entry.conditions,
|
|
256
|
+
...entry.muted !== undefined ? {
|
|
257
|
+
muted: entry.muted
|
|
258
|
+
} : {}
|
|
205
259
|
};
|
|
206
260
|
await writeJson(path, pool);
|
|
207
261
|
return {
|
|
262
|
+
ok: true,
|
|
208
263
|
added: false,
|
|
264
|
+
wish_id: String(pool[existing]?.['wish_id']),
|
|
209
265
|
total: pool.length,
|
|
210
266
|
path
|
|
211
267
|
};
|
|
212
268
|
}
|
|
213
|
-
|
|
269
|
+
const created = {
|
|
270
|
+
wish_id: `w${Date.now().toString(36)}`,
|
|
214
271
|
reason: '',
|
|
215
272
|
...entry,
|
|
216
273
|
added_at: new Date().toISOString()
|
|
217
|
-
}
|
|
274
|
+
};
|
|
275
|
+
pool.push(created);
|
|
218
276
|
await writeJson(path, pool);
|
|
219
277
|
return {
|
|
278
|
+
ok: true,
|
|
220
279
|
added: true,
|
|
280
|
+
wish_id: created.wish_id,
|
|
221
281
|
total: pool.length,
|
|
222
282
|
path
|
|
223
283
|
};
|
|
@@ -225,18 +285,127 @@ export function apply(ctx, config) {
|
|
|
225
285
|
presentCall: (args)=>({
|
|
226
286
|
card: 'generic',
|
|
227
287
|
title: '加入「下一次出发」清单',
|
|
228
|
-
kind: '
|
|
288
|
+
kind: 'edit',
|
|
229
289
|
rawInput: args.entry
|
|
230
290
|
})
|
|
231
291
|
}));
|
|
292
|
+
registerGuarded(defineTool({
|
|
293
|
+
name: 'gotry_wish_pool_list',
|
|
294
|
+
description: 'Surface AT MOST ONE "next departure" wish whose fulfilment conditions match the user\'s current window ' + '(0..1 rule: never more than one nudge per turn, never push when nothing matches — 憧憬不被拒绝,也不被硬推). ' + 'Muted wishes never surface. Surfacing records a recalled event in the memory-utility sidecar. ' + 'action="confirm-outcome" records the user-confirmed real-world outcome (attribution helpful/harmful/neutral) — ' + 'ONLY pass attribution the user explicitly stated; the agent must never self-attribute usefulness.',
|
|
295
|
+
parameters: {
|
|
296
|
+
query: {
|
|
297
|
+
type: 'json',
|
|
298
|
+
required: true,
|
|
299
|
+
description: '{ action?: "recall"|"confirm-outcome", days?: number, budgetCny?: number, month?: number, wishId?: string, attribution?: "helpful"|"harmful"|"neutral", detail?: string }'
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
output: {
|
|
303
|
+
schema: {
|
|
304
|
+
type: 'json'
|
|
305
|
+
},
|
|
306
|
+
render: (_args, value)=>[
|
|
307
|
+
{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: String(value.summary ?? '')
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
async execute (args, _exec) {
|
|
314
|
+
const q = unwrapQuery(args, 'action');
|
|
315
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
316
|
+
const pool = await readJson(join(dir, 'wish-pool.json'), []);
|
|
317
|
+
const sidecarPath = join(dir, 'memory-utility.jsonl');
|
|
318
|
+
const loadSidecar = async ()=>{
|
|
319
|
+
try {
|
|
320
|
+
const raw = await readFile(sidecarPath, 'utf-8');
|
|
321
|
+
return raw.split('\n').filter(Boolean).map((l)=>JSON.parse(l));
|
|
322
|
+
} catch {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const saveSidecar = async (events)=>{
|
|
327
|
+
await writeFile(sidecarPath, events.map((e)=>JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
328
|
+
};
|
|
329
|
+
const now = new Date().toISOString();
|
|
330
|
+
if (q.action === 'confirm-outcome') {
|
|
331
|
+
if (!q.wishId || !q.attribution) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
summary: 'confirm-outcome 需要 wishId + attribution(helpful|harmful|neutral)'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const events = await loadSidecar();
|
|
338
|
+
const { events: next, appended } = appendEvent(events, {
|
|
339
|
+
wish_id: q.wishId,
|
|
340
|
+
kind: 'verified_outcome',
|
|
341
|
+
ts: now,
|
|
342
|
+
ctx: 'gotry_wish_pool_list.confirm',
|
|
343
|
+
detail: q.detail,
|
|
344
|
+
attribution: q.attribution
|
|
345
|
+
});
|
|
346
|
+
if (appended) await saveSidecar(next);
|
|
347
|
+
return {
|
|
348
|
+
ok: true,
|
|
349
|
+
recorded: appended,
|
|
350
|
+
wish_id: q.wishId,
|
|
351
|
+
status: q.attribution
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const candidates = pool.filter((e)=>!e['muted'] && typeof e['wish_id'] === 'string');
|
|
355
|
+
const month = q.month ?? new Date().getMonth() + 1;
|
|
356
|
+
const match = pickNudgeWish(candidates, {
|
|
357
|
+
days: q.days,
|
|
358
|
+
budgetCny: q.budgetCny,
|
|
359
|
+
month
|
|
360
|
+
});
|
|
361
|
+
if (!match) {
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
suggestion: null,
|
|
365
|
+
summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const events = await loadSidecar();
|
|
369
|
+
const { events: next, appended } = appendEvent(events, {
|
|
370
|
+
wish_id: match.wishId,
|
|
371
|
+
kind: 'recalled',
|
|
372
|
+
ts: now,
|
|
373
|
+
ctx: 'gotry_wish_pool_list.recall'
|
|
374
|
+
});
|
|
375
|
+
if (appended) await saveSidecar(next);
|
|
376
|
+
const utility = projectUtility(next)[match.wishId];
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
suggestion: {
|
|
380
|
+
wish_id: match.entry['wish_id'],
|
|
381
|
+
name: match.entry['name'],
|
|
382
|
+
reason: match.entry['reason'],
|
|
383
|
+
conditions: match.entry['conditions'],
|
|
384
|
+
match_score: match.score,
|
|
385
|
+
hits: match.hits
|
|
386
|
+
},
|
|
387
|
+
utility: {
|
|
388
|
+
status: utility?.status ?? 'unknown',
|
|
389
|
+
recalled: utility?.recalled ?? 1
|
|
390
|
+
},
|
|
391
|
+
summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
presentCall: (args)=>({
|
|
395
|
+
card: 'generic',
|
|
396
|
+
title: '「下一次出发」召回',
|
|
397
|
+
kind: 'search',
|
|
398
|
+
rawInput: args.query
|
|
399
|
+
})
|
|
400
|
+
}));
|
|
232
401
|
registerGuarded(defineTool({
|
|
233
402
|
name: 'gotry_hotel_search',
|
|
234
|
-
description: 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). ' + 'Input: destination city name + optional dates/occupancy. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, ' + 'or [static-pack:estimate]) per the L4 invariant.',
|
|
403
|
+
description: 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). ' + 'Input: destination city name + optional dates/occupancy. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — ' + 'the code layer resolves them against the time anchor; unresolved expressions degrade to an undated search with an explicit ' + 'date_notes entry instead of guessing. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, ' + 'or [static-pack:estimate]) per the L4 invariant.',
|
|
235
404
|
parameters: {
|
|
236
405
|
query: {
|
|
237
406
|
type: 'json',
|
|
238
407
|
required: true,
|
|
239
|
-
description: '{ destination: "
|
|
408
|
+
description: '{ destination: "<目的地城市>", checkIn?: "YYYY-MM-DD 或自然表达(下周五/8.20)", checkOut?: 同上, occupancy?: { adults: 2 } }'
|
|
240
409
|
}
|
|
241
410
|
},
|
|
242
411
|
output: {
|
|
@@ -251,14 +420,26 @@ export function apply(ctx, config) {
|
|
|
251
420
|
]
|
|
252
421
|
},
|
|
253
422
|
async execute (args, _exec) {
|
|
254
|
-
const q = args
|
|
423
|
+
const q = unwrapQuery(args, 'destination');
|
|
255
424
|
if (!q.destination) throw new Error('gotry_hotel_search requires destination');
|
|
256
425
|
const started = Date.now();
|
|
257
426
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json');
|
|
427
|
+
const anchor = buildTimeAnchor(new Date());
|
|
428
|
+
const dateNotes = [];
|
|
429
|
+
const resolveDate = (expr)=>{
|
|
430
|
+
if (!expr) return undefined;
|
|
431
|
+
const r = resolveSlotDate(expr, anchor);
|
|
432
|
+
if (!r.date) {
|
|
433
|
+
dateNotes.push(`日期未解析:${r.raw}——请向用户确认具体日期`);
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
if (r.raw !== r.date) dateNotes.push(`slot-resolved: ${r.raw} → ${r.date}`);
|
|
437
|
+
return r.date;
|
|
438
|
+
};
|
|
258
439
|
const resp = await hbcliSearchHotels({
|
|
259
440
|
destination: q.destination,
|
|
260
|
-
checkIn: q.checkIn,
|
|
261
|
-
checkOut: q.checkOut,
|
|
441
|
+
checkIn: resolveDate(q.checkIn),
|
|
442
|
+
checkOut: resolveDate(q.checkOut),
|
|
262
443
|
adults: q.adults
|
|
263
444
|
}, {
|
|
264
445
|
hbcliBin: config.hbcliBin,
|
|
@@ -270,22 +451,40 @@ export function apply(ctx, config) {
|
|
|
270
451
|
const evidence = isLive ? resp.evidence : '[静态包:估算]';
|
|
271
452
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `hotel_search:${resp.via}`).catch(()=>{});
|
|
272
453
|
const payload = {
|
|
454
|
+
ok: true,
|
|
273
455
|
hotels: resp.hotels ?? null,
|
|
274
456
|
evidence,
|
|
275
457
|
destination: q.destination,
|
|
276
458
|
via: resp.via,
|
|
277
459
|
latency_ms: Date.now() - started,
|
|
278
460
|
summary: resp.summary,
|
|
279
|
-
error: resp.error
|
|
461
|
+
error: resp.error,
|
|
462
|
+
...dateNotes.length ? {
|
|
463
|
+
date_notes: dateNotes
|
|
464
|
+
} : {}
|
|
280
465
|
};
|
|
281
466
|
return JSON.parse(JSON.stringify(payload));
|
|
282
467
|
},
|
|
283
468
|
presentCall: (args)=>({
|
|
284
469
|
card: 'generic',
|
|
285
470
|
title: `酒店搜索:${String(args.query?.destination ?? '')}`,
|
|
286
|
-
kind: '
|
|
471
|
+
kind: 'search',
|
|
287
472
|
rawInput: args.query
|
|
288
|
-
})
|
|
473
|
+
}),
|
|
474
|
+
presentResult: (_args, value)=>{
|
|
475
|
+
const r = value;
|
|
476
|
+
const n = Array.isArray(r.hotels) ? r.hotels.length : 0;
|
|
477
|
+
return {
|
|
478
|
+
card: 'generic',
|
|
479
|
+
title: `酒店:${r.destination ?? ''} ${n ? `${n} 家(${r.via === 'hbcli-realtime' ? '实时' : '静态包'})` : '无结果'}`,
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: 'text',
|
|
483
|
+
text: String(r.summary ?? '')
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
};
|
|
487
|
+
}
|
|
289
488
|
}));
|
|
290
489
|
registerGuarded(defineTool({
|
|
291
490
|
name: 'gotry_skeleton_check',
|
|
@@ -304,38 +503,27 @@ export function apply(ctx, config) {
|
|
|
304
503
|
},
|
|
305
504
|
output: {
|
|
306
505
|
schema: {
|
|
307
|
-
type: '
|
|
308
|
-
additionalProperties: false,
|
|
309
|
-
properties: {
|
|
310
|
-
connected: {
|
|
311
|
-
type: 'boolean'
|
|
312
|
-
},
|
|
313
|
-
airlines: {
|
|
314
|
-
type: 'array',
|
|
315
|
-
items: {
|
|
316
|
-
type: 'string'
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
evidence: {
|
|
320
|
-
type: 'string'
|
|
321
|
-
}
|
|
322
|
-
}
|
|
506
|
+
type: 'json'
|
|
323
507
|
},
|
|
324
508
|
render: (_args, value)=>[
|
|
325
509
|
{
|
|
326
510
|
type: 'text',
|
|
327
|
-
text: String(value.evidence ??
|
|
511
|
+
text: String(value.evidence ?? JSON.stringify(value))
|
|
328
512
|
}
|
|
329
513
|
]
|
|
330
514
|
},
|
|
331
515
|
async execute (args, _exec) {
|
|
332
|
-
const
|
|
333
|
-
|
|
516
|
+
const q = unwrapQuery(args);
|
|
517
|
+
const verdict = await checkConnectivity(String(q.from ?? ''), String(q.to ?? ''));
|
|
518
|
+
return JSON.parse(JSON.stringify({
|
|
519
|
+
ok: true,
|
|
520
|
+
...verdict
|
|
521
|
+
}));
|
|
334
522
|
},
|
|
335
523
|
presentCall: (args)=>({
|
|
336
524
|
card: 'generic',
|
|
337
525
|
title: `骨架校验:${args.from}-${args.to}`,
|
|
338
|
-
kind: '
|
|
526
|
+
kind: 'execute',
|
|
339
527
|
rawInput: args
|
|
340
528
|
})
|
|
341
529
|
}));
|
|
@@ -346,7 +534,7 @@ export function apply(ctx, config) {
|
|
|
346
534
|
query: {
|
|
347
535
|
type: 'json',
|
|
348
536
|
required: true,
|
|
349
|
-
description: '{ place: "
|
|
537
|
+
description: '{ place: "<城市名>", month?: 8, mode?: "forecast"|"climate", days?: 7 }'
|
|
350
538
|
}
|
|
351
539
|
},
|
|
352
540
|
output: {
|
|
@@ -361,7 +549,7 @@ export function apply(ctx, config) {
|
|
|
361
549
|
]
|
|
362
550
|
},
|
|
363
551
|
async execute (args, _exec) {
|
|
364
|
-
const q = args
|
|
552
|
+
const q = unwrapQuery(args, 'place');
|
|
365
553
|
const started = Date.now();
|
|
366
554
|
let lat = q.lat, lng = q.lng;
|
|
367
555
|
let placeLabel = q.place ?? `${q.lat},${q.lng}`;
|
|
@@ -407,9 +595,24 @@ export function apply(ctx, config) {
|
|
|
407
595
|
presentCall: (args)=>({
|
|
408
596
|
card: 'generic',
|
|
409
597
|
title: `天气:${String(args.query?.place ?? '')}`,
|
|
410
|
-
kind: '
|
|
598
|
+
kind: 'fetch',
|
|
411
599
|
rawInput: args.query
|
|
412
|
-
})
|
|
600
|
+
}),
|
|
601
|
+
presentResult: (args, value)=>{
|
|
602
|
+
const r = value;
|
|
603
|
+
const place = String(args.query?.place ?? '');
|
|
604
|
+
const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable');
|
|
605
|
+
return {
|
|
606
|
+
card: 'generic',
|
|
607
|
+
title: `天气:${place} ${failed ? '降级' : 'ok'}`,
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: 'text',
|
|
611
|
+
text: String(r.summary ?? '')
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
}
|
|
413
616
|
}));
|
|
414
617
|
registerGuarded(defineTool({
|
|
415
618
|
name: 'gotry_flight_verify',
|
|
@@ -433,7 +636,7 @@ export function apply(ctx, config) {
|
|
|
433
636
|
]
|
|
434
637
|
},
|
|
435
638
|
async execute (args, _exec) {
|
|
436
|
-
const q = args
|
|
639
|
+
const q = unwrapQuery(args, 'callsign');
|
|
437
640
|
const started = Date.now();
|
|
438
641
|
if (!q.callsign) {
|
|
439
642
|
return JSON.parse(JSON.stringify({
|
|
@@ -451,6 +654,7 @@ export function apply(ctx, config) {
|
|
|
451
654
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `flight_verify:${r.via}`).catch(()=>{});
|
|
452
655
|
const summary = r.verdict === 'observed' ? `${r.callsign} 当前 ADS-B 观测命中 (${r.hits?.length ?? 0} 架)${r.airport ? ` 在 ${r.airport}` : ''}\n${r.evidence}` : r.verdict === 'not_observed' ? `${r.callsign} 当前观测列表未见(ADS-B 覆盖有限,不否定该航班存在)\n${r.evidence}` : `${r.callsign} OpenSky 不可用:${r.error}\n${r.evidence}`;
|
|
453
656
|
return JSON.parse(JSON.stringify({
|
|
657
|
+
ok: true,
|
|
454
658
|
verdict: r.verdict,
|
|
455
659
|
callsign: r.callsign,
|
|
456
660
|
airport: r.airport,
|
|
@@ -464,18 +668,18 @@ export function apply(ctx, config) {
|
|
|
464
668
|
presentCall: (args)=>({
|
|
465
669
|
card: 'generic',
|
|
466
670
|
title: `飞行校验:${String(args.query?.callsign ?? '')}`,
|
|
467
|
-
kind: '
|
|
671
|
+
kind: 'fetch',
|
|
468
672
|
rawInput: args.query
|
|
469
673
|
})
|
|
470
674
|
}));
|
|
471
675
|
registerGuarded(defineTool({
|
|
472
676
|
name: 'gotry_anything_search',
|
|
473
|
-
description: '
|
|
677
|
+
description: 'Travel-domain search via hotel-byte CLI → hotel-be Anything (cities/hotels/destinations) — NOT general web search; for general internet facts use gotry_agent_reach. ' + 'Mixed destinations (cities / metropolitan areas / high-level regions) + hotels in one call. ' + 'Returns candidates with type, name, optional coordinates and hotel-id. ' + 'Three-valued semantics: hit = ≥1 candidate; miss = 0 candidates (try synonyms or contentType=city/hotel); ' + 'unavailable = hbcli failed (degraded, never blocks). ' + 'Use as the first stop when the user mentions a place/city/hotel name and you need to ground it in real catalog data ' + '(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).',
|
|
474
678
|
parameters: {
|
|
475
679
|
query: {
|
|
476
680
|
type: 'json',
|
|
477
681
|
required: true,
|
|
478
|
-
description: '{ keyword: "
|
|
682
|
+
description: '{ keyword: "<搜索关键词>", contentType?: "city"|"hotel", parentDestinationId?: "?", timeoutMs?: 12000 }'
|
|
479
683
|
}
|
|
480
684
|
},
|
|
481
685
|
output: {
|
|
@@ -490,7 +694,7 @@ export function apply(ctx, config) {
|
|
|
490
694
|
]
|
|
491
695
|
},
|
|
492
696
|
async execute (args, _exec) {
|
|
493
|
-
const q = args
|
|
697
|
+
const q = unwrapQuery(args, 'keyword');
|
|
494
698
|
const started = Date.now();
|
|
495
699
|
if (!q.keyword) {
|
|
496
700
|
return JSON.parse(JSON.stringify({
|
|
@@ -520,9 +724,23 @@ export function apply(ctx, config) {
|
|
|
520
724
|
presentCall: (args)=>({
|
|
521
725
|
card: 'generic',
|
|
522
726
|
title: `Anything search:${String(args.query?.keyword ?? '')}`,
|
|
523
|
-
kind: '
|
|
727
|
+
kind: 'search',
|
|
524
728
|
rawInput: args.query
|
|
525
|
-
})
|
|
729
|
+
}),
|
|
730
|
+
presentResult: (_args, value)=>{
|
|
731
|
+
const r = value;
|
|
732
|
+
const n = Array.isArray(r.hits) ? r.hits.length : r.total_candidates ?? 0;
|
|
733
|
+
return {
|
|
734
|
+
card: 'generic',
|
|
735
|
+
title: `Anything:${r.keyword ?? ''} ${r.verdict === 'hit' ? `${n} hits` : r.verdict ?? 'no-result'}`,
|
|
736
|
+
content: [
|
|
737
|
+
{
|
|
738
|
+
type: 'text',
|
|
739
|
+
text: String(r.summary ?? '')
|
|
740
|
+
}
|
|
741
|
+
]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
526
744
|
}));
|
|
527
745
|
registerGuarded(defineTool({
|
|
528
746
|
name: 'gotry_web_search',
|
|
@@ -546,7 +764,7 @@ export function apply(ctx, config) {
|
|
|
546
764
|
]
|
|
547
765
|
},
|
|
548
766
|
async execute (args, _exec) {
|
|
549
|
-
const q = args
|
|
767
|
+
const q = unwrapQuery(args, 'url');
|
|
550
768
|
const started = Date.now();
|
|
551
769
|
if (!q.url) {
|
|
552
770
|
return JSON.parse(JSON.stringify({
|
|
@@ -576,7 +794,7 @@ export function apply(ctx, config) {
|
|
|
576
794
|
presentCall: (args)=>({
|
|
577
795
|
card: 'generic',
|
|
578
796
|
title: `读网页:${String(args.query?.url ?? '')}`,
|
|
579
|
-
kind: '
|
|
797
|
+
kind: 'fetch',
|
|
580
798
|
rawInput: args.query
|
|
581
799
|
})
|
|
582
800
|
}));
|
|
@@ -602,7 +820,7 @@ export function apply(ctx, config) {
|
|
|
602
820
|
]
|
|
603
821
|
},
|
|
604
822
|
async execute (args, _exec) {
|
|
605
|
-
const q = args
|
|
823
|
+
const q = unwrapQuery(args, 'url');
|
|
606
824
|
if (!q.url) {
|
|
607
825
|
return JSON.parse(JSON.stringify({
|
|
608
826
|
ok: false,
|
|
@@ -627,7 +845,7 @@ export function apply(ctx, config) {
|
|
|
627
845
|
presentCall: (args)=>({
|
|
628
846
|
card: 'generic',
|
|
629
847
|
title: `视频字幕:${String(args.query?.url ?? '')}`,
|
|
630
|
-
kind: '
|
|
848
|
+
kind: 'fetch',
|
|
631
849
|
rawInput: args.query
|
|
632
850
|
})
|
|
633
851
|
}));
|
|
@@ -653,7 +871,7 @@ export function apply(ctx, config) {
|
|
|
653
871
|
]
|
|
654
872
|
},
|
|
655
873
|
async execute (args, _exec) {
|
|
656
|
-
const q = args
|
|
874
|
+
const q = unwrapQuery(args, 'query');
|
|
657
875
|
if (!q.query) {
|
|
658
876
|
return JSON.parse(JSON.stringify({
|
|
659
877
|
ok: false,
|
|
@@ -678,13 +896,13 @@ export function apply(ctx, config) {
|
|
|
678
896
|
presentCall: (args)=>({
|
|
679
897
|
card: 'generic',
|
|
680
898
|
title: `GitHub 搜索:${String(args.query?.query ?? '')}`,
|
|
681
|
-
kind: '
|
|
899
|
+
kind: 'search',
|
|
682
900
|
rawInput: args.query
|
|
683
901
|
})
|
|
684
902
|
}));
|
|
685
903
|
registerGuarded(defineTool({
|
|
686
904
|
name: 'gotry_agent_reach',
|
|
687
|
-
description: 'Agent Reach — thin wrapper over Panniantong/Agent-Reach upstream registry
|
|
905
|
+
description: 'Agent Reach — the PRIMARY external-data gateway (thin wrapper over Panniantong/Agent-Reach upstream registry, zero channel knowledge here). ' + 'Prefer this for ANY external/internet fact beyond weather/flights/hotels. ' + 'Call ANY upstream channel method by reflection: web.read(url) / v2ex.get_hot_topics() / v2ex.search(query) / ' + 'xueqiu.get_stock_quote(symbol) / xueqiu.search_stock(query) / youtube.transcribe(url) / <channel>.check() ... ' + 'Unknown channel or method? Just call it — the error returns the upstream inventory (channel list or method signatures) so you can self-correct. ' + 'Action "status" runs the real `agent-reach doctor` (.venv/bin/agent-reach). ' + 'Channels needing cookies/setup return the upstream check() guidance verbatim — do NOT give up there: hand the user the exact configure command, then offer to re-check. ' + 'Evidence chain: [agent-reach:<channel>.<method>@ts].',
|
|
688
906
|
parameters: {
|
|
689
907
|
query: {
|
|
690
908
|
type: 'json',
|
|
@@ -704,7 +922,7 @@ export function apply(ctx, config) {
|
|
|
704
922
|
]
|
|
705
923
|
},
|
|
706
924
|
async execute (args, _exec) {
|
|
707
|
-
const q = args
|
|
925
|
+
const q = unwrapQuery(args, 'channel');
|
|
708
926
|
const started = Date.now();
|
|
709
927
|
const dir = await ensureStateDir(config.stateRoot);
|
|
710
928
|
if (q.action === 'status' || !q.action && !q.channel) {
|
|
@@ -732,6 +950,23 @@ export function apply(ctx, config) {
|
|
|
732
950
|
args: q.args,
|
|
733
951
|
timeoutMs: q.timeoutMs
|
|
734
952
|
});
|
|
953
|
+
const dataStr = (v)=>{
|
|
954
|
+
if (Array.isArray(v)) {
|
|
955
|
+
const kept = [];
|
|
956
|
+
let budget = 3500;
|
|
957
|
+
for (const item of v){
|
|
958
|
+
const s = JSON.stringify(item);
|
|
959
|
+
if (budget - s.length < 0) break;
|
|
960
|
+
budget -= s.length + 1;
|
|
961
|
+
kept.push(item);
|
|
962
|
+
}
|
|
963
|
+
if (kept.length < v.length) kept.push(`…(截断:保留 ${kept.length}/${v.length} 条;可用上游方法带 limit 参数取更少)`);
|
|
964
|
+
return kept;
|
|
965
|
+
}
|
|
966
|
+
if (typeof v === 'string') return v.length > 4000 ? v.slice(0, 4000) + '…(截断)' : v;
|
|
967
|
+
return v;
|
|
968
|
+
};
|
|
969
|
+
if (r.ok) r.data = dataStr(r.data);
|
|
735
970
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${q.channel}.${q.method}:${r.verdict}`).catch(()=>{});
|
|
736
971
|
const summary = r.verdict === 'found' ? `${q.channel}.${q.method} → found (${r.latencyMs}ms)\n${r.evidence}\n${typeof r.data === 'string' ? r.data.slice(0, 600) : JSON.stringify(r.data ?? null).slice(0, 600)}` : r.verdict === 'needs-setup' ? `${q.channel}.${q.method} → 需配置(上游 check() 原话): ${r.setup ?? ''}\n${r.evidence}` : r.verdict === 'not-installed' ? `${q.channel}.${q.method} → 上游未装: ${r.setup ?? ''}\n${r.evidence}` : `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`;
|
|
737
972
|
return JSON.parse(JSON.stringify({
|
|
@@ -750,9 +985,24 @@ export function apply(ctx, config) {
|
|
|
750
985
|
presentCall: (args)=>({
|
|
751
986
|
card: 'generic',
|
|
752
987
|
title: `Agent Reach:${String(args.query?.channel ?? '')}.${String(args.query?.method ?? 'status')}`,
|
|
753
|
-
kind: '
|
|
988
|
+
kind: 'fetch',
|
|
754
989
|
rawInput: args.query
|
|
755
|
-
})
|
|
990
|
+
}),
|
|
991
|
+
presentResult: (args, value)=>{
|
|
992
|
+
const r = value;
|
|
993
|
+
const q = args.query ?? {};
|
|
994
|
+
const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌';
|
|
995
|
+
return {
|
|
996
|
+
card: 'generic',
|
|
997
|
+
title: `AgentReach ${icon} ${q.channel ?? ''}.${q.method ?? 'status'} ${r.verdict ?? ''}`,
|
|
998
|
+
content: [
|
|
999
|
+
{
|
|
1000
|
+
type: 'text',
|
|
1001
|
+
text: String(r.summary ?? '')
|
|
1002
|
+
}
|
|
1003
|
+
]
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
756
1006
|
}));
|
|
757
1007
|
}
|
|
758
1008
|
|