@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.
Files changed (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. 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 { anythingSearch } from '../capabilities/anything.js';
18
- import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.js';
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 { installToolBudget } from './tool-budget.js';
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: 'json',
197
+ type: 'object',
198
+ additionalProperties: false,
154
199
  required: true,
155
- description: 'The full engine payload: { request, candidates }.'
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: 'json',
277
+ type: 'object',
278
+ additionalProperties: false,
217
279
  required: true,
218
- description: '{ weights: {escape_rest: 0.7, ...}, evidence: [user quotes...], hard: {wake_not_before, min_arrival_energy_pct} }'
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
- query: {
318
- type: 'json',
319
- required: true,
320
- description: '{ action?: "recall"|"confirm-outcome", days?, budgetCny?, month?, wishId?, attribution?: "helpful"|"harmful"|"neutral", detail?, tripStart?: "行程起始(确认成行时挂时间线)" }'
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.query
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: 'json',
600
+ type: 'object',
601
+ additionalProperties: false,
428
602
  required: true,
429
- description: '{ label: "爸爸", constraints: { mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }, evidence: "<用户原话>" }'
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
- query: {
483
- type: 'json',
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: '{ destination: "大理", start: "2025-10-01 或绝对日期表达", end?: 同上, companions?: ["爸爸"], evidence: "<用户原话>" }'
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 = unwrapQuery(args, 'destination');
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.query
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/occupancy. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — ' + 'the code layer resolves them against the time anchor; unresolved expressions degrade to an undated search with an explicit ' + 'date_notes entry instead of guessing. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, ' + 'or [static-pack:estimate]) per the L4 invariant.',
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
- query: {
549
- type: 'json',
759
+ destination: {
760
+ type: 'string',
550
761
  required: true,
551
- description: '{ destination: "<目的地城市>", checkIn?: "YYYY-MM-DD 或自然表达(下周五/8.20)", checkOut?: 同上, occupancy?: { adults: 2 } }'
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 = unwrapQuery(args, 'destination');
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: `酒店搜索:${String(args.query?.destination ?? '')}`,
840
+ title: `酒店搜索:${args.destination ?? ''}`,
618
841
  kind: 'search',
619
- rawInput: args.query
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
- query: {
686
- type: 'json',
687
- required: true,
688
- description: '{ place: "<城市名>", month?: 8, mode?: "forecast"|"climate", days?: 7 }'
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 = unwrapQuery(args, 'place');
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: `天气:${String(args.query?.place ?? '')}`,
1009
+ title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`,
764
1010
  kind: 'fetch',
765
- rawInput: args.query
1011
+ rawInput: args
766
1012
  }),
767
1013
  presentResult: (args, value)=>{
768
1014
  const r = value;
769
- const place = String(args.query?.place ?? '');
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
- query: {
788
- type: 'json',
1033
+ callsign: {
1034
+ type: 'string',
789
1035
  required: true,
790
- description: '{ callsign: "EK329", airport?: "OMDB", timeoutMs?: 10000 }'
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 = unwrapQuery(args, 'callsign');
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: `飞行校验:${String(args.query?.callsign ?? '')}`,
1095
+ title: `飞行校验:${args.callsign ?? ''}`,
842
1096
  kind: 'fetch',
843
- rawInput: args.query
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": { kind, from, to, date } (中文城市名, date YYYY-MM-DD) — real schedules & prices, split 直达/中转 in results. ' + 'kind="hotel": { 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]. Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.',
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
- query: {
851
- type: 'json',
1104
+ kind: {
1105
+ type: 'string',
1106
+ enum: [
1107
+ 'flight',
1108
+ 'train',
1109
+ 'hotel'
1110
+ ],
852
1111
  required: true,
853
- description: 'kind=flight|train: { kind, from: "上海", to: "丽江", date: "2026-10-01" }; kind="hotel": { kind:"hotel", to:"大理", checkIn?: "YYYY-MM-DD", checkOut?: "YYYY-MM-DD", keyWords?: "洱海" }'
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 = unwrapQuery(args, 'from');
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: `官方检索:${String(args.query?.kind ?? 'flight')}`,
1261
+ title: `官方检索:${args.kind}`,
965
1262
  kind: 'fetch',
966
- rawInput: args.query
1263
+ rawInput: args
967
1264
  }),
968
1265
  presentResult: (args, value)=>{
969
- const isHotel = String(args.query?.kind) === 'hotel';
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
- query: {
989
- type: 'json',
990
- required: true,
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 r = await sessionLogin({
1008
- waitMs: typeof q?.waitSeconds === 'number' ? q.waitSeconds * 1000 : undefined
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 logged-in browser session (Ctrip flights today; the account channel, not an anonymous instance). ' + '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). ' + 'Currently ctrip-flight: sniffs the site search API for structured options. Evidence [会话:ctrip-flight@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).',
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
- query: {
1048
- type: 'json',
1049
- required: true,
1050
- description: '{ from: "上海", to: "丽江", date: "2026-10-01" }'
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, 'from');
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: `会话检索:${String(args.query?.from ?? '')}`,
1522
+ title: callTitle,
1099
1523
  kind: 'fetch',
1100
- rawInput: args.query
1101
- }),
1102
- presentResult: (_args, value)=>{
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' ? `会话 ${r.options.length} 条` : r.verdict === 'needs-login' ? '需登录' : needsExt ? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)' : r.verdict ?? '降级';
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: `会话检索:${label}`,
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
- query: {
1128
- type: 'json',
1555
+ keyword: {
1556
+ type: 'string',
1129
1557
  required: true,
1130
- description: '{ keyword: "<搜索关键词>", contentType?: "city"|"hotel", parentDestinationId?: "?", timeoutMs?: 12000 }'
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 = unwrapQuery(args, 'keyword');
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 r = await anythingSearch(q);
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:${String(args.query?.keyword ?? '')}`,
1624
+ title: `Anything search:${args.keyword ?? ''}`,
1175
1625
  kind: 'search',
1176
- rawInput: args.query
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
- query: {
1198
- type: 'json',
1647
+ url: {
1648
+ type: 'string',
1199
1649
  required: true,
1200
- description: '{ url: "https://example.com", timeoutMs?: 20000 }'
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 = unwrapQuery(args, 'url');
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 r = await readUrl({
1225
- url: q.url,
1226
- timeoutMs: q.timeoutMs
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: `读网页:${String(args.query?.url ?? '')}`,
1703
+ title: `读网页:${args.url ?? ''}`,
1245
1704
  kind: 'fetch',
1246
- rawInput: args.query
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
- query: {
1254
- type: 'json',
1712
+ url: {
1713
+ type: 'string',
1255
1714
  required: true,
1256
- description: '{ url: "https://www.youtube.com/watch?v=...", lang?: "zh-Hans,zh,en" }'
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 = unwrapQuery(args, 'url');
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 r = await videoSubtitle({
1279
- url: q.url,
1280
- lang: q.lang
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: `视频字幕:${String(args.query?.url ?? '')}`,
1763
+ title: `视频字幕:${args.url ?? ''}`,
1296
1764
  kind: 'fetch',
1297
- rawInput: args.query
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: 'json',
1773
+ type: 'string',
1306
1774
  required: true,
1307
- description: '{ query: "agent-reach", limit?: 5 }'
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 = unwrapQuery(args, 'query');
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 r = await githubSearch({
1330
- query: q.query,
1331
- limit: q.limit
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 搜索:${String(args.query?.query ?? '')}`,
1823
+ title: `GitHub 搜索:${args.query ?? ''}`,
1347
1824
  kind: 'search',
1348
- rawInput: args.query
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
- query: {
1356
- type: 'json',
1357
- required: true,
1358
- description: '{ action: "status" } 或 { action: "reach", channel: "<上游渠道名,如 web/v2ex/xueqiu>", method: "<上游方法名,如 read/get_hot_topics/get_stock_quote>", args?: "<空格分隔参数>" }'
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, 'channel');
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 = await reachStatus(q.timeoutMs);
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 未装:\n${st.output}\n${st.evidence}`;
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 = await reach({
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} → 上游未装: ${r.setup ?? ''}\n${r.evidence}` : `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`;
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.query?.channel ?? '')}.${String(args.query?.method ?? 'status')}`,
1933
+ title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`,
1436
1934
  kind: 'fetch',
1437
- rawInput: args.query
1935
+ rawInput: args
1438
1936
  }),
1439
1937
  presentResult: (args, value)=>{
1440
1938
  const r = value;
1441
- const q = args.query ?? {};
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
- query: {
1460
- type: 'json',
1461
- required: true,
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
- query: {
1516
- type: 'json',
2080
+ path: {
2081
+ type: 'string',
1517
2082
  required: true,
1518
- description: '{ path: "<list 返回的路径或工单 id>", offset?: 1, limit?: 400 }'
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 = unwrapQuery(args, 'path');
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: `读产物:${String(args.query?.path ?? '')}`,
2126
+ title: `读产物:${args.path ?? ''}`,
1553
2127
  kind: 'read',
1554
- rawInput: args.query
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
- query: {
1592
- type: 'json',
1593
- required: true,
1594
- description: '{ markdown?: "<产物全文>", path?: "<产物路径(artifacts_list 返回的)>— 二选一", tripYear?: 2027, itinerary?: { 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? } }'
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.query
2237
+ rawInput: args
1652
2238
  }),
1653
2239
  presentResult: (_args, value)=>{
1654
2240
  const r = value;