@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
package/dist/src/index.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import z from '@deepseek-ai/schemastery';
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
|
-
import { ensureStateDir,
|
|
4
|
+
import { ensureStateDir, recordLatency } from './bridge.js';
|
|
5
5
|
import { segmentsFromCandidate, solveChoiceSegment } from './unified.js';
|
|
6
6
|
import { checkConnectivity } from '../scripts/skeleton-check.js';
|
|
7
7
|
import { parseCandidate, parseRequest } from './model.js';
|
|
8
8
|
import { searchHotels as hbcliSearchHotels } from '../capabilities/hbcli.js';
|
|
9
9
|
import { installProcessGuards, guardToolExecute } from '../capabilities/incident-log.js';
|
|
10
|
+
import { interpretArgs } from './tool-packet.js';
|
|
11
|
+
import { projectUtility } from './memory-utility.js';
|
|
12
|
+
import { pickNudgeWish } from './wish-pool.js';
|
|
13
|
+
import { resolveTimelineDate } from './travel-timeline.js';
|
|
14
|
+
import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, readTripsWithFallback, readWishPoolWithFallback } from './state-ledger.js';
|
|
15
|
+
import { buildTimeAnchor } from './time-anchor.js';
|
|
16
|
+
import { resolveSlotDate } from './slot-spec.js';
|
|
10
17
|
import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
|
|
11
18
|
import { verifyFlight } from '../capabilities/opensky.js';
|
|
12
19
|
import { anythingSearch } from '../capabilities/anything.js';
|
|
13
20
|
import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
14
21
|
import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.js';
|
|
22
|
+
import { flyaiSearch } from '../capabilities/flyai.js';
|
|
23
|
+
import { sessionFlightSearch } from '../capabilities/session-search.js';
|
|
15
24
|
export const name = 'gotry-tools';
|
|
16
25
|
export const inject = [
|
|
17
26
|
'tools',
|
|
@@ -22,6 +31,48 @@ export const Config = z.object({
|
|
|
22
31
|
timeoutMs: z.number().default(30_000),
|
|
23
32
|
hbcliBin: z.string().default('hbcli')
|
|
24
33
|
});
|
|
34
|
+
const unwrapQuery = interpretArgs;
|
|
35
|
+
function renderMotivationBrief(stateRoot) {
|
|
36
|
+
const p = readMotivationWithFallback(stateRoot);
|
|
37
|
+
if (!p) return '';
|
|
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
|
+
const trips = readTimelineTrips(stateRoot);
|
|
46
|
+
if (trips.length) {
|
|
47
|
+
lines.push(`- 去过: ${trips.map((t)=>`${t.destination}(${t.start.slice(0, 7)})`).join('、')}——去过的地方不再主动推荐,除非用户点名`);
|
|
48
|
+
}
|
|
49
|
+
const companions = readCompanions(stateRoot);
|
|
50
|
+
if (companions.length) {
|
|
51
|
+
lines.push(`- 同行人: ${companions.map((c)=>`${c.label}(${c.brief})`).join('、')}——约束进结构与排序建议,不硬过滤;引用时带当初的原话依据`);
|
|
52
|
+
}
|
|
53
|
+
lines.push(`- 愿望池: 用 gotry_wish_pool_list 按条件召回(0..1),勿直接堆砌`);
|
|
54
|
+
if (p.updated_at) lines.push(`- 更新于: ${p.updated_at}`);
|
|
55
|
+
return weights.length || hard.length ? lines.join('\n') : '';
|
|
56
|
+
}
|
|
57
|
+
function readCompanions(stateRoot) {
|
|
58
|
+
return readCompanionsWithFallback(stateRoot).map((c)=>{
|
|
59
|
+
const bits = [
|
|
60
|
+
c.constraints.mobility,
|
|
61
|
+
...c.constraints.health ?? [],
|
|
62
|
+
...c.constraints.prefs ?? []
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
return {
|
|
65
|
+
label: c.label,
|
|
66
|
+
brief: bits.join('/')
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function readTimelineTrips(stateRoot) {
|
|
71
|
+
return readTripsWithFallback(stateRoot).map((t)=>({
|
|
72
|
+
destination: t.destination,
|
|
73
|
+
start: t.start
|
|
74
|
+
})).sort((a, b)=>a.start < b.start ? 1 : -1).slice(0, 3);
|
|
75
|
+
}
|
|
25
76
|
export function apply(ctx, config) {
|
|
26
77
|
const sp = ctx['systemPrompt'];
|
|
27
78
|
sp?.variable?.('current_date', ()=>{
|
|
@@ -38,6 +89,8 @@ export function apply(ctx, config) {
|
|
|
38
89
|
];
|
|
39
90
|
return `${ymd} 周${weekdays[d.getDay()]}`;
|
|
40
91
|
});
|
|
92
|
+
sp?.variable?.('time_anchor_card', ()=>buildTimeAnchor(new Date()).card);
|
|
93
|
+
sp?.variable?.('motivation_brief', ()=>renderMotivationBrief(config.stateRoot ?? '.'));
|
|
41
94
|
installProcessGuards(config.stateRoot ?? '.', {
|
|
42
95
|
uncaughtException: 'gotry-tools',
|
|
43
96
|
unhandledRejection: 'gotry-tools'
|
|
@@ -53,7 +106,7 @@ export function apply(ctx, config) {
|
|
|
53
106
|
};
|
|
54
107
|
registerGuarded(defineTool({
|
|
55
108
|
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),
|
|
109
|
+
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
110
|
parameters: {
|
|
58
111
|
payload: {
|
|
59
112
|
type: 'json',
|
|
@@ -82,6 +135,7 @@ export function apply(ctx, config) {
|
|
|
82
135
|
const dir = await ensureStateDir(config.stateRoot);
|
|
83
136
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(()=>{});
|
|
84
137
|
return {
|
|
138
|
+
ok: true,
|
|
85
139
|
...result,
|
|
86
140
|
latency_ms: Date.now() - started,
|
|
87
141
|
via: 'in-process-unified'
|
|
@@ -90,13 +144,32 @@ export function apply(ctx, config) {
|
|
|
90
144
|
presentCall: (args)=>({
|
|
91
145
|
card: 'generic',
|
|
92
146
|
title: 'GoTry 可行性检查(门到门全成本)',
|
|
93
|
-
kind: '
|
|
147
|
+
kind: 'execute',
|
|
94
148
|
rawInput: args.payload
|
|
95
|
-
})
|
|
149
|
+
}),
|
|
150
|
+
presentResult: (args, value)=>{
|
|
151
|
+
const r = value;
|
|
152
|
+
const budget = args.payload?.request?.budget_cny;
|
|
153
|
+
const lines = (r.verdicts ?? []).map((v)=>{
|
|
154
|
+
const tc = v.true_cost ?? {};
|
|
155
|
+
const money = typeof tc.money_cny === 'number' ? ` ¥${tc.money_cny}/人` + (typeof budget === 'number' ? `(预算 ¥${budget},${tc.money_cny <= budget ? '余' : '超'} ¥${Math.abs(budget - tc.money_cny)})` : '') : '';
|
|
156
|
+
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(',') : '不可行'}`;
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
card: 'generic',
|
|
160
|
+
title: `可行性:${r.recommended ? `推荐 ${r.recommended}` : '全部不可行'}`,
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: 'text',
|
|
164
|
+
text: (lines.length ? lines.join('\n') + '\n\n' : '') + String(r.answer_md ?? '').slice(0, 1500)
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
96
169
|
}));
|
|
97
170
|
registerGuarded(defineTool({
|
|
98
171
|
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.
|
|
172
|
+
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
173
|
parameters: {
|
|
101
174
|
profile: {
|
|
102
175
|
type: 'json',
|
|
@@ -106,19 +179,7 @@ export function apply(ctx, config) {
|
|
|
106
179
|
},
|
|
107
180
|
output: {
|
|
108
181
|
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
|
-
}
|
|
182
|
+
type: 'json'
|
|
122
183
|
},
|
|
123
184
|
render: (_args, value)=>[
|
|
124
185
|
{
|
|
@@ -128,55 +189,51 @@ export function apply(ctx, config) {
|
|
|
128
189
|
]
|
|
129
190
|
},
|
|
130
191
|
async execute (args, _exec) {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
192
|
+
const incoming = args.profile ?? {};
|
|
193
|
+
if (!incoming.evidence?.length) {
|
|
133
194
|
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)');
|
|
134
195
|
}
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
return {
|
|
196
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
197
|
+
const res = ledger.appendMotivationPatch({
|
|
198
|
+
weights: incoming.weights,
|
|
199
|
+
evidence: incoming.evidence,
|
|
200
|
+
hard: incoming.hard
|
|
201
|
+
});
|
|
202
|
+
const profileJson = JSON.parse(JSON.stringify(res.profile));
|
|
203
|
+
return res.saved ? {
|
|
204
|
+
ok: true,
|
|
143
205
|
saved: true,
|
|
144
|
-
path,
|
|
145
|
-
profile:
|
|
206
|
+
path: ledger.dbPath,
|
|
207
|
+
profile: profileJson,
|
|
208
|
+
summary: '画像已合并入账本(单事务)'
|
|
209
|
+
} : {
|
|
210
|
+
ok: true,
|
|
211
|
+
saved: false,
|
|
212
|
+
path: ledger.dbPath,
|
|
213
|
+
profile: profileJson,
|
|
214
|
+
summary: '无新内容(幂等跳过)'
|
|
146
215
|
};
|
|
147
216
|
},
|
|
148
217
|
presentCall: (args)=>({
|
|
149
218
|
card: 'generic',
|
|
150
219
|
title: '保存动机画像',
|
|
151
|
-
kind: '
|
|
220
|
+
kind: 'edit',
|
|
152
221
|
rawInput: args.profile
|
|
153
222
|
})
|
|
154
223
|
}));
|
|
155
224
|
registerGuarded(defineTool({
|
|
156
225
|
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. 憧憬不被拒绝。',
|
|
226
|
+
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
227
|
parameters: {
|
|
159
228
|
entry: {
|
|
160
229
|
type: 'json',
|
|
161
230
|
required: true,
|
|
162
|
-
description: '{ name, reason
|
|
231
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months }, muted?: boolean }'
|
|
163
232
|
}
|
|
164
233
|
},
|
|
165
234
|
output: {
|
|
166
235
|
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
|
-
}
|
|
236
|
+
type: 'json'
|
|
180
237
|
},
|
|
181
238
|
render: (_args, value)=>{
|
|
182
239
|
const v = value;
|
|
@@ -190,53 +247,267 @@ export function apply(ctx, config) {
|
|
|
190
247
|
},
|
|
191
248
|
async execute (args, _exec) {
|
|
192
249
|
const entry = args.entry ?? {};
|
|
193
|
-
|
|
194
|
-
|
|
250
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
251
|
+
const r = ledger.appendWish({
|
|
252
|
+
name: entry.name,
|
|
253
|
+
reason: entry.reason,
|
|
254
|
+
conditions: entry.conditions,
|
|
255
|
+
muted: entry.muted
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
added: r.added,
|
|
260
|
+
wish_id: r.wish_id,
|
|
261
|
+
total: r.total,
|
|
262
|
+
path: ledger.dbPath
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
presentCall: (args)=>({
|
|
266
|
+
card: 'generic',
|
|
267
|
+
title: '加入「下一次出发」清单',
|
|
268
|
+
kind: 'edit',
|
|
269
|
+
rawInput: args.entry
|
|
270
|
+
})
|
|
271
|
+
}));
|
|
272
|
+
registerGuarded(defineTool({
|
|
273
|
+
name: 'gotry_wish_pool_list',
|
|
274
|
+
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.',
|
|
275
|
+
parameters: {
|
|
276
|
+
query: {
|
|
277
|
+
type: 'json',
|
|
278
|
+
required: true,
|
|
279
|
+
description: '{ action?: "recall"|"confirm-outcome", days?, budgetCny?, month?, wishId?, attribution?: "helpful"|"harmful"|"neutral", detail?, tripStart?: "行程起始(确认成行时挂时间线)" }'
|
|
195
280
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
281
|
+
},
|
|
282
|
+
output: {
|
|
283
|
+
schema: {
|
|
284
|
+
type: 'json'
|
|
285
|
+
},
|
|
286
|
+
render: (_args, value)=>[
|
|
287
|
+
{
|
|
288
|
+
type: 'text',
|
|
289
|
+
text: String(value.summary ?? '')
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
async execute (args, _exec) {
|
|
294
|
+
const q = unwrapQuery(args, 'action');
|
|
295
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
if (q.action === 'confirm-outcome') {
|
|
298
|
+
if (!q.wishId || !q.attribution) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
summary: 'confirm-outcome 需要 wishId + attribution(helpful|harmful|neutral)'
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
let tripInput;
|
|
305
|
+
const wish = ledger.readWishPool().find((e)=>String(e.wish_id) === q.wishId);
|
|
306
|
+
if (q.tripStart && wish?.name) {
|
|
307
|
+
const anchor = buildTimeAnchor(new Date());
|
|
308
|
+
const start = resolveTimelineDate(q.tripStart, anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.tripStart) ? q.tripStart : undefined);
|
|
309
|
+
if (start) {
|
|
310
|
+
tripInput = {
|
|
311
|
+
destination: String(wish.name),
|
|
312
|
+
start,
|
|
313
|
+
companions: undefined,
|
|
314
|
+
source: 'wish-confirmed',
|
|
315
|
+
evidence: `wish ${q.wishId} confirm-outcome(${q.attribution})`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const r = ledger.confirmOutcome({
|
|
320
|
+
wishId: q.wishId,
|
|
321
|
+
attribution: q.attribution,
|
|
322
|
+
detail: q.detail,
|
|
323
|
+
trip: tripInput
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
recorded: r.recorded,
|
|
328
|
+
wish_id: q.wishId,
|
|
329
|
+
status: q.attribution,
|
|
330
|
+
...r.trip ? {
|
|
331
|
+
trip: r.trip
|
|
332
|
+
} : {}
|
|
205
333
|
};
|
|
206
|
-
|
|
334
|
+
}
|
|
335
|
+
const pool = ledger.readWishPool();
|
|
336
|
+
const candidates = pool.filter((e)=>!e.muted && typeof e.wish_id === 'string');
|
|
337
|
+
const month = q.month ?? new Date().getMonth() + 1;
|
|
338
|
+
const match = pickNudgeWish(candidates, {
|
|
339
|
+
days: q.days,
|
|
340
|
+
budgetCny: q.budgetCny,
|
|
341
|
+
month
|
|
342
|
+
});
|
|
343
|
+
if (!match) {
|
|
207
344
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
345
|
+
ok: true,
|
|
346
|
+
suggestion: null,
|
|
347
|
+
summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)`
|
|
211
348
|
};
|
|
212
349
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
350
|
+
const { events: next } = ledger.appendUtilityEvent({
|
|
351
|
+
wish_id: match.wishId,
|
|
352
|
+
kind: 'recalled',
|
|
353
|
+
ts: now,
|
|
354
|
+
ctx: 'gotry_wish_pool_list.recall'
|
|
217
355
|
});
|
|
218
|
-
|
|
356
|
+
const utility = projectUtility(next)[match.wishId];
|
|
219
357
|
return {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
358
|
+
ok: true,
|
|
359
|
+
suggestion: {
|
|
360
|
+
wish_id: match.entry['wish_id'],
|
|
361
|
+
name: match.entry['name'],
|
|
362
|
+
reason: match.entry['reason'],
|
|
363
|
+
conditions: match.entry['conditions'],
|
|
364
|
+
match_score: match.score,
|
|
365
|
+
hits: match.hits
|
|
366
|
+
},
|
|
367
|
+
utility: {
|
|
368
|
+
status: utility?.status ?? 'unknown',
|
|
369
|
+
recalled: utility?.recalled ?? 1
|
|
370
|
+
},
|
|
371
|
+
summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`
|
|
223
372
|
};
|
|
224
373
|
},
|
|
225
374
|
presentCall: (args)=>({
|
|
226
375
|
card: 'generic',
|
|
227
|
-
title: '
|
|
228
|
-
kind: '
|
|
229
|
-
rawInput: args.
|
|
376
|
+
title: '「下一次出发」召回',
|
|
377
|
+
kind: 'search',
|
|
378
|
+
rawInput: args.query
|
|
379
|
+
})
|
|
380
|
+
}));
|
|
381
|
+
registerGuarded(defineTool({
|
|
382
|
+
name: 'gotry_companion_save',
|
|
383
|
+
description: 'Save/update a travel companion profile (memory-design M5 layer): constraints that shape itinerary STRUCTURE and ranking — ' + 'never a hard filter (「爸爸65轻度高血压」→ 不排高海拔/控制步行量;「晕车」→ 优先火车/备提示). ' + 'evidence MUST be the user\'s verbatim words (append-only, traceable). ' + 'NEGATIVE LIST: passport/ID/phone numbers are rejected on sight — such fields never enter storage.',
|
|
384
|
+
parameters: {
|
|
385
|
+
companion: {
|
|
386
|
+
type: 'json',
|
|
387
|
+
required: true,
|
|
388
|
+
description: '{ label: "爸爸", constraints: { mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }, evidence: "<用户原话>" }'
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
output: {
|
|
392
|
+
schema: {
|
|
393
|
+
type: 'json'
|
|
394
|
+
},
|
|
395
|
+
render: (_args, value)=>[
|
|
396
|
+
{
|
|
397
|
+
type: 'text',
|
|
398
|
+
text: String(value.summary ?? '')
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
async execute (args, _exec) {
|
|
403
|
+
const c = args.companion ?? {};
|
|
404
|
+
if (!c.evidence) return {
|
|
405
|
+
ok: false,
|
|
406
|
+
summary: 'evidence 必填(用户原话,溯源 P0)'
|
|
407
|
+
};
|
|
408
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
409
|
+
const res = ledger.appendCompanion({
|
|
410
|
+
label: String(c.label ?? ''),
|
|
411
|
+
constraints: {
|
|
412
|
+
mobility: c.constraints?.mobility,
|
|
413
|
+
health: c.constraints?.health,
|
|
414
|
+
prefs: c.constraints?.prefs
|
|
415
|
+
},
|
|
416
|
+
evidence: c.evidence
|
|
417
|
+
});
|
|
418
|
+
if (!res.appended && res.reason) return {
|
|
419
|
+
ok: false,
|
|
420
|
+
summary: res.reason
|
|
421
|
+
};
|
|
422
|
+
return {
|
|
423
|
+
ok: true,
|
|
424
|
+
appended: res.appended,
|
|
425
|
+
companion_id: res.companionId,
|
|
426
|
+
total: res.total,
|
|
427
|
+
summary: res.appended ? `同行人已保存:${res.companionId}` : `无新内容(幂等跳过):${res.companionId}`
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
presentCall: (args)=>({
|
|
431
|
+
card: 'generic',
|
|
432
|
+
title: '保存同行人',
|
|
433
|
+
kind: 'edit',
|
|
434
|
+
rawInput: args.companion
|
|
435
|
+
})
|
|
436
|
+
}));
|
|
437
|
+
registerGuarded(defineTool({
|
|
438
|
+
name: 'gotry_trip_log',
|
|
439
|
+
description: 'Record a PAST trip into the travel timeline (memory-design M4 layer). Use when the user mentions a trip they already took ' + '(「去年国庆去了大理」) — dates resolve via the time anchor (词表外日期会拒绝,请用户给绝对日期), evidence MUST be the user\'s verbatim words. ' + 'Append-only and idempotent (same destination+start+source = same trip); overlapping same-destination trips are rejected for human adjudication. ' + 'Past trips power origin resolution and 「去过不再推」 ranking — never a hard filter.',
|
|
440
|
+
parameters: {
|
|
441
|
+
query: {
|
|
442
|
+
type: 'json',
|
|
443
|
+
required: true,
|
|
444
|
+
description: '{ destination: "大理", start: "2025-10-01 或绝对日期表达", end?: 同上, companions?: ["爸爸"], evidence: "<用户原话>" }'
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
output: {
|
|
448
|
+
schema: {
|
|
449
|
+
type: 'json'
|
|
450
|
+
},
|
|
451
|
+
render: (_args, value)=>[
|
|
452
|
+
{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: String(value.summary ?? '')
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
async execute (args, _exec) {
|
|
459
|
+
const q = unwrapQuery(args, 'destination');
|
|
460
|
+
if (!q.destination) return {
|
|
461
|
+
ok: false,
|
|
462
|
+
summary: 'destination 必填'
|
|
463
|
+
};
|
|
464
|
+
if (!q.evidence) return {
|
|
465
|
+
ok: false,
|
|
466
|
+
summary: 'evidence 必填(用户原话,溯源 P0)'
|
|
467
|
+
};
|
|
468
|
+
const anchor = buildTimeAnchor(new Date());
|
|
469
|
+
const start = resolveTimelineDate(q.start ?? '', anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.start ?? '') ? q.start : undefined);
|
|
470
|
+
if (!start) return {
|
|
471
|
+
ok: false,
|
|
472
|
+
summary: `start 无法解析为绝对日期(${q.start ?? '缺'})——请向用户要具体日期,不猜`
|
|
473
|
+
};
|
|
474
|
+
const end = q.end ? resolveTimelineDate(q.end, anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.end) ? q.end : undefined) : undefined;
|
|
475
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
476
|
+
const res = ledger.appendTripEvent({
|
|
477
|
+
destination: q.destination.trim(),
|
|
478
|
+
start,
|
|
479
|
+
end,
|
|
480
|
+
companions: q.companions,
|
|
481
|
+
source: 'user-verbatim',
|
|
482
|
+
evidence: q.evidence
|
|
483
|
+
});
|
|
484
|
+
if (!res.appended && res.reason) return {
|
|
485
|
+
ok: false,
|
|
486
|
+
summary: res.reason
|
|
487
|
+
};
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
appended: res.appended,
|
|
491
|
+
trip_id: res.tripId,
|
|
492
|
+
total: res.total,
|
|
493
|
+
summary: res.appended ? `已入时间线:${q.destination} @ ${start}` : `已存在(幂等跳过):${res.tripId}`
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
presentCall: (args)=>({
|
|
497
|
+
card: 'generic',
|
|
498
|
+
title: '记录旅行',
|
|
499
|
+
kind: 'edit',
|
|
500
|
+
rawInput: args.query
|
|
230
501
|
})
|
|
231
502
|
}));
|
|
232
503
|
registerGuarded(defineTool({
|
|
233
504
|
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.',
|
|
505
|
+
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
506
|
parameters: {
|
|
236
507
|
query: {
|
|
237
508
|
type: 'json',
|
|
238
509
|
required: true,
|
|
239
|
-
description: '{ destination: "
|
|
510
|
+
description: '{ destination: "<目的地城市>", checkIn?: "YYYY-MM-DD 或自然表达(下周五/8.20)", checkOut?: 同上, occupancy?: { adults: 2 } }'
|
|
240
511
|
}
|
|
241
512
|
},
|
|
242
513
|
output: {
|
|
@@ -251,14 +522,26 @@ export function apply(ctx, config) {
|
|
|
251
522
|
]
|
|
252
523
|
},
|
|
253
524
|
async execute (args, _exec) {
|
|
254
|
-
const q = args
|
|
525
|
+
const q = unwrapQuery(args, 'destination');
|
|
255
526
|
if (!q.destination) throw new Error('gotry_hotel_search requires destination');
|
|
256
527
|
const started = Date.now();
|
|
257
528
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json');
|
|
529
|
+
const anchor = buildTimeAnchor(new Date());
|
|
530
|
+
const dateNotes = [];
|
|
531
|
+
const resolveDate = (expr)=>{
|
|
532
|
+
if (!expr) return undefined;
|
|
533
|
+
const r = resolveSlotDate(expr, anchor);
|
|
534
|
+
if (!r.date) {
|
|
535
|
+
dateNotes.push(`日期未解析:${r.raw}——请向用户确认具体日期`);
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
if (r.raw !== r.date) dateNotes.push(`slot-resolved: ${r.raw} → ${r.date}`);
|
|
539
|
+
return r.date;
|
|
540
|
+
};
|
|
258
541
|
const resp = await hbcliSearchHotels({
|
|
259
542
|
destination: q.destination,
|
|
260
|
-
checkIn: q.checkIn,
|
|
261
|
-
checkOut: q.checkOut,
|
|
543
|
+
checkIn: resolveDate(q.checkIn),
|
|
544
|
+
checkOut: resolveDate(q.checkOut),
|
|
262
545
|
adults: q.adults
|
|
263
546
|
}, {
|
|
264
547
|
hbcliBin: config.hbcliBin,
|
|
@@ -270,22 +553,40 @@ export function apply(ctx, config) {
|
|
|
270
553
|
const evidence = isLive ? resp.evidence : '[静态包:估算]';
|
|
271
554
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `hotel_search:${resp.via}`).catch(()=>{});
|
|
272
555
|
const payload = {
|
|
556
|
+
ok: true,
|
|
273
557
|
hotels: resp.hotels ?? null,
|
|
274
558
|
evidence,
|
|
275
559
|
destination: q.destination,
|
|
276
560
|
via: resp.via,
|
|
277
561
|
latency_ms: Date.now() - started,
|
|
278
562
|
summary: resp.summary,
|
|
279
|
-
error: resp.error
|
|
563
|
+
error: resp.error,
|
|
564
|
+
...dateNotes.length ? {
|
|
565
|
+
date_notes: dateNotes
|
|
566
|
+
} : {}
|
|
280
567
|
};
|
|
281
568
|
return JSON.parse(JSON.stringify(payload));
|
|
282
569
|
},
|
|
283
570
|
presentCall: (args)=>({
|
|
284
571
|
card: 'generic',
|
|
285
572
|
title: `酒店搜索:${String(args.query?.destination ?? '')}`,
|
|
286
|
-
kind: '
|
|
573
|
+
kind: 'search',
|
|
287
574
|
rawInput: args.query
|
|
288
|
-
})
|
|
575
|
+
}),
|
|
576
|
+
presentResult: (_args, value)=>{
|
|
577
|
+
const r = value;
|
|
578
|
+
const n = Array.isArray(r.hotels) ? r.hotels.length : 0;
|
|
579
|
+
return {
|
|
580
|
+
card: 'generic',
|
|
581
|
+
title: `酒店:${r.destination ?? ''} ${n ? `${n} 家(${r.via === 'hbcli-realtime' ? '实时' : '静态包'})` : '无结果'}`,
|
|
582
|
+
content: [
|
|
583
|
+
{
|
|
584
|
+
type: 'text',
|
|
585
|
+
text: String(r.summary ?? '')
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
};
|
|
589
|
+
}
|
|
289
590
|
}));
|
|
290
591
|
registerGuarded(defineTool({
|
|
291
592
|
name: 'gotry_skeleton_check',
|
|
@@ -304,38 +605,27 @@ export function apply(ctx, config) {
|
|
|
304
605
|
},
|
|
305
606
|
output: {
|
|
306
607
|
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
|
-
}
|
|
608
|
+
type: 'json'
|
|
323
609
|
},
|
|
324
610
|
render: (_args, value)=>[
|
|
325
611
|
{
|
|
326
612
|
type: 'text',
|
|
327
|
-
text: String(value.evidence ??
|
|
613
|
+
text: String(value.evidence ?? JSON.stringify(value))
|
|
328
614
|
}
|
|
329
615
|
]
|
|
330
616
|
},
|
|
331
617
|
async execute (args, _exec) {
|
|
332
|
-
const
|
|
333
|
-
|
|
618
|
+
const q = unwrapQuery(args);
|
|
619
|
+
const verdict = await checkConnectivity(String(q.from ?? ''), String(q.to ?? ''));
|
|
620
|
+
return JSON.parse(JSON.stringify({
|
|
621
|
+
ok: true,
|
|
622
|
+
...verdict
|
|
623
|
+
}));
|
|
334
624
|
},
|
|
335
625
|
presentCall: (args)=>({
|
|
336
626
|
card: 'generic',
|
|
337
627
|
title: `骨架校验:${args.from}-${args.to}`,
|
|
338
|
-
kind: '
|
|
628
|
+
kind: 'execute',
|
|
339
629
|
rawInput: args
|
|
340
630
|
})
|
|
341
631
|
}));
|
|
@@ -346,7 +636,7 @@ export function apply(ctx, config) {
|
|
|
346
636
|
query: {
|
|
347
637
|
type: 'json',
|
|
348
638
|
required: true,
|
|
349
|
-
description: '{ place: "
|
|
639
|
+
description: '{ place: "<城市名>", month?: 8, mode?: "forecast"|"climate", days?: 7 }'
|
|
350
640
|
}
|
|
351
641
|
},
|
|
352
642
|
output: {
|
|
@@ -361,7 +651,7 @@ export function apply(ctx, config) {
|
|
|
361
651
|
]
|
|
362
652
|
},
|
|
363
653
|
async execute (args, _exec) {
|
|
364
|
-
const q = args
|
|
654
|
+
const q = unwrapQuery(args, 'place');
|
|
365
655
|
const started = Date.now();
|
|
366
656
|
let lat = q.lat, lng = q.lng;
|
|
367
657
|
let placeLabel = q.place ?? `${q.lat},${q.lng}`;
|
|
@@ -407,9 +697,24 @@ export function apply(ctx, config) {
|
|
|
407
697
|
presentCall: (args)=>({
|
|
408
698
|
card: 'generic',
|
|
409
699
|
title: `天气:${String(args.query?.place ?? '')}`,
|
|
410
|
-
kind: '
|
|
700
|
+
kind: 'fetch',
|
|
411
701
|
rawInput: args.query
|
|
412
|
-
})
|
|
702
|
+
}),
|
|
703
|
+
presentResult: (args, value)=>{
|
|
704
|
+
const r = value;
|
|
705
|
+
const place = String(args.query?.place ?? '');
|
|
706
|
+
const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable');
|
|
707
|
+
return {
|
|
708
|
+
card: 'generic',
|
|
709
|
+
title: `天气:${place} ${failed ? '降级' : 'ok'}`,
|
|
710
|
+
content: [
|
|
711
|
+
{
|
|
712
|
+
type: 'text',
|
|
713
|
+
text: String(r.summary ?? '')
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
413
718
|
}));
|
|
414
719
|
registerGuarded(defineTool({
|
|
415
720
|
name: 'gotry_flight_verify',
|
|
@@ -433,7 +738,7 @@ export function apply(ctx, config) {
|
|
|
433
738
|
]
|
|
434
739
|
},
|
|
435
740
|
async execute (args, _exec) {
|
|
436
|
-
const q = args
|
|
741
|
+
const q = unwrapQuery(args, 'callsign');
|
|
437
742
|
const started = Date.now();
|
|
438
743
|
if (!q.callsign) {
|
|
439
744
|
return JSON.parse(JSON.stringify({
|
|
@@ -451,6 +756,7 @@ export function apply(ctx, config) {
|
|
|
451
756
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `flight_verify:${r.via}`).catch(()=>{});
|
|
452
757
|
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
758
|
return JSON.parse(JSON.stringify({
|
|
759
|
+
ok: true,
|
|
454
760
|
verdict: r.verdict,
|
|
455
761
|
callsign: r.callsign,
|
|
456
762
|
airport: r.airport,
|
|
@@ -464,18 +770,145 @@ export function apply(ctx, config) {
|
|
|
464
770
|
presentCall: (args)=>({
|
|
465
771
|
card: 'generic',
|
|
466
772
|
title: `飞行校验:${String(args.query?.callsign ?? '')}`,
|
|
467
|
-
kind: '
|
|
773
|
+
kind: 'fetch',
|
|
468
774
|
rawInput: args.query
|
|
469
775
|
})
|
|
470
776
|
}));
|
|
777
|
+
registerGuarded(defineTool({
|
|
778
|
+
name: 'gotry_flyai_search',
|
|
779
|
+
description: 'Live flight/train search via Fliggy official FlyAI channel (read-only, no key, booking only via jumpUrl by the human). ' + 'PRIMARY source for real schedules & prices (RFC user-session-data-rfc). Input: kind=flight|train, from/to city names (Chinese), date YYYY-MM-DD. ' + 'Evidence [实时API:flyai@ts]. Cross-validate expensive claims against gotry_session_search when it matters.',
|
|
780
|
+
parameters: {
|
|
781
|
+
query: {
|
|
782
|
+
type: 'json',
|
|
783
|
+
required: true,
|
|
784
|
+
description: '{ kind: "flight"|"train", from: "上海", to: "丽江", date: "2026-10-01" }'
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
output: {
|
|
788
|
+
schema: {
|
|
789
|
+
type: 'json'
|
|
790
|
+
},
|
|
791
|
+
render: (_a, v)=>[
|
|
792
|
+
{
|
|
793
|
+
type: 'text',
|
|
794
|
+
text: String(v.summary ?? JSON.stringify(v).slice(0, 600))
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
},
|
|
798
|
+
async execute (args, _exec) {
|
|
799
|
+
const q = unwrapQuery(args, 'from');
|
|
800
|
+
const kind = q.kind === 'train' ? 'train' : 'flight';
|
|
801
|
+
if (!q.from || !q.to || !q.date) {
|
|
802
|
+
return {
|
|
803
|
+
ok: false,
|
|
804
|
+
summary: '需要 from/to(中文城市名)与 date(YYYY-MM-DD)'
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const r = await flyaiSearch({
|
|
808
|
+
kind,
|
|
809
|
+
origin: q.from,
|
|
810
|
+
destination: q.to,
|
|
811
|
+
depDate: q.date
|
|
812
|
+
});
|
|
813
|
+
const top = (r.options ?? []).slice(0, 8).map((o)=>`${o.no} ${o.name} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
814
|
+
const summary = r.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} ${kind === 'flight' ? '机票' : '火车票'}(飞猪官方只读)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}` : `${q.from}→${q.to} ${q.date} 无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
815
|
+
return JSON.parse(JSON.stringify({
|
|
816
|
+
...r,
|
|
817
|
+
kind,
|
|
818
|
+
summary
|
|
819
|
+
}));
|
|
820
|
+
},
|
|
821
|
+
presentCall: (args)=>({
|
|
822
|
+
card: 'generic',
|
|
823
|
+
title: `官方检索:${String(args.query?.kind ?? 'flight')}`,
|
|
824
|
+
kind: 'fetch',
|
|
825
|
+
rawInput: args.query
|
|
826
|
+
}),
|
|
827
|
+
presentResult: (args, value)=>{
|
|
828
|
+
const r = value;
|
|
829
|
+
return {
|
|
830
|
+
card: 'generic',
|
|
831
|
+
title: `飞猪检索:${r.ok && (r.options?.length ?? 0) > 0 ? `${r.options.length} 条` : '降级'}`,
|
|
832
|
+
content: [
|
|
833
|
+
{
|
|
834
|
+
type: 'text',
|
|
835
|
+
text: String(value.summary ?? '')
|
|
836
|
+
}
|
|
837
|
+
]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}));
|
|
841
|
+
registerGuarded(defineTool({
|
|
842
|
+
name: 'gotry_session_search',
|
|
843
|
+
description: 'Cross-validation search on the user\'s OWN logged-in browser session (dedicated persistent profile, ReadGuard = physically read-only: ' + 'write requests are aborted at network layer; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). ' + 'Currently ctrip-flight: sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. ' + 'verdict needs-login = run scripts/session-login.ts once with the human logging in. Rate-limited (≥30s between same-site calls). ' + 'Use to verify/double-check prices from gotry_flyai_search on the same itinerary (split by 直达/中转).',
|
|
844
|
+
parameters: {
|
|
845
|
+
query: {
|
|
846
|
+
type: 'json',
|
|
847
|
+
required: true,
|
|
848
|
+
description: '{ from: "上海", to: "丽江", date: "2026-10-01" }'
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
output: {
|
|
852
|
+
schema: {
|
|
853
|
+
type: 'json'
|
|
854
|
+
},
|
|
855
|
+
render: (_a, v)=>[
|
|
856
|
+
{
|
|
857
|
+
type: 'text',
|
|
858
|
+
text: String(v.summary ?? JSON.stringify(v).slice(0, 600))
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
async execute (args, _exec) {
|
|
863
|
+
const q = unwrapQuery(args, 'from');
|
|
864
|
+
if (!q.from || !q.to || !q.date) {
|
|
865
|
+
return {
|
|
866
|
+
ok: false,
|
|
867
|
+
summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD)'
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const r = await sessionFlightSearch({
|
|
871
|
+
from: q.from,
|
|
872
|
+
to: q.to,
|
|
873
|
+
date: q.date,
|
|
874
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl')
|
|
875
|
+
});
|
|
876
|
+
const top = (r.options ?? []).slice(0, 8).map((o)=>`${o.flightNo} ${o.airline} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
877
|
+
const summary = r.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} 会话检索(携程,用户本人登录态)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}` : `会话检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`;
|
|
878
|
+
return JSON.parse(JSON.stringify({
|
|
879
|
+
...r,
|
|
880
|
+
summary
|
|
881
|
+
}));
|
|
882
|
+
},
|
|
883
|
+
presentCall: (args)=>({
|
|
884
|
+
card: 'generic',
|
|
885
|
+
title: `会话检索:${String(args.query?.from ?? '')}`,
|
|
886
|
+
kind: 'fetch',
|
|
887
|
+
rawInput: args.query
|
|
888
|
+
}),
|
|
889
|
+
presentResult: (_args, value)=>{
|
|
890
|
+
const r = value;
|
|
891
|
+
const label = r.verdict === 'hit' ? `会话 ${r.options.length} 条` : r.verdict === 'needs-login' ? '需登录' : r.verdict ?? '降级';
|
|
892
|
+
return {
|
|
893
|
+
card: 'generic',
|
|
894
|
+
title: `会话检索:${label}`,
|
|
895
|
+
content: [
|
|
896
|
+
{
|
|
897
|
+
type: 'text',
|
|
898
|
+
text: String(value.summary ?? '')
|
|
899
|
+
}
|
|
900
|
+
]
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}));
|
|
471
904
|
registerGuarded(defineTool({
|
|
472
905
|
name: 'gotry_anything_search',
|
|
473
|
-
description: '
|
|
906
|
+
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
907
|
parameters: {
|
|
475
908
|
query: {
|
|
476
909
|
type: 'json',
|
|
477
910
|
required: true,
|
|
478
|
-
description: '{ keyword: "
|
|
911
|
+
description: '{ keyword: "<搜索关键词>", contentType?: "city"|"hotel", parentDestinationId?: "?", timeoutMs?: 12000 }'
|
|
479
912
|
}
|
|
480
913
|
},
|
|
481
914
|
output: {
|
|
@@ -490,7 +923,7 @@ export function apply(ctx, config) {
|
|
|
490
923
|
]
|
|
491
924
|
},
|
|
492
925
|
async execute (args, _exec) {
|
|
493
|
-
const q = args
|
|
926
|
+
const q = unwrapQuery(args, 'keyword');
|
|
494
927
|
const started = Date.now();
|
|
495
928
|
if (!q.keyword) {
|
|
496
929
|
return JSON.parse(JSON.stringify({
|
|
@@ -520,9 +953,23 @@ export function apply(ctx, config) {
|
|
|
520
953
|
presentCall: (args)=>({
|
|
521
954
|
card: 'generic',
|
|
522
955
|
title: `Anything search:${String(args.query?.keyword ?? '')}`,
|
|
523
|
-
kind: '
|
|
956
|
+
kind: 'search',
|
|
524
957
|
rawInput: args.query
|
|
525
|
-
})
|
|
958
|
+
}),
|
|
959
|
+
presentResult: (_args, value)=>{
|
|
960
|
+
const r = value;
|
|
961
|
+
const n = Array.isArray(r.hits) ? r.hits.length : r.total_candidates ?? 0;
|
|
962
|
+
return {
|
|
963
|
+
card: 'generic',
|
|
964
|
+
title: `Anything:${r.keyword ?? ''} ${r.verdict === 'hit' ? `${n} hits` : r.verdict ?? 'no-result'}`,
|
|
965
|
+
content: [
|
|
966
|
+
{
|
|
967
|
+
type: 'text',
|
|
968
|
+
text: String(r.summary ?? '')
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
};
|
|
972
|
+
}
|
|
526
973
|
}));
|
|
527
974
|
registerGuarded(defineTool({
|
|
528
975
|
name: 'gotry_web_search',
|
|
@@ -546,7 +993,7 @@ export function apply(ctx, config) {
|
|
|
546
993
|
]
|
|
547
994
|
},
|
|
548
995
|
async execute (args, _exec) {
|
|
549
|
-
const q = args
|
|
996
|
+
const q = unwrapQuery(args, 'url');
|
|
550
997
|
const started = Date.now();
|
|
551
998
|
if (!q.url) {
|
|
552
999
|
return JSON.parse(JSON.stringify({
|
|
@@ -576,7 +1023,7 @@ export function apply(ctx, config) {
|
|
|
576
1023
|
presentCall: (args)=>({
|
|
577
1024
|
card: 'generic',
|
|
578
1025
|
title: `读网页:${String(args.query?.url ?? '')}`,
|
|
579
|
-
kind: '
|
|
1026
|
+
kind: 'fetch',
|
|
580
1027
|
rawInput: args.query
|
|
581
1028
|
})
|
|
582
1029
|
}));
|
|
@@ -602,7 +1049,7 @@ export function apply(ctx, config) {
|
|
|
602
1049
|
]
|
|
603
1050
|
},
|
|
604
1051
|
async execute (args, _exec) {
|
|
605
|
-
const q = args
|
|
1052
|
+
const q = unwrapQuery(args, 'url');
|
|
606
1053
|
if (!q.url) {
|
|
607
1054
|
return JSON.parse(JSON.stringify({
|
|
608
1055
|
ok: false,
|
|
@@ -627,7 +1074,7 @@ export function apply(ctx, config) {
|
|
|
627
1074
|
presentCall: (args)=>({
|
|
628
1075
|
card: 'generic',
|
|
629
1076
|
title: `视频字幕:${String(args.query?.url ?? '')}`,
|
|
630
|
-
kind: '
|
|
1077
|
+
kind: 'fetch',
|
|
631
1078
|
rawInput: args.query
|
|
632
1079
|
})
|
|
633
1080
|
}));
|
|
@@ -653,7 +1100,7 @@ export function apply(ctx, config) {
|
|
|
653
1100
|
]
|
|
654
1101
|
},
|
|
655
1102
|
async execute (args, _exec) {
|
|
656
|
-
const q = args
|
|
1103
|
+
const q = unwrapQuery(args, 'query');
|
|
657
1104
|
if (!q.query) {
|
|
658
1105
|
return JSON.parse(JSON.stringify({
|
|
659
1106
|
ok: false,
|
|
@@ -678,13 +1125,13 @@ export function apply(ctx, config) {
|
|
|
678
1125
|
presentCall: (args)=>({
|
|
679
1126
|
card: 'generic',
|
|
680
1127
|
title: `GitHub 搜索:${String(args.query?.query ?? '')}`,
|
|
681
|
-
kind: '
|
|
1128
|
+
kind: 'search',
|
|
682
1129
|
rawInput: args.query
|
|
683
1130
|
})
|
|
684
1131
|
}));
|
|
685
1132
|
registerGuarded(defineTool({
|
|
686
1133
|
name: 'gotry_agent_reach',
|
|
687
|
-
description: 'Agent Reach — thin wrapper over Panniantong/Agent-Reach upstream registry
|
|
1134
|
+
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
1135
|
parameters: {
|
|
689
1136
|
query: {
|
|
690
1137
|
type: 'json',
|
|
@@ -704,7 +1151,7 @@ export function apply(ctx, config) {
|
|
|
704
1151
|
]
|
|
705
1152
|
},
|
|
706
1153
|
async execute (args, _exec) {
|
|
707
|
-
const q = args
|
|
1154
|
+
const q = unwrapQuery(args, 'channel');
|
|
708
1155
|
const started = Date.now();
|
|
709
1156
|
const dir = await ensureStateDir(config.stateRoot);
|
|
710
1157
|
if (q.action === 'status' || !q.action && !q.channel) {
|
|
@@ -732,6 +1179,23 @@ export function apply(ctx, config) {
|
|
|
732
1179
|
args: q.args,
|
|
733
1180
|
timeoutMs: q.timeoutMs
|
|
734
1181
|
});
|
|
1182
|
+
const dataStr = (v)=>{
|
|
1183
|
+
if (Array.isArray(v)) {
|
|
1184
|
+
const kept = [];
|
|
1185
|
+
let budget = 3500;
|
|
1186
|
+
for (const item of v){
|
|
1187
|
+
const s = JSON.stringify(item);
|
|
1188
|
+
if (budget - s.length < 0) break;
|
|
1189
|
+
budget -= s.length + 1;
|
|
1190
|
+
kept.push(item);
|
|
1191
|
+
}
|
|
1192
|
+
if (kept.length < v.length) kept.push(`…(截断:保留 ${kept.length}/${v.length} 条;可用上游方法带 limit 参数取更少)`);
|
|
1193
|
+
return kept;
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof v === 'string') return v.length > 4000 ? v.slice(0, 4000) + '…(截断)' : v;
|
|
1196
|
+
return v;
|
|
1197
|
+
};
|
|
1198
|
+
if (r.ok) r.data = dataStr(r.data);
|
|
735
1199
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${q.channel}.${q.method}:${r.verdict}`).catch(()=>{});
|
|
736
1200
|
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
1201
|
return JSON.parse(JSON.stringify({
|
|
@@ -750,9 +1214,24 @@ export function apply(ctx, config) {
|
|
|
750
1214
|
presentCall: (args)=>({
|
|
751
1215
|
card: 'generic',
|
|
752
1216
|
title: `Agent Reach:${String(args.query?.channel ?? '')}.${String(args.query?.method ?? 'status')}`,
|
|
753
|
-
kind: '
|
|
1217
|
+
kind: 'fetch',
|
|
754
1218
|
rawInput: args.query
|
|
755
|
-
})
|
|
1219
|
+
}),
|
|
1220
|
+
presentResult: (args, value)=>{
|
|
1221
|
+
const r = value;
|
|
1222
|
+
const q = args.query ?? {};
|
|
1223
|
+
const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌';
|
|
1224
|
+
return {
|
|
1225
|
+
card: 'generic',
|
|
1226
|
+
title: `AgentReach ${icon} ${q.channel ?? ''}.${q.method ?? 'status'} ${r.verdict ?? ''}`,
|
|
1227
|
+
content: [
|
|
1228
|
+
{
|
|
1229
|
+
type: 'text',
|
|
1230
|
+
text: String(r.summary ?? '')
|
|
1231
|
+
}
|
|
1232
|
+
]
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
756
1235
|
}));
|
|
757
1236
|
}
|
|
758
1237
|
|