@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/ts/src/index.ts
CHANGED
|
@@ -31,19 +31,21 @@ import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, r
|
|
|
31
31
|
import { buildTimeAnchor } from './time-anchor.ts'
|
|
32
32
|
import { resolveSlotDate } from './slot-spec.ts'
|
|
33
33
|
import { wmoLabel } from '../capabilities/weather.ts'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
34
|
+
// D-23 收编(issue #115):anything/web/github/video/agent_reach/session_login 六渠道改经
|
|
35
|
+
// 效应注册表(退避/熔断/declined 面统一);能力层直接 import 仅留类型位(reach/reachStatus)
|
|
36
|
+
import { reach, reachStatus } from '../capabilities/agent-reach.ts'
|
|
37
|
+
import { runDoctorChecks, renderDoctorReportMd } from '../capabilities/doctor.ts'
|
|
38
38
|
import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.ts'
|
|
39
39
|
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.ts'
|
|
40
40
|
import { installModelOverride } from '../capabilities/model-override.ts'
|
|
41
41
|
import { listArtifacts, readArtifact } from '../capabilities/artifacts.ts'
|
|
42
42
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.ts'
|
|
43
43
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.ts'
|
|
44
|
-
import { factsFromFlyai, factsFromSession } from './bookable-facts.ts'
|
|
44
|
+
import { factsFromFlyai, factsFromHotel, factsFromSession } from './bookable-facts.ts'
|
|
45
45
|
import { gateArtifact, type AirlineAirportMap } from './artifact-gate.ts'
|
|
46
|
-
import {
|
|
46
|
+
import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.ts'
|
|
47
|
+
import { noteChannelVerdict, recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.ts'
|
|
48
|
+
import { routingAdvice, renderRoutingCard, toolRoutingHeadline, type ChannelIntent } from '../capabilities/channel-registry.ts'
|
|
47
49
|
import { registerBenchmarkEnvironmentBridge, type BenchmarkSubprocessService } from './benchmark-environment-bridge.ts'
|
|
48
50
|
import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.ts'
|
|
49
51
|
import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.ts'
|
|
@@ -163,15 +165,44 @@ function readTimelineTrips(stateRoot: string): Array<{ destination: string; star
|
|
|
163
165
|
type Json = string | number | boolean | null | Json[] | { [k: string]: Json }
|
|
164
166
|
type JsonObject = { [k: string]: Json }
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
/** Benchmark 治理面钉死预算(Round 9,#100/#102):官方任务形态(如 ChinaTravel
|
|
169
|
+
* 单查询 300s)超出内置 60/120s;预算仍必须「按 run 钉死」保证可复现——允许
|
|
170
|
+
* owner 经 env 按次显式设定,缺省维持既有值,非法值启动即抛。 */
|
|
171
|
+
function benchmarkPinnedPolicy(): { softMs: number; hardMs: number; exit: 'converge' } {
|
|
172
|
+
const parse = (name: string, fallback: number): number => {
|
|
173
|
+
const raw = process.env[name]
|
|
174
|
+
if (raw === undefined || raw === '') return fallback
|
|
175
|
+
const value = Number.parseInt(raw, 10)
|
|
176
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
177
|
+
throw new Error(`invalid ${name}: ${JSON.stringify(raw)} (expected a positive safe integer)`)
|
|
178
|
+
}
|
|
179
|
+
return value
|
|
180
|
+
}
|
|
181
|
+
const softMs = parse('GOTRY_BENCHMARK_SOFT_MS', 60_000)
|
|
182
|
+
const hardMs = parse('GOTRY_BENCHMARK_HARD_MS', 120_000)
|
|
183
|
+
if (hardMs <= softMs) throw new Error(`GOTRY_BENCHMARK_HARD_MS (${hardMs}) must exceed GOTRY_BENCHMARK_SOFT_MS (${softMs})`)
|
|
184
|
+
return { softMs, hardMs, exit: 'converge' }
|
|
185
|
+
}
|
|
168
186
|
|
|
187
|
+
export function apply(ctx: Context, config: Config): void {
|
|
169
188
|
const rawBenchmarkEnvironmentConfigPath = config.benchmarkEnvironmentConfigPath ?? ''
|
|
189
|
+
// ADR-24 v2:产品路径装「路由 + wall-clock 双出口」——用户主观时间是唯一
|
|
190
|
+
// 预算,复杂度决定出口结构(converge/handoff)。benchmark opt-in 钉死
|
|
191
|
+
// 固定 policy 保证评测可复现,不走路由。GOTRY_HANDOFF_CHILD=1 是收集器
|
|
192
|
+
// 派生的后台规划会话:唯一出口 converge + 长 leash——前台转后台,后台
|
|
193
|
+
// 必须产出最终交付物,不得再次 handoff(否则无限递归)。
|
|
194
|
+
const handoffChild = process.env.GOTRY_HANDOFF_CHILD === '1'
|
|
195
|
+
installTurnDeadline(ctx, handoffChild
|
|
196
|
+
? { fixedPolicy: { softMs: 300_000, hardMs: 900_000, exit: 'converge' } }
|
|
197
|
+
: rawBenchmarkEnvironmentConfigPath.trim()
|
|
198
|
+
? { fixedPolicy: benchmarkPinnedPolicy() }
|
|
199
|
+
: { stateRoot: process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.' })
|
|
170
200
|
if (rawBenchmarkEnvironmentConfigPath.trim()) {
|
|
171
201
|
// Benchmark mode is a deliberately minimal kernel: only the model
|
|
172
|
-
// override and the environment bridge are installed
|
|
173
|
-
//
|
|
174
|
-
// tools must not be
|
|
202
|
+
// override and the environment bridge are installed besides the pinned
|
|
203
|
+
// turn-deadline policy above. In particular, product prompt variables,
|
|
204
|
+
// process/consent guards, and guarded product tools must not be
|
|
205
|
+
// observable on this path.
|
|
175
206
|
installModelOverride(ctx as unknown as Parameters<typeof installModelOverride>[0])
|
|
176
207
|
const getService = (ctx as unknown as { get?: (name: string, fallback?: unknown) => unknown }).get
|
|
177
208
|
const directSubprocess = (typeof getService === 'function'
|
|
@@ -207,6 +238,21 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
207
238
|
// 渲染成紧凑 brief 注入 persona;为空 = 首访。与当轮说法冲突时以用户为准。
|
|
208
239
|
sp?.variable?.('motivation_brief', () => renderMotivationBrief(config.stateRoot ?? '.'))
|
|
209
240
|
|
|
241
|
+
// 通道路由卡(通道注册表生成,docs/design/tool-orchestration-design.md §2.1/D-8):
|
|
242
|
+
// persona (19) 只留行为契约,机/火/酒通道顺位与额度口径查卡——prose 教义
|
|
243
|
+
// 变查表教义,注册表加通道卡片自动一致。
|
|
244
|
+
sp?.variable?.('channel_routing_card', () => renderRoutingCard())
|
|
245
|
+
|
|
246
|
+
// 通道健康面接线(issue #107/#108):检索通道 verdict → 会话状态标记 +
|
|
247
|
+
// down/cooldown 事件落持久面(doctor CLI 进程消费)。落盘失败不阻塞检索。
|
|
248
|
+
const noteChannel = async (channel: string, verdict: string): Promise<void> => {
|
|
249
|
+
const ev = noteChannelVerdict(channel, verdict)
|
|
250
|
+
if (ev) await recordChannelEvent(config.stateRoot ?? '.', ev)
|
|
251
|
+
}
|
|
252
|
+
/** verdict≠hit 时在平铺 envelope 上追加 routing 建议(§3.3:失败现场教学) */
|
|
253
|
+
const routingField = (intent: ChannelIntent, excludeChannel: string) =>
|
|
254
|
+
({ routing: routingAdvice(intent, { excludeChannel }) })
|
|
255
|
+
|
|
210
256
|
// D-NEW 进程护栏(Z3 WASM crash 教训):dsh 0.1.1-rc.1 缺 uncaughtException
|
|
211
257
|
// handler,插件异常穿透即杀进程。我们在 gotry 侧挂护栏:同步 fsync 写事故证据
|
|
212
258
|
// (gotry-state/incidents.jsonl),handler 自身不再抛,不阻塞后续控制流。
|
|
@@ -242,6 +288,14 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
242
288
|
ctx.tools.register(t as unknown as ReturnType<typeof defineTool>)
|
|
243
289
|
}
|
|
244
290
|
|
|
291
|
+
// 工具描述首行生成(issue #113,L1):检索工具描述统一前置注册表生成的
|
|
292
|
+
// 「服务意图 × 通道顺位」卡——模型选工具时(读描述)与失败后(读 routing)
|
|
293
|
+
// 两个决策点拿到同一张表;注册表加行,描述自动一致。非检索工具原样返回。
|
|
294
|
+
const routed = (tool: string, desc: string): string => {
|
|
295
|
+
const head = toolRoutingHeadline(tool)
|
|
296
|
+
return head ? `${head}\n${desc}` : desc
|
|
297
|
+
}
|
|
298
|
+
|
|
245
299
|
registerGuarded(defineTool({
|
|
246
300
|
name: 'gotry_feasibility_check',
|
|
247
301
|
description:
|
|
@@ -252,11 +306,17 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
252
306
|
+ 'structure { request: { motivation weights, hard constraints, window, budget, home hubs }, candidates: [ { id, label, services, transfers, stay, minDays } ] }. '
|
|
253
307
|
+ 'Returns per-candidate verdicts, unsat cores with minimal-modification suggestions, '
|
|
254
308
|
+ 'a wish-pool entry for infeasible aspirations, and a ready-to-show markdown answer.',
|
|
309
|
+
// D-30 第五刀(issue #112):payload json blob → 结构化对象(request/candidates 模型可见,
|
|
310
|
+
// 嵌套形状权威在 unified 引擎契约,工具面不越层强约束)
|
|
255
311
|
parameters: {
|
|
256
312
|
payload: {
|
|
257
|
-
type: '
|
|
313
|
+
type: 'object',
|
|
314
|
+
additionalProperties: false,
|
|
258
315
|
required: true,
|
|
259
|
-
|
|
316
|
+
properties: {
|
|
317
|
+
request: { type: 'object', additionalProperties: true, required: true, description: '引擎请求:{ motivation weights, hard constraints, window, budget, home hubs }' },
|
|
318
|
+
candidates: { type: 'array', items: { type: 'object', additionalProperties: true }, required: true, description: '候选列表:[{ id, label, services, transfers, stay, minDays }]' },
|
|
319
|
+
},
|
|
260
320
|
},
|
|
261
321
|
},
|
|
262
322
|
output: {
|
|
@@ -309,11 +369,18 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
309
369
|
+ 'never the principal/sponsor distinction. MERGE semantics (T1): call again with just the NEW '
|
|
310
370
|
+ 'facts learned this turn (weights delta optional but MUST bring fresh evidence; evidence = user quotes); '
|
|
311
371
|
+ 'existing history is never deleted. Requires evidence on every call (P0 anti-fabrication rule).',
|
|
372
|
+
// D-30 第五刀(issue #112):profile blob → 结构化闭合对象;evidence(P0 反伪造红线)进嵌套
|
|
373
|
+
// schema 由宿主权闸——无证据的保存请求入口即被结构化拒绝(ToolFailure 形状)
|
|
312
374
|
parameters: {
|
|
313
375
|
profile: {
|
|
314
|
-
type: '
|
|
376
|
+
type: 'object',
|
|
377
|
+
additionalProperties: false,
|
|
315
378
|
required: true,
|
|
316
|
-
|
|
379
|
+
properties: {
|
|
380
|
+
weights: { type: 'object', additionalProperties: true, description: '动机权重增量,如 { escape_rest: 0.7 }(可选,权重变更须伴新证据)' },
|
|
381
|
+
evidence: { type: 'array', items: { type: 'string' }, required: true, description: '本轮新事实的用户原话数组(P0:必带)' },
|
|
382
|
+
hard: { type: 'object', additionalProperties: true, description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)' },
|
|
383
|
+
},
|
|
317
384
|
},
|
|
318
385
|
},
|
|
319
386
|
output: {
|
|
@@ -351,7 +418,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
351
418
|
entry: {
|
|
352
419
|
type: 'json',
|
|
353
420
|
required: true,
|
|
354
|
-
description: '{ name, reason?, conditions: { days, budget_cny, best_months }, muted?: boolean }',
|
|
421
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months, channels? }, muted?: boolean }',
|
|
355
422
|
},
|
|
356
423
|
},
|
|
357
424
|
output: {
|
|
@@ -379,20 +446,25 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
379
446
|
'Surface AT MOST ONE "next departure" wish whose fulfilment conditions match the user\'s current window '
|
|
380
447
|
+ '(0..1 rule: never more than one nudge per turn, never push when nothing matches — 憧憬不被拒绝,也不被硬推). '
|
|
381
448
|
+ 'Muted wishes never surface. Surfacing records a recalled event in the memory-utility sidecar. '
|
|
449
|
+
+ 'A wish whose conditions.channels names a channel that is currently down (out-of-band health facts) is vetoed for this recall — 不推当下走不通的憧憬. '
|
|
382
450
|
+ 'action="confirm-outcome" records the user-confirmed real-world outcome (attribution helpful/harmful/neutral) — '
|
|
383
451
|
+ 'ONLY pass attribution the user explicitly stated; the agent must never self-attribute usefulness.',
|
|
452
|
+
// D-30 第四刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
384
453
|
parameters: {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
},
|
|
454
|
+
action: { type: 'string', description: 'recall(默认,召回下一次出发建议)或 confirm-outcome(确认某条建议成/不成)' },
|
|
455
|
+
days: { type: 'integer', description: '回看天数窗(recall)' },
|
|
456
|
+
budgetCny: { type: 'number', description: '预算上限(recall 过滤)' },
|
|
457
|
+
month: { type: 'integer', description: '出行月份 1-12(recall)' },
|
|
458
|
+
wishId: { type: 'string', description: 'confirm-outcome 目标愿望 id' },
|
|
459
|
+
attribution: { type: 'string', enum: ['helpful', 'harmful', 'neutral'], description: 'confirm-outcome 归因' },
|
|
460
|
+
detail: { type: 'string', description: '一句话细节(用户原话)' },
|
|
461
|
+
tripStart: { type: 'string', description: '确认成行时挂时间线的行程起始日' },
|
|
390
462
|
},
|
|
391
463
|
output: {
|
|
392
464
|
schema: { type: 'json' },
|
|
393
465
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }],
|
|
394
466
|
},
|
|
395
|
-
async execute(args
|
|
467
|
+
async execute(args, _exec: unknown) {
|
|
396
468
|
const q = unwrapQuery<{ action?: string; days?: number; budgetCny?: number; month?: number; wishId?: string; attribution?: 'helpful' | 'harmful' | 'neutral'; detail?: string; tripStart?: string }>(args, 'action')
|
|
397
469
|
const ledger = ensureLedger(config.stateRoot)
|
|
398
470
|
const now = new Date().toISOString()
|
|
@@ -416,11 +488,20 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
416
488
|
const r = ledger.confirmOutcome({ wishId: q.wishId, attribution: q.attribution, detail: q.detail, trip: tripInput })
|
|
417
489
|
return { ok: true, recorded: r.recorded, wish_id: q.wishId, status: q.attribution, ...(r.trip ? { trip: r.trip } : {}) } as never
|
|
418
490
|
}
|
|
419
|
-
// recall:0..1 条件匹配(判定归 wish-pool 纯函数),muted
|
|
491
|
+
// recall:0..1 条件匹配(判定归 wish-pool 纯函数),muted 永不召回,无命中不硬推;
|
|
492
|
+
// 通道否证(接缝第 2 段):持久健康面里处于 down 的通道集合进 context——
|
|
493
|
+
// conditions.channels 命名依赖通道的愿望在被否证时不当场推荐(读不到健康面=不启用)。
|
|
420
494
|
const pool = ledger.readWishPool()
|
|
421
495
|
const candidates = pool.filter(e => !e.muted && typeof e.wish_id === 'string')
|
|
422
496
|
const month = q.month ?? new Date().getMonth() + 1
|
|
423
|
-
|
|
497
|
+
let channelDown: ReadonlySet<string> | undefined
|
|
498
|
+
try {
|
|
499
|
+
const latest = await readLatestChannelEvents(config.stateRoot)
|
|
500
|
+
const down = new Set<string>()
|
|
501
|
+
for (const [ch, ev] of latest) if (ev.state === 'down') down.add(ch)
|
|
502
|
+
channelDown = down
|
|
503
|
+
} catch { /* 健康面缺席=不启用否证,召回行为与旧版一致 */ }
|
|
504
|
+
const match = pickNudgeWish(candidates as WishPoolEntry[], { days: q.days, budgetCny: q.budgetCny, month, channelDown })
|
|
424
505
|
if (!match) {
|
|
425
506
|
return { ok: true, suggestion: null, summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)` } as never
|
|
426
507
|
}
|
|
@@ -435,7 +516,52 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
435
516
|
summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`,
|
|
436
517
|
} as never
|
|
437
518
|
},
|
|
438
|
-
presentCall: args => ({ card: 'generic', title: '「下一次出发」召回', kind: 'search', rawInput: args
|
|
519
|
+
presentCall: args => ({ card: 'generic', title: '「下一次出发」召回', kind: 'search', rawInput: args }),
|
|
520
|
+
}))
|
|
521
|
+
|
|
522
|
+
// ADR-24 v2 复访交付面:用户回问「行程规划好了吗」时,模型用这个只读工具
|
|
523
|
+
// 查后台工单状态;settled 附交付物摘录(完整内容在工单同目录
|
|
524
|
+
// .deliverable.md)。工单由 scripts/turn-handoff-collect.ts 收集结算。
|
|
525
|
+
registerGuarded(defineTool({
|
|
526
|
+
name: 'gotry_turn_handoff_list',
|
|
527
|
+
description:
|
|
528
|
+
'List background deep-planning handoff tickets (read-only). Call this whenever the user asks about '
|
|
529
|
+
+ 'a previously handed-off plan («规划好了吗» / «上次那个行程»): open = still being worked in the '
|
|
530
|
+
+ 'background (ETA ' + '约 1 小时' + '), settled = deliverable ready (excerpt included, full text in the '
|
|
531
|
+
+ 'ticket\'s .deliverable.md), failed = honest failure note. Never fabricate a deliverable that is not here.',
|
|
532
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层(同 session 刀法)
|
|
533
|
+
parameters: {
|
|
534
|
+
ticketId: { type: 'string', description: '只看这一单,如 th-...(缺省列全部)' },
|
|
535
|
+
},
|
|
536
|
+
output: {
|
|
537
|
+
schema: { type: 'json' },
|
|
538
|
+
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }],
|
|
539
|
+
},
|
|
540
|
+
async execute(args, _exec: unknown) {
|
|
541
|
+
const q = interpretArgs<{ ticketId?: string }>(args)
|
|
542
|
+
const root = process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.'
|
|
543
|
+
const all = await listTurnHandoffTickets(root)
|
|
544
|
+
const tickets = (q.ticketId ? all.filter(t => t.id === q.ticketId) : all).slice(0, 10)
|
|
545
|
+
if (tickets.length === 0) {
|
|
546
|
+
return {
|
|
547
|
+
ok: true, count: 0,
|
|
548
|
+
summary: q.ticketId
|
|
549
|
+
? `没有 id 为 ${q.ticketId} 的后台规划工单(可能已被清理或记错 id)`
|
|
550
|
+
: '当前没有后台深度规划工单(无挂起、无历史)',
|
|
551
|
+
} as never
|
|
552
|
+
}
|
|
553
|
+
const lines = tickets.map(t => {
|
|
554
|
+
const head = `[${t.status}] ${t.id} ${t.objective.slice(0, 40)}`
|
|
555
|
+
if (t.status === 'open') return `${head}——后台规划中,ETA ${t.etaLabel}`
|
|
556
|
+
if (t.status === 'failed') return `${head}——失败:${t.error ?? '未知原因'}(请重新发起规划)`
|
|
557
|
+
return `${head}——已交付,摘要:${(t.deliverableExcerpt ?? '').slice(0, 120).replace(/\n/g, ' ')}`
|
|
558
|
+
})
|
|
559
|
+
return {
|
|
560
|
+
ok: true, count: tickets.length, tickets,
|
|
561
|
+
summary: `后台规划工单 ${tickets.length} 张:\n${lines.join('\n')}`,
|
|
562
|
+
} as never
|
|
563
|
+
},
|
|
564
|
+
presentCall: args => ({ card: 'generic', title: '后台规划工单查询', kind: 'search', rawInput: args }),
|
|
439
565
|
}))
|
|
440
566
|
|
|
441
567
|
registerGuarded(defineTool({
|
|
@@ -445,11 +571,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
445
571
|
+ 'never a hard filter (「爸爸65轻度高血压」→ 不排高海拔/控制步行量;「晕车」→ 优先火车/备提示). '
|
|
446
572
|
+ 'evidence MUST be the user\'s verbatim words (append-only, traceable). '
|
|
447
573
|
+ 'NEGATIVE LIST: passport/ID/phone numbers are rejected on sight — such fields never enter storage.',
|
|
574
|
+
// D-30 第五刀(issue #112):companion blob → 结构化闭合对象;label/evidence required 由宿主权闸
|
|
575
|
+
// (evidence append-only 溯源 P0;additionalProperties:false 与负面清单同向——护照/证件号类
|
|
576
|
+
// 杂散字段入口即拒)
|
|
448
577
|
parameters: {
|
|
449
578
|
companion: {
|
|
450
|
-
type: '
|
|
579
|
+
type: 'object',
|
|
580
|
+
additionalProperties: false,
|
|
451
581
|
required: true,
|
|
452
|
-
|
|
582
|
+
properties: {
|
|
583
|
+
label: { type: 'string', required: true, description: '称呼,如 爸爸' },
|
|
584
|
+
constraints: { type: 'object', additionalProperties: true, description: '{ mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }' },
|
|
585
|
+
evidence: { type: 'string', required: true, description: '用户原话(append-only 可溯源,P0)' },
|
|
586
|
+
},
|
|
453
587
|
},
|
|
454
588
|
},
|
|
455
589
|
output: {
|
|
@@ -479,19 +613,21 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
479
613
|
+ '(「去年国庆去了大理」) — dates resolve via the time anchor (词表外日期会拒绝,请用户给绝对日期), evidence MUST be the user\'s verbatim words. '
|
|
480
614
|
+ 'Append-only and idempotent (same destination+start+source = same trip); overlapping same-destination trips are rejected for human adjudication. '
|
|
481
615
|
+ 'Past trips power origin resolution and 「去过不再推」 ranking — never a hard filter.',
|
|
616
|
+
// D-30 第四刀(issue #112):平铺 typed;destination/evidence required → 宿主权入口拒缺参
|
|
617
|
+
// (evidence 是 P0 溯源红线,进 schema 由宿主权闸,不再依赖 execute 散文报错)
|
|
482
618
|
parameters: {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
},
|
|
619
|
+
destination: { type: 'string', required: true, description: '目的地,如 大理' },
|
|
620
|
+
start: { type: 'string', required: true, description: '行程起始:YYYY-MM-DD 或绝对日期表达(解析不了 execute 结构化报错不猜)' },
|
|
621
|
+
end: { type: 'string', description: '行程结束,形态同 start(可选)' },
|
|
622
|
+
companions: { type: 'array', items: { type: 'string' }, description: '同行人,如 ["爸爸"]' },
|
|
623
|
+
evidence: { type: 'string', required: true, description: '用户原话(溯源 P0 红线)' },
|
|
488
624
|
},
|
|
489
625
|
output: {
|
|
490
626
|
schema: { type: 'json' },
|
|
491
627
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }],
|
|
492
628
|
},
|
|
493
|
-
async execute(args
|
|
494
|
-
const q =
|
|
629
|
+
async execute(args, _exec: unknown) {
|
|
630
|
+
const q = args
|
|
495
631
|
if (!q.destination) return { ok: false, summary: 'destination 必填' } as never
|
|
496
632
|
if (!q.evidence) return { ok: false, summary: 'evidence 必填(用户原话,溯源 P0)' } as never
|
|
497
633
|
const anchor = buildTimeAnchor(new Date())
|
|
@@ -507,30 +643,30 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
507
643
|
if (!res.appended && res.reason) return { ok: false, summary: res.reason } as never
|
|
508
644
|
return { ok: true, appended: res.appended, trip_id: res.tripId, total: res.total, summary: res.appended ? `已入时间线:${q.destination} @ ${start}` : `已存在(幂等跳过):${res.tripId}` } as never
|
|
509
645
|
},
|
|
510
|
-
presentCall: args => ({ card: 'generic', title: '记录旅行', kind: 'edit', rawInput: args
|
|
646
|
+
presentCall: args => ({ card: 'generic', title: '记录旅行', kind: 'edit', rawInput: args }),
|
|
511
647
|
}))
|
|
512
648
|
|
|
513
649
|
registerGuarded(defineTool({
|
|
514
650
|
name: 'gotry_hotel_search',
|
|
515
|
-
description:
|
|
651
|
+
description: routed('gotry_hotel_search',
|
|
516
652
|
'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). '
|
|
517
|
-
+ 'Input: destination city name + optional dates/
|
|
653
|
+
+ 'Input: destination city name + optional dates/adults. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — '
|
|
518
654
|
+ 'the code layer resolves them against the time anchor; unresolved expressions degrade to an undated search with an explicit '
|
|
519
655
|
+ 'date_notes entry instead of guessing. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, '
|
|
520
|
-
+ 'or [static-pack:estimate]) per the L4 invariant.',
|
|
656
|
+
+ 'or [static-pack:estimate]) per the L4 invariant.'),
|
|
657
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。
|
|
521
658
|
parameters: {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
},
|
|
659
|
+
destination: { type: 'string', required: true, description: '目的地城市,如 大理' },
|
|
660
|
+
checkIn: { type: 'string', description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);解析不了降级无日期检索并记 date_notes' },
|
|
661
|
+
checkOut: { type: 'string', description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn' },
|
|
662
|
+
adults: { type: 'integer', description: '成人数(顶层字段,默认 2)' },
|
|
527
663
|
},
|
|
528
664
|
output: {
|
|
529
665
|
schema: { type: 'json' },
|
|
530
666
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 400)) }],
|
|
531
667
|
},
|
|
532
|
-
async execute(args
|
|
533
|
-
const q =
|
|
668
|
+
async execute(args, _exec) {
|
|
669
|
+
const q = args
|
|
534
670
|
if (!q.destination) throw new Error('gotry_hotel_search requires destination')
|
|
535
671
|
const started = Date.now()
|
|
536
672
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json')
|
|
@@ -573,7 +709,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
573
709
|
} as never
|
|
574
710
|
return JSON.parse(JSON.stringify(payload)) as never
|
|
575
711
|
},
|
|
576
|
-
presentCall: args => ({ card: 'generic', title: `酒店搜索:${
|
|
712
|
+
presentCall: args => ({ card: 'generic', title: `酒店搜索:${args.destination ?? ''}`, kind: 'search', rawInput: args }),
|
|
577
713
|
presentResult: (_args, value) => {
|
|
578
714
|
const r = value as { hotels?: unknown; via?: string; destination?: string; summary?: string }
|
|
579
715
|
const h = r.hotels
|
|
@@ -619,24 +755,26 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
619
755
|
|
|
620
756
|
registerGuarded(defineTool({
|
|
621
757
|
name: 'gotry_weather_check',
|
|
622
|
-
description:
|
|
758
|
+
description: routed('gotry_weather_check',
|
|
623
759
|
'Check weather for a destination: forecast (≤16 days) or historical climate (seasonality baseline). '
|
|
624
760
|
+ 'Free Open-Meteo API, no key required. Input: place name (Chinese ok) or lat/lng. '
|
|
625
761
|
+ 'Returns daily temp range, precipitation probability, weather code — with evidence chain tagging '
|
|
626
|
-
+ '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.',
|
|
762
|
+
+ '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.'),
|
|
627
763
|
parameters: {
|
|
628
|
-
query
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
},
|
|
764
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。
|
|
765
|
+
place: { type: 'string', description: '城市名(中文可),如 大理;与 lat/lng 二选一,都没给 execute 结构化报错' },
|
|
766
|
+
lat: { type: 'number', description: '纬度(与 lng 成对;给了则跳过地理编码)' },
|
|
767
|
+
lng: { type: 'number', description: '经度(与 lat 成对)' },
|
|
768
|
+
month: { type: 'integer', description: '气候基线月份 1-12(mode=climate 或给了 month 未指 mode 时用)' },
|
|
769
|
+
mode: { type: 'string', enum: ['forecast', 'climate'], description: '默认 forecast' },
|
|
770
|
+
days: { type: 'integer', description: '预报天数(≤16,默认 7)' },
|
|
633
771
|
},
|
|
634
772
|
output: {
|
|
635
773
|
schema: { type: 'json' },
|
|
636
774
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
637
775
|
},
|
|
638
|
-
async execute(args
|
|
639
|
-
const q =
|
|
776
|
+
async execute(args, _exec) {
|
|
777
|
+
const q = args
|
|
640
778
|
const started = Date.now()
|
|
641
779
|
let lat: number | undefined = q.lat, lng: number | undefined = q.lng
|
|
642
780
|
let placeLabel = q.place ?? `${q.lat},${q.lng}`
|
|
@@ -671,10 +809,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
671
809
|
latency_ms: Date.now() - started,
|
|
672
810
|
})) as Record<string, never>
|
|
673
811
|
},
|
|
674
|
-
presentCall: args => ({ card: 'generic', title: `天气:${
|
|
812
|
+
presentCall: args => ({ card: 'generic', title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`, kind: 'fetch', rawInput: args }),
|
|
675
813
|
presentResult: (args, value) => {
|
|
676
814
|
const r = value as { summary?: string }
|
|
677
|
-
const place = String(
|
|
815
|
+
const place = String(args.place ?? '')
|
|
678
816
|
const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable')
|
|
679
817
|
return {
|
|
680
818
|
card: 'generic',
|
|
@@ -686,7 +824,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
686
824
|
|
|
687
825
|
registerGuarded(defineTool({
|
|
688
826
|
name: 'gotry_flight_verify',
|
|
689
|
-
description:
|
|
827
|
+
description: routed('gotry_flight_verify',
|
|
690
828
|
'Verify whether a flight callsign is currently observable on the OpenSky ADS-B network. '
|
|
691
829
|
+ 'Free anonymous API (~400 credits/day, 4 req/s burst). Three-valued semantics: '
|
|
692
830
|
+ 'observed = strong positive (the aircraft is currently being broadcast); '
|
|
@@ -694,20 +832,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
694
832
|
+ 'a missing signal does NOT disprove the flight); '
|
|
695
833
|
+ 'unavailable = API failure, gracefully degraded. '
|
|
696
834
|
+ 'Use to ground "is this flight actually flying right now?" in real data, complementing '
|
|
697
|
-
+ 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).',
|
|
835
|
+
+ 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).'),
|
|
836
|
+
// D-30 第四刀(issue #112):平铺 typed;callsign required → 宿主权即拒
|
|
698
837
|
parameters: {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
description: '{ callsign: "EK329", airport?: "OMDB", timeoutMs?: 10000 }',
|
|
703
|
-
},
|
|
838
|
+
callsign: { type: 'string', required: true, description: '航班呼号,如 EK329' },
|
|
839
|
+
airport: { type: 'string', description: '预期机场 IATA,如 OMDB(观测命中时展示)' },
|
|
840
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 10000' },
|
|
704
841
|
},
|
|
705
842
|
output: {
|
|
706
843
|
schema: { type: 'json' },
|
|
707
844
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 500)) }],
|
|
708
845
|
},
|
|
709
|
-
async execute(args
|
|
710
|
-
const q =
|
|
846
|
+
async execute(args, _exec: unknown) {
|
|
847
|
+
const q = args
|
|
711
848
|
const started = Date.now()
|
|
712
849
|
if (!q.callsign) {
|
|
713
850
|
return JSON.parse(JSON.stringify({ verdict: 'unavailable', evidence: '[校验不可用:无 callsign]', summary: 'callsign 必填' })) as Record<string, never>
|
|
@@ -729,23 +866,35 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
729
866
|
latency_ms: Date.now() - started,
|
|
730
867
|
})) as Record<string, never>
|
|
731
868
|
},
|
|
732
|
-
presentCall: args => ({ card: 'generic', title: `飞行校验:${
|
|
869
|
+
presentCall: args => ({ card: 'generic', title: `飞行校验:${args.callsign ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
733
870
|
}))
|
|
734
871
|
|
|
735
872
|
registerGuarded(defineTool({
|
|
736
873
|
name: 'gotry_flyai_search',
|
|
737
|
-
description:
|
|
874
|
+
description: routed('gotry_flyai_search',
|
|
738
875
|
'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). '
|
|
739
|
-
+ 'kind="flight"|"train":
|
|
740
|
-
+ 'kind="hotel":
|
|
876
|
+
+ 'kind="flight"|"train": from/to 中文城市名 + date YYYY-MM-DD — real schedules & prices, split 直达/中转 in results. '
|
|
877
|
+
+ 'kind="hotel": to=目的地中文(如 大理), checkIn/checkOut (YYYY-MM-DD,成对可选——未定档期可不填先摸底), keyWords?. '
|
|
741
878
|
+ '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. '
|
|
742
|
-
+ 'Evidence [实时API:flyai@ts].
|
|
879
|
+
+ '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. '
|
|
880
|
+
+ 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.'),
|
|
881
|
+
// D-30 第一刀(issue #112):query json blob → 平铺 typed 契约。逐字段 schema 由 dsh
|
|
882
|
+
// parameterSchemaSpecToJsonSchema 投影为模型可见 JSON Schema,validateArgs 在 execute 前
|
|
883
|
+
// 宿主权校验——畸形参数(缺 kind/枚举外值/类型错/legacy blob 包裹)在入口即被结构化拒绝
|
|
884
|
+
// (ToolArgsError → guardToolExecute 兜成 ADR-13 ToolFailure,形状由迁移测试锁死)。
|
|
885
|
+
// 条件必填(机/火要 from/to/date,酒店要 to)不在 schema 强制,仍由 execute 结构化报错给方向。
|
|
743
886
|
parameters: {
|
|
744
|
-
|
|
887
|
+
kind: { type: 'string', enum: ['flight', 'train', 'hotel'], required: true, description: '检索类型:flight 机票 / train 火车 / hotel 酒店' },
|
|
888
|
+
from: { type: 'string', description: '出发城市中文,如 上海——kind=flight|train 必填' },
|
|
889
|
+
to: { type: 'string', description: '到达城市中文,如 丽江;kind=hotel 时为目的地,如 大理(必填)' },
|
|
890
|
+
date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' },
|
|
891
|
+
checkIn: { type: 'string', description: '入住日期 YYYY-MM-DD,仅 kind=hotel;与 checkOut 成对可选(未定档期可不填先摸底)' },
|
|
892
|
+
checkOut: { type: 'string', description: '退房日期 YYYY-MM-DD,仅 kind=hotel;与 checkIn 成对' },
|
|
893
|
+
keyWords: { type: 'string', description: '酒店关键词,如 洱海——仅 kind=hotel 可选' },
|
|
745
894
|
},
|
|
746
895
|
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { summary?: string }).summary ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
747
|
-
async execute(args
|
|
748
|
-
const q =
|
|
896
|
+
async execute(args, _exec) {
|
|
897
|
+
const q = args
|
|
749
898
|
if (q.kind === 'hotel') {
|
|
750
899
|
const dest = (q.to ?? '').trim()
|
|
751
900
|
if (!dest) return { ok: false, summary: 'kind=hotel 需要 to(目的地中文,如 大理)' } as const
|
|
@@ -769,11 +918,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
769
918
|
const itp = await interpretEffect({ effect: 'FLYAI_SEARCH', params: { kind: 'hotel', destName: dest, checkInDate: q.checkIn, checkOutDate: q.checkOut, keyWords: q.keyWords } })
|
|
770
919
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace)
|
|
771
920
|
const r = itp.result
|
|
921
|
+
await noteChannel('flyai', r.verdict)
|
|
922
|
+
// D-26(issue #118):exact-date 酒店检索 hit/miss 落账(摸底无档期不落,传输失败不落负事实)
|
|
923
|
+
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
924
|
+
source: 'flyai-hotel', destination: dest, checkIn: q.checkIn, checkOut: q.checkOut,
|
|
925
|
+
verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(),
|
|
926
|
+
}))
|
|
772
927
|
const top = (r.hotels ?? []).slice(0, 8).map(o => `${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`)
|
|
773
928
|
const summary = r.verdict === 'hit'
|
|
774
929
|
? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}`
|
|
775
|
-
:
|
|
776
|
-
|
|
930
|
+
: r.verdict === 'needs-setup'
|
|
931
|
+
? `${dest} 酒店检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}`
|
|
932
|
+
: `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`
|
|
933
|
+
return JSON.parse(JSON.stringify({ ...r, summary, ...(r.verdict !== 'hit' ? routingField('search-hotel', 'flyai') : {}) })) as Record<string, never>
|
|
777
934
|
}
|
|
778
935
|
const kind = q.kind === 'train' ? 'train' : 'flight'
|
|
779
936
|
if (!q.from || !q.to || !q.date) {
|
|
@@ -794,6 +951,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
794
951
|
const itp = await interpretEffect({ effect: 'FLYAI_SEARCH', params: { kind, origin: q.from, destination: q.to, depDate: q.date } })
|
|
795
952
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace)
|
|
796
953
|
const r = itp.result
|
|
954
|
+
await noteChannel('flyai', r.verdict)
|
|
797
955
|
// issue #46 事实落账(ADR-19):exact-date 检索结论(hit 正事实 / miss 负事实)追加进
|
|
798
956
|
// bookable-facts 侧车——产物事实闸(gotry_fact_gate)的唯一事实源;落盘失败不阻塞检索
|
|
799
957
|
const avMap = await loadAirlineAirportMap()
|
|
@@ -806,12 +964,17 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
806
964
|
? `${q.from}→${q.to} ${q.date} ${label}(飞猪官方只读)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}`
|
|
807
965
|
: r.verdict === 'miss'
|
|
808
966
|
? `${q.from}→${q.to} ${q.date} ${label}官方通道正常返回 0 条(常见原因:航线未开放/当日售罄)。${r.evidence}`
|
|
809
|
-
:
|
|
810
|
-
|
|
967
|
+
: r.verdict === 'needs-setup'
|
|
968
|
+
? `${q.from}→${q.to} ${q.date} ${label}检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}`
|
|
969
|
+
: `${q.from}→${q.to} ${q.date} ${label}检索失败(可能限流/网络):${r.error ?? ''} ${r.evidence}`
|
|
970
|
+
return JSON.parse(JSON.stringify({
|
|
971
|
+
...r, kind, summary,
|
|
972
|
+
...(r.verdict !== 'hit' ? routingField(kind === 'train' ? 'search-train' : 'search-flight', 'flyai') : {}),
|
|
973
|
+
})) as Record<string, never>
|
|
811
974
|
},
|
|
812
|
-
presentCall: args => ({ card: 'generic', title: `官方检索:${
|
|
975
|
+
presentCall: args => ({ card: 'generic', title: `官方检索:${args.kind}`, kind: 'fetch', rawInput: args }),
|
|
813
976
|
presentResult: (args, value) => {
|
|
814
|
-
const isHotel =
|
|
977
|
+
const isHotel = args.kind === 'hotel'
|
|
815
978
|
const r = value as { ok?: boolean; options?: unknown[]; hotels?: unknown[] }
|
|
816
979
|
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length)
|
|
817
980
|
return { card: 'generic', title: `${isHotel ? '飞猪酒店' : '飞猪检索'}:${r.ok && n > 0 ? `${n} 条` : '降级'}`, content: [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }] }
|
|
@@ -827,13 +990,17 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
827
990
|
+ 'Transport: the GoTry Session Bridge browser extension (one-time install, ZERO Chrome system dialogs). '
|
|
828
991
|
+ `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). `
|
|
829
992
|
+ 'Evidence [会话:<site>-login@ts].',
|
|
993
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
830
994
|
parameters: {
|
|
831
|
-
|
|
995
|
+
waitSeconds: { type: 'integer', description: '等待用户完成登录的上限秒数,默认 90,至多 300;不传用默认' },
|
|
832
996
|
},
|
|
833
997
|
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { evidence?: string }).evidence ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
834
|
-
async execute(args
|
|
998
|
+
async execute(args, _exec: unknown) {
|
|
835
999
|
const q = unwrapQuery<{ waitSeconds?: number }>(args, 'waitSeconds')
|
|
836
|
-
|
|
1000
|
+
// D-23 收编(issue #115):登录引导入效应注册表(SESSION_LOGIN,浏览器族永不重试不熔断)
|
|
1001
|
+
const itpL = await interpretEffect({ effect: 'SESSION_LOGIN', params: { waitSeconds: q?.waitSeconds } })
|
|
1002
|
+
if (!itpL.result) return declinedObservation('SESSION_LOGIN', itpL.trace)
|
|
1003
|
+
const r = itpL.result
|
|
837
1004
|
const summary = r.verdict === 'logged-in'
|
|
838
1005
|
? `${r.site} 登录完成确认(票据 cookie 名已检出,只读名字)。说明:登录是在携程官网、用你自己的浏览器完成的——gotry 全程未接触任何密码/验证码/cookie 值。现在可以继续会话检索了。${r.evidence}`
|
|
839
1006
|
: r.verdict === 'pending'
|
|
@@ -866,22 +1033,106 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
866
1033
|
|
|
867
1034
|
registerGuarded(defineTool({
|
|
868
1035
|
name: 'gotry_session_search',
|
|
869
|
-
description:
|
|
870
|
-
'Search on the USER\'S OWN
|
|
1036
|
+
description: routed('gotry_session_search',
|
|
1037
|
+
'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). '
|
|
871
1038
|
+ '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). '
|
|
872
1039
|
+ '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). '
|
|
873
|
-
+ '
|
|
1040
|
+
+ 'kind="flight": from/to 中文城市名 + date YYYY-MM-DD — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. '
|
|
1041
|
+
+ '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]. '
|
|
1042
|
+
+ '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]. '
|
|
874
1043
|
+ '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); '
|
|
875
1044
|
+ `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. `
|
|
876
|
-
+ 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).',
|
|
1045
|
+
+ 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).'),
|
|
1046
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(逐字段 schema 模型可见)。
|
|
1047
|
+
// 与 flyai 刀的差异:本工具三意图无公共 required 字段(kind 缺省=flight),根 schema 是
|
|
1048
|
+
// 隐式开放对象,宿主权无法用 required 拒 legacy blob——故保留 interpretArgs 容忍层
|
|
1049
|
+
// (docs/design/tool-orchestration-design.md §4③「interpretArgs 留作旧形态容忍层」),blob 调用在
|
|
1050
|
+
// execute 内归一后走原条件闸,结构化报错不崩。flyai 因 kind required 仍在宿主权即拒。
|
|
877
1051
|
parameters: {
|
|
878
|
-
|
|
1052
|
+
kind: { type: 'string', enum: ['flight', 'hotel', 'train'], description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面)' },
|
|
1053
|
+
from: { type: 'string', description: '出发城市中文(词表内),如 上海——kind=flight|train 必填' },
|
|
1054
|
+
to: { type: 'string', description: '到达城市中文——kind=flight|train 必填;kind=hotel 时为目的地(必填)' },
|
|
1055
|
+
date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' },
|
|
1056
|
+
cityId: { type: 'integer', description: '携程酒店 city= 数字,仅 kind=hotel;目的地码表外时 web 搜 hotels.ctrip.com list URL 取 city=' },
|
|
1057
|
+
checkIn: { type: 'string', description: '入住日 YYYY-MM-DD,仅 kind=hotel 可选' },
|
|
1058
|
+
checkOut: { type: 'string', description: '退房日 YYYY-MM-DD,仅 kind=hotel 可选' },
|
|
1059
|
+
adults: { type: 'integer', description: '成人数,仅 kind=hotel 可选' },
|
|
1060
|
+
fromStationTelecode: { type: 'string', description: '出发站三位电报码,仅 kind=train;城市码表外时填(kyfw 查询页 URL 里)' },
|
|
1061
|
+
toStationTelecode: { type: 'string', description: '到达站三位电报码,仅 kind=train,同上' },
|
|
879
1062
|
},
|
|
880
1063
|
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { summary?: string }).summary ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
881
|
-
async execute(args
|
|
882
|
-
const q = unwrapQuery<{ from?: string; to?: string; date?: string }>(args
|
|
1064
|
+
async execute(args, _exec) {
|
|
1065
|
+
const q = unwrapQuery<{ kind?: string; from?: string; to?: string; date?: string; cityId?: number | string; checkIn?: string; checkOut?: string; adults?: number; fromStationTelecode?: string; toStationTelecode?: string }>(args)
|
|
1066
|
+
// ---- 会话酒店(2026-09-03 实装;2026-09-02 迪拜 session:用户要携程找酒店,会话面却只有机票)----
|
|
1067
|
+
// ---- 会话火车(2026-09-03 实装;12306 公开查询面,无登录闸)----
|
|
1068
|
+
if (q.kind === 'train') {
|
|
1069
|
+
if (!q.from || !q.to || !q.date) {
|
|
1070
|
+
return { ok: false, summary: 'kind=train 需要 from/to(中文城市/车站名)与 date(YYYY-MM-DD);城市电报码表外带 fromStationTelecode/toStationTelecode(kyfw 查询页 URL 里的三位码)' } as const
|
|
1071
|
+
}
|
|
1072
|
+
const nowT = new Date()
|
|
1073
|
+
const todayT = `${nowT.getFullYear()}-${String(nowT.getMonth() + 1).padStart(2, '0')}-${String(nowT.getDate()).padStart(2, '0')}`
|
|
1074
|
+
if (q.date < todayT) {
|
|
1075
|
+
return JSON.parse(JSON.stringify({
|
|
1076
|
+
ok: false, verdict: 'error',
|
|
1077
|
+
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayT}),过去不存在在售火车票。向用户确认日期后再查。`,
|
|
1078
|
+
})) as Record<string, never>
|
|
1079
|
+
}
|
|
1080
|
+
const itpT = await interpretEffect({
|
|
1081
|
+
effect: 'SESSION_TRAIN_SEARCH',
|
|
1082
|
+
params: {
|
|
1083
|
+
from: q.from, to: q.to, date: q.date,
|
|
1084
|
+
fromStationTelecode: q.fromStationTelecode, toStationTelecode: q.toStationTelecode,
|
|
1085
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1086
|
+
},
|
|
1087
|
+
})
|
|
1088
|
+
if (!itpT.result) return declinedObservation('SESSION_TRAIN_SEARCH', itpT.trace)
|
|
1089
|
+
const rT = itpT.result
|
|
1090
|
+
await noteChannel('session:12306-train', rT.verdict)
|
|
1091
|
+
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(' ')}`)
|
|
1092
|
+
const summaryT = rT.verdict === 'hit'
|
|
1093
|
+
? `${q.from}→${q.to} ${q.date} 余票(12306 公开查询面,${(rT.trains ?? []).length} 趟)前 ${topT.length} 条(列表接口不含票价,票价以 12306 落地页为准):\n${topT.join('\n')}\n${rT.evidence}`
|
|
1094
|
+
: rT.verdict === 'cooldown'
|
|
1095
|
+
? `会话火车检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${rT.evidence}`
|
|
1096
|
+
: `会话火车检索未取回(${rT.verdict}):${rT.error ?? ''} ${rT.evidence}`
|
|
1097
|
+
return JSON.parse(JSON.stringify({
|
|
1098
|
+
...rT, summary: summaryT,
|
|
1099
|
+
...(rT.verdict !== 'hit' ? routingField('search-train', 'session:12306-train') : {}),
|
|
1100
|
+
})) as Record<string, never>
|
|
1101
|
+
}
|
|
1102
|
+
if (q.kind === 'hotel') {
|
|
1103
|
+
if (!q.to) {
|
|
1104
|
+
return { ok: false, summary: 'kind=hotel 需要 to(目的地中文;城市码表外带 cityId=携程酒店 list 页 URL 里的 city= 数字)' } as const
|
|
1105
|
+
}
|
|
1106
|
+
const itp = await interpretEffect({
|
|
1107
|
+
effect: 'SESSION_HOTEL_SEARCH',
|
|
1108
|
+
params: {
|
|
1109
|
+
to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults,
|
|
1110
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1113
|
+
if (!itp.result) return declinedObservation('SESSION_HOTEL_SEARCH', itp.trace)
|
|
1114
|
+
const r = itp.result
|
|
1115
|
+
await noteChannel('session:ctrip-hotel', r.verdict)
|
|
1116
|
+
// D-26(issue #118):exact-date 会话酒店 hit/miss 落账(摸底无档期不落;cooldown 是节律闸非结论,不落)
|
|
1117
|
+
if (r.verdict === 'hit' || r.verdict === 'miss') {
|
|
1118
|
+
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
1119
|
+
source: 'session:ctrip-hotel', destination: q.to ?? '', checkIn: q.checkIn, checkOut: q.checkOut,
|
|
1120
|
+
verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(),
|
|
1121
|
+
}))
|
|
1122
|
+
}
|
|
1123
|
+
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}` : ''}`)
|
|
1124
|
+
const summary = r.verdict === 'hit'
|
|
1125
|
+
? `${q.to} 酒店(携程,用户本人登录态,真实价)前 ${top.length} 家(预订以 jumpUrl 落地页为准):\n${top.join('\n')}\n${r.evidence}`
|
|
1126
|
+
: r.verdict === 'cooldown'
|
|
1127
|
+
? `会话酒店检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${r.evidence}`
|
|
1128
|
+
: `会话酒店检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`
|
|
1129
|
+
return JSON.parse(JSON.stringify({
|
|
1130
|
+
...r, summary,
|
|
1131
|
+
...(r.verdict !== 'hit' ? routingField('search-hotel', 'session:ctrip-hotel') : {}),
|
|
1132
|
+
})) as Record<string, never>
|
|
1133
|
+
}
|
|
883
1134
|
if (!q.from || !q.to || !q.date) {
|
|
884
|
-
return { ok: false, summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD)' } as const
|
|
1135
|
+
return { ok: false, summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD);酒店/火车检索分别用 kind=hotel / kind=train' } as const
|
|
885
1136
|
}
|
|
886
1137
|
// 效应解译层(ADR-18):SESSION 通道策略=永不重试/不熔断,节律闸在渠道内;
|
|
887
1138
|
// 解译器只做分发与证据拼装,verdict 语义(risk 型 needs-login/challenged)原样透传
|
|
@@ -895,6 +1146,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
895
1146
|
})
|
|
896
1147
|
if (!itp.result) return declinedObservation('SESSION_FLIGHT_SEARCH', itp.trace)
|
|
897
1148
|
const r = itp.result
|
|
1149
|
+
await noteChannel('session:ctrip-flight', r.verdict)
|
|
898
1150
|
// issue #46 事实落账(ADR-19):会话面 exact-date 结论同样进事实注册表(独立 source 并列标注)
|
|
899
1151
|
const avMapS = await loadAirlineAirportMap()
|
|
900
1152
|
await appendFacts(config.stateRoot ?? '.', factsFromSession({ origin: q.from, destination: q.to, date: q.date }, r, new Date().toISOString(), avMapS?.city_alias))
|
|
@@ -902,14 +1154,22 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
902
1154
|
const summary = r.verdict === 'hit'
|
|
903
1155
|
? `${q.from}→${q.to} ${q.date} 会话检索(携程,用户本人登录态)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}`
|
|
904
1156
|
: `会话检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`
|
|
905
|
-
return JSON.parse(JSON.stringify({
|
|
1157
|
+
return JSON.parse(JSON.stringify({
|
|
1158
|
+
...r, summary,
|
|
1159
|
+
...(r.verdict !== 'hit' ? routingField('search-flight', 'session:ctrip-flight') : {}),
|
|
1160
|
+
})) as Record<string, never>
|
|
906
1161
|
},
|
|
907
|
-
presentCall: args =>
|
|
908
|
-
|
|
909
|
-
|
|
1162
|
+
presentCall: args => {
|
|
1163
|
+
const callTitle = args.kind === 'hotel' ? `会话酒店:${args.to ?? ''}` : args.kind === 'train' ? `会话火车:${args.from ?? ''}` : `会话检索:${args.from ?? ''}`
|
|
1164
|
+
return { card: 'generic', title: callTitle, kind: 'fetch', rawInput: args }
|
|
1165
|
+
},
|
|
1166
|
+
presentResult: (args, value) => {
|
|
1167
|
+
const r = value as { verdict?: string; options?: unknown[]; hotels?: unknown[]; installUrl?: string }
|
|
1168
|
+
const isHotel = args.kind === 'hotel'
|
|
1169
|
+
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length)
|
|
910
1170
|
const needsExt = r.verdict === 'needs-extension'
|
|
911
1171
|
const label = r.verdict === 'hit'
|
|
912
|
-
? `会话 ${
|
|
1172
|
+
? `会话 ${n} 条`
|
|
913
1173
|
: r.verdict === 'needs-login'
|
|
914
1174
|
? '需登录'
|
|
915
1175
|
: needsExt
|
|
@@ -917,38 +1177,42 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
917
1177
|
: r.verdict ?? '降级'
|
|
918
1178
|
const content: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }]
|
|
919
1179
|
if (needsExt && r.installUrl) content.push({ type: 'text', text: `安装链接:${r.installUrl}` })
|
|
920
|
-
|
|
1180
|
+
const face = args.kind === 'hotel' ? '会话酒店' : args.kind === 'train' ? '会话火车' : '会话检索'
|
|
1181
|
+
return { card: 'generic', title: `${face}:${label}`, content }
|
|
921
1182
|
},
|
|
922
1183
|
}))
|
|
923
1184
|
|
|
924
1185
|
registerGuarded(defineTool({
|
|
925
1186
|
name: 'gotry_anything_search',
|
|
926
|
-
description:
|
|
1187
|
+
description: routed('gotry_anything_search',
|
|
927
1188
|
'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. ' +
|
|
928
1189
|
'Mixed destinations (cities / metropolitan areas / high-level regions) + hotels in one call. ' +
|
|
929
1190
|
'Returns candidates with type, name, optional coordinates and hotel-id. ' +
|
|
930
1191
|
'Three-valued semantics: hit = ≥1 candidate; miss = 0 candidates (try synonyms or contentType=city/hotel); ' +
|
|
931
1192
|
'unavailable = hbcli failed (degraded, never blocks). ' +
|
|
932
1193
|
'Use as the first stop when the user mentions a place/city/hotel name and you need to ground it in real catalog data ' +
|
|
933
|
-
'(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).',
|
|
1194
|
+
'(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).'),
|
|
1195
|
+
// D-30 第四刀(issue #112):平铺 typed;keyword required → 宿主权即拒
|
|
934
1196
|
parameters: {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
},
|
|
1197
|
+
keyword: { type: 'string', required: true, description: '搜索关键词' },
|
|
1198
|
+
contentType: { type: 'string', enum: ['city', 'hotel'], description: '内容类型(缺省由上游推断)' },
|
|
1199
|
+
parentDestinationId: { type: 'string', description: '父目的地 id(上游形态)' },
|
|
1200
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 12000' },
|
|
940
1201
|
},
|
|
941
1202
|
output: {
|
|
942
1203
|
schema: { type: 'json' },
|
|
943
1204
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
944
1205
|
},
|
|
945
|
-
async execute(args
|
|
946
|
-
const q =
|
|
1206
|
+
async execute(args, _exec: unknown) {
|
|
1207
|
+
const q = args
|
|
1208
|
+
const rawQuery = String(q.keyword ?? '')
|
|
947
1209
|
const started = Date.now()
|
|
948
1210
|
if (!q.keyword) {
|
|
949
1211
|
return JSON.parse(JSON.stringify({ ok: false, verdict: 'error', summary: 'keyword 必填', evidence: '[hbcli-anything@error] empty' })) as Record<string, never>
|
|
950
1212
|
}
|
|
951
|
-
const
|
|
1213
|
+
const itpA = await interpretEffect({ effect: 'ANYTHING_SEARCH', params: q })
|
|
1214
|
+
if (!itpA.result) return declinedObservation('ANYTHING_SEARCH', itpA.trace)
|
|
1215
|
+
const r = itpA.result
|
|
952
1216
|
const dir = await ensureStateDir(config.stateRoot)
|
|
953
1217
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(() => {})
|
|
954
1218
|
const top5 = (r.hits ?? []).slice(0, 5)
|
|
@@ -964,7 +1228,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
964
1228
|
latency_ms: Date.now() - started,
|
|
965
1229
|
})) as Record<string, never>
|
|
966
1230
|
},
|
|
967
|
-
presentCall: args => ({ card: 'generic', title: `Anything search:${
|
|
1231
|
+
presentCall: args => ({ card: 'generic', title: `Anything search:${args.keyword ?? ''}`, kind: 'search', rawInput: args }),
|
|
968
1232
|
presentResult: (_args, value) => {
|
|
969
1233
|
const r = value as { hits?: unknown[]; total_candidates?: number; verdict?: string; keyword?: string; summary?: string }
|
|
970
1234
|
const n = Array.isArray(r.hits) ? r.hits.length : (r.total_candidates ?? 0)
|
|
@@ -978,30 +1242,30 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
978
1242
|
|
|
979
1243
|
registerGuarded(defineTool({
|
|
980
1244
|
name: 'gotry_web_search',
|
|
981
|
-
description:
|
|
1245
|
+
description: routed('gotry_web_search',
|
|
982
1246
|
'Read any public URL as markdown (Jina Reader, free, no key). ' +
|
|
983
1247
|
'Use as the "last mile" web reader when hotel-be Anything or gotry tools lack the answer. ' +
|
|
984
1248
|
'NOT a general-purpose search engine — only fetches a URL you already know. ' +
|
|
985
1249
|
'Three-valued: ok / error(非法 URL/超时)/not-reachable(r.jina.ai 不可用).' +
|
|
986
|
-
'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。',
|
|
1250
|
+
'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。'),
|
|
1251
|
+
// D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒(字符串 blob 形态退役)
|
|
987
1252
|
parameters: {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
required: true,
|
|
991
|
-
description: '{ url: "https://example.com", timeoutMs?: 20000 }',
|
|
992
|
-
},
|
|
1253
|
+
url: { type: 'string', required: true, description: '完整 URL,https://…' },
|
|
1254
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 20000' },
|
|
993
1255
|
},
|
|
994
1256
|
output: {
|
|
995
1257
|
schema: { type: 'json' },
|
|
996
1258
|
render: (_args, value) => [{ type: 'text', text: String((value as { content?: string }).content?.slice(0, 800) ?? JSON.stringify(value).slice(0, 800)) }],
|
|
997
1259
|
},
|
|
998
|
-
async execute(args
|
|
999
|
-
const q =
|
|
1260
|
+
async execute(args, _exec: unknown) {
|
|
1261
|
+
const q = args
|
|
1000
1262
|
const started = Date.now()
|
|
1001
1263
|
if (!q.url) {
|
|
1002
1264
|
return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填', evidence: '[agent-reach:error] empty url' })) as Record<string, never>
|
|
1003
1265
|
}
|
|
1004
|
-
const
|
|
1266
|
+
const itpW = await interpretEffect({ effect: 'WEB_READ', params: { url: q.url, timeoutMs: q.timeoutMs } })
|
|
1267
|
+
if (!itpW.result) return declinedObservation('WEB_READ', itpW.trace)
|
|
1268
|
+
const r = itpW.result
|
|
1005
1269
|
const dir = await ensureStateDir(config.stateRoot)
|
|
1006
1270
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(() => {})
|
|
1007
1271
|
const summary = r.ok
|
|
@@ -1013,7 +1277,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1013
1277
|
latency_ms: Date.now() - started,
|
|
1014
1278
|
})) as Record<string, never>
|
|
1015
1279
|
},
|
|
1016
|
-
presentCall: args => ({ card: 'generic', title: `读网页:${
|
|
1280
|
+
presentCall: args => ({ card: 'generic', title: `读网页:${args.url ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
1017
1281
|
}))
|
|
1018
1282
|
|
|
1019
1283
|
registerGuarded(defineTool({
|
|
@@ -1023,23 +1287,23 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1023
1287
|
'If yt-dlp is installed on this machine, returns the subtitle text (vtt, zh-Hans/zh/en preference). ' +
|
|
1024
1288
|
'If NOT installed, degrades gracefully with install instructions — never blocks. ' +
|
|
1025
1289
|
'Evidence chain: [agent-reach:yt-dlp@ts] / [@not-installed@ts].',
|
|
1290
|
+
// D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒
|
|
1026
1291
|
parameters: {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
required: true,
|
|
1030
|
-
description: '{ url: "https://www.youtube.com/watch?v=...", lang?: "zh-Hans,zh,en" }',
|
|
1031
|
-
},
|
|
1292
|
+
url: { type: 'string', required: true, description: '视频页 URL(youtube 等)' },
|
|
1293
|
+
lang: { type: 'string', description: '字幕语言优先级,如 zh-Hans,zh,en' },
|
|
1032
1294
|
},
|
|
1033
1295
|
output: {
|
|
1034
1296
|
schema: { type: 'json' },
|
|
1035
1297
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1036
1298
|
},
|
|
1037
|
-
async execute(args
|
|
1038
|
-
const q =
|
|
1299
|
+
async execute(args, _exec: unknown) {
|
|
1300
|
+
const q = args
|
|
1039
1301
|
if (!q.url) {
|
|
1040
1302
|
return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填' })) as Record<string, never>
|
|
1041
1303
|
}
|
|
1042
|
-
const
|
|
1304
|
+
const itpV = await interpretEffect({ effect: 'VIDEO_SUBTITLE', params: { url: q.url, lang: q.lang } })
|
|
1305
|
+
if (!itpV.result) return declinedObservation('VIDEO_SUBTITLE', itpV.trace)
|
|
1306
|
+
const r = itpV.result
|
|
1043
1307
|
const summary = r.verdict === 'found'
|
|
1044
1308
|
? `${q.url} 字幕提取成功 (${r.latencyMs}ms)\n${r.evidence}\n---\n${(r.subtitles ?? '').slice(0, 800)}`
|
|
1045
1309
|
: r.verdict === 'not-installed'
|
|
@@ -1051,7 +1315,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1051
1315
|
latency_ms: r.latencyMs,
|
|
1052
1316
|
})) as Record<string, never>
|
|
1053
1317
|
},
|
|
1054
|
-
presentCall: args => ({ card: 'generic', title: `视频字幕:${
|
|
1318
|
+
presentCall: args => ({ card: 'generic', title: `视频字幕:${args.url ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
1055
1319
|
}))
|
|
1056
1320
|
|
|
1057
1321
|
registerGuarded(defineTool({
|
|
@@ -1061,23 +1325,23 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1061
1325
|
'If gh is installed and authenticated, returns repos with name/description/stars/url. ' +
|
|
1062
1326
|
'If NOT installed, degrades with install instructions — never blocks. ' +
|
|
1063
1327
|
'Evidence chain: [agent-reach:gh@ts] / [@not-installed@ts].',
|
|
1328
|
+
// D-30 第四刀(issue #112):平铺 typed;query required → 宿主权即拒(顶层字段与旧内层同名,形态从对象变字符串)
|
|
1064
1329
|
parameters: {
|
|
1065
|
-
query: {
|
|
1066
|
-
|
|
1067
|
-
required: true,
|
|
1068
|
-
description: '{ query: "agent-reach", limit?: 5 }',
|
|
1069
|
-
},
|
|
1330
|
+
query: { type: 'string', required: true, description: 'GitHub 搜索关键词,如 agent-reach' },
|
|
1331
|
+
limit: { type: 'integer', description: '结果数上限,默认 5' },
|
|
1070
1332
|
},
|
|
1071
1333
|
output: {
|
|
1072
1334
|
schema: { type: 'json' },
|
|
1073
1335
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1074
1336
|
},
|
|
1075
|
-
async execute(args
|
|
1076
|
-
const q =
|
|
1337
|
+
async execute(args, _exec: unknown) {
|
|
1338
|
+
const q = args
|
|
1077
1339
|
if (!q.query) {
|
|
1078
1340
|
return JSON.parse(JSON.stringify({ ok: false, summary: 'query 必填' })) as Record<string, never>
|
|
1079
1341
|
}
|
|
1080
|
-
const
|
|
1342
|
+
const itpG = await interpretEffect({ effect: 'GITHUB_SEARCH', params: { query: q.query, limit: q.limit } })
|
|
1343
|
+
if (!itpG.result) return declinedObservation('GITHUB_SEARCH', itpG.trace)
|
|
1344
|
+
const r = itpG.result
|
|
1081
1345
|
const summary = r.verdict === 'found'
|
|
1082
1346
|
? `${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}`
|
|
1083
1347
|
: r.verdict === 'not-installed'
|
|
@@ -1089,7 +1353,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1089
1353
|
latency_ms: r.latencyMs,
|
|
1090
1354
|
})) as Record<string, never>
|
|
1091
1355
|
},
|
|
1092
|
-
presentCall: args => ({ card: 'generic', title: `GitHub 搜索:${
|
|
1356
|
+
presentCall: args => ({ card: 'generic', title: `GitHub 搜索:${args.query ?? ''}`, kind: 'search', rawInput: args }),
|
|
1093
1357
|
}))
|
|
1094
1358
|
|
|
1095
1359
|
registerGuarded(defineTool({
|
|
@@ -1103,28 +1367,35 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1103
1367
|
'Action "status" runs the real `agent-reach doctor` (.venv/bin/agent-reach). ' +
|
|
1104
1368
|
'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. ' +
|
|
1105
1369
|
'Evidence chain: [agent-reach:<channel>.<method>@ts].',
|
|
1370
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层。
|
|
1371
|
+
// action 不设 enum:反射桥按 D-4a' 透传上游新增渠道/方法,gotry 零改动(生态开放面)
|
|
1106
1372
|
parameters: {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
},
|
|
1373
|
+
action: { type: 'string', description: 'status(默认,无 channel 时)或 reach' },
|
|
1374
|
+
channel: { type: 'string', description: '上游渠道名,如 web/v2ex/xueqiu(action=reach 时)' },
|
|
1375
|
+
method: { type: 'string', description: '上游方法名,如 read/get_hot_topics/get_stock_quote' },
|
|
1376
|
+
args: { type: 'string', description: '空格分隔参数' },
|
|
1377
|
+
timeoutMs: { type: 'integer', description: '超时毫秒' },
|
|
1112
1378
|
},
|
|
1113
1379
|
output: {
|
|
1114
1380
|
schema: { type: 'json' },
|
|
1115
1381
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1116
1382
|
},
|
|
1117
|
-
async execute(args
|
|
1118
|
-
const q = unwrapQuery<{ action?: string; channel?: string; method?: string; args?: string; timeoutMs?: number }>(args
|
|
1383
|
+
async execute(args, _exec: unknown) {
|
|
1384
|
+
const q = unwrapQuery<{ action?: string; channel?: string; method?: string; args?: string; timeoutMs?: number }>(args)
|
|
1119
1385
|
const started = Date.now()
|
|
1120
1386
|
const dir = await ensureStateDir(config.stateRoot)
|
|
1121
1387
|
|
|
1388
|
+
// D-23 收编(issue #115):status/reach 两分支收敛为单一 AGENT_REACH 效应——
|
|
1389
|
+
// 解译器横切(trace/declined 面)统一,渲染逻辑留在工具层不变
|
|
1390
|
+
const itpAR = await interpretEffect({ effect: 'AGENT_REACH', params: { action: q.action, channel: q.channel, method: q.method, args: q.args, timeoutMs: q.timeoutMs } })
|
|
1391
|
+
if (!itpAR.result) return declinedObservation('AGENT_REACH', itpAR.trace)
|
|
1392
|
+
|
|
1122
1393
|
if (q.action === 'status' || (!q.action && !q.channel)) {
|
|
1123
|
-
const st =
|
|
1394
|
+
const st = itpAR.result as Awaited<ReturnType<typeof reachStatus>>
|
|
1124
1395
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(() => {})
|
|
1125
1396
|
const summary = st.via === 'agent-reach-cli'
|
|
1126
1397
|
? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}`
|
|
1127
|
-
: `Agent Reach
|
|
1398
|
+
: `Agent Reach 未装配(.venv 缺失)——可选依赖,机/酒/网页社媒读取之外的工具不受影响。\n补装:终端跑 npx gotry doctor --fix;整体依赖状态可调 gotry_doctor 工具查看。\n${st.evidence}`
|
|
1128
1399
|
return JSON.parse(JSON.stringify({
|
|
1129
1400
|
ok: st.ok, via: st.via, output: st.output, evidence: st.evidence, summary,
|
|
1130
1401
|
latency_ms: Date.now() - started,
|
|
@@ -1134,7 +1405,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1134
1405
|
if (!q.channel || !q.method) {
|
|
1135
1406
|
return JSON.parse(JSON.stringify({ ok: false, summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表' })) as Record<string, never>
|
|
1136
1407
|
}
|
|
1137
|
-
const r =
|
|
1408
|
+
const r = itpAR.result as Awaited<ReturnType<typeof reach>>
|
|
1138
1409
|
// 长结果干净截断:数组按条目边界保留(dsh 工具上限会拦腰断 JSON,模型只能看到半条)
|
|
1139
1410
|
const dataStr = (v: unknown): unknown => {
|
|
1140
1411
|
if (Array.isArray(v)) {
|
|
@@ -1159,7 +1430,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1159
1430
|
: r.verdict === 'needs-setup'
|
|
1160
1431
|
? `${q.channel}.${q.method} → 需配置(上游 check() 原话): ${r.setup ?? ''}\n${r.evidence}`
|
|
1161
1432
|
: r.verdict === 'not-installed'
|
|
1162
|
-
? `${q.channel}.${q.method} →
|
|
1433
|
+
? `${q.channel}.${q.method} → 上游未装配(可选依赖): ${r.setup ?? ''}\n整体依赖状态可调 gotry_doctor 查看;补装: npx gotry doctor --fix\n${r.evidence}`
|
|
1163
1434
|
: `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`
|
|
1164
1435
|
return JSON.parse(JSON.stringify({
|
|
1165
1436
|
ok: r.ok, channel: r.channel, method: q.method, verdict: r.verdict,
|
|
@@ -1168,10 +1439,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1168
1439
|
latency_ms: Date.now() - started,
|
|
1169
1440
|
})) as Record<string, never>
|
|
1170
1441
|
},
|
|
1171
|
-
presentCall: args => ({ card: 'generic', title: `Agent Reach:${String(
|
|
1442
|
+
presentCall: args => ({ card: 'generic', title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`, kind: 'fetch', rawInput: args }),
|
|
1172
1443
|
presentResult: (args, value) => {
|
|
1173
1444
|
const r = value as { verdict?: string; summary?: string }
|
|
1174
|
-
const q = (args
|
|
1445
|
+
const q = (args as { channel?: string; method?: string }) ?? {}
|
|
1175
1446
|
const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌'
|
|
1176
1447
|
return {
|
|
1177
1448
|
card: 'generic',
|
|
@@ -1181,6 +1452,54 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1181
1452
|
},
|
|
1182
1453
|
}))
|
|
1183
1454
|
|
|
1455
|
+
// ---- 依赖体检面(2026-09-02 迪拜 session 复盘:可选依赖未装配时,LLM 需要一个
|
|
1456
|
+
// 统一的状态入口来「显示状态 + 引导补装」,而不是在 not-installed 里各自摸索)----
|
|
1457
|
+
|
|
1458
|
+
registerGuarded(defineTool({
|
|
1459
|
+
name: 'gotry_doctor',
|
|
1460
|
+
description:
|
|
1461
|
+
'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. '
|
|
1462
|
+
+ 'Call this when ANY gotry tool returns not-installed / needs-setup, when the user asks 「体检/依赖状态/工具为什么不可用」, or BEFORE leaning on a channel for a plan. '
|
|
1463
|
+
+ 'Returns per-item status (ok/degraded/missing) with exact fix commands. '
|
|
1464
|
+
+ '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. '
|
|
1465
|
+
+ 'A markdown report is rendered for the workspace (gotry-state/doctor-report.md) so the sidebar workbench can preview it.',
|
|
1466
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
1467
|
+
parameters: {
|
|
1468
|
+
writeReport: { type: 'boolean', description: '默认 true,把 markdown 报告写进 gotry-state/doctor-report.md(侧栏工作台可预览)' },
|
|
1469
|
+
},
|
|
1470
|
+
output: {
|
|
1471
|
+
schema: { type: 'json' },
|
|
1472
|
+
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 900)) }],
|
|
1473
|
+
},
|
|
1474
|
+
async execute(args, _exec: unknown) {
|
|
1475
|
+
const q = unwrapQuery<{ writeReport?: boolean }>(args, 'writeReport')
|
|
1476
|
+
const report = await runDoctorChecks({ stateRoot: config.stateRoot ?? '.' })
|
|
1477
|
+
// 报告落 gotry-state(侧栏工作台预览面);写失败不阻塞体检结论本身
|
|
1478
|
+
let reportPath: string | undefined
|
|
1479
|
+
if (q?.writeReport !== false) {
|
|
1480
|
+
try {
|
|
1481
|
+
const { writeFile } = await import('node:fs/promises')
|
|
1482
|
+
const dir = await ensureStateDir(config.stateRoot)
|
|
1483
|
+
reportPath = join(dir, 'doctor-report.md')
|
|
1484
|
+
await writeFile(reportPath, renderDoctorReportMd(report), 'utf-8')
|
|
1485
|
+
} catch { reportPath = undefined }
|
|
1486
|
+
}
|
|
1487
|
+
const icon = (s: string) => (s === 'ok' ? '✅' : s === 'degraded' ? '⚠️' : '❌')
|
|
1488
|
+
const lines = report.items.map(i => `${icon(i.status)} ${i.label}:${i.detail}${i.fix ? `\n ↳ 修复: ${i.fix}` : ''}`)
|
|
1489
|
+
const summary = `${report.summary}\n${lines.join('\n')}${reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)'}`
|
|
1490
|
+
return JSON.parse(JSON.stringify({ ok: true, verdict: report.ok ? 'all-clear' : 'needs-attention', items: report.items, report_path: reportPath, evidence: `[doctor@${new Date().toISOString()}]`, summary })) as Record<string, never>
|
|
1491
|
+
},
|
|
1492
|
+
presentCall: _args => ({ card: 'generic', title: '🩺 依赖体检', kind: 'execute', rawInput: {} }),
|
|
1493
|
+
presentResult: (_args, value) => {
|
|
1494
|
+
const r = value as { verdict?: string; summary?: string }
|
|
1495
|
+
return {
|
|
1496
|
+
card: 'generic',
|
|
1497
|
+
title: `🩺 依赖体检:${r.verdict === 'all-clear' ? '✅ 全部就绪' : '🔧 有项待处理'}`,
|
|
1498
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
1499
|
+
}
|
|
1500
|
+
},
|
|
1501
|
+
}))
|
|
1502
|
+
|
|
1184
1503
|
// ---- 产物面(issue #25):agent 生成的文件可在 dsh 内发现与阅读,不再只是本地文件名 ----
|
|
1185
1504
|
|
|
1186
1505
|
registerGuarded(defineTool({
|
|
@@ -1190,18 +1509,15 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1190
1509
|
'in the working directory (trip plans etc.). READ-ONLY discovery. ' +
|
|
1191
1510
|
'Use when the user asks to see/open/revisit a previously generated artifact ' +
|
|
1192
1511
|
'(「看看刚才生成的行程」「上次的规划在哪」「打开那个 md」) — list first, then read with gotry_artifacts_read.',
|
|
1512
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
1193
1513
|
parameters: {
|
|
1194
|
-
|
|
1195
|
-
type: 'json',
|
|
1196
|
-
required: true,
|
|
1197
|
-
description: '{ limit?: 20 }',
|
|
1198
|
-
},
|
|
1514
|
+
limit: { type: 'integer', description: '最多返回条数,默认 20' },
|
|
1199
1515
|
},
|
|
1200
1516
|
output: {
|
|
1201
1517
|
schema: { type: 'json' },
|
|
1202
1518
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1203
1519
|
},
|
|
1204
|
-
async execute(args
|
|
1520
|
+
async execute(args, _exec: unknown) {
|
|
1205
1521
|
const q = unwrapQuery<{ limit?: number }>(args, 'limit')
|
|
1206
1522
|
const r = await listArtifacts({ stateRoot: config.stateRoot ?? '.', limit: q.limit })
|
|
1207
1523
|
const lines = r.artifacts.map(a =>
|
|
@@ -1229,19 +1545,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1229
1545
|
'Input: the path from gotry_artifacts_list, or a bare async ticket id (e.g. dp-xxxx). ' +
|
|
1230
1546
|
'Optional offset (1-based) / limit window for paging large files. ' +
|
|
1231
1547
|
'READ-ONLY; text artifacts only (md/txt/json/jsonl/csv/log/yaml).',
|
|
1548
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;path required → 宿主权入口拒畸形参数(同 flyai/hotel 刀法)
|
|
1232
1549
|
parameters: {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
description: '{ path: "<list 返回的路径或工单 id>", offset?: 1, limit?: 400 }',
|
|
1237
|
-
},
|
|
1550
|
+
path: { type: 'string', required: true, description: 'list 返回的路径或工单 id' },
|
|
1551
|
+
offset: { type: 'integer', description: '起始行号(1 起)' },
|
|
1552
|
+
limit: { type: 'integer', description: '窗口行数,默认 400' },
|
|
1238
1553
|
},
|
|
1239
1554
|
output: {
|
|
1240
1555
|
schema: { type: 'json' },
|
|
1241
1556
|
render: (_args, value) => [{ type: 'text', text: String((value as { content?: string }).content ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1242
1557
|
},
|
|
1243
|
-
async execute(args
|
|
1244
|
-
const q =
|
|
1558
|
+
async execute(args, _exec) {
|
|
1559
|
+
const q = args
|
|
1560
|
+
if (!q.path) throw new Error('gotry_artifacts_read requires path')
|
|
1245
1561
|
if (!q.path) return JSON.parse(JSON.stringify({ ok: false, error: 'path 必填(来自 gotry_artifacts_list)' })) as Record<string, never>
|
|
1246
1562
|
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', path: q.path, offset: q.offset, limit: q.limit })
|
|
1247
1563
|
if (!r.ok) return JSON.parse(JSON.stringify(r)) as Record<string, never>
|
|
@@ -1250,7 +1566,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1250
1566
|
summary: `${r.path}(${r.totalLines} 行)第 ${r.offset}-${r.offset + r.lines.length - 1} 行${r.windowed ? `(共 ${r.totalLines} 行,可翻页)` : ''}`,
|
|
1251
1567
|
})) as Record<string, never>
|
|
1252
1568
|
},
|
|
1253
|
-
presentCall: args => ({ card: 'generic', title: `读产物:${
|
|
1569
|
+
presentCall: args => ({ card: 'generic', title: `读产物:${args.path ?? ''}`, kind: 'read', rawInput: args }),
|
|
1254
1570
|
presentResult: (_args, value) => {
|
|
1255
1571
|
const r = value as unknown as { ok?: boolean; path?: string; offset?: number; lines?: Array<{ number: number; text: string }>; totalLines?: number; lang?: string; content?: string; error?: string }
|
|
1256
1572
|
if (!r.ok) {
|
|
@@ -1285,18 +1601,20 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1285
1601
|
+ '「联程」without protected_connection=true; policy claims without 「截至 YYYY-MM-DD」; unconditional ✓ on unverified claims. '
|
|
1286
1602
|
+ 'Optional itinerary object enables machine invariants (hotel_nights + onboard_nights = total nights, O&D segments vs flight legs, '
|
|
1287
1603
|
+ 'budget floor ≥ sum of item minimums). verdict=blocked ⇒ fix or downgrade wording — never present as a verified plan.',
|
|
1604
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;markdown/path 二选一无公共 required
|
|
1605
|
+
// → interpretArgs 容忍层;itinerary 保 object+additionalProperties:true(嵌套形状归一在闸内,
|
|
1606
|
+
// #118 D-26 渲染原语切片再收紧),给模型「这是对象」的可见信号
|
|
1288
1607
|
parameters: {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
},
|
|
1608
|
+
markdown: { type: 'string', description: '产物全文;与 path 二选一' },
|
|
1609
|
+
path: { type: 'string', description: '产物路径(artifacts_list 返回的);与 markdown 二选一' },
|
|
1610
|
+
tripYear: { type: 'integer', description: '行程年份,如 2027(日期归一用)' },
|
|
1611
|
+
itinerary: { type: 'object', additionalProperties: true, 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? }' },
|
|
1294
1612
|
},
|
|
1295
1613
|
output: {
|
|
1296
1614
|
schema: { type: 'json' },
|
|
1297
1615
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1298
1616
|
},
|
|
1299
|
-
async execute(args
|
|
1617
|
+
async execute(args, _exec: unknown) {
|
|
1300
1618
|
const q = unwrapQuery<{ markdown?: string; path?: string; tripYear?: number; itinerary?: Record<string, unknown> }>(args, 'markdown')
|
|
1301
1619
|
const avMap = await loadAirlineAirportMap()
|
|
1302
1620
|
if (!avMap) {
|
|
@@ -1318,7 +1636,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1318
1636
|
: `事实闸 BLOCKED(${report.violations.length} 违例,${report.traceable}/${report.claims_checked} claim 可回溯)——不得宣称「已验证方案」:\n${lines.join('\n')}\n修正路径:逐条改成 exact-date 源返回的事实,或降级为「未确认/当前不可售,到 D-xx 复核」;exact-date miss 的 route+date 不得用历史班期/相邻日期/航线页填充。`
|
|
1319
1637
|
return JSON.parse(JSON.stringify({ ok: true, ...report, summary })) as Record<string, never>
|
|
1320
1638
|
},
|
|
1321
|
-
presentCall: args => ({ card: 'generic', title: '产物事实闸', kind: 'execute', rawInput: args
|
|
1639
|
+
presentCall: args => ({ card: 'generic', title: '产物事实闸', kind: 'execute', rawInput: args }),
|
|
1322
1640
|
presentResult: (_args, value) => {
|
|
1323
1641
|
const r = value as { verdict?: string; violations?: unknown[]; summary?: string }
|
|
1324
1642
|
return {
|