@danceiny/gotry 0.2.0-rc.31 → 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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +63 -53
  4. package/bin/gotry-hbcli-release.d.ts +3 -0
  5. package/bin/gotry-hbcli-release.js +4 -0
  6. package/client/client.js +66 -21
  7. package/dist/capabilities/doctor.js +7 -8
  8. package/dist/capabilities/effect.js +45 -12
  9. package/dist/capabilities/flyai-config.js +12 -10
  10. package/dist/capabilities/flyai.js +87 -56
  11. package/dist/capabilities/hbcli-config.js +156 -0
  12. package/dist/capabilities/hbcli.js +29 -2
  13. package/dist/capabilities/resilience.js +6 -6
  14. package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
  16. package/dist/capabilities/session/ctrip-city.js +186 -0
  17. package/dist/capabilities/session/extension-bridge.js +8 -1
  18. package/dist/capabilities/session/gui-flight.js +737 -0
  19. package/dist/capabilities/session/gui-hotel.js +413 -0
  20. package/dist/capabilities/session-search.js +98 -11
  21. package/dist/scripts/bootstrap-tests.js +23 -6
  22. package/dist/scripts/ctrip-city-tests.js +80 -0
  23. package/dist/scripts/effect-tests.js +602 -504
  24. package/dist/scripts/extension-tests.js +14 -8
  25. package/dist/scripts/flight-malformed-tests.js +1 -1
  26. package/dist/scripts/flyai-setup-tests.js +19 -4
  27. package/dist/scripts/flyai-tests.js +768 -653
  28. package/dist/scripts/gotry-web-api-tests.js +5 -0
  29. package/dist/scripts/gotry-web-client-tests.js +20 -1
  30. package/dist/scripts/gui-flight-live.js +27 -0
  31. package/dist/scripts/hbcli-tests.js +1 -1
  32. package/dist/scripts/hbcli-upgrade-tests.js +107 -0
  33. package/dist/scripts/hbcli-web-setup-tests.js +114 -0
  34. package/dist/scripts/hotel-date-gate-tests.js +1 -1
  35. package/dist/scripts/hotel-result-tests.js +169 -0
  36. package/dist/scripts/issue-271-liveness-tests.js +1 -1
  37. package/dist/scripts/session-tests.js +177 -6
  38. package/dist/src/gotry-web-api.js +64 -2
  39. package/dist/src/index.js +40 -18
  40. package/extension/manifest.json +2 -2
  41. package/package.json +2 -1
  42. package/ts/capabilities/flyai-config.ts +12 -10
  43. package/ts/capabilities/flyai.ts +91 -50
  44. package/ts/capabilities/hbcli-config.ts +128 -0
  45. package/ts/capabilities/hbcli.ts +30 -3
  46. package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
  47. package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
  48. package/ts/capabilities/session/ctrip-city.ts +158 -0
  49. package/ts/capabilities/session/extension-bridge.ts +9 -2
  50. package/ts/capabilities/session/gui-flight.ts +792 -0
  51. package/ts/capabilities/session/gui-hotel.ts +451 -0
  52. package/ts/capabilities/session-search.ts +115 -15
  53. package/ts/src/gotry-web-api.ts +51 -2
  54. package/ts/src/index.ts +41 -19
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.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.
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.30` (npm `latest`).** Verification gates: `scripts/run-all-tests.sh`; release flow: `scripts/publish-npm.sh`.
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.30**。未到 1.0:核心链路已端到端可用;评测仍停留在确定性合同与校验器阶段,无外部分数、无 uplift 声明。外部 W2A 事件仍是合同层、默认 inert;尚无真实 sensor 桥或 consumer 激活。
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.30`(npm `latest`)。** 验证闸:`scripts/run-all-tests.sh`;发布流程:`scripts/publish-npm.sh`。
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>
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * gotry 外部依赖自举(founder 2026-08-29 指令:安装 gotry 时按上游本身的方式装好外部依赖):
4
- * hbcli(hotelbyte-cli) → npm 优先(staicli@npmjs;Node ≥ 20),原生二进制 install.sh 兜底
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 万周装):
@@ -62,6 +62,7 @@ import { fileURLToPath } from 'node:url'
62
62
  import { createRequire } from 'node:module'
63
63
  import { createHash } from 'node:crypto'
64
64
  import { checkExtensionStatus } from './gotry-extension-status.js'
65
+ import { HBCLI_RELEASE_VERSION, HBCLI_INSTALL_CMD } from './gotry-hbcli-release.js'
65
66
 
66
67
  // ── FlyAI 凭据面(纯 JS 内联;与 ts/capabilities/flyai-config.ts 成对维护,
67
68
  // 口径以官方 @fly-ai/flyai-cli 1.0.16 解包事实为准。bootstrap 由 inner 以
@@ -164,24 +165,24 @@ function verifySecureModeInline(dir, file) {
164
165
 
165
166
  /**
166
167
  * 官方 CLI 首次运行可能先创建 ~/.flyai 为 0755(并写入 0600 device-id)。
167
- * 仅对当前用户拥有、且本身不是 symlink 的 .flyai 目录收紧权限;.gotry
168
- * 仍由 atomicWriteJsonInline 严格要求预先存在时就是 0700,不走此兼容路径。
168
+ * 仅对当前用户拥有、且本身不是 symlink 的目录收紧权限。
169
+ * 回执所在 .gotry 同样兼容 doctor/runtime 创建的既有 0755 目录。
169
170
  */
170
- function prepareFlyaiConfigDirInline() {
171
- const dir = dirname(flyaiConfigFile())
171
+ function preparePrivateDirInline(file = flyaiConfigFile()) {
172
+ const dir = dirname(file)
172
173
  try {
173
174
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
174
175
  const entry = lstatSync(dir)
175
- if (entry.isSymbolicLink()) return { ok: false, error: '.flyai 目录是符号链接,拒绝写入' }
176
- if (!entry.isDirectory()) return { ok: false, error: '.flyai 路径不是目录,拒绝写入' }
176
+ if (entry.isSymbolicLink()) return { ok: false, error: '配置目录是符号链接,拒绝写入' }
177
+ if (!entry.isDirectory()) return { ok: false, error: '配置路径不是目录,拒绝写入' }
177
178
  if (typeof process.getuid === 'function' && entry.uid !== process.getuid()) {
178
- return { ok: false, error: '.flyai 目录不属于当前用户,拒绝写入' }
179
+ return { ok: false, error: '配置目录不属于当前用户,拒绝写入' }
179
180
  }
180
181
  if ((entry.mode & 0o777) !== 0o700) chmodSync(dir, 0o700)
181
182
  const after = lstatSync(dir)
182
- if (after.isSymbolicLink() || !after.isDirectory()) return { ok: false, error: '.flyai 目录状态发生变化,拒绝写入' }
183
- if (typeof process.getuid === 'function' && after.uid !== process.getuid()) return { ok: false, error: '.flyai 目录所有者发生变化,拒绝写入' }
184
- return verifySecureModeInline(dir, flyaiConfigFile())
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)
185
186
  } catch (e) {
186
187
  return { ok: false, error: e.message }
187
188
  }
@@ -291,7 +292,7 @@ async function runSetupFlyai() {
291
292
  if (Object.keys(next).length === 0) {
292
293
  rmSync(flyaiConfigFile(), { force: true })
293
294
  } else {
294
- const configDir = prepareFlyaiConfigDirInline()
295
+ const configDir = preparePrivateDirInline()
295
296
  if (!configDir.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${configDir.error}`); return 1 }
296
297
  const w = atomicWriteJsonInline(flyaiConfigFile(), next)
297
298
  if (!w.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${w.error}`); return 1 }
@@ -349,7 +350,7 @@ async function runSetupFlyai() {
349
350
  return 1
350
351
  }
351
352
  const hadConfigFile = existsSync(flyaiConfigFile())
352
- const configDir = prepareFlyaiConfigDirInline()
353
+ const configDir = preparePrivateDirInline()
353
354
  if (!configDir.ok) {
354
355
  say(`[gotry-setup] ✗ 配置目录不安全,旧配置保留:${configDir.error}`)
355
356
  return 1
@@ -380,7 +381,8 @@ async function runSetupFlyai() {
380
381
  endpointDebug: epAfter.debug,
381
382
  at: new Date().toISOString(),
382
383
  }
383
- const receiptWrite = atomicWriteJsonInline(flyaiVerificationFile(), receipt)
384
+ const receiptDir = preparePrivateDirInline(flyaiVerificationFile())
385
+ const receiptWrite = receiptDir.ok ? atomicWriteJsonInline(flyaiVerificationFile(), receipt) : receiptDir
384
386
  if (!receiptWrite.ok) {
385
387
  const rollback = hadConfigFile
386
388
  ? atomicWriteJsonInline(flyaiConfigFile(), before.data)
@@ -480,9 +482,6 @@ const WIZARD_DRY_RUN = process.argv.includes('--dry-run')
480
482
  const EXT_FROM_ARG = (process.argv.find((a) => a.startsWith('--extension-from=')) ?? '').split('=')[1]
481
483
  const EXTENSION_FROM = EXT_FROM_ARG === 'github' || EXT_FROM_ARG === 'bundled' ? EXT_FROM_ARG : process.env.GOTRY_EXTENSION_SOURCE === 'github' ? 'github' : 'bundled'
482
484
 
483
- // hbcli 安装通道:npm 优先(staicli@npmjs 双轨;显式 registry 防 bnpm 404),install.sh 兜底
484
- const HBCLI_NPM_INSTALL_CMD = 'npm install -g staicli --registry=https://registry.npmjs.org/'
485
- const HBCLI_INSTALL_CMD = HBCLI_NPM_INSTALL_CMD
486
485
  const REACH_INSTALL_URL = 'git+https://github.com/Panniantong/Agent-Reach.git'
487
486
 
488
487
  let activeInstallerChild = null
@@ -631,11 +630,8 @@ function classifyHbcliWhoami(out) {
631
630
 
632
631
  const say = (s) => console.log(s)
633
632
 
634
- /** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖)。
635
- * 0.0.3 修了 portal-ticket fallback bug(issue #142):低于该版本的 hbcli
636
- * 会把 literal "stored-ticket" 当 bearer 发到 hotel-be,让 trade.* /
637
- * search/checkAvail 等 @permission: openapi 端点全部 401。 */
638
- 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
639
635
 
640
636
  function parseVersion(v) {
641
637
  const m = String(v || '').match(/(\d+)\.(\d+)\.(\d+)/)
@@ -663,38 +659,53 @@ async function hbcliVersion(bin) {
663
659
  })
664
660
  }
665
661
 
666
- async function setupHbcli() {
667
- say(`[gotry-setup] hbcli(hotelbyte-cli,可选酒店实时源;需 ≥ v${MIN_HBCLI_VERSION},低于会踩 issue #142 的 401)`)
668
- const candidates = ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]
669
- const present = candidates.some((p) => existsSync(p)) && (await probe(candidates[0], ['version']) || await probe(candidates[1], ['version']) || await probe(candidates[2], ['version']))
670
- const credFile = join(homedir(), '.staicli', 'credentials.json')
671
- if (present) {
672
- const hit = candidates.find((p) => existsSync(p)) || 'hbcli'
673
- const v = await hbcliVersion(hit)
674
- if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
675
- say(` ✗ 版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——@permission: openapi 端点(trade.* / search/checkAvail)会 401(issue #142)`)
676
- say(` 升级: npm install -g staicli --registry=https://registry.npmjs.org/`)
677
- 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 }
678
672
  }
679
- say(` ✓ 已安装(v${v ? v.join('.') : '?'})`)
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('.')})`)
680
679
  if (existsSync(credFile)) say(' ✓ 凭证已配置(自检: hbcli auth whoami)')
681
680
  else say(' ✗ 凭证未配置——酒店检索将用内置静态包(非实时),账号配置见下方指引')
682
- if (CHECK_ONLY) say(' (--check-only 只报告,不安装)')
681
+ if (checkOnly) say(' (--check-only 只报告,不安装)')
683
682
  return { ok: true }
684
683
  }
685
- if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
686
- say(` 安装中(npm,staicli@npmjs): ${HBCLI_INSTALL_CMD}`)
687
- const r = await run('bash', ['-c', HBCLI_INSTALL_CMD], { timeoutMs: 120_000 })
688
- if (!r.ok) { say(` ✗ 安装失败(${r.error})——不影响 gotry,酒店检索将用内置静态包;可稍后重试: npx @danceiny/gotry setup`); return { ok: false } }
689
- const binDir = join(homedir(), '.local/bin')
690
- if (!process.env.PATH.split(':').includes(binDir)) {
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)) {
691
702
  say(` ⚠ ${binDir} 不在当前 PATH —— gotry 工具已内建候选路径回退,无需手动处理;其他程序可用: export PATH="${binDir}:$PATH"`)
692
703
  }
693
- say(' ✓ 安装完成——账号配置(未配时酒店检索自动用静态包,非实时):')
694
- say(' · 快速试用(官方沙箱演示账号): hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo')
695
- say(' · 正式接入: 向 HotelByte 申请专属 appKey/appSecret 后用同一命令替换')
696
- say(' · 门户账号(权限更大,酒店搜索无必要): hbcli auth login --username <email>')
697
- say(' 自检: hbcli auth whoami(api_key.configured=true 即就位)')
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;实际酒店搜索仍需账号开通可用供应商')
698
709
  return { ok: true }
699
710
  }
700
711
 
@@ -856,7 +867,7 @@ async function doctorChecks() {
856
867
  const reachOk = existsSync(reachBin)
857
868
  const reachLevel = reachOk ? 'ok' : existsSync(venvPython) ? 'degraded' : 'missing'
858
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' })
859
- // hbcli(裸名靠 PATH 探测;绝对路径 existsSync;版本 < MIN_HBCLI_VERSION 视为 missing,见 issue #142)
870
+ // hbcli(裸名靠 PATH 探测;绝对路径 existsSync;版本 < MIN_HBCLI_VERSION 需要升级)
860
871
  let hbBin = ''
861
872
  for (const p of ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]) {
862
873
  if (p === 'hbcli') { if (await probe(p, ['version'])) { hbBin = 'hbcli(PATH)'; break } } else if (existsSync(p)) { hbBin = p; break }
@@ -864,7 +875,7 @@ async function doctorChecks() {
864
875
  if (hbBin) {
865
876
  const v = await hbcliVersion(hbBin)
866
877
  if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
867
- items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——trade.* / search/checkAvail 会 401(issue #142)`, fix: 'npm install -g staicli --registry=https://registry.npmjs.org/' })
878
+ items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——请升级到支持当前认证与酒店配置的版本`, fix: HBCLI_INSTALL_CMD })
868
879
  } else {
869
880
  // 凭证按 whoami 的 JSON 三档判定,不再看退出码(#623:whoami 无凭证也退 0,旧判法恒报「凭证有效」)
870
881
  const cred = classifyHbcliWhoami(await probeStdout(hbBin === 'hbcli(PATH)' ? 'hbcli' : hbBin, ['--json', 'auth', 'whoami']))
@@ -873,7 +884,7 @@ async function doctorChecks() {
873
884
  detail: cred.ok
874
885
  ? `已安装且凭证有效(${hbBin}, v${v ? v.join('.') : '?'}${cred.customer ? ',customer 档在用(客户邮箱验证码登录)' : ''})`
875
886
  : '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
876
- fix: cred.ok ? undefined : 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(客户邮箱验证码登录,新邮箱即注册,需 npm staicli ≥ 0.0.4);B 端租户注册走 hbcli auth register(先 auth send-code);或 hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)',
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',
877
888
  })
878
889
  }
879
890
  } else {
@@ -1130,8 +1141,7 @@ function classifyDoctorGap(item, opts = {}) {
1130
1141
  const autoSupported = doctorFixAutoSupported(platform)
1131
1142
  if (label.startsWith('Agent Reach')) return installerEnabled('reach', env) && autoSupported ? 'auto' : 'unavailable'
1132
1143
  // hbcli:二进制缺失(missing)= auto(setupHbcli 装;win32 → unavailable,上游无 win 安装面);
1133
- // 凭证未配(degraded)= user-action(平台无关);旧版本(missing,安装器不自动升级)= auto 分桶
1134
- // 但安装会失败 → 结果兜底 unavailable,诚实不冒充。
1144
+ // 凭证未配(degraded)= user-action(平台无关);旧版本(missing)= auto,同安装器升级后复检。
1135
1145
  if (label.startsWith('hbcli')) {
1136
1146
  if (item.level === 'degraded') return 'user-action'
1137
1147
  return installerEnabled('hbcli', env) && autoSupported ? 'auto' : 'unavailable'
@@ -1753,7 +1763,7 @@ if (process.argv[1] && process.argv[1].endsWith('gotry-bootstrap.js')) {
1753
1763
  }
1754
1764
 
1755
1765
  export {
1756
- setupSidebar, sidebarInstalled,
1766
+ setupHbcli, setupSidebar, sidebarInstalled,
1757
1767
  classifyDoctorGap, buildOnboardingPlan, onboardingSkipReason,
1758
1768
  runOnboardingFix, promptOnboarding, renderClassifiedPlan, runOnboarding, orchestrateWebLaunch,
1759
1769
  doctorFixAutoSupported, platformAutoUnavailableReason, installerEnabled,
@@ -0,0 +1,3 @@
1
+ export const HBCLI_RELEASE_VERSION: string
2
+ export const HBCLI_INSTALL_URL: string
3
+ export const HBCLI_INSTALL_CMD: string
@@ -0,0 +1,4 @@
1
+ /** Official stable hbcli baseline. npm's staicli@latest still points to 0.0.3. */
2
+ export const HBCLI_RELEASE_VERSION = '0.0.4'
3
+ export const HBCLI_INSTALL_URL = `https://github.com/hotelbyte-com/docs/releases/download/staicli-v${HBCLI_RELEASE_VERSION}/install.sh`
4
+ export const HBCLI_INSTALL_CMD = `curl -fsSL --connect-timeout 15 --max-time 60 ${HBCLI_INSTALL_URL} | bash -s -- --version ${HBCLI_RELEASE_VERSION}`
package/client/client.js CHANGED
@@ -26,6 +26,15 @@ window.__ModuleLoader__.load({
26
26
  saved: 'Verified and saved.', clear: 'Clear local key', cleared: 'Local key cleared. Anonymous trial is now active.',
27
27
  retry: 'Refresh status', source: 'Source', config: 'Local configuration', env: 'Environment variable',
28
28
  'env-debug': 'Debug environment variable', none: 'Shared trial',
29
+ hbcli: 'HotelByte · sandbox', appKey: 'App Key', appSecret: 'App Secret',
30
+ hbHint: 'Use the credentials supplied with your sandbox account. They never enter the conversation.',
31
+ hbRegistration: 'Apply for a sandbox account', hbUnconfigured: 'Sandbox API credentials are not configured', hbConfigured: 'Sandbox API credentials configured',
32
+ hbReadOnly: 'Credentials come from the launch environment, or the local configuration cannot be safely written.',
33
+ hbSaveFailed: 'Credentials were not saved; the previous configuration is retained.',
34
+ hbSaved: 'Credentials saved and the read-only hotel query completed.', hbClear: 'Clear local sandbox credentials',
35
+ hbCleared: 'Local sandbox API credentials cleared.', hbNoSuppliers: 'Saved · no available suppliers configured',
36
+ hbNoSuppliersHint: 'HotelByte must enable suppliers for your account. This result does not mean the destination has no rooms.',
37
+ hbMiss: 'Query verified · no hotels in this check', hbProbeHint: 'Verification makes one read-only Shanghai hotel query for tomorrow, one night and two adults.',
29
38
  artifacts: 'GoTry artifacts', artifactsHint: 'Browse saved itineraries and planning deliveries',
30
39
  search: 'Search artifacts', searchPlaceholder: 'Title or filename', refresh: 'Refresh',
31
40
  empty: 'No saved artifacts in this workspace.', noMatches: 'No matching artifacts.', noSession: 'Open a conversation to browse its workspace.',
@@ -43,6 +52,15 @@ window.__ModuleLoader__.load({
43
52
  saved: '验证通过,已保存。', clear: '清除本机 Key', cleared: '本机 Key 已清除,已恢复匿名试用。',
44
53
  retry: '重新读取状态', source: '来源', config: '本机配置', env: '环境变量',
45
54
  'env-debug': '调试环境变量', none: '共享试用',
55
+ hbcli: 'HotelByte(沙箱)', appKey: 'App Key', appSecret: 'App Secret',
56
+ hbHint: '填写开通沙箱账号后取得的凭证,内容不会进入对话。',
57
+ hbRegistration: '申请沙箱账号', hbUnconfigured: '尚未配置沙箱 API 凭证', hbConfigured: '已配置沙箱 API 凭证',
58
+ hbReadOnly: '当前凭证来自启动环境变量,或本机配置无法安全写入。',
59
+ hbSaveFailed: '新凭证未保存,旧配置已保留。',
60
+ hbSaved: '凭证已保存,只读酒店查询已完成。', hbClear: '清除本机沙箱凭证',
61
+ hbCleared: '本机沙箱 API 凭证已清除。', hbNoSuppliers: '已保存 · 未配置可用供应商',
62
+ hbNoSuppliersHint: '请联系 HotelByte 开通当前账号的供应商。这次结果不能判断目的地无房。',
63
+ hbMiss: '查询通道已验证 · 本次未返回酒店', hbProbeHint: '验证会只读查询上海明天入住的一晚酒店,两位成人。',
46
64
  artifacts: 'GoTry 产物', artifactsHint: '查看已保存行程和规划交付物',
47
65
  search: '搜索产物', searchPlaceholder: '标题或文件名', refresh: '刷新',
48
66
  empty: '这个工作区尚无已保存产物。', noMatches: '没有匹配的产物。', noSession: '打开一个对话后即可查看其工作区产物。',
@@ -59,10 +77,12 @@ window.__ModuleLoader__.load({
59
77
  return value
60
78
  }
61
79
 
62
- function FlyaiSettings(props) {
80
+ function SourceSettings(props) {
63
81
  var t = props.t
82
+ var hotelbyte = props.provider === 'hbcli', provider = hotelbyte ? 'hbcli' : 'flyai'
64
83
  var state = React.useState(null), status = state[0], setStatus = state[1]
65
84
  var draft = React.useState(''), key = draft[0], setKey = draft[1]
85
+ var secretDraft = React.useState(''), secret = secretDraft[0], setSecret = secretDraft[1]
66
86
  var noticeState = React.useState(''), notice = noticeState[0], setNotice = noticeState[1]
67
87
  var errorState = React.useState(''), error = errorState[0], setError = errorState[1]
68
88
  var busyState = React.useState(false), busy = busyState[0], setBusy = busyState[1]
@@ -72,7 +92,7 @@ window.__ModuleLoader__.load({
72
92
  var controller = new AbortController()
73
93
  lifetime.current = controller
74
94
  setError('')
75
- webRequest('flyai', { signal: controller.signal }).then(function (value) {
95
+ webRequest(provider, { signal: controller.signal }).then(function (value) {
76
96
  if (!controller.signal.aborted) setStatus(value)
77
97
  }).catch(function () { if (!controller.signal.aborted) setError(t('failed')) })
78
98
  return function () { controller.abort(); lifetime.current = null }
@@ -82,14 +102,20 @@ window.__ModuleLoader__.load({
82
102
  var controller = lifetime.current
83
103
  activeRequest.current = true
84
104
  setBusy(true); setError(''); setNotice('')
85
- var candidate = key
86
- setKey('')
105
+ var candidate = key, candidateSecret = secret
106
+ setKey(''); setSecret('')
87
107
  try {
88
- var value = await webRequest('flyai/write', { method: 'POST', signal: controller.signal,
89
- headers: { 'content-type': 'application/json' }, body: JSON.stringify(action === 'save' ? { action: action, key: candidate } : { action: action }) })
90
- if (!controller.signal.aborted) { setStatus(value); setNotice(t(action === 'save' ? 'saved' : 'cleared')) }
108
+ var payload = action === 'clear' ? { action: action } : hotelbyte
109
+ ? { action: action, appKey: candidate, appSecret: candidateSecret } : { action: action, key: candidate }
110
+ var value = await webRequest(provider + '/write', { method: 'POST', signal: controller.signal,
111
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
112
+ if (!controller.signal.aborted) {
113
+ setStatus(value)
114
+ setNotice(hotelbyte && action === 'save' && value.verdict === 'no-suppliers' ? ''
115
+ : t(hotelbyte ? action === 'clear' ? 'hbCleared' : 'hbSaved' : action === 'save' ? 'saved' : 'cleared'))
116
+ }
91
117
  } catch (failure) {
92
- if (!controller.signal.aborted) setError(failure.message || t('saveFailed'))
118
+ if (!controller.signal.aborted) setError(failure.message || t(hotelbyte ? 'hbSaveFailed' : 'saveFailed'))
93
119
  } finally {
94
120
  activeRequest.current = false
95
121
  if (!controller.signal.aborted) setBusy(false)
@@ -97,21 +123,32 @@ window.__ModuleLoader__.load({
97
123
  }
98
124
  if (props.view === 'summary') return t('description')
99
125
  var disabled = busy || !status?.writable
100
- var stateLabel = !status ? t('loading') : !status.configured ? t('anonymous') : status.verified ? t('verified') : t('unverified')
101
- return element('section', { 'data-gotry-flyai-settings': '', style: { display: 'grid', gap: '16px', maxWidth: '640px' } }, [
102
- element('h3', { key: 'heading', style: { margin: 0 } }, t('flyai')),
103
- element('div', { key: 'status', role: 'status', 'data-gotry-flyai-status': '', style: { display: 'grid', gap: '6px' } }, [
126
+ var stateLabel = !status ? t('loading') : !status.configured ? t(hotelbyte ? 'hbUnconfigured' : 'anonymous')
127
+ : hotelbyte && status.verdict === 'no-suppliers' ? t('hbNoSuppliers') : hotelbyte && status.verdict === 'miss' ? t('hbMiss')
128
+ : status.verified ? t('verified') : t('unverified')
129
+ var sectionProps = { style: { display: 'grid', gap: '16px', maxWidth: '640px' } }
130
+ sectionProps['data-gotry-' + provider + '-settings'] = ''
131
+ var statusProps = { key: 'status', role: 'status', style: { display: 'grid', gap: '6px' } }
132
+ statusProps['data-gotry-' + provider + '-status'] = ''
133
+ return element('section', sectionProps, [
134
+ element('h3', { key: 'heading', style: { margin: 0 } }, t(provider)),
135
+ element('div', statusProps, [
104
136
  element(UI.Tag, { key: 'badge', tone: status?.verified ? 'success' : 'neutral' }, stateLabel),
105
- status ? element('span', { key: 'source', style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, t('source') + ':' + t(status.source)) : null,
137
+ status ? element('span', { key: 'source', style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, t('source') + ':' + t(hotelbyte && status.source === 'none' ? 'hbUnconfigured' : status.source)) : null,
106
138
  status?.endpointDebug ? element('span', { key: 'debug' }, t('debug') + ':' + status.endpoint) : null,
107
139
  ]),
108
- element('a', { key: 'console', href: 'https://flyai.open.fliggy.com/console', target: '_blank', rel: 'noopener noreferrer' }, t('console')),
109
- element(UI.SettingsForm, { key: 'form', labels: { unavailable: t('loading'), readOnly: t('readOnly'), saveFailed: t('saveFailed'), save: t('save'), saving: t('saving') },
110
- state: { available: Boolean(status), writable: Boolean(status?.writable), dirty: key.trim().length > 0, invalid: false, saving: busy, failed: false },
111
- onSave: function () { void write('save') }, onDiscard: function () { setKey('') } }, [
112
- element(UI.SettingsSecretField, { key: 'key', id: 'gotry-flyai-key', label: t('key'), hint: t('keyHint'), text: key,
113
- configured: Boolean(status?.configured), stateLabel: t(status?.configured ? 'configured' : 'anonymous'), disabled: disabled,
140
+ element('a', { key: 'console', href: hotelbyte ? 'https://hotelbyte.com/zh/guides/sandbox-verification' : 'https://flyai.open.fliggy.com/console', target: '_blank', rel: 'noopener noreferrer' }, t(hotelbyte ? 'hbRegistration' : 'console')),
141
+ hotelbyte ? element('p', { key: 'probe-hint', style: { margin: 0, fontSize: '12px' } }, t('hbProbeHint')) : null,
142
+ hotelbyte && status?.verdict === 'no-suppliers' ? element('p', { key: 'supplier-hint', role: 'status', style: { margin: 0 } }, t('hbNoSuppliersHint')) : null,
143
+ element(UI.SettingsForm, { key: 'form', labels: { unavailable: t('loading'), readOnly: t(hotelbyte ? 'hbReadOnly' : 'readOnly'), saveFailed: t(hotelbyte ? 'hbSaveFailed' : 'saveFailed'), save: t('save'), saving: t('saving') },
144
+ state: { available: Boolean(status), writable: Boolean(status?.writable), dirty: key.trim().length > 0 && (!hotelbyte || secret.trim().length > 0), invalid: false, saving: busy, failed: false },
145
+ onSave: function () { void write('save') }, onDiscard: function () { setKey(''); setSecret('') } }, [
146
+ element(UI.SettingsSecretField, { key: 'key', id: 'gotry-' + provider + '-key', label: t(hotelbyte ? 'appKey' : 'key'), hint: t(hotelbyte ? 'hbHint' : 'keyHint'), text: key,
147
+ configured: Boolean(status?.configured), stateLabel: t(status?.configured ? hotelbyte ? 'hbConfigured' : 'configured' : hotelbyte ? 'hbUnconfigured' : 'anonymous'), disabled: disabled,
114
148
  onEdit: function (value) { setKey(value); setError(''); setNotice('') } }),
149
+ hotelbyte ? element(UI.SettingsSecretField, { key: 'secret', id: 'gotry-hbcli-secret', label: t('appSecret'), text: secret,
150
+ configured: Boolean(status?.configured), stateLabel: t(status?.configured ? 'hbConfigured' : 'hbUnconfigured'), disabled: disabled,
151
+ onEdit: function (value) { setSecret(value); setError(''); setNotice('') } }) : null,
115
152
  ]),
116
153
  error ? element('p', { key: 'error', role: 'alert', style: { margin: 0, color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
117
154
  notice ? element('p', { key: 'notice', role: 'status', style: { margin: 0 } }, notice) : null,
@@ -119,11 +156,19 @@ window.__ModuleLoader__.load({
119
156
  element(UI.Button, { key: 'retry', type: 'button', variant: 'outline', disabled: busy,
120
157
  onClick: function () { setReload(function (value) { return value + 1 }) } }, t('retry')),
121
158
  status?.configured && status.writable ? element(UI.Button, { key: 'clear', type: 'button', disabled: busy,
122
- onClick: function () { void write('clear') } }, t('clear')) : null,
159
+ onClick: function () { void write('clear') } }, t(hotelbyte ? 'hbClear' : 'clear')) : null,
123
160
  ]),
124
161
  ])
125
162
  }
126
163
 
164
+ function GoTrySettings(props) {
165
+ if (props.view === 'summary') return props.t('description')
166
+ return element('div', { style: { display: 'grid', gap: '32px' } }, [
167
+ element(SourceSettings, Object.assign({}, props, { key: 'flyai', provider: 'flyai' })),
168
+ element(SourceSettings, Object.assign({}, props, { key: 'hbcli', provider: 'hbcli' })),
169
+ ])
170
+ }
171
+
127
172
  function ArtifactBrowser(props) {
128
173
  var t = props.t, info = props.useTabInfo(), sessionId = props.sessionId
129
174
  var cwd = props.useSessions(function (sessions) { return sessions.byId[sessionId]?.cwd })
@@ -367,7 +412,7 @@ window.__ModuleLoader__.load({
367
412
  var t = scope.locale.bind(ns)
368
413
  scope.effect(function () { return scope.locale.register(ns, webLocale) })
369
414
  scope.slots.inject('plugins.item', function () {
370
- return scope.slots.register({ name: 'plugins.item', id: 'gotry', order: 50, label: function () { return t('title') }, locale: ns }, FlyaiSettings)
415
+ return scope.slots.register({ name: 'plugins.item', id: 'gotry', order: 50, label: function () { return t('title') }, locale: ns }, GoTrySettings)
371
416
  })
372
417
  scope.inject(['sidebarRightTabs'], function (sidebar) {
373
418
  sidebar.effect(function () { return sidebar.sidebarRightTabs.register({ id: '@danceiny/gotry:artifacts', kind: 'gotry-artifacts',
@@ -20,6 +20,7 @@ import { createRequire } from 'node:module';
20
20
  import { homedir } from 'node:os';
21
21
  import { join, resolve } from 'node:path';
22
22
  import { checkExtensionStatus } from '../../bin/gotry-extension-status.js';
23
+ import { HBCLI_RELEASE_VERSION, HBCLI_INSTALL_CMD } from '../../bin/gotry-hbcli-release.js';
23
24
  import { readLatestChannelEvents } from './channel-health.js';
24
25
  import { displayEndpoint, endpointFingerprint, readFlyaiVerification, resolveFlyaiEndpoint, resolveFlyaiKey, sha256Hex, } from './flyai-config.js';
25
26
  /** hbcli 已知安装位(与 capabilities/hbcli.ts hbcliBinCandidates 同清单) */
@@ -110,10 +111,8 @@ export function nodeOk(version) {
110
111
  const [maj = '0', min = '0'] = version.split('.');
111
112
  return Number(maj) > 22 || (Number(maj) === 22 && Number(min) >= 15);
112
113
  }
113
- /** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖)。0.0.3 修了 portal-ticket
114
- * fallback bug(issue #142)——低于该版本的 hbcli 会把 literal "stored-ticket"
115
- * 当 bearer 发到 hotel-be,让 trade.* / search/checkAvail 全部 401。 */
116
- const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? '0.0.3';
114
+ /** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖),与 CLI doctor 共用发布基线。 */
115
+ const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? HBCLI_RELEASE_VERSION;
117
116
  /** 单项检查:全部只读、永不抛错;env/homeDir/repoRoot 可注入(测试确定性) */
118
117
  export async function runDoctorChecks(opts = {}) {
119
118
  const repoRoot = opts.repoRoot ? resolve(opts.repoRoot) : resolve(import.meta.dirname, '..', '..');
@@ -149,7 +148,7 @@ export async function runDoctorChecks(opts = {}) {
149
148
  }
150
149
  // 3. hbcli(酒店实时源;静态包自动降级,缺了不致命)
151
150
  // 裸名 'hbcli' 靠 PATH 解析(existsSync 对裸名是 cwd 相对,无意义)——先探测再落位
152
- // 版本 < MIN_HBCLI_VERSION 视为 missing(issue #142:portal-ticket fallback 401)
151
+ // 版本 < MIN_HBCLI_VERSION 视为 missing,修复走版本钉扎的官方安装器
153
152
  let hbPresent = '';
154
153
  for (const p of hbcliCandidates(home)) {
155
154
  if (p !== 'hbcli' && existsSync(p)) {
@@ -167,8 +166,8 @@ export async function runDoctorChecks(opts = {}) {
167
166
  if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
168
167
  items.push({
169
168
  id: 'hbcli', label: 'hbcli(酒店实时源)', status: 'missing',
170
- detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——trade.* / search/checkAvail 会 401(issue #142)`,
171
- fix: 'npm install -g staicli --registry=https://registry.npmjs.org/',
169
+ detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——请升级到支持当前认证与酒店配置的版本`,
170
+ fix: HBCLI_INSTALL_CMD,
172
171
  });
173
172
  }
174
173
  else {
@@ -194,7 +193,7 @@ export async function runDoctorChecks(opts = {}) {
194
193
  : {
195
194
  id: 'hbcli', label: 'hbcli(酒店实时源)', status: 'degraded',
196
195
  detail: '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
197
- fix: 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(客户邮箱验证码登录,新邮箱即注册,需 npm staicli ≥ 0.0.4);B 端租户注册走 hbcli auth register(先 auth send-code);或 hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)',
196
+ fix: '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',
198
197
  });
199
198
  }
200
199
  }