dsh-calibguard 0.2.3 → 0.2.5
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.en.md +27 -16
- package/README.md +27 -16
- package/docs/configuration.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +1 -1
- package/setup.mjs +170 -6
package/README.en.md
CHANGED
|
@@ -59,7 +59,7 @@ restart DSH, if `<engineRoot>/.venv` is missing the plugin automatically, in the
|
|
|
59
59
|
|
|
60
60
|
1. downloads the official CG release `CalibGuard-GUI.zip` (~467 MB) and extracts it to `CG_GUI/`
|
|
61
61
|
**next to this package**;
|
|
62
|
-
2. creates `.venv` (
|
|
62
|
+
2. creates `.venv` (**no preinstalled Python required**: if no 64-bit 3.10–3.12 is found, a portable Python 3.10.8 is downloaded automatically, ~30 MB);
|
|
63
63
|
3. installs dependencies from `requirements-gui.txt` (1–3 GB);
|
|
64
64
|
4. becomes ready — tools and the panel just work.
|
|
65
65
|
|
|
@@ -82,7 +82,7 @@ once (depends on network and pip mirror).
|
|
|
82
82
|
### Prerequisites
|
|
83
83
|
|
|
84
84
|
1. **DSH** (DeepSeek Harness) — verified on 0.1.5-rc.2;
|
|
85
|
-
2. **64-bit
|
|
85
|
+
2. **Python**: nothing to install — when no 64-bit 3.10–3.12 is available the installer downloads a portable Python automatically (cached in `~/.dsh/calibguard-tools`; disable with `--no-portable-python`);
|
|
86
86
|
3. **The CG engine** — downloaded automatically, or an existing `CG_GUI/` (with `calibguard/`, `models/` and `.venv`).
|
|
87
87
|
|
|
88
88
|
### Option 0 — desktop "Add plugin" dialog (easiest, recommended)
|
|
@@ -93,21 +93,34 @@ DSH desktop → Settings → **Add plugin**, paste any one of the following (res
|
|
|
93
93
|
|---|---|---|
|
|
94
94
|
| **GitHub repository URL** (recommended) | `https://github.com/maomitgd2006/DSH_CG` | Fetches the source and registers the bundle in the current profile |
|
|
95
95
|
| **Local directory path** | `<path to this package>` (e.g. `/opt/dsh-calibguard`) | For a local clone / extracted copy |
|
|
96
|
-
| Package name | `dsh-calibguard` | ✅ **Published on npm
|
|
96
|
+
| **Package name** | `dsh-calibguard` | ✅ **Published on npm**; just enter the package name (installs npm `latest`) |
|
|
97
97
|
|
|
98
98
|
> Equivalent CLI (verified: after install DSH automatically appends the package to the profile's
|
|
99
99
|
> `dsh.profile.bundles`):
|
|
100
100
|
>
|
|
101
101
|
> ```powershell
|
|
102
|
-
> dsh plugin --profile <profile> add
|
|
103
|
-
> dsh plugin --profile <profile> add
|
|
102
|
+
> dsh plugin --profile <profile> add dsh-calibguard # npm package (recommended)
|
|
103
|
+
> dsh plugin --profile <profile> add https://github.com/maomitgd2006/DSH_CG # GitHub source
|
|
104
|
+
> dsh plugin --profile <profile> add file:<absolute path to this package> # local directory
|
|
104
105
|
> ```
|
|
105
106
|
>
|
|
106
|
-
>
|
|
107
|
+
> npm package page: <https://www.npmjs.com/package/dsh-calibguard>
|
|
107
108
|
|
|
108
109
|
**Whichever way you install it**, the first run prepares the engine automatically (next section) — no manual setup needed.
|
|
109
110
|
|
|
110
|
-
### Option A —
|
|
111
|
+
### Option A — install via the CLI (npm package; for non-desktop / scripted setups)
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
dsh plugin --profile <profile> add dsh-calibguard
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then insert the loader row in the profile's `cordis.patch.yml` with `name: dsh-calibguard`
|
|
118
|
+
(this package ships its own `cordis.patch.yml`, which is applied automatically as a bundle).
|
|
119
|
+
|
|
120
|
+
> The `desktop` profile is managed exclusively by the Electron app — CLI refuses it. Desktop users
|
|
121
|
+
> should use "Option 0", or the `file://` row in "Option B".
|
|
122
|
+
|
|
123
|
+
### Option B — source mode (a `file://` loader row, for local development / offline use)
|
|
111
124
|
|
|
112
125
|
Append to the profile's `cordis.patch.yml` (replace the path with yours):
|
|
113
126
|
|
|
@@ -127,14 +140,11 @@ The `insert` path is rewritten to a `file://` URL by DSH. On desktop the profile
|
|
|
127
140
|
Optional: `node scripts/link-dsh.mjs` junctions DSH's own `@deepseek-ai/*` packages into this package so
|
|
128
141
|
the optional `defineTool` / Config schema support is active (everything still works without it).
|
|
129
142
|
|
|
130
|
-
### Option
|
|
131
|
-
|
|
132
|
-
```powershell
|
|
133
|
-
dsh plugin --profile <profile> add file:<absolute path to this package>
|
|
134
|
-
```
|
|
143
|
+
### Option C — release archive (offline / no npm)
|
|
135
144
|
|
|
136
|
-
|
|
137
|
-
(
|
|
145
|
+
Download `dsh-calibguard-<version>.zip` (~100 KB, no `test/`) from
|
|
146
|
+
[Releases](https://github.com/maomitgd2006/DSH_CG/releases/latest), extract it to a fixed directory,
|
|
147
|
+
then point the `name` in "Option B" at `<extracted dir>/lib/index.js`.
|
|
138
148
|
|
|
139
149
|
### Verify
|
|
140
150
|
|
|
@@ -215,7 +225,7 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
|
|
|
215
225
|
| `device` | `auto` | Checks both CUDA build and compute capability; low-end GPUs fall back to CPU |
|
|
216
226
|
| `credentialRef` | `''` | Recommended credential reference name, e.g. `DEEPSEEK_API_KEY` (resolved via the DSH credential store) |
|
|
217
227
|
| `announceToModel` | `true` | Add "when to use CalibGuard" to the system prompt |
|
|
218
|
-
| `autoSetup` | `true` | On first run, automatically download the engine
|
|
228
|
+
| `autoSetup` | `true` | On first run, automatically download the engine, a portable Python when needed, and install deps; set `false` to only fail with instructions |
|
|
219
229
|
| `maxFiles` | `20` | Directory recursion cap; explicitly given/uploaded files are not capped |
|
|
220
230
|
|
|
221
231
|
---
|
|
@@ -245,7 +255,8 @@ DSH_CG/
|
|
|
245
255
|
├── bridge/ Python bridge: persistent child process, line-delimited JSON protocol
|
|
246
256
|
├── cordis.patch.yml Bundle patch (inserts the loader row)
|
|
247
257
|
├── scripts/ link-dsh (dev-time junction), bridge-selftest (engine self-check)
|
|
248
|
-
├── test/ node --test (
|
|
258
|
+
├── test/ node --test (95 cases; auto-skip when the engine env is missing)
|
|
259
|
+
├── .github/workflows/ publish.yml: npm trusted publishing (OIDC, optional)
|
|
249
260
|
└── docs/ configuration, performance, troubleshooting, plugin contract, design notes
|
|
250
261
|
```
|
|
251
262
|
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
**不用手动装**——装好插件重启 DSH 后,若检测到缺少 `<engineRoot>/.venv`,插件会在**后台自动**:
|
|
53
53
|
|
|
54
54
|
1. 下载 CG 官方发行包 `CalibGuard-GUI.zip`(约 467 MB)并解压为**本包同级的 `CG_GUI/`**;
|
|
55
|
-
2. 创建 `.venv
|
|
55
|
+
2. 创建 `.venv`(**不需要你预装 Python**:本机没有 64 位 3.10–3.12 时会自动下载便携 Python 3.10.8,约 30 MB);
|
|
56
56
|
3. 按 `requirements-gui.txt` 安装依赖(约 1–3 GB);
|
|
57
57
|
4. 就绪后工具/面板自动可用。
|
|
58
58
|
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
### 前提
|
|
75
75
|
|
|
76
76
|
1. **DSH**(DeepSeek Harness)0.1.5-rc.2 实测可用;
|
|
77
|
-
2. **Python 64 位 3.10–3.12
|
|
77
|
+
2. **Python**:无需预装——没有 64 位 3.10–3.12 时安装器会自动下载便携版(缓存在 `~/.dsh/calibguard-tools`,可用 `--no-portable-python` 关闭);
|
|
78
78
|
3. **CG 引擎**:自动下载,或已有可用的 `CG_GUI/`(含 `calibguard/`、`models/`、`.venv`)。
|
|
79
79
|
|
|
80
80
|
### 方式 0:桌面端「添加插件」(最省事,推荐)
|
|
@@ -85,20 +85,33 @@ DSH 桌面版 → 设置 → **添加插件**,输入框里填以下任意一
|
|
|
85
85
|
|---|---|---|
|
|
86
86
|
| **GitHub 仓库地址**(推荐) | `https://github.com/maomitgd2006/DSH_CG` | 自动拉取源码并登记到当前 profile 的 bundle |
|
|
87
87
|
| **本地目录路径** | `<你的插件目录>`(例:`D:\code\dsh-calibguard`) | 已 clone / 解压到本地时用 |
|
|
88
|
-
|
|
|
88
|
+
| **包名** | `dsh-calibguard` | ✅ **已发布到 npm**,直接输包名即可(自动装 npm 上的 latest) |
|
|
89
89
|
|
|
90
90
|
> 等价命令行(已实测:安装后 DSH 会自动把包加进 profile 的 `dsh.profile.bundles`):
|
|
91
91
|
>
|
|
92
92
|
> ```powershell
|
|
93
|
-
> dsh plugin --profile <profile> add
|
|
94
|
-
> dsh plugin --profile <profile> add
|
|
93
|
+
> dsh plugin --profile <profile> add dsh-calibguard # npm 包(推荐)
|
|
94
|
+
> dsh plugin --profile <profile> add https://github.com/maomitgd2006/DSH_CG # GitHub 源码
|
|
95
|
+
> dsh plugin --profile <profile> add file:<本包绝对路径> # 本地目录
|
|
95
96
|
> ```
|
|
96
97
|
>
|
|
97
|
-
>
|
|
98
|
+
> npm 包页:<https://www.npmjs.com/package/dsh-calibguard>
|
|
98
99
|
|
|
99
100
|
**无论用哪种方式**,装好后首次运行都不需要手动装引擎——插件会自动准备(见下一节)。
|
|
100
101
|
|
|
101
|
-
### 方式 A
|
|
102
|
+
### 方式 A:命令行安装(npm 包,适合非桌面版 / 脚本化)
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
dsh plugin --profile <profile> add dsh-calibguard
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
之后同样在 profile 的 `cordis.patch.yml` 里插一行,`name` 直接写包名 `dsh-calibguard`
|
|
109
|
+
(本包自带的 `cordis.patch.yml` 会自动把插件行叠加进配置树)。
|
|
110
|
+
|
|
111
|
+
> `desktop` profile 被 Electron 独占,CLI 拒绝管理它 —— 桌面版请用「方式 0」,
|
|
112
|
+
> 或「方式 B」的 `file://` 行。
|
|
113
|
+
|
|
114
|
+
### 方式 B:源码方式(`file://` 行,适合本地开发 / 离线)
|
|
102
115
|
|
|
103
116
|
在 profile 的 `cordis.patch.yml` 末尾追加(路径改成你的实际位置):
|
|
104
117
|
|
|
@@ -118,14 +131,11 @@ DSH 桌面版 → 设置 → **添加插件**,输入框里填以下任意一
|
|
|
118
131
|
可选:`node scripts/link-dsh.mjs` 把 DSH 自带的 `@deepseek-ai/*` 以目录联接挂到本包,让
|
|
119
132
|
`defineTool` / Config 结构校验这两个可选能力生效(不做也能跑)。
|
|
120
133
|
|
|
121
|
-
### 方式
|
|
122
|
-
|
|
123
|
-
```powershell
|
|
124
|
-
dsh plugin --profile <profile> add file:<本包绝对路径>
|
|
125
|
-
```
|
|
134
|
+
### 方式 C:发行包(离线 / 不想用 npm)
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
从 [Releases](https://github.com/maomitgd2006/DSH_CG/releases/latest) 下载
|
|
137
|
+
`dsh-calibguard-<版本>.zip`(约 100 KB,不含 `test/`),解压到一个固定目录,然后按「方式 B」
|
|
138
|
+
把 `name` 指向解压目录下的 `lib/index.js` 即可。
|
|
129
139
|
|
|
130
140
|
### 验证
|
|
131
141
|
|
|
@@ -204,7 +214,7 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
|
|
|
204
214
|
| `device` | `auto` | 自动做「CUDA 版本 + 算力」双检,低算力显卡回落 CPU |
|
|
205
215
|
| `credentialRef` | `''` | 推荐的云端复核凭据引用名,如 `DEEPSEEK_API_KEY`(交给 DSH 凭据库解析) |
|
|
206
216
|
| `announceToModel` | `true` | 是否把「何时该用 CalibGuard」写进系统提示词 |
|
|
207
|
-
| `autoSetup` | `true` | 首次运行缺 `.venv`
|
|
217
|
+
| `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**下载引擎、按需下载便携 Python、安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
|
|
208
218
|
| `maxFiles` | `20` | 目录展开上限;显式给出/上传的文件不受限 |
|
|
209
219
|
|
|
210
220
|
---
|
|
@@ -234,7 +244,8 @@ DSH_CG/
|
|
|
234
244
|
├── bridge/ Python 桥接:常驻子进程,行式 JSON 协议,复用引擎
|
|
235
245
|
├── cordis.patch.yml 组合包 patch(把插件行插进 profile 配置树)
|
|
236
246
|
├── scripts/ link-dsh(开发期挂包)、bridge-selftest(引擎自检)
|
|
237
|
-
├── test/ node --test(
|
|
247
|
+
├── test/ node --test(95 个用例;缺引擎环境时自动 skip)
|
|
248
|
+
├── .github/workflows/ publish.yml:npm 可信发布(OIDC,可选)
|
|
238
249
|
└── docs/ 配置全表、性能、排错、插件契约、实现决策
|
|
239
250
|
```
|
|
240
251
|
|
package/docs/configuration.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| `device` | `auto` | `auto` 做「CUDA 版本 + 算力」双检;`cpu` / `cuda` 为强制 |
|
|
13
13
|
| `useGuiSettings` | `true` | 复用 `CG_GUI/gui-settings.json`(与桌面壳共享阈值/云端配置) |
|
|
14
14
|
| `announceToModel` | `true` | 是否把「何时该用 CalibGuard」写进系统提示词 |
|
|
15
|
-
| `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**下载引擎(467 MB
|
|
15
|
+
| `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**下载引擎(467 MB)、按需下载便携 Python 3.10.8(约 30 MB,存 `~/.dsh/calibguard-tools`)并安装依赖(1–3 GB);面板显示进度。设 `false` 则只报错并提示手动 `node setup.mjs` |
|
|
16
16
|
| `maxFiles` | `20` | **目录展开上限**(1–200):只限制"给一个目录时递归展开多少个文件";显式给出/上传的文件全部审计 |
|
|
17
17
|
| `auditTimeoutMs` | `600000` | 单次审计超时(毫秒) |
|
|
18
18
|
| `idleShutdownMs` | `600000` | 空闲多久回收桥接进程(`0`=常驻不回收) |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| `凭据引用 X 未配置` 告警 | `credentialRef` 指向的引用在 DSH 凭据库里没有值;在 Models 页填 Key 或用 `llmApiKey` |
|
|
12
12
|
| **首次运行卡在"正在准备环境"** | 正常:首次自动下载引擎(467 MB)+ 装依赖(1–3 GB),约 10–20 分钟;面板顶部有进度与输出尾巴 |
|
|
13
13
|
| 环境准备失败 | 面板会显示失败原因与最后几行输出;在本插件目录跑 `node setup.mjs` 看完整输出后重试(也支持 `--from` 离线包) |
|
|
14
|
-
|
|
|
14
|
+
| **本机没有 Python** | 会自动用 uv 下载便携 Python 3.10.8(约 30 MB,缓存 `~/.dsh/calibguard-tools`);若这一步失败会自动给出 python.org 下载链接,也可用 `--python` 指定解释器 |
|
|
15
15
|
| 大文件审计几十秒 | 正常:耗时 ≈ 窗口数 ÷ CPU 吞吐,见 [performance.md](performance.md) |
|
|
16
16
|
| **侧边栏没有 CalibGuard 图标** | 客户端 bundle 在**宿主启动时**扫描:改完 `dsh.client`/`lib/client.js` 必须**重启 DSH**;并确认 `package.json` 的 `name` 与 bundle `id` 一致 |
|
|
17
17
|
| **面板空白 / 拖拽后报错** | 先看宿主日志里 `[calibguard]` 开头的 warning;路由是否注册可试 `GET /api/calibguard/status` |
|
package/package.json
CHANGED
package/setup.mjs
CHANGED
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
*
|
|
19
19
|
* 无第三方依赖,只用 Node 内置模块;下载走 Node 20+ 的 fetch。
|
|
20
20
|
*/
|
|
21
|
-
import { createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
21
|
+
import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
22
22
|
import { dirname, join, resolve } from 'node:path'
|
|
23
23
|
import { spawn, spawnSync } from 'node:child_process'
|
|
24
24
|
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
25
26
|
import { Readable } from 'node:stream'
|
|
26
27
|
|
|
27
28
|
const PKG_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
@@ -84,6 +85,8 @@ function parseArgs(argv) {
|
|
|
84
85
|
skipInstall: false,
|
|
85
86
|
skipVerify: false,
|
|
86
87
|
force: false,
|
|
88
|
+
noPortablePython: false,
|
|
89
|
+
forcePortablePython: false,
|
|
87
90
|
help: false,
|
|
88
91
|
}
|
|
89
92
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -104,6 +107,8 @@ function parseArgs(argv) {
|
|
|
104
107
|
case '--skip-install': options.skipInstall = true; break
|
|
105
108
|
case '--skip-verify': options.skipVerify = true; break
|
|
106
109
|
case '--force': options.force = true; break
|
|
110
|
+
case '--no-portable-python': options.noPortablePython = true; break
|
|
111
|
+
case '--force-portable-python': options.forcePortablePython = true; break
|
|
107
112
|
default: fail(`未知参数:${arg}(用 --help 查看用法)`)
|
|
108
113
|
}
|
|
109
114
|
}
|
|
@@ -126,6 +131,8 @@ function printHelp() {
|
|
|
126
131
|
--skip-install 跳过 pip install
|
|
127
132
|
--skip-verify 跳过最后的桥接自检
|
|
128
133
|
--force 覆盖已有引擎目录 / 重建 venv
|
|
134
|
+
--no-portable-python 本机没有 Python 时不自动下载便携版(默认会自动下载)
|
|
135
|
+
--force-portable-python 跳过本机 Python 检测,强制用便携版(测试用)
|
|
129
136
|
-h, --help 显示本帮助
|
|
130
137
|
|
|
131
138
|
示例:
|
|
@@ -135,6 +142,7 @@ function printHelp() {
|
|
|
135
142
|
|
|
136
143
|
说明:
|
|
137
144
|
• 引擎约 480 MB(含 475 MB 模型权重),依赖安装另需约 1–3 GB 磁盘;
|
|
145
|
+
• 本机没有 64 位 Python 3.10–3.12 时,会自动用 uv 下载便携 Python 3.10.8(约 30 MB);
|
|
138
146
|
• 默认用 PyPI 的 CPU 版 torch 即可(低算力显卡本来也会被定档到 CPU);
|
|
139
147
|
要用 GPU 请按 PyTorch 官网自行安装对应 CUDA 轮子。
|
|
140
148
|
`)
|
|
@@ -294,8 +302,146 @@ function findPython(explicit) {
|
|
|
294
302
|
+ `请安装 64 位 Python 3.12 后重试,或用 --python 指定解释器(如 --python "C:\\Python312\\python.exe")。`)
|
|
295
303
|
}
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
|
|
305
|
+
// ============================== 便携 Python(本机没有时用 uv 自动装) ==============================
|
|
306
|
+
|
|
307
|
+
/** 自动下载的便携 Python 版本(与 CG_GUI launcher 的 PYTHON_PIN 一致)。 */
|
|
308
|
+
const PYTHON_PIN = '3.10.8'
|
|
309
|
+
|
|
310
|
+
/** uv 与便携 Python 的存放目录:放在 DSH 主目录下,避免污染仓库;删引擎目录不影响它。 */
|
|
311
|
+
function toolsDir() {
|
|
312
|
+
const dshHome = process.env.DSH_HOME && process.env.DSH_HOME.trim()
|
|
313
|
+
? resolve(process.env.DSH_HOME.trim())
|
|
314
|
+
: join(homedir(), '.dsh')
|
|
315
|
+
return join(dshHome, 'calibguard-tools')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 当前平台对应的 uv 发行包名;不支持返回空串。 */
|
|
319
|
+
function uvAssetName() {
|
|
320
|
+
const { platform, arch } = process
|
|
321
|
+
if (platform === 'win32') {
|
|
322
|
+
if (arch === 'x64') return 'uv-x86_64-pc-windows-msvc.zip'
|
|
323
|
+
if (arch === 'arm64') return 'uv-aarch64-pc-windows-msvc.zip'
|
|
324
|
+
} else if (platform === 'linux') {
|
|
325
|
+
if (arch === 'x64') return 'uv-x86_64-unknown-linux-gnu.tar.gz'
|
|
326
|
+
if (arch === 'arm64') return 'uv-aarch64-unknown-linux-gnu.tar.gz'
|
|
327
|
+
} else if (platform === 'darwin') {
|
|
328
|
+
if (arch === 'arm64') return 'uv-aarch64-apple-darwin.tar.gz'
|
|
329
|
+
if (arch === 'x64') return 'uv-x86_64-apple-darwin.tar.gz'
|
|
330
|
+
}
|
|
331
|
+
return ''
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function findBinary(dir, name) {
|
|
335
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
336
|
+
const full = join(dir, entry.name)
|
|
337
|
+
if (entry.isFile() && entry.name === name) return full
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
const nested = findBinary(full, name)
|
|
340
|
+
if (nested) return nested
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return ''
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** 拿到可执行的 uv:PATH 上已有就用;否则下载官方发行包并缓存。失败返回空串。 */
|
|
347
|
+
async function ensureUv(dest) {
|
|
348
|
+
const binaryName = process.platform === 'win32' ? 'uv.exe' : 'uv'
|
|
349
|
+
const cached = join(toolsDir(), binaryName)
|
|
350
|
+
if (existsSync(cached)) return cached
|
|
351
|
+
const probe = spawnSync('uv', ['--version'], { encoding: 'utf8' })
|
|
352
|
+
if (!probe.error && probe.status === 0) return 'uv'
|
|
353
|
+
const asset = uvAssetName()
|
|
354
|
+
if (!asset) return ''
|
|
355
|
+
const dir = toolsDir()
|
|
356
|
+
mkdirSync(dir, { recursive: true })
|
|
357
|
+
const archive = join(dir, asset)
|
|
358
|
+
const urls = [
|
|
359
|
+
`https://github.com/astral-sh/uv/releases/latest/download/${asset}`,
|
|
360
|
+
`https://ghproxy.cn/https://github.com/astral-sh/uv/releases/latest/download/${asset}`,
|
|
361
|
+
]
|
|
362
|
+
let downloaded = false
|
|
363
|
+
for (const url of urls) {
|
|
364
|
+
try {
|
|
365
|
+
log(`下载 uv(约 12 MB):${url}`)
|
|
366
|
+
await download(url, archive)
|
|
367
|
+
downloaded = true
|
|
368
|
+
break
|
|
369
|
+
} catch (error) {
|
|
370
|
+
warn(`uv 下载失败(${error.message}),换下一个源再试…`)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!downloaded) return ''
|
|
374
|
+
const unpacked = join(dir, 'uv-unpacked')
|
|
375
|
+
rmSync(unpacked, { recursive: true, force: true })
|
|
376
|
+
mkdirSync(unpacked, { recursive: true })
|
|
377
|
+
extract(archive, unpacked)
|
|
378
|
+
const found = findBinary(unpacked, binaryName)
|
|
379
|
+
if (!found) {
|
|
380
|
+
rmSync(unpacked, { recursive: true, force: true })
|
|
381
|
+
rmSync(archive, { force: true })
|
|
382
|
+
return ''
|
|
383
|
+
}
|
|
384
|
+
copyFileSync(found, cached)
|
|
385
|
+
if (process.platform !== 'win32') {
|
|
386
|
+
try { chmodSync(cached, 0o755) } catch { /* 某些文件系统不支持,忽略 */ }
|
|
387
|
+
}
|
|
388
|
+
rmSync(unpacked, { recursive: true, force: true })
|
|
389
|
+
rmSync(archive, { force: true })
|
|
390
|
+
log('✓ uv 已就绪')
|
|
391
|
+
return cached
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 用 uv 下载便携 Python 并创建带 pip 的 venv;成功返回 true。 */
|
|
395
|
+
async function installPortablePython(uv, dest) {
|
|
396
|
+
const dir = toolsDir()
|
|
397
|
+
const pythonDir = join(dir, 'python')
|
|
398
|
+
mkdirSync(pythonDir, { recursive: true })
|
|
399
|
+
const baseEnv = {
|
|
400
|
+
...process.env,
|
|
401
|
+
UV_PYTHON_INSTALL_DIR: pythonDir,
|
|
402
|
+
UV_NO_MODIFY_PATH: '1',
|
|
403
|
+
PYTHONIOENCODING: 'utf-8',
|
|
404
|
+
}
|
|
405
|
+
const attempts = [
|
|
406
|
+
{ label: '直连', env: baseEnv },
|
|
407
|
+
{
|
|
408
|
+
label: '镜像 ghproxy',
|
|
409
|
+
env: {
|
|
410
|
+
...baseEnv,
|
|
411
|
+
UV_PYTHON_INSTALL_MIRROR: 'https://ghproxy.cn/https://github.com/astral-sh/python-build-standalone/releases/download',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
]
|
|
415
|
+
let installed = false
|
|
416
|
+
for (const attempt of attempts) {
|
|
417
|
+
log(`uv python install ${PYTHON_PIN}(${attempt.label},约 30 MB)…`)
|
|
418
|
+
const result = spawnSync(uv, ['python', 'install', PYTHON_PIN], {
|
|
419
|
+
encoding: 'utf8',
|
|
420
|
+
env: attempt.env,
|
|
421
|
+
timeout: 15 * 60_000,
|
|
422
|
+
})
|
|
423
|
+
if (result.status === 0) { installed = true; break }
|
|
424
|
+
const tail = String(result.stderr || result.stdout || '').trim().split('\n').slice(-2).join(' / ')
|
|
425
|
+
warn(`失败:${tail || `exit ${result.status ?? 'error'}`}`)
|
|
426
|
+
}
|
|
427
|
+
if (!installed) return false
|
|
428
|
+
log('uv venv --seed(创建 .venv)…')
|
|
429
|
+
const venvDir = join(dest, '.venv')
|
|
430
|
+
rmSync(venvDir, { recursive: true, force: true })
|
|
431
|
+
const venv = spawnSync(uv, ['venv', '--seed', '--python', PYTHON_PIN, venvDir], {
|
|
432
|
+
encoding: 'utf8',
|
|
433
|
+
env: baseEnv,
|
|
434
|
+
timeout: 10 * 60_000,
|
|
435
|
+
})
|
|
436
|
+
if (venv.status !== 0) {
|
|
437
|
+
const tail = String(venv.stderr || venv.stdout || '').trim().split('\n').slice(-2).join(' / ')
|
|
438
|
+
warn(`uv venv 失败:${tail}`)
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
return existsSync(venvPython(dest))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function venvPython(dest) { return process.platform === 'win32'
|
|
299
445
|
? join(dest, '.venv', 'Scripts', 'python.exe')
|
|
300
446
|
: join(dest, '.venv', 'bin', 'python')
|
|
301
447
|
}
|
|
@@ -381,9 +527,27 @@ async function main() {
|
|
|
381
527
|
log('\n跳过创建虚拟环境(--skip-venv)')
|
|
382
528
|
} else {
|
|
383
529
|
step('创建 Python 虚拟环境')
|
|
384
|
-
const found = findPython(options.python)
|
|
385
|
-
|
|
386
|
-
|
|
530
|
+
const found = options.forcePortablePython ? null : findPython(options.python)
|
|
531
|
+
if (found) {
|
|
532
|
+
log(`使用本机 Python:${found.label}(${found.version})`)
|
|
533
|
+
run(found.cmd, [...found.args, '-m', 'venv', join(options.dest, '.venv')])
|
|
534
|
+
} else if (options.noPortablePython) {
|
|
535
|
+
fail('本机没有 64 位 Python 3.10–3.12,且已指定 --no-portable-python。'
|
|
536
|
+
+ '请安装后再试:https://www.python.org/downloads/(或用 --python 指定解释器)')
|
|
537
|
+
} else {
|
|
538
|
+
log('本机没有可用的 64 位 Python 3.10–3.12,自动下载便携版(首次约 30 MB)…')
|
|
539
|
+
const uv = await ensureUv(options.dest)
|
|
540
|
+
if (!uv) {
|
|
541
|
+
fail('自动获取 uv 失败,无法下载便携 Python。请手动安装 64 位 Python 3.12 后重试:'
|
|
542
|
+
+ 'https://www.python.org/downloads/')
|
|
543
|
+
}
|
|
544
|
+
const ok = await installPortablePython(uv, options.dest)
|
|
545
|
+
if (!ok) {
|
|
546
|
+
fail('自动安装便携 Python 失败。请手动安装 64 位 Python 3.12 后重试:'
|
|
547
|
+
+ 'https://www.python.org/downloads/')
|
|
548
|
+
}
|
|
549
|
+
log(`✓ 便携 Python ${PYTHON_PIN} 已就绪(${join(toolsDir(), 'python')})`)
|
|
550
|
+
}
|
|
387
551
|
if (!existsSync(python)) fail(`虚拟环境创建后找不到解释器:${python}`)
|
|
388
552
|
log('✓ 虚拟环境已创建')
|
|
389
553
|
}
|