dsh-calibguard 0.3.1 → 0.3.3

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
@@ -68,6 +68,15 @@ are missing. **You don't install them by hand.** After you install the plugin an
68
68
  > The CG release zip is **no longer downloaded from GitHub by default**; only an explicit
69
69
  > `node setup.mjs --from <zip>` or `--url <url>` uses it (offline installs / pinning an engine version).
70
70
 
71
+ **Install speed**: the model/engine archive is downloaded with **4 parallel ranged connections** (measured
72
+ 0.6 MB/s single → 2.3 MB/s parallel); GitHub direct failures fall back to the gh-proxy / ghfast / ghproxy
73
+ mirrors, and pip tries Tsinghua → Aliyun → USTC → Tencent → PyPI in order. Tune with
74
+ `DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS`.
75
+
76
+ **GPU auto-tiering**: with an NVIDIA GPU that has **CUDA ≥ 12.8 and compute capability ≥ 7.5** (RTX 20/30/40)
77
+ the installer pulls the **CUDA (cu128) build of torch**; otherwise it installs the CPU build (GTX 10 series is
78
+ SM 6.1 and has no kernels in the cu128 wheels). The panel header shows the inference device and the reason.
79
+
71
80
  **Where to watch progress**: the panel shows a "First run: preparing engine and dependencies" card with
72
81
  the current step and the tail of the installer output; host logs show `[calibguard]` lines. The audit
73
82
  button stays disabled until it is ready — press "刷新状态 / Refresh" afterwards. Expect **10–20 minutes**
@@ -76,7 +85,7 @@ once (depends on network and pip mirror).
76
85
  > You can also pre-warm, or install offline, by running the same installer manually (idempotent):
77
86
  >
78
87
  > ```powershell
79
- > node setup.mjs # bundled engine → npm model shards → venv → deps → self-check
88
+ > node setup.mjs # bundled engine → model release asset → venv → deps → self-check
80
89
  > node setup.mjs --from D:\CalibGuard-GUI.zip # use an already-downloaded release zip (offline)
81
90
  > node setup.mjs --dest D:\CG_GUI # custom location (then configure engineRoot)
82
91
  > ```
@@ -92,13 +101,17 @@ once (depends on network and pip mirror).
92
101
 
93
102
  ### Option 0 — desktop "Add plugin" dialog (easiest, recommended)
94
103
 
95
- DSH desktop → Settings → **Add plugin**, paste any one of the following (restart DSH afterwards):
104
+ **On DSH desktop it is one step: Settings → Add plugin → enter `dsh-calibguard` → restart DSH.**
105
+ That installs the latest npm release; to pin a version write `dsh-calibguard@0.3.3` (`@` + version).
106
+ No terminal and no source download needed.
107
+
108
+ The dialog also accepts any of the following (restart DSH afterwards):
96
109
 
97
110
  | Paste | Example | Notes |
98
111
  |---|---|---|
99
112
  | **GitHub repository URL** (recommended) | `https://github.com/maomitgd2006/dsh-CalibGuard` | Fetches the source and registers the bundle in the current profile |
100
113
  | **Local directory path** | `<path to this package>` (e.g. `/opt/dsh-calibguard`) | For a local clone / extracted copy |
101
- | **Package name** | `dsh-calibguard` | ✅ **Published on npm**; just enter the package name (installs npm `latest`) |
114
+ | **Package name** | `dsh-calibguard` or `dsh-calibguard@0.3.3` | ✅ **Published on npm**; without a version it installs `latest`, with `@version` it pins that version |
102
115
 
103
116
  > Equivalent CLI (verified: after install DSH automatically appends the package to the profile's
104
117
  > `dsh.profile.bundles`):
@@ -201,7 +214,14 @@ After pressing Save:
201
214
  - values are written to `$DSH_HOME/calibguard-settings.json` (defaults to `~/.dsh`, never committed);
202
215
  - **the AI uses the same values** (precedence: plugin config < shared settings < per-call options);
203
216
  - changes take effect immediately, no restart;
204
- - the API key goes into the DSH credential store (`ctx.credentials`), not into that settings file.
217
+ - the API key goes into the DSH credential store (`ctx.credentials`), not into that settings file;
218
+ - the **credential name** is only the name of the storage slot (not the secret): fill it in the panel
219
+ (e.g. `DEEPSEEK_API_KEY`) together with the API key; the name is remembered in shared settings
220
+ (`credentialRef`) and the value is written to the DSH credential store. Leave it empty to use the
221
+ plugin config `credentialRef`;
222
+ - **audit results**: every audit writes JSON + Markdown into the engine's `reports/` directory
223
+ (default `<engineRoot>/reports`, `engineRoot` defaults to `~/.dsh/calibguard-engine`). The panel shows
224
+ that path, has a "clear results" button, and each item links to its Markdown report.
205
225
 
206
226
  ### 4) Tool parameters (per-call, advanced)
207
227
 
@@ -228,7 +248,7 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
228
248
  | `enabled` | `true` | When false nothing is registered (no need to remove the row) |
229
249
  | `engineRoot` | `''` | CG_GUI directory; empty = the `CG_GUI` next to this package |
230
250
  | `device` | `auto` | Checks both CUDA build and compute capability; low-end GPUs fall back to CPU |
231
- | `credentialRef` | `''` | Recommended credential reference name, e.g. `DEEPSEEK_API_KEY` (resolved via the DSH credential store) |
251
+ | `credentialRef` | `''` | Credential **name** (storage slot, not the secret), e.g. `DEEPSEEK_API_KEY`; empty = use the name saved from the panel; the value lives in the DSH credential store / env |
232
252
  | `announceToModel` | `true` | Add "when to use CalibGuard" to the system prompt |
233
253
  | `autoSetup` | `true` | On first run, automatically download the engine, a portable Python when needed, and install deps; set `false` to only fail with instructions |
234
254
  | `maxFiles` | `20` | Directory recursion cap; explicitly given/uploaded files are not capped |
@@ -240,10 +260,14 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
240
260
  | Symptom | Fix |
241
261
  |---|---|
242
262
  | No sidebar icon / panel says `not found` | Client bundle and routes load at host startup: **restart DSH** |
243
- | **First run takes 10–20 minutes** | Normal: the model (476 MB) is downloaded from npm mirrors and dependencies (1–3 GB) are installed once; the panel shows progress |
263
+ | **First run takes 10–20 minutes** | Normal: the model (~442 MB, from the repository's model release asset with mirror fallback) and dependencies (1–3 GB) are installed once; the panel shows progress |
244
264
  | First audit is slow | Normal: ~475 MB of weights are loaded once (then the process stays resident) |
245
265
  | Big file takes tens of seconds | Normal: full-text sliding-window inference; see [docs/performance.md](docs/performance.md) |
246
- | Gray zone always goes to manual review | No credential configured: set `credentialRef` (recommended) or configure it in DSH Models |
266
+ | Gray zone always goes to manual review | No credential configured: save "credential name + API key" in the panel, or set `credentialRef`, or configure it in DSH Models |
267
+ | **Panel says "credentialRef required" when saving the API key** | Upgrade to ≥ **0.3.3**: the panel has a "credential name" field and remembers it (no plugin config edit needed); older versions require `credentialRef` in the plugin config |
268
+ | **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
+ | **"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
+ | **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 |
247
271
  | 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) |
248
272
  | Panel returns 401/403 | Browser session isn't authenticated with the host: refresh / sign in again |
249
273
  | The AI ignores changed settings | Press Save in the panel; editing fields without saving applies to that run only |
package/README.md CHANGED
@@ -62,6 +62,14 @@
62
62
  > 默认**不再**从 GitHub 下载 CG 发行包;只有显式 `node setup.mjs --from <zip>` 或 `--url <地址>`
63
63
  > 才会走 CG 发行包,用于离线/指定引擎版本。
64
64
 
65
+ **安装速度**:模型/引擎包默认 **4 路并发分段下载**(实测单连接 0.6 MB/s → 并发 2.3 MB/s),
66
+ GitHub 直连失败会自动换 gh-proxy / ghfast / ghproxy 镜像;pip 依赖按 清华 → 阿里 → 中科大 → 腾讯 → 官方
67
+ 依次回退。可用 `DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` 调整并发数。
68
+
69
+ **GPU 自动定档**:检测到 NVIDIA 且 **CUDA ≥ 12.8、算力 ≥ 7.5**(RTX 20/30/40 系)时自动安装
70
+ **CUDA(cu128)版 torch**;否则装 CPU 版(GTX 10 系算力 6.1,cu128 轮子里没有对应 kernel,只能 CPU)。
71
+ 面板顶部会显示「推理设备:cpu/cuda」和定档原因。
72
+
65
73
  **进度在哪看**:面板顶部会出现「首次运行:正在自动准备引擎与依赖」卡片(含当前步骤与输出尾巴);
66
74
  宿主日志里是 `[calibguard]` 开头的行。准备期间审计按钮不可用,完成后点「刷新状态」即可。
67
75
  首次约需 **10–20 分钟**(取决于网速和 pip 源),只需一次。
@@ -69,7 +77,7 @@
69
77
  > 想提前预热、或断网/锁网环境需要离线安装,也可以手动跑安装器(同一套逻辑,幂等):
70
78
  >
71
79
  > ```powershell
72
- > node setup.mjs # 一键:下载 → 解压 → venv → 依赖 → 自检
80
+ > node setup.mjs # 一键:内置引擎 → 下载模型 → venv → 依赖 → 自检
73
81
  > node setup.mjs --from D:\CalibGuard-GUI.zip # 用已下载的发行包,不联网
74
82
  > node setup.mjs --dest D:\CG_GUI # 装到自定义位置(之后配置 engineRoot)
75
83
  > ```
@@ -81,17 +89,21 @@
81
89
 
82
90
  1. **DSH**(DeepSeek Harness)0.1.5-rc.2 实测可用;
83
91
  2. **Python**:无需预装——没有 64 位 3.10–3.12 时安装器会自动下载便携版(缓存在 `~/.dsh/calibguard-tools`,可用 `--no-portable-python` 关闭);
84
- 3. **引擎**:插件自带;模型权重会自动从 npm 镜像下载。也可用 `--from` 指定 CG 发行包,或把 `engineRoot` 指向已有 `CG_GUI/`。
92
+ 3. **引擎**:插件自带;模型权重会自动从本仓库 Release 下载(自动镜像回退)。也可用 `--from` 指定 CG 发行包,或把 `engineRoot` 指向已有 `CG_GUI/`。
85
93
 
86
94
  ### 方式 0:桌面端「添加插件」(最省事,推荐)
87
95
 
88
- DSH 桌面版 → 设置 → **添加插件**,输入框里填以下任意一种即可(装完按提示重启 DSH):
96
+ **DSH 桌面版只需要一步:设置 → 添加插件 → 输入 `dsh-calibguard` → 重启 DSH。**
97
+ 默认装 npm 上的最新版;要固定版本就写 `dsh-calibguard@0.3.3`(`@` 后面是版本号)。
98
+ 不需要命令行、不需要下载源码。
99
+
100
+ 输入框里也可以填以下任意一种(装完按提示重启 DSH):
89
101
 
90
102
  | 填什么 | 示例 | 说明 |
91
103
  |---|---|---|
92
104
  | **GitHub 仓库地址**(推荐) | `https://github.com/maomitgd2006/dsh-CalibGuard` | 自动拉取源码并登记到当前 profile 的 bundle |
93
105
  | **本地目录路径** | `<你的插件目录>`(例:`D:\code\dsh-calibguard`) | 已 clone / 解压到本地时用 |
94
- | **包名** | `dsh-calibguard` | ✅ **已发布到 npm**,直接输包名即可(自动装 npm 上的 latest) |
106
+ | **包名** | `dsh-calibguard` 或 `dsh-calibguard@0.3.3` | ✅ **已发布到 npm**;不写版本=装 latest,写 `@版本`=固定版本 |
95
107
 
96
108
  > 等价命令行(已实测:安装后 DSH 会自动把包加进 profile 的 `dsh.profile.bundles`):
97
109
  >
@@ -191,7 +203,12 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
191
203
  - 参数写入 `$DSH_HOME/calibguard-settings.json`(默认 `~/.dsh`,不写进仓库);
192
204
  - **AI 调用同样使用这份设置**(优先级:插件配置 < 共享设置 < 本次调用参数);
193
205
  - 改完立即生效,**不用重启**;
194
- - API Key 写入 DSH 凭据库(`ctx.credentials`),不落入该设置文件。
206
+ - API Key 写入 DSH 凭据库(`ctx.credentials`),不落入该设置文件;
207
+ - **凭据名**只是"保存位置"的名字(不是密钥本身):面板里填(如 `DEEPSEEK_API_KEY`)+ API Key,
208
+ 保存后名字记进共享设置的 `credentialRef`,值写进 DSH 凭据库;留空则用插件配置里的 `credentialRef`;
209
+ - **审计结果**:每次审计在引擎的 `reports/` 目录生成 JSON + Markdown(默认 `<engineRoot>/reports`,
210
+ `engineRoot` 默认 `~/.dsh/calibguard-engine`)。面板结果区会显示该路径并可「清空结果」,
211
+ 条目里的「查看 Markdown 报告」可直接打开。
195
212
 
196
213
  ### 4) 工具参数(按次覆盖,写给进阶用户)
197
214
 
@@ -218,7 +235,7 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
218
235
  | `enabled` | `true` | 关掉则什么都不注册(临时停用不必删行) |
219
236
  | `engineRoot` | `''` | 引擎目录;留空自动推导:**npm/桌面端安装**时为 `~/.dsh/calibguard-engine`,源码仓库里为本包同级 `CG_GUI` |
220
237
  | `device` | `auto` | 自动做「CUDA 版本 + 算力」双检,低算力显卡回落 CPU |
221
- | `credentialRef` | `''` | 推荐的云端复核凭据引用名,如 `DEEPSEEK_API_KEY`(交给 DSH 凭据库解析) |
238
+ | `credentialRef` | `''` | 云端复核凭据**名**(保存位置,不是密钥),如 `DEEPSEEK_API_KEY`;留空=用面板「凭据名」保存的名字;值存放在 DSH 凭据库/环境变量 |
222
239
  | `announceToModel` | `true` | 是否把「何时该用 CalibGuard」写进系统提示词 |
223
240
  | `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**准备引擎(内置代码 + npm 模型分片)、按需下载便携 Python、安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
224
241
  | `maxFiles` | `20` | 目录展开上限;显式给出/上传的文件不受限 |
@@ -232,8 +249,12 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
232
249
  | 侧边栏没有图标 / 面板报 `not found` | 客户端与路由在宿主启动时加载:**重启 DSH** |
233
250
  | **首次运行要等十几分钟** | 正常:首次下载模型(约 442 MB,自动镜像回退)并安装依赖(1–3 GB);面板顶部有进度卡片,只需一次 |
234
251
  | 首次审计很慢 | 正常:要加载约 475 MB 权重(之后走常驻进程) |
252
+ | **面板保存 API Key 提示"需要 credentialRef"** | 升级到 ≥ **0.3.3**:面板「凭据名」可直接填写并记住(不用改插件配置);旧版本请把 `credentialRef` 写进插件配置 |
253
+ | **审计结果保存在哪 / 想清空** | 保存在引擎 `reports/` 目录(默认 `<engineRoot>/reports`),每次审计生成 JSON + Markdown;面板结果区显示路径并带「清空结果」按钮;AI 工具返回 `report_md_path` / `report_json_path` |
254
+ | **审计时总提示"首次会加载模型"** | 0.3.3 起只在模型尚未加载时提示;加载后按钮只显示「审计中…」,顶部状态显示「模型 已加载」 |
255
+ | **有 NVIDIA 显卡(如 RTX 2060)但一直显示 CPU** | 说明装的是 CPU 版 torch(旧版安装器行为)。升级到 ≥0.3.3 后面板会给出提示;在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装上 CUDA 版即可,之后重启 DSH |
235
256
  | 大文件审计几十秒 | 正常:全文滑窗逐窗推理,见 [docs/performance.md](docs/performance.md) |
236
- | 灰区全部转人工 | 没配凭据:设 `credentialRef`(推荐)或在 DSH Models 页配置 |
257
+ | 灰区全部转人工 | 没配凭据:在面板「凭据名 + API Key」保存,或设 `credentialRef`、或在 DSH Models 页配置 |
237
258
  | 正规第三方库被判 BLOCK | 压缩库充满 eval/base64 等特征,模型语料偏 Python 生态 → 属已知误报倾向,见 [docs/troubleshooting.md](docs/troubleshooting.md) |
238
259
  | 面板报 401/403 | 浏览器会话未通过宿主鉴权:刷新页面/重新登录 |
239
260
  | 改了参数 AI 没变 | 面板里需点「保存」(只改表单不保存 = 仅影响那一次) |
@@ -372,7 +372,13 @@ def _op_status(options: dict | None = None) -> dict:
372
372
 
373
373
  pipeline = _STATE["pipeline"]
374
374
  cfg = pipeline.cfg if pipeline is not None else _build_cfg(options)
375
- policy = _STATE["device_policy"] or {}
375
+ policy = dict(_STATE["device_policy"] or {})
376
+ # 定档说 CUDA、但实际 torch 没启用 CUDA(典型:装的是 CPU 版 torch)→ 如实标注
377
+ if policy.get("mode") == "cuda" and not cuda_available:
378
+ policy["mode"] = "cpu"
379
+ policy["reason"] = (policy.get("reason") or "") + \
380
+ ";但当前 torch 未启用 CUDA(可能是 CPU 版 torch),实际用 CPU"
381
+ policy["env"] = "CUDA_VISIBLE_DEVICES 未设置(由 torch 决定)"
376
382
  return {
377
383
  "engineRoot": root,
378
384
  "engineVersion": _engine_version(),
package/cordis.patch.yml CHANGED
@@ -48,8 +48,9 @@
48
48
  # allowThreshold: 0.15
49
49
  # blockThreshold: 0.85
50
50
  # llmEnabled: false
51
- # 【推荐】云端复核凭据:交给 DSH 的 ctx.credentials 解析(继承环境 > 凭据库 > .env)。
52
- # 名字写在配置里,插件代码不含硬编码凭据名,也不会隐式继承宿主环境变量。
51
+ # 【推荐】云端复核凭据名(保存位置,不是密钥):交给 DSH 的 ctx.credentials 解析(继承环境 > 凭据库 > .env)。
52
+ # 名字可以写在这里,也可以留空、直接在面板「凭据名」里填(保存后记进共享设置);
53
+ # 插件代码不含硬编码凭据名,也不会隐式继承宿主环境变量。
53
54
  # credentialRef: DEEPSEEK_API_KEY
54
55
  # 备选:直接写字面值(不推荐,凭据会出现在配置文件里)
55
56
  # llmApiKey: ''
@@ -22,12 +22,15 @@
22
22
  | `llmBaseUrl` / `llmModel` | 跟随引擎 | OpenAI 兼容端点与模型名 |
23
23
  | `llmContentSource` | 跟随引擎 | 送审内容源:`evidence`(精炼证据)/ `raw`(预处理后全文) |
24
24
  | `llmDeliveryMode` | 跟随引擎 | 投递方式:`truncate` / `window` / `all` |
25
- | `credentialRef` | `''` | **推荐**:DSH 凭据引用名(如 `DEEPSEEK_API_KEY`),交给 `ctx.credentials.resolve()` |
25
+ | `credentialRef` | `''` | **推荐**:凭据**名**(保存位置,不是密钥,如 `DEEPSEEK_API_KEY`),交给 `ctx.credentials.resolve()`。留空时用面板「凭据名」保存进共享设置的名字 |
26
26
  | `llmApiKey` | `''` | 字面值 Key(不推荐,会落在配置文件里);优先于 `credentialRef` |
27
27
  | `sensitiveEnvPattern` | `''` | 桥接子进程环境清洗的兜底正则;留空=用 DSH 的实现或内置启发式 |
28
28
  | `settingsPath` | `''` | GUI 与 AI 共用的设置文件;留空=`$DSH_HOME/calibguard-settings.json` |
29
29
  | `windowStride` | 跟随引擎(256) | 模型侧全文滑窗步长(1~511)。**不在面板/共享设置里**,只走插件配置或工具参数;设 510 时窗口数约减半 |
30
30
 
31
+ > **GPU 定档**:安装器检测到 NVIDIA 且 CUDA ≥ 12.8、算力 ≥ 7.5 时自动装 CUDA(cu128)版 torch,
32
+ > 否则装 CPU 版。已经装成 CPU 版时:在插件目录运行 `node setup.mjs --reinstall-torch`。
33
+
31
34
  ## 二、参数优先级
32
35
 
33
36
  ```
@@ -40,7 +43,7 @@
40
43
 
41
44
  ## 三、云端复核的 Key 从哪来(不硬编码、不隐式继承)
42
45
 
43
- 1. `credentialRef`(推荐)→ `ctx.credentials.resolve(ref)`,优先级
46
+ 1. `credentialRef`(推荐;插件配置或面板「凭据名」两者取其一)→ `ctx.credentials.resolve(ref)`,优先级
44
47
  **继承环境 > `~/.dsh/.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`**;
45
48
  解析到的值随请求走 **stdin** 下发,**不进 argv、不进子进程环境变量**,且每次操作重新解析(换 Key 下一次审计即生效)。
46
49
  2. `llmApiKey`(字面值,次选)。
@@ -55,17 +58,21 @@
55
58
  | 项 | 说明 |
56
59
  |---|---|
57
60
  | 文件 | `$DSH_HOME/calibguard-settings.json`(默认 `~/.dsh`,**不写进仓库**;可用 `settingsPath` 覆盖) |
58
- | 内容 | 云端复核开关、Base URL、模型名、送审内容、投递方式、截断/滑窗大小与重叠、灰区上下限、目录上限(**不含凭据、不含模型侧步长**) |
61
+ | 内容 | 云端复核开关、Base URL、模型名、送审内容、投递方式、截断/滑窗大小与重叠、灰区上下限、目录上限、**凭据名 `credentialRef`**(**不含密钥本身、不含模型侧步长**) |
59
62
  | 写入 | 面板「保存」→ `PUT /api/calibguard/settings`;原子写,非法值不落盘(校验口径与 CG_GUI 一致) |
60
63
  | 读取 | 面板加载时回填;AI 每次调用时由引擎合并,**改完立即生效、无需重启** |
61
64
  | 重置 | 面板「重置」→ 清空为「跟随插件配置默认值」 |
62
- | API Key | 面板输入框 → `PUT /api/calibguard/credential` → 写入 DSH 凭据库,不落设置文件 |
65
+ | API Key | 面板「凭据名 + API Key」→ `PUT /api/calibguard/credential` → 值写入 DSH 凭据库(不落设置文件),名字记入共享设置 `credentialRef`(便于下次启动找到) |
63
66
 
64
67
  ## 五、环境变量
65
68
 
66
69
  | 变量 | 作用 |
67
70
  |---|---|
68
71
  | `DSH_CALIBGUARD_PYTHON` | 指定桥接进程使用的 Python 解释器(优先于 `<engineRoot>/.venv`) |
72
+ | `DSH_CALIBGUARD_CPU_ONLY` | 设为 `1` 强制使用 CPU 版 torch(跳过 GPU 定档) |
73
+ | `DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` | 大文件并发分段下载的连接数(默认 4;模型/引擎包下载提速) |
74
+ | `DSH_CALIBGUARD_TORCH_INDEX` | CUDA 版 torch 的安装索引(默认 `https://download.pytorch.org/whl/cu128`) |
75
+ | `DSH_CALIBGUARD_PIP_INDEX` | 指定 pip 主索引(默认依次尝试 清华 → 阿里 → 中科大 → 腾讯 → 官方) |
69
76
  | `DSH_HOME` | DSH 主目录(默认 `~/.dsh`);决定共享设置文件位置 |
70
77
 
71
78
  ## 六、宿主 HTTP 路由(浏览器面板使用)
@@ -9,6 +9,11 @@
9
9
  | 每份文件都 `manual_review` + `CUDA error: no kernel image` | 设备定档失效:确认 `device` 未被强制成 `cuda` |
10
10
  | 灰区全部转人工、`llm.status=api_error` | 没配凭据(预期降级)→ 设 `credentialRef`(推荐)或在 DSH Models 页配置 |
11
11
  | `凭据引用 X 未配置` 告警 | `credentialRef` 指向的引用在 DSH 凭据库里没有值;在 Models 页填 Key 或用 `llmApiKey` |
12
+ | **面板保存 API Key 报「需要 credentialRef」** | 升级到 ≥0.3.3:面板「凭据名」直接填(如 `DEEPSEEK_API_KEY`)并保存,会记进共享设置;旧版本只能把 `credentialRef` 写进插件配置 |
13
+ | **审计结果在哪里 / 怎么清空** | 引擎 `reports/` 目录(默认 `<engineRoot>/reports`):每次审计生成 JSON + Markdown;面板结果区显示路径并有「清空结果」,工具返回 `report_md_path` / `report_json_path` |
14
+ | **每次审计都提示"首次会加载模型"** | 0.3.3 起仅在模型未加载时提示;加载后不再出现,顶部状态显示「模型 已加载」 |
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
+ | **首次安装特别慢 / 提示某个源不可用** | 模型默认 4 路并发分段下载(`DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` 可调),GitHub 失败自动换 gh-proxy/ghfast/ghproxy 镜像;pip 自动在 清华/阿里/中科大/腾讯/官方 之间回退。实在慢可手动下载 `calibguard-model-1.0.0.zip` 放到缓存目录(见下一条) |
12
17
  | **模型下载失败 / 想离线** | 手动下载 `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 并跳过下载 |
13
18
  | **首次运行卡在"正在准备环境"** | 正常:首次下载模型(约 442 MB,自动镜像回退)+ 装依赖(1–3 GB),约 10–20 分钟;面板顶部有进度与输出尾巴。若长时间无进度,可看 `~/.dsh/calibguard-tools/model-cache` 是否在增长 |
14
19
  | 环境准备失败 | 面板会显示失败原因与最后几行输出;在本插件目录跑 `node setup.mjs` 看完整输出后重试(也支持 `--from` 离线包) |
package/lib/client.js CHANGED
@@ -340,6 +340,11 @@ window.__ModuleLoader__.load({
340
340
  var apiKey = apiKeyState[0]
341
341
  var setApiKey = apiKeyState[1]
342
342
 
343
+ // 凭据名(保存位置的名字);留空则用插件配置里的 credentialRef
344
+ var credNameState = useState('')
345
+ var credName = credNameState[0]
346
+ var setCredName = credNameState[1]
347
+
343
348
  var busyState = useState(false)
344
349
  var busy = busyState[0]
345
350
  var setBusy = busyState[1]
@@ -387,6 +392,7 @@ window.__ModuleLoader__.load({
387
392
  writable: Boolean(payload && payload.credentialWritable),
388
393
  configured: Boolean(llm.configured),
389
394
  })
395
+ setCredName(function (prev) { return prev || (payload && payload.credentialRef) || '' })
390
396
  setForm(function (current) {
391
397
  var next = update(current, 'llmEnabled', llm.enabled !== false)
392
398
  next = update(next, 'allowThreshold', gate.allowThreshold !== undefined ? gate.allowThreshold : current.allowThreshold)
@@ -448,6 +454,9 @@ window.__ModuleLoader__.load({
448
454
  // 只保留「工具参数 / 插件配置」两个入口,避免多一个难懂的概念。
449
455
  if (String(form.llmBaseUrl || '').trim()) payload.llmBaseUrl = String(form.llmBaseUrl).trim()
450
456
  if (String(form.llmModel || '').trim()) payload.llmModel = String(form.llmModel).trim()
457
+ // 凭据名(保存位置)由面板填写,也存进共享设置,下次启动审计仍能找到
458
+ var credentialName = String(credName || '').trim() || (credential.ref || '')
459
+ if (credentialName) payload.credentialRef = credentialName
451
460
 
452
461
  apiFetch('/settings', {
453
462
  method: 'PUT',
@@ -463,11 +472,13 @@ window.__ModuleLoader__.load({
463
472
  return apiFetch('/credential', {
464
473
  method: 'PUT',
465
474
  headers: { 'content-type': 'application/json' },
466
- body: JSON.stringify({ apiKey: apiKey }),
467
- }).then(function () {
475
+ body: JSON.stringify({ apiKey: apiKey, name: credentialName }),
476
+ }).then(function (outcome) {
468
477
  setApiKey('')
469
- setSettingsMsg('已保存 ✓ 参数共用;API Key 已写入 DSH 凭据库')
478
+ if (outcome && outcome.credentialRef) setCredName(outcome.credentialRef)
479
+ setSettingsMsg('已保存 ✓ 参数共用;API Key 已写入 DSH 凭据库(' + ((outcome && outcome.credentialRef) || credentialName) + ')')
470
480
  refreshStatus()
481
+ loadSettings()
471
482
  }).catch(function (err) {
472
483
  setSettingsMsg('参数已保存,但 API Key 写入失败:' + String((err && err.message) || err))
473
484
  })
@@ -536,12 +547,19 @@ window.__ModuleLoader__.load({
536
547
  }).then(function (body) {
537
548
  setResult(body.result)
538
549
  setBusy(false)
550
+ refreshStatus()
539
551
  }).catch(function (err) {
540
552
  setBusy(false)
541
553
  setError(String((err && err.message) || err))
542
554
  })
543
555
  }
544
556
 
557
+ function clearResult() {
558
+ setResult(null)
559
+ setReport(null)
560
+ setError('')
561
+ }
562
+
545
563
  function openReport(name) {
546
564
  setReport({ name: name, text: '加载中…' })
547
565
  fetch(API + '/report?name=' + encodeURIComponent(name))
@@ -586,12 +604,23 @@ window.__ModuleLoader__.load({
586
604
 
587
605
  var envInfo = status && status.environment ? status.environment : null
588
606
  var engineInfo = status && status.status ? status.status : null
607
+ var reportDir = (engineInfo && engineInfo.reportDir)
608
+ || (status && status.settings && status.settings.reportDir)
609
+ || (result && result.config && result.config.reportDir)
610
+ || ''
611
+ var devicePolicy = (engineInfo && engineInfo.devicePolicy) || null
612
+ var setupScript = (status && status.setupScript) || '<插件目录>/setup.mjs'
613
+ // 有 NVIDIA 显卡、但 torch 没启用 CUDA(装的是 CPU 版)→ 提示怎么开 GPU
614
+ var gpuTorchHint = engineInfo && engineInfo.cudaAvailable === false
615
+ && devicePolicy && devicePolicy.gpuName ? devicePolicy.gpuName : ''
589
616
  var engineReady = Boolean(engineInfo)
590
617
  var envBusy = Boolean(envInfo) && !engineReady && envInfo.state !== 'failed'
591
618
  var envFailed = Boolean(envInfo) && !engineReady && envInfo.state === 'failed'
592
619
 
593
620
  var statusText = engineInfo
594
- ? '引擎就绪:' + engineInfo.device + ' | 阈值 ' + engineInfo.gate.allowThreshold + '/' + engineInfo.gate.blockThreshold
621
+ ? '引擎就绪:' + engineInfo.device
622
+ + ' | 模型 ' + (engineInfo.loaded ? '已加载' : '待加载')
623
+ + ' | 阈值 ' + engineInfo.gate.allowThreshold + '/' + engineInfo.gate.blockThreshold
595
624
  + ' | 云端复核 ' + (engineInfo.llm.enabled ? (engineInfo.llm.configured ? '可用' : '已启用但无凭据') : '关闭')
596
625
  : (envFailed ? '环境准备失败' : (envBusy ? '首次运行:正在准备引擎与依赖…' : '状态读取中…'))
597
626
 
@@ -609,6 +638,23 @@ window.__ModuleLoader__.load({
609
638
  ],
610
639
  }),
611
640
  statusError ? jsx('div', { style: S.err, children: statusError }) : null,
641
+ engineInfo && devicePolicy && devicePolicy.reason
642
+ ? jsx('div', {
643
+ style: S.muted,
644
+ children: '推理设备:' + engineInfo.device
645
+ + (devicePolicy.gpuName ? '(' + devicePolicy.gpuName + ')' : '')
646
+ + ' | ' + devicePolicy.reason,
647
+ })
648
+ : 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。',
656
+ })
657
+ : null,
612
658
 
613
659
  // ---------- 首次运行:自动准备环境(对齐 CG_GUI 首启装环境) ----------
614
660
  envInfo && !engineReady
@@ -676,6 +722,21 @@ window.__ModuleLoader__.load({
676
722
  jsxs('div', {
677
723
  style: Object.assign({}, S.group, llmOn ? null : S.groupOff),
678
724
  children: [
725
+ jsxs('label', {
726
+ style: S.field,
727
+ children: [
728
+ jsx('span', { style: S.label, children: '凭据名(保存位置的名字,不是密钥)' }),
729
+ jsx('input', {
730
+ style: S.input,
731
+ type: 'text',
732
+ autoComplete: 'off',
733
+ placeholder: credential.ref || 'DEEPSEEK_API_KEY',
734
+ value: credName,
735
+ disabled: !llmOn,
736
+ onChange: function (event) { setCredName(event.target.value) },
737
+ }),
738
+ ],
739
+ }),
679
740
  jsxs('label', {
680
741
  style: S.field,
681
742
  children: [
@@ -686,7 +747,7 @@ window.__ModuleLoader__.load({
686
747
  autoComplete: 'off',
687
748
  placeholder: credential.configured
688
749
  ? '已配置(留空表示不修改)'
689
- : (credential.ref ? '未配置,填入后保存到 DSH 凭据库' : '需插件配置 credentialRef 后才能保存'),
750
+ : '未配置,填入后点「保存」写入 DSH 凭据库',
690
751
  value: apiKey,
691
752
  disabled: !llmOn,
692
753
  onChange: function (event) { setApiKey(event.target.value) },
@@ -879,7 +940,9 @@ window.__ModuleLoader__.load({
879
940
  jsx('div', {
880
941
  style: S.note,
881
942
  children: '设置保存在本地 ' + (settingsFile || '$DSH_HOME/calibguard-settings.json')
882
- + '(GUI 与 AI 工具共用);API Key 写入 DSH 凭据库,不落此文件。',
943
+ + '(GUI 与 AI 工具共用);API Key 写入 DSH 凭据库,不落此文件。'
944
+ + '「凭据名」只是保存位置的名字(可留空=用插件配置 credentialRef);'
945
+ + '审计结果(JSON + Markdown)保存在引擎的 reports 目录,见下方「审计结果」。',
883
946
  }),
884
947
  jsxs('div', {
885
948
  style: S.row,
@@ -968,18 +1031,40 @@ window.__ModuleLoader__.load({
968
1031
  busy || !engineReady ? { opacity: 0.6, cursor: 'default' } : null),
969
1032
  onClick: busy || !engineReady ? function () {} : run,
970
1033
  children: busy
971
- ? '审计中…(首次会加载模型)'
1034
+ ? '审计中…' + (engineInfo && engineInfo.loaded === false ? '(首次会加载模型)' : '')
972
1035
  : (!engineReady ? '环境准备完成后可审计' : '开始审计'),
973
1036
  }),
974
1037
  jsx('span', { style: S.muted, children: '使用上面「设置」里保存的参数(与 AI 工具一致)' }),
975
1038
  ],
976
1039
  }),
1040
+ engineReady && reportDir
1041
+ ? jsx('div', {
1042
+ style: S.muted,
1043
+ children: '审计结果(JSON + Markdown)保存到:' + reportDir,
1044
+ })
1045
+ : null,
977
1046
  ],
978
1047
  }),
979
1048
 
980
1049
  error ? jsx('div', { style: S.err, children: error }) : null,
981
1050
 
982
1051
  // ---------- 结果 ----------
1052
+ result
1053
+ ? jsxs('div', {
1054
+ style: S.row,
1055
+ children: [
1056
+ jsx('div', { style: S.cardTitle, children: '审计结果' }),
1057
+ jsx('button', { style: S.button, onClick: clearResult, children: '清空结果' }),
1058
+ ],
1059
+ })
1060
+ : null,
1061
+ result && reportDir
1062
+ ? jsx('div', {
1063
+ style: S.muted,
1064
+ children: '结果文件保存在:' + reportDir
1065
+ + '(每次审计生成 JSON + Markdown;点条目里的「查看 Markdown 报告」可直接打开)',
1066
+ })
1067
+ : null,
983
1068
  summary
984
1069
  ? jsxs('div', {
985
1070
  style: S.summary,
package/lib/engine.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * 凭据(**零硬编码**):
12
12
  * - 子进程环境经 `buildChildEnv()` 清洗:`*KEY*` / `*PASSWORD*` / `*SECRET*` / `*TOKEN*`
13
13
  * 与 `DSH_*` 一律不传,与 DSH 自己的 `scrubbedParentEnv()` 同源(能导入就直接用它的);
14
- * - 云端复核 Key 的唯一来源是配置:`llmApiKey`(字面值)或 `credentialRef`
14
+ * - 云端复核 Key 的来源:`llmApiKey`(字面值)或 `credentialRef`(插件配置,或面板「凭据名」保存进共享设置)
15
15
  * (交给 `ctx.credentials.resolve()` 解析,优先级 env > 凭据库 > .env,随 DSH 策略演进);
16
16
  * - 解析结果**每次操作重新解析**(凭据轮换即时生效),并只通过 subprocess 的 stdin
17
17
  * 私有管道随请求下发 —— 不进 argv(进程列表可见)、不进子进程环境变量。
@@ -124,14 +124,25 @@ export class CalibGuardEngine {
124
124
 
125
125
  // ============================== 凭据解析 ==============================
126
126
 
127
+ /**
128
+ * 生效的凭据名:插件配置 `credentialRef` 优先,其次是面板保存进共享设置的 `credentialRef`。
129
+ * 两者都只是"名字",值在 DSH 凭据库/环境里,不会进配置文件。
130
+ */
131
+ effectiveCredentialRef() {
132
+ const fromConfig = typeof this.config.credentialRef === 'string' ? this.config.credentialRef.trim() : ''
133
+ if (fromConfig) return fromConfig
134
+ const fromSettings = this.settings()?.credentialRef
135
+ return typeof fromSettings === 'string' ? fromSettings.trim() : ''
136
+ }
137
+
127
138
  /**
128
139
  * 解析云端复核使用的 Key。**每次操作都重新解析**,因此凭据轮换无需重启进程。
129
140
  *
130
141
  * 顺序:
131
142
  * 1. `config.llmApiKey`(配置里的字面值,最低推荐度);
132
- * 2. `config.credentialRef` → `ctx.credentials.resolve(ref)`(DSH 的分层解析:
143
+ * 2. 生效的 `credentialRef` → `ctx.credentials.resolve(ref)`(DSH 的分层解析:
133
144
  * 继承环境 > 凭据库 > .env 文件,具体层由来源 `source` 报告);
134
- * 3. 若凭据服务不可用,则按同名环境变量读取(名字来自配置,不是硬编码)。
145
+ * 3. 若凭据服务不可用,则按同名环境变量读取(名字来自配置/面板,不是硬编码)。
135
146
  *
136
147
  * @returns {Promise<{key: string|undefined, source: string}>}
137
148
  */
@@ -142,7 +153,7 @@ export class CalibGuardEngine {
142
153
  return { key: config.llmApiKey, source: this.lastCredential.source }
143
154
  }
144
155
 
145
- const ref = config.credentialRef
156
+ const ref = this.effectiveCredentialRef()
146
157
  if (!ref) {
147
158
  this.lastCredential = { source: 'none', configured: false }
148
159
  return { key: undefined, source: 'none' }
@@ -182,17 +193,29 @@ export class CalibGuardEngine {
182
193
  * @param {string} value API Key
183
194
  * @returns {Promise<{ref: string, saved: boolean}>}
184
195
  */
185
- async setCredential(value) {
196
+ async setCredential(value, name) {
186
197
  if (typeof value !== 'string' || value.trim() === '') throw new Error('API Key 不能为空')
187
- const ref = this.config.credentialRef
198
+ const requested = typeof name === 'string' ? name.trim() : ''
199
+ if (requested && !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(requested)) {
200
+ throw new Error('凭据名不合法:只允许字母/数字/下划线,且以字母或下划线开头(如 DEEPSEEK_API_KEY)')
201
+ }
202
+ const ref = requested || this.effectiveCredentialRef()
188
203
  if (!ref) {
189
- throw new Error('未配置 credentialRef:请在插件配置里指定凭据名(如 DEEPSEEK_API_KEY),'
190
- + '或改用 DSH 的 Models 页配置 Key')
204
+ throw new Error('还没有凭据名:请在面板「凭据名」里填一个(如 DEEPSEEK_API_KEY),'
205
+ + '或在插件配置里设置 credentialRef。凭据名不是密钥本身,只是"保存位置"的名字。')
191
206
  }
192
207
  if (typeof this.credentials?.set !== 'function') {
193
- throw new Error('当前环境不支持写入凭据库:请在 DSH Models 页配置,或用插件配置 llmApiKey')
208
+ throw new Error('当前环境不支持写入 DSH 凭据库:请在 DSH Models 页配置 Key,或用插件配置 llmApiKey')
194
209
  }
195
210
  await this.credentials.set(ref, value.trim())
211
+ // 记住这个名字(只存名字,不存密钥),下次启动后审计仍能找到它
212
+ if (this.settingsStore) {
213
+ try {
214
+ this.settingsStore.save({ ...this.settings(), credentialRef: ref })
215
+ } catch (error) {
216
+ this.logger?.warn?.(`[calibguard] 记住凭据名失败(不影响本次写入):${error?.message ?? error}`)
217
+ }
218
+ }
196
219
  this.lastCredential = { source: 'credentials:set', configured: true }
197
220
  this.logger?.info?.(`[calibguard] 已写入凭据引用 ${ref}(值不落日志)`)
198
221
  return { ref, saved: true }
@@ -207,6 +230,8 @@ export class CalibGuardEngine {
207
230
  */
208
231
  async #prepare(options) {
209
232
  const merged = { ...this.baseOptions, ...this.settings(), ...options }
233
+ // credentialRef 是本插件 Node 侧的配置,不下发给桥接进程
234
+ delete merged.credentialRef
210
235
  if (!merged.llmApiKey) {
211
236
  const { key } = await this.resolveCredential()
212
237
  if (key) merged.llmApiKey = key
@@ -475,7 +500,7 @@ export class CalibGuardEngine {
475
500
  pendingRequests: this.pending.size,
476
501
  lastExit: this.lastExit,
477
502
  credential: { ...this.lastCredential },
478
- credentialRef: this.config.credentialRef || null,
503
+ credentialRef: this.effectiveCredentialRef() || null,
479
504
  credentialWritable: typeof this.credentials?.set === 'function',
480
505
  environment: this.bootstrap.snapshot(),
481
506
  settings: this.settings(),
package/lib/routes.js CHANGED
@@ -130,6 +130,9 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
130
130
  }
131
131
  }
132
132
 
133
+ /** 面板提示用:`node <setupScript> --reinstall-torch` 的绝对路径。 */
134
+ const setupScript = join(config.packageRoot ?? '', 'setup.mjs')
135
+
133
136
  /** GET /status */
134
137
  async function handleStatus(req, res) {
135
138
  if (req.method !== 'GET') return sendJson(res, 405, { error: '仅支持 GET' })
@@ -145,6 +148,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
145
148
  credential: diagnostics.credential,
146
149
  credentialRef: diagnostics.credentialRef ?? null,
147
150
  credentialWritable: Boolean(diagnostics.credentialWritable),
151
+ setupScript,
148
152
  settings: engine.settings(),
149
153
  settingsFile: settingsStore?.file ?? null,
150
154
  })
@@ -158,6 +162,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
158
162
  credential: diagnostics.credential,
159
163
  credentialRef: diagnostics.credentialRef ?? null,
160
164
  credentialWritable: Boolean(diagnostics.credentialWritable),
165
+ setupScript,
161
166
  settings: engine.settings(),
162
167
  settingsFile: settingsStore?.file ?? null,
163
168
  })
@@ -174,6 +179,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
174
179
  credential: diagnostics.credential,
175
180
  credentialRef: diagnostics.credentialRef ?? null,
176
181
  credentialWritable: Boolean(diagnostics.credentialWritable),
182
+ setupScript,
177
183
  settings: engine.settings(),
178
184
  settingsFile: settingsStore?.file ?? null,
179
185
  })
@@ -196,10 +202,14 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
196
202
  }
197
203
  const apiKey = typeof payload?.apiKey === 'string' ? payload.apiKey : ''
198
204
  if (!apiKey.trim()) return sendJson(res, 400, { error: 'apiKey 不能为空' })
205
+ // 凭据名可以由面板填写(不写死在代码里);留空则用插件配置里的 credentialRef
206
+ const name = typeof payload?.name === 'string' ? payload.name.trim() : ''
199
207
  if (typeof engine.setCredential !== 'function') {
200
208
  return sendJson(res, 503, { error: '当前插件版本不支持写入凭据' })
201
209
  }
202
- const result = await engine.setCredential(apiKey)
210
+ const result = await engine.setCredential(apiKey, name || undefined)
211
+ const ref = result && result.ref ? result.ref : null
212
+ sendJson(res, 200, { ok: true, ...result, credentialRef: ref, settings: engine.settings() })
203
213
  sendJson(res, 200, { ok: true, ...result })
204
214
  } catch (error) {
205
215
  sendJson(res, 400, { error: String(error?.message ?? error) })
package/lib/settings.js CHANGED
@@ -33,6 +33,8 @@ export const SETTINGS_KEYS = [
33
33
  'llmWindowChars',
34
34
  'llmWindowOverlap',
35
35
  'maxFiles',
36
+ // 凭据名(保存位置的名字,不是密钥本身)。只在本插件 Node 侧使用,不会下发给桥接进程。
37
+ 'credentialRef',
36
38
  ]
37
39
 
38
40
  /** DSH home:优先环境变量,其次 `~/.dsh`。 */
@@ -69,6 +71,12 @@ export function validateSettings(raw) {
69
71
  throw new Error('设置必须是 JSON 对象')
70
72
  }
71
73
  const picked = pickSettings(raw ?? {})
74
+ if (picked.credentialRef !== undefined) {
75
+ if (typeof picked.credentialRef !== 'string'
76
+ || !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(picked.credentialRef)) {
77
+ throw new Error('credentialRef 必须是合法凭据名(字母/数字/下划线,且以字母或下划线开头,如 DEEPSEEK_API_KEY)')
78
+ }
79
+ }
72
80
  validateAuditOptions(picked)
73
81
  return picked
74
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calibguard",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/setup.mjs CHANGED
@@ -28,14 +28,22 @@ import { spawn, spawnSync } from 'node:child_process'
28
28
  import { fileURLToPath } from 'node:url'
29
29
  import { homedir } from 'node:os'
30
30
  import { Readable } from 'node:stream'
31
+ import { pipeline as streamPipeline } from 'node:stream/promises'
31
32
 
32
33
  const PKG_ROOT = dirname(fileURLToPath(import.meta.url))
33
34
  /** torch 官方轮子目前覆盖的 Python 版本上限(3.13/3.14 装不上 torch)。 */
34
35
  const PY_MIN = 10
35
36
  const PY_MAX = 12
36
37
 
37
- /** 引擎下载的镜像前缀:GitHub 直连失败时依次重试。 */
38
- const ENGINE_MIRRORS = ['https://gh-proxy.com/', 'https://ghfast.top/', 'https://ghproxy.cn/']
38
+ /** 引擎/模型下载的镜像前缀:GitHub 直连失败时依次重试(均实测支持 Range 分段)。 */
39
+ const ENGINE_MIRRORS = ['https://gh-proxy.com/', 'https://ghfast.top/', 'https://ghproxy.net/']
40
+
41
+ /** CUDA 版 torch(与 CG_GUI launcher 相同的定档口径)。 */
42
+ const TORCH_CUDA_INDEX = process.env.DSH_CALIBGUARD_TORCH_INDEX || 'https://download.pytorch.org/whl/cu128'
43
+ const CUDA_MIN = [12, 8]
44
+ const CUDA_MIN_COMPUTE_CAP = [7, 5]
45
+ /** 并发分段下载的段数(0/负数 = 单连接)。 */
46
+ const DOWNLOAD_CONNECTIONS = Number(process.env.DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS || 4)
39
47
  /** pip 索引:默认清华镜像,失败回退官方 PyPI。 */
40
48
  const DEFAULT_PIP_INDEX = 'https://pypi.tuna.tsinghua.edu.cn/simple'
41
49
  const OFFICIAL_PIP_INDEX = 'https://pypi.org/simple'
@@ -71,10 +79,79 @@ function mirroredUrls(url) {
71
79
  return urls
72
80
  }
73
81
 
74
- /** pip 索引候选(自定义 → 清华镜像 → 官方源)。 */
82
+ /** pip 索引候选(自定义 → 清华 → 阿里 → 中科大 → 腾讯 → 官方)。 */
75
83
  function pipIndexes() {
76
84
  const custom = String(process.env.DSH_CALIBGUARD_PIP_INDEX || process.env.PIP_INDEX_URL || '').trim()
77
- return [...new Set([custom, DEFAULT_PIP_INDEX, OFFICIAL_PIP_INDEX].filter(Boolean))]
85
+ return [...new Set([
86
+ custom,
87
+ DEFAULT_PIP_INDEX,
88
+ 'https://mirrors.aliyun.com/pypi/simple/',
89
+ 'https://pypi.mirrors.ustc.edu.cn/simple/',
90
+ 'https://mirrors.cloud.tencent.com/pypi/simple',
91
+ OFFICIAL_PIP_INDEX,
92
+ ].filter(Boolean))]
93
+ }
94
+
95
+ /** 比较版本元组:[a,b] vs [c,d]。 */
96
+ function cmpVersion(a, b) {
97
+ return (a[0] - b[0]) || ((a[1] ?? 0) - (b[1] ?? 0))
98
+ }
99
+
100
+ function fmtVersion(version) {
101
+ return Array.isArray(version) ? version.slice(0, 2).join('.') : '未知'
102
+ }
103
+
104
+ /** 用 nvidia-smi 探测 GPU:返回 { hasNvidia, gpuName, computeCap, cudaVersion }。 */
105
+ function detectGpu() {
106
+ const probe = (args) => {
107
+ const result = spawnSync('nvidia-smi', args, { encoding: 'utf8', timeout: 20_000 })
108
+ if (result.error || result.status !== 0) return ''
109
+ return String(result.stdout || '').trim()
110
+ }
111
+ const csv = probe(['--query-gpu=name,compute_cap', '--format=csv,noheader'])
112
+ if (!csv) return { hasNvidia: false, gpuName: null, computeCap: null, cudaVersion: null }
113
+ const first = csv.split(/\r?\n/)[0] || ''
114
+ const parts = first.split(',').map((item) => item.trim())
115
+ const gpuName = parts[0] || null
116
+ let computeCap = null
117
+ if (parts[1] && /^\d+\.\d+$/.test(parts[1])) {
118
+ computeCap = parts[1].split('.').map(Number)
119
+ }
120
+ const header = probe([])
121
+ const match = /CUDA Version:\s*(\d+)\.(\d+)/.exec(header)
122
+ const cudaVersion = match ? [Number(match[1]), Number(match[2])] : null
123
+ return { hasNvidia: true, gpuName, computeCap, cudaVersion }
124
+ }
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' }
130
+ }
131
+ if (!gpu.hasNvidia) return { useCuda: false, reason: '未检测到 NVIDIA 显卡(nvidia-smi 不可用)' }
132
+ if (!gpu.cudaVersion || cmpVersion(gpu.cudaVersion, CUDA_MIN) < 0) {
133
+ return {
134
+ useCuda: false,
135
+ reason: `CUDA 版本 ${fmtVersion(gpu.cudaVersion)} < ${fmtVersion(CUDA_MIN)}(或无法探测)`,
136
+ }
137
+ }
138
+ if (gpu.computeCap && cmpVersion(gpu.computeCap, CUDA_MIN_COMPUTE_CAP) < 0) {
139
+ return {
140
+ useCuda: false,
141
+ reason: `算力 ${fmtVersion(gpu.computeCap)} < ${fmtVersion(CUDA_MIN_COMPUTE_CAP)}(cu128 轮子无对应 kernel)`,
142
+ }
143
+ }
144
+ return { useCuda: true, reason: `算力 ${fmtVersion(gpu.computeCap)} | CUDA ${fmtVersion(gpu.cudaVersion)}` }
145
+ }
146
+
147
+ /** 已安装 torch 的 CUDA 版本(空串 = CPU 版;null = 未安装/无法导入)。 */
148
+ function installedTorchCuda(python) {
149
+ const result = spawnSync(python, ['-c', 'import torch;print(torch.version.cuda or "")'], {
150
+ encoding: 'utf8',
151
+ timeout: 180_000,
152
+ })
153
+ if (result.error || result.status !== 0) return null
154
+ return String(result.stdout || '').trim()
78
155
  }
79
156
 
80
157
  /** 插件内置引擎目录:CG_GUI 核心文件(代码 / 词表 / 校准参数),不含 476 MB 模型权重。 */
@@ -122,7 +199,7 @@ async function fetchModelWeights(dest) {
122
199
  for (const url of mirroredUrls(MODEL_URL)) {
123
200
  try {
124
201
  log(`下载模型权重(约 442 MB):${url}`)
125
- await download(url, zipPath)
202
+ await downloadParallel(url, zipPath)
126
203
  const size = statSync(zipPath).size
127
204
  if (size < MODEL_ZIP_MIN_BYTES) {
128
205
  throw new SetupError(`下载内容异常(${bytes(size)},预期约 442 MB,可能是镜像错误页)`)
@@ -233,6 +310,7 @@ function parseArgs(argv) {
233
310
  force: false,
234
311
  noPortablePython: false,
235
312
  forcePortablePython: false,
313
+ reinstallTorch: false,
236
314
  help: false,
237
315
  }
238
316
  for (let i = 0; i < argv.length; i += 1) {
@@ -255,6 +333,7 @@ function parseArgs(argv) {
255
333
  case '--force': options.force = true; break
256
334
  case '--no-portable-python': options.noPortablePython = true; break
257
335
  case '--force-portable-python': options.forcePortablePython = true; break
336
+ case '--reinstall-torch': options.reinstallTorch = true; break
258
337
  default: fail(`未知参数:${arg}(用 --help 查看用法)`)
259
338
  }
260
339
  }
@@ -280,6 +359,7 @@ function printHelp() {
280
359
  --force 覆盖已有引擎目录 / 重建 venv
281
360
  --no-portable-python 本机没有 Python 时不自动下载便携版(默认会自动下载)
282
361
  --force-portable-python 跳过本机 Python 检测,强制用便携版(测试用)
362
+ --reinstall-torch 重新安装 torch(检测到 NVIDIA 时装 CUDA/cu128 版,约 2.5 GB)
283
363
  -h, --help 显示本帮助
284
364
 
285
365
  示例:
@@ -297,8 +377,10 @@ function printHelp() {
297
377
  • pip 默认用清华镜像(pypi.tuna.tsinghua.edu.cn),失败自动回退官方 PyPI;
298
378
  可用 DSH_CALIBGUARD_PIP_INDEX 指定其它索引;
299
379
  • 从 npm 安装时引擎默认放在 ~/.dsh/calibguard-engine(可用 --dest 或插件配置 engineRoot 指定);
300
- • 默认用 CPU 版 torch(低算力显卡本来也会被定档到 CPU);
301
- 要用 GPU 请按 PyTorch 官网自行安装对应 CUDA 轮子。
380
+ • GPU 自动定档:检测到 NVIDIA 且 CUDA≥12.8、算力≥7.5(如 RTX 20/30/40 系)时
381
+ 自动安装 CUDA(cu128)版 torch;否则用 CPU 版;可用 DSH_CALIBGUARD_CPU_ONLY=1 强制 CPU;
382
+ • 大文件(模型/引擎包)默认 4 路并发分段下载,可用 DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS 调整;
383
+ • 装完想换 GPU/CPU 版 torch:node setup.mjs --reinstall-torch。
302
384
  `)
303
385
  }
304
386
 
@@ -410,6 +492,119 @@ async function download(url, file) {
410
492
  log(`下载完成:${bytes(received)}(用时 ${Math.round((Date.now() - startedAt) / 1000)} 秒)`)
411
493
  }
412
494
 
495
+ /** 下载一个区间的分片(支持断点续传/重试)。 */
496
+ async function downloadRange(url, file, start, end, { attempts = 3, idleMs = 120_000 } = {}) {
497
+ const total = end - start + 1
498
+ let lastError = null
499
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
500
+ const offset = existsSync(file) ? statSync(file).size : 0
501
+ if (offset >= total) return
502
+ const controller = new AbortController()
503
+ let idleTimer = null
504
+ const resetIdle = () => {
505
+ if (idleTimer) clearTimeout(idleTimer)
506
+ idleTimer = setTimeout(() => controller.abort(new Error(`${Math.round(idleMs / 1000)} 秒没有收到数据`)), idleMs)
507
+ }
508
+ let response
509
+ try {
510
+ resetIdle()
511
+ response = await fetch(url, {
512
+ redirect: 'follow',
513
+ signal: controller.signal,
514
+ headers: { Range: `bytes=${start + offset}-${end}` },
515
+ })
516
+ } catch (error) {
517
+ if (idleTimer) clearTimeout(idleTimer)
518
+ lastError = error
519
+ warn(`分片 ${start}-${end} 第 ${attempt} 次失败:${error.message}`)
520
+ continue
521
+ }
522
+ if (response.status !== 206 && !(response.status === 200 && offset === 0 && start === 0)) {
523
+ if (idleTimer) clearTimeout(idleTimer)
524
+ lastError = new SetupError(`分片请求返回 HTTP ${response.status}`)
525
+ continue
526
+ }
527
+ const sink = createWriteStream(file, { flags: offset > 0 ? 'a' : 'w' })
528
+ const reader = Readable.fromWeb(response.body)
529
+ try {
530
+ await new Promise((resolvePromise, rejectPromise) => {
531
+ reader.on('data', (chunk) => { resetIdle() })
532
+ reader.on('error', (error) => { clearTimeout(idleTimer); rejectPromise(error) })
533
+ sink.on('error', (error) => { clearTimeout(idleTimer); rejectPromise(error) })
534
+ sink.on('finish', () => { clearTimeout(idleTimer); resolvePromise() })
535
+ reader.pipe(sink)
536
+ })
537
+ } catch (error) {
538
+ lastError = error
539
+ warn(`分片 ${start}-${end} 第 ${attempt} 次中断:${error.message}(已保留断点)`)
540
+ continue
541
+ }
542
+ if (statSync(file).size >= total) return
543
+ lastError = new SetupError('分片下载不完整')
544
+ }
545
+ throw new SetupError(`分片 ${start}-${end} 下载失败:${lastError?.message ?? '未知错误'}`)
546
+ }
547
+
548
+ /**
549
+ * 并发分段下载(大幅提升 GitHub/镜像的单连接限速场景)。
550
+ * 先探测是否支持 Range;不支持或文件不够大时自动退回单连接 download()。
551
+ */
552
+ async function downloadParallel(url, file, { connections = DOWNLOAD_CONNECTIONS } = {}) {
553
+ if (!connections || connections < 2) return download(url, file)
554
+ let total = 0
555
+ let supportsRange = false
556
+ try {
557
+ const probe = await fetch(url, {
558
+ redirect: 'follow',
559
+ headers: { Range: 'bytes=0-0' },
560
+ signal: AbortSignal.timeout(60_000),
561
+ })
562
+ if (probe.ok) {
563
+ supportsRange = probe.status === 206
564
+ const contentRange = probe.headers.get('content-range') || ''
565
+ const sizePart = contentRange.split('/')[1]
566
+ total = Number(sizePart || probe.headers.get('content-length') || 0)
567
+ try { await probe.arrayBuffer() } catch { /* 忽略探测体 */ }
568
+ }
569
+ } catch {
570
+ supportsRange = false
571
+ }
572
+ if (!supportsRange || !(total > 0) || total < 32 * 1024 ** 2) return download(url, file)
573
+
574
+ const chunk = Math.ceil(total / connections)
575
+ const parts = []
576
+ for (let index = 0; index < connections; index += 1) {
577
+ const start = index * chunk
578
+ const end = Math.min(total - 1, start + chunk - 1)
579
+ if (start > end) break
580
+ parts.push({ start, end, file: `${file}.part${index}`, percent: 0 })
581
+ }
582
+ log(`分段并发下载:${parts.length} 路,共 ${bytes(total)}(单连接慢时明显提速)`)
583
+ const startedAt = Date.now()
584
+ let finished = 0
585
+ await Promise.all(parts.map(async (part) => {
586
+ await downloadRange(url, part.file, part.start, part.end)
587
+ finished += 1
588
+ log(` 分片 ${finished}/${parts.length} 完成(${bytes(statSync(part.file).size)})`)
589
+ }))
590
+ log(`分段下载完成:${bytes(total)}(用时 ${Math.round((Date.now() - startedAt) / 1000)} 秒),开始合并…`)
591
+ const temp = `${file}.merging`
592
+ deleteFile(temp)
593
+ await streamPipeline((async function* generate() {
594
+ for (const part of parts) {
595
+ for await (const chunkData of createReadStream(part.file)) yield chunkData
596
+ }
597
+ })(), createWriteStream(temp))
598
+ for (const part of parts) deleteFile(part.file)
599
+ if (statSync(temp).size !== total) {
600
+ deleteFile(temp)
601
+ throw new SetupError(`合并后大小不符:${bytes(statSync(temp).size)} / ${bytes(total)}`)
602
+ }
603
+ deleteFile(file)
604
+ renameSync(temp, file)
605
+ log(`✓ 分段下载合并完成:${file}(${bytes(statSync(file).size)})`)
606
+ }
607
+
413
608
  // ============================== 解压 ==============================
414
609
 
415
610
  function extract(zipFile, dest) {
@@ -688,7 +883,7 @@ async function main() {
688
883
  let lastError = null
689
884
  for (const candidate of candidates) {
690
885
  try {
691
- await download(candidate, downloadedZip)
886
+ await downloadParallel(candidate, downloadedZip)
692
887
  if (statSync(downloadedZip).size < 100 * 1024 ** 2) {
693
888
  throw new SetupError('下载内容明显偏小,可能是镜像错误页')
694
889
  }
@@ -804,7 +999,40 @@ async function main() {
804
999
  throw lastError ?? new SetupError('pip 安装失败')
805
1000
  }
806
1001
  await installWithMirrors(['--upgrade', 'pip', '--quiet'])
807
- await installWithMirrors(['-r', requirements])
1002
+
1003
+ // GPU 定档:有可用 NVIDIA(CUDA≥12.8、算力≥7.5)时先装 CUDA 版 torch,
1004
+ // 与 CG_GUI launcher 的口径一致;否则装 CPU 版 torch。
1005
+ const gpu = detectGpu()
1006
+ const decision = decideCudaTorch(gpu)
1007
+ const gpuLabel = gpu.gpuName ? `(${gpu.gpuName})` : ''
1008
+ log(`GPU 定档:${decision.useCuda ? 'CUDA(cu128)' : 'CPU'} —— ${decision.reason}${gpuLabel}`)
1009
+ const existingTorchCuda = installedTorchCuda(python)
1010
+ if (decision.useCuda && (!existingTorchCuda || options.reinstallTorch)) {
1011
+ step(`安装 CUDA 版 torch(cu128,约 2.5 GB${existingTorchCuda ? ',重装' : ''})`)
1012
+ if (options.reinstallTorch) {
1013
+ try {
1014
+ await runLive(python, ['-m', 'pip', 'uninstall', '-y', 'torch'])
1015
+ } catch { /* 未安装则忽略 */ }
1016
+ }
1017
+ try {
1018
+ await runLive(python, [
1019
+ '-m', 'pip', 'install', 'torch', '--index-url', TORCH_CUDA_INDEX,
1020
+ '--timeout', '300', '--retries', '3',
1021
+ ])
1022
+ } catch (error) {
1023
+ warn(`CUDA 版 torch 安装失败(${error.message}),回退 CPU 版 torch`)
1024
+ }
1025
+ } else if (existingTorchCuda) {
1026
+ log(`✓ 已安装 CUDA 版 torch(CUDA ${existingTorchCuda})`)
1027
+ }
1028
+
1029
+ await installWithMirrors(['-r', requirements, '--prefer-binary'])
1030
+ const finalTorchCuda = installedTorchCuda(python)
1031
+ log(`torch CUDA:${finalTorchCuda || '无(CPU 版)'}`)
1032
+ if (decision.useCuda && !finalTorchCuda) {
1033
+ warn('检测到可用 GPU 但 torch 是 CPU 版:审计仍可运行,只是用 CPU。'
1034
+ + '可重跑:node setup.mjs --reinstall-torch')
1035
+ }
808
1036
  log('✓ 依赖安装完成')
809
1037
  }
810
1038