@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 +88 -67
- package/package.json +1 -1
- package/src/commands/webui.js +309 -58
- package/src/webui/aionui-runtime-fetcher.js +49 -25
- package/src/webui/server.js +20 -2
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
|
|
110
|
+
### `hs web` — real AionUi v1.9.18, HolySheep-powered (2.1.0+)
|
|
111
111
|
|
|
112
|
-
`hs web`
|
|
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
|
-
#
|
|
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
|
-
#
|
|
121
|
-
hs web --
|
|
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
|
-
**
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
-
**
|
|
149
|
+
**Prebuilt runtime (baked-in default):**
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
**
|
|
155
|
+
**Rebuild from source** (audit the patches yourself):
|
|
148
156
|
|
|
149
157
|
```bash
|
|
150
|
-
git clone https://github.com/iOfficeAI/AionUi.git
|
|
151
|
-
cd
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
**
|
|
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
|
-
-
|
|
161
|
-
-
|
|
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
|
|
267
|
+
### `hs web` —— 真 AionUi v1.9.18,HolySheep 驱动(2.1.0+)
|
|
256
268
|
|
|
257
|
-
|
|
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
|
-
#
|
|
272
|
+
# 安装 CLI(npm 包仍然 ~100 kB;21 MB runtime 首次运行再下载)
|
|
263
273
|
npm install -g @simonyea/holysheep-cli
|
|
264
274
|
|
|
265
|
-
#
|
|
266
|
-
hs web --
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
280
|
-
3.
|
|
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
|
-
|
|
306
|
+
**预编译 runtime(内置默认值):**
|
|
283
307
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
**备用方案 —— 从 AionUi 源码构建**(高级用户):
|
|
312
|
+
**从源码自己构建**(审计所有 patch):
|
|
293
313
|
|
|
294
314
|
```bash
|
|
295
|
-
git clone https://github.com/iOfficeAI/AionUi.git
|
|
296
|
-
cd
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/commands/webui.js
CHANGED
|
@@ -1,103 +1,340 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hs web — 启动 WebUI 本地管理面板
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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
|
|
63
|
-
console.log(chalk.
|
|
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
|
|
294
|
+
console.log(chalk.cyan(`▶ Starting AionUi v1.9.18 (HolySheep fork, source: ${runtime.source})`))
|
|
68
295
|
|
|
69
|
-
|
|
296
|
+
// 3. Spawn server on the user-visible port directly — no proxy wrapper
|
|
297
|
+
let aionuiProc
|
|
70
298
|
try {
|
|
71
|
-
|
|
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
|
|
80
|
-
|
|
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-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
114
|
-
try {
|
|
115
|
-
|
|
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',
|
|
118
|
-
process.on('SIGTERM',
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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 {
|
|
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.
|
|
170
|
+
'AionUi runtime not installed. Fastest way to get started:',
|
|
153
171
|
'',
|
|
154
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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,
|
package/src/webui/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|