dsh-calibguard 0.2.2 → 0.2.4

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)
@@ -215,7 +215,7 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
215
215
  | `device` | `auto` | Checks both CUDA build and compute capability; low-end GPUs fall back to CPU |
216
216
  | `credentialRef` | `''` | Recommended credential reference name, e.g. `DEEPSEEK_API_KEY` (resolved via the DSH credential store) |
217
217
  | `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 |
218
+ | `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
219
  | `maxFiles` | `20` | Directory recursion cap; explicitly given/uploaded files are not capped |
220
220
 
221
221
  ---
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:桌面端「添加插件」(最省事,推荐)
@@ -204,7 +204,7 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
204
204
  | `device` | `auto` | 自动做「CUDA 版本 + 算力」双检,低算力显卡回落 CPU |
205
205
  | `credentialRef` | `''` | 推荐的云端复核凭据引用名,如 `DEEPSEEK_API_KEY`(交给 DSH 凭据库解析) |
206
206
  | `announceToModel` | `true` | 是否把「何时该用 CalibGuard」写进系统提示词 |
207
- | `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**下载引擎并安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
207
+ | `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**下载引擎、按需下载便携 Python、安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
208
208
  | `maxFiles` | `20` | 目录展开上限;显式给出/上传的文件不受限 |
209
209
 
210
210
  ---
@@ -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.2",
3
+ "version": "0.2.4",
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
  }