@danceiny/gotry 0.0.1-rc.8 → 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 +6 -4
- package/cordis.gotry-patch.yml +7 -0
- package/dist/capabilities/flyai.js +131 -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 +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/async-collect.js +26 -7
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/ledger-tests.js +344 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +6 -20
- package/dist/scripts/nudge-digest.js +4 -14
- 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/smoke.js +41 -1
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +234 -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/index.js +346 -117
- package/dist/src/loop.js +52 -15
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/state-ledger.js +848 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +3 -1
- package/package.json +1 -1
- package/ts/package.json +3 -0
- package/ts/src/index.ts +216 -83
- package/ts/src/loop.ts +57 -17
- package/ts/src/unified.ts +6 -1
package/dist/src/index.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
2
|
import z from '@deepseek-ai/schemastery';
|
|
5
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
6
|
-
import { ensureStateDir,
|
|
4
|
+
import { ensureStateDir, recordLatency } from './bridge.js';
|
|
7
5
|
import { segmentsFromCandidate, solveChoiceSegment } from './unified.js';
|
|
8
6
|
import { checkConnectivity } from '../scripts/skeleton-check.js';
|
|
9
7
|
import { parseCandidate, parseRequest } from './model.js';
|
|
10
8
|
import { searchHotels as hbcliSearchHotels } from '../capabilities/hbcli.js';
|
|
11
9
|
import { installProcessGuards, guardToolExecute } from '../capabilities/incident-log.js';
|
|
12
10
|
import { interpretArgs } from './tool-packet.js';
|
|
13
|
-
import {
|
|
11
|
+
import { projectUtility } from './memory-utility.js';
|
|
14
12
|
import { pickNudgeWish } from './wish-pool.js';
|
|
15
|
-
import {
|
|
13
|
+
import { resolveTimelineDate } from './travel-timeline.js';
|
|
14
|
+
import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, readTripsWithFallback, readWishPoolWithFallback } from './state-ledger.js';
|
|
16
15
|
import { buildTimeAnchor } from './time-anchor.js';
|
|
17
16
|
import { resolveSlotDate } from './slot-spec.js';
|
|
18
17
|
import { geocodePlace, getForecast, getClimate, wmoLabel } from '../capabilities/weather.js';
|
|
@@ -20,6 +19,8 @@ import { verifyFlight } from '../capabilities/opensky.js';
|
|
|
20
19
|
import { anythingSearch } from '../capabilities/anything.js';
|
|
21
20
|
import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
22
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';
|
|
23
24
|
export const name = 'gotry-tools';
|
|
24
25
|
export const inject = [
|
|
25
26
|
'tools',
|
|
@@ -32,22 +33,45 @@ export const Config = z.object({
|
|
|
32
33
|
});
|
|
33
34
|
const unwrapQuery = interpretArgs;
|
|
34
35
|
function renderMotivationBrief(stateRoot) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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('、')}——约束进结构与排序建议,不硬过滤;引用时带当初的原话依据`);
|
|
50
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);
|
|
51
75
|
}
|
|
52
76
|
export function apply(ctx, config) {
|
|
53
77
|
const sp = ctx['systemPrompt'];
|
|
@@ -169,41 +193,25 @@ export function apply(ctx, config) {
|
|
|
169
193
|
if (!incoming.evidence?.length) {
|
|
170
194
|
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)');
|
|
171
195
|
}
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
let existing = null;
|
|
175
|
-
try {
|
|
176
|
-
existing = await readJson(path, null);
|
|
177
|
-
} catch {}
|
|
178
|
-
const merged = mergeProfile(existing, {
|
|
196
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
197
|
+
const res = ledger.appendMotivationPatch({
|
|
179
198
|
weights: incoming.weights,
|
|
180
199
|
evidence: incoming.evidence,
|
|
181
200
|
hard: incoming.hard
|
|
182
201
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
...existing ?? {},
|
|
186
|
-
updated_at: new Date().toISOString()
|
|
187
|
-
}));
|
|
188
|
-
return {
|
|
189
|
-
ok: true,
|
|
190
|
-
saved: false,
|
|
191
|
-
path,
|
|
192
|
-
profile: currentJson,
|
|
193
|
-
summary: '无新内容(幂等跳过)'
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const saved = JSON.parse(JSON.stringify({
|
|
197
|
-
...merged,
|
|
198
|
-
updated_at: new Date().toISOString()
|
|
199
|
-
}));
|
|
200
|
-
await writeJson(path, saved);
|
|
201
|
-
return {
|
|
202
|
+
const profileJson = JSON.parse(JSON.stringify(res.profile));
|
|
203
|
+
return res.saved ? {
|
|
202
204
|
ok: true,
|
|
203
205
|
saved: true,
|
|
204
|
-
path,
|
|
205
|
-
profile:
|
|
206
|
-
summary: '
|
|
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: '无新内容(幂等跳过)'
|
|
207
215
|
};
|
|
208
216
|
},
|
|
209
217
|
presentCall: (args)=>({
|
|
@@ -239,47 +247,19 @@ export function apply(ctx, config) {
|
|
|
239
247
|
},
|
|
240
248
|
async execute (args, _exec) {
|
|
241
249
|
const entry = args.entry ?? {};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (existing >= 0) {
|
|
250
|
-
const prev = pool[existing] ?? {};
|
|
251
|
-
pool[existing] = {
|
|
252
|
-
...prev,
|
|
253
|
-
wish_id: prev['wish_id'] ?? `w${Date.now().toString(36)}`,
|
|
254
|
-
reason: entry.reason ?? prev['reason'],
|
|
255
|
-
conditions: entry.conditions,
|
|
256
|
-
...entry.muted !== undefined ? {
|
|
257
|
-
muted: entry.muted
|
|
258
|
-
} : {}
|
|
259
|
-
};
|
|
260
|
-
await writeJson(path, pool);
|
|
261
|
-
return {
|
|
262
|
-
ok: true,
|
|
263
|
-
added: false,
|
|
264
|
-
wish_id: String(pool[existing]?.['wish_id']),
|
|
265
|
-
total: pool.length,
|
|
266
|
-
path
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
const created = {
|
|
270
|
-
wish_id: `w${Date.now().toString(36)}`,
|
|
271
|
-
reason: '',
|
|
272
|
-
...entry,
|
|
273
|
-
added_at: new Date().toISOString()
|
|
274
|
-
};
|
|
275
|
-
pool.push(created);
|
|
276
|
-
await writeJson(path, pool);
|
|
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
|
+
});
|
|
277
257
|
return {
|
|
278
258
|
ok: true,
|
|
279
|
-
added:
|
|
280
|
-
wish_id:
|
|
281
|
-
total:
|
|
282
|
-
path
|
|
259
|
+
added: r.added,
|
|
260
|
+
wish_id: r.wish_id,
|
|
261
|
+
total: r.total,
|
|
262
|
+
path: ledger.dbPath
|
|
283
263
|
};
|
|
284
264
|
},
|
|
285
265
|
presentCall: (args)=>({
|
|
@@ -296,7 +276,7 @@ export function apply(ctx, config) {
|
|
|
296
276
|
query: {
|
|
297
277
|
type: 'json',
|
|
298
278
|
required: true,
|
|
299
|
-
description: '{ action?: "recall"|"confirm-outcome", days
|
|
279
|
+
description: '{ action?: "recall"|"confirm-outcome", days?, budgetCny?, month?, wishId?, attribution?: "helpful"|"harmful"|"neutral", detail?, tripStart?: "行程起始(确认成行时挂时间线)" }'
|
|
300
280
|
}
|
|
301
281
|
},
|
|
302
282
|
output: {
|
|
@@ -312,20 +292,7 @@ export function apply(ctx, config) {
|
|
|
312
292
|
},
|
|
313
293
|
async execute (args, _exec) {
|
|
314
294
|
const q = unwrapQuery(args, 'action');
|
|
315
|
-
const
|
|
316
|
-
const pool = await readJson(join(dir, 'wish-pool.json'), []);
|
|
317
|
-
const sidecarPath = join(dir, 'memory-utility.jsonl');
|
|
318
|
-
const loadSidecar = async ()=>{
|
|
319
|
-
try {
|
|
320
|
-
const raw = await readFile(sidecarPath, 'utf-8');
|
|
321
|
-
return raw.split('\n').filter(Boolean).map((l)=>JSON.parse(l));
|
|
322
|
-
} catch {
|
|
323
|
-
return [];
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
const saveSidecar = async (events)=>{
|
|
327
|
-
await writeFile(sidecarPath, events.map((e)=>JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
328
|
-
};
|
|
295
|
+
const ledger = ensureLedger(config.stateRoot);
|
|
329
296
|
const now = new Date().toISOString();
|
|
330
297
|
if (q.action === 'confirm-outcome') {
|
|
331
298
|
if (!q.wishId || !q.attribution) {
|
|
@@ -334,24 +301,39 @@ export function apply(ctx, config) {
|
|
|
334
301
|
summary: 'confirm-outcome 需要 wishId + attribution(helpful|harmful|neutral)'
|
|
335
302
|
};
|
|
336
303
|
}
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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,
|
|
343
322
|
detail: q.detail,
|
|
344
|
-
|
|
323
|
+
trip: tripInput
|
|
345
324
|
});
|
|
346
|
-
if (appended) await saveSidecar(next);
|
|
347
325
|
return {
|
|
348
326
|
ok: true,
|
|
349
|
-
recorded:
|
|
327
|
+
recorded: r.recorded,
|
|
350
328
|
wish_id: q.wishId,
|
|
351
|
-
status: q.attribution
|
|
329
|
+
status: q.attribution,
|
|
330
|
+
...r.trip ? {
|
|
331
|
+
trip: r.trip
|
|
332
|
+
} : {}
|
|
352
333
|
};
|
|
353
334
|
}
|
|
354
|
-
const
|
|
335
|
+
const pool = ledger.readWishPool();
|
|
336
|
+
const candidates = pool.filter((e)=>!e.muted && typeof e.wish_id === 'string');
|
|
355
337
|
const month = q.month ?? new Date().getMonth() + 1;
|
|
356
338
|
const match = pickNudgeWish(candidates, {
|
|
357
339
|
days: q.days,
|
|
@@ -365,14 +347,12 @@ export function apply(ctx, config) {
|
|
|
365
347
|
summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)`
|
|
366
348
|
};
|
|
367
349
|
}
|
|
368
|
-
const events =
|
|
369
|
-
const { events: next, appended } = appendEvent(events, {
|
|
350
|
+
const { events: next } = ledger.appendUtilityEvent({
|
|
370
351
|
wish_id: match.wishId,
|
|
371
352
|
kind: 'recalled',
|
|
372
353
|
ts: now,
|
|
373
354
|
ctx: 'gotry_wish_pool_list.recall'
|
|
374
355
|
});
|
|
375
|
-
if (appended) await saveSidecar(next);
|
|
376
356
|
const utility = projectUtility(next)[match.wishId];
|
|
377
357
|
return {
|
|
378
358
|
ok: true,
|
|
@@ -398,6 +378,128 @@ export function apply(ctx, config) {
|
|
|
398
378
|
rawInput: args.query
|
|
399
379
|
})
|
|
400
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
|
+
}));
|
|
401
503
|
registerGuarded(defineTool({
|
|
402
504
|
name: 'gotry_hotel_search',
|
|
403
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.',
|
|
@@ -672,6 +774,133 @@ export function apply(ctx, config) {
|
|
|
672
774
|
rawInput: args.query
|
|
673
775
|
})
|
|
674
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
|
+
}));
|
|
675
904
|
registerGuarded(defineTool({
|
|
676
905
|
name: 'gotry_anything_search',
|
|
677
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).',
|
package/dist/src/loop.js
CHANGED
|
@@ -264,36 +264,73 @@ export async function runTurn(state, userMsg, llm, history = [], solve = null) {
|
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
266
|
import { join } from 'node:path';
|
|
267
|
-
import { mkdir } from 'node:fs/promises';
|
|
268
|
-
import {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
import { mkdir, rename, writeFile, readFile } from 'node:fs/promises';
|
|
268
|
+
import { ensureLedger, openLedgerIfExists } from './state-ledger.js';
|
|
269
|
+
async function asyncDir(stateRoot) {
|
|
270
|
+
const root = stateRoot === '.' ? process.cwd() : stateRoot;
|
|
271
|
+
const dir = join(root, 'gotry-state', 'async');
|
|
272
|
+
await mkdir(dir, {
|
|
272
273
|
recursive: true
|
|
273
274
|
});
|
|
274
|
-
return
|
|
275
|
+
return dir;
|
|
275
276
|
}
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
await writeFile(
|
|
277
|
+
async function atomicWrite(path, text) {
|
|
278
|
+
const tmp = `${path}.tmp`;
|
|
279
|
+
await writeFile(tmp, text, 'utf-8');
|
|
280
|
+
await rename(tmp, path);
|
|
281
|
+
}
|
|
282
|
+
export async function persistAsyncTicket(ticket, state, stateRoot = '.') {
|
|
283
|
+
const ledger = ensureLedger(stateRoot);
|
|
284
|
+
ledger.createWorkflowRun({
|
|
285
|
+
id: ticket.id,
|
|
286
|
+
goal: ticket.objective,
|
|
287
|
+
ticket,
|
|
288
|
+
state
|
|
289
|
+
});
|
|
290
|
+
const dir = await asyncDir(stateRoot);
|
|
291
|
+
const p = join(dir, `${ticket.id}.json`);
|
|
292
|
+
await atomicWrite(p, JSON.stringify({
|
|
279
293
|
ticket,
|
|
280
294
|
state
|
|
281
|
-
}, null, 2)
|
|
295
|
+
}, null, 2));
|
|
282
296
|
return p;
|
|
283
297
|
}
|
|
284
|
-
export async function loadAsyncTicket(ticketId) {
|
|
298
|
+
export async function loadAsyncTicket(ticketId, stateRoot = '.') {
|
|
299
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
300
|
+
const run = ledger?.getWorkflowRun(ticketId);
|
|
301
|
+
if (run) {
|
|
302
|
+
return {
|
|
303
|
+
ticket: JSON.parse(run.ticket_json),
|
|
304
|
+
state: JSON.parse(run.state_json)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
285
307
|
try {
|
|
286
|
-
const p = await
|
|
308
|
+
const p = join(await asyncDir(stateRoot), `${ticketId}.json`);
|
|
287
309
|
return JSON.parse(await readFile(p, 'utf-8'));
|
|
288
310
|
} catch {
|
|
289
311
|
return null;
|
|
290
312
|
}
|
|
291
313
|
}
|
|
292
|
-
export async function settleAsyncTicket(ticketId, reply) {
|
|
293
|
-
|
|
294
|
-
await
|
|
314
|
+
export async function settleAsyncTicket(ticketId, reply, stateRoot = '.') {
|
|
315
|
+
openLedgerIfExists(stateRoot)?.settleWorkflowRun(ticketId, reply);
|
|
316
|
+
const dir = await asyncDir(stateRoot);
|
|
317
|
+
const p = join(dir, `${ticketId}.deliverable.md`);
|
|
318
|
+
await atomicWrite(p, reply);
|
|
295
319
|
return p;
|
|
296
320
|
}
|
|
321
|
+
export function makeJournaledSolvePort(ledger, runId, solve, opts) {
|
|
322
|
+
return async (spec)=>{
|
|
323
|
+
const step = ledger.getWorkflowStep(runId, 'solve');
|
|
324
|
+
if (step?.status === 'done' && step.result) {
|
|
325
|
+
return JSON.parse(step.result);
|
|
326
|
+
}
|
|
327
|
+
ledger.markStepIntent(runId, 'solve');
|
|
328
|
+
opts?.onRealSolve?.();
|
|
329
|
+
const r = await solve(spec);
|
|
330
|
+
ledger.markStepDone(runId, 'solve', r);
|
|
331
|
+
return r;
|
|
332
|
+
};
|
|
333
|
+
}
|
|
297
334
|
export function probePoi(msg) {
|
|
298
335
|
const trimmed = msg.trim();
|
|
299
336
|
if (!trimmed) return null;
|