@danceiny/gotry 0.2.0-rc.30 → 0.2.0-rc.32
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +119 -136
- package/bin/gotry-extension-status.d.ts +12 -0
- package/bin/gotry-extension-status.js +113 -0
- package/bin/gotry-flyai-verification.d.ts +9 -0
- package/bin/gotry-flyai-verification.js +63 -0
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/bin/gotry-inner.js +2 -1
- package/client/client.js +228 -0
- package/dist/capabilities/doctor.js +14 -23
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +8 -1
- package/dist/capabilities/session/gui-flight.js +737 -0
- package/dist/capabilities/session/gui-hotel.js +413 -0
- package/dist/capabilities/session-search.js +98 -11
- package/dist/scripts/artifact-client-contract-tests.js +4 -3
- package/dist/scripts/artifact-delivery-tests.js +31 -0
- package/dist/scripts/bootstrap-tests.js +37 -10
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/doctor-tests.js +36 -7
- package/dist/scripts/dsh-artifact-web-e2e.js +204 -43
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +93 -5
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +119 -0
- package/dist/scripts/gotry-web-client-tests.js +47 -0
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +3 -1
- package/dist/scripts/session-tests.js +177 -6
- package/dist/src/artifact-delivery.js +26 -0
- package/dist/src/gotry-web-api.js +219 -0
- package/dist/src/index.js +45 -17
- package/extension/manifest.json +2 -2
- package/package.json +8 -2
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +9 -2
- package/ts/capabilities/session/gui-flight.ts +792 -0
- package/ts/capabilities/session/gui-hotel.ts +451 -0
- package/ts/capabilities/session-search.ts +115 -15
- package/ts/src/artifact-delivery.ts +25 -0
- package/ts/src/gotry-web-api.ts +189 -0
- package/ts/src/index.ts +46 -18
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.
|
|
156
|
+
**v0.2.0-rc.31** 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.
|
|
212
|
+
**Version baseline: `v0.2.0-rc.31` (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.
|
|
158
|
+
npm `latest`:**v0.2.0-rc.31**。未到 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.
|
|
214
|
+
**版本基线:`v0.2.0-rc.31`(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>
|
package/bin/gotry-bootstrap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* gotry 外部依赖自举(founder 2026-08-29 指令:安装 gotry 时按上游本身的方式装好外部依赖):
|
|
4
|
-
* hbcli(hotelbyte-cli) →
|
|
4
|
+
* hbcli(hotelbyte-cli) → 官方版本钉扎的原生 install.sh(npm 发布暂落后)
|
|
5
5
|
* agent-reach → 官方 pip 安装 git+ upstream(包内 .venv,与 z3-solver 同址原则)
|
|
6
6
|
* flyai → 无需安装(npx 每次自拉 @fly-ai/flyai-cli)
|
|
7
7
|
* dsh-better-sidebar → dsh 宿主层插件市场组件(dshmarket.com #1 UI,18.9 万周装):
|
|
@@ -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,8 @@ 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'
|
|
65
|
+
import { HBCLI_RELEASE_VERSION, HBCLI_INSTALL_CMD } from './gotry-hbcli-release.js'
|
|
63
66
|
|
|
64
67
|
// ── FlyAI 凭据面(纯 JS 内联;与 ts/capabilities/flyai-config.ts 成对维护,
|
|
65
68
|
// 口径以官方 @fly-ai/flyai-cli 1.0.16 解包事实为准。bootstrap 由 inner 以
|
|
@@ -162,24 +165,24 @@ function verifySecureModeInline(dir, file) {
|
|
|
162
165
|
|
|
163
166
|
/**
|
|
164
167
|
* 官方 CLI 首次运行可能先创建 ~/.flyai 为 0755(并写入 0600 device-id)。
|
|
165
|
-
* 仅对当前用户拥有、且本身不是 symlink
|
|
166
|
-
*
|
|
168
|
+
* 仅对当前用户拥有、且本身不是 symlink 的目录收紧权限。
|
|
169
|
+
* 回执所在 .gotry 同样兼容 doctor/runtime 创建的既有 0755 目录。
|
|
167
170
|
*/
|
|
168
|
-
function
|
|
169
|
-
const dir = dirname(
|
|
171
|
+
function preparePrivateDirInline(file = flyaiConfigFile()) {
|
|
172
|
+
const dir = dirname(file)
|
|
170
173
|
try {
|
|
171
174
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
172
175
|
const entry = lstatSync(dir)
|
|
173
|
-
if (entry.isSymbolicLink()) return { ok: false, error: '
|
|
174
|
-
if (!entry.isDirectory()) return { ok: false, error: '
|
|
176
|
+
if (entry.isSymbolicLink()) return { ok: false, error: '配置目录是符号链接,拒绝写入' }
|
|
177
|
+
if (!entry.isDirectory()) return { ok: false, error: '配置路径不是目录,拒绝写入' }
|
|
175
178
|
if (typeof process.getuid === 'function' && entry.uid !== process.getuid()) {
|
|
176
|
-
return { ok: false, error: '
|
|
179
|
+
return { ok: false, error: '配置目录不属于当前用户,拒绝写入' }
|
|
177
180
|
}
|
|
178
181
|
if ((entry.mode & 0o777) !== 0o700) chmodSync(dir, 0o700)
|
|
179
182
|
const after = lstatSync(dir)
|
|
180
|
-
if (after.isSymbolicLink() || !after.isDirectory()) return { ok: false, error: '
|
|
181
|
-
if (typeof process.getuid === 'function' && after.uid !== process.getuid()) return { ok: false, error: '
|
|
182
|
-
return verifySecureModeInline(dir,
|
|
183
|
+
if (after.isSymbolicLink() || !after.isDirectory()) return { ok: false, error: '配置目录状态发生变化,拒绝写入' }
|
|
184
|
+
if (typeof process.getuid === 'function' && after.uid !== process.getuid()) return { ok: false, error: '配置目录所有者发生变化,拒绝写入' }
|
|
185
|
+
return verifySecureModeInline(dir, file)
|
|
183
186
|
} catch (e) {
|
|
184
187
|
return { ok: false, error: e.message }
|
|
185
188
|
}
|
|
@@ -267,7 +270,7 @@ async function runSetupFlyai() {
|
|
|
267
270
|
say(' 验证: 未配置——可直接匿名试用(共享额度易达限),或粘贴正式 key 验证后保存')
|
|
268
271
|
}
|
|
269
272
|
if (extra) say(extra)
|
|
270
|
-
say('
|
|
273
|
+
say(' 本机配置:`npx @danceiny/gotry setup flyai`(隐藏输入);`--status` 查看;`--clear` 清除;模型工具不能传 key')
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
if (STATUS) {
|
|
@@ -289,7 +292,7 @@ async function runSetupFlyai() {
|
|
|
289
292
|
if (Object.keys(next).length === 0) {
|
|
290
293
|
rmSync(flyaiConfigFile(), { force: true })
|
|
291
294
|
} else {
|
|
292
|
-
const configDir =
|
|
295
|
+
const configDir = preparePrivateDirInline()
|
|
293
296
|
if (!configDir.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${configDir.error}`); return 1 }
|
|
294
297
|
const w = atomicWriteJsonInline(flyaiConfigFile(), next)
|
|
295
298
|
if (!w.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${w.error}`); return 1 }
|
|
@@ -315,8 +318,10 @@ async function runSetupFlyai() {
|
|
|
315
318
|
// 非 TTY 且未显式 --stdin:仍接受管道输入,但不进入交互隐藏模式。
|
|
316
319
|
}
|
|
317
320
|
} else {
|
|
318
|
-
say('[gotry-setup] FlyAI
|
|
319
|
-
say(' 打开 https://flyai.open.fliggy.com/console,登录后复制 API Key
|
|
321
|
+
say('[gotry-setup] FlyAI 配置向导(无需设置环境变量或手工编辑文件)')
|
|
322
|
+
say(' 1. 打开 https://flyai.open.fliggy.com/console,登录后复制 API Key。')
|
|
323
|
+
say(' 2. 在下面粘贴并回车(输入隐藏,不回显);直接回车取消。请勿把 key 发到聊天里。')
|
|
324
|
+
say(' 3. 自动做只读验证,通过后保存到本机;失败保留原配置。')
|
|
320
325
|
candidate = await hiddenInput(' key: ')
|
|
321
326
|
}
|
|
322
327
|
candidate = (candidate ?? '').trim()
|
|
@@ -336,7 +341,7 @@ async function runSetupFlyai() {
|
|
|
336
341
|
if (verify.verdict !== 'hit') {
|
|
337
342
|
say(`[gotry-setup] ✗ 验证未通过(${verify.verdict}):${verify.error ?? ''}`)
|
|
338
343
|
say(' 配置未改动(失败不覆盖原有效设置)。核对 key 后重试,或 `--clear` 回退匿名。')
|
|
339
|
-
return 1
|
|
344
|
+
return verify.exitCode ?? 1
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
const before = readFlyaiConfigObject()
|
|
@@ -345,7 +350,7 @@ async function runSetupFlyai() {
|
|
|
345
350
|
return 1
|
|
346
351
|
}
|
|
347
352
|
const hadConfigFile = existsSync(flyaiConfigFile())
|
|
348
|
-
const configDir =
|
|
353
|
+
const configDir = preparePrivateDirInline()
|
|
349
354
|
if (!configDir.ok) {
|
|
350
355
|
say(`[gotry-setup] ✗ 配置目录不安全,旧配置保留:${configDir.error}`)
|
|
351
356
|
return 1
|
|
@@ -376,7 +381,8 @@ async function runSetupFlyai() {
|
|
|
376
381
|
endpointDebug: epAfter.debug,
|
|
377
382
|
at: new Date().toISOString(),
|
|
378
383
|
}
|
|
379
|
-
const
|
|
384
|
+
const receiptDir = preparePrivateDirInline(flyaiVerificationFile())
|
|
385
|
+
const receiptWrite = receiptDir.ok ? atomicWriteJsonInline(flyaiVerificationFile(), receipt) : receiptDir
|
|
380
386
|
if (!receiptWrite.ok) {
|
|
381
387
|
const rollback = hadConfigFile
|
|
382
388
|
? atomicWriteJsonInline(flyaiConfigFile(), before.data)
|
|
@@ -393,7 +399,6 @@ async function runSetupFlyai() {
|
|
|
393
399
|
/** TTY 隐藏输入:raw mode,不回显;Enter 结束;Ctrl+C/Ctrl+D 取消 */
|
|
394
400
|
function hiddenInput(prompt) {
|
|
395
401
|
return new Promise((resolve) => {
|
|
396
|
-
process.stdout.write(prompt)
|
|
397
402
|
let buf = ''
|
|
398
403
|
const stdin = process.stdin
|
|
399
404
|
stdin.setRawMode(true)
|
|
@@ -433,6 +438,9 @@ function hiddenInput(prompt) {
|
|
|
433
438
|
}
|
|
434
439
|
}
|
|
435
440
|
stdin.on('data', onData)
|
|
441
|
+
// Disable terminal echo before advertising readiness: a fast paste can
|
|
442
|
+
// reach the PTY as soon as the prompt is written, even within this tick.
|
|
443
|
+
process.stdout.write(prompt)
|
|
436
444
|
})
|
|
437
445
|
}
|
|
438
446
|
|
|
@@ -441,73 +449,26 @@ function hiddenInput(prompt) {
|
|
|
441
449
|
* 子进程 env scrub FLYAI_API_KEY/DEBUG_FLYAI_API_KEY 后注入候选。
|
|
442
450
|
* 返回 {verdict,error}。
|
|
443
451
|
*/
|
|
444
|
-
function verifyCandidateKeyInline(candidate) {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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 */ }
|
|
452
|
+
async function verifyCandidateKeyInline(candidate) {
|
|
453
|
+
const controller = new AbortController()
|
|
454
|
+
let interrupted
|
|
455
|
+
const listeners = new Map()
|
|
456
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
457
|
+
const listener = () => {
|
|
458
|
+
interrupted ??= signal
|
|
459
|
+
controller.abort()
|
|
475
460
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
})
|
|
461
|
+
listeners.set(signal, listener)
|
|
462
|
+
process.on(signal, listener)
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const result = await verifyFlyaiCandidate(candidate, { signal: controller.signal })
|
|
466
|
+
return interrupted
|
|
467
|
+
? { ...result, verdict: 'cancelled', exitCode: 128 + (interrupted === 'SIGINT' ? 2 : 15) }
|
|
468
|
+
: result
|
|
469
|
+
} finally {
|
|
470
|
+
for (const [signal, listener] of listeners) process.off(signal, listener)
|
|
471
|
+
}
|
|
511
472
|
}
|
|
512
473
|
|
|
513
474
|
|
|
@@ -521,9 +482,6 @@ const WIZARD_DRY_RUN = process.argv.includes('--dry-run')
|
|
|
521
482
|
const EXT_FROM_ARG = (process.argv.find((a) => a.startsWith('--extension-from=')) ?? '').split('=')[1]
|
|
522
483
|
const EXTENSION_FROM = EXT_FROM_ARG === 'github' || EXT_FROM_ARG === 'bundled' ? EXT_FROM_ARG : process.env.GOTRY_EXTENSION_SOURCE === 'github' ? 'github' : 'bundled'
|
|
523
484
|
|
|
524
|
-
// hbcli 安装通道:npm 优先(staicli@npmjs 双轨;显式 registry 防 bnpm 404),install.sh 兜底
|
|
525
|
-
const HBCLI_NPM_INSTALL_CMD = 'npm install -g staicli --registry=https://registry.npmjs.org/'
|
|
526
|
-
const HBCLI_INSTALL_CMD = HBCLI_NPM_INSTALL_CMD
|
|
527
485
|
const REACH_INSTALL_URL = 'git+https://github.com/Panniantong/Agent-Reach.git'
|
|
528
486
|
|
|
529
487
|
let activeInstallerChild = null
|
|
@@ -672,11 +630,8 @@ function classifyHbcliWhoami(out) {
|
|
|
672
630
|
|
|
673
631
|
const say = (s) => console.log(s)
|
|
674
632
|
|
|
675
|
-
/** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖)
|
|
676
|
-
|
|
677
|
-
* 会把 literal "stored-ticket" 当 bearer 发到 hotel-be,让 trade.* /
|
|
678
|
-
* search/checkAvail 等 @permission: openapi 端点全部 401。 */
|
|
679
|
-
const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? '0.0.3'
|
|
633
|
+
/** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖),与工具层 doctor 共用发布基线。 */
|
|
634
|
+
const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? HBCLI_RELEASE_VERSION
|
|
680
635
|
|
|
681
636
|
function parseVersion(v) {
|
|
682
637
|
const m = String(v || '').match(/(\d+)\.(\d+)\.(\d+)/)
|
|
@@ -704,38 +659,53 @@ async function hbcliVersion(bin) {
|
|
|
704
659
|
})
|
|
705
660
|
}
|
|
706
661
|
|
|
707
|
-
async function setupHbcli() {
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
say(` 升级: npm install -g staicli --registry=https://registry.npmjs.org/`)
|
|
718
|
-
return { ok: CHECK_ONLY ? true : false }
|
|
662
|
+
async function setupHbcli(opts = {}) {
|
|
663
|
+
const home = opts.homeDir ?? homedir()
|
|
664
|
+
const staicliHome = opts.homeDir ? join(home, '.staicli') : process.env.STAICLI_HOME ?? join(home, '.staicli')
|
|
665
|
+
const checkOnly = opts.checkOnly ?? CHECK_ONLY
|
|
666
|
+
say(`[gotry-setup] hbcli(hotelbyte-cli,可选酒店实时源;需 ≥ v${MIN_HBCLI_VERSION})`)
|
|
667
|
+
const candidates = ['hbcli', join(home, '.local/bin/hbcli'), join(home, '.staicli/current/hbcli')]
|
|
668
|
+
const findInstalled = async () => {
|
|
669
|
+
for (const bin of candidates) {
|
|
670
|
+
const version = await hbcliVersion(bin)
|
|
671
|
+
if (version) return { bin, version }
|
|
719
672
|
}
|
|
720
|
-
|
|
673
|
+
return null
|
|
674
|
+
}
|
|
675
|
+
const present = await findInstalled()
|
|
676
|
+
const credFile = join(staicliHome, 'credentials.json')
|
|
677
|
+
if (present && versionAtLeast(present.version, MIN_HBCLI_VERSION)) {
|
|
678
|
+
say(` ✓ 已安装(v${present.version.join('.')})`)
|
|
721
679
|
if (existsSync(credFile)) say(' ✓ 凭证已配置(自检: hbcli auth whoami)')
|
|
722
680
|
else say(' ✗ 凭证未配置——酒店检索将用内置静态包(非实时),账号配置见下方指引')
|
|
723
|
-
if (
|
|
681
|
+
if (checkOnly) say(' (--check-only 只报告,不安装)')
|
|
724
682
|
return { ok: true }
|
|
725
683
|
}
|
|
726
|
-
if (
|
|
727
|
-
say(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
684
|
+
if (present) say(` ✗ 版本过旧(v${present.version.join('.')} < v${MIN_HBCLI_VERSION})`)
|
|
685
|
+
else say(' ✗ 未安装或无法读取版本')
|
|
686
|
+
if (checkOnly) { say(' (--check-only 只报告,不安装)'); return { ok: true } }
|
|
687
|
+
say(` ${present ? '升级' : '安装'}中(官方 staicli v${HBCLI_RELEASE_VERSION}): ${HBCLI_INSTALL_CMD}`)
|
|
688
|
+
const attemptInstall = opts.attemptInstall ?? ((command) => run('bash', ['-o', 'pipefail', '-c', command], {
|
|
689
|
+
timeoutMs: 120_000,
|
|
690
|
+
env: { ...process.env, HOME: home, STAICLI_HOME: staicliHome },
|
|
691
|
+
}))
|
|
692
|
+
const r = await attemptInstall(HBCLI_INSTALL_CMD)
|
|
693
|
+
if (!r.ok) { say(` ✗ 安装失败(${r.error})——不影响 gotry,酒店检索将用内置静态包;可稍后重试: npx @danceiny/gotry doctor --fix`); return { ok: false, error: r.error } }
|
|
694
|
+
const installed = await findInstalled()
|
|
695
|
+
if (!installed || !versionAtLeast(installed.version, MIN_HBCLI_VERSION)) {
|
|
696
|
+
const error = `安装后当前候选路径仍未找到 hbcli ≥ v${MIN_HBCLI_VERSION};检查 PATH 是否优先命中了旧版,以及 ${join(home, '.local/bin/hbcli')}`
|
|
697
|
+
say(` ✗ ${error}`)
|
|
698
|
+
return { ok: false, error }
|
|
699
|
+
}
|
|
700
|
+
const binDir = join(home, '.local/bin')
|
|
701
|
+
if (!(process.env.PATH ?? '').split(':').includes(binDir)) {
|
|
732
702
|
say(` ⚠ ${binDir} 不在当前 PATH —— gotry 工具已内建候选路径回退,无需手动处理;其他程序可用: export PATH="${binDir}:$PATH"`)
|
|
733
703
|
}
|
|
734
|
-
say(
|
|
735
|
-
say(' ·
|
|
736
|
-
say(' ·
|
|
737
|
-
say(' ·
|
|
738
|
-
say(' 自检: hbcli auth whoami
|
|
704
|
+
say(` ✓ 安装完成(v${installed.version.join('.')})——已有凭证保留;账号配置:`)
|
|
705
|
+
say(' · 打开 插件 → GoTry → HotelByte,申请沙箱账号并验证保存 App Key/App Secret')
|
|
706
|
+
say(' · 申请入口: https://hotelbyte.com/zh/guides/sandbox-verification')
|
|
707
|
+
say(' · CLI 配置: hbcli auth set-credentials --app-key <appKey> --app-secret <appSecret>')
|
|
708
|
+
say(' 自检: hbcli auth whoami;实际酒店搜索仍需账号开通可用供应商')
|
|
739
709
|
return { ok: true }
|
|
740
710
|
}
|
|
741
711
|
|
|
@@ -889,16 +859,15 @@ async function doctorChecks() {
|
|
|
889
859
|
const nodeFine = maj > 22 || (maj === 22 && min >= 15)
|
|
890
860
|
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
861
|
// 扩展
|
|
892
|
-
const
|
|
893
|
-
|
|
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' })
|
|
862
|
+
const extension = await checkExtensionStatus()
|
|
863
|
+
items.push({ label: 'Stai Travel Bridge 扩展', ok: extension.status === 'ok', level: extension.status, detail: extension.detail, fix: extension.fix })
|
|
895
864
|
// agent-reach(.venv 装在包内)
|
|
896
865
|
const venvPython = join(repoRoot, '.venv/bin/python')
|
|
897
866
|
const reachBin = join(repoRoot, '.venv/bin/agent-reach')
|
|
898
867
|
const reachOk = existsSync(reachBin)
|
|
899
868
|
const reachLevel = reachOk ? 'ok' : existsSync(venvPython) ? 'degraded' : 'missing'
|
|
900
869
|
items.push({ label: 'Agent Reach(网页/社媒读取)', ok: reachOk, level: reachLevel, detail: reachOk ? `已安装(${reachBin})` : reachLevel === 'degraded' ? '.venv 在但缺 agent-reach 包——gotry_agent_reach / gotry_web_search 读页会失败' : '未装配——gotry_agent_reach / gotry_web_search(读网页)/ gotry_video_subtitle / gotry_github_search 全部不可用', fix: reachOk ? undefined : 'npx @danceiny/gotry doctor --fix' })
|
|
901
|
-
// hbcli(裸名靠 PATH 探测;绝对路径 existsSync;版本 < MIN_HBCLI_VERSION
|
|
870
|
+
// hbcli(裸名靠 PATH 探测;绝对路径 existsSync;版本 < MIN_HBCLI_VERSION 需要升级)
|
|
902
871
|
let hbBin = ''
|
|
903
872
|
for (const p of ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]) {
|
|
904
873
|
if (p === 'hbcli') { if (await probe(p, ['version'])) { hbBin = 'hbcli(PATH)'; break } } else if (existsSync(p)) { hbBin = p; break }
|
|
@@ -906,7 +875,7 @@ async function doctorChecks() {
|
|
|
906
875
|
if (hbBin) {
|
|
907
876
|
const v = await hbcliVersion(hbBin)
|
|
908
877
|
if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
|
|
909
|
-
items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})
|
|
878
|
+
items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——请升级到支持当前认证与酒店配置的版本`, fix: HBCLI_INSTALL_CMD })
|
|
910
879
|
} else {
|
|
911
880
|
// 凭证按 whoami 的 JSON 三档判定,不再看退出码(#623:whoami 无凭证也退 0,旧判法恒报「凭证有效」)
|
|
912
881
|
const cred = classifyHbcliWhoami(await probeStdout(hbBin === 'hbcli(PATH)' ? 'hbcli' : hbBin, ['--json', 'auth', 'whoami']))
|
|
@@ -915,7 +884,7 @@ async function doctorChecks() {
|
|
|
915
884
|
detail: cred.ok
|
|
916
885
|
? `已安装且凭证有效(${hbBin}, v${v ? v.join('.') : '?'}${cred.customer ? ',customer 档在用(客户邮箱验证码登录)' : ''})`
|
|
917
886
|
: '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
|
|
918
|
-
fix: cred.ok ? undefined : 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(
|
|
887
|
+
fix: cred.ok ? undefined : 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(客户邮箱验证码登录,新邮箱即注册);B 端租户注册走 hbcli auth register(先 auth send-code);或在 插件 → GoTry → HotelByte 配置沙箱 App Key/App Secret',
|
|
919
888
|
})
|
|
920
889
|
}
|
|
921
890
|
} else {
|
|
@@ -931,7 +900,7 @@ async function doctorChecks() {
|
|
|
931
900
|
let flyaiFix
|
|
932
901
|
if (!flyaiResolved.key) {
|
|
933
902
|
flyaiDetail = '未配置 key——匿名试用中(共享额度易达限;达限报 Trial limit reached)。影响面:共享池额度小,频繁会话易触顶;达限本会话内 gotry_flyai_search(机票/酒店/票务/AI/万豪 8 类)失败,改走 gotry_session_search 账号会话通道。'
|
|
934
|
-
flyaiFix = '
|
|
903
|
+
flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`:① 打开 https://flyai.open.fliggy.com/console 复制 API Key;② 在终端隐藏粘贴;③ 验证后保存。也可运行 `npx @danceiny/gotry doctor --fix` 按提示配置'
|
|
935
904
|
} else {
|
|
936
905
|
const keySha = sha256Inline(flyaiResolved.key)
|
|
937
906
|
const receiptMatches = flyaiReceipt
|
|
@@ -946,16 +915,16 @@ async function doctorChecks() {
|
|
|
946
915
|
flyaiDetail = `已验证 ${flyaiReceipt.at}(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
|
|
947
916
|
} else if (flyaiReceipt && flyaiReceipt.keySha256 === keySha && flyaiReceipt.verdict !== 'verified') {
|
|
948
917
|
flyaiDetail = `已配置未通过验证(${flyaiReceipt.verdict}@ ${flyaiReceipt.at};${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})——非空不等于鉴权通过`
|
|
949
|
-
flyaiFix = '
|
|
918
|
+
flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」重新验证;或运行 `npx @danceiny/gotry setup flyai`;`npx @danceiny/gotry setup flyai --clear` 回退匿名'
|
|
950
919
|
} else {
|
|
951
920
|
flyaiDetail = `已配置,未验证(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
|
|
952
|
-
flyaiFix = '
|
|
921
|
+
flyaiFix = 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`,按提示隐藏粘贴 API Key'
|
|
953
922
|
}
|
|
954
923
|
}
|
|
955
924
|
items.push({ label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪 8 类)', ok: flyaiLevel === 'ok', level: flyaiLevel, detail: flyaiDetail, fix: flyaiFix })
|
|
956
925
|
// sidebar(状态面与 setupSidebar 的落盘复核同一口径)
|
|
957
926
|
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)' : '
|
|
927
|
+
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
928
|
// dsh-calendar(setup 状态面;默认不挂载=ok 是合法态,opt-in 未配置才 degraded)
|
|
960
929
|
const calState = readCalendarState()
|
|
961
930
|
const calOn = calState?.enabled === true
|
|
@@ -1018,8 +987,8 @@ const fixCell = (fix) => (!fix ? '—' : /^(npx|hbcli|curl|pip|python|\$)/.test(
|
|
|
1018
987
|
function startupDoctorLine(items) {
|
|
1019
988
|
const broken = (items ?? []).filter((i) => i.level && i.level !== 'ok')
|
|
1020
989
|
if (broken.length === 0) return null
|
|
1021
|
-
const human =
|
|
1022
|
-
return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i
|
|
990
|
+
const human = i => i.level === 'missing' ? '缺' : i.label.startsWith('Stai Travel Bridge') ? '待确认' : '半可用'
|
|
991
|
+
return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx @danceiny/gotry doctor`
|
|
1023
992
|
}
|
|
1024
993
|
|
|
1025
994
|
/** 体检报告 markdown(与 ts/capabilities/doctor.ts renderDoctorReportMd 同形) */
|
|
@@ -1073,11 +1042,26 @@ async function runDoctor() {
|
|
|
1073
1042
|
if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
|
|
1074
1043
|
const failedFix = results.filter((r) => !r.ok).length
|
|
1075
1044
|
say(failedFix === 0 ? '[gotry-doctor] 补装完成;下面是补装后复检。' : `[gotry-doctor] ${failedFix} 项补装失败——见上方安装器输出;可重跑 npx @danceiny/gotry doctor --fix`)
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1045
|
+
}
|
|
1046
|
+
// Explicit --fix may offer local credential setup, but never consumes a
|
|
1047
|
+
// secret from CI, a pipe, startup diagnostics or the model tool surface.
|
|
1048
|
+
if (items.some(i => i.label.startsWith('FlyAI(') && i.level !== 'ok')
|
|
1049
|
+
&& process.platform !== 'win32' && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
1050
|
+
process.stdout.write('[gotry-doctor] 现在配置 FlyAI?控制台取 key → 隐藏粘贴 → 验证后保存(y/N):')
|
|
1051
|
+
const answer = await readOneLine(process.stdin)
|
|
1052
|
+
if (/^y(?:es)?$/i.test(answer.trim())) {
|
|
1053
|
+
const setupCode = await runSetupFlyai()
|
|
1054
|
+
if (setupCode === 130 || setupCode === 143) return setupCode
|
|
1055
|
+
if (setupCode !== 0) say('[gotry-doctor] FlyAI 配置未完成;可重跑 `npx @danceiny/gotry setup flyai`。')
|
|
1056
|
+
} else {
|
|
1057
|
+
say('[gotry-doctor] 已跳过 FlyAI 配置;稍后运行 `npx @danceiny/gotry setup flyai`。')
|
|
1079
1058
|
}
|
|
1080
1059
|
}
|
|
1060
|
+
items = await doctorChecks()
|
|
1061
|
+
for (const i of items) {
|
|
1062
|
+
say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
|
|
1063
|
+
if (i.level !== 'ok' && i.fix) say(` ↳ 修复: ${i.fix}`)
|
|
1064
|
+
}
|
|
1081
1065
|
}
|
|
1082
1066
|
// 报告落盘(侧栏工作台预览面;写失败不挡体检结论)
|
|
1083
1067
|
try {
|
|
@@ -1157,8 +1141,7 @@ function classifyDoctorGap(item, opts = {}) {
|
|
|
1157
1141
|
const autoSupported = doctorFixAutoSupported(platform)
|
|
1158
1142
|
if (label.startsWith('Agent Reach')) return installerEnabled('reach', env) && autoSupported ? 'auto' : 'unavailable'
|
|
1159
1143
|
// hbcli:二进制缺失(missing)= auto(setupHbcli 装;win32 → unavailable,上游无 win 安装面);
|
|
1160
|
-
// 凭证未配(degraded)= user-action(平台无关);旧版本(missing
|
|
1161
|
-
// 但安装会失败 → 结果兜底 unavailable,诚实不冒充。
|
|
1144
|
+
// 凭证未配(degraded)= user-action(平台无关);旧版本(missing)= auto,同安装器升级后复检。
|
|
1162
1145
|
if (label.startsWith('hbcli')) {
|
|
1163
1146
|
if (item.level === 'degraded') return 'user-action'
|
|
1164
1147
|
return installerEnabled('hbcli', env) && autoSupported ? 'auto' : 'unavailable'
|
|
@@ -1780,7 +1763,7 @@ if (process.argv[1] && process.argv[1].endsWith('gotry-bootstrap.js')) {
|
|
|
1780
1763
|
}
|
|
1781
1764
|
|
|
1782
1765
|
export {
|
|
1783
|
-
setupSidebar, sidebarInstalled,
|
|
1766
|
+
setupHbcli, setupSidebar, sidebarInstalled,
|
|
1784
1767
|
classifyDoctorGap, buildOnboardingPlan, onboardingSkipReason,
|
|
1785
1768
|
runOnboardingFix, promptOnboarding, renderClassifiedPlan, runOnboarding, orchestrateWebLaunch,
|
|
1786
1769
|
doctorFixAutoSupported, platformAutoUnavailableReason, installerEnabled,
|
|
@@ -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
|
+
}
|