@danceiny/gotry 0.2.0-rc.30 → 0.2.0-rc.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -153,7 +153,7 @@ Trust is structural, not promised:
153
153
  ## Project Status and Roadmap
154
154
  Current boundary: deterministic planning and evidence-bound read capabilities are available; booking and real-user acceptance remain outside the shipped claim. See the [architecture](docs/architecture.md) and [roadmap](docs/roadmap.md).
155
155
 
156
- **v0.2.0-rc.29** on npm (`latest`). Pre-1.0: the core loop works end to end; evaluation is still at deterministic contracts and validators, with no external scores or uplift claims. External W2A events are contract-only and inert; no real sensor bridge or consumer is active yet.
156
+ **v0.2.0-rc.30** on npm (`latest`). Pre-1.0: the core loop works end to end; evaluation is still at deterministic contracts and validators, with no external scores or uplift claims. External W2A events are contract-only and inert; no real sensor bridge or consumer is active yet.
157
157
 
158
158
  **Working today**
159
159
 
@@ -209,7 +209,7 @@ Documents ship as bilingual pairs (`x.md` English + `x.zh-CN.md` 中文); diverg
209
209
 
210
210
  **Built with**: DeepSeek Harness 0.2.0-rc.2 (root-pinned) · Cordis · Z3 (WASM) · loopx (pipx) · hotelbyte-cli · Agent-Reach v1.5.0 · OpenFlights · TypeScript
211
211
 
212
- **Version baseline: `v0.2.0-rc.29` (npm `latest`).** Verification gates: `scripts/run-all-tests.sh`; release flow: `scripts/publish-npm.sh`.
212
+ **Version baseline: `v0.2.0-rc.30` (npm `latest`).** Verification gates: `scripts/run-all-tests.sh`; release flow: `scripts/publish-npm.sh`.
213
213
 
214
214
  <a href="https://www.star-history.com/?repos=danceiny%2Fgotry&type=date&legend=top-left">
215
215
  <picture>
package/README.zh-CN.md CHANGED
@@ -155,7 +155,7 @@ npx @danceiny/gotry "我想从深圳休整两天,预算 3000" # headless 一
155
155
  ## 状态与路线图
156
156
  当前边界:确定性规划与有证据边界的只读能力已经可用;预订与真实用户验收仍不属于当前交付声明。见[架构权威文档](docs/architecture.zh-CN.md)与[路线图](docs/roadmap.zh-CN.md)。
157
157
 
158
- npm `latest`:**v0.2.0-rc.29**。未到 1.0:核心链路已端到端可用;评测仍停留在确定性合同与校验器阶段,无外部分数、无 uplift 声明。外部 W2A 事件仍是合同层、默认 inert;尚无真实 sensor 桥或 consumer 激活。
158
+ npm `latest`:**v0.2.0-rc.30**。未到 1.0:核心链路已端到端可用;评测仍停留在确定性合同与校验器阶段,无外部分数、无 uplift 声明。外部 W2A 事件仍是合同层、默认 inert;尚无真实 sensor 桥或 consumer 激活。
159
159
 
160
160
  **今天可用**
161
161
 
@@ -211,7 +211,7 @@ AI agent:[`AGENTS.md`](AGENTS.zh-CN.md) 是绑定契约——入场先清扫
211
211
 
212
212
  **Built with**: DeepSeek Harness 0.2.0-rc.2 (root-pinned) · Cordis · Z3 (WASM) · loopx (pipx) · hotelbyte-cli · Agent-Reach v1.5.0 · OpenFlights · TypeScript
213
213
 
214
- **版本基线:`v0.2.0-rc.29`(npm `latest`)。** 验证闸:`scripts/run-all-tests.sh`;发布流程:`scripts/publish-npm.sh`。
214
+ **版本基线:`v0.2.0-rc.30`(npm `latest`)。** 验证闸:`scripts/run-all-tests.sh`;发布流程:`scripts/publish-npm.sh`。
215
215
 
216
216
  <a href="https://www.star-history.com/?repos=danceiny%2Fgotry&type=date&legend=top-left">
217
217
  <picture>
@@ -53,6 +53,7 @@
53
53
  * 自渲。wizard 子命令退化为「离线健康探活等待」,只输出 stdout,不挡用户。
54
54
  */
55
55
 
56
+ import { verifyFlyaiCandidate } from './gotry-flyai-verification.js'
56
57
  import { spawn } from 'node:child_process'
57
58
  import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
58
59
  import { homedir } from 'node:os'
@@ -60,6 +61,7 @@ import { join, dirname } from 'node:path'
60
61
  import { fileURLToPath } from 'node:url'
61
62
  import { createRequire } from 'node:module'
62
63
  import { createHash } from 'node:crypto'
64
+ import { checkExtensionStatus } from './gotry-extension-status.js'
63
65
 
64
66
  // ── FlyAI 凭据面(纯 JS 内联;与 ts/capabilities/flyai-config.ts 成对维护,
65
67
  // 口径以官方 @fly-ai/flyai-cli 1.0.16 解包事实为准。bootstrap 由 inner 以
@@ -267,7 +269,7 @@ async function runSetupFlyai() {
267
269
  say(' 验证: 未配置——可直接匿名试用(共享额度易达限),或粘贴正式 key 验证后保存')
268
270
  }
269
271
  if (extra) say(extra)
270
- say(' 凭据修改仅本机命令可达: `gotry setup flyai`(隐藏输入)/ `--stdin` / `--clear`;模型工具不能传 key')
272
+ say(' 本机配置:`npx @danceiny/gotry setup flyai`(隐藏输入);`--status` 查看;`--clear` 清除;模型工具不能传 key')
271
273
  }
272
274
 
273
275
  if (STATUS) {
@@ -315,8 +317,10 @@ async function runSetupFlyai() {
315
317
  // 非 TTY 且未显式 --stdin:仍接受管道输入,但不进入交互隐藏模式。
316
318
  }
317
319
  } else {
318
- say('[gotry-setup] FlyAI API key 设置(输入隐藏,不回显;粘贴后回车)')
319
- say(' 打开 https://flyai.open.fliggy.com/console,登录后复制 API Key。直接回车取消。')
320
+ say('[gotry-setup] FlyAI 配置向导(无需设置环境变量或手工编辑文件)')
321
+ say(' 1. 打开 https://flyai.open.fliggy.com/console,登录后复制 API Key。')
322
+ say(' 2. 在下面粘贴并回车(输入隐藏,不回显);直接回车取消。请勿把 key 发到聊天里。')
323
+ say(' 3. 自动做只读验证,通过后保存到本机;失败保留原配置。')
320
324
  candidate = await hiddenInput(' key: ')
321
325
  }
322
326
  candidate = (candidate ?? '').trim()
@@ -336,7 +340,7 @@ async function runSetupFlyai() {
336
340
  if (verify.verdict !== 'hit') {
337
341
  say(`[gotry-setup] ✗ 验证未通过(${verify.verdict}):${verify.error ?? ''}`)
338
342
  say(' 配置未改动(失败不覆盖原有效设置)。核对 key 后重试,或 `--clear` 回退匿名。')
339
- return 1
343
+ return verify.exitCode ?? 1
340
344
  }
341
345
 
342
346
  const before = readFlyaiConfigObject()
@@ -393,7 +397,6 @@ async function runSetupFlyai() {
393
397
  /** TTY 隐藏输入:raw mode,不回显;Enter 结束;Ctrl+C/Ctrl+D 取消 */
394
398
  function hiddenInput(prompt) {
395
399
  return new Promise((resolve) => {
396
- process.stdout.write(prompt)
397
400
  let buf = ''
398
401
  const stdin = process.stdin
399
402
  stdin.setRawMode(true)
@@ -433,6 +436,9 @@ function hiddenInput(prompt) {
433
436
  }
434
437
  }
435
438
  stdin.on('data', onData)
439
+ // Disable terminal echo before advertising readiness: a fast paste can
440
+ // reach the PTY as soon as the prompt is written, even within this tick.
441
+ process.stdout.write(prompt)
436
442
  })
437
443
  }
438
444
 
@@ -441,73 +447,26 @@ function hiddenInput(prompt) {
441
447
  * 子进程 env scrub FLYAI_API_KEY/DEBUG_FLYAI_API_KEY 后注入候选。
442
448
  * 返回 {verdict,error}。
443
449
  */
444
- function verifyCandidateKeyInline(candidate) {
445
- const GOTRY_FLYAI_CLI = process.env.GOTRY_FLYAI_CLI_BIN ?? ''
446
- const cliBin = GOTRY_FLYAI_CLI || 'npx'
447
- const prefix = GOTRY_FLYAI_CLI ? [] : ['-y', '@fly-ai/flyai-cli@1.0.16']
448
- const env = { ...process.env }
449
- delete env.FLYAI_API_KEY
450
- delete env.DEBUG_FLYAI_API_KEY
451
- env.FLYAI_API_KEY = candidate
452
- const date = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 10)
453
- const args = [...prefix, 'search-flight', '--origin', '上海', '--destination', '丽江', '--dep-date', date]
454
- const configuredTimeout = Number(process.env.GOTRY_FLYAI_VERIFY_TIMEOUT_MS ?? 20_000)
455
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? Math.min(configuredTimeout, 60_000) : 20_000
456
- return new Promise((resolve) => {
457
- let stdout = ''
458
- let stderr = ''
459
- let timedOut = false
460
- let settled = false
461
- const child = spawn(cliBin, args, {
462
- env, stdio: ['ignore', 'pipe', 'pipe'],
463
- detached: process.platform !== 'win32',
464
- })
465
- const finish = (v) => {
466
- if (settled) return
467
- settled = true
468
- resolve(v)
469
- }
470
- const killGroup = (signal) => {
471
- if (process.platform !== 'win32' && child.pid) {
472
- try { process.kill(-child.pid, signal); return } catch { /* fall through */ }
473
- }
474
- try { child.kill(signal) } catch { /* ignore */ }
450
+ async function verifyCandidateKeyInline(candidate) {
451
+ const controller = new AbortController()
452
+ let interrupted
453
+ const listeners = new Map()
454
+ for (const signal of ['SIGINT', 'SIGTERM']) {
455
+ const listener = () => {
456
+ interrupted ??= signal
457
+ controller.abort()
475
458
  }
476
- const timer = setTimeout(() => { timedOut = true; killGroup('SIGKILL') }, timeoutMs)
477
- child.stdout?.on('data', d => { stdout += d.toString() })
478
- child.stderr?.on('data', d => { stderr += d.toString() })
479
- child.on('error', (e) => { clearTimeout(timer); finish({ verdict: 'error', error: e.message }) })
480
- child.on('close', (code) => {
481
- clearTimeout(timer)
482
- const combined = `${stderr}\n${stdout}`
483
- if (timedOut) return finish({ verdict: 'timeout', error: `验证调用超时 ${timeoutMs}ms` })
484
- if (/Invalid API key|HTTP\s*401|\b401\b/.test(combined)) {
485
- return finish({ verdict: 'auth-error', error: '401 Invalid API key' })
486
- }
487
- if (/HTTP\s*403|\b403\b/.test(combined)) {
488
- return finish({ verdict: 'forbidden', error: 'HTTP 403:无访问权限,请核对控制台权限。' })
489
- }
490
- if (/Trial limit reached/.test(combined)) {
491
- return finish({ verdict: 'needs-setup', error: 'Trial limit reached(候选 key 未被识别为正式 key?)' })
492
- }
493
- if (/HTTP\s*429|\b429\b/.test(combined)) {
494
- return finish({ verdict: 'rate-limited', error: '普通限流,稍后重试' })
495
- }
496
- if (code !== 0) {
497
- // Upstream diagnostics can echo credentials or a private debug URL.
498
- return finish({ verdict: 'error', error: `验证进程失败(exit ${code});请检查网络与服务状态。` })
499
- }
500
- try {
501
- const envelope = JSON.parse(stdout)
502
- if (Array.isArray(envelope?.data?.itemList)) {
503
- return finish({ verdict: 'hit', count: envelope.data.itemList.length })
504
- }
505
- return finish({ verdict: 'error', error: '响应缺 data.itemList' })
506
- } catch {
507
- return finish({ verdict: 'error', error: '响应不是合法 JSON' })
508
- }
509
- })
510
- })
459
+ listeners.set(signal, listener)
460
+ process.on(signal, listener)
461
+ }
462
+ try {
463
+ const result = await verifyFlyaiCandidate(candidate, { signal: controller.signal })
464
+ return interrupted
465
+ ? { ...result, verdict: 'cancelled', exitCode: 128 + (interrupted === 'SIGINT' ? 2 : 15) }
466
+ : result
467
+ } finally {
468
+ for (const [signal, listener] of listeners) process.off(signal, listener)
469
+ }
511
470
  }
512
471
 
513
472
 
@@ -889,9 +848,8 @@ async function doctorChecks() {
889
848
  const nodeFine = maj > 22 || (maj === 22 && min >= 15)
890
849
  items.push({ label: 'Node 运行时', ok: nodeFine, level: nodeFine ? 'ok' : 'missing', detail: `Node ${nv}(需 ≥22.15)`, fix: nodeFine ? undefined : '升级 Node.js 至 22.15+(https://nodejs.org)' })
891
850
  // 扩展
892
- const extManifest = join(homedir(), '.gotry', 'extension', 'manifest.json')
893
- const extOk = existsSync(extManifest)
894
- items.push({ label: 'Stai Travel Bridge 扩展', ok: extOk, level: extOk ? 'ok' : 'missing', detail: extOk ? `已就位(${extManifest})` : '未安装——影响面:gotry_session_search / gotry_session_login(账号会话通道)不可用;携程机票/酒店实时检索、12306 余票、Dida 实时报价都依赖此通道。其它工具(机票 FlyAI、酒店 hbcli、地图、天气等)不受影响', fix: extOk ? undefined : '在 Chrome 应用商店一键安装(自动更新): https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd' })
851
+ const extension = await checkExtensionStatus()
852
+ items.push({ label: 'Stai Travel Bridge 扩展', ok: extension.status === 'ok', level: extension.status, detail: extension.detail, fix: extension.fix })
895
853
  // agent-reach(.venv 装在包内)
896
854
  const venvPython = join(repoRoot, '.venv/bin/python')
897
855
  const reachBin = join(repoRoot, '.venv/bin/agent-reach')
@@ -931,7 +889,7 @@ async function doctorChecks() {
931
889
  let flyaiFix
932
890
  if (!flyaiResolved.key) {
933
891
  flyaiDetail = '未配置 key——匿名试用中(共享额度易达限;达限报 Trial limit reached)。影响面:共享池额度小,频繁会话易触顶;达限本会话内 gotry_flyai_search(机票/酒店/票务/AI/万豪 8 类)失败,改走 gotry_session_search 账号会话通道。'
934
- flyaiFix = '本机运行 `gotry setup flyai`(隐藏输入,先验证后保存 FLYAI_API_KEY);打开 https://flyai.open.fliggy.com/console,登录后复制 API Key'
892
+ flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`:① 打开 https://flyai.open.fliggy.com/console 复制 API Key;② 在终端隐藏粘贴;③ 验证后保存。也可运行 `npx @danceiny/gotry doctor --fix` 按提示配置'
935
893
  } else {
936
894
  const keySha = sha256Inline(flyaiResolved.key)
937
895
  const receiptMatches = flyaiReceipt
@@ -946,16 +904,16 @@ async function doctorChecks() {
946
904
  flyaiDetail = `已验证 ${flyaiReceipt.at}(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
947
905
  } else if (flyaiReceipt && flyaiReceipt.keySha256 === keySha && flyaiReceipt.verdict !== 'verified') {
948
906
  flyaiDetail = `已配置未通过验证(${flyaiReceipt.verdict}@ ${flyaiReceipt.at};${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})——非空不等于鉴权通过`
949
- flyaiFix = '本机运行 `gotry setup flyai` 重新验证;`gotry setup flyai --clear` 回退匿名'
907
+ flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」重新验证;或运行 `npx @danceiny/gotry setup flyai`;`npx @danceiny/gotry setup flyai --clear` 回退匿名'
950
908
  } else {
951
909
  flyaiDetail = `已配置,未验证(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
952
- flyaiFix = '本机运行 `gotry setup flyai`(候选 key scrub-env 验证后保存)'
910
+ flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`,按提示隐藏粘贴 API Key'
953
911
  }
954
912
  }
955
913
  items.push({ label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪 8 类)', ok: flyaiLevel === 'ok', level: flyaiLevel, detail: flyaiDetail, fix: flyaiFix })
956
914
  // sidebar(状态面与 setupSidebar 的落盘复核同一口径)
957
915
  const sbOk = sidebarInstalled()
958
- items.push({ label: 'dsh-better-sidebar(侧栏工作台)', ok: sbOk, level: sbOk ? 'ok' : 'missing', detail: sbOk ? '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' : '未安装——dsh web 无右侧工作台,产物与 doctor 报告只能在对话里看(gotry_artifacts_list)', fix: sbOk ? undefined : 'npx @danceiny/gotry doctor --fix' })
916
+ items.push({ label: 'dsh-better-sidebar(侧栏工作台)', ok: sbOk, level: sbOk ? 'ok' : 'missing', detail: sbOk ? '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' : '未安装此增强组件——Web 自带 GoTry 产物页与原生预览;可选补装以增强文件浏览', fix: sbOk ? undefined : 'npx @danceiny/gotry doctor --fix' })
959
917
  // dsh-calendar(setup 状态面;默认不挂载=ok 是合法态,opt-in 未配置才 degraded)
960
918
  const calState = readCalendarState()
961
919
  const calOn = calState?.enabled === true
@@ -1018,8 +976,8 @@ const fixCell = (fix) => (!fix ? '—' : /^(npx|hbcli|curl|pip|python|\$)/.test(
1018
976
  function startupDoctorLine(items) {
1019
977
  const broken = (items ?? []).filter((i) => i.level && i.level !== 'ok')
1020
978
  if (broken.length === 0) return null
1021
- const human = (lv) => (lv === 'missing' ? '缺' : '半可用')
1022
- return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i.level)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx @danceiny/gotry doctor`
979
+ const human = i => i.level === 'missing' ? '缺' : i.label.startsWith('Stai Travel Bridge') ? '待确认' : '半可用'
980
+ return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx @danceiny/gotry doctor`
1023
981
  }
1024
982
 
1025
983
  /** 体检报告 markdown(与 ts/capabilities/doctor.ts renderDoctorReportMd 同形) */
@@ -1073,11 +1031,26 @@ async function runDoctor() {
1073
1031
  if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
1074
1032
  const failedFix = results.filter((r) => !r.ok).length
1075
1033
  say(failedFix === 0 ? '[gotry-doctor] 补装完成;下面是补装后复检。' : `[gotry-doctor] ${failedFix} 项补装失败——见上方安装器输出;可重跑 npx @danceiny/gotry doctor --fix`)
1076
- items = await doctorChecks()
1077
- for (const i of items) {
1078
- say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
1034
+ }
1035
+ // Explicit --fix may offer local credential setup, but never consumes a
1036
+ // secret from CI, a pipe, startup diagnostics or the model tool surface.
1037
+ if (items.some(i => i.label.startsWith('FlyAI(') && i.level !== 'ok')
1038
+ && process.platform !== 'win32' && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
1039
+ process.stdout.write('[gotry-doctor] 现在配置 FlyAI?控制台取 key → 隐藏粘贴 → 验证后保存(y/N):')
1040
+ const answer = await readOneLine(process.stdin)
1041
+ if (/^y(?:es)?$/i.test(answer.trim())) {
1042
+ const setupCode = await runSetupFlyai()
1043
+ if (setupCode === 130 || setupCode === 143) return setupCode
1044
+ if (setupCode !== 0) say('[gotry-doctor] FlyAI 配置未完成;可重跑 `npx @danceiny/gotry setup flyai`。')
1045
+ } else {
1046
+ say('[gotry-doctor] 已跳过 FlyAI 配置;稍后运行 `npx @danceiny/gotry setup flyai`。')
1079
1047
  }
1080
1048
  }
1049
+ items = await doctorChecks()
1050
+ for (const i of items) {
1051
+ say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
1052
+ if (i.level !== 'ok' && i.fix) say(` ↳ 修复: ${i.fix}`)
1053
+ }
1081
1054
  }
1082
1055
  // 报告落盘(侧栏工作台预览面;写失败不挡体检结论)
1083
1056
  try {
@@ -0,0 +1,12 @@
1
+ export interface ExtensionStatusOptions {
2
+ homeDir?: string
3
+ platform?: NodeJS.Platform
4
+ env?: NodeJS.ProcessEnv
5
+ ports?: readonly number[]
6
+ timeoutMs?: number
7
+ }
8
+ export function checkExtensionStatus(opts?: ExtensionStatusOptions): Promise<{
9
+ status: 'ok' | 'degraded'
10
+ detail: string
11
+ fix?: string
12
+ }>
@@ -0,0 +1,113 @@
1
+ // Shared by the npm CLI and gotry_doctor. Disk presence proves installation,
2
+ // while only a fresh, trusted-origin heartbeat on the existing bridge proves
3
+ // connection. This diagnostic never starts a bridge or sends a heartbeat.
4
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
5
+ import { get } from 'node:http'
6
+ import { homedir } from 'node:os'
7
+ import { join } from 'node:path'
8
+
9
+ const IDS = ['oeajpiccmonococjcegddlooeeohlbgd', 'olpgkofjhhiiiahdkkbcninhjmegghfe']
10
+ const PORTS = [8791, 8792, 8793, 8794, 8795]
11
+ const STORE_URL = `https://chromewebstore.google.com/detail/gotry-session-bridge/${IDS[0]}`
12
+
13
+ function json(path) {
14
+ try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
15
+ }
16
+ function directories(path) {
17
+ try { return readdirSync(path, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name) } catch { return [] }
18
+ }
19
+
20
+ function browserRoots(home, platform, env) {
21
+ if (platform === 'darwin') {
22
+ const base = join(home, 'Library', 'Application Support')
23
+ return ['Google/Chrome', 'Google/Chrome Beta', 'Google/Chrome Canary', 'Chromium', 'Microsoft Edge', 'BraveSoftware/Brave-Browser'].map(path => join(base, path))
24
+ }
25
+ if (platform === 'win32') {
26
+ const base = env.LOCALAPPDATA || join(home, 'AppData', 'Local')
27
+ return ['Google/Chrome/User Data', 'Google/Chrome Beta/User Data', 'Google/Chrome SxS/User Data', 'Chromium/User Data', 'Microsoft/Edge/User Data', 'BraveSoftware/Brave-Browser/User Data'].map(path => join(base, path))
28
+ }
29
+ const base = env.XDG_CONFIG_HOME || join(home, '.config')
30
+ return ['google-chrome', 'google-chrome-beta', 'google-chrome-unstable', 'chromium', 'microsoft-edge', 'BraveSoftware/Brave-Browser'].map(path => join(base, path))
31
+ }
32
+
33
+ function installedProfiles(home, platform, env) {
34
+ const found = []
35
+ for (const root of browserRoots(home, platform, env)) {
36
+ for (const name of directories(root)) {
37
+ const profile = join(root, name)
38
+ const matches = IDS.filter(id => directories(join(profile, 'Extensions', id)).some(version => {
39
+ const manifest = json(join(profile, 'Extensions', id, version, 'manifest.json'))
40
+ return manifest?.manifest_version === 3 && typeof manifest.version === 'string'
41
+ }))
42
+ if (!matches.length) continue
43
+ const secure = json(join(profile, 'Secure Preferences'))?.extensions?.settings
44
+ const preferences = json(join(profile, 'Preferences'))?.extensions?.settings
45
+ for (const id of matches) {
46
+ const settings = secure?.[id] ?? preferences?.[id]
47
+ // Uninstall/update may leave version directories behind. Require the
48
+ // browser's current registration as well as its on-disk manifest.
49
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) continue
50
+ const disabled = (typeof settings?.state === 'number' && settings.state !== 1)
51
+ || (Array.isArray(settings?.disable_reasons) && settings.disable_reasons.length > 0)
52
+ found.push({ profile, disabled })
53
+ }
54
+ }
55
+ }
56
+ return found
57
+ }
58
+
59
+ function bridgeStatus(port, timeoutMs) {
60
+ return new Promise(resolve => {
61
+ let done = false
62
+ const finish = value => { if (!done) { done = true; clearTimeout(timer); resolve(value) } }
63
+ const req = get({ hostname: '127.0.0.1', port, path: '/status' }, res => {
64
+ let body = ''
65
+ res.setEncoding('utf8')
66
+ res.on('data', chunk => {
67
+ body += chunk
68
+ if (body.length > 16_384) { finish(null); req.destroy() }
69
+ })
70
+ res.on('error', () => finish(null))
71
+ res.on('end', () => {
72
+ try {
73
+ const status = JSON.parse(body)
74
+ if (res.statusCode !== 200 || status.ok !== true || status.protocol !== 'session-bridge.v1'
75
+ || typeof status.extensionConnected !== 'boolean') { finish(null); return }
76
+ finish({ port, connected: status.extensionConnected === true
77
+ && Number.isFinite(status.lastSeenMsAgo) && status.lastSeenMsAgo >= 0 && status.lastSeenMsAgo < 45_000 })
78
+ } catch { finish(null) }
79
+ })
80
+ })
81
+ const timer = setTimeout(() => { finish(null); req.destroy() }, timeoutMs)
82
+ req.on('error', () => finish(null))
83
+ })
84
+ }
85
+
86
+ export async function checkExtensionStatus(opts = {}) {
87
+ const home = opts.homeDir ?? homedir()
88
+ const platform = opts.platform ?? process.platform
89
+ const env = opts.env ?? process.env
90
+ const bridges = await Promise.all((opts.ports ?? PORTS).map(port => bridgeStatus(port, opts.timeoutMs ?? 400)))
91
+ const connected = bridges.find(status => status?.connected)
92
+ if (connected) return {
93
+ status: 'ok', detail: `已连接(本机桥端口 ${connected.port},近期扩展心跳已确认)`,
94
+ }
95
+ const profiles = installedProfiles(home, platform, env)
96
+ if (profiles.length) {
97
+ const enabled = profiles.find(profile => !profile.disabled)
98
+ return {
99
+ status: 'degraded',
100
+ detail: enabled ? `已安装(浏览器配置:${enabled.profile});当前连接未确认,离线安装检查不代表会话检索已就绪`
101
+ : `已安装,但已停用(浏览器配置:${profiles[0].profile});当前连接未确认`,
102
+ fix: '打开安装扩展的浏览器,在 chrome://extensions 启用 Stai Travel Bridge;启动 `npx @danceiny/gotry web`,在对话中发起账号会话检索并按提示授权以启动连接;保持会话运行时重跑 doctor,检索以 gotry_session_search 的结果为准',
103
+ }
104
+ }
105
+ if (existsSync(join(home, '.gotry', 'extension', 'manifest.json'))) return {
106
+ status: 'degraded', detail: '本地扩展文件已就位,浏览器加载及当前连接未确认',
107
+ fix: '推荐从商店安装:' + STORE_URL + ';或在 chrome://extensions 加载 ~/.gotry/extension;启动 `npx @danceiny/gotry web`,在对话中发起账号会话检索并按提示授权以启动连接',
108
+ }
109
+ return {
110
+ status: 'degraded', detail: '未检测到浏览器安装记录,当前连接未确认(浏览器可能使用其他配置目录);无法仅凭文件判断安装状态。影响面:账号会话检索需扩展在线,机票 FlyAI、酒店 hbcli、地图、天气等其它工具不受影响',
111
+ fix: '已安装时:打开对应浏览器,在 chrome://extensions 确认已启用;启动 `npx @danceiny/gotry web`,在对话中发起账号会话检索并按提示授权以启动连接;保持会话运行时复检。首次安装:' + STORE_URL,
112
+ }
113
+ }
@@ -0,0 +1,9 @@
1
+ export interface FlyaiCandidateVerification {
2
+ verdict: 'hit' | 'auth-error' | 'forbidden' | 'needs-setup' | 'rate-limited' | 'timeout' | 'cancelled' | 'error'
3
+ count?: number
4
+ error?: string
5
+ }
6
+ export declare function verifyFlyaiCandidate(candidate: string, options?: {
7
+ env?: NodeJS.ProcessEnv
8
+ signal?: AbortSignal
9
+ }): Promise<FlyaiCandidateVerification>
@@ -0,0 +1,63 @@
1
+ /** Shared, bounded, read-only candidate verification for terminal and native Web setup. */
2
+ import { spawnOwnedChild, terminateOwnedChild } from './gotry-process-liveness.js'
3
+
4
+ export async function verifyFlyaiCandidate(candidate, options = {}) {
5
+ const configured = options.env ?? process.env
6
+ if (options.signal?.aborted) return { verdict: 'cancelled', error: '验证已取消。' }
7
+ const env = { ...configured, FLYAI_API_KEY: candidate }
8
+ delete env.DEBUG_FLYAI_API_KEY
9
+ const command = configured.GOTRY_FLYAI_CLI_BIN || 'npx'
10
+ const prefix = configured.GOTRY_FLYAI_CLI_BIN ? [] : ['-y', '@fly-ai/flyai-cli@1.0.16']
11
+ const date = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 10)
12
+ const args = [...prefix, 'search-flight', '--origin', '上海', '--destination', '丽江', '--dep-date', date]
13
+ const limit = Number(configured.GOTRY_FLYAI_VERIFY_TIMEOUT_MS ?? 20_000)
14
+ const timeoutMs = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 60_000) : 20_000
15
+ const owned = spawnOwnedChild(command, args, { env, stdio: ['ignore', 'pipe', 'pipe'] })
16
+ let stopReason, timer, stdout = '', stderr = '', bytes = 0
17
+ const stop = (reason) => {
18
+ stopReason ??= reason
19
+ void terminateOwnedChild({ ...owned, signal: 'SIGKILL', termGraceMs: 100, killWaitMs: 1000 })
20
+ }
21
+ const abort = () => stop('cancelled')
22
+ try {
23
+ const result = await new Promise(resolve => {
24
+ let settled = false
25
+ const finish = value => { if (!settled) { settled = true; resolve(value) } }
26
+ timer = setTimeout(() => stop('timeout'), timeoutMs)
27
+ options.signal?.addEventListener('abort', abort, { once: true })
28
+ if (options.signal?.aborted) abort()
29
+ owned.child.stdout?.on('data', chunk => {
30
+ bytes += chunk.length
31
+ if (bytes > 256 * 1024) return stop('output-limit')
32
+ stdout += chunk.toString()
33
+ })
34
+ owned.child.stderr?.on('data', chunk => {
35
+ bytes += chunk.length
36
+ if (bytes > 256 * 1024) return stop('output-limit')
37
+ stderr += chunk.toString()
38
+ })
39
+ owned.child.once('error', () => finish({ verdict: 'error', error: '无法启动验证进程。' }))
40
+ owned.child.once('close', code => {
41
+ if (stopReason) return finish({ verdict: stopReason === 'output-limit' ? 'error' : stopReason,
42
+ error: stopReason === 'cancelled' ? '验证已取消。' : stopReason === 'timeout' ? `验证调用超时 ${timeoutMs}ms。` : '验证响应过大。' })
43
+ const combined = `${stderr}\n${stdout}`
44
+ if (/Invalid API key|HTTP\s*401|\b401\b/.test(combined)) return finish({ verdict: 'auth-error', error: '401 Invalid API key' })
45
+ if (/HTTP\s*403|\b403\b/.test(combined)) return finish({ verdict: 'forbidden', error: 'HTTP 403:无访问权限,请核对控制台权限。' })
46
+ if (/Trial limit reached/.test(combined)) return finish({ verdict: 'needs-setup', error: '试用额度已用尽,请核对控制台 Key。' })
47
+ if (/HTTP\s*429|\b429\b/.test(combined)) return finish({ verdict: 'rate-limited', error: '普通限流,稍后重试。' })
48
+ if (code !== 0) return finish({ verdict: 'error', error: `验证进程失败(exit ${code});请检查网络与服务状态。` })
49
+ try {
50
+ const envelope = JSON.parse(stdout)
51
+ finish(Array.isArray(envelope?.data?.itemList)
52
+ ? { verdict: 'hit', count: envelope.data.itemList.length }
53
+ : { verdict: 'error', error: '响应缺 data.itemList。' })
54
+ } catch { finish({ verdict: 'error', error: '响应不是合法 JSON。' }) }
55
+ })
56
+ })
57
+ return result
58
+ } finally {
59
+ clearTimeout(timer)
60
+ options.signal?.removeEventListener('abort', abort)
61
+ await terminateOwnedChild({ ...owned, signal: 'SIGKILL', termGraceMs: 100, killWaitMs: 1000 })
62
+ }
63
+ }
@@ -88,8 +88,9 @@ Usage:
88
88
  # 跳过该问: --no-onboarding 或 GOTRY_ONBOARDING_SKIP=1
89
89
  gotry setup # 扩展就位检查/指引(商店一键装)
90
90
  gotry setup calendar # 可选日历(CalDAV 工作窗口)挂载开关:默认关;--off 关闭;--status 查看
91
+ gotry setup flyai # FlyAI 配置向导:控制台取 key → 隐藏粘贴 → 验证后保存;--status 查看;--clear 清除
91
92
  gotry doctor # 可选依赖体检:扩展/agent-reach/hbcli/flyai/sidebar 状态 + 补装指引
92
- gotry doctor --fix # 体检 + 按报告补装(hbcli 官方脚本 / agent-reach pip / sidebar 插件)
93
+ gotry doctor --fix # 补装可选依赖;交互终端可选择接着配置 FlyAI
93
94
  gotry "一段完整任务..." # headless 一问一答
94
95
  gotry help # this help
95
96
  gotry --version # 版本号(bug 报告必填项)