dsh-calibguard 0.3.3 → 0.3.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
@@ -4,7 +4,7 @@ English | [中文](README.md)
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-green)
6
6
  ![node](https://img.shields.io/badge/node-%E2%89%A520-blue)
7
- ![tests](https://img.shields.io/badge/tests-95%20passed-brightgreen)
7
+ ![tests](https://img.shields.io/badge/tests-103%20passed-brightgreen)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-calibguard.svg)](https://www.npmjs.com/package/dsh-calibguard)
9
9
 
10
10
  Plugs the [CalibGuard](https://github.com/P-436/CG) file-security auditing engine into
@@ -77,6 +77,19 @@ mirrors, and pip tries Tsinghua → Aliyun → USTC → Tencent → PyPI in orde
77
77
  the installer pulls the **CUDA (cu128) build of torch**; otherwise it installs the CPU build (GTX 10 series is
78
78
  SM 6.1 and has no kernels in the cu128 wheels). The panel header shows the inference device and the reason.
79
79
 
80
+ Manual override (optional): `node setup.mjs --force-cuda` (force the CUDA build, skipping the CUDA/compute
81
+ checks — use at your own risk) or `node setup.mjs --cpu` (force CPU), or the env vars
82
+ `DSH_CALIBGUARD_FORCE_CUDA=1` / `DSH_CALIBGUARD_CPU_ONLY=1`. Switch later with
83
+ `node setup.mjs --reinstall-torch --force-cuda` (or `--cpu`).
84
+
85
+ > The CPU and CUDA builds of torch **cannot coexist in one venv** (same package name; installing one replaces
86
+ > the other). The CUDA build is a full torch and falls back to CPU when no GPU is available, so coexistence is
87
+ > unnecessary.
88
+ >
89
+ > **Machines that already have the CPU build**: with ≥0.3.5 the panel header detects "NVIDIA … but torch is the
90
+ > CPU build" and offers a **one-click "Reinstall GPU torch (~2.5 GB)"** button (uninstalls the CPU build, installs
91
+ > CUDA, then restarts the bridge). CLI equivalent: `node "<plugin dir>/setup.mjs" --reinstall-torch --force-cuda`.
92
+
80
93
  **Where to watch progress**: the panel shows a "First run: preparing engine and dependencies" card with
81
94
  the current step and the tail of the installer output; host logs show `[calibguard]` lines. The audit
82
95
  button stays disabled until it is ready — press "刷新状态 / Refresh" afterwards. Expect **10–20 minutes**
@@ -268,6 +281,7 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
268
281
  | **Where are audit results saved / how to clear them** | In the engine's `reports/` directory (default `<engineRoot>/reports`): JSON + Markdown per audit. The panel shows the path and has a "clear results" button; AI tools return `report_md_path` / `report_json_path` |
269
282
  | **"First audit will load the model" hint shows every time** | Since 0.3.3 it is only shown while the model is not loaded yet; afterwards the button just says "auditing…" and the header shows "model loaded" |
270
283
  | **NVIDIA GPU (e.g. RTX 2060) but the engine keeps using CPU** | The installed torch is the CPU build (old installer behavior). Upgrade to ≥0.3.3 (the panel will tell you) and run `node setup.mjs --reinstall-torch` (~2.5 GB) in the plugin directory, then restart DSH |
284
+ | **Want to force the GPU / CPU torch build** | Add `--force-cuda` / `--cpu` at install time (combine with `--reinstall-torch` to replace an existing build); the runtime device is controlled by the plugin config `device` (auto/cpu/cuda); `cuda` requires the CUDA build |
271
285
  | A legitimate third-party library is BLOCKed | Minified libraries are full of eval/base64 patterns and the corpus is Python-heavy — known false-positive tendency, see [docs/troubleshooting.md](docs/troubleshooting.md) |
272
286
  | Panel returns 401/403 | Browser session isn't authenticated with the host: refresh / sign in again |
273
287
  | The AI ignores changed settings | Press Save in the panel; editing fields without saving applies to that run only |
@@ -285,7 +299,7 @@ dsh-CalibGuard/
285
299
  ├── engine/ Bundled engine core copied from CG_GUI (code / tokenizer / calibration, no weights)
286
300
  ├── cordis.patch.yml Bundle patch (inserts the loader row)
287
301
  ├── scripts/ link-dsh (dev-time junction), bridge-selftest (engine self-check)
288
- ├── test/ node --test (95 cases; auto-skip when the engine env is missing)
302
+ ├── test/ node --test (103 cases; auto-skip when the engine env is missing)
289
303
  ├── .github/workflows/ publish.yml: npm trusted publishing (OIDC, optional)
290
304
  └── docs/ configuration, performance, troubleshooting, plugin contract, design notes
291
305
  ```
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-green)
6
6
  ![node](https://img.shields.io/badge/node-%E2%89%A520-blue)
7
- ![tests](https://img.shields.io/badge/tests-95%20passed-brightgreen)
7
+ ![tests](https://img.shields.io/badge/tests-103%20passed-brightgreen)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-calibguard.svg)](https://www.npmjs.com/package/dsh-calibguard)
9
9
 
10
10
  把 [CalibGuard](https://github.com/P-436/CG) 的文件安全审计引擎接进 **DeepSeek Harness(DSH)**:
@@ -70,6 +70,19 @@ GitHub 直连失败会自动换 gh-proxy / ghfast / ghproxy 镜像;pip 依赖
70
70
  **CUDA(cu128)版 torch**;否则装 CPU 版(GTX 10 系算力 6.1,cu128 轮子里没有对应 kernel,只能 CPU)。
71
71
  面板顶部会显示「推理设备:cpu/cuda」和定档原因。
72
72
 
73
+ 手动覆盖(可选):
74
+ `node setup.mjs --force-cuda`(强制装 CUDA 版,跳过 CUDA/算力检查,风险自负)、
75
+ `node setup.mjs --cpu`(强制 CPU 版),或环境变量 `DSH_CALIBGUARD_FORCE_CUDA=1` /
76
+ `DSH_CALIBGUARD_CPU_ONLY=1`。装完想切换:`node setup.mjs --reinstall-torch --force-cuda` 或
77
+ `--reinstall-torch --cpu`。
78
+
79
+ > CPU 版与 CUDA 版 torch **不能同时存在于同一个 venv**(同名包会互相覆盖);
80
+ > 但 CUDA 版 torch 本身就是完整 torch,没有 GPU 时会自动用 CPU,所以不需要共存。
81
+ >
82
+ > **已经有 CPU 版的机器**:升级到 ≥0.3.5 后,面板顶部会显示「检测到 NVIDIA … 但当前 torch 是 CPU 版」
83
+ > 并提供 **「一键重装 GPU 版 torch(约 2.5 GB)」** 按钮(会先卸载 CPU 版再装 CUDA 版,完成后自动重启桥接);
84
+ > 也可以用命令行:`node "<插件目录>/setup.mjs" --reinstall-torch --force-cuda`。
85
+
73
86
  **进度在哪看**:面板顶部会出现「首次运行:正在自动准备引擎与依赖」卡片(含当前步骤与输出尾巴);
74
87
  宿主日志里是 `[calibguard]` 开头的行。准备期间审计按钮不可用,完成后点「刷新状态」即可。
75
88
  首次约需 **10–20 分钟**(取决于网速和 pip 源),只需一次。
@@ -253,6 +266,7 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
253
266
  | **审计结果保存在哪 / 想清空** | 保存在引擎 `reports/` 目录(默认 `<engineRoot>/reports`),每次审计生成 JSON + Markdown;面板结果区显示路径并带「清空结果」按钮;AI 工具返回 `report_md_path` / `report_json_path` |
254
267
  | **审计时总提示"首次会加载模型"** | 0.3.3 起只在模型尚未加载时提示;加载后按钮只显示「审计中…」,顶部状态显示「模型 已加载」 |
255
268
  | **有 NVIDIA 显卡(如 RTX 2060)但一直显示 CPU** | 说明装的是 CPU 版 torch(旧版安装器行为)。升级到 ≥0.3.3 后面板会给出提示;在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装上 CUDA 版即可,之后重启 DSH |
269
+ | **想强制用 GPU / CPU 版 torch** | 安装时加 `--force-cuda` / `--cpu`(配合 `--reinstall-torch` 可覆盖已装版本);运行时设备由插件配置 `device`(auto/cpu/cuda)决定,`cuda` 需要 CUDA 版 torch |
256
270
  | 大文件审计几十秒 | 正常:全文滑窗逐窗推理,见 [docs/performance.md](docs/performance.md) |
257
271
  | 灰区全部转人工 | 没配凭据:在面板「凭据名 + API Key」保存,或设 `credentialRef`、或在 DSH Models 页配置 |
258
272
  | 正规第三方库被判 BLOCK | 压缩库充满 eval/base64 等特征,模型语料偏 Python 生态 → 属已知误报倾向,见 [docs/troubleshooting.md](docs/troubleshooting.md) |
@@ -272,7 +286,7 @@ dsh-CalibGuard/
272
286
  ├── engine/ 从 CG_GUI 复制来的引擎核心(代码/词表/校准参数,不含 475 MB 权重)
273
287
  ├── cordis.patch.yml 组合包 patch(把插件行插进 profile 配置树)
274
288
  ├── scripts/ link-dsh(开发期挂包)、bridge-selftest(引擎自检)
275
- ├── test/ node --test(95 个用例;缺引擎环境时自动 skip)
289
+ ├── test/ node --test(103 个用例;缺引擎环境时自动 skip)
276
290
  ├── .github/workflows/ publish.yml:npm 可信发布(OIDC,可选)
277
291
  └── docs/ 配置全表、性能、排错、插件契约、实现决策
278
292
  ```
@@ -30,6 +30,9 @@
30
30
 
31
31
  > **GPU 定档**:安装器检测到 NVIDIA 且 CUDA ≥ 12.8、算力 ≥ 7.5 时自动装 CUDA(cu128)版 torch,
32
32
  > 否则装 CPU 版。已经装成 CPU 版时:在插件目录运行 `node setup.mjs --reinstall-torch`。
33
+ > 手动覆盖:`--force-cuda` / `--cpu`(或环境变量)决定**安装哪个 build**(面板顶部也有「一键重装 GPU 版 torch」按钮,走 `POST /install-torch`);插件配置 `device`
34
+ > (auto/cpu/cuda)决定**运行时用哪个设备**。CPU 版与 CUDA 版 torch 不能共存于同一个 venv
35
+ >(同名包互斥);CUDA 版在无 GPU 时也能跑 CPU。
33
36
 
34
37
  ## 二、参数优先级
35
38
 
@@ -70,6 +73,7 @@
70
73
  |---|---|
71
74
  | `DSH_CALIBGUARD_PYTHON` | 指定桥接进程使用的 Python 解释器(优先于 `<engineRoot>/.venv`) |
72
75
  | `DSH_CALIBGUARD_CPU_ONLY` | 设为 `1` 强制使用 CPU 版 torch(跳过 GPU 定档) |
76
+ | `DSH_CALIBGUARD_FORCE_CUDA` | 设为 `1` 强制安装 CUDA 版 torch(跳过 CUDA 版本/算力检查,风险自负) |
73
77
  | `DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` | 大文件并发分段下载的连接数(默认 4;模型/引擎包下载提速) |
74
78
  | `DSH_CALIBGUARD_TORCH_INDEX` | CUDA 版 torch 的安装索引(默认 `https://download.pytorch.org/whl/cu128`) |
75
79
  | `DSH_CALIBGUARD_PIP_INDEX` | 指定 pip 主索引(默认依次尝试 清华 → 阿里 → 中科大 → 腾讯 → 官方) |
@@ -13,6 +13,8 @@
13
13
  | **审计结果在哪里 / 怎么清空** | 引擎 `reports/` 目录(默认 `<engineRoot>/reports`):每次审计生成 JSON + Markdown;面板结果区显示路径并有「清空结果」,工具返回 `report_md_path` / `report_json_path` |
14
14
  | **每次审计都提示"首次会加载模型"** | 0.3.3 起仅在模型未加载时提示;加载后不再出现,顶部状态显示「模型 已加载」 |
15
15
  | **有 NVIDIA 显卡但引擎显示 CPU** | 装的是 CPU 版 torch。升级到 ≥0.3.3(面板顶部会提示),然后在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装 CUDA 版;装完重启 DSH。`DSH_CALIBGUARD_CPU_ONLY=1` 可反向强制 CPU |
16
+ | **已经装了 CPU 版 torch,想换成 GPU 版** | 升级到 ≥0.3.5:面板顶部点「一键重装 GPU 版 torch(约 2.5 GB)」(先卸载再装,完成自动重启桥接);或命令行 `node "<插件目录>/setup.mjs" --reinstall-torch --force-cuda` |
17
+ | **想强制装 GPU / CPU 版 torch** | 安装时加 `--force-cuda` / `--cpu`(可配合 `--reinstall-torch` 覆盖已装版本);CPU 版与 CUDA 版不能共存于同一 venv,CUDA 版在无 GPU 时会自动回落 CPU |
16
18
  | **首次安装特别慢 / 提示某个源不可用** | 模型默认 4 路并发分段下载(`DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` 可调),GitHub 失败自动换 gh-proxy/ghfast/ghproxy 镜像;pip 自动在 清华/阿里/中科大/腾讯/官方 之间回退。实在慢可手动下载 `calibguard-model-1.0.0.zip` 放到缓存目录(见下一条) |
17
19
  | **模型下载失败 / 想离线** | 手动下载 `calibguard-model-1.0.0.zip`(Release `model-v1.0.0`),解压出 `model.safetensors` 放到 `~/.dsh/calibguard-tools/model-cache/model.safetensors`(或 `<engineRoot>/models/codebert_finetuned/`),重跑安装即会校验 SHA 并跳过下载 |
18
20
  | **首次运行卡在"正在准备环境"** | 正常:首次下载模型(约 442 MB,自动镜像回退)+ 装依赖(1–3 GB),约 10–20 分钟;面板顶部有进度与输出尾巴。若长时间无进度,可看 `~/.dsh/calibguard-tools/model-cache` 是否在增长 |
package/lib/client.js CHANGED
@@ -345,6 +345,11 @@ window.__ModuleLoader__.load({
345
345
  var credName = credNameState[0]
346
346
  var setCredName = credNameState[1]
347
347
 
348
+ // torch 重装提示(面板按钮/结果)
349
+ var torchMsgState = useState('')
350
+ var torchMsg = torchMsgState[0]
351
+ var setTorchMsg = torchMsgState[1]
352
+
348
353
  var busyState = useState(false)
349
354
  var busy = busyState[0]
350
355
  var setBusy = busyState[1]
@@ -382,6 +387,15 @@ window.__ModuleLoader__.load({
382
387
  loadSettings()
383
388
  }, [])
384
389
 
390
+ useEffect(function () {
391
+ if (!torchRunning) return undefined
392
+ var timer = setInterval(refreshStatus, 3000)
393
+ return function () { clearInterval(timer) }
394
+ }, [torchRunning])
395
+ useEffect(function () {
396
+ if (torchState === 'done') refreshStatus()
397
+ }, [torchState])
398
+
385
399
  function applyStatus(payload) {
386
400
  var data = (payload && payload.status) || {}
387
401
  var llm = data.llm || {}
@@ -554,6 +568,19 @@ window.__ModuleLoader__.load({
554
568
  })
555
569
  }
556
570
 
571
+ function installTorch(mode) {
572
+ setTorchMsg('已启动重装 torch(后台进行,面板自动刷新进度)…')
573
+ apiFetch('/install-torch', {
574
+ method: 'POST',
575
+ headers: { 'content-type': 'application/json' },
576
+ body: JSON.stringify({ mode: mode }),
577
+ }).then(function () {
578
+ refreshStatus()
579
+ }).catch(function (err) {
580
+ setTorchMsg('重装启动失败:' + String((err && err.message) || err))
581
+ })
582
+ }
583
+
557
584
  function clearResult() {
558
585
  setResult(null)
559
586
  setReport(null)
@@ -610,9 +637,15 @@ window.__ModuleLoader__.load({
610
637
  || ''
611
638
  var devicePolicy = (engineInfo && engineInfo.devicePolicy) || null
612
639
  var setupScript = (status && status.setupScript) || '<插件目录>/setup.mjs'
613
- // 有 NVIDIA 显卡、但 torch 没启用 CUDA(装的是 CPU 版)→ 提示怎么开 GPU
640
+ var torchJob = (status && status.torchJob) || null
641
+ var torchRunning = Boolean(torchJob && torchJob.state === 'running')
642
+ var torchState = (torchJob && torchJob.state) || 'idle'
643
+ var gpuInfo = (status && status.gpuTorch) || null
644
+ // 有 NVIDIA 显卡、但 torch 没启用 CUDA(装的是 CPU 版)→ 提示并允许一键重装
614
645
  var gpuTorchHint = engineInfo && engineInfo.cudaAvailable === false
615
- && devicePolicy && devicePolicy.gpuName ? devicePolicy.gpuName : ''
646
+ && ((gpuInfo && gpuInfo.gpuName) || (devicePolicy && devicePolicy.gpuName))
647
+ ? ((gpuInfo && gpuInfo.gpuName) || (devicePolicy && devicePolicy.gpuName))
648
+ : ''
616
649
  var engineReady = Boolean(engineInfo)
617
650
  var envBusy = Boolean(envInfo) && !engineReady && envInfo.state !== 'failed'
618
651
  var envFailed = Boolean(envInfo) && !engineReady && envInfo.state === 'failed'
@@ -646,13 +679,62 @@ window.__ModuleLoader__.load({
646
679
  + ' | ' + devicePolicy.reason,
647
680
  })
648
681
  : null,
649
- gpuTorchHint
650
- ? jsx('div', {
651
- style: S.note,
652
- children: '检测到 NVIDIA ' + gpuTorchHint + ',但当前 torch 是 CPU 版,审计会走 CPU。'
653
- + '要启用 GPU(RTX 20/30/40 系需算力 ≥7.5 且 CUDA ≥12.8):'
654
- + '在插件目录运行 node "' + setupScript + '" --reinstall-torch(约 2.5 GB),'
655
- + '装完重启 DSH;或重装插件到最新版会自动选 CUDA 版 torch。',
682
+ gpuTorchHint && torchState !== 'running'
683
+ ? jsxs('div', {
684
+ style: S.card,
685
+ children: [
686
+ jsx('div', { style: S.cardTitle, children: '检测到 NVIDIA ' + gpuTorchHint + ',但当前 torch 是 CPU 版' }),
687
+ jsx('div', {
688
+ style: S.muted,
689
+ children: 'CPU 版与 GPU 版 torch 不能共存于同一个 venv,重装会先卸载当前的再装 CUDA(cu128)版'
690
+ + '(约 2.5 GB,下载走 PyTorch 官方源)。装完桥接会自动重启,无需重启 DSH。',
691
+ }),
692
+ jsxs('div', {
693
+ style: S.row,
694
+ children: [
695
+ jsx('button', {
696
+ style: S.buttonPrimary,
697
+ onClick: function () { installTorch('cuda') },
698
+ children: '一键重装 GPU 版 torch(约 2.5 GB)',
699
+ }),
700
+ jsx('button', {
701
+ style: S.button,
702
+ onClick: function () { installTorch('auto') },
703
+ children: '按自动定档重装',
704
+ }),
705
+ torchMsg ? jsx('span', { style: S.muted, children: torchMsg }) : null,
706
+ ],
707
+ }),
708
+ torchState === 'done'
709
+ ? jsx('div', { style: { color: C.allow, fontSize: 12, marginTop: 6 }, children: torchMsg || 'torch 重装完成(如仍显示 CPU,点「刷新状态」)' })
710
+ : null,
711
+ torchState === 'failed'
712
+ ? jsx('div', {
713
+ style: { color: C.error, fontSize: 12, whiteSpace: 'pre-wrap', marginTop: 6 },
714
+ children: '重装失败:' + ((torchJob && (torchJob.error || torchJob.message)) || '详见日志')
715
+ + '\n手动方式:node "' + setupScript + '" --reinstall-torch --force-cuda',
716
+ })
717
+ : null,
718
+ jsx('div', {
719
+ style: S.note,
720
+ children: '不想用面板也可以手动运行:node "' + setupScript + '" --reinstall-torch --force-cuda',
721
+ }),
722
+ ],
723
+ })
724
+ : null,
725
+ torchRunning
726
+ ? jsxs('div', {
727
+ style: S.card,
728
+ children: [
729
+ jsx('div', { style: S.cardTitle, children: '正在重装 torch…' }),
730
+ jsx('div', { style: S.muted, children: (torchJob && (torchJob.step || torchJob.message)) || '准备中…' }),
731
+ torchJob && torchJob.tail && torchJob.tail.length > 0
732
+ ? jsx('pre', {
733
+ style: Object.assign({}, S.pre, { maxHeight: 140, marginTop: 8 }),
734
+ children: torchJob.tail.slice(-5).join('\n'),
735
+ })
736
+ : null,
737
+ ],
656
738
  })
657
739
  : null,
658
740
 
package/lib/engine.js CHANGED
@@ -32,6 +32,8 @@ import { EnvironmentBootstrap, venvPythonPath } from './bootstrap.js'
32
32
 
33
33
  /** 桥接脚本相对本文件的位置(spawn 需要真实路径,不能直接把 URL 对象交给子进程)。 */
34
34
  const BRIDGE_SCRIPT = fileURLToPath(new URL('../bridge/calibguard_bridge.py', import.meta.url))
35
+ /** 安装器脚本(面板「重装 GPU 版 torch」会调用它)。 */
36
+ const SETUP_SCRIPT = fileURLToPath(new URL('../setup.mjs', import.meta.url))
35
37
 
36
38
  /** 单次 status 请求的默认超时(毫秒):只做文件探测与 torch 导入,很快。 */
37
39
  const STATUS_TIMEOUT_MS = 60_000
@@ -71,6 +73,9 @@ export class CalibGuardEngine {
71
73
  this.statusCache = null
72
74
  /** 最近一次凭据解析结果(只用于诊断,不缓存值给下次复用)。 */
73
75
  this.lastCredential = { source: 'none', configured: false }
76
+ /** 面板「重装 torch」任务状态(CPU 版 → GPU 版一键替换用)。 */
77
+ this.torchJob = { state: 'idle', step: '', message: '', tail: [], startedAt: null, finishedAt: null, error: null }
78
+ this.torchChild = null
74
79
  /**
75
80
  * 首次运行自动准备环境(对齐 CG_GUI 的 launcher 首启装环境):
76
81
  * 缺 `<engineRoot>/.venv` 时在后台跑 setup.mjs(内置引擎 + npm 模型分片 → 建 venv → 装依赖)。
@@ -156,6 +161,9 @@ export class CalibGuardEngine {
156
161
  const ref = this.effectiveCredentialRef()
157
162
  if (!ref) {
158
163
  this.lastCredential = { source: 'none', configured: false }
164
+ /** 面板「重装 torch」任务状态(CPU 版 → GPU 版一键替换用)。 */
165
+ this.torchJob = { state: 'idle', step: '', message: '', tail: [], startedAt: null, finishedAt: null, error: null }
166
+ this.torchChild = null
159
167
  return { key: undefined, source: 'none' }
160
168
  }
161
169
 
@@ -274,6 +282,121 @@ export class CalibGuardEngine {
274
282
  )
275
283
  }
276
284
 
285
+ /**
286
+ * GPU/CPU torch 状态(面板据此决定是否显示「重装 GPU 版」)。
287
+ * 数据来自最近一次 status 快照:gpuName 仅当检测到 NVIDIA;cudaAvailable=torch 是否启用 CUDA。
288
+ */
289
+ gpuTorchStatus() {
290
+ const status = this.statusCache
291
+ if (!status) return { gpuName: null, cudaAvailable: null, device: null, policyMode: null, reason: '' }
292
+ const policy = status.devicePolicy || {}
293
+ return {
294
+ gpuName: policy.gpuName ?? null,
295
+ cudaAvailable: status.cudaAvailable ?? null,
296
+ device: status.device ?? null,
297
+ policyMode: policy.mode ?? null,
298
+ reason: policy.reason ?? '',
299
+ }
300
+ }
301
+
302
+ /** 重装 torch 任务快照。 */
303
+ torchJobSnapshot() {
304
+ const job = this.torchJob
305
+ return {
306
+ state: job.state,
307
+ step: job.step,
308
+ message: job.message,
309
+ startedAt: job.startedAt,
310
+ finishedAt: job.finishedAt,
311
+ error: job.error,
312
+ tail: [...job.tail],
313
+ }
314
+ }
315
+
316
+ /**
317
+ * 重装 torch(CPU 版 ↔ GPU 版互斥,先卸载再装),完成后停掉桥接让下次请求用新的 torch。
318
+ * @param {'auto'|'cuda'|'cpu'} [mode] auto=按自动定档;cuda=强制 CUDA 版(跳过兼容检查);cpu=强制 CPU 版
319
+ */
320
+ async installTorch(mode = 'auto') {
321
+ if (this.torchJob.state === 'running') return this.torchJobSnapshot()
322
+ if (!['auto', 'cuda', 'cpu'].includes(mode)) throw new Error('mode 必须是 auto / cuda / cpu')
323
+ if (!existsSync(venvPythonPath(this.config.engineRoot))) {
324
+ throw new Error('环境还没准备好(缺 .venv):请先完成首次环境准备再重装 torch。')
325
+ }
326
+ this.torchJob = {
327
+ state: 'running', step: '', message: '正在重装 torch…', tail: [],
328
+ startedAt: Date.now(), finishedAt: null, error: null,
329
+ }
330
+ const extra = ['--reinstall-torch']
331
+ if (mode === 'cuda') extra.push('--force-cuda')
332
+ if (mode === 'cpu') extra.push('--cpu')
333
+ const args = [SETUP_SCRIPT, '--dest', this.config.engineRoot, '--skip-verify', ...extra]
334
+ this.logger?.info?.(`[calibguard] 重装 torch:node setup.mjs ${args.slice(1).join(' ')}`)
335
+
336
+ const finish = new Promise((resolvePromise) => {
337
+ const child = spawn(process.execPath, args, {
338
+ cwd: this.config.engineRoot,
339
+ stdio: ['ignore', 'pipe', 'pipe'],
340
+ windowsHide: true,
341
+ })
342
+ this.torchChild = child
343
+ const onLine = (line) => {
344
+ const text = String(line).trim()
345
+ if (!text) return
346
+ this.torchJob.tail.push(text)
347
+ if (this.torchJob.tail.length > 20) this.torchJob.tail.shift()
348
+ const stepMatch = /^==\s*(.+?)\s*==$/.exec(text)
349
+ if (stepMatch) {
350
+ this.torchJob.step = stepMatch[1]
351
+ this.torchJob.message = `${stepMatch[1]}…`
352
+ return
353
+ }
354
+ this.torchJob.message = text
355
+ }
356
+ const wire = (stream) => {
357
+ let buffer = ''
358
+ stream.setEncoding('utf8')
359
+ stream.on('data', (chunk) => {
360
+ buffer += chunk
361
+ const lines = buffer.split(/\r?\n/)
362
+ buffer = lines.pop() ?? ''
363
+ for (const line of lines) onLine(line)
364
+ })
365
+ stream.on('end', () => { if (buffer) onLine(buffer) })
366
+ }
367
+ wire(child.stdout)
368
+ wire(child.stderr)
369
+ child.on('error', (error) => {
370
+ this.torchJob.state = 'failed'
371
+ this.torchJob.error = String(error?.message ?? error)
372
+ this.torchJob.message = `重装失败:${this.torchJob.error}`
373
+ this.torchJob.finishedAt = Date.now()
374
+ this.torchChild = null
375
+ resolvePromise(this.torchJobSnapshot())
376
+ })
377
+ child.on('close', async (code) => {
378
+ this.torchChild = null
379
+ this.torchJob.finishedAt = Date.now()
380
+ if (code === 0) {
381
+ this.torchJob.state = 'done'
382
+ this.torchJob.step = ''
383
+ this.torchJob.message = 'torch 重装完成,桥接将重启以使用新的 torch'
384
+ this.statusCache = null
385
+ try { await this.stop('torch-reinstalled') } catch { /* 忽略 */ }
386
+ this.logger?.info?.('[calibguard] torch 重装完成,桥接已停止(下次调用重启)')
387
+ } else {
388
+ this.torchJob.state = 'failed'
389
+ this.torchJob.error = `setup.mjs 退出码 ${code}`
390
+ this.torchJob.message = `重装失败(${this.torchJob.error}):${this.torchJob.tail.slice(-3).join(' | ')}`
391
+ this.logger?.warn?.(`[calibguard] ${this.torchJob.message}`)
392
+ }
393
+ resolvePromise(this.torchJobSnapshot())
394
+ })
395
+ })
396
+ this.torchJob.promise = finish
397
+ return finish
398
+ }
399
+
277
400
  // ============================== 进程生命周期 ==============================
278
401
 
279
402
  /** 启动常驻桥接进程(已启动则直接返回)。子进程环境经过凭据清洗。 */
@@ -503,6 +626,8 @@ export class CalibGuardEngine {
503
626
  credentialRef: this.effectiveCredentialRef() || null,
504
627
  credentialWritable: typeof this.credentials?.set === 'function',
505
628
  environment: this.bootstrap.snapshot(),
629
+ torchJob: this.torchJobSnapshot(),
630
+ gpuTorch: this.gpuTorchStatus(),
506
631
  settings: this.settings(),
507
632
  stderrTail: [...this.stderrTail],
508
633
  }
package/lib/routes.js CHANGED
@@ -149,6 +149,8 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
149
149
  credentialRef: diagnostics.credentialRef ?? null,
150
150
  credentialWritable: Boolean(diagnostics.credentialWritable),
151
151
  setupScript,
152
+ torchJob: typeof engine.torchJobSnapshot === 'function' ? engine.torchJobSnapshot() : null,
153
+ gpuTorch: typeof engine.gpuTorchStatus === 'function' ? engine.gpuTorchStatus() : null,
152
154
  settings: engine.settings(),
153
155
  settingsFile: settingsStore?.file ?? null,
154
156
  })
@@ -180,6 +182,8 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
180
182
  credentialRef: diagnostics.credentialRef ?? null,
181
183
  credentialWritable: Boolean(diagnostics.credentialWritable),
182
184
  setupScript,
185
+ torchJob: typeof engine.torchJobSnapshot === 'function' ? engine.torchJobSnapshot() : null,
186
+ gpuTorch: typeof engine.gpuTorchStatus === 'function' ? engine.gpuTorchStatus() : null,
183
187
  settings: engine.settings(),
184
188
  settingsFile: settingsStore?.file ?? null,
185
189
  })
@@ -216,6 +220,30 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
216
220
  }
217
221
  }
218
222
 
223
+ /** POST /install-torch —— 面板「重装 GPU/CPU 版 torch」。启动后台任务,进度走 /status 的 torchJob。 */
224
+ async function handleInstallTorch(req, res) {
225
+ if (req.method !== 'POST') return sendJson(res, 405, { error: '仅支持 POST' })
226
+ try {
227
+ const body = await readBody(req, 1024 * 1024)
228
+ let payload
229
+ try {
230
+ payload = JSON.parse(body.toString('utf8') || '{}')
231
+ } catch {
232
+ return sendJson(res, 400, { error: '请求体必须是合法 JSON' })
233
+ }
234
+ const mode = typeof payload?.mode === 'string' ? payload.mode : 'auto'
235
+ if (typeof engine.installTorch !== 'function') {
236
+ return sendJson(res, 503, { error: '当前插件版本不支持重装 torch' })
237
+ }
238
+ engine.installTorch(mode).catch((error) => {
239
+ logger?.warn?.(`[calibguard] 重装 torch 失败:${error?.message ?? error}`)
240
+ })
241
+ sendJson(res, 200, { ok: true, started: true, mode })
242
+ } catch (error) {
243
+ sendJson(res, 400, { error: String(error?.message ?? error) })
244
+ }
245
+ }
246
+
219
247
  /** POST /audit */
220
248
  async function handleAudit(req, res) {
221
249
  if (req.method !== 'POST') return sendJson(res, 405, { error: '仅支持 POST' })
@@ -308,6 +336,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
308
336
  state,
309
337
  routes: [
310
338
  { kind: 'exact', path: `${ROUTE_PREFIX}/status`, handler: handleStatus },
339
+ { kind: 'exact', path: `${ROUTE_PREFIX}/install-torch`, handler: handleInstallTorch },
311
340
  { kind: 'exact', path: `${ROUTE_PREFIX}/audit`, handler: handleAudit },
312
341
  { kind: 'exact', path: `${ROUTE_PREFIX}/report`, handler: handleReport },
313
342
  { kind: 'exact', path: `${ROUTE_PREFIX}/settings`, handler: async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calibguard",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/setup.mjs CHANGED
@@ -123,10 +123,25 @@ function detectGpu() {
123
123
  return { hasNvidia: true, gpuName, computeCap, cudaVersion }
124
124
  }
125
125
 
126
- /** 是否需要安装 CUDA 版 torch(与 CG_GUI launcher 同款「CUDA 版本 + 算力」双检)。 */
127
- function decideCudaTorch(gpu) {
128
- if (String(process.env.DSH_CALIBGUARD_CPU_ONLY || '') === '1') {
129
- return { useCuda: false, reason: 'DSH_CALIBGUARD_CPU_ONLY=1,强制 CPU' }
126
+ /**
127
+ * 是否需要安装 CUDA 版 torch。
128
+ * 优先级(高 → 低):--cpu / DSH_CALIBGUARD_CPU_ONLY=1 > --force-cuda / DSH_CALIBGUARD_FORCE_CUDA=1
129
+ * > auto(CG_GUI launcher 同款「CUDA 版本 + 算力」双检)。
130
+ * 说明:CUDA 版 torch 在没有 GPU 时也能正常跑 CPU,所以「强制 CUDA」只是安装时跳过兼容性检查;
131
+ * 风险:cu128 轮子里没有该显卡算力的 kernel 时,运行期会回落 CPU 或报错(自担)。
132
+ */
133
+ function decideCudaTorch(gpu, options = {}) {
134
+ if (options.forceCpu || String(process.env.DSH_CALIBGUARD_CPU_ONLY || '') === '1') {
135
+ return { useCuda: false, reason: options.forceCpu ? '--cpu 指定' : 'DSH_CALIBGUARD_CPU_ONLY=1,强制 CPU' }
136
+ }
137
+ if (options.forceCuda || String(process.env.DSH_CALIBGUARD_FORCE_CUDA || '') === '1') {
138
+ return {
139
+ useCuda: true,
140
+ forced: true,
141
+ reason: options.forceCuda
142
+ ? '--force-cuda 显式强制(跳过 CUDA/算力检查,风险自负)'
143
+ : 'DSH_CALIBGUARD_FORCE_CUDA=1 强制(跳过 CUDA/算力检查,风险自负)',
144
+ }
130
145
  }
131
146
  if (!gpu.hasNvidia) return { useCuda: false, reason: '未检测到 NVIDIA 显卡(nvidia-smi 不可用)' }
132
147
  if (!gpu.cudaVersion || cmpVersion(gpu.cudaVersion, CUDA_MIN) < 0) {
@@ -311,6 +326,8 @@ function parseArgs(argv) {
311
326
  noPortablePython: false,
312
327
  forcePortablePython: false,
313
328
  reinstallTorch: false,
329
+ forceCuda: false,
330
+ forceCpu: false,
314
331
  help: false,
315
332
  }
316
333
  for (let i = 0; i < argv.length; i += 1) {
@@ -334,6 +351,8 @@ function parseArgs(argv) {
334
351
  case '--no-portable-python': options.noPortablePython = true; break
335
352
  case '--force-portable-python': options.forcePortablePython = true; break
336
353
  case '--reinstall-torch': options.reinstallTorch = true; break
354
+ case '--force-cuda': options.forceCuda = true; break
355
+ case '--cpu': options.forceCpu = true; break
337
356
  default: fail(`未知参数:${arg}(用 --help 查看用法)`)
338
357
  }
339
358
  }
@@ -360,6 +379,8 @@ function printHelp() {
360
379
  --no-portable-python 本机没有 Python 时不自动下载便携版(默认会自动下载)
361
380
  --force-portable-python 跳过本机 Python 检测,强制用便携版(测试用)
362
381
  --reinstall-torch 重新安装 torch(检测到 NVIDIA 时装 CUDA/cu128 版,约 2.5 GB)
382
+ --force-cuda 安装时强制选 CUDA 版 torch(跳过 CUDA版本/算力检查,风险自负)
383
+ --cpu 安装时强制选 CPU 版 torch
363
384
  -h, --help 显示本帮助
364
385
 
365
386
  示例:
@@ -378,7 +399,10 @@ function printHelp() {
378
399
  可用 DSH_CALIBGUARD_PIP_INDEX 指定其它索引;
379
400
  • 从 npm 安装时引擎默认放在 ~/.dsh/calibguard-engine(可用 --dest 或插件配置 engineRoot 指定);
380
401
  • GPU 自动定档:检测到 NVIDIA 且 CUDA≥12.8、算力≥7.5(如 RTX 20/30/40 系)时
381
- 自动安装 CUDA(cu128)版 torch;否则用 CPU 版;可用 DSH_CALIBGUARD_CPU_ONLY=1 强制 CPU;
402
+ 自动安装 CUDA(cu128)版 torch;否则用 CPU 版;
403
+ - 手动覆盖:--force-cuda(强制 GPU 版)/ --cpu(强制 CPU 版),
404
+ 或环境变量 DSH_CALIBGUARD_FORCE_CUDA=1 / DSH_CALIBGUARD_CPU_ONLY=1;
405
+ - CUDA 版 torch 在无 GPU 时也能跑 CPU,两者**不能同时装在同一个 venv**(同名包互斥);
382
406
  • 大文件(模型/引擎包)默认 4 路并发分段下载,可用 DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS 调整;
383
407
  • 装完想换 GPU/CPU 版 torch:node setup.mjs --reinstall-torch。
384
408
  `)
@@ -1003,7 +1027,10 @@ async function main() {
1003
1027
  // GPU 定档:有可用 NVIDIA(CUDA≥12.8、算力≥7.5)时先装 CUDA 版 torch,
1004
1028
  // 与 CG_GUI launcher 的口径一致;否则装 CPU 版 torch。
1005
1029
  const gpu = detectGpu()
1006
- const decision = decideCudaTorch(gpu)
1030
+ const decision = decideCudaTorch(gpu, options)
1031
+ if (decision.forced && !gpu.hasNvidia) {
1032
+ warn('强制 CUDA 但未检测到 NVIDIA 显卡:torch 会安装成功,但运行期仍会回落 CPU。')
1033
+ }
1007
1034
  const gpuLabel = gpu.gpuName ? `(${gpu.gpuName})` : ''
1008
1035
  log(`GPU 定档:${decision.useCuda ? 'CUDA(cu128)' : 'CPU'} —— ${decision.reason}${gpuLabel}`)
1009
1036
  const existingTorchCuda = installedTorchCuda(python)
@@ -1030,8 +1057,8 @@ async function main() {
1030
1057
  const finalTorchCuda = installedTorchCuda(python)
1031
1058
  log(`torch CUDA:${finalTorchCuda || '无(CPU 版)'}`)
1032
1059
  if (decision.useCuda && !finalTorchCuda) {
1033
- warn('检测到可用 GPU 但 torch 是 CPU 版:审计仍可运行,只是用 CPU。'
1034
- + '可重跑:node setup.mjs --reinstall-torch')
1060
+ warn('当前 torch 是 CPU 版:审计仍可运行,只是用 CPU。'
1061
+ + '可重跑:node setup.mjs --reinstall-torch(或 --force-cuda 跳过兼容性检查)')
1035
1062
  }
1036
1063
  log('✓ 依赖安装完成')
1037
1064
  }