@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19
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 +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +236 -3
- package/bin/gotry-inner.js +66 -20
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +87 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +121 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +735 -149
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/package.json +1 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
package/dist/src/index.js
CHANGED
|
@@ -14,19 +14,19 @@ import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, r
|
|
|
14
14
|
import { buildTimeAnchor } from './time-anchor.js';
|
|
15
15
|
import { resolveSlotDate } from './slot-spec.js';
|
|
16
16
|
import { wmoLabel } from '../capabilities/weather.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.js';
|
|
20
|
-
import { sessionLogin } from '../capabilities/session-login.js';
|
|
17
|
+
import { reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
18
|
+
import { runDoctorChecks, renderDoctorReportMd } from '../capabilities/doctor.js';
|
|
21
19
|
import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.js';
|
|
22
20
|
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.js';
|
|
23
21
|
import { installModelOverride } from '../capabilities/model-override.js';
|
|
24
22
|
import { listArtifacts, readArtifact } from '../capabilities/artifacts.js';
|
|
25
23
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.js';
|
|
26
24
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.js';
|
|
27
|
-
import { factsFromFlyai, factsFromSession } from './bookable-facts.js';
|
|
25
|
+
import { factsFromFlyai, factsFromHotel, factsFromSession } from './bookable-facts.js';
|
|
28
26
|
import { gateArtifact } from './artifact-gate.js';
|
|
29
|
-
import {
|
|
27
|
+
import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.js';
|
|
28
|
+
import { noteChannelVerdict, recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
|
|
29
|
+
import { routingAdvice, renderRoutingCard, toolRoutingHeadline } from '../capabilities/channel-registry.js';
|
|
30
30
|
import { registerBenchmarkEnvironmentBridge } from './benchmark-environment-bridge.js';
|
|
31
31
|
import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.js';
|
|
32
32
|
import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.js';
|
|
@@ -95,9 +95,39 @@ function readTimelineTrips(stateRoot) {
|
|
|
95
95
|
start: t.start
|
|
96
96
|
})).sort((a, b)=>a.start < b.start ? 1 : -1).slice(0, 3);
|
|
97
97
|
}
|
|
98
|
+
function benchmarkPinnedPolicy() {
|
|
99
|
+
const parse = (name, fallback)=>{
|
|
100
|
+
const raw = process.env[name];
|
|
101
|
+
if (raw === undefined || raw === '') return fallback;
|
|
102
|
+
const value = Number.parseInt(raw, 10);
|
|
103
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
104
|
+
throw new Error(`invalid ${name}: ${JSON.stringify(raw)} (expected a positive safe integer)`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
};
|
|
108
|
+
const softMs = parse('GOTRY_BENCHMARK_SOFT_MS', 60_000);
|
|
109
|
+
const hardMs = parse('GOTRY_BENCHMARK_HARD_MS', 120_000);
|
|
110
|
+
if (hardMs <= softMs) throw new Error(`GOTRY_BENCHMARK_HARD_MS (${hardMs}) must exceed GOTRY_BENCHMARK_SOFT_MS (${softMs})`);
|
|
111
|
+
return {
|
|
112
|
+
softMs,
|
|
113
|
+
hardMs,
|
|
114
|
+
exit: 'converge'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
98
117
|
export function apply(ctx, config) {
|
|
99
|
-
installToolBudget(ctx);
|
|
100
118
|
const rawBenchmarkEnvironmentConfigPath = config.benchmarkEnvironmentConfigPath ?? '';
|
|
119
|
+
const handoffChild = process.env.GOTRY_HANDOFF_CHILD === '1';
|
|
120
|
+
installTurnDeadline(ctx, handoffChild ? {
|
|
121
|
+
fixedPolicy: {
|
|
122
|
+
softMs: 300_000,
|
|
123
|
+
hardMs: 900_000,
|
|
124
|
+
exit: 'converge'
|
|
125
|
+
}
|
|
126
|
+
} : rawBenchmarkEnvironmentConfigPath.trim() ? {
|
|
127
|
+
fixedPolicy: benchmarkPinnedPolicy()
|
|
128
|
+
} : {
|
|
129
|
+
stateRoot: process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.'
|
|
130
|
+
});
|
|
101
131
|
if (rawBenchmarkEnvironmentConfigPath.trim()) {
|
|
102
132
|
installModelOverride(ctx);
|
|
103
133
|
const getService = ctx.get;
|
|
@@ -124,6 +154,16 @@ export function apply(ctx, config) {
|
|
|
124
154
|
});
|
|
125
155
|
sp?.variable?.('time_anchor_card', ()=>buildTimeAnchor(new Date()).card);
|
|
126
156
|
sp?.variable?.('motivation_brief', ()=>renderMotivationBrief(config.stateRoot ?? '.'));
|
|
157
|
+
sp?.variable?.('channel_routing_card', ()=>renderRoutingCard());
|
|
158
|
+
const noteChannel = async (channel, verdict)=>{
|
|
159
|
+
const ev = noteChannelVerdict(channel, verdict);
|
|
160
|
+
if (ev) await recordChannelEvent(config.stateRoot ?? '.', ev);
|
|
161
|
+
};
|
|
162
|
+
const routingField = (intent, excludeChannel)=>({
|
|
163
|
+
routing: routingAdvice(intent, {
|
|
164
|
+
excludeChannel
|
|
165
|
+
})
|
|
166
|
+
});
|
|
127
167
|
installProcessGuards(config.stateRoot ?? '.', {
|
|
128
168
|
uncaughtException: 'gotry-tools',
|
|
129
169
|
unhandledRejection: 'gotry-tools'
|
|
@@ -145,14 +185,35 @@ export function apply(ctx, config) {
|
|
|
145
185
|
}
|
|
146
186
|
ctx.tools.register(t);
|
|
147
187
|
};
|
|
188
|
+
const routed = (tool, desc)=>{
|
|
189
|
+
const head = toolRoutingHeadline(tool);
|
|
190
|
+
return head ? `${head}\n${desc}` : desc;
|
|
191
|
+
};
|
|
148
192
|
registerGuarded(defineTool({
|
|
149
193
|
name: 'gotry_feasibility_check',
|
|
150
194
|
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.',
|
|
151
195
|
parameters: {
|
|
152
196
|
payload: {
|
|
153
|
-
type: '
|
|
197
|
+
type: 'object',
|
|
198
|
+
additionalProperties: false,
|
|
154
199
|
required: true,
|
|
155
|
-
|
|
200
|
+
properties: {
|
|
201
|
+
request: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
additionalProperties: true,
|
|
204
|
+
required: true,
|
|
205
|
+
description: '引擎请求:{ motivation weights, hard constraints, window, budget, home hubs }'
|
|
206
|
+
},
|
|
207
|
+
candidates: {
|
|
208
|
+
type: 'array',
|
|
209
|
+
items: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
additionalProperties: true
|
|
212
|
+
},
|
|
213
|
+
required: true,
|
|
214
|
+
description: '候选列表:[{ id, label, services, transfers, stay, minDays }]'
|
|
215
|
+
}
|
|
216
|
+
}
|
|
156
217
|
}
|
|
157
218
|
},
|
|
158
219
|
output: {
|
|
@@ -213,9 +274,29 @@ export function apply(ctx, config) {
|
|
|
213
274
|
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).',
|
|
214
275
|
parameters: {
|
|
215
276
|
profile: {
|
|
216
|
-
type: '
|
|
277
|
+
type: 'object',
|
|
278
|
+
additionalProperties: false,
|
|
217
279
|
required: true,
|
|
218
|
-
|
|
280
|
+
properties: {
|
|
281
|
+
weights: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
additionalProperties: true,
|
|
284
|
+
description: '动机权重增量,如 { escape_rest: 0.7 }(可选,权重变更须伴新证据)'
|
|
285
|
+
},
|
|
286
|
+
evidence: {
|
|
287
|
+
type: 'array',
|
|
288
|
+
items: {
|
|
289
|
+
type: 'string'
|
|
290
|
+
},
|
|
291
|
+
required: true,
|
|
292
|
+
description: '本轮新事实的用户原话数组(P0:必带)'
|
|
293
|
+
},
|
|
294
|
+
hard: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
additionalProperties: true,
|
|
297
|
+
description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
219
300
|
}
|
|
220
301
|
},
|
|
221
302
|
output: {
|
|
@@ -269,7 +350,7 @@ export function apply(ctx, config) {
|
|
|
269
350
|
entry: {
|
|
270
351
|
type: 'json',
|
|
271
352
|
required: true,
|
|
272
|
-
description: '{ name, reason?, conditions: { days, budget_cny, best_months }, muted?: boolean }'
|
|
353
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months, channels? }, muted?: boolean }'
|
|
273
354
|
}
|
|
274
355
|
},
|
|
275
356
|
output: {
|
|
@@ -312,12 +393,44 @@ export function apply(ctx, config) {
|
|
|
312
393
|
}));
|
|
313
394
|
registerGuarded(defineTool({
|
|
314
395
|
name: 'gotry_wish_pool_list',
|
|
315
|
-
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.',
|
|
396
|
+
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. ' + 'A wish whose conditions.channels names a channel that is currently down (out-of-band health facts) is vetoed for this recall — 不推当下走不通的憧憬. ' + '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.',
|
|
316
397
|
parameters: {
|
|
317
|
-
|
|
318
|
-
type: '
|
|
319
|
-
|
|
320
|
-
|
|
398
|
+
action: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'recall(默认,召回下一次出发建议)或 confirm-outcome(确认某条建议成/不成)'
|
|
401
|
+
},
|
|
402
|
+
days: {
|
|
403
|
+
type: 'integer',
|
|
404
|
+
description: '回看天数窗(recall)'
|
|
405
|
+
},
|
|
406
|
+
budgetCny: {
|
|
407
|
+
type: 'number',
|
|
408
|
+
description: '预算上限(recall 过滤)'
|
|
409
|
+
},
|
|
410
|
+
month: {
|
|
411
|
+
type: 'integer',
|
|
412
|
+
description: '出行月份 1-12(recall)'
|
|
413
|
+
},
|
|
414
|
+
wishId: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
description: 'confirm-outcome 目标愿望 id'
|
|
417
|
+
},
|
|
418
|
+
attribution: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
enum: [
|
|
421
|
+
'helpful',
|
|
422
|
+
'harmful',
|
|
423
|
+
'neutral'
|
|
424
|
+
],
|
|
425
|
+
description: 'confirm-outcome 归因'
|
|
426
|
+
},
|
|
427
|
+
detail: {
|
|
428
|
+
type: 'string',
|
|
429
|
+
description: '一句话细节(用户原话)'
|
|
430
|
+
},
|
|
431
|
+
tripStart: {
|
|
432
|
+
type: 'string',
|
|
433
|
+
description: '确认成行时挂时间线的行程起始日'
|
|
321
434
|
}
|
|
322
435
|
},
|
|
323
436
|
output: {
|
|
@@ -376,10 +489,18 @@ export function apply(ctx, config) {
|
|
|
376
489
|
const pool = ledger.readWishPool();
|
|
377
490
|
const candidates = pool.filter((e)=>!e.muted && typeof e.wish_id === 'string');
|
|
378
491
|
const month = q.month ?? new Date().getMonth() + 1;
|
|
492
|
+
let channelDown;
|
|
493
|
+
try {
|
|
494
|
+
const latest = await readLatestChannelEvents(config.stateRoot);
|
|
495
|
+
const down = new Set();
|
|
496
|
+
for (const [ch, ev] of latest)if (ev.state === 'down') down.add(ch);
|
|
497
|
+
channelDown = down;
|
|
498
|
+
} catch {}
|
|
379
499
|
const match = pickNudgeWish(candidates, {
|
|
380
500
|
days: q.days,
|
|
381
501
|
budgetCny: q.budgetCny,
|
|
382
|
-
month
|
|
502
|
+
month,
|
|
503
|
+
channelDown
|
|
383
504
|
});
|
|
384
505
|
if (!match) {
|
|
385
506
|
return {
|
|
@@ -416,7 +537,59 @@ export function apply(ctx, config) {
|
|
|
416
537
|
card: 'generic',
|
|
417
538
|
title: '「下一次出发」召回',
|
|
418
539
|
kind: 'search',
|
|
419
|
-
rawInput: args
|
|
540
|
+
rawInput: args
|
|
541
|
+
})
|
|
542
|
+
}));
|
|
543
|
+
registerGuarded(defineTool({
|
|
544
|
+
name: 'gotry_turn_handoff_list',
|
|
545
|
+
description: 'List background deep-planning handoff tickets (read-only). Call this whenever the user asks about ' + 'a previously handed-off plan («规划好了吗» / «上次那个行程»): open = still being worked in the ' + 'background (ETA ' + '约 1 小时' + '), settled = deliverable ready (excerpt included, full text in the ' + 'ticket\'s .deliverable.md), failed = honest failure note. Never fabricate a deliverable that is not here.',
|
|
546
|
+
parameters: {
|
|
547
|
+
ticketId: {
|
|
548
|
+
type: 'string',
|
|
549
|
+
description: '只看这一单,如 th-...(缺省列全部)'
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
output: {
|
|
553
|
+
schema: {
|
|
554
|
+
type: 'json'
|
|
555
|
+
},
|
|
556
|
+
render: (_args, value)=>[
|
|
557
|
+
{
|
|
558
|
+
type: 'text',
|
|
559
|
+
text: String(value.summary ?? '')
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
},
|
|
563
|
+
async execute (args, _exec) {
|
|
564
|
+
const q = interpretArgs(args);
|
|
565
|
+
const root = process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.';
|
|
566
|
+
const all = await listTurnHandoffTickets(root);
|
|
567
|
+
const tickets = (q.ticketId ? all.filter((t)=>t.id === q.ticketId) : all).slice(0, 10);
|
|
568
|
+
if (tickets.length === 0) {
|
|
569
|
+
return {
|
|
570
|
+
ok: true,
|
|
571
|
+
count: 0,
|
|
572
|
+
summary: q.ticketId ? `没有 id 为 ${q.ticketId} 的后台规划工单(可能已被清理或记错 id)` : '当前没有后台深度规划工单(无挂起、无历史)'
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const lines = tickets.map((t)=>{
|
|
576
|
+
const head = `[${t.status}] ${t.id} ${t.objective.slice(0, 40)}`;
|
|
577
|
+
if (t.status === 'open') return `${head}——后台规划中,ETA ${t.etaLabel}`;
|
|
578
|
+
if (t.status === 'failed') return `${head}——失败:${t.error ?? '未知原因'}(请重新发起规划)`;
|
|
579
|
+
return `${head}——已交付,摘要:${(t.deliverableExcerpt ?? '').slice(0, 120).replace(/\n/g, ' ')}`;
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
count: tickets.length,
|
|
584
|
+
tickets,
|
|
585
|
+
summary: `后台规划工单 ${tickets.length} 张:\n${lines.join('\n')}`
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
presentCall: (args)=>({
|
|
589
|
+
card: 'generic',
|
|
590
|
+
title: '后台规划工单查询',
|
|
591
|
+
kind: 'search',
|
|
592
|
+
rawInput: args
|
|
420
593
|
})
|
|
421
594
|
}));
|
|
422
595
|
registerGuarded(defineTool({
|
|
@@ -424,9 +597,26 @@ export function apply(ctx, config) {
|
|
|
424
597
|
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.',
|
|
425
598
|
parameters: {
|
|
426
599
|
companion: {
|
|
427
|
-
type: '
|
|
600
|
+
type: 'object',
|
|
601
|
+
additionalProperties: false,
|
|
428
602
|
required: true,
|
|
429
|
-
|
|
603
|
+
properties: {
|
|
604
|
+
label: {
|
|
605
|
+
type: 'string',
|
|
606
|
+
required: true,
|
|
607
|
+
description: '称呼,如 爸爸'
|
|
608
|
+
},
|
|
609
|
+
constraints: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
additionalProperties: true,
|
|
612
|
+
description: '{ mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }'
|
|
613
|
+
},
|
|
614
|
+
evidence: {
|
|
615
|
+
type: 'string',
|
|
616
|
+
required: true,
|
|
617
|
+
description: '用户原话(append-only 可溯源,P0)'
|
|
618
|
+
}
|
|
619
|
+
}
|
|
430
620
|
}
|
|
431
621
|
},
|
|
432
622
|
output: {
|
|
@@ -479,10 +669,31 @@ export function apply(ctx, config) {
|
|
|
479
669
|
name: 'gotry_trip_log',
|
|
480
670
|
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.',
|
|
481
671
|
parameters: {
|
|
482
|
-
|
|
483
|
-
type: '
|
|
672
|
+
destination: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
required: true,
|
|
675
|
+
description: '目的地,如 大理'
|
|
676
|
+
},
|
|
677
|
+
start: {
|
|
678
|
+
type: 'string',
|
|
679
|
+
required: true,
|
|
680
|
+
description: '行程起始:YYYY-MM-DD 或绝对日期表达(解析不了 execute 结构化报错不猜)'
|
|
681
|
+
},
|
|
682
|
+
end: {
|
|
683
|
+
type: 'string',
|
|
684
|
+
description: '行程结束,形态同 start(可选)'
|
|
685
|
+
},
|
|
686
|
+
companions: {
|
|
687
|
+
type: 'array',
|
|
688
|
+
items: {
|
|
689
|
+
type: 'string'
|
|
690
|
+
},
|
|
691
|
+
description: '同行人,如 ["爸爸"]'
|
|
692
|
+
},
|
|
693
|
+
evidence: {
|
|
694
|
+
type: 'string',
|
|
484
695
|
required: true,
|
|
485
|
-
description: '
|
|
696
|
+
description: '用户原话(溯源 P0 红线)'
|
|
486
697
|
}
|
|
487
698
|
},
|
|
488
699
|
output: {
|
|
@@ -497,7 +708,7 @@ export function apply(ctx, config) {
|
|
|
497
708
|
]
|
|
498
709
|
},
|
|
499
710
|
async execute (args, _exec) {
|
|
500
|
-
const q =
|
|
711
|
+
const q = args;
|
|
501
712
|
if (!q.destination) return {
|
|
502
713
|
ok: false,
|
|
503
714
|
summary: 'destination 必填'
|
|
@@ -538,17 +749,29 @@ export function apply(ctx, config) {
|
|
|
538
749
|
card: 'generic',
|
|
539
750
|
title: '记录旅行',
|
|
540
751
|
kind: 'edit',
|
|
541
|
-
rawInput: args
|
|
752
|
+
rawInput: args
|
|
542
753
|
})
|
|
543
754
|
}));
|
|
544
755
|
registerGuarded(defineTool({
|
|
545
756
|
name: 'gotry_hotel_search',
|
|
546
|
-
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/
|
|
757
|
+
description: routed('gotry_hotel_search', '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/adults. 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.'),
|
|
547
758
|
parameters: {
|
|
548
|
-
|
|
549
|
-
type: '
|
|
759
|
+
destination: {
|
|
760
|
+
type: 'string',
|
|
550
761
|
required: true,
|
|
551
|
-
description: '
|
|
762
|
+
description: '目的地城市,如 大理'
|
|
763
|
+
},
|
|
764
|
+
checkIn: {
|
|
765
|
+
type: 'string',
|
|
766
|
+
description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);解析不了降级无日期检索并记 date_notes'
|
|
767
|
+
},
|
|
768
|
+
checkOut: {
|
|
769
|
+
type: 'string',
|
|
770
|
+
description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn'
|
|
771
|
+
},
|
|
772
|
+
adults: {
|
|
773
|
+
type: 'integer',
|
|
774
|
+
description: '成人数(顶层字段,默认 2)'
|
|
552
775
|
}
|
|
553
776
|
},
|
|
554
777
|
output: {
|
|
@@ -563,7 +786,7 @@ export function apply(ctx, config) {
|
|
|
563
786
|
]
|
|
564
787
|
},
|
|
565
788
|
async execute (args, _exec) {
|
|
566
|
-
const q =
|
|
789
|
+
const q = args;
|
|
567
790
|
if (!q.destination) throw new Error('gotry_hotel_search requires destination');
|
|
568
791
|
const started = Date.now();
|
|
569
792
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json');
|
|
@@ -614,9 +837,9 @@ export function apply(ctx, config) {
|
|
|
614
837
|
},
|
|
615
838
|
presentCall: (args)=>({
|
|
616
839
|
card: 'generic',
|
|
617
|
-
title: `酒店搜索:${
|
|
840
|
+
title: `酒店搜索:${args.destination ?? ''}`,
|
|
618
841
|
kind: 'search',
|
|
619
|
-
rawInput: args
|
|
842
|
+
rawInput: args
|
|
620
843
|
}),
|
|
621
844
|
presentResult: (_args, value)=>{
|
|
622
845
|
const r = value;
|
|
@@ -680,12 +903,35 @@ export function apply(ctx, config) {
|
|
|
680
903
|
}));
|
|
681
904
|
registerGuarded(defineTool({
|
|
682
905
|
name: 'gotry_weather_check',
|
|
683
|
-
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.',
|
|
906
|
+
description: routed('gotry_weather_check', '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.'),
|
|
684
907
|
parameters: {
|
|
685
|
-
|
|
686
|
-
type: '
|
|
687
|
-
|
|
688
|
-
|
|
908
|
+
place: {
|
|
909
|
+
type: 'string',
|
|
910
|
+
description: '城市名(中文可),如 大理;与 lat/lng 二选一,都没给 execute 结构化报错'
|
|
911
|
+
},
|
|
912
|
+
lat: {
|
|
913
|
+
type: 'number',
|
|
914
|
+
description: '纬度(与 lng 成对;给了则跳过地理编码)'
|
|
915
|
+
},
|
|
916
|
+
lng: {
|
|
917
|
+
type: 'number',
|
|
918
|
+
description: '经度(与 lat 成对)'
|
|
919
|
+
},
|
|
920
|
+
month: {
|
|
921
|
+
type: 'integer',
|
|
922
|
+
description: '气候基线月份 1-12(mode=climate 或给了 month 未指 mode 时用)'
|
|
923
|
+
},
|
|
924
|
+
mode: {
|
|
925
|
+
type: 'string',
|
|
926
|
+
enum: [
|
|
927
|
+
'forecast',
|
|
928
|
+
'climate'
|
|
929
|
+
],
|
|
930
|
+
description: '默认 forecast'
|
|
931
|
+
},
|
|
932
|
+
days: {
|
|
933
|
+
type: 'integer',
|
|
934
|
+
description: '预报天数(≤16,默认 7)'
|
|
689
935
|
}
|
|
690
936
|
},
|
|
691
937
|
output: {
|
|
@@ -700,7 +946,7 @@ export function apply(ctx, config) {
|
|
|
700
946
|
]
|
|
701
947
|
},
|
|
702
948
|
async execute (args, _exec) {
|
|
703
|
-
const q =
|
|
949
|
+
const q = args;
|
|
704
950
|
const started = Date.now();
|
|
705
951
|
let lat = q.lat, lng = q.lng;
|
|
706
952
|
let placeLabel = q.place ?? `${q.lat},${q.lng}`;
|
|
@@ -760,13 +1006,13 @@ export function apply(ctx, config) {
|
|
|
760
1006
|
},
|
|
761
1007
|
presentCall: (args)=>({
|
|
762
1008
|
card: 'generic',
|
|
763
|
-
title: `天气:${
|
|
1009
|
+
title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`,
|
|
764
1010
|
kind: 'fetch',
|
|
765
|
-
rawInput: args
|
|
1011
|
+
rawInput: args
|
|
766
1012
|
}),
|
|
767
1013
|
presentResult: (args, value)=>{
|
|
768
1014
|
const r = value;
|
|
769
|
-
const place = String(args.
|
|
1015
|
+
const place = String(args.place ?? '');
|
|
770
1016
|
const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable');
|
|
771
1017
|
return {
|
|
772
1018
|
card: 'generic',
|
|
@@ -782,12 +1028,20 @@ export function apply(ctx, config) {
|
|
|
782
1028
|
}));
|
|
783
1029
|
registerGuarded(defineTool({
|
|
784
1030
|
name: 'gotry_flight_verify',
|
|
785
|
-
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).',
|
|
1031
|
+
description: routed('gotry_flight_verify', '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).'),
|
|
786
1032
|
parameters: {
|
|
787
|
-
|
|
788
|
-
type: '
|
|
1033
|
+
callsign: {
|
|
1034
|
+
type: 'string',
|
|
789
1035
|
required: true,
|
|
790
|
-
description: '
|
|
1036
|
+
description: '航班呼号,如 EK329'
|
|
1037
|
+
},
|
|
1038
|
+
airport: {
|
|
1039
|
+
type: 'string',
|
|
1040
|
+
description: '预期机场 IATA,如 OMDB(观测命中时展示)'
|
|
1041
|
+
},
|
|
1042
|
+
timeoutMs: {
|
|
1043
|
+
type: 'integer',
|
|
1044
|
+
description: '超时毫秒,默认 10000'
|
|
791
1045
|
}
|
|
792
1046
|
},
|
|
793
1047
|
output: {
|
|
@@ -802,7 +1056,7 @@ export function apply(ctx, config) {
|
|
|
802
1056
|
]
|
|
803
1057
|
},
|
|
804
1058
|
async execute (args, _exec) {
|
|
805
|
-
const q =
|
|
1059
|
+
const q = args;
|
|
806
1060
|
const started = Date.now();
|
|
807
1061
|
if (!q.callsign) {
|
|
808
1062
|
return JSON.parse(JSON.stringify({
|
|
@@ -838,19 +1092,48 @@ export function apply(ctx, config) {
|
|
|
838
1092
|
},
|
|
839
1093
|
presentCall: (args)=>({
|
|
840
1094
|
card: 'generic',
|
|
841
|
-
title: `飞行校验:${
|
|
1095
|
+
title: `飞行校验:${args.callsign ?? ''}`,
|
|
842
1096
|
kind: 'fetch',
|
|
843
|
-
rawInput: args
|
|
1097
|
+
rawInput: args
|
|
844
1098
|
})
|
|
845
1099
|
}));
|
|
846
1100
|
registerGuarded(defineTool({
|
|
847
1101
|
name: 'gotry_flyai_search',
|
|
848
|
-
description: 'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). ' + 'kind="flight"|"train":
|
|
1102
|
+
description: routed('gotry_flyai_search', 'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). ' + 'kind="flight"|"train": from/to 中文城市名 + date YYYY-MM-DD — real schedules & prices, split 直达/中转 in results. ' + 'kind="hotel": to=目的地中文(如 大理), checkIn/checkOut (YYYY-MM-DD,成对可选——未定档期可不填先摸底), keyWords?. ' + 'Hotel prices may be masked upstream (priceRaw like "¥7xx"): always present the mask as a range, and let the human open jumpUrl for the real price. ' + 'Evidence [实时API:flyai@ts]. verdict=needs-setup (anonymous trial quota exhausted, upstream 429) is a CONFIG issue, not a search failure: surface the setup hint once, do NOT retry this tool this session — switch to gotry_session_search or web search. ' + 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.'),
|
|
849
1103
|
parameters: {
|
|
850
|
-
|
|
851
|
-
type: '
|
|
1104
|
+
kind: {
|
|
1105
|
+
type: 'string',
|
|
1106
|
+
enum: [
|
|
1107
|
+
'flight',
|
|
1108
|
+
'train',
|
|
1109
|
+
'hotel'
|
|
1110
|
+
],
|
|
852
1111
|
required: true,
|
|
853
|
-
description: '
|
|
1112
|
+
description: '检索类型:flight 机票 / train 火车 / hotel 酒店'
|
|
1113
|
+
},
|
|
1114
|
+
from: {
|
|
1115
|
+
type: 'string',
|
|
1116
|
+
description: '出发城市中文,如 上海——kind=flight|train 必填'
|
|
1117
|
+
},
|
|
1118
|
+
to: {
|
|
1119
|
+
type: 'string',
|
|
1120
|
+
description: '到达城市中文,如 丽江;kind=hotel 时为目的地,如 大理(必填)'
|
|
1121
|
+
},
|
|
1122
|
+
date: {
|
|
1123
|
+
type: 'string',
|
|
1124
|
+
description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来'
|
|
1125
|
+
},
|
|
1126
|
+
checkIn: {
|
|
1127
|
+
type: 'string',
|
|
1128
|
+
description: '入住日期 YYYY-MM-DD,仅 kind=hotel;与 checkOut 成对可选(未定档期可不填先摸底)'
|
|
1129
|
+
},
|
|
1130
|
+
checkOut: {
|
|
1131
|
+
type: 'string',
|
|
1132
|
+
description: '退房日期 YYYY-MM-DD,仅 kind=hotel;与 checkIn 成对'
|
|
1133
|
+
},
|
|
1134
|
+
keyWords: {
|
|
1135
|
+
type: 'string',
|
|
1136
|
+
description: '酒店关键词,如 洱海——仅 kind=hotel 可选'
|
|
854
1137
|
}
|
|
855
1138
|
},
|
|
856
1139
|
output: {
|
|
@@ -865,7 +1148,7 @@ export function apply(ctx, config) {
|
|
|
865
1148
|
]
|
|
866
1149
|
},
|
|
867
1150
|
async execute (args, _exec) {
|
|
868
|
-
const q =
|
|
1151
|
+
const q = args;
|
|
869
1152
|
if (q.kind === 'hotel') {
|
|
870
1153
|
const dest = (q.to ?? '').trim();
|
|
871
1154
|
if (!dest) return {
|
|
@@ -908,11 +1191,23 @@ export function apply(ctx, config) {
|
|
|
908
1191
|
});
|
|
909
1192
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
910
1193
|
const r = itp.result;
|
|
1194
|
+
await noteChannel('flyai', r.verdict);
|
|
1195
|
+
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
1196
|
+
source: 'flyai-hotel',
|
|
1197
|
+
destination: dest,
|
|
1198
|
+
checkIn: q.checkIn,
|
|
1199
|
+
checkOut: q.checkOut,
|
|
1200
|
+
verdict: r.verdict,
|
|
1201
|
+
options: r.hotels?.length ?? 0,
|
|
1202
|
+
evidence: r.evidence,
|
|
1203
|
+
fetchedAt: new Date().toISOString()
|
|
1204
|
+
}));
|
|
911
1205
|
const top = (r.hotels ?? []).slice(0, 8).map((o)=>`${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`);
|
|
912
|
-
const summary = r.verdict === 'hit' ? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}` : `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
1206
|
+
const summary = r.verdict === 'hit' ? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}` : r.verdict === 'needs-setup' ? `${dest} 酒店检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}` : `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
913
1207
|
return JSON.parse(JSON.stringify({
|
|
914
1208
|
...r,
|
|
915
|
-
summary
|
|
1209
|
+
summary,
|
|
1210
|
+
...r.verdict !== 'hit' ? routingField('search-hotel', 'flyai') : {}
|
|
916
1211
|
}));
|
|
917
1212
|
}
|
|
918
1213
|
const kind = q.kind === 'train' ? 'train' : 'flight';
|
|
@@ -943,6 +1238,7 @@ export function apply(ctx, config) {
|
|
|
943
1238
|
});
|
|
944
1239
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
945
1240
|
const r = itp.result;
|
|
1241
|
+
await noteChannel('flyai', r.verdict);
|
|
946
1242
|
const avMap = await loadAirlineAirportMap();
|
|
947
1243
|
await appendFacts(config.stateRoot ?? '.', factsFromFlyai({
|
|
948
1244
|
kind,
|
|
@@ -952,21 +1248,22 @@ export function apply(ctx, config) {
|
|
|
952
1248
|
}, r, new Date().toISOString(), avMap?.city_alias));
|
|
953
1249
|
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}`);
|
|
954
1250
|
const label = kind === 'flight' ? '机票' : '火车票';
|
|
955
|
-
const summary = r.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} ${label}(飞猪官方只读)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}` : r.verdict === 'miss' ? `${q.from}→${q.to} ${q.date} ${label}官方通道正常返回 0 条(常见原因:航线未开放/当日售罄)。${r.evidence}` : `${q.from}→${q.to} ${q.date} ${label}检索失败(可能限流/网络):${r.error ?? ''} ${r.evidence}`;
|
|
1251
|
+
const summary = r.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} ${label}(飞猪官方只读)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}` : r.verdict === 'miss' ? `${q.from}→${q.to} ${q.date} ${label}官方通道正常返回 0 条(常见原因:航线未开放/当日售罄)。${r.evidence}` : r.verdict === 'needs-setup' ? `${q.from}→${q.to} ${q.date} ${label}检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}` : `${q.from}→${q.to} ${q.date} ${label}检索失败(可能限流/网络):${r.error ?? ''} ${r.evidence}`;
|
|
956
1252
|
return JSON.parse(JSON.stringify({
|
|
957
1253
|
...r,
|
|
958
1254
|
kind,
|
|
959
|
-
summary
|
|
1255
|
+
summary,
|
|
1256
|
+
...r.verdict !== 'hit' ? routingField(kind === 'train' ? 'search-train' : 'search-flight', 'flyai') : {}
|
|
960
1257
|
}));
|
|
961
1258
|
},
|
|
962
1259
|
presentCall: (args)=>({
|
|
963
1260
|
card: 'generic',
|
|
964
|
-
title: `官方检索:${
|
|
1261
|
+
title: `官方检索:${args.kind}`,
|
|
965
1262
|
kind: 'fetch',
|
|
966
|
-
rawInput: args
|
|
1263
|
+
rawInput: args
|
|
967
1264
|
}),
|
|
968
1265
|
presentResult: (args, value)=>{
|
|
969
|
-
const isHotel =
|
|
1266
|
+
const isHotel = args.kind === 'hotel';
|
|
970
1267
|
const r = value;
|
|
971
1268
|
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length);
|
|
972
1269
|
return {
|
|
@@ -985,10 +1282,9 @@ export function apply(ctx, config) {
|
|
|
985
1282
|
name: 'gotry_session_login',
|
|
986
1283
|
description: 'Productized login bootstrap for the account session channel (call this when gotry_session_search returns needs-login — the user never needs a terminal). ' + 'AUTO-DETECTION FIRST: it reads ticket-cookie NAMES before anything else — if the user already logged in (on the external site) it confirms instantly WITHOUT opening any page. ' + 'OPENS the site login entry in the USER\'S OWN Chrome (foreground tab, left open for the user) and waits for the user to finish logging in on the external site. ' + 'GoTry NEVER collects, stores, or transmits credentials: no passwords, no SMS codes, no cookie values — it only checks the boolean fact "already logged in" (reads cookie NAMES only, zero values). ' + 'Transport: the GoTry Session Bridge browser extension (one-time install, ZERO Chrome system dialogs). ' + `verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-extension (one-time extension install: the verdict surfaces the Chrome Web Store installUrl as a clickable link for dsh UI to render — installation is a browser concern, not gotry's). ` + 'Evidence [会话:<site>-login@ts].',
|
|
987
1284
|
parameters: {
|
|
988
|
-
|
|
989
|
-
type: '
|
|
990
|
-
|
|
991
|
-
description: '可空对象 {}: { waitSeconds?: number }(等待用户完成登录的上限秒数,默认 90,至多 300)'
|
|
1285
|
+
waitSeconds: {
|
|
1286
|
+
type: 'integer',
|
|
1287
|
+
description: '等待用户完成登录的上限秒数,默认 90,至多 300;不传用默认'
|
|
992
1288
|
}
|
|
993
1289
|
},
|
|
994
1290
|
output: {
|
|
@@ -1004,9 +1300,14 @@ export function apply(ctx, config) {
|
|
|
1004
1300
|
},
|
|
1005
1301
|
async execute (args, _exec) {
|
|
1006
1302
|
const q = unwrapQuery(args, 'waitSeconds');
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1303
|
+
const itpL = await interpretEffect({
|
|
1304
|
+
effect: 'SESSION_LOGIN',
|
|
1305
|
+
params: {
|
|
1306
|
+
waitSeconds: q?.waitSeconds
|
|
1307
|
+
}
|
|
1009
1308
|
});
|
|
1309
|
+
if (!itpL.result) return declinedObservation('SESSION_LOGIN', itpL.trace);
|
|
1310
|
+
const r = itpL.result;
|
|
1010
1311
|
const summary = r.verdict === 'logged-in' ? `${r.site} 登录完成确认(票据 cookie 名已检出,只读名字)。说明:登录是在携程官网、用你自己的浏览器完成的——gotry 全程未接触任何密码/验证码/cookie 值。现在可以继续会话检索了。${r.evidence}` : r.verdict === 'pending' ? `登录入口已在你的 Chrome 打开;请在弹出的标签页里正常登录携程(登录由你在官网完成,不属于 gotry)。完成后说一声"继续",我再确认。gotry 只检查"是否已登录",永不收集你的账号信息。${r.evidence}` : r.verdict === 'needs-extension' ? `需要一次性安装 GoTry Session Bridge 浏览器扩展:这是浏览器的事,gotry 不弹面板不开剪贴板——请直接在 Chrome 应用商店一键装(add-to-chrome 自动更新)。storeUrl=${EXTENSION_STORE_URL}. 装好即生效,装完后告诉我「重试」即可。${r.evidence}` : r.verdict === 'needs-attach' ? `cdp 车道需要一次性开启你 Chrome 的远程调试开关:在你的 Chrome 地址栏打开 chrome://inspect/#remote-debugging 并打开开关,然后说一声"重试"(默认走扩展车道,无需此步)。${r.evidence}` : `登录引导未完成:${r.error ?? '未知原因'} ${r.evidence}`;
|
|
1011
1312
|
return JSON.parse(JSON.stringify({
|
|
1012
1313
|
...r,
|
|
@@ -1042,12 +1343,52 @@ export function apply(ctx, config) {
|
|
|
1042
1343
|
}));
|
|
1043
1344
|
registerGuarded(defineTool({
|
|
1044
1345
|
name: 'gotry_session_search',
|
|
1045
|
-
description: 'Search on the USER\'S OWN
|
|
1346
|
+
description: routed('gotry_session_search', 'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). ' + 'Consent gate: the FIRST call in a session asks the user via the runtime approval card; once granted it holds for the session, a refusal revokes it for the session (no repeat prompting). ' + 'Transport: GoTry Session Bridge browser extension (one-time install) — the agent side never talks to Chrome debugging, ZERO system dialogs; read-only by construction (the extension never issues requests; it only passively forwards the site\'s own search responses; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). ' + 'kind="flight": from/to 中文城市名 + date YYYY-MM-DD — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. ' + 'kind="hotel": to=目的地中文, cityId? = the numeric city= in a hotels.ctrip.com list URL (web-search it when the destination is outside the built-in city table), checkIn?/checkOut? (YYYY-MM-DD), adults?; hotel prices are the user\'s real logged-in prices. Evidence [会话:ctrip-hotel@ts]. ' + 'kind="train": from/to/date(YYYY-MM-DD) + fromStationTelecode?/toStationTelecode? (three-letter codes in the kyfw query URL, for cities outside the built-in table) — 12306 left-ticket query (public face): train codes, times, durations, seat availability; the list API carries NO prices (prices live on the 12306 page). Evidence [会话:train-12306@ts]. ' + 'verdict needs-login = call gotry_session_login (opens the Ctrip login entry in the user\'s own foreground tab — no terminal, no credentials through GoTry); ' + `needs-extension = one-time browser-extension install (Chrome Web Store one-click, installUrl is also surfaced as a clickable link in the verdict field for dsh UI to render) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. ` + 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).'),
|
|
1046
1347
|
parameters: {
|
|
1047
|
-
|
|
1048
|
-
type: '
|
|
1049
|
-
|
|
1050
|
-
|
|
1348
|
+
kind: {
|
|
1349
|
+
type: 'string',
|
|
1350
|
+
enum: [
|
|
1351
|
+
'flight',
|
|
1352
|
+
'hotel',
|
|
1353
|
+
'train'
|
|
1354
|
+
],
|
|
1355
|
+
description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面)'
|
|
1356
|
+
},
|
|
1357
|
+
from: {
|
|
1358
|
+
type: 'string',
|
|
1359
|
+
description: '出发城市中文(词表内),如 上海——kind=flight|train 必填'
|
|
1360
|
+
},
|
|
1361
|
+
to: {
|
|
1362
|
+
type: 'string',
|
|
1363
|
+
description: '到达城市中文——kind=flight|train 必填;kind=hotel 时为目的地(必填)'
|
|
1364
|
+
},
|
|
1365
|
+
date: {
|
|
1366
|
+
type: 'string',
|
|
1367
|
+
description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来'
|
|
1368
|
+
},
|
|
1369
|
+
cityId: {
|
|
1370
|
+
type: 'integer',
|
|
1371
|
+
description: '携程酒店 city= 数字,仅 kind=hotel;目的地码表外时 web 搜 hotels.ctrip.com list URL 取 city='
|
|
1372
|
+
},
|
|
1373
|
+
checkIn: {
|
|
1374
|
+
type: 'string',
|
|
1375
|
+
description: '入住日 YYYY-MM-DD,仅 kind=hotel 可选'
|
|
1376
|
+
},
|
|
1377
|
+
checkOut: {
|
|
1378
|
+
type: 'string',
|
|
1379
|
+
description: '退房日 YYYY-MM-DD,仅 kind=hotel 可选'
|
|
1380
|
+
},
|
|
1381
|
+
adults: {
|
|
1382
|
+
type: 'integer',
|
|
1383
|
+
description: '成人数,仅 kind=hotel 可选'
|
|
1384
|
+
},
|
|
1385
|
+
fromStationTelecode: {
|
|
1386
|
+
type: 'string',
|
|
1387
|
+
description: '出发站三位电报码,仅 kind=train;城市码表外时填(kyfw 查询页 URL 里)'
|
|
1388
|
+
},
|
|
1389
|
+
toStationTelecode: {
|
|
1390
|
+
type: 'string',
|
|
1391
|
+
description: '到达站三位电报码,仅 kind=train,同上'
|
|
1051
1392
|
}
|
|
1052
1393
|
},
|
|
1053
1394
|
output: {
|
|
@@ -1062,11 +1403,90 @@ export function apply(ctx, config) {
|
|
|
1062
1403
|
]
|
|
1063
1404
|
},
|
|
1064
1405
|
async execute (args, _exec) {
|
|
1065
|
-
const q = unwrapQuery(args
|
|
1406
|
+
const q = unwrapQuery(args);
|
|
1407
|
+
if (q.kind === 'train') {
|
|
1408
|
+
if (!q.from || !q.to || !q.date) {
|
|
1409
|
+
return {
|
|
1410
|
+
ok: false,
|
|
1411
|
+
summary: 'kind=train 需要 from/to(中文城市/车站名)与 date(YYYY-MM-DD);城市电报码表外带 fromStationTelecode/toStationTelecode(kyfw 查询页 URL 里的三位码)'
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
const nowT = new Date();
|
|
1415
|
+
const todayT = `${nowT.getFullYear()}-${String(nowT.getMonth() + 1).padStart(2, '0')}-${String(nowT.getDate()).padStart(2, '0')}`;
|
|
1416
|
+
if (q.date < todayT) {
|
|
1417
|
+
return JSON.parse(JSON.stringify({
|
|
1418
|
+
ok: false,
|
|
1419
|
+
verdict: 'error',
|
|
1420
|
+
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayT}),过去不存在在售火车票。向用户确认日期后再查。`
|
|
1421
|
+
}));
|
|
1422
|
+
}
|
|
1423
|
+
const itpT = await interpretEffect({
|
|
1424
|
+
effect: 'SESSION_TRAIN_SEARCH',
|
|
1425
|
+
params: {
|
|
1426
|
+
from: q.from,
|
|
1427
|
+
to: q.to,
|
|
1428
|
+
date: q.date,
|
|
1429
|
+
fromStationTelecode: q.fromStationTelecode,
|
|
1430
|
+
toStationTelecode: q.toStationTelecode,
|
|
1431
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl')
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
if (!itpT.result) return declinedObservation('SESSION_TRAIN_SEARCH', itpT.trace);
|
|
1435
|
+
const rT = itpT.result;
|
|
1436
|
+
await noteChannel('session:12306-train', rT.verdict);
|
|
1437
|
+
const topT = (rT.trains ?? []).slice(0, 10).map((t)=>`${t.trainCode} ${t.depTime}→${t.arrTime} 历时${Math.round(t.durationMin / 60 * 10) / 10}h ${t.canWebBuy === 'Y' ? '可订' : t.canWebBuy}${Object.entries(t.seats).filter(([, v])=>v && v !== '--' && v !== '无').slice(0, 3).map(([k, v])=>`${k}:${v}`).join(' ')}`);
|
|
1438
|
+
const summaryT = rT.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} 余票(12306 公开查询面,${(rT.trains ?? []).length} 趟)前 ${topT.length} 条(列表接口不含票价,票价以 12306 落地页为准):\n${topT.join('\n')}\n${rT.evidence}` : rT.verdict === 'cooldown' ? `会话火车检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${rT.evidence}` : `会话火车检索未取回(${rT.verdict}):${rT.error ?? ''} ${rT.evidence}`;
|
|
1439
|
+
return JSON.parse(JSON.stringify({
|
|
1440
|
+
...rT,
|
|
1441
|
+
summary: summaryT,
|
|
1442
|
+
...rT.verdict !== 'hit' ? routingField('search-train', 'session:12306-train') : {}
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
if (q.kind === 'hotel') {
|
|
1446
|
+
if (!q.to) {
|
|
1447
|
+
return {
|
|
1448
|
+
ok: false,
|
|
1449
|
+
summary: 'kind=hotel 需要 to(目的地中文;城市码表外带 cityId=携程酒店 list 页 URL 里的 city= 数字)'
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
const itp = await interpretEffect({
|
|
1453
|
+
effect: 'SESSION_HOTEL_SEARCH',
|
|
1454
|
+
params: {
|
|
1455
|
+
to: q.to,
|
|
1456
|
+
cityId: q.cityId,
|
|
1457
|
+
checkIn: q.checkIn,
|
|
1458
|
+
checkOut: q.checkOut,
|
|
1459
|
+
adults: q.adults,
|
|
1460
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl')
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
if (!itp.result) return declinedObservation('SESSION_HOTEL_SEARCH', itp.trace);
|
|
1464
|
+
const r = itp.result;
|
|
1465
|
+
await noteChannel('session:ctrip-hotel', r.verdict);
|
|
1466
|
+
if (r.verdict === 'hit' || r.verdict === 'miss') {
|
|
1467
|
+
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
1468
|
+
source: 'session:ctrip-hotel',
|
|
1469
|
+
destination: q.to ?? '',
|
|
1470
|
+
checkIn: q.checkIn,
|
|
1471
|
+
checkOut: q.checkOut,
|
|
1472
|
+
verdict: r.verdict,
|
|
1473
|
+
options: r.hotels?.length ?? 0,
|
|
1474
|
+
evidence: r.evidence,
|
|
1475
|
+
fetchedAt: new Date().toISOString()
|
|
1476
|
+
}));
|
|
1477
|
+
}
|
|
1478
|
+
const top = (r.hotels ?? []).slice(0, 8).map((h)=>`${h.name}${h.star ? `(${h.star}星)` : ''} ${h.priceRaw ?? (h.price > 0 ? `¥${h.price}` : '价待询')}${h.score ? ` 评分${h.score}` : ''}`);
|
|
1479
|
+
const summary = r.verdict === 'hit' ? `${q.to} 酒店(携程,用户本人登录态,真实价)前 ${top.length} 家(预订以 jumpUrl 落地页为准):\n${top.join('\n')}\n${r.evidence}` : r.verdict === 'cooldown' ? `会话酒店检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${r.evidence}` : `会话酒店检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`;
|
|
1480
|
+
return JSON.parse(JSON.stringify({
|
|
1481
|
+
...r,
|
|
1482
|
+
summary,
|
|
1483
|
+
...r.verdict !== 'hit' ? routingField('search-hotel', 'session:ctrip-hotel') : {}
|
|
1484
|
+
}));
|
|
1485
|
+
}
|
|
1066
1486
|
if (!q.from || !q.to || !q.date) {
|
|
1067
1487
|
return {
|
|
1068
1488
|
ok: false,
|
|
1069
|
-
summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD)'
|
|
1489
|
+
summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD);酒店/火车检索分别用 kind=hotel / kind=train'
|
|
1070
1490
|
};
|
|
1071
1491
|
}
|
|
1072
1492
|
const itp = await interpretEffect({
|
|
@@ -1080,6 +1500,7 @@ export function apply(ctx, config) {
|
|
|
1080
1500
|
});
|
|
1081
1501
|
if (!itp.result) return declinedObservation('SESSION_FLIGHT_SEARCH', itp.trace);
|
|
1082
1502
|
const r = itp.result;
|
|
1503
|
+
await noteChannel('session:ctrip-flight', r.verdict);
|
|
1083
1504
|
const avMapS = await loadAirlineAirportMap();
|
|
1084
1505
|
await appendFacts(config.stateRoot ?? '.', factsFromSession({
|
|
1085
1506
|
origin: q.from,
|
|
@@ -1090,19 +1511,25 @@ export function apply(ctx, config) {
|
|
|
1090
1511
|
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}`;
|
|
1091
1512
|
return JSON.parse(JSON.stringify({
|
|
1092
1513
|
...r,
|
|
1093
|
-
summary
|
|
1514
|
+
summary,
|
|
1515
|
+
...r.verdict !== 'hit' ? routingField('search-flight', 'session:ctrip-flight') : {}
|
|
1094
1516
|
}));
|
|
1095
1517
|
},
|
|
1096
|
-
presentCall: (args)=>
|
|
1518
|
+
presentCall: (args)=>{
|
|
1519
|
+
const callTitle = args.kind === 'hotel' ? `会话酒店:${args.to ?? ''}` : args.kind === 'train' ? `会话火车:${args.from ?? ''}` : `会话检索:${args.from ?? ''}`;
|
|
1520
|
+
return {
|
|
1097
1521
|
card: 'generic',
|
|
1098
|
-
title:
|
|
1522
|
+
title: callTitle,
|
|
1099
1523
|
kind: 'fetch',
|
|
1100
|
-
rawInput: args
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1524
|
+
rawInput: args
|
|
1525
|
+
};
|
|
1526
|
+
},
|
|
1527
|
+
presentResult: (args, value)=>{
|
|
1103
1528
|
const r = value;
|
|
1529
|
+
const isHotel = args.kind === 'hotel';
|
|
1530
|
+
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length);
|
|
1104
1531
|
const needsExt = r.verdict === 'needs-extension';
|
|
1105
|
-
const label = r.verdict === 'hit' ? `会话 ${
|
|
1532
|
+
const label = r.verdict === 'hit' ? `会话 ${n} 条` : r.verdict === 'needs-login' ? '需登录' : needsExt ? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)' : r.verdict ?? '降级';
|
|
1106
1533
|
const content = [
|
|
1107
1534
|
{
|
|
1108
1535
|
type: 'text',
|
|
@@ -1113,21 +1540,38 @@ export function apply(ctx, config) {
|
|
|
1113
1540
|
type: 'text',
|
|
1114
1541
|
text: `安装链接:${r.installUrl}`
|
|
1115
1542
|
});
|
|
1543
|
+
const face = args.kind === 'hotel' ? '会话酒店' : args.kind === 'train' ? '会话火车' : '会话检索';
|
|
1116
1544
|
return {
|
|
1117
1545
|
card: 'generic',
|
|
1118
|
-
title:
|
|
1546
|
+
title: `${face}:${label}`,
|
|
1119
1547
|
content
|
|
1120
1548
|
};
|
|
1121
1549
|
}
|
|
1122
1550
|
}));
|
|
1123
1551
|
registerGuarded(defineTool({
|
|
1124
1552
|
name: 'gotry_anything_search',
|
|
1125
|
-
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).',
|
|
1553
|
+
description: routed('gotry_anything_search', '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).'),
|
|
1126
1554
|
parameters: {
|
|
1127
|
-
|
|
1128
|
-
type: '
|
|
1555
|
+
keyword: {
|
|
1556
|
+
type: 'string',
|
|
1129
1557
|
required: true,
|
|
1130
|
-
description: '
|
|
1558
|
+
description: '搜索关键词'
|
|
1559
|
+
},
|
|
1560
|
+
contentType: {
|
|
1561
|
+
type: 'string',
|
|
1562
|
+
enum: [
|
|
1563
|
+
'city',
|
|
1564
|
+
'hotel'
|
|
1565
|
+
],
|
|
1566
|
+
description: '内容类型(缺省由上游推断)'
|
|
1567
|
+
},
|
|
1568
|
+
parentDestinationId: {
|
|
1569
|
+
type: 'string',
|
|
1570
|
+
description: '父目的地 id(上游形态)'
|
|
1571
|
+
},
|
|
1572
|
+
timeoutMs: {
|
|
1573
|
+
type: 'integer',
|
|
1574
|
+
description: '超时毫秒,默认 12000'
|
|
1131
1575
|
}
|
|
1132
1576
|
},
|
|
1133
1577
|
output: {
|
|
@@ -1142,7 +1586,8 @@ export function apply(ctx, config) {
|
|
|
1142
1586
|
]
|
|
1143
1587
|
},
|
|
1144
1588
|
async execute (args, _exec) {
|
|
1145
|
-
const q =
|
|
1589
|
+
const q = args;
|
|
1590
|
+
const rawQuery = String(q.keyword ?? '');
|
|
1146
1591
|
const started = Date.now();
|
|
1147
1592
|
if (!q.keyword) {
|
|
1148
1593
|
return JSON.parse(JSON.stringify({
|
|
@@ -1152,7 +1597,12 @@ export function apply(ctx, config) {
|
|
|
1152
1597
|
evidence: '[hbcli-anything@error] empty'
|
|
1153
1598
|
}));
|
|
1154
1599
|
}
|
|
1155
|
-
const
|
|
1600
|
+
const itpA = await interpretEffect({
|
|
1601
|
+
effect: 'ANYTHING_SEARCH',
|
|
1602
|
+
params: q
|
|
1603
|
+
});
|
|
1604
|
+
if (!itpA.result) return declinedObservation('ANYTHING_SEARCH', itpA.trace);
|
|
1605
|
+
const r = itpA.result;
|
|
1156
1606
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1157
1607
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(()=>{});
|
|
1158
1608
|
const top5 = (r.hits ?? []).slice(0, 5);
|
|
@@ -1171,9 +1621,9 @@ export function apply(ctx, config) {
|
|
|
1171
1621
|
},
|
|
1172
1622
|
presentCall: (args)=>({
|
|
1173
1623
|
card: 'generic',
|
|
1174
|
-
title: `Anything search:${
|
|
1624
|
+
title: `Anything search:${args.keyword ?? ''}`,
|
|
1175
1625
|
kind: 'search',
|
|
1176
|
-
rawInput: args
|
|
1626
|
+
rawInput: args
|
|
1177
1627
|
}),
|
|
1178
1628
|
presentResult: (_args, value)=>{
|
|
1179
1629
|
const r = value;
|
|
@@ -1192,12 +1642,16 @@ export function apply(ctx, config) {
|
|
|
1192
1642
|
}));
|
|
1193
1643
|
registerGuarded(defineTool({
|
|
1194
1644
|
name: 'gotry_web_search',
|
|
1195
|
-
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 证据链 + 降级不阻塞 + 三值)。',
|
|
1645
|
+
description: routed('gotry_web_search', '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 证据链 + 降级不阻塞 + 三值)。'),
|
|
1196
1646
|
parameters: {
|
|
1197
|
-
|
|
1198
|
-
type: '
|
|
1647
|
+
url: {
|
|
1648
|
+
type: 'string',
|
|
1199
1649
|
required: true,
|
|
1200
|
-
description: '
|
|
1650
|
+
description: '完整 URL,https://…'
|
|
1651
|
+
},
|
|
1652
|
+
timeoutMs: {
|
|
1653
|
+
type: 'integer',
|
|
1654
|
+
description: '超时毫秒,默认 20000'
|
|
1201
1655
|
}
|
|
1202
1656
|
},
|
|
1203
1657
|
output: {
|
|
@@ -1212,7 +1666,7 @@ export function apply(ctx, config) {
|
|
|
1212
1666
|
]
|
|
1213
1667
|
},
|
|
1214
1668
|
async execute (args, _exec) {
|
|
1215
|
-
const q =
|
|
1669
|
+
const q = args;
|
|
1216
1670
|
const started = Date.now();
|
|
1217
1671
|
if (!q.url) {
|
|
1218
1672
|
return JSON.parse(JSON.stringify({
|
|
@@ -1221,10 +1675,15 @@ export function apply(ctx, config) {
|
|
|
1221
1675
|
evidence: '[agent-reach:error] empty url'
|
|
1222
1676
|
}));
|
|
1223
1677
|
}
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1678
|
+
const itpW = await interpretEffect({
|
|
1679
|
+
effect: 'WEB_READ',
|
|
1680
|
+
params: {
|
|
1681
|
+
url: q.url,
|
|
1682
|
+
timeoutMs: q.timeoutMs
|
|
1683
|
+
}
|
|
1227
1684
|
});
|
|
1685
|
+
if (!itpW.result) return declinedObservation('WEB_READ', itpW.trace);
|
|
1686
|
+
const r = itpW.result;
|
|
1228
1687
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1229
1688
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(()=>{});
|
|
1230
1689
|
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}`;
|
|
@@ -1241,19 +1700,23 @@ export function apply(ctx, config) {
|
|
|
1241
1700
|
},
|
|
1242
1701
|
presentCall: (args)=>({
|
|
1243
1702
|
card: 'generic',
|
|
1244
|
-
title: `读网页:${
|
|
1703
|
+
title: `读网页:${args.url ?? ''}`,
|
|
1245
1704
|
kind: 'fetch',
|
|
1246
|
-
rawInput: args
|
|
1705
|
+
rawInput: args
|
|
1247
1706
|
})
|
|
1248
1707
|
}));
|
|
1249
1708
|
registerGuarded(defineTool({
|
|
1250
1709
|
name: 'gotry_video_subtitle',
|
|
1251
1710
|
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].',
|
|
1252
1711
|
parameters: {
|
|
1253
|
-
|
|
1254
|
-
type: '
|
|
1712
|
+
url: {
|
|
1713
|
+
type: 'string',
|
|
1255
1714
|
required: true,
|
|
1256
|
-
description: '
|
|
1715
|
+
description: '视频页 URL(youtube 等)'
|
|
1716
|
+
},
|
|
1717
|
+
lang: {
|
|
1718
|
+
type: 'string',
|
|
1719
|
+
description: '字幕语言优先级,如 zh-Hans,zh,en'
|
|
1257
1720
|
}
|
|
1258
1721
|
},
|
|
1259
1722
|
output: {
|
|
@@ -1268,17 +1731,22 @@ export function apply(ctx, config) {
|
|
|
1268
1731
|
]
|
|
1269
1732
|
},
|
|
1270
1733
|
async execute (args, _exec) {
|
|
1271
|
-
const q =
|
|
1734
|
+
const q = args;
|
|
1272
1735
|
if (!q.url) {
|
|
1273
1736
|
return JSON.parse(JSON.stringify({
|
|
1274
1737
|
ok: false,
|
|
1275
1738
|
summary: 'url 必填'
|
|
1276
1739
|
}));
|
|
1277
1740
|
}
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1741
|
+
const itpV = await interpretEffect({
|
|
1742
|
+
effect: 'VIDEO_SUBTITLE',
|
|
1743
|
+
params: {
|
|
1744
|
+
url: q.url,
|
|
1745
|
+
lang: q.lang
|
|
1746
|
+
}
|
|
1281
1747
|
});
|
|
1748
|
+
if (!itpV.result) return declinedObservation('VIDEO_SUBTITLE', itpV.trace);
|
|
1749
|
+
const r = itpV.result;
|
|
1282
1750
|
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}`;
|
|
1283
1751
|
return JSON.parse(JSON.stringify({
|
|
1284
1752
|
ok: r.ok,
|
|
@@ -1292,9 +1760,9 @@ export function apply(ctx, config) {
|
|
|
1292
1760
|
},
|
|
1293
1761
|
presentCall: (args)=>({
|
|
1294
1762
|
card: 'generic',
|
|
1295
|
-
title: `视频字幕:${
|
|
1763
|
+
title: `视频字幕:${args.url ?? ''}`,
|
|
1296
1764
|
kind: 'fetch',
|
|
1297
|
-
rawInput: args
|
|
1765
|
+
rawInput: args
|
|
1298
1766
|
})
|
|
1299
1767
|
}));
|
|
1300
1768
|
registerGuarded(defineTool({
|
|
@@ -1302,9 +1770,13 @@ export function apply(ctx, config) {
|
|
|
1302
1770
|
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].',
|
|
1303
1771
|
parameters: {
|
|
1304
1772
|
query: {
|
|
1305
|
-
type: '
|
|
1773
|
+
type: 'string',
|
|
1306
1774
|
required: true,
|
|
1307
|
-
description: '
|
|
1775
|
+
description: 'GitHub 搜索关键词,如 agent-reach'
|
|
1776
|
+
},
|
|
1777
|
+
limit: {
|
|
1778
|
+
type: 'integer',
|
|
1779
|
+
description: '结果数上限,默认 5'
|
|
1308
1780
|
}
|
|
1309
1781
|
},
|
|
1310
1782
|
output: {
|
|
@@ -1319,17 +1791,22 @@ export function apply(ctx, config) {
|
|
|
1319
1791
|
]
|
|
1320
1792
|
},
|
|
1321
1793
|
async execute (args, _exec) {
|
|
1322
|
-
const q =
|
|
1794
|
+
const q = args;
|
|
1323
1795
|
if (!q.query) {
|
|
1324
1796
|
return JSON.parse(JSON.stringify({
|
|
1325
1797
|
ok: false,
|
|
1326
1798
|
summary: 'query 必填'
|
|
1327
1799
|
}));
|
|
1328
1800
|
}
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1801
|
+
const itpG = await interpretEffect({
|
|
1802
|
+
effect: 'GITHUB_SEARCH',
|
|
1803
|
+
params: {
|
|
1804
|
+
query: q.query,
|
|
1805
|
+
limit: q.limit
|
|
1806
|
+
}
|
|
1332
1807
|
});
|
|
1808
|
+
if (!itpG.result) return declinedObservation('GITHUB_SEARCH', itpG.trace);
|
|
1809
|
+
const r = itpG.result;
|
|
1333
1810
|
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}`;
|
|
1334
1811
|
return JSON.parse(JSON.stringify({
|
|
1335
1812
|
ok: r.ok,
|
|
@@ -1343,19 +1820,34 @@ export function apply(ctx, config) {
|
|
|
1343
1820
|
},
|
|
1344
1821
|
presentCall: (args)=>({
|
|
1345
1822
|
card: 'generic',
|
|
1346
|
-
title: `GitHub 搜索:${
|
|
1823
|
+
title: `GitHub 搜索:${args.query ?? ''}`,
|
|
1347
1824
|
kind: 'search',
|
|
1348
|
-
rawInput: args
|
|
1825
|
+
rawInput: args
|
|
1349
1826
|
})
|
|
1350
1827
|
}));
|
|
1351
1828
|
registerGuarded(defineTool({
|
|
1352
1829
|
name: 'gotry_agent_reach',
|
|
1353
1830
|
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].',
|
|
1354
1831
|
parameters: {
|
|
1355
|
-
|
|
1356
|
-
type: '
|
|
1357
|
-
|
|
1358
|
-
|
|
1832
|
+
action: {
|
|
1833
|
+
type: 'string',
|
|
1834
|
+
description: 'status(默认,无 channel 时)或 reach'
|
|
1835
|
+
},
|
|
1836
|
+
channel: {
|
|
1837
|
+
type: 'string',
|
|
1838
|
+
description: '上游渠道名,如 web/v2ex/xueqiu(action=reach 时)'
|
|
1839
|
+
},
|
|
1840
|
+
method: {
|
|
1841
|
+
type: 'string',
|
|
1842
|
+
description: '上游方法名,如 read/get_hot_topics/get_stock_quote'
|
|
1843
|
+
},
|
|
1844
|
+
args: {
|
|
1845
|
+
type: 'string',
|
|
1846
|
+
description: '空格分隔参数'
|
|
1847
|
+
},
|
|
1848
|
+
timeoutMs: {
|
|
1849
|
+
type: 'integer',
|
|
1850
|
+
description: '超时毫秒'
|
|
1359
1851
|
}
|
|
1360
1852
|
},
|
|
1361
1853
|
output: {
|
|
@@ -1370,13 +1862,24 @@ export function apply(ctx, config) {
|
|
|
1370
1862
|
]
|
|
1371
1863
|
},
|
|
1372
1864
|
async execute (args, _exec) {
|
|
1373
|
-
const q = unwrapQuery(args
|
|
1865
|
+
const q = unwrapQuery(args);
|
|
1374
1866
|
const started = Date.now();
|
|
1375
1867
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1868
|
+
const itpAR = await interpretEffect({
|
|
1869
|
+
effect: 'AGENT_REACH',
|
|
1870
|
+
params: {
|
|
1871
|
+
action: q.action,
|
|
1872
|
+
channel: q.channel,
|
|
1873
|
+
method: q.method,
|
|
1874
|
+
args: q.args,
|
|
1875
|
+
timeoutMs: q.timeoutMs
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
if (!itpAR.result) return declinedObservation('AGENT_REACH', itpAR.trace);
|
|
1376
1879
|
if (q.action === 'status' || !q.action && !q.channel) {
|
|
1377
|
-
const st =
|
|
1880
|
+
const st = itpAR.result;
|
|
1378
1881
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(()=>{});
|
|
1379
|
-
const summary = st.via === 'agent-reach-cli' ? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}` : `Agent Reach
|
|
1882
|
+
const summary = st.via === 'agent-reach-cli' ? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}` : `Agent Reach 未装配(.venv 缺失)——可选依赖,机/酒/网页社媒读取之外的工具不受影响。\n补装:终端跑 npx gotry doctor --fix;整体依赖状态可调 gotry_doctor 工具查看。\n${st.evidence}`;
|
|
1380
1883
|
return JSON.parse(JSON.stringify({
|
|
1381
1884
|
ok: st.ok,
|
|
1382
1885
|
via: st.via,
|
|
@@ -1392,12 +1895,7 @@ export function apply(ctx, config) {
|
|
|
1392
1895
|
summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表'
|
|
1393
1896
|
}));
|
|
1394
1897
|
}
|
|
1395
|
-
const r =
|
|
1396
|
-
channel: q.channel,
|
|
1397
|
-
method: q.method,
|
|
1398
|
-
args: q.args,
|
|
1399
|
-
timeoutMs: q.timeoutMs
|
|
1400
|
-
});
|
|
1898
|
+
const r = itpAR.result;
|
|
1401
1899
|
const dataStr = (v)=>{
|
|
1402
1900
|
if (Array.isArray(v)) {
|
|
1403
1901
|
const kept = [];
|
|
@@ -1416,7 +1914,7 @@ export function apply(ctx, config) {
|
|
|
1416
1914
|
};
|
|
1417
1915
|
if (r.ok) r.data = dataStr(r.data);
|
|
1418
1916
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${q.channel}.${q.method}:${r.verdict}`).catch(()=>{});
|
|
1419
|
-
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} →
|
|
1917
|
+
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整体依赖状态可调 gotry_doctor 查看;补装: npx gotry doctor --fix\n${r.evidence}` : `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`;
|
|
1420
1918
|
return JSON.parse(JSON.stringify({
|
|
1421
1919
|
ok: r.ok,
|
|
1422
1920
|
channel: r.channel,
|
|
@@ -1432,13 +1930,13 @@ export function apply(ctx, config) {
|
|
|
1432
1930
|
},
|
|
1433
1931
|
presentCall: (args)=>({
|
|
1434
1932
|
card: 'generic',
|
|
1435
|
-
title: `Agent Reach:${String(args.
|
|
1933
|
+
title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`,
|
|
1436
1934
|
kind: 'fetch',
|
|
1437
|
-
rawInput: args
|
|
1935
|
+
rawInput: args
|
|
1438
1936
|
}),
|
|
1439
1937
|
presentResult: (args, value)=>{
|
|
1440
1938
|
const r = value;
|
|
1441
|
-
const q = args
|
|
1939
|
+
const q = args ?? {};
|
|
1442
1940
|
const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌';
|
|
1443
1941
|
return {
|
|
1444
1942
|
card: 'generic',
|
|
@@ -1452,14 +1950,81 @@ export function apply(ctx, config) {
|
|
|
1452
1950
|
};
|
|
1453
1951
|
}
|
|
1454
1952
|
}));
|
|
1953
|
+
registerGuarded(defineTool({
|
|
1954
|
+
name: 'gotry_doctor',
|
|
1955
|
+
description: 'Check optional-dependency health for ALL gotry tools (read-only, never installs): GoTry Session Bridge extension / Agent Reach (.venv) / hbcli (hotel realtime) / FlyAI key + recent trial-quota exhaustion time / dsh-calendar mount state / dsh-better-sidebar. ' + 'Call this when ANY gotry tool returns not-installed / needs-setup, when the user asks 「体检/依赖状态/工具为什么不可用」, or BEFORE leaning on a channel for a plan. ' + 'Returns per-item status (ok/degraded/missing) with exact fix commands. ' + 'Repair = `npx gotry doctor --fix` run BY THE USER in a terminal (this tool never installs anything); LLM keys are the dsh host\'s business and are deliberately out of scope. ' + 'A markdown report is rendered for the workspace (gotry-state/doctor-report.md) so the sidebar workbench can preview it.',
|
|
1956
|
+
parameters: {
|
|
1957
|
+
writeReport: {
|
|
1958
|
+
type: 'boolean',
|
|
1959
|
+
description: '默认 true,把 markdown 报告写进 gotry-state/doctor-report.md(侧栏工作台可预览)'
|
|
1960
|
+
}
|
|
1961
|
+
},
|
|
1962
|
+
output: {
|
|
1963
|
+
schema: {
|
|
1964
|
+
type: 'json'
|
|
1965
|
+
},
|
|
1966
|
+
render: (_args, value)=>[
|
|
1967
|
+
{
|
|
1968
|
+
type: 'text',
|
|
1969
|
+
text: String(value.summary ?? JSON.stringify(value).slice(0, 900))
|
|
1970
|
+
}
|
|
1971
|
+
]
|
|
1972
|
+
},
|
|
1973
|
+
async execute (args, _exec) {
|
|
1974
|
+
const q = unwrapQuery(args, 'writeReport');
|
|
1975
|
+
const report = await runDoctorChecks({
|
|
1976
|
+
stateRoot: config.stateRoot ?? '.'
|
|
1977
|
+
});
|
|
1978
|
+
let reportPath;
|
|
1979
|
+
if (q?.writeReport !== false) {
|
|
1980
|
+
try {
|
|
1981
|
+
const { writeFile } = await import('node:fs/promises');
|
|
1982
|
+
const dir = await ensureStateDir(config.stateRoot);
|
|
1983
|
+
reportPath = join(dir, 'doctor-report.md');
|
|
1984
|
+
await writeFile(reportPath, renderDoctorReportMd(report), 'utf-8');
|
|
1985
|
+
} catch {
|
|
1986
|
+
reportPath = undefined;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const icon = (s)=>s === 'ok' ? '✅' : s === 'degraded' ? '⚠️' : '❌';
|
|
1990
|
+
const lines = report.items.map((i)=>`${icon(i.status)} ${i.label}:${i.detail}${i.fix ? `\n ↳ 修复: ${i.fix}` : ''}`);
|
|
1991
|
+
const summary = `${report.summary}\n${lines.join('\n')}${reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)'}`;
|
|
1992
|
+
return JSON.parse(JSON.stringify({
|
|
1993
|
+
ok: true,
|
|
1994
|
+
verdict: report.ok ? 'all-clear' : 'needs-attention',
|
|
1995
|
+
items: report.items,
|
|
1996
|
+
report_path: reportPath,
|
|
1997
|
+
evidence: `[doctor@${new Date().toISOString()}]`,
|
|
1998
|
+
summary
|
|
1999
|
+
}));
|
|
2000
|
+
},
|
|
2001
|
+
presentCall: (_args)=>({
|
|
2002
|
+
card: 'generic',
|
|
2003
|
+
title: '🩺 依赖体检',
|
|
2004
|
+
kind: 'execute',
|
|
2005
|
+
rawInput: {}
|
|
2006
|
+
}),
|
|
2007
|
+
presentResult: (_args, value)=>{
|
|
2008
|
+
const r = value;
|
|
2009
|
+
return {
|
|
2010
|
+
card: 'generic',
|
|
2011
|
+
title: `🩺 依赖体检:${r.verdict === 'all-clear' ? '✅ 全部就绪' : '🔧 有项待处理'}`,
|
|
2012
|
+
content: [
|
|
2013
|
+
{
|
|
2014
|
+
type: 'text',
|
|
2015
|
+
text: String(r.summary ?? '')
|
|
2016
|
+
}
|
|
2017
|
+
]
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
}));
|
|
1455
2021
|
registerGuarded(defineTool({
|
|
1456
2022
|
name: 'gotry_artifacts_list',
|
|
1457
2023
|
description: 'List GoTry artifacts — deep-planning deliverables (async runs from the state ledger) plus agent-written markdown files ' + 'in the working directory (trip plans etc.). READ-ONLY discovery. ' + 'Use when the user asks to see/open/revisit a previously generated artifact ' + '(「看看刚才生成的行程」「上次的规划在哪」「打开那个 md」) — list first, then read with gotry_artifacts_read.',
|
|
1458
2024
|
parameters: {
|
|
1459
|
-
|
|
1460
|
-
type: '
|
|
1461
|
-
|
|
1462
|
-
description: '{ limit?: 20 }'
|
|
2025
|
+
limit: {
|
|
2026
|
+
type: 'integer',
|
|
2027
|
+
description: '最多返回条数,默认 20'
|
|
1463
2028
|
}
|
|
1464
2029
|
},
|
|
1465
2030
|
output: {
|
|
@@ -1512,10 +2077,18 @@ export function apply(ctx, config) {
|
|
|
1512
2077
|
name: 'gotry_artifacts_read',
|
|
1513
2078
|
description: 'Read one GoTry artifact as a line-numbered file view rendered directly in the chat UI. ' + 'Input: the path from gotry_artifacts_list, or a bare async ticket id (e.g. dp-xxxx). ' + 'Optional offset (1-based) / limit window for paging large files. ' + 'READ-ONLY; text artifacts only (md/txt/json/jsonl/csv/log/yaml).',
|
|
1514
2079
|
parameters: {
|
|
1515
|
-
|
|
1516
|
-
type: '
|
|
2080
|
+
path: {
|
|
2081
|
+
type: 'string',
|
|
1517
2082
|
required: true,
|
|
1518
|
-
description: '
|
|
2083
|
+
description: 'list 返回的路径或工单 id'
|
|
2084
|
+
},
|
|
2085
|
+
offset: {
|
|
2086
|
+
type: 'integer',
|
|
2087
|
+
description: '起始行号(1 起)'
|
|
2088
|
+
},
|
|
2089
|
+
limit: {
|
|
2090
|
+
type: 'integer',
|
|
2091
|
+
description: '窗口行数,默认 400'
|
|
1519
2092
|
}
|
|
1520
2093
|
},
|
|
1521
2094
|
output: {
|
|
@@ -1530,7 +2103,8 @@ export function apply(ctx, config) {
|
|
|
1530
2103
|
]
|
|
1531
2104
|
},
|
|
1532
2105
|
async execute (args, _exec) {
|
|
1533
|
-
const q =
|
|
2106
|
+
const q = args;
|
|
2107
|
+
if (!q.path) throw new Error('gotry_artifacts_read requires path');
|
|
1534
2108
|
if (!q.path) return JSON.parse(JSON.stringify({
|
|
1535
2109
|
ok: false,
|
|
1536
2110
|
error: 'path 必填(来自 gotry_artifacts_list)'
|
|
@@ -1549,9 +2123,9 @@ export function apply(ctx, config) {
|
|
|
1549
2123
|
},
|
|
1550
2124
|
presentCall: (args)=>({
|
|
1551
2125
|
card: 'generic',
|
|
1552
|
-
title: `读产物:${
|
|
2126
|
+
title: `读产物:${args.path ?? ''}`,
|
|
1553
2127
|
kind: 'read',
|
|
1554
|
-
rawInput: args
|
|
2128
|
+
rawInput: args
|
|
1555
2129
|
}),
|
|
1556
2130
|
presentResult: (_args, value)=>{
|
|
1557
2131
|
const r = value;
|
|
@@ -1588,10 +2162,22 @@ export function apply(ctx, config) {
|
|
|
1588
2162
|
name: 'gotry_fact_gate',
|
|
1589
2163
|
description: 'Factuality gate for itinerary artifacts (issue #46): call BEFORE delivering any artifact that contains bookable facts ' + '(flight numbers, times, airports, prices, visa/entry policies). Every bookable claim in the markdown is checked against ' + 'the session fact registry (exact-date tool results recorded by gotry_flyai_search / gotry_session_search — hit AND miss). ' + 'Blocked when: a claimed flight is absent from the exact-date source (never backfill from route pages/history/adjacent dates); ' + 'route+date never queried; times contradict the snapshot; carrier→airport mapping conflicts (FD=DMK vs VZ=BKK are NOT interchangeable); ' + '「联程」without protected_connection=true; policy claims without 「截至 YYYY-MM-DD」; unconditional ✓ on unverified claims. ' + 'Optional itinerary object enables machine invariants (hotel_nights + onboard_nights = total nights, O&D segments vs flight legs, ' + 'budget floor ≥ sum of item minimums). verdict=blocked ⇒ fix or downgrade wording — never present as a verified plan.',
|
|
1590
2164
|
parameters: {
|
|
1591
|
-
|
|
1592
|
-
type: '
|
|
1593
|
-
|
|
1594
|
-
|
|
2165
|
+
markdown: {
|
|
2166
|
+
type: 'string',
|
|
2167
|
+
description: '产物全文;与 path 二选一'
|
|
2168
|
+
},
|
|
2169
|
+
path: {
|
|
2170
|
+
type: 'string',
|
|
2171
|
+
description: '产物路径(artifacts_list 返回的);与 markdown 二选一'
|
|
2172
|
+
},
|
|
2173
|
+
tripYear: {
|
|
2174
|
+
type: 'integer',
|
|
2175
|
+
description: '行程年份,如 2027(日期归一用)'
|
|
2176
|
+
},
|
|
2177
|
+
itinerary: {
|
|
2178
|
+
type: 'object',
|
|
2179
|
+
additionalProperties: true,
|
|
2180
|
+
description: '结构化行程 { trip_start, trip_end, stays:[{place,check_in,check_out}], onboard_nights, od_segments:[{from,to,date,mode,legs}], budget_items:[{label,min_cny,max_cny}], claimed_floor_cny? }'
|
|
1595
2181
|
}
|
|
1596
2182
|
},
|
|
1597
2183
|
output: {
|
|
@@ -1648,7 +2234,7 @@ export function apply(ctx, config) {
|
|
|
1648
2234
|
card: 'generic',
|
|
1649
2235
|
title: '产物事实闸',
|
|
1650
2236
|
kind: 'execute',
|
|
1651
|
-
rawInput: args
|
|
2237
|
+
rawInput: args
|
|
1652
2238
|
}),
|
|
1653
2239
|
presentResult: (_args, value)=>{
|
|
1654
2240
|
const r = value;
|