@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +29 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +63 -5
  5. package/data/flights_2026.json +7 -4
  6. package/data/hotels_2026.json +102 -15
  7. package/data/time-slot-eval.json +384 -0
  8. package/data/yunnan-pack.json +2 -1
  9. package/dist/capabilities/agent-reach-bridge.py +90 -0
  10. package/dist/capabilities/anything.js +2 -2
  11. package/dist/capabilities/flyai.js +131 -0
  12. package/dist/capabilities/hbcli.js +11 -6
  13. package/dist/capabilities/incident-log.js +2 -1
  14. package/dist/capabilities/session/action-cache.js +120 -0
  15. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  16. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  17. package/dist/capabilities/session/extract.js +40 -0
  18. package/dist/capabilities/session/read-guard.js +56 -0
  19. package/dist/capabilities/session/transport.js +95 -0
  20. package/dist/capabilities/session-search.js +95 -0
  21. package/dist/scripts/action-cache-tests.js +107 -0
  22. package/dist/scripts/agent-reach-tests.js +1 -1
  23. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  24. package/dist/scripts/async-collect.js +26 -7
  25. package/dist/scripts/companion-tests.js +112 -0
  26. package/dist/scripts/hbcli-tests.js +18 -2
  27. package/dist/scripts/ledger-tests.js +344 -0
  28. package/dist/scripts/ledger-workflow-crash.js +42 -0
  29. package/dist/scripts/memory-capture-tests.js +77 -0
  30. package/dist/scripts/memory-decay-tests.js +83 -0
  31. package/dist/scripts/memory-metrics.js +24 -0
  32. package/dist/scripts/nudge-digest.js +83 -0
  33. package/dist/scripts/probe-poi-tests.js +9 -0
  34. package/dist/scripts/replay.js +73 -1
  35. package/dist/scripts/session-attach-diagnose.js +31 -0
  36. package/dist/scripts/session-attach-poc.js +65 -0
  37. package/dist/scripts/session-attach-wait.js +41 -0
  38. package/dist/scripts/session-extract-tests.js +81 -0
  39. package/dist/scripts/session-login.js +48 -0
  40. package/dist/scripts/session-tests.js +162 -0
  41. package/dist/scripts/skeleton-check.js +3 -1
  42. package/dist/scripts/skills-contract-tests.js +85 -0
  43. package/dist/scripts/smoke.js +242 -4
  44. package/dist/scripts/state-cli-tests.js +136 -0
  45. package/dist/scripts/state-cli.js +234 -0
  46. package/dist/scripts/time-eval-tests.js +318 -0
  47. package/dist/scripts/travel-timeline-tests.js +123 -0
  48. package/dist/scripts/unified-tests.js +1 -1
  49. package/dist/src/companions.js +112 -0
  50. package/dist/src/dsh-llm.js +27 -7
  51. package/dist/src/index.js +600 -121
  52. package/dist/src/loop.js +89 -25
  53. package/dist/src/memory-capture.js +40 -0
  54. package/dist/src/memory-decay.js +31 -0
  55. package/dist/src/memory-utility.js +55 -0
  56. package/dist/src/mock-llm.js +6 -1
  57. package/dist/src/slot-spec.js +165 -0
  58. package/dist/src/state-ledger.js +848 -0
  59. package/dist/src/time-anchor.js +100 -0
  60. package/dist/src/tool-packet.js +12 -0
  61. package/dist/src/travel-slots.js +144 -0
  62. package/dist/src/travel-timeline.js +78 -0
  63. package/dist/src/unified.js +3 -1
  64. package/dist/src/wish-pool.js +27 -0
  65. package/package.json +6 -2
  66. package/ts/capabilities/hbcli.ts +6 -6
  67. package/ts/capabilities/incident-log.ts +6 -3
  68. package/ts/package.json +3 -0
  69. package/ts/scripts/skeleton-check.ts +5 -1
  70. package/ts/src/dsh-llm.ts +27 -7
  71. package/ts/src/index.ts +439 -91
  72. package/ts/src/loop.ts +109 -34
  73. package/ts/src/mock-llm.ts +13 -1
  74. package/ts/src/unified.ts +6 -1
  75. package/ts/cordis.gotry-patch.yml +0 -36
package/ts/src/loop.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { InterviewQuestion, TripState, TravelerProfile, Turn, CalendarState, SolveResult } from './contracts.ts'
10
10
  import type { JourneySpecTS } from './unified.ts'
11
+ import type { TravelSlotExtraction } from './travel-slots.ts'
11
12
  import { anythingSearch } from '../capabilities/anything.ts'
12
13
 
13
14
  /** LLM 端口:S2 mock(确定性剧本) / S4 真(dsh 运行时)。循环不感知差异。 */
@@ -20,6 +21,12 @@ export interface LlmPort {
20
21
  }>
21
22
  /** 翻译:对话历史 → 统一模型 spec;约束未齐时返回 null(循环继续访谈) */
22
23
  extractSpec(history: Turn[], state: TripState): Promise<JourneySpecTS | null>
24
+ /**
25
+ * 槽位抽取(travel_slot_extraction.v1):差旅 intake 面。时间表达逐字保留,
26
+ * 过期判定在确定性层(flagExpiredSlots),实现方负责注入时间锚点卡。
27
+ * now 可注入(评测用固定锚点保证确定性;生产省略取系统时钟)。
28
+ */
29
+ extractSlots(history: Turn[], now?: Date): Promise<TravelSlotExtraction | null>
23
30
  /** 问句润色(ADR-9:确定性驱动,LLM 只管语气) */
24
31
  polishQuestion(q: InterviewQuestion): Promise<string>
25
32
  /** 结果解释(S3 接 solve 后启用) */
@@ -235,7 +242,7 @@ export async function runTurn(
235
242
 
236
243
  for (const q of blocking) parts.push(await llm.polishQuestion(q))
237
244
 
238
- // 约束齐备(无阻塞问题)→ 翻译 spec → **校验闸** → 求解 → 渲染
245
+ // 约束齐备(无阻塞问题)→ 翻译 spec → **校验闸 + 日期一致性闸** → 求解 → 渲染
239
246
  if (blocking.length === 0 && solve) {
240
247
  const spec = await llm.extractSpec([...history, { role: 'user', text: userMsg }], state)
241
248
  // 场景路由:erhai 候选标记 → 候选求解(洱海金标准的引擎判定)——纯 TS unify 路径
@@ -258,6 +265,20 @@ export async function runTurn(
258
265
  } else {
259
266
  const invalid = spec ? validateSpec(spec) : '翻译器未产出 spec'
260
267
  if (spec && !invalid) {
268
+ // D-10 切片 C:spec↔槽位日期一致性闸(ADR-10 翻译≠造数的执行点)。
269
+ // LLM 翻译的 spec 日期与代码层换算的槽位日期分歧时不求解、不猜,追问确认——
270
+ // 日期是求解关键输入,错日期 = 错判决。槽位缺失/unresolved 不参与(不造假阳性)。
271
+ const ext = await llm.extractSlots([...history, { role: 'user', text: userMsg }])
272
+ if (ext) {
273
+ const { resolveSlots, specDateMismatches } = await import('./slot-spec.ts')
274
+ const { buildTimeAnchor } = await import('./time-anchor.ts')
275
+ const mismatches = specDateMismatches(spec, resolveSlots(ext, buildTimeAnchor()))
276
+ if (mismatches.length > 0) {
277
+ const lines = mismatches.map(m => `- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`)
278
+ parts.push(`⚠️ 日期分歧,确认后我再算(以你为准):\n${lines.join('\n')}`)
279
+ return { reply: parts.join('\n\n'), state }
280
+ }
281
+ }
261
282
  state.spec = spec
262
283
  state.solve = await solve(spec)
263
284
  parts.push(llm.render ? await llm.render(state) : renderSolve(state))
@@ -271,66 +292,120 @@ export async function runTurn(
271
292
  return { reply: parts.join('\n\n'), state }
272
293
  }
273
294
 
274
- // ---- 异步工单持久化(S5 编排半段):真正的「一小时后」必须跨进程存续 ----
275
- // 请求时落盘(ticket+state 快照);任意后续进程(如 loopx 驱动的 tick)执行
276
- // async-collect 加载、求解、写回交付物。状态目录属用户数据(红线 6)。
295
+ // ---- 异步工单持久化(S5 编排半段 → ADR-15 durable 工单):真正的「一小时后」必须跨进程存续 ----
296
+ // 权威 = 账本 workflow_runs/workflow_steps(单事务;崩溃后 done 步骤不重执行);
297
+ // {id}.json / {id}.deliverable.md 降级为视图(AGENTS.md 清扫合同 + 人工检视),
298
+ // tmp+rename 原子写——此前裸 writeFile 的非原子缺口(RFC §1.1)就此关闭。
277
299
 
278
300
  import { join } from 'node:path'
279
- import { mkdir } from 'node:fs/promises'
280
- import { readFile, writeFile } from 'node:fs/promises'
301
+ import { mkdir, rename, writeFile, readFile } from 'node:fs/promises'
302
+ import { ensureLedger, openLedgerIfExists, type StateLedger } from './state-ledger.ts'
281
303
 
282
- const ASYNC_DIR = 'gotry-state/async'
304
+ async function asyncDir(stateRoot: string): Promise<string> {
305
+ const root = stateRoot === '.' ? process.cwd() : stateRoot
306
+ const dir = join(root, 'gotry-state', 'async')
307
+ await mkdir(dir, { recursive: true })
308
+ return dir
309
+ }
283
310
 
284
- async function asyncPath(name: string): Promise<string> {
285
- await mkdir(ASYNC_DIR, { recursive: true })
286
- return join(ASYNC_DIR, name)
311
+ async function atomicWrite(path: string, text: string): Promise<void> {
312
+ const tmp = `${path}.tmp`
313
+ await writeFile(tmp, text, 'utf-8')
314
+ await rename(tmp, path)
287
315
  }
288
316
 
289
- export async function persistAsyncTicket(ticket: AsyncTicket, state: TripState): Promise<string> {
290
- const p = await asyncPath(`${ticket.id}.json`)
291
- await writeFile(p, JSON.stringify({ ticket, state }, null, 2), 'utf-8')
317
+ export async function persistAsyncTicket(ticket: AsyncTicket, state: TripState, stateRoot = '.'): Promise<string> {
318
+ const ledger = ensureLedger(stateRoot)
319
+ ledger.createWorkflowRun({ id: ticket.id, goal: ticket.objective, ticket, state })
320
+ const dir = await asyncDir(stateRoot)
321
+ const p = join(dir, `${ticket.id}.json`)
322
+ await atomicWrite(p, JSON.stringify({ ticket, state }, null, 2))
292
323
  return p
293
324
  }
294
325
 
295
- export async function loadAsyncTicket(ticketId: string): Promise<{ ticket: AsyncTicket; state: TripState } | null> {
326
+ export async function loadAsyncTicket(ticketId: string, stateRoot = '.'): Promise<{ ticket: AsyncTicket; state: TripState } | null> {
327
+ // 账本优先;无账本的 root 回退旧 json 文件(只读,兼容存量已交付工单)
328
+ const ledger = openLedgerIfExists(stateRoot)
329
+ const run = ledger?.getWorkflowRun(ticketId)
330
+ if (run) {
331
+ return {
332
+ ticket: JSON.parse(run.ticket_json) as AsyncTicket,
333
+ state: JSON.parse(run.state_json) as TripState,
334
+ }
335
+ }
296
336
  try {
297
- const p = await asyncPath(`${ticketId}.json`)
337
+ const p = join(await asyncDir(stateRoot), `${ticketId}.json`)
298
338
  return JSON.parse(await readFile(p, 'utf-8')) as { ticket: AsyncTicket; state: TripState }
299
339
  } catch {
300
340
  return null
301
341
  }
302
342
  }
303
343
 
304
- export async function settleAsyncTicket(ticketId: string, reply: string): Promise<string> {
305
- const p = await asyncPath(`${ticketId}.deliverable.md`)
306
- await writeFile(p, reply, 'utf-8')
344
+ export async function settleAsyncTicket(ticketId: string, reply: string, stateRoot = '.'): Promise<string> {
345
+ openLedgerIfExists(stateRoot)?.settleWorkflowRun(ticketId, reply)
346
+ const dir = await asyncDir(stateRoot)
347
+ const p = join(dir, `${ticketId}.deliverable.md`)
348
+ await atomicWrite(p, reply)
307
349
  return p
308
350
  }
309
351
 
352
+ /**
353
+ * 可日志化 solve 端口(ADR-15 步骤日志,intent-before-execute):
354
+ * 先记 intent 再执行,done 后结果落账本——任意进程恢复时 done 的步骤直接复用
355
+ * 结果不重执行(exactly-once:LLM/求解调用不重复花钱),intent 悬挂的重试。
356
+ */
357
+ export function makeJournaledSolvePort(ledger: StateLedger, runId: string, solve: SolvePort, opts?: { onRealSolve?: () => void }): SolvePort {
358
+ return async spec => {
359
+ const step = ledger.getWorkflowStep(runId, 'solve')
360
+ if (step?.status === 'done' && step.result) {
361
+ return JSON.parse(step.result) as Awaited<ReturnType<SolvePort>>
362
+ }
363
+ ledger.markStepIntent(runId, 'solve')
364
+ opts?.onRealSolve?.()
365
+ const r = await solve(spec)
366
+ ledger.markStepDone(runId, 'solve', r)
367
+ return r
368
+ }
369
+ }
370
+
310
371
  /**
311
372
  * probePoi:从 user msg 探测"查 POI/酒店" 信号,返回关键词。
312
- * 触发的不是关键词,是**结构**:句首含"查/搜/找" + 后接地理/酒店类名词。
373
+ * 触发的不是关键词,是**结构**;关键词抓取有方向性——
374
+ * 1) 显式搜索动词(查/搜/找/看看/推荐/告诉我/检索)+ 宾语在动词后;
375
+ * 2) 住宿名词(酒店/民宿/客栈/饭店):优先取名词**之后**的名称段(须含拉丁/数字,
376
+ * 「我订了酒店:The Title…」→ The Title…);退路是名词前 2-6 字紧邻地名
377
+ * (「大理酒店」→ 大理;「机票和」类垃圾后缀拒);
378
+ * 3) 开放问(有什么/玩什么/有哪些):取名词前的地名;
379
+ * 4) 短裸地名:≤12 字、无标点、无陈述动词(「大理」✓;「明天有空」「我的工作时间是…」✗——
380
+ * 2026-08-28 巡检收紧:旧版 ≤24 直通把访谈答案整句当关键词,垃圾 hbcli 调用+证据噪音)。
313
381
  * 不是意图(改求解器),只是 datasources 编排层的"提早调 anything"提示。
314
382
  */
315
383
  export function probePoi(msg: string): string | null {
316
384
  const trimmed = msg.trim()
317
385
  if (!trimmed) return null
318
- // 短查询(<=24 字符 且非问句): 优先剥 trigger 词,再 fall-through 让内容触发也跑
319
- const isShort = trimmed.length <= 24 && !/[??!!。.,,]/.test(trimmed)
320
- if (isShort) {
321
- const stripped = trimmed.replace(/^(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*/, '').trim()
322
- // 即使没 trigger 词,只要 stripped 仍是 ≥2 字符的非空串就当关键词
323
- if (stripped.length >= 2) return stripped.slice(0, 24)
324
- }
325
- // 触发模式:查/搜/找/看看/推荐/告诉我/检索 + 地理/酒店类名词(贪吃但只切前 24 字符)
386
+ const cutAtPunct = (s: string): string => s.split(/[,,。.;;!!??]/)[0]?.trim() ?? ''
387
+
388
+ // 1) 显式搜索动词 + 宾语
326
389
  const m = trimmed.match(/(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*(.{2,24})/)
327
- if (m) return m[2].trim().slice(0, 24)
328
- // 另一模式:含"酒店"/"民宿"/"客栈" 触发酒店查
329
- if (/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/.test(trimmed)) {
330
- // 尝试提取前面的地名关键词
331
- const place = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/)
332
- if (place) return place[1]
333
- return trimmed.replace(/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/, '').trim().slice(0, 24) || trimmed.slice(0, 24)
390
+ if (m?.[2]) {
391
+ const obj = cutAtPunct(m[2])
392
+ if (obj.length >= 2) return obj.slice(0, 24)
393
+ }
394
+ // 2) 住宿名词:名称段在名词后
395
+ if (/(酒店|民宿|客栈|饭店)/.test(trimmed)) {
396
+ const parts = trimmed.split(/(酒店|民宿|客栈|饭店)/)
397
+ const after = cutAtPunct((parts[2] ?? '').replace(/^[的::\s]+/, ''))
398
+ if (after.length >= 2 && /[A-Za-z0-9]/.test(after)) return after.slice(0, 24)
399
+ const nounAt = trimmed.search(/(酒店|民宿|客栈|饭店)/)
400
+ const pre = trimmed.slice(Math.max(0, nounAt - 6), nounAt).match(/[一-龥]{2,6}$/)
401
+ if (pre && !/(机票|车票|和|做|订|的)$/.test(pre[0])) return pre[0]
402
+ }
403
+ // 3) 开放问:地名在疑问词前
404
+ const q = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:有什么|玩什么|有哪些)/)
405
+ if (q?.[1]) return q[1]
406
+ // 4) 短裸地名(≤12 字,无标点,无陈述动词)
407
+ if (trimmed.length <= 12 && !/[,,。.;;!!??::]/.test(trimmed) && !/[是有在要想得到说订吧呢]/.test(trimmed)) {
408
+ return trimmed.slice(0, 24)
334
409
  }
335
410
  return null
336
411
  }
@@ -7,6 +7,7 @@
7
7
  import type { LlmPort } from './loop.ts'
8
8
  import type { InterviewQuestion, TravelerProfile, TripState, Turn, CalendarState } from './contracts.ts'
9
9
  import type { JourneySpecTS } from './unified.ts'
10
+ import type { TravelSlotExtraction } from './travel-slots.ts'
10
11
  import { parseFlightPackToSpec } from './unified.ts'
11
12
 
12
13
  interface ScriptStep {
@@ -16,6 +17,12 @@ interface ScriptStep {
16
17
  profile?: Partial<TravelerProfile>
17
18
  }
18
19
 
20
+ /** 槽位抽取剧本:评测 harness 把 golden 逐条挂进来,mock 原样回吐(管道自测用) */
21
+ export interface SlotScriptStep {
22
+ when: string
23
+ extraction: TravelSlotExtraction
24
+ }
25
+
19
26
  /** 剧本:来自真实 Kimi 对话里用户给出关键事实的时刻 */
20
27
  const SCRIPT: ScriptStep[] = [
21
28
  {
@@ -48,7 +55,7 @@ const SCRIPT: ScriptStep[] = [
48
55
  },
49
56
  ]
50
57
 
51
- export function createMockLlm(flightPackPath?: string): LlmPort {
58
+ export function createMockLlm(flightPackPath?: string, slotScript: SlotScriptStep[] = []): LlmPort {
52
59
  let stepIdx = 0
53
60
  return {
54
61
  async extractFacts(history: Turn[], _state: TripState) {
@@ -81,6 +88,11 @@ export function createMockLlm(flightPackPath?: string): LlmPort {
81
88
  spec.budgetCny = 9000
82
89
  return spec
83
90
  },
91
+ async extractSlots(history: Turn[]): Promise<TravelSlotExtraction | null> {
92
+ const last = history[history.length - 1]?.text ?? ''
93
+ const hit = slotScript.find(s => last.includes(s.when))
94
+ return hit ? structuredClone(hit.extraction) : null
95
+ },
84
96
  async polishQuestion(q: InterviewQuestion) {
85
97
  return `【${q.key}】${q.text}\n(为什么问:${q.why})`
86
98
  },
package/ts/src/unified.ts CHANGED
@@ -28,6 +28,8 @@ export interface MoveSpecTS {
28
28
  destTransferMin: number
29
29
  redEye?: boolean
30
30
  redEyeDurationMin?: number
31
+ /** D-6:红眼航段落地后接驳(机场→住处/办公室)的乘车补眠时长(分钟);机上已算,落地接驳未算 */
32
+ groundRecoveryMin?: number
31
33
  /** D-5:目的地相对出发地的时差(KMG-BKK=+60,DXB-SZX=-240) */
32
34
  tzOffsetMin?: number
33
35
  /** M-1:出发地 UTC 偏移(工作窗口换算用) */
@@ -227,7 +229,10 @@ function evaluateOptionMove(segId: string, mv: MoveSpecTS): LegReport & { d2d_mi
227
229
  let energy: number
228
230
  if (mv.redEye && (mv.redEyeDurationMin ?? 0) > 0) {
229
231
  const sleepH = ((mv.redEyeDurationMin ?? 0) - 60) / 60
230
- energy = Math.max(30, Math.min(75, 30 + 8 * sleepH))
232
+ // D-6 校准:机上睡眠上限 75;落地接驳(destTransferMin)乘车补眠回血(1h≈+5%,上限 80)
233
+ const groundH = (mv.groundRecoveryMin ?? mv.destTransferMin ?? 0) / 60
234
+ const recovery = Math.min(5, 5 * groundH)
235
+ energy = Math.max(30, Math.min(75 + recovery, 80, 30 + 8 * sleepH + recovery))
231
236
  } else {
232
237
  energy = 100 - 2 * 8
233
238
  if (wake < 5 * 60) energy -= 30
@@ -1,36 +0,0 @@
1
- # GoTry × dsh 成品组合(DeepSeek 原生——创始人官方 key)
2
- # 用法(在 ts/dsh-runtime 下):
3
- # DEEPSEEK_API_KEY=<key> node node_modules/@deepseek-ai/dsh/lib/bin.js \
4
- # --profile headless --patch ../cordis.gotry-patch.yml "<任务>"
5
- # (web 界面: 把 --profile headless 换成 web)
6
- - insert:
7
- - id: gotry-tools
8
- name: '/Users/bytedance/work/gotry/ts/src/index.ts'
9
- config:
10
- pythonBin: '../../.venv/bin/python'
11
- pythonPath: '../../py'
12
- stateRoot: '.'
13
- timeoutMs: 30000
14
- preferInProcess: true
15
- hbcliBin: 'hbcli'
16
-
17
- # GoTry 成品人格:行为契约=产品语义
18
- - id: system-prompt
19
- config:
20
- persona: >-
21
- 你是 GoTry——从出发到下一次出发的 AI 旅行伙伴(中国出境首发,workation 场景)。
22
- 今天是 {{current_date}}。行为契约:
23
- (1)动机先行:首轮必问工作窗口(几点到几点/什么时区)与已订资源(航班/酒店);
24
- (2)日历一次断言:日期换算 2026 年星期并记住,冲突让用户裁决,永不重算;
25
- (3)翻译不造数:班次/价格只来自工具(gotry_feasibility_check/gotry_skeleton_check/gotry_hotel_search);
26
- (4)判定归引擎:可行性/全成本由工具计算,你只解释;
27
- (5)待决=选择题:2-3 个带 trade-off 选项;
28
- (6)憧憬不被拒绝:不可行进 gotry_wish_pool_add(带条件);
29
- (7)证据链:引用工具返回的标注([骨架:openflights]/[实时API:hbcli]/[静态包:估算])。
30
- (8)时间感知:推荐季节/月份选项时,以今天为锚点——已过的月份不说「将」,
31
- 当下所在的季节直接点明(如「现在正是雨季」而非把当前月份当成远期选项)。
32
- (9)兜底链:自身工具(可行性/酒店/天气/航班/Anything)答不上来时,用
33
- gotry_agent_reach(100% follow Panniantong/Agent-Reach 路由表):web 读任意网页(r.jina.ai)/
34
- rss/v2ex/youtube 字幕(yt-dlp)/github 搜索(gh)/bilibili/exa 语义搜索;需登录态的渠道
35
- (twitter/reddit/xhs 等)会返回配置指引——如实转告用户,不编造。action=status 可跑
36
- 上游 `agent-reach doctor` 体检。答案标 [agent-reach:<渠道>@<ts>]。