@danceiny/gotry 0.0.1-rc.12 → 0.0.1-rc.14
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 +121 -189
- package/README.zh-CN.md +233 -0
- package/bin/gotry-bootstrap.js +126 -0
- package/bin/gotry-inner.js +10 -3
- package/cordis.gotry-patch.yml +2 -2
- package/dist/capabilities/hbcli.js +42 -15
- package/dist/capabilities/session/transport.js +11 -1
- package/dist/capabilities/session-login.js +17 -1
- package/dist/capabilities/session-search.js +2 -1
- package/dist/scripts/bootstrap-tests.js +56 -0
- package/dist/scripts/flyai-tests.js +95 -0
- package/dist/scripts/hbcli-tests.js +28 -3
- package/dist/scripts/session-tests.js +2 -0
- package/dist/src/index.js +9 -4
- package/package.json +2 -1
- package/ts/capabilities/hbcli.ts +51 -19
- package/ts/capabilities/session/transport.ts +22 -1
- package/ts/capabilities/session-login.ts +17 -1
- package/ts/capabilities/session-search.ts +2 -1
- package/ts/src/index.ts +18 -6
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# GoTry
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Danceiny/gotry/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
> **身体和灵魂,更多旅行,更少旅游。**
|
|
8
|
+
> *Body and soul — more travel, less tourism.*
|
|
9
|
+
|
|
10
|
+
**GoTry 是「从出发到下一次出发」的 AI 旅行 Agent**:你用一句话说想去哪、为什么想出发;它先问清楚你的工作时间和已订资源,再用**数学求解器**给你一份经过验证的行程方案。
|
|
11
|
+
|
|
12
|
+
**GoTry is an AI travel agent for departure-to-next-departure.** Tell it where you want to go and why. It asks what's missing, then hands you a **formally verified itinerary** — not vibes.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🚀 30 秒上手
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @danceiny/gotry web
|
|
20
|
+
# 首跑会提示建 .env:LLM_API_KEY=<DeepSeek key 或 OpenAI 兼容 key>
|
|
21
|
+
# → 浏览器打开 http://127.0.0.1:3080,像聊天一样说「我想去大理三天」
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| 你想要的 | 命令 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| 🖥️ 对话规划(推荐) | `npx @danceiny/gotry web` → :3080 对话界面 |
|
|
27
|
+
| 🤖 脚本/一次性问答 | `npx @danceiny/gotry "我想从深圳休整两天,预算 3000"` |
|
|
28
|
+
| 🛠️ 开发者:仓内运行 | 见下方[源码安装](#-快速开始) |
|
|
29
|
+
|
|
30
|
+
- 前置:Node 22+;一个 LLM API key。零成本启动——dsh 运行时以 cordis patch 自动挂载,无额外配置。
|
|
31
|
+
- 例外提示:`:3080` 端口被占时先 `kill <PID>`;首启 6–15 秒属正常冷启动;异常退出会留证据到 `gotry-state/incidents.jsonl`(不静默)。
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary>🛠️ 开发者:源码安装(仓内运行)</summary>
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/Danceiny/gotry && cd gotry
|
|
38
|
+
cd ts/dsh-runtime && pnpm install && cd ../.. # ① vendored dsh 0.1.2-alpha.1(一次性)
|
|
39
|
+
cp .env.example .env # ② 填 LLM_API_KEY
|
|
40
|
+
./gotry web # ③ 仓内入口,同 npm 形态
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| 入口 | 命令 | 什么时候用 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| dsh Web 对话(推荐) | `./gotry web` | 持续多轮规划,看推理可视化 → :3080 |
|
|
46
|
+
| headless 一问一答 | `./gotry "一句任务"` | 脚本 / CI / 定向调试 → stdout |
|
|
47
|
+
| help | `./gotry help` | 三行帮助 |
|
|
48
|
+
|
|
49
|
+
为什么源码模式要先装 runtime:dsh 以 vendored tarball 进 git(`ts/dsh-runtime/`),npm 一键分发不打包它;源码检出读 `.ts` 源码,装完一次重复可用。
|
|
50
|
+
|
|
51
|
+
</details>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ✨ 它做什么 — What it does
|
|
56
|
+
|
|
57
|
+
GoTry 把「想去哪」变成「能不能、怎么去」:
|
|
58
|
+
|
|
59
|
+
| 阶段 | 由谁做 | 给你什么 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **动机访谈** | LLM | 必问项:工作窗口 / 已订资源 / 出发城市 |
|
|
62
|
+
| **事实抽取** | LLM | 工作窗口生效 + 休假语义识别 |
|
|
63
|
+
| **可行性判定** | **Z3 求解器** | 哪些候选可行、哪些不可行、为什么、**最小改动让它可行** |
|
|
64
|
+
| **门到门全成本** | 求解器 | 真实飞行时长(含时差)+ 醒来起夜惩罚 + 接驳代价 + 到达精力 % |
|
|
65
|
+
| **证据链** | 渲染层 | 每个数字带标签:`[骨架:openflights]` / `[实时API:flyai]` / `[静态包:估算]` |
|
|
66
|
+
|
|
67
|
+
**不像普通 AI 聊天**——LLM 只做理解和解释,**判定与算术是数学求解器**算的。
|
|
68
|
+
|
|
69
|
+
> **Unlike a regular AI chat**, the LLM only translates and explains. **Decisions and arithmetic are computed by a Z3 solver**, not guessed.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🎬 一段对话 — Demo
|
|
74
|
+
|
|
75
|
+
输入 → 引擎判定 → 输出(真实输出,milliseconds 保留原味):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
> 我想去洱海边发呆两三天,上海出发,预算 3000,年假别让我办公。
|
|
79
|
+
|
|
80
|
+
GoTry: 收到。先把约束记下来——
|
|
81
|
+
• 窗口: 2 天(2026 年窗口) • 出发: 上海 • 预算: ¥3000 全含
|
|
82
|
+
• 动机: 休整 / [escape_rest: 0.7] • 还没订任何东西
|
|
83
|
+
|
|
84
|
+
引擎判定:
|
|
85
|
+
**大理·洱海: 现在不行** —— 2 天窗口装不下「至少 5 天的洱海休整」。
|
|
86
|
+
放宽方案: 行程延长到 5 天,约 ¥4950。★已放入「下一次出发」清单。
|
|
87
|
+
**千岛湖: 可行** (G7315 06:35 出发,¥996,到达精力 84%,有效休整 4.4h)
|
|
88
|
+
**太湖: 可行** (G101 09:00 出发,¥716,有效休整 4.6h)
|
|
89
|
+
建议: 千岛湖(意象匹配 80%)。
|
|
90
|
+
|
|
91
|
+
[骨架:openflights] ✓ SZX↔PVG 已校验 [实时API:hbcli] 上海机场在跑
|
|
92
|
+
[静态包:估算] G7315/G7316 价格按 7-8 月淡季估算
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Brief English summary**: input → engine verdicts (feasibility + whole-cost) → recommendation + wish-pool entry for infeasible candidates. Every numeric carries an evidence tag.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🧰 18 个工具 — Tools
|
|
100
|
+
|
|
101
|
+
| 组 | 工具 | 干什么 |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| **实时检索(OTA/官方只读)** | `gotry_flyai_search` | 机票/火车/酒店实时报价(飞猪官方通道;酒店价格为上游打码展示,真实价以 jumpUrl 为准) |
|
|
104
|
+
| | `gotry_session_search` | 在**用户本人登录态**里查携程机票(授权后,只读) |
|
|
105
|
+
| | `gotry_session_login` | 登录引导:自动检测已登录与否,未登录才在用户 Chrome 弹登录入口(**零终端**) |
|
|
106
|
+
| | `gotry_weather_check` | Open-Meteo 预报≤16 天 + 历史气候基线 |
|
|
107
|
+
| | `gotry_flight_verify` | OpenSky ADS-B 航班实时观测(三值) |
|
|
108
|
+
| | `gotry_skeleton_check` | OpenFlights 168 对枢纽通航性(三值) |
|
|
109
|
+
| **库存与目录** | `gotry_hotel_search` | hotel-byte 实时桥,降级静态包(证据标注) |
|
|
110
|
+
| | `gotry_anything_search` | 城市/酒店/地标混合目录(hotel-be Anything) |
|
|
111
|
+
| **判定引擎** | `gotry_feasibility_check` | 门到门真成本可行性(Z3),逐候选判定 |
|
|
112
|
+
| **记忆与触达** | `gotry_motivation_save` | 动机画像落盘(evidence 强制,反幻觉) |
|
|
113
|
+
| | `gotry_wish_pool_add` / `gotry_wish_pool_list` | 「下一次出发」愿望池 + 0..1 条件召回 |
|
|
114
|
+
| | `gotry_companion_save` · `gotry_trip_log` | 同行人档案 / 旅行时间线 |
|
|
115
|
+
| **通用外部** | `gotry_web_search` · `gotry_video_subtitle` · `gotry_github_search` · `gotry_agent_reach` | 网页/字幕/GitHub/全渠道外部信息(经 Agent-Reach) |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🔐 账号会话:授权与隐私 — Account consent
|
|
120
|
+
|
|
121
|
+
会话检索使用**你本人已登录的 Chrome**读取酒店/机票实时数据,为此立了四条 hard 规则:
|
|
122
|
+
|
|
123
|
+
1. **登录在外部网站完成** —— gotry 从不提供、不代填、不收集任何密码/验证码/cookie 值。它只回答一个布尔问题:"登录票据 cookie 存在吗"(只读**名字**,0 值过手)。
|
|
124
|
+
2. **授权卡,每会话一次** —— 首次动用账号会话会弹运行时审批卡;批准后会话内记住,拒绝即本会话吊销,不再打扰。总闸 `sessionAccess: ask|allow|off` 随时可关。
|
|
125
|
+
3. **物理只读** —— ReadGuard 在网络层中止一切写请求(下单/支付在传输层不可达),agent 永不接触凭证与验证码;遇到验证码立即停,交还给你。
|
|
126
|
+
4. **绝不劫持你的浏览器** —— 检索/登录只开自己的独立标签页,登录页置前台、留在你那;例行动测试永不自动开浏览器窗。
|
|
127
|
+
|
|
128
|
+
> 前置(一次性):日常 Chrome 打开 `chrome://inspect/#remote-debugging` 打开开关(Chrome 144+),并保持 Chrome 在线。未开启时工具返回 `needs-attach` 并给出指引,不消耗执行配额。
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🏛️ 架构 — Architecture
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
136
|
+
│ L1 对话即界面 chat-as-UI; gates 是消息内选择题 │
|
|
137
|
+
│ L2 编排 dsh 运行时 + GoTry 插件(ReAct);18 个工具 │
|
|
138
|
+
│ L3 领域 统一行程模型 + Z3 可行性引擎(枚举/Z3 双形态) │
|
|
139
|
+
│ L4 数据 静态数据包 + hotelbyte-cli 实时桥 + OpenFlights 骨架 │
|
|
140
|
+
│ L5 治理 LoopX(objective / gates / evidence / quota) │
|
|
141
|
+
└──────────────────────────────────────────────────────────────┘
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| 层 | 模块 | 角色 |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| L2 | `ts/src/index.ts`(dsh 插件) | 注册 18 工具,挂时间锚点/记忆 brief 变量;execute 异常隔离 + 授权闸 + 进程护栏 |
|
|
147
|
+
| L3 | `ts/src/unified.ts` · `py/gotry_feasibility/` | 唯一求解入口(候选枚举 + 航班链 Z3) |
|
|
148
|
+
| L4 | `ts/capabilities/hbcli.ts` · `skeleton-check.ts` | 实时库存桥 + OpenFlights 骨架(三值语义) |
|
|
149
|
+
| L5 | loopx 治理面 | objective / gates / evidence / quota |
|
|
150
|
+
|
|
151
|
+
> 📖 完整 ADR / 演进 / 债务清单: [`docs/architecture.md`](docs/architecture.md)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## ⚠️ 状态与限制 — Status & limitations
|
|
156
|
+
|
|
157
|
+
**已就绪(全绿可验)**:
|
|
158
|
+
|
|
159
|
+
- ✅ **Z3 WASM race 已根治**(`z3-shared.ts` 单实例+会话级互斥;run-all §30 并发回归闸)
|
|
160
|
+
- ✅ **实时机票/酒店/会话检索**已接入:FlyAI 官方通道(机/火/酒;`GOTRY_REALTIME_PRICING=1` 可选把实时票价覆写进求解)
|
|
161
|
+
- ✅ **英文面(工程层)**:`GOTRY_LOCALE=en` 一键切换,en 零缺键,金标准行为不变
|
|
162
|
+
- ✅ **账号会话:授权闸 + 登录产品化 + 自动检测**(见上方 [🔐 账号会话](#-账号会话授权与隐私--account-consent))
|
|
163
|
+
- ✅ License MIT;薄壳遗留已删除(dsh web 唯一产品面)
|
|
164
|
+
|
|
165
|
+
**未收口(诚实清单)**:
|
|
166
|
+
|
|
167
|
+
- ⏳ **M3 Exit 未关闭** —— 工程与分发面就绪,但真实种子用户 evidence(50–200 人 cohort)未积累;合成 fixture 只证明合同不证明 business pass
|
|
168
|
+
- ⏳ **中文之外的界面残余** —— dsh web 界面属宿主;工具结果卡与人格对话面的英文校准件挂 M4 校准样本
|
|
169
|
+
- ⏳ **携程酒店/美团会话适配器** —— 机票已通,酒店会话面等登录态实测回填(下一 tick)
|
|
170
|
+
|
|
171
|
+
<details>
|
|
172
|
+
<summary>📖 展开完整状态表(账本/证据合同/里程碑口径)</summary>
|
|
173
|
+
|
|
174
|
+
事务化状态账本(ADR-15,`gotry_async_terminal.v1`:4/4→`succeeded`/ledger `settled`/exit 0,非 4/4→`failed`/`failed`/exit 2,终态复诵零重算);双形态架构冻结(ADR-16:本地+Web 一套账本语义,tenant_id 一等字段);会话数据面 #21 的字段 fixture scorer、双源合同与 waiting-attach no-spend 已进确定性回归,真实 sf-01..08 尚未验收;里程碑口径(2026-08-29):M3 工程与分发面已就绪但真实种子用户 evidence 未收口,M4 由 founder 授权并行推进不构成 M3 Exit 证明,M5/M6 仅在各自 Entry gate 满足后启动——细则见 [`docs/roadmap.md`](docs/roadmap.md) 与 [`docs/architecture.md`](docs/architecture.md)。
|
|
175
|
+
|
|
176
|
+
</details>
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 📚 文档 — Docs
|
|
181
|
+
|
|
182
|
+
| 文档 | 适合谁 |
|
|
183
|
+
|---|---|
|
|
184
|
+
| [本 README](README.md) | **所有人**:是什么 · 怎么用 · 限制 |
|
|
185
|
+
| [`docs/architecture.md`](docs/architecture.md) | 工程师:分层 · ADR · 演进 · 债务 |
|
|
186
|
+
| [`docs/roadmap.md`](docs/roadmap.md) | 项目管理:M0–M6 与当前位置 |
|
|
187
|
+
| [`docs/user-guide.md`](docs/user-guide.md) | 终端用户:详细使用文档 |
|
|
188
|
+
| [`docs/gotry-product-design.md`](docs/gotry-product-design.md) | 产品:主循环 · 透明机制 · 全成本模型 |
|
|
189
|
+
| [`docs/gotry-master-outline.md`](docs/gotry-master-outline.md) | 决策者:工作分解 · 复用矩阵 |
|
|
190
|
+
| [`docs/kimi-postmortem.md`](docs/kimi-postmortem.md) | 所有人:真实 AI 旅行规划失败复盘(反面教材) |
|
|
191
|
+
| [`docs/release-notes.md`](docs/release-notes.md) | 历史:每个 tag 的发布闸勾稽 |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 🧪 跑测试 — Verify
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
./scripts/run-all-tests.sh
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
全栈一次性绿(纯 TS,无 Python 依赖):engine/journey/unified 金标准 · 对话重放 · 异步工单跨进程 · 插件 smoke · hbcli · 进程护栏(含工具异常隔离)· 天气 · 航班 · Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 · 时间感评测(锚点卡/槽位过期校验/评分器/mock 回放;真模型巡检 `time-eval-tests.ts --real`)· 记忆域(动机合并守门/效用 sidecar/只读指标投影)· **Z3 并发竞态(§30)· 实时票价桥(§31)· i18n 目录(§32)· M3 cohort 证据合同(§33)· M4 价值证据合同(§34)**。
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 🤝 参与开发 — Contributing
|
|
206
|
+
|
|
207
|
+
> *PR-based flow: branch off the latest `main`, full suite green, open a Pull Request — `main` never takes direct pushes; merge after review. Full guide: [CONTRIBUTING.md](CONTRIBUTING.md).*
|
|
208
|
+
|
|
209
|
+
标准开源流程:**`main` 不直接推**——从最新 `main` 切出 `feat/ · fix/ · docs/ · chore/` 分支,本地全栈绿后开 Pull Request,CI(Node 22/24,typecheck + 全部套件)与维护者 review 双绿后 squash 合入。**测试红着不许合。**
|
|
210
|
+
|
|
211
|
+
- 完整贡献指南(环境搭建 · 测试 · 分支与提交约定 · PR 流程): **[CONTRIBUTING.md](CONTRIBUTING.md)**
|
|
212
|
+
- Bug / 功能建议:[issue 模板](.github/ISSUE_TEMPLATE/bug_report.yml)(搜过既有 issue 再提)
|
|
213
|
+
- 行为或架构改动先立 ADR(`docs/architecture.md` §8);多 agent 协作契约见 [`AGENTS.md`](AGENTS.md)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 📜 License
|
|
218
|
+
|
|
219
|
+
**MIT**(2026-08-23 落定)。与上游 dsh(MIT)/ loopx 一致——宽松、可商用、可闭源分叉。文本见 [LICENSE](LICENSE)。
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🌐 中英版 — Locales
|
|
224
|
+
|
|
225
|
+
- 本文件(main)**英文版独立成文**:[README.md](README.md)——两文件各自完整,常见开源双语布局。
|
|
226
|
+
- 深 README(中文)面向中国出境首发种子用户群;英文版是它的完整镜像(不含深度工程文档)。
|
|
227
|
+
- `docs/` 深度工程文档当前中文先行,英文版计划 v0.1.0 同步。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
**Built with**: DeepSeek Harness 0.1.2-alpha.1 (vendored) · Cordis · Z3 (WASM) · loopx (pipx) · hotelbyte-cli · Agent-Reach v1.5.0 (`.venv/`) · OpenFlights · TypeScript
|
|
232
|
+
|
|
233
|
+
**Last verified against `v0.0.1-rc.14`(2026-08-29)** — 全栈回归全绿 §1-§34(发布流程见 `scripts/publish-npm.sh`)。
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gotry 外部依赖自举(founder 2026-08-29 指令:安装 gotry 时按上游本身的方式装好外部依赖):
|
|
4
|
+
* hbcli(hotelbyte-cli) → 官方 install.sh(原生二进制,~/.local/bin/hbcli)
|
|
5
|
+
* agent-reach → 官方 pip 安装 git+ upstream(包内 .venv,与 z3-solver 同址原则)
|
|
6
|
+
* flyai → 无需安装(npx 每次自拉 @fly-ai/flyai-cli)
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* node bin/gotry-bootstrap.js # 显式安装(缺啥装啥;失败 exit 1)
|
|
10
|
+
* node bin/gotry-bootstrap.js --auto # postinstall 模式:CI/跳过开关检测,任何失败不挡安装(exit 0)
|
|
11
|
+
* node bin/gotry-bootstrap.js --check-only # 只探测报告,不安装(测试钩子)
|
|
12
|
+
*
|
|
13
|
+
* 环境开关:
|
|
14
|
+
* GOTRY_SETUP_SKIP=1 全部跳过
|
|
15
|
+
* GOTRY_SETUP_HBCLI=0 跳过 hbcli
|
|
16
|
+
* GOTRY_SETUP_REACH=0 跳过 agent-reach
|
|
17
|
+
*
|
|
18
|
+
* 契约:安装外部依赖永远不挡 gotry 本体——能力层各有降级路径(静态包/not-installed
|
|
19
|
+
* verdict),自举失败只降级体验,不产生故障。凭证(hbcli auth / agent-reach 渠道
|
|
20
|
+
* cookie)属用户资产,不自动配置,装完二进制后给指引。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawn } from 'node:child_process'
|
|
24
|
+
import { existsSync } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { join, dirname } from 'node:path'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
|
|
29
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
30
|
+
const AUTO = process.argv.includes('--auto')
|
|
31
|
+
const CHECK_ONLY = process.argv.includes('--check-only')
|
|
32
|
+
|
|
33
|
+
const HBCLI_INSTALL_CMD = 'curl -fsSL https://github.com/hotelbyte-com/docs/releases/latest/download/install.sh | bash'
|
|
34
|
+
const REACH_INSTALL_URL = 'git+https://github.com/Panniantong/Agent-Reach.git'
|
|
35
|
+
|
|
36
|
+
/** 带超时的子进程(inherit stdio 让用户看见上游安装进度) */
|
|
37
|
+
function run(cmd, args, { timeoutMs, cwd } = {}) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = spawn(cmd, args, { stdio: 'inherit', cwd, env: process.env })
|
|
40
|
+
let done = false
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } }
|
|
43
|
+
}, timeoutMs)
|
|
44
|
+
child.on('error', (e) => { if (!done) { done = true; clearTimeout(timer); resolve({ ok: false, error: e.message }) } })
|
|
45
|
+
child.on('exit', (code) => { if (!done) { done = true; clearTimeout(timer); resolve({ ok: code === 0, error: code === 0 ? undefined : `exit ${code}` }) } })
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 静默探测命令是否可执行(带超时) */
|
|
50
|
+
function probe(cmd, args, timeoutMs = 10_000) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const child = spawn(cmd, args, { stdio: 'ignore' })
|
|
53
|
+
let done = false
|
|
54
|
+
const timer = setTimeout(() => { if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } resolve(false) } }, timeoutMs)
|
|
55
|
+
child.on('error', () => { if (!done) { done = true; clearTimeout(timer); resolve(false) } })
|
|
56
|
+
child.on('exit', (code) => { if (!done) { done = true; clearTimeout(timer); resolve(code === 0) } })
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const say = (s) => console.log(s)
|
|
61
|
+
|
|
62
|
+
async function setupHbcli() {
|
|
63
|
+
say('[gotry-setup] hbcli(hotelbyte-cli,可选酒店实时源)')
|
|
64
|
+
const candidates = ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]
|
|
65
|
+
const present = candidates.some((p) => existsSync(p)) && (await probe(candidates[0], ['version']) || await probe(candidates[1], ['version']) || await probe(candidates[2], ['version']))
|
|
66
|
+
if (present) { say(' ✓ 已安装'); return { ok: true } }
|
|
67
|
+
if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
|
|
68
|
+
say(` 安装中(官方脚本): ${HBCLI_INSTALL_CMD}`)
|
|
69
|
+
const r = await run('bash', ['-c', HBCLI_INSTALL_CMD], { timeoutMs: 120_000 })
|
|
70
|
+
if (!r.ok) { say(` ✗ 安装失败(${r.error})——不影响 gotry,酒店检索将用内置静态包;可稍后重试: npx gotry setup`); return { ok: false } }
|
|
71
|
+
const binDir = join(homedir(), '.local/bin')
|
|
72
|
+
if (!process.env.PATH.split(':').includes(binDir)) {
|
|
73
|
+
say(` ⚠ ${binDir} 不在当前 PATH —— gotry 工具已内建候选路径回退,无需手动处理;其他程序可用: export PATH="${binDir}:$PATH"`)
|
|
74
|
+
}
|
|
75
|
+
say(' ✓ 安装完成(凭证选配: hbcli auth set-credentials --app-key ... --app-secret ...,或 HOTELBYTE_TOKEN;未配时酒店检索自动用静态包)')
|
|
76
|
+
return { ok: true }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function setupReach() {
|
|
80
|
+
say('[gotry-setup] agent-reach(可选网页/社媒读取源,pip 装入包内 .venv)')
|
|
81
|
+
const venvBin = join(repoRoot, '.venv/bin/agent-reach')
|
|
82
|
+
if (existsSync(venvBin)) { say(' ✓ 已安装(.venv)'); return { ok: true } }
|
|
83
|
+
if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
|
|
84
|
+
const hasPy = await probe('python3', ['--version'], 10_000)
|
|
85
|
+
if (!hasPy) { say(' ✗ 跳过:未找到 python3(agent-reach 需 Python 3;装好后重跑 npx gotry setup)'); return { ok: false } }
|
|
86
|
+
say(` 创建 .venv 并安装上游(${REACH_INSTALL_URL})`)
|
|
87
|
+
const venv = await run('python3', ['-m', 'venv', join(repoRoot, '.venv')], { timeoutMs: 120_000 })
|
|
88
|
+
if (!venv.ok) { say(` ✗ venv 创建失败(${venv.error})——不影响 gotry,gotry_agent_reach 将返回 not-installed 指引`); return { ok: false } }
|
|
89
|
+
const pip = join(repoRoot, '.venv/bin/pip')
|
|
90
|
+
const inst = await run(pip, ['install', '-q', REACH_INSTALL_URL], { timeoutMs: 300_000 })
|
|
91
|
+
if (!inst.ok) { say(` ✗ pip 安装失败(${inst.error})——可稍后重试: npx gotry setup`); return { ok: false } }
|
|
92
|
+
say(' ✓ 安装完成(渠道凭证选配见 docs/tokens.md: .venv/bin/agent-reach configure --from-browser chrome --platform <渠道>)')
|
|
93
|
+
return { ok: true }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
if (process.platform === 'win32') {
|
|
98
|
+
say('[gotry-setup] Windows 暂不支持自动安装(hbcli 上游仅 darwin/linux)。手动指引:')
|
|
99
|
+
say(` hbcli: ${HBCLI_INSTALL_CMD}(WSL);agent-reach: python -m venv .venv && .venv/Scripts/pip install ${REACH_INSTALL_URL}`)
|
|
100
|
+
process.exit(AUTO ? 0 : 1)
|
|
101
|
+
}
|
|
102
|
+
if (AUTO && (process.env.CI || process.env.GOTRY_SETUP_SKIP === '1')) {
|
|
103
|
+
say('[gotry-setup] CI/GOTRY_SETUP_SKIP 检测——跳过外部依赖自举(可随时手动: npx gotry setup)')
|
|
104
|
+
say('GoTry installed. Run: npx gotry web (dsh Web UI on :3080)')
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
if (!AUTO && process.env.GOTRY_SETUP_SKIP === '1') { say('[gotry-setup] GOTRY_SETUP_SKIP=1——跳过'); process.exit(0) }
|
|
108
|
+
const results = []
|
|
109
|
+
if (process.env.GOTRY_SETUP_HBCLI !== '0') results.push(await setupHbcli())
|
|
110
|
+
else say('[gotry-setup] hbcli:GOTRY_SETUP_HBCLI=0 跳过')
|
|
111
|
+
if (process.env.GOTRY_SETUP_REACH !== '0') results.push(await setupReach())
|
|
112
|
+
else say('[gotry-setup] agent-reach:GOTRY_SETUP_REACH=0 跳过')
|
|
113
|
+
say('[gotry-setup] flyai:无需安装(npx 每次自拉 @fly-ai/flyai-cli,免 key)')
|
|
114
|
+
const failed = results.filter((r) => !r.ok).length
|
|
115
|
+
if (failed > 0) {
|
|
116
|
+
say(`[gotry-setup] ${failed} 项未就绪——gotry 本体不受影响(各能力均有降级路径);可稍后重跑: npx gotry setup`)
|
|
117
|
+
process.exit(AUTO ? 0 : 1)
|
|
118
|
+
}
|
|
119
|
+
say('[gotry-setup] 全部就绪。Run: npx gotry web')
|
|
120
|
+
process.exit(0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((e) => {
|
|
124
|
+
say(`[gotry-setup] 异常:${e.message}(不影响 gotry 本体;可重试 npx gotry setup)`)
|
|
125
|
+
process.exit(AUTO ? 0 : 1)
|
|
126
|
+
})
|
package/bin/gotry-inner.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 占位(本机绝对路径),随 tarball 分发后对其他机器必错。
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { spawn } from 'node:child_process'
|
|
19
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { fileURLToPath } from 'node:url'
|
|
22
22
|
import { dirname, join } from 'node:path'
|
|
@@ -56,6 +56,7 @@ if (help) {
|
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
58
|
gotry web # dsh Web UI on http://127.0.0.1:3080
|
|
59
|
+
gotry setup # 安装可选外部依赖(hbcli 官方脚本 / agent-reach pip;装时已自动跑过)
|
|
59
60
|
gotry "一段完整任务..." # headless 一问一答
|
|
60
61
|
gotry help # this help
|
|
61
62
|
|
|
@@ -68,12 +69,18 @@ Detail: https://github.com/Danceiny/gotry — README
|
|
|
68
69
|
process.exit(0)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
// mode 决定路径: 'web'/'
|
|
72
|
-
const literal = new Set(['web', 'help', '-h', '--help'])
|
|
72
|
+
// mode 决定路径: 'web'/'setup'/'help' 是字面命令;否则第一段 args[0] 是任务本身的一部分
|
|
73
|
+
const literal = new Set(['web', 'setup', 'help', '-h', '--help'])
|
|
73
74
|
const isLiteral = literal.has(args[0])
|
|
74
75
|
const mode = isLiteral ? args[0] : 'headless'
|
|
75
76
|
const rest = isLiteral ? args.slice(1) : args
|
|
76
77
|
|
|
78
|
+
// setup:外部依赖自举(hbcli/agent-reach),不需要 dsh runtime 与 LLM key,同步分发后即退
|
|
79
|
+
if (mode === 'setup') {
|
|
80
|
+
const r = spawnSync(process.execPath, [join(here, 'gotry-bootstrap.js'), ...rest], { stdio: 'inherit' })
|
|
81
|
+
process.exit(r.status ?? (r.error ? 1 : 0))
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// --- dsh runtime 定位:repo checkout(vendored)优先,npm 安装走依赖解析 ---
|
|
78
85
|
const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
|
|
79
86
|
let dshBin = ''
|
package/cordis.gotry-patch.yml
CHANGED
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
登录态,现携程机票):**首次调用**经运行时审批卡向用户请求授权,本会话
|
|
85
85
|
内记住、不再重复弹卡;用户拒绝即本会话吊销——不再请求也不再执行,改走
|
|
86
86
|
其他工具推进;无审批通道(headless)不执行。返回 needs-login→调
|
|
87
|
-
gotry_session_login 产品工具(
|
|
88
|
-
|
|
87
|
+
gotry_session_login 产品工具(先自动检测票据——已登录则直接确认零弹窗;未登录才
|
|
88
|
+
打开登录入口等待其完成,不需要任何终端命令;**登录永远发生在携程官网、不在 gotry
|
|
89
89
|
里进行**——gotry 永不收集/存储/传输密码、验证码或任何 cookie 值,只检查
|
|
90
90
|
"是否已登录"这一个事实;向用户说明这一点),完成前用官方源推进,不反复
|
|
91
91
|
开页探测;返回 challenged→立即停手如实告知,绝不重试
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function hbcliBinCandidates(bin, homeDir = homedir()) {
|
|
5
|
+
if (bin !== 'hbcli') return [
|
|
6
|
+
bin
|
|
7
|
+
];
|
|
8
|
+
return [
|
|
9
|
+
bin,
|
|
10
|
+
join(homeDir, '.local/bin/hbcli'),
|
|
11
|
+
join(homeDir, '.staicli/current/hbcli')
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function attemptHbcli(bin, args, opts) {
|
|
3
15
|
const started = Date.now();
|
|
4
|
-
const bin = opts.hbcliBin ?? 'hbcli';
|
|
5
|
-
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
6
|
-
const env = opts.env ?? 'uat';
|
|
7
|
-
const envVars = {
|
|
8
|
-
HOTELBYTE_ENV: env
|
|
9
|
-
};
|
|
10
|
-
if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
11
16
|
return new Promise((resolve)=>{
|
|
12
17
|
let stdout = '';
|
|
13
18
|
let stderr = '';
|
|
@@ -15,7 +20,7 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
15
20
|
const child = spawn(bin, args, {
|
|
16
21
|
env: {
|
|
17
22
|
...process.env,
|
|
18
|
-
...envVars
|
|
23
|
+
...opts.envVars
|
|
19
24
|
}
|
|
20
25
|
});
|
|
21
26
|
const timer = setTimeout(()=>{
|
|
@@ -28,10 +33,10 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
28
33
|
result: null,
|
|
29
34
|
evidence: `[实时API:hbcli@timeout@${new Date().toISOString()}]`,
|
|
30
35
|
latencyMs: Date.now() - started,
|
|
31
|
-
error: `timeout after ${timeoutMs}ms`
|
|
36
|
+
error: `timeout after ${opts.timeoutMs}ms`
|
|
32
37
|
});
|
|
33
38
|
}
|
|
34
|
-
}, timeoutMs);
|
|
39
|
+
}, opts.timeoutMs);
|
|
35
40
|
child.stdout.on('data', (d)=>{
|
|
36
41
|
stdout += d.toString();
|
|
37
42
|
});
|
|
@@ -82,11 +87,31 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
82
87
|
result: null,
|
|
83
88
|
evidence: `[实时API:hbcli@spawn_error@${new Date().toISOString()}]`,
|
|
84
89
|
latencyMs: Date.now() - started,
|
|
85
|
-
error: e.message
|
|
90
|
+
error: e.message,
|
|
91
|
+
spawnError: true
|
|
86
92
|
});
|
|
87
93
|
});
|
|
88
94
|
});
|
|
89
95
|
}
|
|
96
|
+
export async function callHbcliJson(args, opts = {}) {
|
|
97
|
+
const env = opts.env ?? 'uat';
|
|
98
|
+
const envVars = {
|
|
99
|
+
HOTELBYTE_ENV: env
|
|
100
|
+
};
|
|
101
|
+
if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
102
|
+
const callOpts = {
|
|
103
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
104
|
+
env,
|
|
105
|
+
envVars
|
|
106
|
+
};
|
|
107
|
+
const candidates = hbcliBinCandidates(opts.hbcliBin ?? 'hbcli');
|
|
108
|
+
let last;
|
|
109
|
+
for (const bin of candidates){
|
|
110
|
+
last = await attemptHbcli(bin, args, callOpts);
|
|
111
|
+
if (!(last.spawnError && candidates.indexOf(bin) < candidates.length - 1)) return last;
|
|
112
|
+
}
|
|
113
|
+
return last;
|
|
114
|
+
}
|
|
90
115
|
export async function searchHotels(query, opts = {}) {
|
|
91
116
|
const hbArgs = [
|
|
92
117
|
'search',
|
|
@@ -110,6 +135,8 @@ export async function searchHotels(query, opts = {}) {
|
|
|
110
135
|
summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}`
|
|
111
136
|
};
|
|
112
137
|
}
|
|
138
|
+
const rawReason = live.error ?? live.via;
|
|
139
|
+
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason;
|
|
113
140
|
const fallback = opts.fallbackPath;
|
|
114
141
|
if (fallback) {
|
|
115
142
|
try {
|
|
@@ -124,19 +151,19 @@ export async function searchHotels(query, opts = {}) {
|
|
|
124
151
|
hotels: {
|
|
125
152
|
stays: matched
|
|
126
153
|
},
|
|
127
|
-
summary: `${query.destination}:hbcli
|
|
154
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块`
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
return {
|
|
131
158
|
...live,
|
|
132
159
|
hotels: null,
|
|
133
|
-
summary: `${query.destination}:hbcli
|
|
160
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`
|
|
134
161
|
};
|
|
135
162
|
} catch {}
|
|
136
163
|
}
|
|
137
164
|
return {
|
|
138
165
|
...live,
|
|
139
|
-
summary: `${query.destination}:hbcli
|
|
166
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)`
|
|
140
167
|
};
|
|
141
168
|
}
|
|
142
169
|
export async function listDestinations(opts = {}) {
|
|
@@ -87,7 +87,14 @@ export async function openSession(opts = {}) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
let ownPage = false;
|
|
91
|
+
let page;
|
|
92
|
+
if (opts.newPage) {
|
|
93
|
+
page = await browser.newPage();
|
|
94
|
+
ownPage = true;
|
|
95
|
+
} else {
|
|
96
|
+
page = (await browser.pages())[0] ?? await browser.newPage();
|
|
97
|
+
}
|
|
91
98
|
return {
|
|
92
99
|
ok: true,
|
|
93
100
|
browser,
|
|
@@ -97,6 +104,9 @@ export async function openSession(opts = {}) {
|
|
|
97
104
|
requestCount: ()=>0
|
|
98
105
|
},
|
|
99
106
|
close: async ()=>{
|
|
107
|
+
if (ownPage && (opts.closeOwnPage ?? true)) {
|
|
108
|
+
await page.close().catch(()=>{});
|
|
109
|
+
}
|
|
100
110
|
await (isCdp ? browser.disconnect() : browser.close()).catch(()=>{});
|
|
101
111
|
}
|
|
102
112
|
};
|
|
@@ -57,12 +57,28 @@ export async function sessionLogin(q = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
const evidenceTag = `[会话:${site}-login@${ts}]`;
|
|
59
59
|
try {
|
|
60
|
+
const pre = await pollTicketNames(t.browser, target);
|
|
61
|
+
if (pre.length > 0) {
|
|
62
|
+
await t.close();
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
via: 'session-login',
|
|
66
|
+
latencyMs: Date.now() - started,
|
|
67
|
+
verdict: 'logged-in',
|
|
68
|
+
site,
|
|
69
|
+
tickets: pre,
|
|
70
|
+
evidence: `${evidenceTag} 自动检测:票据 cookie 已在(先前登录已生效)——[${pre.join(', ')}](只读名字,0 网页交互)`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const page = await t.browser.newPage();
|
|
74
|
+
await page.bringToFront().catch(()=>{});
|
|
60
75
|
try {
|
|
61
|
-
await
|
|
76
|
+
await page.goto(target.entryUrl, {
|
|
62
77
|
waitUntil: 'domcontentloaded',
|
|
63
78
|
timeout: 30_000
|
|
64
79
|
});
|
|
65
80
|
} catch {}
|
|
81
|
+
console.log(`[session-login] ${target.label} 登录入口已在新标签页置前打开`);
|
|
66
82
|
const waitMs = Math.min(Math.max(q.waitMs ?? 90_000, 0), 300_000);
|
|
67
83
|
const pollMs = Math.min(Math.max(q.pollMs ?? 3_000, 500), 10_000);
|
|
68
84
|
const deadline = Date.now() + waitMs;
|
|
@@ -34,7 +34,8 @@ export async function sessionFlightSearch(q) {
|
|
|
34
34
|
profileDir: q.profileDir,
|
|
35
35
|
headless: q.headless,
|
|
36
36
|
auditPath: q.auditPath,
|
|
37
|
-
mode: q.profileDir ? 'persistent' : 'cdp'
|
|
37
|
+
mode: q.profileDir ? 'persistent' : 'cdp',
|
|
38
|
+
newPage: true
|
|
38
39
|
});
|
|
39
40
|
if (!t.ok) {
|
|
40
41
|
return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const repoRoot = join(import.meta.dirname, '..', '..');
|
|
5
|
+
const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
|
|
6
|
+
function runBootstrap(extraArgs, extraEnv) {
|
|
7
|
+
try {
|
|
8
|
+
const out = execFileSync('node', [
|
|
9
|
+
bootstrap,
|
|
10
|
+
...extraArgs
|
|
11
|
+
], {
|
|
12
|
+
encoding: 'utf-8',
|
|
13
|
+
timeout: 60_000,
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
...extraEnv
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
code: 0,
|
|
21
|
+
out
|
|
22
|
+
};
|
|
23
|
+
} catch (e) {
|
|
24
|
+
const err = e;
|
|
25
|
+
return {
|
|
26
|
+
code: err.status ?? 1,
|
|
27
|
+
out: err.stdout ?? ''
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const c1 = runBootstrap([
|
|
32
|
+
'--check-only'
|
|
33
|
+
], {});
|
|
34
|
+
assert.equal(c1.code, 0, `--check-only 应 exit 0,实际 ${c1.code}\n${c1.out}`);
|
|
35
|
+
assert.ok(c1.out.includes('hbcli'), '报告应含 hbcli 节');
|
|
36
|
+
assert.ok(c1.out.includes('agent-reach'), '报告应含 agent-reach 节');
|
|
37
|
+
assert.ok(c1.out.includes('flyai'), '报告应含 flyai(无需安装)节');
|
|
38
|
+
console.log('1. --check-only 探测报告 exit 0(hbcli/agent-reach/flyai 三节齐)OK');
|
|
39
|
+
const c2 = runBootstrap([
|
|
40
|
+
'--auto'
|
|
41
|
+
], {
|
|
42
|
+
GOTRY_SETUP_SKIP: '1'
|
|
43
|
+
});
|
|
44
|
+
assert.equal(c2.code, 0, '--auto 跳过态应 exit 0(永不挡 npm install)');
|
|
45
|
+
assert.ok(c2.out.includes('跳过'), '应输出跳过说明');
|
|
46
|
+
console.log('2. --auto + GOTRY_SETUP_SKIP=1 → exit 0(安装永不失败)OK');
|
|
47
|
+
const c3 = runBootstrap([], {
|
|
48
|
+
GOTRY_SETUP_SKIP: '1'
|
|
49
|
+
});
|
|
50
|
+
assert.equal(c3.code, 0, '显式模式跳过态应 exit 0');
|
|
51
|
+
assert.ok(c3.out.includes('跳过'), '应输出跳过说明');
|
|
52
|
+
console.log('3. 显式模式 + GOTRY_SETUP_SKIP=1 → exit 0 OK');
|
|
53
|
+
console.log('BOOTSTRAP TESTS: 3/3 OK(探测报告/跳过开关/postinstall 非致命)');
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/bootstrap-tests.ts
|