@danceiny/gotry 0.0.1-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/bin/gotry-inner.js +214 -0
- package/bin/gotry-stdio-ask.js +59 -0
- package/bin/gotry.js +28 -0
- package/cordis.gotry-patch.yml +86 -0
- package/data/flights_2026.json +218 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +151 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +202 -0
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +143 -0
- package/dist/capabilities/incident-log.js +100 -0
- package/dist/capabilities/opensky.js +74 -0
- 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 +99 -0
- package/dist/capabilities/session/transport.js +101 -0
- package/dist/capabilities/session-search.js +93 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +37 -0
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +101 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/ledger-tests.js +365 -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/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +88 -0
- package/dist/scripts/publish-preverify.js +68 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +116 -0
- package/dist/scripts/session-attach-diagnose.js +27 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +37 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +328 -0
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +248 -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 +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/companions.js +112 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +174 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +1239 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +360 -0
- 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 +103 -0
- package/dist/src/model.js +134 -0
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +901 -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 +538 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +84 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +158 -0
- package/ts/capabilities/anything.ts +201 -0
- package/ts/capabilities/flyai.ts +151 -0
- package/ts/capabilities/hbcli.ts +152 -0
- package/ts/capabilities/incident-log.ts +148 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/transport.ts +114 -0
- package/ts/capabilities/session-search.ts +129 -0
- package/ts/capabilities/weather.ts +176 -0
- package/ts/package.json +27 -0
- package/ts/scripts/async-collect.ts +50 -0
- package/ts/scripts/skeleton-check.ts +49 -0
- package/ts/scripts/skeleton-integration-test.ts +23 -0
- package/ts/scripts/state-cli.ts +201 -0
- package/ts/src/bridge.ts +44 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +165 -0
- package/ts/src/dsh-llm.ts +155 -0
- package/ts/src/index.ts +968 -0
- package/ts/src/loop.ts +411 -0
- package/ts/src/memory-capture.ts +59 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +100 -0
- package/ts/src/model.ts +220 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +849 -0
- package/ts/src/time-anchor.ts +134 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/unified.ts +582 -0
- package/ts/src/wish-pool.ts +66 -0
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
|
+
import { ensureStateDir, recordLatency } from './bridge.js';
|
|
5
|
+
import { segmentsFromCandidate, solveChoiceSegment } from './unified.js';
|
|
6
|
+
import { checkConnectivity } from '../scripts/skeleton-check.js';
|
|
7
|
+
import { parseCandidate, parseRequest } from './model.js';
|
|
8
|
+
import { searchHotels as hbcliSearchHotels } from '../capabilities/hbcli.js';
|
|
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';
|
|
17
|
+
import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
|
|
18
|
+
import { verifyFlight } from '../capabilities/opensky.js';
|
|
19
|
+
import { anythingSearch } from '../capabilities/anything.js';
|
|
20
|
+
import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
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';
|
|
24
|
+
export const name = 'gotry-tools';
|
|
25
|
+
export const inject = [
|
|
26
|
+
'tools',
|
|
27
|
+
'systemPrompt'
|
|
28
|
+
];
|
|
29
|
+
export const Config = z.object({
|
|
30
|
+
stateRoot: z.string().default('.'),
|
|
31
|
+
timeoutMs: z.number().default(30_000),
|
|
32
|
+
hbcliBin: z.string().default('hbcli')
|
|
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
|
+
}
|
|
76
|
+
export function apply(ctx, config) {
|
|
77
|
+
const sp = ctx['systemPrompt'];
|
|
78
|
+
sp?.variable?.('current_date', ()=>{
|
|
79
|
+
const d = new Date();
|
|
80
|
+
const ymd = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
81
|
+
const weekdays = [
|
|
82
|
+
'日',
|
|
83
|
+
'一',
|
|
84
|
+
'二',
|
|
85
|
+
'三',
|
|
86
|
+
'四',
|
|
87
|
+
'五',
|
|
88
|
+
'六'
|
|
89
|
+
];
|
|
90
|
+
return `${ymd} 周${weekdays[d.getDay()]}`;
|
|
91
|
+
});
|
|
92
|
+
sp?.variable?.('time_anchor_card', ()=>buildTimeAnchor(new Date()).card);
|
|
93
|
+
sp?.variable?.('motivation_brief', ()=>renderMotivationBrief(config.stateRoot ?? '.'));
|
|
94
|
+
installProcessGuards(config.stateRoot ?? '.', {
|
|
95
|
+
uncaughtException: 'gotry-tools',
|
|
96
|
+
unhandledRejection: 'gotry-tools'
|
|
97
|
+
});
|
|
98
|
+
const registerGuarded = (tool)=>{
|
|
99
|
+
const t = {
|
|
100
|
+
...tool
|
|
101
|
+
};
|
|
102
|
+
if (typeof t.execute === 'function') {
|
|
103
|
+
t.execute = guardToolExecute(String(t.name), config.stateRoot ?? '.', t.execute);
|
|
104
|
+
}
|
|
105
|
+
ctx.tools.register(t);
|
|
106
|
+
};
|
|
107
|
+
registerGuarded(defineTool({
|
|
108
|
+
name: 'gotry_feasibility_check',
|
|
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.',
|
|
110
|
+
parameters: {
|
|
111
|
+
payload: {
|
|
112
|
+
type: 'json',
|
|
113
|
+
required: true,
|
|
114
|
+
description: 'The full engine payload: { request, candidates }.'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
output: {
|
|
118
|
+
schema: {
|
|
119
|
+
type: 'json'
|
|
120
|
+
},
|
|
121
|
+
render: (_args, value)=>[
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: String(value.answer_md ?? JSON.stringify(value))
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
async execute (args, _exec) {
|
|
129
|
+
const started = Date.now();
|
|
130
|
+
const payload = args.payload;
|
|
131
|
+
const req = parseRequest(payload['request']);
|
|
132
|
+
const cands = payload['candidates'].map(parseCandidate);
|
|
133
|
+
const spec = segmentsFromCandidate(req, cands);
|
|
134
|
+
const result = solveChoiceSegment(spec, req);
|
|
135
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
136
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(()=>{});
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
...result,
|
|
140
|
+
latency_ms: Date.now() - started,
|
|
141
|
+
via: 'in-process-unified'
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
presentCall: (args)=>({
|
|
145
|
+
card: 'generic',
|
|
146
|
+
title: 'GoTry 可行性检查(门到门全成本)',
|
|
147
|
+
kind: 'execute',
|
|
148
|
+
rawInput: args.payload
|
|
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
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
registerGuarded(defineTool({
|
|
171
|
+
name: 'gotry_motivation_save',
|
|
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).',
|
|
173
|
+
parameters: {
|
|
174
|
+
profile: {
|
|
175
|
+
type: 'json',
|
|
176
|
+
required: true,
|
|
177
|
+
description: '{ weights: {escape_rest: 0.7, ...}, evidence: [user quotes...], hard: {wake_not_before, min_arrival_energy_pct} }'
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
output: {
|
|
181
|
+
schema: {
|
|
182
|
+
type: 'json'
|
|
183
|
+
},
|
|
184
|
+
render: (_args, value)=>[
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: `动机画像已保存:${String(value.path ?? '')}`
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
async execute (args, _exec) {
|
|
192
|
+
const incoming = args.profile ?? {};
|
|
193
|
+
if (!incoming.evidence?.length) {
|
|
194
|
+
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)');
|
|
195
|
+
}
|
|
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,
|
|
205
|
+
saved: true,
|
|
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: '无新内容(幂等跳过)'
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
presentCall: (args)=>({
|
|
218
|
+
card: 'generic',
|
|
219
|
+
title: '保存动机画像',
|
|
220
|
+
kind: 'edit',
|
|
221
|
+
rawInput: args.profile
|
|
222
|
+
})
|
|
223
|
+
}));
|
|
224
|
+
registerGuarded(defineTool({
|
|
225
|
+
name: 'gotry_wish_pool_add',
|
|
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 (永不删除,只是不再召回). 憧憬不被拒绝。',
|
|
227
|
+
parameters: {
|
|
228
|
+
entry: {
|
|
229
|
+
type: 'json',
|
|
230
|
+
required: true,
|
|
231
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months }, muted?: boolean }'
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
output: {
|
|
235
|
+
schema: {
|
|
236
|
+
type: 'json'
|
|
237
|
+
},
|
|
238
|
+
render: (_args, value)=>{
|
|
239
|
+
const v = value;
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: `已加入「下一次出发」清单(共 ${v.total ?? '?'} 项):${v.path ?? ''}`
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async execute (args, _exec) {
|
|
249
|
+
const entry = args.entry ?? {};
|
|
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?: "行程起始(确认成行时挂时间线)" }'
|
|
280
|
+
}
|
|
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
|
+
} : {}
|
|
333
|
+
};
|
|
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) {
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
suggestion: null,
|
|
347
|
+
summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)`
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const { events: next } = ledger.appendUtilityEvent({
|
|
351
|
+
wish_id: match.wishId,
|
|
352
|
+
kind: 'recalled',
|
|
353
|
+
ts: now,
|
|
354
|
+
ctx: 'gotry_wish_pool_list.recall'
|
|
355
|
+
});
|
|
356
|
+
const utility = projectUtility(next)[match.wishId];
|
|
357
|
+
return {
|
|
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'}`
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
presentCall: (args)=>({
|
|
375
|
+
card: 'generic',
|
|
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
|
|
501
|
+
})
|
|
502
|
+
}));
|
|
503
|
+
registerGuarded(defineTool({
|
|
504
|
+
name: 'gotry_hotel_search',
|
|
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.',
|
|
506
|
+
parameters: {
|
|
507
|
+
query: {
|
|
508
|
+
type: 'json',
|
|
509
|
+
required: true,
|
|
510
|
+
description: '{ destination: "<目的地城市>", checkIn?: "YYYY-MM-DD 或自然表达(下周五/8.20)", checkOut?: 同上, occupancy?: { adults: 2 } }'
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
output: {
|
|
514
|
+
schema: {
|
|
515
|
+
type: 'json'
|
|
516
|
+
},
|
|
517
|
+
render: (_args, value)=>[
|
|
518
|
+
{
|
|
519
|
+
type: 'text',
|
|
520
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 400))
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
},
|
|
524
|
+
async execute (args, _exec) {
|
|
525
|
+
const q = unwrapQuery(args, 'destination');
|
|
526
|
+
if (!q.destination) throw new Error('gotry_hotel_search requires destination');
|
|
527
|
+
const started = Date.now();
|
|
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
|
+
};
|
|
541
|
+
const resp = await hbcliSearchHotels({
|
|
542
|
+
destination: q.destination,
|
|
543
|
+
checkIn: resolveDate(q.checkIn),
|
|
544
|
+
checkOut: resolveDate(q.checkOut),
|
|
545
|
+
adults: q.adults
|
|
546
|
+
}, {
|
|
547
|
+
hbcliBin: config.hbcliBin,
|
|
548
|
+
timeoutMs: config.timeoutMs,
|
|
549
|
+
fallbackPath
|
|
550
|
+
});
|
|
551
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
552
|
+
const isLive = resp.via === 'hbcli-realtime';
|
|
553
|
+
const evidence = isLive ? resp.evidence : '[静态包:估算]';
|
|
554
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `hotel_search:${resp.via}`).catch(()=>{});
|
|
555
|
+
const payload = {
|
|
556
|
+
ok: true,
|
|
557
|
+
hotels: resp.hotels ?? null,
|
|
558
|
+
evidence,
|
|
559
|
+
destination: q.destination,
|
|
560
|
+
via: resp.via,
|
|
561
|
+
latency_ms: Date.now() - started,
|
|
562
|
+
summary: resp.summary,
|
|
563
|
+
error: resp.error,
|
|
564
|
+
...dateNotes.length ? {
|
|
565
|
+
date_notes: dateNotes
|
|
566
|
+
} : {}
|
|
567
|
+
};
|
|
568
|
+
return JSON.parse(JSON.stringify(payload));
|
|
569
|
+
},
|
|
570
|
+
presentCall: (args)=>({
|
|
571
|
+
card: 'generic',
|
|
572
|
+
title: `酒店搜索:${String(args.query?.destination ?? '')}`,
|
|
573
|
+
kind: 'search',
|
|
574
|
+
rawInput: args.query
|
|
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
|
+
}
|
|
590
|
+
}));
|
|
591
|
+
registerGuarded(defineTool({
|
|
592
|
+
name: 'gotry_skeleton_check',
|
|
593
|
+
description: 'Check flight connectivity between two airports against the OpenFlights skeleton (free tier). ' + 'Three-valued: found = strong positive (airlines returned); hub-to-hub absence = downgrade signal, NEVER disproof ' + '(skeleton lags reality); outside hub set = no conclusion. Use BEFORE recommending a route.',
|
|
594
|
+
parameters: {
|
|
595
|
+
from: {
|
|
596
|
+
type: 'string',
|
|
597
|
+
required: true,
|
|
598
|
+
description: 'IATA, e.g. HKG'
|
|
599
|
+
},
|
|
600
|
+
to: {
|
|
601
|
+
type: 'string',
|
|
602
|
+
required: true,
|
|
603
|
+
description: 'IATA, e.g. HKT'
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
output: {
|
|
607
|
+
schema: {
|
|
608
|
+
type: 'json'
|
|
609
|
+
},
|
|
610
|
+
render: (_args, value)=>[
|
|
611
|
+
{
|
|
612
|
+
type: 'text',
|
|
613
|
+
text: String(value.evidence ?? JSON.stringify(value))
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
async execute (args, _exec) {
|
|
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
|
+
}));
|
|
624
|
+
},
|
|
625
|
+
presentCall: (args)=>({
|
|
626
|
+
card: 'generic',
|
|
627
|
+
title: `骨架校验:${args.from}-${args.to}`,
|
|
628
|
+
kind: 'execute',
|
|
629
|
+
rawInput: args
|
|
630
|
+
})
|
|
631
|
+
}));
|
|
632
|
+
registerGuarded(defineTool({
|
|
633
|
+
name: 'gotry_weather_check',
|
|
634
|
+
description: 'Check weather for a destination: forecast (≤16 days) or historical climate (seasonality baseline). ' + 'Free Open-Meteo API, no key required. Input: place name (Chinese ok) or lat/lng. ' + 'Returns daily temp range, precipitation probability, weather code — with evidence chain tagging ' + '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.',
|
|
635
|
+
parameters: {
|
|
636
|
+
query: {
|
|
637
|
+
type: 'json',
|
|
638
|
+
required: true,
|
|
639
|
+
description: '{ place: "<城市名>", month?: 8, mode?: "forecast"|"climate", days?: 7 }'
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
output: {
|
|
643
|
+
schema: {
|
|
644
|
+
type: 'json'
|
|
645
|
+
},
|
|
646
|
+
render: (_args, value)=>[
|
|
647
|
+
{
|
|
648
|
+
type: 'text',
|
|
649
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
},
|
|
653
|
+
async execute (args, _exec) {
|
|
654
|
+
const q = unwrapQuery(args, 'place');
|
|
655
|
+
const started = Date.now();
|
|
656
|
+
let lat = q.lat, lng = q.lng;
|
|
657
|
+
let placeLabel = q.place ?? `${q.lat},${q.lng}`;
|
|
658
|
+
if (lat === undefined || lng === undefined) {
|
|
659
|
+
if (!q.place) throw new Error('gotry_weather_check requires place name or lat/lng');
|
|
660
|
+
const geo = await geocodePlace(q.place);
|
|
661
|
+
if (!geo.ok || geo.results.length === 0) {
|
|
662
|
+
return JSON.parse(JSON.stringify({
|
|
663
|
+
ok: false,
|
|
664
|
+
summary: `地点「${q.place}」地理编码失败:${geo.error ?? '无结果'}`,
|
|
665
|
+
evidence: geo.evidence
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
const hit = geo.results[0];
|
|
669
|
+
lat = hit.latitude;
|
|
670
|
+
lng = hit.longitude;
|
|
671
|
+
placeLabel = `${hit.name}(${hit.admin1 ?? hit.country ?? ''})`;
|
|
672
|
+
}
|
|
673
|
+
const isClimate = q.mode === 'climate' || q.month !== undefined && q.mode !== 'forecast';
|
|
674
|
+
const r = isClimate ? await getClimate({
|
|
675
|
+
latitude: lat,
|
|
676
|
+
longitude: lng
|
|
677
|
+
}, q.month ?? new Date().getMonth() + 1) : await getForecast({
|
|
678
|
+
latitude: lat,
|
|
679
|
+
longitude: lng
|
|
680
|
+
}, {
|
|
681
|
+
days: q.days
|
|
682
|
+
});
|
|
683
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
684
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `weather:${r.via}`).catch(()=>{});
|
|
685
|
+
const dailyLines = (r.daily ?? []).slice(0, 7).map((d)=>`${d.date} ${d.tempMinC.toFixed(0)}–${d.tempMaxC.toFixed(0)}°C ${wmoLabel(d.weatherCode)}${d.precipProbMaxPct !== null ? ` 降水概率${d.precipProbMaxPct}%` : ''}`);
|
|
686
|
+
const summary = r.ok ? `${placeLabel}:${isClimate ? '历史气候' : `${q.days ?? 7} 天预报`}\n${dailyLines.join('\n')}\n${r.evidence}` : `${placeLabel}:天气查询失败(${r.error})${r.evidence}`;
|
|
687
|
+
return JSON.parse(JSON.stringify({
|
|
688
|
+
ok: r.ok,
|
|
689
|
+
place: placeLabel,
|
|
690
|
+
mode: isClimate ? 'climate' : 'forecast',
|
|
691
|
+
daily: r.daily,
|
|
692
|
+
evidence: r.evidence,
|
|
693
|
+
summary,
|
|
694
|
+
latency_ms: Date.now() - started
|
|
695
|
+
}));
|
|
696
|
+
},
|
|
697
|
+
presentCall: (args)=>({
|
|
698
|
+
card: 'generic',
|
|
699
|
+
title: `天气:${String(args.query?.place ?? '')}`,
|
|
700
|
+
kind: 'fetch',
|
|
701
|
+
rawInput: args.query
|
|
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
|
+
}
|
|
718
|
+
}));
|
|
719
|
+
registerGuarded(defineTool({
|
|
720
|
+
name: 'gotry_flight_verify',
|
|
721
|
+
description: 'Verify whether a flight callsign is currently observable on the OpenSky ADS-B network. ' + 'Free anonymous API (~400 credits/day, 4 req/s burst). Three-valued semantics: ' + 'observed = strong positive (the aircraft is currently being broadcast); ' + 'not_observed = no conclusion (ADS-B coverage is limited by geography/altitude — ' + 'a missing signal does NOT disprove the flight); ' + 'unavailable = API failure, gracefully degraded. ' + 'Use to ground "is this flight actually flying right now?" in real data, complementing ' + 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).',
|
|
722
|
+
parameters: {
|
|
723
|
+
query: {
|
|
724
|
+
type: 'json',
|
|
725
|
+
required: true,
|
|
726
|
+
description: '{ callsign: "EK329", airport?: "OMDB", timeoutMs?: 10000 }'
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
output: {
|
|
730
|
+
schema: {
|
|
731
|
+
type: 'json'
|
|
732
|
+
},
|
|
733
|
+
render: (_args, value)=>[
|
|
734
|
+
{
|
|
735
|
+
type: 'text',
|
|
736
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 500))
|
|
737
|
+
}
|
|
738
|
+
]
|
|
739
|
+
},
|
|
740
|
+
async execute (args, _exec) {
|
|
741
|
+
const q = unwrapQuery(args, 'callsign');
|
|
742
|
+
const started = Date.now();
|
|
743
|
+
if (!q.callsign) {
|
|
744
|
+
return JSON.parse(JSON.stringify({
|
|
745
|
+
verdict: 'unavailable',
|
|
746
|
+
evidence: '[校验不可用:无 callsign]',
|
|
747
|
+
summary: 'callsign 必填'
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
const r = await verifyFlight({
|
|
751
|
+
callsign: q.callsign,
|
|
752
|
+
airport: q.airport,
|
|
753
|
+
timeoutMs: q.timeoutMs
|
|
754
|
+
});
|
|
755
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
756
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `flight_verify:${r.via}`).catch(()=>{});
|
|
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}`;
|
|
758
|
+
return JSON.parse(JSON.stringify({
|
|
759
|
+
ok: true,
|
|
760
|
+
verdict: r.verdict,
|
|
761
|
+
callsign: r.callsign,
|
|
762
|
+
airport: r.airport,
|
|
763
|
+
sample_size: r.sampleSize,
|
|
764
|
+
hits: r.hits,
|
|
765
|
+
evidence: r.evidence,
|
|
766
|
+
summary,
|
|
767
|
+
latency_ms: Date.now() - started
|
|
768
|
+
}));
|
|
769
|
+
},
|
|
770
|
+
presentCall: (args)=>({
|
|
771
|
+
card: 'generic',
|
|
772
|
+
title: `飞行校验:${String(args.query?.callsign ?? '')}`,
|
|
773
|
+
kind: 'fetch',
|
|
774
|
+
rawInput: args.query
|
|
775
|
+
})
|
|
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
|
+
}));
|
|
904
|
+
registerGuarded(defineTool({
|
|
905
|
+
name: 'gotry_anything_search',
|
|
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).',
|
|
907
|
+
parameters: {
|
|
908
|
+
query: {
|
|
909
|
+
type: 'json',
|
|
910
|
+
required: true,
|
|
911
|
+
description: '{ keyword: "<搜索关键词>", contentType?: "city"|"hotel", parentDestinationId?: "?", timeoutMs?: 12000 }'
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
output: {
|
|
915
|
+
schema: {
|
|
916
|
+
type: 'json'
|
|
917
|
+
},
|
|
918
|
+
render: (_args, value)=>[
|
|
919
|
+
{
|
|
920
|
+
type: 'text',
|
|
921
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 800))
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
},
|
|
925
|
+
async execute (args, _exec) {
|
|
926
|
+
const q = unwrapQuery(args, 'keyword');
|
|
927
|
+
const started = Date.now();
|
|
928
|
+
if (!q.keyword) {
|
|
929
|
+
return JSON.parse(JSON.stringify({
|
|
930
|
+
ok: false,
|
|
931
|
+
verdict: 'error',
|
|
932
|
+
summary: 'keyword 必填',
|
|
933
|
+
evidence: '[hbcli-anything@error] empty'
|
|
934
|
+
}));
|
|
935
|
+
}
|
|
936
|
+
const r = await anythingSearch(q);
|
|
937
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
938
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(()=>{});
|
|
939
|
+
const top5 = (r.hits ?? []).slice(0, 5);
|
|
940
|
+
const summary = r.verdict === 'hit' ? `${q.keyword} → hit (${r.hits?.length ?? 0} 候选项)\n${top5.map((h, i)=>` ${i + 1}. [${h.type}] ${h.name}${h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : ''}`).join('\n')}\n${r.evidence}` : r.verdict === 'miss' ? `${q.keyword} → miss (酒店-be 一切正常但无候选)\n${r.evidence}` : `${q.keyword} → unavailable (${r.error})\n${r.evidence}`;
|
|
941
|
+
return JSON.parse(JSON.stringify({
|
|
942
|
+
ok: r.ok,
|
|
943
|
+
verdict: r.verdict,
|
|
944
|
+
keyword: q.keyword,
|
|
945
|
+
content_type: q.contentType ?? null,
|
|
946
|
+
total_candidates: r.totalCandidates,
|
|
947
|
+
hits: r.hits,
|
|
948
|
+
evidence: r.evidence,
|
|
949
|
+
summary,
|
|
950
|
+
latency_ms: Date.now() - started
|
|
951
|
+
}));
|
|
952
|
+
},
|
|
953
|
+
presentCall: (args)=>({
|
|
954
|
+
card: 'generic',
|
|
955
|
+
title: `Anything search:${String(args.query?.keyword ?? '')}`,
|
|
956
|
+
kind: 'search',
|
|
957
|
+
rawInput: args.query
|
|
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
|
+
}
|
|
973
|
+
}));
|
|
974
|
+
registerGuarded(defineTool({
|
|
975
|
+
name: 'gotry_web_search',
|
|
976
|
+
description: 'Read any public URL as markdown (Jina Reader, free, no key). ' + 'Use as the "last mile" web reader when hotel-be Anything or gotry tools lack the answer. ' + 'NOT a general-purpose search engine — only fetches a URL you already know. ' + 'Three-valued: ok / error(非法 URL/超时)/not-reachable(r.jina.ai 不可用).' + 'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。',
|
|
977
|
+
parameters: {
|
|
978
|
+
query: {
|
|
979
|
+
type: 'json',
|
|
980
|
+
required: true,
|
|
981
|
+
description: '{ url: "https://example.com", timeoutMs?: 20000 }'
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
output: {
|
|
985
|
+
schema: {
|
|
986
|
+
type: 'json'
|
|
987
|
+
},
|
|
988
|
+
render: (_args, value)=>[
|
|
989
|
+
{
|
|
990
|
+
type: 'text',
|
|
991
|
+
text: String(value.content?.slice(0, 800) ?? JSON.stringify(value).slice(0, 800))
|
|
992
|
+
}
|
|
993
|
+
]
|
|
994
|
+
},
|
|
995
|
+
async execute (args, _exec) {
|
|
996
|
+
const q = unwrapQuery(args, 'url');
|
|
997
|
+
const started = Date.now();
|
|
998
|
+
if (!q.url) {
|
|
999
|
+
return JSON.parse(JSON.stringify({
|
|
1000
|
+
ok: false,
|
|
1001
|
+
summary: 'url 必填',
|
|
1002
|
+
evidence: '[agent-reach:error] empty url'
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
1005
|
+
const r = await readUrl({
|
|
1006
|
+
url: q.url,
|
|
1007
|
+
timeoutMs: q.timeoutMs
|
|
1008
|
+
});
|
|
1009
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
1010
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(()=>{});
|
|
1011
|
+
const summary = r.ok ? `${q.url} → ${r.title ?? '(no title)'} (${r.latencyMs}ms)\n${r.evidence}\n---\n${r.content?.slice(0, 600) ?? ''}` : `${q.url} → unavailable (${r.error})\n${r.evidence}`;
|
|
1012
|
+
return JSON.parse(JSON.stringify({
|
|
1013
|
+
ok: r.ok,
|
|
1014
|
+
url: q.url,
|
|
1015
|
+
via: r.via,
|
|
1016
|
+
title: r.title,
|
|
1017
|
+
content: r.content,
|
|
1018
|
+
evidence: r.evidence,
|
|
1019
|
+
summary,
|
|
1020
|
+
latency_ms: Date.now() - started
|
|
1021
|
+
}));
|
|
1022
|
+
},
|
|
1023
|
+
presentCall: (args)=>({
|
|
1024
|
+
card: 'generic',
|
|
1025
|
+
title: `读网页:${String(args.query?.url ?? '')}`,
|
|
1026
|
+
kind: 'fetch',
|
|
1027
|
+
rawInput: args.query
|
|
1028
|
+
})
|
|
1029
|
+
}));
|
|
1030
|
+
registerGuarded(defineTool({
|
|
1031
|
+
name: 'gotry_video_subtitle',
|
|
1032
|
+
description: 'Extract subtitles from a YouTube/Bilibili video (yt-dlp, optional tool). ' + 'If yt-dlp is installed on this machine, returns the subtitle text (vtt, zh-Hans/zh/en preference). ' + 'If NOT installed, degrades gracefully with install instructions — never blocks. ' + 'Evidence chain: [agent-reach:yt-dlp@ts] / [@not-installed@ts].',
|
|
1033
|
+
parameters: {
|
|
1034
|
+
query: {
|
|
1035
|
+
type: 'json',
|
|
1036
|
+
required: true,
|
|
1037
|
+
description: '{ url: "https://www.youtube.com/watch?v=...", lang?: "zh-Hans,zh,en" }'
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
output: {
|
|
1041
|
+
schema: {
|
|
1042
|
+
type: 'json'
|
|
1043
|
+
},
|
|
1044
|
+
render: (_args, value)=>[
|
|
1045
|
+
{
|
|
1046
|
+
type: 'text',
|
|
1047
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
},
|
|
1051
|
+
async execute (args, _exec) {
|
|
1052
|
+
const q = unwrapQuery(args, 'url');
|
|
1053
|
+
if (!q.url) {
|
|
1054
|
+
return JSON.parse(JSON.stringify({
|
|
1055
|
+
ok: false,
|
|
1056
|
+
summary: 'url 必填'
|
|
1057
|
+
}));
|
|
1058
|
+
}
|
|
1059
|
+
const r = await videoSubtitle({
|
|
1060
|
+
url: q.url,
|
|
1061
|
+
lang: q.lang
|
|
1062
|
+
});
|
|
1063
|
+
const summary = r.verdict === 'found' ? `${q.url} 字幕提取成功 (${r.latencyMs}ms)\n${r.evidence}\n---\n${(r.subtitles ?? '').slice(0, 800)}` : r.verdict === 'not-installed' ? `yt-dlp 未安装。${r.stderr}\n${r.evidence}` : `${q.url} 字幕提取失败(${r.verdict})\n${r.evidence}`;
|
|
1064
|
+
return JSON.parse(JSON.stringify({
|
|
1065
|
+
ok: r.ok,
|
|
1066
|
+
verdict: r.verdict,
|
|
1067
|
+
url: q.url,
|
|
1068
|
+
subtitles: r.subtitles?.slice(0, 4000),
|
|
1069
|
+
evidence: r.evidence,
|
|
1070
|
+
summary,
|
|
1071
|
+
latency_ms: r.latencyMs
|
|
1072
|
+
}));
|
|
1073
|
+
},
|
|
1074
|
+
presentCall: (args)=>({
|
|
1075
|
+
card: 'generic',
|
|
1076
|
+
title: `视频字幕:${String(args.query?.url ?? '')}`,
|
|
1077
|
+
kind: 'fetch',
|
|
1078
|
+
rawInput: args.query
|
|
1079
|
+
})
|
|
1080
|
+
}));
|
|
1081
|
+
registerGuarded(defineTool({
|
|
1082
|
+
name: 'gotry_github_search',
|
|
1083
|
+
description: 'Search GitHub repositories (gh CLI, optional tool). ' + 'If gh is installed and authenticated, returns repos with name/description/stars/url. ' + 'If NOT installed, degrades with install instructions — never blocks. ' + 'Evidence chain: [agent-reach:gh@ts] / [@not-installed@ts].',
|
|
1084
|
+
parameters: {
|
|
1085
|
+
query: {
|
|
1086
|
+
type: 'json',
|
|
1087
|
+
required: true,
|
|
1088
|
+
description: '{ query: "agent-reach", limit?: 5 }'
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
output: {
|
|
1092
|
+
schema: {
|
|
1093
|
+
type: 'json'
|
|
1094
|
+
},
|
|
1095
|
+
render: (_args, value)=>[
|
|
1096
|
+
{
|
|
1097
|
+
type: 'text',
|
|
1098
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
1099
|
+
}
|
|
1100
|
+
]
|
|
1101
|
+
},
|
|
1102
|
+
async execute (args, _exec) {
|
|
1103
|
+
const q = unwrapQuery(args, 'query');
|
|
1104
|
+
if (!q.query) {
|
|
1105
|
+
return JSON.parse(JSON.stringify({
|
|
1106
|
+
ok: false,
|
|
1107
|
+
summary: 'query 必填'
|
|
1108
|
+
}));
|
|
1109
|
+
}
|
|
1110
|
+
const r = await githubSearch({
|
|
1111
|
+
query: q.query,
|
|
1112
|
+
limit: q.limit
|
|
1113
|
+
});
|
|
1114
|
+
const summary = r.verdict === 'found' ? `${q.query} → ${r.repos?.length ?? 0} repos\n${(r.repos ?? []).map((x, i)=>` ${i + 1}. ${x.name} ★${x.stars ?? '?'} — ${(x.description ?? '').slice(0, 60)}`).join('\n')}\n${r.evidence}` : r.verdict === 'not-installed' ? `gh 未安装。${r.stderr}\n${r.evidence}` : `${q.query} 搜索失败(${r.verdict})\n${r.evidence}`;
|
|
1115
|
+
return JSON.parse(JSON.stringify({
|
|
1116
|
+
ok: r.ok,
|
|
1117
|
+
verdict: r.verdict,
|
|
1118
|
+
query: q.query,
|
|
1119
|
+
repos: r.repos,
|
|
1120
|
+
evidence: r.evidence,
|
|
1121
|
+
summary,
|
|
1122
|
+
latency_ms: r.latencyMs
|
|
1123
|
+
}));
|
|
1124
|
+
},
|
|
1125
|
+
presentCall: (args)=>({
|
|
1126
|
+
card: 'generic',
|
|
1127
|
+
title: `GitHub 搜索:${String(args.query?.query ?? '')}`,
|
|
1128
|
+
kind: 'search',
|
|
1129
|
+
rawInput: args.query
|
|
1130
|
+
})
|
|
1131
|
+
}));
|
|
1132
|
+
registerGuarded(defineTool({
|
|
1133
|
+
name: 'gotry_agent_reach',
|
|
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].',
|
|
1135
|
+
parameters: {
|
|
1136
|
+
query: {
|
|
1137
|
+
type: 'json',
|
|
1138
|
+
required: true,
|
|
1139
|
+
description: '{ action: "status" } 或 { action: "reach", channel: "<上游渠道名,如 web/v2ex/xueqiu>", method: "<上游方法名,如 read/get_hot_topics/get_stock_quote>", args?: "<空格分隔参数>" }'
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
output: {
|
|
1143
|
+
schema: {
|
|
1144
|
+
type: 'json'
|
|
1145
|
+
},
|
|
1146
|
+
render: (_args, value)=>[
|
|
1147
|
+
{
|
|
1148
|
+
type: 'text',
|
|
1149
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 800))
|
|
1150
|
+
}
|
|
1151
|
+
]
|
|
1152
|
+
},
|
|
1153
|
+
async execute (args, _exec) {
|
|
1154
|
+
const q = unwrapQuery(args, 'channel');
|
|
1155
|
+
const started = Date.now();
|
|
1156
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
1157
|
+
if (q.action === 'status' || !q.action && !q.channel) {
|
|
1158
|
+
const st = await reachStatus(q.timeoutMs);
|
|
1159
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(()=>{});
|
|
1160
|
+
const summary = st.via === 'agent-reach-cli' ? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}` : `Agent Reach 未装:\n${st.output}\n${st.evidence}`;
|
|
1161
|
+
return JSON.parse(JSON.stringify({
|
|
1162
|
+
ok: st.ok,
|
|
1163
|
+
via: st.via,
|
|
1164
|
+
output: st.output,
|
|
1165
|
+
evidence: st.evidence,
|
|
1166
|
+
summary,
|
|
1167
|
+
latency_ms: Date.now() - started
|
|
1168
|
+
}));
|
|
1169
|
+
}
|
|
1170
|
+
if (!q.channel || !q.method) {
|
|
1171
|
+
return JSON.parse(JSON.stringify({
|
|
1172
|
+
ok: false,
|
|
1173
|
+
summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表'
|
|
1174
|
+
}));
|
|
1175
|
+
}
|
|
1176
|
+
const r = await reach({
|
|
1177
|
+
channel: q.channel,
|
|
1178
|
+
method: q.method,
|
|
1179
|
+
args: q.args,
|
|
1180
|
+
timeoutMs: q.timeoutMs
|
|
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);
|
|
1199
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${q.channel}.${q.method}:${r.verdict}`).catch(()=>{});
|
|
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}`;
|
|
1201
|
+
return JSON.parse(JSON.stringify({
|
|
1202
|
+
ok: r.ok,
|
|
1203
|
+
channel: r.channel,
|
|
1204
|
+
method: q.method,
|
|
1205
|
+
verdict: r.verdict,
|
|
1206
|
+
data: typeof r.data === 'string' ? r.data.slice(0, 4000) : r.data,
|
|
1207
|
+
inventory: r.inventory,
|
|
1208
|
+
setup: r.setup,
|
|
1209
|
+
evidence: r.evidence,
|
|
1210
|
+
summary,
|
|
1211
|
+
latency_ms: Date.now() - started
|
|
1212
|
+
}));
|
|
1213
|
+
},
|
|
1214
|
+
presentCall: (args)=>({
|
|
1215
|
+
card: 'generic',
|
|
1216
|
+
title: `Agent Reach:${String(args.query?.channel ?? '')}.${String(args.query?.method ?? 'status')}`,
|
|
1217
|
+
kind: 'fetch',
|
|
1218
|
+
rawInput: args.query
|
|
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
|
+
}
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/index.ts
|