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 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` (requires **64-bit Python 3.10–3.12**; it fails with a clear explanation otherwise);
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 Python 3.10–3.12** (needed for the one-time dependency install; irrelevant if `.venv` already exists);
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** (latest v0.2.1); just enter the package name |
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 https://github.com/maomitgd2006/DSH_CG
103
- > dsh plugin --profile <profile> add file:<absolute path to this package>
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
- > Published on npm: <https://www.npmjs.com/package/dsh-calibguard> (`npm i dsh-calibguard` also works).
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 — source mode (a `file://` loader row, no install)
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 B — install into a profile
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
- Then insert the loader row as above, using the package name `dsh-calibguard` as `name`.
137
- (The `desktop` profile is managed exclusively by the Electron app — desktop users must use Option A.)
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 and install Python deps when `.venv` is missing; set `false` to only fail with instructions |
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 (87 cases; auto-skip when the engine env is missing)
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`(需要 **64 位 Python 3.10–3.12**,没有会明确报错说明原因);
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**(首次自动安装依赖需要;已装好 `.venv` 的机器可忽略);
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
- | 包名 | `dsh-calibguard` | ✅ **已发布到 npm**(最新 v0.2.1);直接输包名即可 |
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 https://github.com/maomitgd2006/DSH_CG
94
- > dsh plugin --profile <profile> add file:<本包绝对路径>
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
- > 包名已发布:<https://www.npmjs.com/package/dsh-calibguard>(`npm i dsh-calibguard` 亦可)。
98
+ > npm 包页:<https://www.npmjs.com/package/dsh-calibguard>
98
99
 
99
100
  **无论用哪种方式**,装好后首次运行都不需要手动装引擎——插件会自动准备(见下一节)。
100
101
 
101
- ### 方式 A:源码方式(`file://` 行,无需安装)
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
- ### 方式 B:安装进 profile
122
-
123
- ```powershell
124
- dsh plugin --profile <profile> add file:<本包绝对路径>
125
- ```
134
+ ### 方式 C:发行包(离线 / 不想用 npm)
126
135
 
127
- 之后同样在 patch 里插一行,`name` 可以直接写包名 `dsh-calibguard`。
128
- (`desktop` profile 被 Electron 独占,CLI 拒绝管理,桌面版只能走方式 A。)
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` 时**自动**下载引擎并安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
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(87 个用例;缺引擎环境时自动 skip)
247
+ ├── test/ node --test(95 个用例;缺引擎环境时自动 skip)
248
+ ├── .github/workflows/ publish.yml:npm 可信发布(OIDC,可选)
238
249
  └── docs/ 配置全表、性能、排错、插件契约、实现决策
239
250
  ```
240
251
 
@@ -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)并安装依赖(1–3 GB);面板显示进度。设 `false` 则只报错并提示手动 `node setup.mjs` |
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`=常驻不回收) |
@@ -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
- | 首次审计很久 | 正常:加载约 475 MB 权重;之后走常驻进程 |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calibguard",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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
- function venvPython(dest) {
298
- return process.platform === 'win32'
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
- log(`使用 Python:${found.label}(${found.version})`)
386
- run(found.cmd, [...found.args, '-m', 'venv', join(options.dest, '.venv')])
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
  }