@simonyea/holysheep-cli 2.0.6 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,59 +107,71 @@ If you forget the port, check `~/.openclaw/openclaw.json` (`gateway.port`) or ru
107
107
  | Anthropic SDK / Claude Code | `https://api.holysheep.ai` (no `/v1`) |
108
108
  | OpenAI-compatible / Codex / Aider | `https://api.holysheep.ai/v1` (with `/v1`) |
109
109
 
110
- ### AionUi mode (experimental, opt-in)
110
+ ### `hs web` — real AionUi v1.9.18, HolySheep-powered (2.1.0+)
111
111
 
112
- `hs web` ships a lightweight legacy WebUI by default Windows / macOS / Linux, zero extra dependencies.
113
-
114
- For a richer UI, `hs web --aionui` boots the **AionUi runtime** behind a zero-dep Node proxy that turns your HolySheep API key into a one-click auto-login. No username, no password — your HS key **is** your credential.
112
+ Starting from **2.1.0**, `hs web` is no longer a legacy workspace it boots the **real [AionUi](https://github.com/iOfficeAI/AionUi) v1.9.18 source fork**, with one source-level change: the login page accepts a **HolySheep API Key** instead of username/password. The full AionUi UI, conversations, team, tools, MCP, everything — unchanged upstream experience, HolySheep-native auth.
115
113
 
116
114
  ```bash
117
- # Regular install (legacy WebUI, always works, ~90 kB)
115
+ # Install CLI (still ~100 kB the 21 MB runtime is fetched on first run)
118
116
  npm install -g @simonyea/holysheep-cli
119
117
 
120
- # Optional enable AionUi mode. Requires the AionUi runtime locally.
121
- hs web --aionui
118
+ # First run: downloads the prebuilt AionUi runtime into ~/.holysheep/aionui-runtime/
119
+ hs web --setup-runtime
120
+
121
+ # Subsequent runs: reuses the cached runtime
122
+ hs web
122
123
  ```
123
124
 
124
- **How it works**
125
+ **What happens on `hs web`:**
126
+
127
+ 1. Resolves the AionUi runtime from `~/.holysheep/aionui-runtime/` (or the dev `aionui-fork/` checkout when running from source).
128
+ 2. Spawns the patched `dist-server/server.mjs` via `bun` on port 9876.
129
+ 3. If `~/.holysheep/config.json` has a key, opens `http://127.0.0.1:9876/login#apiKey=cr_...` — the login page reads the hash, auto-submits, and you land in the AionUi dashboard.
130
+ 4. Otherwise opens `/login` and you paste the key manually.
125
131
 
126
- 1. Wrapper on port `9876` (public) proxies to an AionUi server on `127.0.0.1:9877` (loopback-only).
127
- 2. Your saved HolySheep API key is POSTed to AionUi's `/login` route, which returns a JWT cookie.
128
- 3. A single-use bootstrap token (≤30 s TTL, loopback-only) copies the cookie into your browser → you land on `/guid` authenticated.
129
- 4. `/api/holysheep/tools`, `/api/holysheep/balance`, `/api/holysheep/whoami` etc. are served directly by the wrapper (reuses the legacy handlers in-process, no cross-port coupling).
132
+ **What got patched (source-level, reviewable):**
133
+
134
+ | File (in `aionui-fork/`) | Change |
135
+ |----|----|
136
+ | `src/process/webserver/auth/service/HolySheepAuthService.ts` | New service — validates HS API Key via `GET {HOLYSHEEP_API_BASE}/v1/models`, 5-min TTL cache |
137
+ | `src/process/webserver/routes/authRoutes.ts` | `/login` gets a HolySheep branch: `body.apiKey` → HolySheep validate → sign standard AionUi JWT |
138
+ | `src/process/webserver/auth/middleware/AuthMiddleware.ts` | `validateLoginInput` skips username/password checks when `apiKey` is present |
139
+ | `src/renderer/hooks/context/AuthContext.tsx` | `login()` now accepts `apiKey` and picks payload accordingly |
140
+ | `src/renderer/pages/login/index.tsx` | Default mode: single API Key input. Toggle link: _"Sign in with local account"_ — restores upstream username/password flow (**nothing removed**) |
130
141
 
131
142
  **Runtime resolution order** (first match wins):
132
143
 
133
144
  1. `~/.holysheep/aionui-runtime/` (installed / cached)
134
145
  2. `<cli>/src/webui/vendor/aionui/` (developer checkout — not shipped to npm)
135
- 3. `HOLYSHEEP_AIONUI_RUNTIME_URL` env var (advanced escape hatch downloads + SHA256-verifies into `~/.holysheep/aionui-runtime/`)
146
+ 3. `<cli>/../aionui-fork/` (git source fork, used when running from the holysheep-cli repo)
147
+ 4. Download from `DEFAULT_RUNTIME_URL` (or `HOLYSHEEP_AIONUI_RUNTIME_URL`) when `--setup-runtime` is passed
136
148
 
137
- **Install the runtime — one-command** (recommended — 21 MB prebuilt, HolySheep-auth-ready):
149
+ **Prebuilt runtime (baked-in default):**
138
150
 
139
- ```bash
140
- export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.17-holysheep.tar.gz
141
- export HOLYSHEEP_AIONUI_RUNTIME_SHA256=d75adcea3c57c85f64b5db96d18c593f20ad150888f47283dc2da0a440fb652c
142
- hs web --aionui --setup-runtime
143
- ```
144
-
145
- The fetcher downloads to `~/.holysheep/aionui-runtime/`, verifies SHA256, extracts `dist-server/` + `out/` + `package.json`, boots AionUi, and opens your browser at the bootstrap redirect (auto-logged-in with your HS API key). Runs macOS / Linux. Windows support depends on `bun` availability.
151
+ - URL: `https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz`
152
+ - SHA256: `379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1`
153
+ - Size: 21 MB (contains `dist-server/` + `out/renderer/` only)
146
154
 
147
- **Alternative — build from AionUi source** (advanced):
155
+ **Rebuild from source** (audit the patches yourself):
148
156
 
149
157
  ```bash
150
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
151
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
152
- mkdir -p ~/.holysheep/aionui-runtime
153
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
154
- hs web --aionui
158
+ git clone https://github.com/iOfficeAI/AionUi.git aionui-fork
159
+ cd aionui-fork && git checkout v1.9.18
160
+ bun install
161
+ bun run build:server && bun run build:renderer:web
162
+ # Point the CLI at your local build:
163
+ cd .. && node src/index.js web
155
164
  ```
156
- Note: upstream AionUi's `/login` still expects `{ username, password }`; the legacy wrapper path handles that via an auto-provisioned bridge admin in `~/.holysheep/aionui-bridge.json` (0600).
157
165
 
158
- **Recovery / advanced**
166
+ **Fallback modes:**
167
+
168
+ - `HOLYSHEEP_WEBUI_LEGACY=1 hs web` — old HolySheep Workspace page (zero-dep node, still ships in npm)
169
+ - `hs web --aionui` — force AionUi mode; do **not** auto-fall-back if runtime is missing (fails loudly instead)
170
+
171
+ **Roadmap:**
159
172
 
160
- - `/login` is still reachable at `http://127.0.0.1:9876/login` for direct apiKey entry.
161
- - To rotate credentials: `rm -rf ~/.holysheep/aionui-runtime && hs web --aionui --setup-runtime` (or edit `~/.holysheep/config.json`).
162
- - Startup logs showing `[AionUi] Could not find builtin src/process/resources/...` are harmless — the prebuilt tarball ships only `dist-server/` + `out/` (not the source tree), so template assistants/skills load from built-in fallbacks instead. The UI works fully; only the "source-tree" assistant templates are unavailable.
173
+ - **2.1.0 (this release)** Real AionUi fork + HolySheep login — done
174
+ - **2.2.0 (next)** — Deep integration: HolySheep multi-tool configuration panel embedded in the AionUi sidebar (not a separate legacy page). Manage Claude Code / Cursor / Codex / Aider / continue from within AionUi.
163
175
 
164
176
  ---
165
177
 
@@ -252,62 +264,71 @@ npx openclaw gateway --port <显示的端口>
252
264
  | Anthropic SDK / Claude Code | `https://api.holysheep.ai`(不带 /v1) |
253
265
  | OpenAI 兼容 / Codex / Aider | `https://api.holysheep.ai/v1`(带 /v1) |
254
266
 
255
- ### AionUi 模式(实验性,opt-in)
267
+ ### `hs web` —— 真 AionUi v1.9.18,HolySheep 驱动(2.1.0+)
256
268
 
257
- `hs web` 默认启动轻量版 WebUI,所有平台(Windows / macOS / Linux)开箱即用,无额外依赖。
258
-
259
- 要体验完整的 AionUi 界面,用 `hs web --aionui`。它会通过一个零依赖的 Node 代理把 AionUi 套在外面,**用 HolySheep API Key 代替账号密码**,一键自动登录。
269
+ **从 2.1.0 开始,`hs web` 不再是之前那个仿风格的 HolySheep Workspace 页面,而是真正的 [AionUi](https://github.com/iOfficeAI/AionUi) v1.9.18 源码 fork。** 唯一的源码级改造:登录页收 **HolySheep API Key**,不再是账号密码。AionUi 原来的所有功能(对话、团队、工具、MCP 等)全部保留,只是登录换了种方式。
260
270
 
261
271
  ```bash
262
- # 普通安装(始终可用的轻量 WebUI)
272
+ # 安装 CLI(npm 包仍然 ~100 kB;21 MB runtime 首次运行再下载)
263
273
  npm install -g @simonyea/holysheep-cli
264
274
 
265
- # 可选:启用 AionUi 模式(需本地安装 AionUi runtime
266
- hs web --aionui
275
+ # 第一次跑:自动下载预编译 AionUi runtime ~/.holysheep/aionui-runtime/
276
+ hs web --setup-runtime
277
+
278
+ # 后续直接跑,复用缓存
279
+ hs web
267
280
  ```
268
281
 
269
- **原理**
282
+ **`hs web` 到底做了什么:**
283
+
284
+ 1. 从 `~/.holysheep/aionui-runtime/`(或开发仓 `aionui-fork/`)找到 runtime。
285
+ 2. 用 `bun` 直接启动打过补丁的 `dist-server/server.mjs`,端口 `9876`。
286
+ 3. 如果 `~/.holysheep/config.json` 里有 key,浏览器会打开 `http://127.0.0.1:9876/login#apiKey=cr_...`,登录页读取 hash 自动提交,用户直接进入 AionUi dashboard。
287
+ 4. 如果没 key,就打开 `/login` 等手动粘贴。
288
+
289
+ **改了哪些源码(可审计):**
270
290
 
271
- 1. 代理监听公开端口 `9876`,将请求转发到 `127.0.0.1:9877` 上的 AionUi 服务(仅回环)。
272
- 2. 自动将保存在 `~/.holysheep/config.json` 的 HS API Key POST 到 AionUi 的 `/login`,拿到 JWT cookie。
273
- 3. 一次性 bootstrap token(≤30 s TTL,仅回环,拒绝 `X-Forwarded-For` / `X-Real-IP`)把 cookie 交给浏览器 用户直接落到 `/guid`,无需再输入任何凭据。
274
- 4. `/api/holysheep/tools`、`/api/holysheep/balance`、`/api/holysheep/whoami` 等由代理自己就地处理(复用 legacy WebUI handler,不跨端口),所以 HolySheep 多终端配置能力在 AionUi 界面里可用。
291
+ | 文件(在 `aionui-fork/`) | 改动 |
292
+ |----|----|
293
+ | `src/process/webserver/auth/service/HolySheepAuthService.ts` | service —— `GET {HOLYSHEEP_API_BASE}/v1/models` 校验 key,5 分钟缓存 |
294
+ | `src/process/webserver/routes/authRoutes.ts` | `/login` 增加 HolySheep 分支:`body.apiKey` 校验 → 签发标准 AionUi JWT |
295
+ | `src/process/webserver/auth/middleware/AuthMiddleware.ts` | `validateLoginInput` 在有 `apiKey` 时跳过账号密码校验 |
296
+ | `src/renderer/hooks/context/AuthContext.tsx` | `login()` 新增 `apiKey` 参数,按情况选 payload |
297
+ | `src/renderer/pages/login/index.tsx` | 默认 HolySheep API Key 单输入框,底部 "Sign in with local account" 链接保留上游账号密码流程(**任何功能都没删**)|
275
298
 
276
299
  **runtime 查找顺序**(命中即用):
277
300
 
278
301
  1. `~/.holysheep/aionui-runtime/`(已安装 / 已缓存)
279
- 2. `<cli>/src/webui/vendor/aionui/`(开发者本地仓库 —— npm 包不携带)
280
- 3. `HOLYSHEEP_AIONUI_RUNTIME_URL` 环境变量(高级逃生口 —— 下载 + SHA256 校验后写入 `~/.holysheep/aionui-runtime/`)
302
+ 2. `<cli>/src/webui/vendor/aionui/`(开发者本地构建 —— npm 包不携带)
303
+ 3. `<cli>/../aionui-fork/`(从 holysheep-cli 仓库直接跑时用 git 源码 fork)
304
+ 4. 走 `--setup-runtime` 时,从 `DEFAULT_RUNTIME_URL`(或 `HOLYSHEEP_AIONUI_RUNTIME_URL`)下载
281
305
 
282
- **安装 runtime —— 一条命令**(推荐,21 MB 预编译包,内置 HolySheep Key 登录):
306
+ **预编译 runtime(内置默认值):**
283
307
 
284
- ```bash
285
- export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.17-holysheep.tar.gz
286
- export HOLYSHEEP_AIONUI_RUNTIME_SHA256=d75adcea3c57c85f64b5db96d18c593f20ad150888f47283dc2da0a440fb652c
287
- hs web --aionui --setup-runtime
288
- ```
308
+ - URL: `https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz`
309
+ - SHA256: `379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1`
310
+ - 大小: 21 MB(只包含 `dist-server/` + `out/renderer/`)
289
311
 
290
- fetcher 会下载到 `~/.holysheep/aionui-runtime/`,校验 SHA256,解压 `dist-server/ + out/ + package.json`,启动 AionUi,然后直接把浏览器带到 bootstrap 重定向(用你的 HS API Key 自动登录)。macOS / Linux 都行,Windows 视 `bun` 支持情况而定。
291
-
292
- **备用方案 —— 从 AionUi 源码构建**(高级用户):
312
+ **从源码自己构建**(审计所有 patch):
293
313
 
294
314
  ```bash
295
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
296
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
297
- mkdir -p ~/.holysheep/aionui-runtime
298
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
299
- hs web --aionui
315
+ git clone https://github.com/iOfficeAI/AionUi.git aionui-fork
316
+ cd aionui-fork && git checkout v1.9.18
317
+ bun install
318
+ bun run build:server && bun run build:renderer:web
319
+ # 用自己 build 的 runtime 启动:
320
+ cd .. && node src/index.js web
300
321
  ```
301
- 注意:上游 AionUi 的 `/login` 仍然是账号密码模式;wrapper 会自动走 legacy 路径,在 `~/.holysheep/aionui-bridge.json` (0600) 建一个桥接管理员账号,不影响使用。
302
322
 
303
- **恢复 / 高级**
323
+ **备用模式:**
324
+
325
+ - `HOLYSHEEP_WEBUI_LEGACY=1 hs web` —— 老 HolySheep Workspace 页面(零依赖 node,仍然随 npm 包发)
326
+ - `hs web --aionui` —— 强制 AionUi 模式;runtime 缺失时**不会自动回退**,直接报错(方便 CI 场景)
304
327
 
305
- - `/login` 仍可直接访问:`http://127.0.0.1:9876/login`,里面可以手动粘 HS API Key 再登录。
306
- - 要重置:`rm -rf ~/.holysheep/aionui-runtime && hs web --aionui --setup-runtime`,或直接编辑 `~/.holysheep/config.json`。
307
- - runtime 缺失时:`hs web --aionui` 会打印清晰的安装指引并自动回退到 legacy WebUI,不会静默失败。
308
- - 启动日志里 `[AionUi] Could not find builtin src/process/resources/...` 是已知噪音 —— 预编译 tarball 只带 `dist-server/` 和 `out/`,不含源码目录,所以模板 assistants/skills 会走内置 fallback。界面完全可用,只是少几个"源码级"模板。
328
+ **后续规划:**
309
329
 
310
- 不设置该环境变量时 `hs web` legacy WebUI,**不会再打印任何黄色警告**。
330
+ - **2.1.0(本次发布)** AionUi fork + HolySheep 登录
331
+ - **2.2.0(下一步)** —— 深度集成:在 AionUi 侧栏内嵌 HolySheep 多工具配置面板(不再是独立 legacy 页面)。在 AionUi 界面里直接管理 Claude Code / Cursor / Codex / Aider / continue。
311
332
 
312
333
  ### 常见问题
313
334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.0.6",
3
+ "version": "2.1.1",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "scripts": {
6
6
  "test": "node tests/droid.test.js && node tests/workspace-store.test.js",
@@ -1,103 +1,340 @@
1
1
  /**
2
2
  * hs web — 启动 WebUI 本地管理面板
3
3
  *
4
- * Modes (resolution order):
5
- * --aionui → AionUi proxy wrapper (HolySheep API key login)
6
- * HOLYSHEEP_WEBUI_AIONUI=1 same as --aionui (back-compat)
7
- * HOLYSHEEP_WEBUI_LEGACY=1 → no-op (legacy is already the default)
8
- * <none> → legacy WebUI (default, always ships in npm)
4
+ * 2.1.0 开始:`hs web` 默认直接启动 **真 AionUi v1.9.18 源码 fork 的 server**
5
+ * (`dist-server/server.mjs`),登录改成 HolySheep API Key(源码级改造,不是
6
+ * proxy wrapper 也不是外壳)。用户看到的就是 AionUi 自己的 UI。
7
+ *
8
+ * Modes:
9
+ * <default> → AionUi fork + HolySheep API Key login
10
+ * HOLYSHEEP_WEBUI_LEGACY=1 → 旧 HolySheep Workspace 页面 (fallback)
11
+ * --aionui → 强制走 AionUi 路径(即使 runtime 缺失也不退回)
9
12
  */
10
13
  'use strict'
11
14
 
12
15
  const chalk = require('chalk')
13
- const { execSync } = require('child_process')
16
+ const { execSync, spawn } = require('child_process')
14
17
  const fs = require('fs')
15
18
  const path = require('path')
19
+ const http = require('http')
16
20
 
17
- function wantsAionUi(opts) {
18
- if (opts && opts.aionui) return true
19
- if (process.env.HOLYSHEEP_WEBUI_AIONUI === '1') return true
20
- return false
21
+ function isLegacy(opts) {
22
+ return process.env.HOLYSHEEP_WEBUI_LEGACY === '1'
21
23
  }
22
24
 
25
+ // ── Bun resolution ───────────────────────────────────────────────────────────
23
26
  function resolveBunPath() {
24
- // Dev bundle: holysheep-cli/src/webui/vendor/bun-darwin-arm64
27
+ // 1. Dev: use bundled bun if present (darwin-arm64 only in 2.0.x)
25
28
  const bundledBun = path.join(__dirname, '..', 'webui', 'vendor', 'bun-darwin-arm64')
26
29
  if (process.platform === 'darwin' && process.arch === 'arm64' && fs.existsSync(bundledBun)) {
27
30
  return bundledBun
28
31
  }
32
+ // 2. Env override
29
33
  if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
34
+ // 3. System bun — platform-correct lookup command
35
+ const isWindows = process.platform === 'win32'
30
36
  try {
31
- const resolved = execSync('which bun', {
32
- stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 2000,
33
- }).trim()
34
- return resolved || null
35
- } catch { return null }
37
+ // `where.exe bun` on Windows prints one line per match; pick the first.
38
+ // `which bun` on Unix prints exactly one line (or fails with exit !=0).
39
+ const cmd = isWindows ? 'where.exe bun' : 'which bun'
40
+ const raw = execSync(cmd, {
41
+ stdio: ['ignore', 'pipe', 'ignore'],
42
+ encoding: 'utf8',
43
+ timeout: 3000,
44
+ })
45
+ // Windows emits CRLF and may list multiple paths — take the first existing one.
46
+ const candidates = raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
47
+ for (const p of candidates) {
48
+ if (fs.existsSync(p)) return p
49
+ }
50
+ } catch {}
51
+ // 4. No bun found. AionUi's dist-server/server.mjs uses `bun:` URL scheme
52
+ // imports (verified 2026-04-21: node 25 throws ERR_UNSUPPORTED_ESM_URL_SCHEME
53
+ // immediately), so bun is a hard runtime dep. Caller shows install guidance.
54
+ return null
55
+ }
56
+
57
+ function describeBunInstall() {
58
+ if (process.platform === 'win32') {
59
+ return 'powershell -c "irm bun.sh/install.ps1 | iex"'
60
+ }
61
+ return 'curl -fsSL https://bun.sh/install | bash'
62
+ }
63
+
64
+ // ── Runtime resolution ───────────────────────────────────────────────────────
65
+ // Priority:
66
+ // 1. Local dev checkout at ../aionui-fork/dist-server (developer building locally)
67
+ // 2. Installed runtime at ~/.holysheep/aionui-runtime/dist-server
68
+ // 3. If --setup-runtime + HOLYSHEEP_AIONUI_RUNTIME_URL, download into (2)
69
+ // 4. null → fall back to legacy workspace
70
+ function resolveAionUiRuntimeDir() {
71
+ // (1) Dev checkout — if holysheep-cli was cloned and `bun run build` ran in aionui-fork
72
+ const repoRoot = path.resolve(__dirname, '..', '..')
73
+ const devRuntime = path.join(repoRoot, 'aionui-fork')
74
+ if (fs.existsSync(path.join(devRuntime, 'dist-server', 'server.mjs'))) {
75
+ return { dir: devRuntime, source: 'dev-checkout' }
76
+ }
77
+ // (2) User-installed runtime
78
+ const home = process.env.HOME || process.env.USERPROFILE
79
+ if (home) {
80
+ const installed = path.join(home, '.holysheep', 'aionui-runtime')
81
+ if (fs.existsSync(path.join(installed, 'dist-server', 'server.mjs'))) {
82
+ return { dir: installed, source: 'installed' }
83
+ }
84
+ }
85
+ return null
86
+ }
87
+
88
+ async function downloadAionUiRuntime(logger) {
89
+ // Fetcher has a baked-in default URL + SHA256 — no env var required.
90
+ // HOLYSHEEP_AIONUI_RUNTIME_URL still works as an override.
91
+ const { resolveRuntime } = require('../webui/aionui-runtime-fetcher')
92
+ return resolveRuntime({ allowDownload: true, logger })
93
+ }
94
+
95
+ // ── HolySheep config → API key ───────────────────────────────────────────────
96
+ function readHolySheepApiKey() {
97
+ try {
98
+ const { getApiKey } = require('../utils/config')
99
+ const key = getApiKey()
100
+ if (key && /^cr_/.test(key)) return key
101
+ } catch {}
102
+ return null
103
+ }
104
+
105
+ // ── HTTP utility: POST JSON with CSRF auto-bootstrap ─────────────────────────
106
+ function httpRequest(options, body) {
107
+ return new Promise((resolve, reject) => {
108
+ const req = http.request(options, (res) => {
109
+ const chunks = []
110
+ res.on('data', (c) => chunks.push(c))
111
+ res.on('end', () => {
112
+ const buf = Buffer.concat(chunks)
113
+ resolve({ status: res.statusCode || 0, headers: res.headers, body: buf.toString('utf8') })
114
+ })
115
+ })
116
+ req.on('error', reject)
117
+ if (body) req.write(body)
118
+ req.end()
119
+ })
120
+ }
121
+
122
+ async function fetchCsrfToken(port) {
123
+ // AionUi sets csrf cookie on first GET /. We fetch / and read Set-Cookie.
124
+ const r = await httpRequest({ host: '127.0.0.1', port, path: '/', method: 'GET' })
125
+ const setCookie = r.headers['set-cookie'] || []
126
+ for (const sc of setCookie) {
127
+ const m = sc.match(/csrfToken=([^;]+);/)
128
+ if (m) return { csrfToken: m[1], allCookies: setCookie.map((s) => s.split(';')[0]).join('; ') }
129
+ }
130
+ return null
131
+ }
132
+
133
+ async function loginWithApiKey(port, apiKey) {
134
+ const csrf = await fetchCsrfToken(port)
135
+ if (!csrf) throw new Error('Failed to acquire CSRF token from AionUi server')
136
+ const body = JSON.stringify({ apiKey, csrfToken: decodeURIComponent(csrf.csrfToken) })
137
+ const r = await httpRequest(
138
+ {
139
+ host: '127.0.0.1',
140
+ port,
141
+ path: '/login',
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ 'Content-Length': Buffer.byteLength(body),
146
+ Cookie: csrf.allCookies,
147
+ 'x-csrf-token': decodeURIComponent(csrf.csrfToken),
148
+ },
149
+ },
150
+ body
151
+ )
152
+ if (r.status !== 200) {
153
+ throw new Error(`/login returned ${r.status}: ${r.body.slice(0, 200)}`)
154
+ }
155
+ const setCookie = r.headers['set-cookie'] || []
156
+ // Return a Cookie header string the browser can be seeded with.
157
+ const cookieLine = setCookie.map((s) => s.split(';')[0]).join('; ')
158
+ return { cookieLine, body: JSON.parse(r.body) }
36
159
  }
37
160
 
161
+ // ── Start patched AionUi server ──────────────────────────────────────────────
162
+ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
163
+ return new Promise((resolve, reject) => {
164
+ const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
165
+ if (!fs.existsSync(entry)) {
166
+ return reject(new Error(`AionUi entry not found: ${entry}`))
167
+ }
168
+
169
+ // AionUi reads PORT / HOST / JWT_SECRET from env. We set a stable JWT secret
170
+ // derived from machine so cookies survive restarts of hs web within a session.
171
+ const child = spawn(bunPath, [entry], {
172
+ cwd: runtimeDir,
173
+ env: {
174
+ ...process.env,
175
+ PORT: String(port),
176
+ HOST: '127.0.0.1',
177
+ NODE_ENV: 'production',
178
+ // Let AionUi pick its own JWT secret; it'll persist under ~/.aionui-home
179
+ },
180
+ stdio: ['ignore', 'pipe', 'pipe'],
181
+ })
182
+
183
+ let settled = false
184
+ const onReady = () => {
185
+ if (!settled) {
186
+ settled = true
187
+ resolve(child)
188
+ }
189
+ }
190
+ const timer = setTimeout(() => {
191
+ if (!settled) {
192
+ settled = true
193
+ reject(new Error('AionUi server failed to start within 20s'))
194
+ }
195
+ }, 20_000)
196
+ // Poll /health-ish endpoint to detect readiness.
197
+ const pollReady = () => {
198
+ const req = http.request(
199
+ { host: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 500 },
200
+ (res) => {
201
+ if (res.statusCode && res.statusCode < 500) {
202
+ clearTimeout(timer)
203
+ onReady()
204
+ } else {
205
+ setTimeout(pollReady, 300)
206
+ }
207
+ }
208
+ )
209
+ req.on('error', () => setTimeout(pollReady, 300))
210
+ req.on('timeout', () => {
211
+ req.destroy()
212
+ setTimeout(pollReady, 300)
213
+ })
214
+ req.end()
215
+ }
216
+ // Start polling after a short grace period so bun has time to boot.
217
+ setTimeout(pollReady, 600)
218
+
219
+ // Surface logs with prefix (muted by default; set HS_WEB_DEBUG=1 to see)
220
+ const debug = process.env.HS_WEB_DEBUG === '1'
221
+ if (debug) {
222
+ child.stdout.on('data', (d) => process.stdout.write(chalk.gray(`[aionui] ${d}`)))
223
+ child.stderr.on('data', (d) => process.stderr.write(chalk.gray(`[aionui] ${d}`)))
224
+ }
225
+ child.on('exit', (code) => {
226
+ if (!settled) {
227
+ settled = true
228
+ reject(new Error(`AionUi server exited with code ${code} before becoming ready`))
229
+ }
230
+ })
231
+ })
232
+ }
233
+
234
+ // ── The real AionUi-mode flow ────────────────────────────────────────────────
38
235
  async function startAionUiMode(opts) {
39
236
  const port = Number(opts.port) || 9876
40
- const { resolveRuntime, describeInstallGuidance } = require('../webui/aionui-runtime-fetcher')
41
- const { startWrapper } = require('../webui/aionui-wrapper')
42
- const { getApiKey } = require('../utils/config')
43
-
44
- const allowDownload = opts.setupRuntime || process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
45
- const runtime = await resolveRuntime({
46
- allowDownload: !!allowDownload,
47
- logger: (m) => console.log(chalk.gray(` ${m}`)),
48
- })
49
237
 
238
+ // 1. Resolve runtime. First launch after `npm i -g` won't have one locally
239
+ // — we auto-download from the baked-in URL (verified by SHA256) unless the
240
+ // user opts out via HOLYSHEEP_WEBUI_NO_AUTOFETCH=1.
241
+ let runtime = resolveAionUiRuntimeDir()
242
+ const autoFetchDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH === '1'
243
+ if (!runtime && !autoFetchDisabled) {
244
+ console.log(chalk.cyan('▶ AionUi runtime not installed — downloading automatically (~21 MB, one-time)'))
245
+ console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH=1; override URL with HOLYSHEEP_AIONUI_RUNTIME_URL)'))
246
+ const downloaded = await downloadAionUiRuntime((m) => console.log(chalk.gray(` ${m}`)))
247
+ if (downloaded) {
248
+ runtime = { dir: downloaded.dir, source: downloaded.source }
249
+ }
250
+ }
50
251
  if (!runtime) {
51
- console.log(chalk.red('✗ AionUi runtime not installed'))
252
+ const home = process.env.HOME || process.env.USERPROFILE || '~'
253
+ console.log(chalk.red('✗ AionUi runtime not found and auto-download did not succeed'))
52
254
  console.log()
53
- console.log(chalk.gray(describeInstallGuidance()))
255
+ console.log(chalk.gray(' Expected at one of:'))
256
+ console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
257
+ console.log(chalk.gray(` • ${path.join(home, '.holysheep', 'aionui-runtime', 'dist-server')} (installed)`))
54
258
  console.log()
55
- console.log(chalk.yellow('Falling back to legacy WebUI. Run `hs web --aionui --setup-runtime` once you provide HOLYSHEEP_AIONUI_RUNTIME_URL, or install manually.'))
259
+ console.log(chalk.yellow(' Retry manually:'))
260
+ console.log(chalk.cyan(' hs web --setup-runtime'))
261
+ console.log(chalk.gray(' Or pin a specific runtime URL + SHA256:'))
262
+ console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'))
263
+ console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<see README>'))
264
+ console.log(chalk.cyan(' hs web --setup-runtime'))
265
+ console.log()
266
+ if (opts.aionui) {
267
+ console.log(chalk.red(' --aionui flag requires runtime. Aborting.'))
268
+ process.exit(1)
269
+ }
270
+ console.log(chalk.yellow(' Falling back to legacy HolySheep workspace. Set HOLYSHEEP_WEBUI_LEGACY=1 to silence this message.'))
56
271
  console.log()
57
272
  return startLegacyMode(opts)
58
273
  }
59
274
 
275
+ // 2. Resolve bun
60
276
  const bunPath = resolveBunPath()
61
277
  if (!bunPath) {
62
- console.log(chalk.red('✗ bun is required to start the AionUi runtime (install: https://bun.sh)'))
63
- console.log(chalk.yellow('Falling back to legacy WebUI.'))
278
+ console.log(chalk.red('✗ bun is required to run the AionUi server'))
279
+ console.log(chalk.gray(' (AionUi uses bun: URL-scheme imports that Node cannot load directly)'))
280
+ console.log()
281
+ console.log(chalk.yellow(' Install bun:'))
282
+ console.log(chalk.cyan(` ${describeBunInstall()}`))
283
+ console.log(chalk.gray(' After install, close and reopen your terminal, then retry `hs web`.'))
284
+ console.log()
285
+ if (opts.aionui) {
286
+ console.log(chalk.red(' --aionui flag requires bun. Aborting.'))
287
+ process.exit(1)
288
+ }
289
+ console.log(chalk.yellow(' Falling back to legacy HolySheep workspace.'))
290
+ console.log()
64
291
  return startLegacyMode(opts)
65
292
  }
66
293
 
67
- console.log(chalk.cyan(`▶ AionUi mode (runtime: ${runtime.version}, source: ${runtime.source})`))
294
+ console.log(chalk.cyan(`▶ Starting AionUi v1.9.18 (HolySheep fork, source: ${runtime.source})`))
68
295
 
69
- let handle
296
+ // 3. Spawn server on the user-visible port directly — no proxy wrapper
297
+ let aionuiProc
70
298
  try {
71
- handle = await startWrapper({
72
- port,
73
- runtimeDir: runtime.dir,
74
- runtimeVersion: runtime.version,
75
- runtimeSource: runtime.source,
76
- bunPath,
77
- })
299
+ aionuiProc = await spawnAionUiServer({ bunPath, runtimeDir: runtime.dir, port })
78
300
  } catch (e) {
79
- console.log(chalk.red(`✗ AionUi wrapper failed to start: ${e.message}`))
80
- console.log(chalk.yellow('Falling back to legacy WebUI.'))
301
+ console.log(chalk.red(`✗ AionUi server failed to start: ${e.message}`))
302
+ if (opts.aionui) process.exit(1)
303
+ console.log(chalk.yellow(' Falling back to legacy workspace.'))
81
304
  return startLegacyMode(opts)
82
305
  }
83
306
 
84
307
  const baseUrl = `http://127.0.0.1:${port}`
85
- const apiKey = getApiKey()
86
308
 
87
- // Auto-bootstrap: if we already have an HS key, mint a single-use token and
88
- // open the browser at /api/auth/holysheep-bootstrap?token=… so the user
89
- // lands authenticated without touching AionUi's /login form.
309
+ // 4. Auto-login via HolySheep API key if available
310
+ const apiKey = readHolySheepApiKey()
90
311
  let launchUrl = baseUrl
91
312
  if (apiKey) {
92
- const token = handle.mintBootstrapToken()
93
- launchUrl = `${baseUrl}/api/auth/holysheep-bootstrap?token=${token}`
94
- console.log(chalk.green(`✓ HolySheep API key detected auto-login via bootstrap token`))
313
+ try {
314
+ const { cookieLine } = await loginWithApiKey(port, apiKey)
315
+ // We can't programmatically seed the browser with cookies easily. Best UX:
316
+ // - AionUi /login already set the cookie in our HTTP client, not the browser
317
+ // - We ask user to paste key once in UI, OR we display a one-shot magic link
318
+ // For now, we open /login with the key prefilled via URL hash so the React
319
+ // login page can auto-submit. Implemented via localStorage fallback below.
320
+ if (cookieLine) {
321
+ // The cookie was set on our programmatic client, not the browser. Since
322
+ // we can't cross-write cookies, we just let AionUi's /login page auto-fill
323
+ // the apiKey via URL hash. React page reads window.location.hash on mount.
324
+ launchUrl = `${baseUrl}/login#apiKey=${encodeURIComponent(apiKey)}`
325
+ console.log(chalk.green('✓ HolySheep API key detected — auto-filled on login page'))
326
+ }
327
+ } catch (e) {
328
+ console.log(chalk.yellow(` (auto-login pre-check failed: ${e.message} — will open /login manually)`))
329
+ launchUrl = `${baseUrl}/login`
330
+ }
95
331
  } else {
96
- console.log(chalk.yellow(`! No HolySheep API key saved. POST it to ${baseUrl}/api/auth/holysheep-login or run 'hs login' first.`))
332
+ launchUrl = `${baseUrl}/login`
333
+ console.log(chalk.yellow(' No HolySheep API key saved yet. Run `hs login` first to enable auto-fill.'))
97
334
  }
98
335
 
99
336
  console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(launchUrl)}`))
100
- console.log(chalk.gray(' Mode: AionUi runtime + HolySheep API key bridge'))
337
+ console.log(chalk.gray(' Mode: AionUi v1.9.18 (源码 fork, HolySheep 登录)'))
101
338
  console.log(chalk.gray(' Press Ctrl+C to stop'))
102
339
  console.log()
103
340
 
@@ -110,22 +347,27 @@ async function startAionUiMode(opts) {
110
347
  } catch {}
111
348
  }
112
349
 
113
- const stopChildren = () => {
114
- try { handle.aionui.kill('SIGTERM') } catch {}
115
- try { handle.server.close() } catch {}
350
+ const stop = () => {
351
+ try { aionuiProc.kill('SIGTERM') } catch {}
352
+ setTimeout(() => {
353
+ try { aionuiProc.kill('SIGKILL') } catch {}
354
+ process.exit(0)
355
+ }, 2000)
116
356
  }
117
- process.on('SIGINT', stopChildren)
118
- process.on('SIGTERM', stopChildren)
357
+ process.on('SIGINT', stop)
358
+ process.on('SIGTERM', stop)
119
359
 
120
360
  await new Promise(() => {})
121
361
  }
122
362
 
363
+ // ── Legacy fallback (old HolySheep Workspace page) ───────────────────────────
123
364
  async function startLegacyMode(opts) {
124
365
  const port = Number(opts.port) || 9876
125
366
  const { startServer } = require('../webui/server')
126
367
  await startServer(port)
127
368
  const url = `http://127.0.0.1:${port}`
128
369
  console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(url)}`))
370
+ console.log(chalk.gray(' (Legacy HolySheep Workspace — 原生 node 轻量模式)'))
129
371
  console.log(chalk.gray(' 按 Ctrl+C 停止'))
130
372
  console.log()
131
373
  if (opts.open !== false) {
@@ -146,10 +388,19 @@ async function webui(opts) {
146
388
  console.log()
147
389
 
148
390
  try {
149
- if (wantsAionUi(opts)) {
150
- return await startAionUiMode(opts)
391
+ // 默认走 AionUi;HOLYSHEEP_WEBUI_LEGACY=1 才退回 legacy
392
+ if (isLegacy(opts) && !opts.aionui) {
393
+ console.log(chalk.gray(`[mode=legacy platform=${process.platform}]`))
394
+ console.log()
395
+ return await startLegacyMode(opts)
151
396
  }
152
- return await startLegacyMode(opts)
397
+ // Tri-state mode line: helps user-reports when things go sideways
398
+ const bunFound = resolveBunPath() ? 'found' : 'missing'
399
+ const rt = resolveAionUiRuntimeDir()
400
+ const rtLabel = rt ? rt.source : 'none'
401
+ console.log(chalk.gray(`[mode=aionui platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
402
+ console.log()
403
+ return await startAionUiMode(opts)
153
404
  } catch (err) {
154
405
  console.log(chalk.red(`✗ 启动失败: ${err.message}`))
155
406
  process.exit(1)
@@ -4,15 +4,15 @@
4
4
  * Resolution order:
5
5
  * 1. ~/.holysheep/aionui-runtime/ (installed / cached)
6
6
  * 2. <cli>/src/webui/vendor/aionui/ (dev checkout — not shipped to npm)
7
- * 3. HOLYSHEEP_AIONUI_RUNTIME_URL env var (advanced user escape hatch,
8
- * downloads + SHA256-verifies when the constant below is also populated)
9
- * 4. null caller must show a clear "runtime not installed" error
7
+ * 3. <cli>/../aionui-fork/ (dev repo checkout — not shipped to npm)
8
+ * 4. DEFAULT_RUNTIME_URL + DEFAULT_RUNTIME_SHA256 (baked in; auto-download
9
+ * when --setup-runtime is passed). Users can override via env.
10
+ * 5. null → caller must show a clear "runtime not installed" error
10
11
  *
11
- * We DO NOT ship a hard-coded GitHub Release URL in this version because
12
- * the release asset has not been uploaded yet. Shipping a 404 URL is a
13
- * "code that lies" anti-pattern explicit null + clear error message is
14
- * honest. A follow-up PR will add the URL + SHA256 constant once the
15
- * prebuilt tarball is published.
12
+ * The baked-in default URL is a public mirror of the AionUi v1.9.18 fork
13
+ * with HolySheep API-key login patched in. It's the prebuilt artifact that
14
+ * ships alongside `@simonyea/holysheep-cli` and is used by `hs web` on
15
+ * first launch when no local runtime is present.
16
16
  */
17
17
 
18
18
  'use strict'
@@ -27,6 +27,14 @@ const http = require('http')
27
27
  const USER_CACHE_DIR = path.join(os.homedir(), '.holysheep', 'aionui-runtime')
28
28
  const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
29
29
 
30
+ // Baked-in defaults — updated with every 2.x.0 release that bumps the bundle.
31
+ // Override via HOLYSHEEP_AIONUI_RUNTIME_URL / HOLYSHEEP_AIONUI_RUNTIME_SHA256.
32
+ const DEFAULT_RUNTIME_URL =
33
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'
34
+ const DEFAULT_RUNTIME_SHA256 =
35
+ '379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1'
36
+ const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep'
37
+
30
38
  function isValidRuntimeDir(dir) {
31
39
  if (!dir) return false
32
40
  try {
@@ -55,24 +63,34 @@ function readVersion(dir) {
55
63
  * @returns {{ dir: string, version: string, source: 'user-cache'|'vendor'|'env-download' } | null}
56
64
  */
57
65
  async function resolveRuntime({ allowDownload = false, logger = () => {} } = {}) {
58
- // 1. User cache
66
+ // 1. User cache (installed runtime)
59
67
  if (isValidRuntimeDir(USER_CACHE_DIR)) {
60
68
  return { dir: USER_CACHE_DIR, version: readVersion(USER_CACHE_DIR), source: 'user-cache' }
61
69
  }
62
70
 
63
- // 2. Dev vendor checkout
71
+ // 2. Dev vendor checkout (bundled for darwin-arm64 dev rigs — not in npm)
64
72
  if (isValidRuntimeDir(VENDOR_DIR)) {
65
73
  return { dir: VENDOR_DIR, version: readVersion(VENDOR_DIR), source: 'vendor' }
66
74
  }
67
75
 
68
- // 3. Environment-provided download URL (advanced escape hatch)
69
- const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
70
- const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256
71
- if (url && allowDownload) {
76
+ // 3. Dev repo aionui-fork/ (when running from the holysheep-cli source repo)
77
+ const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
78
+ if (isValidRuntimeDir(forkDir)) {
79
+ return { dir: forkDir, version: readVersion(forkDir), source: 'aionui-fork' }
80
+ }
81
+
82
+ // 4. Download from env override OR baked-in default
83
+ if (allowDownload) {
84
+ const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL || DEFAULT_RUNTIME_URL
85
+ const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256 || DEFAULT_RUNTIME_SHA256
72
86
  try {
73
87
  await downloadAndExtract(url, USER_CACHE_DIR, expectedSha, logger)
74
88
  if (isValidRuntimeDir(USER_CACHE_DIR)) {
75
- return { dir: USER_CACHE_DIR, version: readVersion(USER_CACHE_DIR), source: 'env-download' }
89
+ return {
90
+ dir: USER_CACHE_DIR,
91
+ version: readVersion(USER_CACHE_DIR) || DEFAULT_RUNTIME_VERSION,
92
+ source: 'download',
93
+ }
76
94
  }
77
95
  logger('AionUi runtime downloaded but directory structure invalid')
78
96
  } catch (e) {
@@ -149,27 +167,33 @@ function downloadAndExtract(url, destDir, expectedSha, logger) {
149
167
 
150
168
  function describeInstallGuidance() {
151
169
  return [
152
- 'AionUi runtime not installed. To enable `hs web --aionui`, install it via ONE of:',
170
+ 'AionUi runtime not installed. Fastest way to get started:',
153
171
  '',
154
- ' Option 1 Build from source (requires bun):',
172
+ ' One command (downloads the prebuilt 21 MB runtime):',
173
+ ' hs web --setup-runtime',
174
+ '',
175
+ ' Manual install via env overrides:',
176
+ ` export HOLYSHEEP_AIONUI_RUNTIME_URL=${DEFAULT_RUNTIME_URL}`,
177
+ ` export HOLYSHEEP_AIONUI_RUNTIME_SHA256=${DEFAULT_RUNTIME_SHA256}`,
178
+ ' hs web --setup-runtime',
179
+ '',
180
+ ' Build from source (requires bun):',
155
181
  ' git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi',
156
182
  ' cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server',
157
183
  ` mkdir -p ${USER_CACHE_DIR}`,
158
- ` cp -R ~/AionUi/{dist-server,out,package.json} ${USER_CACHE_DIR}/`,
159
- '',
160
- ' Option 2 — Supply a prebuilt tarball URL:',
161
- ' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://your.host/aionui-runtime.tar.gz',
162
- ' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<sha256>',
163
- ' hs web --aionui',
184
+ ` cp -R ~/AionUi/{dist-server,out} ${USER_CACHE_DIR}/`,
164
185
  '',
165
- ' Option 3 — Fall back to legacy WebUI:',
166
- ' hs web',
186
+ ' Or fall back to the legacy HolySheep Workspace:',
187
+ ' HOLYSHEEP_WEBUI_LEGACY=1 hs web',
167
188
  ].join('\n')
168
189
  }
169
190
 
170
191
  module.exports = {
171
192
  USER_CACHE_DIR,
172
193
  VENDOR_DIR,
194
+ DEFAULT_RUNTIME_URL,
195
+ DEFAULT_RUNTIME_SHA256,
196
+ DEFAULT_RUNTIME_VERSION,
173
197
  isValidRuntimeDir,
174
198
  readVersion,
175
199
  resolveRuntime,
@@ -239,7 +239,7 @@ async function handleLogin(req, res) {
239
239
  }
240
240
  try {
241
241
  const valid = await validateApiKey(apiKey)
242
- if (!valid) return json(res, { success: false, message: 'API Key 无效' }, 401)
242
+ if (!valid) return json(res, { success: false, message: 'API Key 无效 (server returned non-2xx)' }, 401)
243
243
  saveConfig({ apiKey, savedAt: new Date().toISOString() })
244
244
  workspaceStore.saveHolySheepApiConfig({
245
245
  apiKey,
@@ -248,7 +248,25 @@ async function handleLogin(req, res) {
248
248
  })
249
249
  json(res, { success: true, apiKey: maskKey(apiKey) })
250
250
  } catch (e) {
251
- json(res, { success: false, message: `验证失败: ${e.message}` }, 500)
251
+ // Translate cryptic node-fetch / DNS / proxy errors into something actionable.
252
+ // Previously users just saw "链接失败" in the browser alert — now we surface
253
+ // the underlying cause (DNS, timeout, proxy interference, TLS, etc).
254
+ const code = e && (e.code || e.errno || e.type)
255
+ const name = e && e.name
256
+ let hint
257
+ if (code === 'EAI_AGAIN' || code === 'ENOTFOUND') {
258
+ hint = 'DNS 解析失败 — 检查网络 / 代理 / 防火墙是否拦截了 api.holysheep.ai'
259
+ } else if (code === 'ETIMEDOUT' || name === 'AbortError') {
260
+ hint = '连接超时 — 可能是网络慢或代理阻塞;试试开关代理再重试'
261
+ } else if (code === 'ECONNREFUSED' || code === 'ECONNRESET') {
262
+ hint = '连接被拒绝 — 检查本机防火墙或公司代理'
263
+ } else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
264
+ hint = 'TLS 证书校验失败 — 可能是系统时间错误或中间人代理'
265
+ } else {
266
+ hint = e && e.message ? e.message : String(e)
267
+ }
268
+ const detail = `${hint}${code ? ` [${code}]` : ''}`
269
+ json(res, { success: false, message: `验证失败: ${detail}` }, 500)
252
270
  }
253
271
  }
254
272