dsh-calibguard 0.3.2 → 0.3.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 +40 -6
- package/README.md +36 -5
- package/bridge/calibguard_bridge.py +7 -1
- package/cordis.patch.yml +3 -2
- package/docs/configuration.md +15 -4
- package/docs/troubleshooting.md +6 -0
- package/lib/client.js +92 -7
- package/lib/engine.js +35 -10
- package/lib/routes.js +11 -1
- package/lib/settings.js +8 -0
- package/package.json +1 -1
- package/setup.mjs +264 -9
package/README.en.md
CHANGED
|
@@ -68,6 +68,24 @@ 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
|
+
|
|
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
|
+
|
|
71
89
|
**Where to watch progress**: the panel shows a "First run: preparing engine and dependencies" card with
|
|
72
90
|
the current step and the tail of the installer output; host logs show `[calibguard]` lines. The audit
|
|
73
91
|
button stays disabled until it is ready — press "刷新状态 / Refresh" afterwards. Expect **10–20 minutes**
|
|
@@ -92,13 +110,17 @@ once (depends on network and pip mirror).
|
|
|
92
110
|
|
|
93
111
|
### Option 0 — desktop "Add plugin" dialog (easiest, recommended)
|
|
94
112
|
|
|
95
|
-
DSH desktop
|
|
113
|
+
**On DSH desktop it is one step: Settings → Add plugin → enter `dsh-calibguard` → restart DSH.**
|
|
114
|
+
That installs the latest npm release; to pin a version write `dsh-calibguard@0.3.3` (`@` + version).
|
|
115
|
+
No terminal and no source download needed.
|
|
116
|
+
|
|
117
|
+
The dialog also accepts any of the following (restart DSH afterwards):
|
|
96
118
|
|
|
97
119
|
| Paste | Example | Notes |
|
|
98
120
|
|---|---|---|
|
|
99
121
|
| **GitHub repository URL** (recommended) | `https://github.com/maomitgd2006/dsh-CalibGuard` | Fetches the source and registers the bundle in the current profile |
|
|
100
122
|
| **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**;
|
|
123
|
+
| **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
124
|
|
|
103
125
|
> Equivalent CLI (verified: after install DSH automatically appends the package to the profile's
|
|
104
126
|
> `dsh.profile.bundles`):
|
|
@@ -201,7 +223,14 @@ After pressing Save:
|
|
|
201
223
|
- values are written to `$DSH_HOME/calibguard-settings.json` (defaults to `~/.dsh`, never committed);
|
|
202
224
|
- **the AI uses the same values** (precedence: plugin config < shared settings < per-call options);
|
|
203
225
|
- changes take effect immediately, no restart;
|
|
204
|
-
- the API key goes into the DSH credential store (`ctx.credentials`), not into that settings file
|
|
226
|
+
- the API key goes into the DSH credential store (`ctx.credentials`), not into that settings file;
|
|
227
|
+
- the **credential name** is only the name of the storage slot (not the secret): fill it in the panel
|
|
228
|
+
(e.g. `DEEPSEEK_API_KEY`) together with the API key; the name is remembered in shared settings
|
|
229
|
+
(`credentialRef`) and the value is written to the DSH credential store. Leave it empty to use the
|
|
230
|
+
plugin config `credentialRef`;
|
|
231
|
+
- **audit results**: every audit writes JSON + Markdown into the engine's `reports/` directory
|
|
232
|
+
(default `<engineRoot>/reports`, `engineRoot` defaults to `~/.dsh/calibguard-engine`). The panel shows
|
|
233
|
+
that path, has a "clear results" button, and each item links to its Markdown report.
|
|
205
234
|
|
|
206
235
|
### 4) Tool parameters (per-call, advanced)
|
|
207
236
|
|
|
@@ -228,7 +257,7 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
|
|
|
228
257
|
| `enabled` | `true` | When false nothing is registered (no need to remove the row) |
|
|
229
258
|
| `engineRoot` | `''` | CG_GUI directory; empty = the `CG_GUI` next to this package |
|
|
230
259
|
| `device` | `auto` | Checks both CUDA build and compute capability; low-end GPUs fall back to CPU |
|
|
231
|
-
| `credentialRef` | `''` |
|
|
260
|
+
| `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
261
|
| `announceToModel` | `true` | Add "when to use CalibGuard" to the system prompt |
|
|
233
262
|
| `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
263
|
| `maxFiles` | `20` | Directory recursion cap; explicitly given/uploaded files are not capped |
|
|
@@ -240,10 +269,15 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
|
|
|
240
269
|
| Symptom | Fix |
|
|
241
270
|
|---|---|
|
|
242
271
|
| 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 (
|
|
272
|
+
| **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
273
|
| First audit is slow | Normal: ~475 MB of weights are loaded once (then the process stays resident) |
|
|
245
274
|
| 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
|
|
275
|
+
| 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 |
|
|
276
|
+
| **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 |
|
|
277
|
+
| **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` |
|
|
278
|
+
| **"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" |
|
|
279
|
+
| **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 |
|
|
280
|
+
| **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 |
|
|
247
281
|
| 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
282
|
| Panel returns 401/403 | Browser session isn't authenticated with the host: refresh / sign in again |
|
|
249
283
|
| 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,23 @@
|
|
|
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
|
+
|
|
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
|
+
|
|
65
82
|
**进度在哪看**:面板顶部会出现「首次运行:正在自动准备引擎与依赖」卡片(含当前步骤与输出尾巴);
|
|
66
83
|
宿主日志里是 `[calibguard]` 开头的行。准备期间审计按钮不可用,完成后点「刷新状态」即可。
|
|
67
84
|
首次约需 **10–20 分钟**(取决于网速和 pip 源),只需一次。
|
|
@@ -85,13 +102,17 @@
|
|
|
85
102
|
|
|
86
103
|
### 方式 0:桌面端「添加插件」(最省事,推荐)
|
|
87
104
|
|
|
88
|
-
DSH
|
|
105
|
+
**DSH 桌面版只需要一步:设置 → 添加插件 → 输入 `dsh-calibguard` → 重启 DSH。**
|
|
106
|
+
默认装 npm 上的最新版;要固定版本就写 `dsh-calibguard@0.3.3`(`@` 后面是版本号)。
|
|
107
|
+
不需要命令行、不需要下载源码。
|
|
108
|
+
|
|
109
|
+
输入框里也可以填以下任意一种(装完按提示重启 DSH):
|
|
89
110
|
|
|
90
111
|
| 填什么 | 示例 | 说明 |
|
|
91
112
|
|---|---|---|
|
|
92
113
|
| **GitHub 仓库地址**(推荐) | `https://github.com/maomitgd2006/dsh-CalibGuard` | 自动拉取源码并登记到当前 profile 的 bundle |
|
|
93
114
|
| **本地目录路径** | `<你的插件目录>`(例:`D:\code\dsh-calibguard`) | 已 clone / 解压到本地时用 |
|
|
94
|
-
| **包名** | `dsh-calibguard` | ✅ **已发布到 npm
|
|
115
|
+
| **包名** | `dsh-calibguard` 或 `dsh-calibguard@0.3.3` | ✅ **已发布到 npm**;不写版本=装 latest,写 `@版本`=固定版本 |
|
|
95
116
|
|
|
96
117
|
> 等价命令行(已实测:安装后 DSH 会自动把包加进 profile 的 `dsh.profile.bundles`):
|
|
97
118
|
>
|
|
@@ -191,7 +212,12 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
|
|
|
191
212
|
- 参数写入 `$DSH_HOME/calibguard-settings.json`(默认 `~/.dsh`,不写进仓库);
|
|
192
213
|
- **AI 调用同样使用这份设置**(优先级:插件配置 < 共享设置 < 本次调用参数);
|
|
193
214
|
- 改完立即生效,**不用重启**;
|
|
194
|
-
- API Key 写入 DSH 凭据库(`ctx.credentials
|
|
215
|
+
- API Key 写入 DSH 凭据库(`ctx.credentials`),不落入该设置文件;
|
|
216
|
+
- **凭据名**只是"保存位置"的名字(不是密钥本身):面板里填(如 `DEEPSEEK_API_KEY`)+ API Key,
|
|
217
|
+
保存后名字记进共享设置的 `credentialRef`,值写进 DSH 凭据库;留空则用插件配置里的 `credentialRef`;
|
|
218
|
+
- **审计结果**:每次审计在引擎的 `reports/` 目录生成 JSON + Markdown(默认 `<engineRoot>/reports`,
|
|
219
|
+
`engineRoot` 默认 `~/.dsh/calibguard-engine`)。面板结果区会显示该路径并可「清空结果」,
|
|
220
|
+
条目里的「查看 Markdown 报告」可直接打开。
|
|
195
221
|
|
|
196
222
|
### 4) 工具参数(按次覆盖,写给进阶用户)
|
|
197
223
|
|
|
@@ -218,7 +244,7 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
|
|
|
218
244
|
| `enabled` | `true` | 关掉则什么都不注册(临时停用不必删行) |
|
|
219
245
|
| `engineRoot` | `''` | 引擎目录;留空自动推导:**npm/桌面端安装**时为 `~/.dsh/calibguard-engine`,源码仓库里为本包同级 `CG_GUI` |
|
|
220
246
|
| `device` | `auto` | 自动做「CUDA 版本 + 算力」双检,低算力显卡回落 CPU |
|
|
221
|
-
| `credentialRef` | `''` |
|
|
247
|
+
| `credentialRef` | `''` | 云端复核凭据**名**(保存位置,不是密钥),如 `DEEPSEEK_API_KEY`;留空=用面板「凭据名」保存的名字;值存放在 DSH 凭据库/环境变量 |
|
|
222
248
|
| `announceToModel` | `true` | 是否把「何时该用 CalibGuard」写进系统提示词 |
|
|
223
249
|
| `autoSetup` | `true` | 首次运行缺 `.venv` 时**自动**准备引擎(内置代码 + npm 模型分片)、按需下载便携 Python、安装依赖;设 `false` 则只报错提示手动 `node setup.mjs` |
|
|
224
250
|
| `maxFiles` | `20` | 目录展开上限;显式给出/上传的文件不受限 |
|
|
@@ -232,8 +258,13 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
|
|
|
232
258
|
| 侧边栏没有图标 / 面板报 `not found` | 客户端与路由在宿主启动时加载:**重启 DSH** |
|
|
233
259
|
| **首次运行要等十几分钟** | 正常:首次下载模型(约 442 MB,自动镜像回退)并安装依赖(1–3 GB);面板顶部有进度卡片,只需一次 |
|
|
234
260
|
| 首次审计很慢 | 正常:要加载约 475 MB 权重(之后走常驻进程) |
|
|
261
|
+
| **面板保存 API Key 提示"需要 credentialRef"** | 升级到 ≥ **0.3.3**:面板「凭据名」可直接填写并记住(不用改插件配置);旧版本请把 `credentialRef` 写进插件配置 |
|
|
262
|
+
| **审计结果保存在哪 / 想清空** | 保存在引擎 `reports/` 目录(默认 `<engineRoot>/reports`),每次审计生成 JSON + Markdown;面板结果区显示路径并带「清空结果」按钮;AI 工具返回 `report_md_path` / `report_json_path` |
|
|
263
|
+
| **审计时总提示"首次会加载模型"** | 0.3.3 起只在模型尚未加载时提示;加载后按钮只显示「审计中…」,顶部状态显示「模型 已加载」 |
|
|
264
|
+
| **有 NVIDIA 显卡(如 RTX 2060)但一直显示 CPU** | 说明装的是 CPU 版 torch(旧版安装器行为)。升级到 ≥0.3.3 后面板会给出提示;在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装上 CUDA 版即可,之后重启 DSH |
|
|
265
|
+
| **想强制用 GPU / CPU 版 torch** | 安装时加 `--force-cuda` / `--cpu`(配合 `--reinstall-torch` 可覆盖已装版本);运行时设备由插件配置 `device`(auto/cpu/cuda)决定,`cuda` 需要 CUDA 版 torch |
|
|
235
266
|
| 大文件审计几十秒 | 正常:全文滑窗逐窗推理,见 [docs/performance.md](docs/performance.md) |
|
|
236
|
-
| 灰区全部转人工 |
|
|
267
|
+
| 灰区全部转人工 | 没配凭据:在面板「凭据名 + API Key」保存,或设 `credentialRef`、或在 DSH Models 页配置 |
|
|
237
268
|
| 正规第三方库被判 BLOCK | 压缩库充满 eval/base64 等特征,模型语料偏 Python 生态 → 属已知误报倾向,见 [docs/troubleshooting.md](docs/troubleshooting.md) |
|
|
238
269
|
| 面板报 401/403 | 浏览器会话未通过宿主鉴权:刷新页面/重新登录 |
|
|
239
270
|
| 改了参数 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
|
-
#
|
|
52
|
-
#
|
|
51
|
+
# 【推荐】云端复核凭据名(保存位置,不是密钥):交给 DSH 的 ctx.credentials 解析(继承环境 > 凭据库 > .env)。
|
|
52
|
+
# 名字可以写在这里,也可以留空、直接在面板「凭据名」里填(保存后记进共享设置);
|
|
53
|
+
# 插件代码不含硬编码凭据名,也不会隐式继承宿主环境变量。
|
|
53
54
|
# credentialRef: DEEPSEEK_API_KEY
|
|
54
55
|
# 备选:直接写字面值(不推荐,凭据会出现在配置文件里)
|
|
55
56
|
# llmApiKey: ''
|
package/docs/configuration.md
CHANGED
|
@@ -22,12 +22,18 @@
|
|
|
22
22
|
| `llmBaseUrl` / `llmModel` | 跟随引擎 | OpenAI 兼容端点与模型名 |
|
|
23
23
|
| `llmContentSource` | 跟随引擎 | 送审内容源:`evidence`(精炼证据)/ `raw`(预处理后全文) |
|
|
24
24
|
| `llmDeliveryMode` | 跟随引擎 | 投递方式:`truncate` / `window` / `all` |
|
|
25
|
-
| `credentialRef` | `''` |
|
|
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
|
+
> 手动覆盖:`--force-cuda` / `--cpu`(或环境变量)决定**安装哪个 build**;插件配置 `device`
|
|
34
|
+
> (auto/cpu/cuda)决定**运行时用哪个设备**。CPU 版与 CUDA 版 torch 不能共存于同一个 venv
|
|
35
|
+
>(同名包互斥);CUDA 版在无 GPU 时也能跑 CPU。
|
|
36
|
+
|
|
31
37
|
## 二、参数优先级
|
|
32
38
|
|
|
33
39
|
```
|
|
@@ -40,7 +46,7 @@
|
|
|
40
46
|
|
|
41
47
|
## 三、云端复核的 Key 从哪来(不硬编码、不隐式继承)
|
|
42
48
|
|
|
43
|
-
1. `credentialRef
|
|
49
|
+
1. `credentialRef`(推荐;插件配置或面板「凭据名」两者取其一)→ `ctx.credentials.resolve(ref)`,优先级
|
|
44
50
|
**继承环境 > `~/.dsh/.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`**;
|
|
45
51
|
解析到的值随请求走 **stdin** 下发,**不进 argv、不进子进程环境变量**,且每次操作重新解析(换 Key 下一次审计即生效)。
|
|
46
52
|
2. `llmApiKey`(字面值,次选)。
|
|
@@ -55,17 +61,22 @@
|
|
|
55
61
|
| 项 | 说明 |
|
|
56
62
|
|---|---|
|
|
57
63
|
| 文件 | `$DSH_HOME/calibguard-settings.json`(默认 `~/.dsh`,**不写进仓库**;可用 `settingsPath` 覆盖) |
|
|
58
|
-
| 内容 | 云端复核开关、Base URL
|
|
64
|
+
| 内容 | 云端复核开关、Base URL、模型名、送审内容、投递方式、截断/滑窗大小与重叠、灰区上下限、目录上限、**凭据名 `credentialRef`**(**不含密钥本身、不含模型侧步长**) |
|
|
59
65
|
| 写入 | 面板「保存」→ `PUT /api/calibguard/settings`;原子写,非法值不落盘(校验口径与 CG_GUI 一致) |
|
|
60
66
|
| 读取 | 面板加载时回填;AI 每次调用时由引擎合并,**改完立即生效、无需重启** |
|
|
61
67
|
| 重置 | 面板「重置」→ 清空为「跟随插件配置默认值」 |
|
|
62
|
-
| API Key |
|
|
68
|
+
| API Key | 面板「凭据名 + API Key」→ `PUT /api/calibguard/credential` → 值写入 DSH 凭据库(不落设置文件),名字记入共享设置 `credentialRef`(便于下次启动找到) |
|
|
63
69
|
|
|
64
70
|
## 五、环境变量
|
|
65
71
|
|
|
66
72
|
| 变量 | 作用 |
|
|
67
73
|
|---|---|
|
|
68
74
|
| `DSH_CALIBGUARD_PYTHON` | 指定桥接进程使用的 Python 解释器(优先于 `<engineRoot>/.venv`) |
|
|
75
|
+
| `DSH_CALIBGUARD_CPU_ONLY` | 设为 `1` 强制使用 CPU 版 torch(跳过 GPU 定档) |
|
|
76
|
+
| `DSH_CALIBGUARD_FORCE_CUDA` | 设为 `1` 强制安装 CUDA 版 torch(跳过 CUDA 版本/算力检查,风险自负) |
|
|
77
|
+
| `DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` | 大文件并发分段下载的连接数(默认 4;模型/引擎包下载提速) |
|
|
78
|
+
| `DSH_CALIBGUARD_TORCH_INDEX` | CUDA 版 torch 的安装索引(默认 `https://download.pytorch.org/whl/cu128`) |
|
|
79
|
+
| `DSH_CALIBGUARD_PIP_INDEX` | 指定 pip 主索引(默认依次尝试 清华 → 阿里 → 中科大 → 腾讯 → 官方) |
|
|
69
80
|
| `DSH_HOME` | DSH 主目录(默认 `~/.dsh`);决定共享设置文件位置 |
|
|
70
81
|
|
|
71
82
|
## 六、宿主 HTTP 路由(浏览器面板使用)
|
package/docs/troubleshooting.md
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
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
|
+
| **想强制装 GPU / CPU 版 torch** | 安装时加 `--force-cuda` / `--cpu`(可配合 `--reinstall-torch` 覆盖已装版本);CPU 版与 CUDA 版不能共存于同一 venv,CUDA 版在无 GPU 时会自动回落 CPU |
|
|
17
|
+
| **首次安装特别慢 / 提示某个源不可用** | 模型默认 4 路并发分段下载(`DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` 可调),GitHub 失败自动换 gh-proxy/ghfast/ghproxy 镜像;pip 自动在 清华/阿里/中科大/腾讯/官方 之间回退。实在慢可手动下载 `calibguard-model-1.0.0.zip` 放到缓存目录(见下一条) |
|
|
12
18
|
| **模型下载失败 / 想离线** | 手动下载 `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
19
|
| **首次运行卡在"正在准备环境"** | 正常:首次下载模型(约 442 MB,自动镜像回退)+ 装依赖(1–3 GB),约 10–20 分钟;面板顶部有进度与输出尾巴。若长时间无进度,可看 `~/.dsh/calibguard-tools/model-cache` 是否在增长 |
|
|
14
20
|
| 环境准备失败 | 面板会显示失败原因与最后几行输出;在本插件目录跑 `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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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. `
|
|
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 =
|
|
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
|
|
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('
|
|
190
|
-
+ '
|
|
204
|
+
throw new Error('还没有凭据名:请在面板「凭据名」里填一个(如 DEEPSEEK_API_KEY),'
|
|
205
|
+
+ '或在插件配置里设置 credentialRef。凭据名不是密钥本身,只是"保存位置"的名字。')
|
|
191
206
|
}
|
|
192
207
|
if (typeof this.credentials?.set !== 'function') {
|
|
193
|
-
throw new Error('
|
|
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.
|
|
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
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
|
-
/**
|
|
38
|
-
const ENGINE_MIRRORS = ['https://gh-proxy.com/', 'https://ghfast.top/', 'https://ghproxy.
|
|
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,94 @@ 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([
|
|
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
|
+
/**
|
|
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
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!gpu.hasNvidia) return { useCuda: false, reason: '未检测到 NVIDIA 显卡(nvidia-smi 不可用)' }
|
|
147
|
+
if (!gpu.cudaVersion || cmpVersion(gpu.cudaVersion, CUDA_MIN) < 0) {
|
|
148
|
+
return {
|
|
149
|
+
useCuda: false,
|
|
150
|
+
reason: `CUDA 版本 ${fmtVersion(gpu.cudaVersion)} < ${fmtVersion(CUDA_MIN)}(或无法探测)`,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (gpu.computeCap && cmpVersion(gpu.computeCap, CUDA_MIN_COMPUTE_CAP) < 0) {
|
|
154
|
+
return {
|
|
155
|
+
useCuda: false,
|
|
156
|
+
reason: `算力 ${fmtVersion(gpu.computeCap)} < ${fmtVersion(CUDA_MIN_COMPUTE_CAP)}(cu128 轮子无对应 kernel)`,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { useCuda: true, reason: `算力 ${fmtVersion(gpu.computeCap)} | CUDA ${fmtVersion(gpu.cudaVersion)}` }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 已安装 torch 的 CUDA 版本(空串 = CPU 版;null = 未安装/无法导入)。 */
|
|
163
|
+
function installedTorchCuda(python) {
|
|
164
|
+
const result = spawnSync(python, ['-c', 'import torch;print(torch.version.cuda or "")'], {
|
|
165
|
+
encoding: 'utf8',
|
|
166
|
+
timeout: 180_000,
|
|
167
|
+
})
|
|
168
|
+
if (result.error || result.status !== 0) return null
|
|
169
|
+
return String(result.stdout || '').trim()
|
|
78
170
|
}
|
|
79
171
|
|
|
80
172
|
/** 插件内置引擎目录:CG_GUI 核心文件(代码 / 词表 / 校准参数),不含 476 MB 模型权重。 */
|
|
@@ -122,7 +214,7 @@ async function fetchModelWeights(dest) {
|
|
|
122
214
|
for (const url of mirroredUrls(MODEL_URL)) {
|
|
123
215
|
try {
|
|
124
216
|
log(`下载模型权重(约 442 MB):${url}`)
|
|
125
|
-
await
|
|
217
|
+
await downloadParallel(url, zipPath)
|
|
126
218
|
const size = statSync(zipPath).size
|
|
127
219
|
if (size < MODEL_ZIP_MIN_BYTES) {
|
|
128
220
|
throw new SetupError(`下载内容异常(${bytes(size)},预期约 442 MB,可能是镜像错误页)`)
|
|
@@ -233,6 +325,9 @@ function parseArgs(argv) {
|
|
|
233
325
|
force: false,
|
|
234
326
|
noPortablePython: false,
|
|
235
327
|
forcePortablePython: false,
|
|
328
|
+
reinstallTorch: false,
|
|
329
|
+
forceCuda: false,
|
|
330
|
+
forceCpu: false,
|
|
236
331
|
help: false,
|
|
237
332
|
}
|
|
238
333
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -255,6 +350,9 @@ function parseArgs(argv) {
|
|
|
255
350
|
case '--force': options.force = true; break
|
|
256
351
|
case '--no-portable-python': options.noPortablePython = true; break
|
|
257
352
|
case '--force-portable-python': options.forcePortablePython = true; break
|
|
353
|
+
case '--reinstall-torch': options.reinstallTorch = true; break
|
|
354
|
+
case '--force-cuda': options.forceCuda = true; break
|
|
355
|
+
case '--cpu': options.forceCpu = true; break
|
|
258
356
|
default: fail(`未知参数:${arg}(用 --help 查看用法)`)
|
|
259
357
|
}
|
|
260
358
|
}
|
|
@@ -280,6 +378,9 @@ function printHelp() {
|
|
|
280
378
|
--force 覆盖已有引擎目录 / 重建 venv
|
|
281
379
|
--no-portable-python 本机没有 Python 时不自动下载便携版(默认会自动下载)
|
|
282
380
|
--force-portable-python 跳过本机 Python 检测,强制用便携版(测试用)
|
|
381
|
+
--reinstall-torch 重新安装 torch(检测到 NVIDIA 时装 CUDA/cu128 版,约 2.5 GB)
|
|
382
|
+
--force-cuda 安装时强制选 CUDA 版 torch(跳过 CUDA版本/算力检查,风险自负)
|
|
383
|
+
--cpu 安装时强制选 CPU 版 torch
|
|
283
384
|
-h, --help 显示本帮助
|
|
284
385
|
|
|
285
386
|
示例:
|
|
@@ -297,8 +398,13 @@ function printHelp() {
|
|
|
297
398
|
• pip 默认用清华镜像(pypi.tuna.tsinghua.edu.cn),失败自动回退官方 PyPI;
|
|
298
399
|
可用 DSH_CALIBGUARD_PIP_INDEX 指定其它索引;
|
|
299
400
|
• 从 npm 安装时引擎默认放在 ~/.dsh/calibguard-engine(可用 --dest 或插件配置 engineRoot 指定);
|
|
300
|
-
•
|
|
301
|
-
|
|
401
|
+
• GPU 自动定档:检测到 NVIDIA 且 CUDA≥12.8、算力≥7.5(如 RTX 20/30/40 系)时
|
|
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**(同名包互斥);
|
|
406
|
+
• 大文件(模型/引擎包)默认 4 路并发分段下载,可用 DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS 调整;
|
|
407
|
+
• 装完想换 GPU/CPU 版 torch:node setup.mjs --reinstall-torch。
|
|
302
408
|
`)
|
|
303
409
|
}
|
|
304
410
|
|
|
@@ -410,6 +516,119 @@ async function download(url, file) {
|
|
|
410
516
|
log(`下载完成:${bytes(received)}(用时 ${Math.round((Date.now() - startedAt) / 1000)} 秒)`)
|
|
411
517
|
}
|
|
412
518
|
|
|
519
|
+
/** 下载一个区间的分片(支持断点续传/重试)。 */
|
|
520
|
+
async function downloadRange(url, file, start, end, { attempts = 3, idleMs = 120_000 } = {}) {
|
|
521
|
+
const total = end - start + 1
|
|
522
|
+
let lastError = null
|
|
523
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
524
|
+
const offset = existsSync(file) ? statSync(file).size : 0
|
|
525
|
+
if (offset >= total) return
|
|
526
|
+
const controller = new AbortController()
|
|
527
|
+
let idleTimer = null
|
|
528
|
+
const resetIdle = () => {
|
|
529
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
530
|
+
idleTimer = setTimeout(() => controller.abort(new Error(`${Math.round(idleMs / 1000)} 秒没有收到数据`)), idleMs)
|
|
531
|
+
}
|
|
532
|
+
let response
|
|
533
|
+
try {
|
|
534
|
+
resetIdle()
|
|
535
|
+
response = await fetch(url, {
|
|
536
|
+
redirect: 'follow',
|
|
537
|
+
signal: controller.signal,
|
|
538
|
+
headers: { Range: `bytes=${start + offset}-${end}` },
|
|
539
|
+
})
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
542
|
+
lastError = error
|
|
543
|
+
warn(`分片 ${start}-${end} 第 ${attempt} 次失败:${error.message}`)
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
if (response.status !== 206 && !(response.status === 200 && offset === 0 && start === 0)) {
|
|
547
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
548
|
+
lastError = new SetupError(`分片请求返回 HTTP ${response.status}`)
|
|
549
|
+
continue
|
|
550
|
+
}
|
|
551
|
+
const sink = createWriteStream(file, { flags: offset > 0 ? 'a' : 'w' })
|
|
552
|
+
const reader = Readable.fromWeb(response.body)
|
|
553
|
+
try {
|
|
554
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
555
|
+
reader.on('data', (chunk) => { resetIdle() })
|
|
556
|
+
reader.on('error', (error) => { clearTimeout(idleTimer); rejectPromise(error) })
|
|
557
|
+
sink.on('error', (error) => { clearTimeout(idleTimer); rejectPromise(error) })
|
|
558
|
+
sink.on('finish', () => { clearTimeout(idleTimer); resolvePromise() })
|
|
559
|
+
reader.pipe(sink)
|
|
560
|
+
})
|
|
561
|
+
} catch (error) {
|
|
562
|
+
lastError = error
|
|
563
|
+
warn(`分片 ${start}-${end} 第 ${attempt} 次中断:${error.message}(已保留断点)`)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
if (statSync(file).size >= total) return
|
|
567
|
+
lastError = new SetupError('分片下载不完整')
|
|
568
|
+
}
|
|
569
|
+
throw new SetupError(`分片 ${start}-${end} 下载失败:${lastError?.message ?? '未知错误'}`)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 并发分段下载(大幅提升 GitHub/镜像的单连接限速场景)。
|
|
574
|
+
* 先探测是否支持 Range;不支持或文件不够大时自动退回单连接 download()。
|
|
575
|
+
*/
|
|
576
|
+
async function downloadParallel(url, file, { connections = DOWNLOAD_CONNECTIONS } = {}) {
|
|
577
|
+
if (!connections || connections < 2) return download(url, file)
|
|
578
|
+
let total = 0
|
|
579
|
+
let supportsRange = false
|
|
580
|
+
try {
|
|
581
|
+
const probe = await fetch(url, {
|
|
582
|
+
redirect: 'follow',
|
|
583
|
+
headers: { Range: 'bytes=0-0' },
|
|
584
|
+
signal: AbortSignal.timeout(60_000),
|
|
585
|
+
})
|
|
586
|
+
if (probe.ok) {
|
|
587
|
+
supportsRange = probe.status === 206
|
|
588
|
+
const contentRange = probe.headers.get('content-range') || ''
|
|
589
|
+
const sizePart = contentRange.split('/')[1]
|
|
590
|
+
total = Number(sizePart || probe.headers.get('content-length') || 0)
|
|
591
|
+
try { await probe.arrayBuffer() } catch { /* 忽略探测体 */ }
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
supportsRange = false
|
|
595
|
+
}
|
|
596
|
+
if (!supportsRange || !(total > 0) || total < 32 * 1024 ** 2) return download(url, file)
|
|
597
|
+
|
|
598
|
+
const chunk = Math.ceil(total / connections)
|
|
599
|
+
const parts = []
|
|
600
|
+
for (let index = 0; index < connections; index += 1) {
|
|
601
|
+
const start = index * chunk
|
|
602
|
+
const end = Math.min(total - 1, start + chunk - 1)
|
|
603
|
+
if (start > end) break
|
|
604
|
+
parts.push({ start, end, file: `${file}.part${index}`, percent: 0 })
|
|
605
|
+
}
|
|
606
|
+
log(`分段并发下载:${parts.length} 路,共 ${bytes(total)}(单连接慢时明显提速)`)
|
|
607
|
+
const startedAt = Date.now()
|
|
608
|
+
let finished = 0
|
|
609
|
+
await Promise.all(parts.map(async (part) => {
|
|
610
|
+
await downloadRange(url, part.file, part.start, part.end)
|
|
611
|
+
finished += 1
|
|
612
|
+
log(` 分片 ${finished}/${parts.length} 完成(${bytes(statSync(part.file).size)})`)
|
|
613
|
+
}))
|
|
614
|
+
log(`分段下载完成:${bytes(total)}(用时 ${Math.round((Date.now() - startedAt) / 1000)} 秒),开始合并…`)
|
|
615
|
+
const temp = `${file}.merging`
|
|
616
|
+
deleteFile(temp)
|
|
617
|
+
await streamPipeline((async function* generate() {
|
|
618
|
+
for (const part of parts) {
|
|
619
|
+
for await (const chunkData of createReadStream(part.file)) yield chunkData
|
|
620
|
+
}
|
|
621
|
+
})(), createWriteStream(temp))
|
|
622
|
+
for (const part of parts) deleteFile(part.file)
|
|
623
|
+
if (statSync(temp).size !== total) {
|
|
624
|
+
deleteFile(temp)
|
|
625
|
+
throw new SetupError(`合并后大小不符:${bytes(statSync(temp).size)} / ${bytes(total)}`)
|
|
626
|
+
}
|
|
627
|
+
deleteFile(file)
|
|
628
|
+
renameSync(temp, file)
|
|
629
|
+
log(`✓ 分段下载合并完成:${file}(${bytes(statSync(file).size)})`)
|
|
630
|
+
}
|
|
631
|
+
|
|
413
632
|
// ============================== 解压 ==============================
|
|
414
633
|
|
|
415
634
|
function extract(zipFile, dest) {
|
|
@@ -688,7 +907,7 @@ async function main() {
|
|
|
688
907
|
let lastError = null
|
|
689
908
|
for (const candidate of candidates) {
|
|
690
909
|
try {
|
|
691
|
-
await
|
|
910
|
+
await downloadParallel(candidate, downloadedZip)
|
|
692
911
|
if (statSync(downloadedZip).size < 100 * 1024 ** 2) {
|
|
693
912
|
throw new SetupError('下载内容明显偏小,可能是镜像错误页')
|
|
694
913
|
}
|
|
@@ -804,7 +1023,43 @@ async function main() {
|
|
|
804
1023
|
throw lastError ?? new SetupError('pip 安装失败')
|
|
805
1024
|
}
|
|
806
1025
|
await installWithMirrors(['--upgrade', 'pip', '--quiet'])
|
|
807
|
-
|
|
1026
|
+
|
|
1027
|
+
// GPU 定档:有可用 NVIDIA(CUDA≥12.8、算力≥7.5)时先装 CUDA 版 torch,
|
|
1028
|
+
// 与 CG_GUI launcher 的口径一致;否则装 CPU 版 torch。
|
|
1029
|
+
const gpu = detectGpu()
|
|
1030
|
+
const decision = decideCudaTorch(gpu, options)
|
|
1031
|
+
if (decision.forced && !gpu.hasNvidia) {
|
|
1032
|
+
warn('强制 CUDA 但未检测到 NVIDIA 显卡:torch 会安装成功,但运行期仍会回落 CPU。')
|
|
1033
|
+
}
|
|
1034
|
+
const gpuLabel = gpu.gpuName ? `(${gpu.gpuName})` : ''
|
|
1035
|
+
log(`GPU 定档:${decision.useCuda ? 'CUDA(cu128)' : 'CPU'} —— ${decision.reason}${gpuLabel}`)
|
|
1036
|
+
const existingTorchCuda = installedTorchCuda(python)
|
|
1037
|
+
if (decision.useCuda && (!existingTorchCuda || options.reinstallTorch)) {
|
|
1038
|
+
step(`安装 CUDA 版 torch(cu128,约 2.5 GB${existingTorchCuda ? ',重装' : ''})`)
|
|
1039
|
+
if (options.reinstallTorch) {
|
|
1040
|
+
try {
|
|
1041
|
+
await runLive(python, ['-m', 'pip', 'uninstall', '-y', 'torch'])
|
|
1042
|
+
} catch { /* 未安装则忽略 */ }
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
await runLive(python, [
|
|
1046
|
+
'-m', 'pip', 'install', 'torch', '--index-url', TORCH_CUDA_INDEX,
|
|
1047
|
+
'--timeout', '300', '--retries', '3',
|
|
1048
|
+
])
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
warn(`CUDA 版 torch 安装失败(${error.message}),回退 CPU 版 torch`)
|
|
1051
|
+
}
|
|
1052
|
+
} else if (existingTorchCuda) {
|
|
1053
|
+
log(`✓ 已安装 CUDA 版 torch(CUDA ${existingTorchCuda})`)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
await installWithMirrors(['-r', requirements, '--prefer-binary'])
|
|
1057
|
+
const finalTorchCuda = installedTorchCuda(python)
|
|
1058
|
+
log(`torch CUDA:${finalTorchCuda || '无(CPU 版)'}`)
|
|
1059
|
+
if (decision.useCuda && !finalTorchCuda) {
|
|
1060
|
+
warn('当前 torch 是 CPU 版:审计仍可运行,只是用 CPU。'
|
|
1061
|
+
+ '可重跑:node setup.mjs --reinstall-torch(或 --force-cuda 跳过兼容性检查)')
|
|
1062
|
+
}
|
|
808
1063
|
log('✓ 依赖安装完成')
|
|
809
1064
|
}
|
|
810
1065
|
|