dsh-calibguard 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -4,7 +4,7 @@ English | [中文](README.md)
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-green)
6
6
  ![node](https://img.shields.io/badge/node-%E2%89%A520-blue)
7
- ![tests](https://img.shields.io/badge/tests-95%20passed-brightgreen)
7
+ ![tests](https://img.shields.io/badge/tests-105%20passed-brightgreen)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-calibguard.svg)](https://www.npmjs.com/package/dsh-calibguard)
9
9
 
10
10
  Plugs the [CalibGuard](https://github.com/P-436/CG) file-security auditing engine into
@@ -85,6 +85,10 @@ checks — use at your own risk) or `node setup.mjs --cpu` (force CPU), or the e
85
85
  > The CPU and CUDA builds of torch **cannot coexist in one venv** (same package name; installing one replaces
86
86
  > the other). The CUDA build is a full torch and falls back to CPU when no GPU is available, so coexistence is
87
87
  > unnecessary.
88
+ >
89
+ > **Machines that already have the CPU build**: with ≥0.3.5 the panel header detects "NVIDIA … but torch is the
90
+ > CPU build" and offers a **one-click "Reinstall GPU torch (~2.5 GB)"** button (uninstalls the CPU build, installs
91
+ > CUDA, then restarts the bridge). CLI equivalent: `node "<plugin dir>/setup.mjs" --reinstall-torch --force-cuda`.
88
92
 
89
93
  **Where to watch progress**: the panel shows a "First run: preparing engine and dependencies" card with
90
94
  the current step and the tail of the installer output; host logs show `[calibguard]` lines. The audit
@@ -224,10 +228,11 @@ After pressing Save:
224
228
  - **the AI uses the same values** (precedence: plugin config < shared settings < per-call options);
225
229
  - changes take effect immediately, no restart;
226
230
  - 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
+ - the **credential name** is only the name of the storage slot (not the secret; pre-filled as
232
+ `DEEPSEEK_API_KEY`): fill in the API key and save; the name is remembered in shared settings
233
+ (`credentialRef`) and the value goes to the DSH credential store first. **If the current DSH version
234
+ cannot be written from a plugin, the key is stored in the local `~/.dsh/calibguard-credentials.json`
235
+ (0600, this machine only)** and the panel tells you where it landed;
231
236
  - **audit results**: every audit writes JSON + Markdown into the engine's `reports/` directory
232
237
  (default `<engineRoot>/reports`, `engineRoot` defaults to `~/.dsh/calibguard-engine`). The panel shows
233
238
  that path, has a "clear results" button, and each item links to its Markdown report.
@@ -273,7 +278,8 @@ Common keys of the loader row `config` (full table: [docs/configuration.md](docs
273
278
  | First audit is slow | Normal: ~475 MB of weights are loaded once (then the process stays resident) |
274
279
  | Big file takes tens of seconds | Normal: full-text sliding-window inference; see [docs/performance.md](docs/performance.md) |
275
280
  | 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 |
281
+ | **Panel says "credentialRef required" when saving the API key** | Upgrade to ≥ **0.3.3**: the panel has a "credential name" field (defaults to `DEEPSEEK_API_KEY`) and remembers it; older versions require `credentialRef` in the plugin config |
282
+ | **"This environment does not support writing the DSH credential store"** | Since 0.3.6 the key automatically falls back to the local `~/.dsh/calibguard-credentials.json` (0600) and the panel shows the storage location; you can also configure it in the DSH Models page, or use plugin config `llmApiKey` (not recommended) |
277
283
  | **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
284
  | **"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
285
  | **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 |
@@ -295,7 +301,7 @@ dsh-CalibGuard/
295
301
  ├── engine/ Bundled engine core copied from CG_GUI (code / tokenizer / calibration, no weights)
296
302
  ├── cordis.patch.yml Bundle patch (inserts the loader row)
297
303
  ├── scripts/ link-dsh (dev-time junction), bridge-selftest (engine self-check)
298
- ├── test/ node --test (95 cases; auto-skip when the engine env is missing)
304
+ ├── test/ node --test (105 cases; auto-skip when the engine env is missing)
299
305
  ├── .github/workflows/ publish.yml: npm trusted publishing (OIDC, optional)
300
306
  └── docs/ configuration, performance, troubleshooting, plugin contract, design notes
301
307
  ```
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-green)
6
6
  ![node](https://img.shields.io/badge/node-%E2%89%A520-blue)
7
- ![tests](https://img.shields.io/badge/tests-95%20passed-brightgreen)
7
+ ![tests](https://img.shields.io/badge/tests-105%20passed-brightgreen)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-calibguard.svg)](https://www.npmjs.com/package/dsh-calibguard)
9
9
 
10
10
  把 [CalibGuard](https://github.com/P-436/CG) 的文件安全审计引擎接进 **DeepSeek Harness(DSH)**:
@@ -78,6 +78,10 @@ GitHub 直连失败会自动换 gh-proxy / ghfast / ghproxy 镜像;pip 依赖
78
78
 
79
79
  > CPU 版与 CUDA 版 torch **不能同时存在于同一个 venv**(同名包会互相覆盖);
80
80
  > 但 CUDA 版 torch 本身就是完整 torch,没有 GPU 时会自动用 CPU,所以不需要共存。
81
+ >
82
+ > **已经有 CPU 版的机器**:升级到 ≥0.3.5 后,面板顶部会显示「检测到 NVIDIA … 但当前 torch 是 CPU 版」
83
+ > 并提供 **「一键重装 GPU 版 torch(约 2.5 GB)」** 按钮(会先卸载 CPU 版再装 CUDA 版,完成后自动重启桥接);
84
+ > 也可以用命令行:`node "<插件目录>/setup.mjs" --reinstall-torch --force-cuda`。
81
85
 
82
86
  **进度在哪看**:面板顶部会出现「首次运行:正在自动准备引擎与依赖」卡片(含当前步骤与输出尾巴);
83
87
  宿主日志里是 `[calibguard]` 开头的行。准备期间审计按钮不可用,完成后点「刷新状态」即可。
@@ -213,8 +217,9 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
213
217
  - **AI 调用同样使用这份设置**(优先级:插件配置 < 共享设置 < 本次调用参数);
214
218
  - 改完立即生效,**不用重启**;
215
219
  - API Key 写入 DSH 凭据库(`ctx.credentials`),不落入该设置文件;
216
- - **凭据名**只是"保存位置"的名字(不是密钥本身):面板里填(如 `DEEPSEEK_API_KEY`)+ API Key,
217
- 保存后名字记进共享设置的 `credentialRef`,值写进 DSH 凭据库;留空则用插件配置里的 `credentialRef`;
220
+ - **凭据名**只是"保存位置"的名字(不是密钥本身,面板默认预填 `DEEPSEEK_API_KEY`):填 API Key 保存后,
221
+ 名字记进共享设置的 `credentialRef`,值优先写入 DSH 凭据库;**当前 DSH 版本不支持从插件写凭据库时,
222
+ 自动保存到本机 `~/.dsh/calibguard-credentials.json`(0600,仅本机)**,面板会明确提示存储位置;
218
223
  - **审计结果**:每次审计在引擎的 `reports/` 目录生成 JSON + Markdown(默认 `<engineRoot>/reports`,
219
224
  `engineRoot` 默认 `~/.dsh/calibguard-engine`)。面板结果区会显示该路径并可「清空结果」,
220
225
  条目里的「查看 Markdown 报告」可直接打开。
@@ -258,7 +263,8 @@ AI 能看到完整四档结论与证据;若你说「只审高危文件」,
258
263
  | 侧边栏没有图标 / 面板报 `not found` | 客户端与路由在宿主启动时加载:**重启 DSH** |
259
264
  | **首次运行要等十几分钟** | 正常:首次下载模型(约 442 MB,自动镜像回退)并安装依赖(1–3 GB);面板顶部有进度卡片,只需一次 |
260
265
  | 首次审计很慢 | 正常:要加载约 475 MB 权重(之后走常驻进程) |
261
- | **面板保存 API Key 提示"需要 credentialRef"** | 升级到 ≥ **0.3.3**:面板「凭据名」可直接填写并记住(不用改插件配置);旧版本请把 `credentialRef` 写进插件配置 |
266
+ | **面板保存 API Key 提示"需要 credentialRef"** | 升级到 ≥ **0.3.3**:面板「凭据名」可直接填写并记住(默认预填 `DEEPSEEK_API_KEY`);旧版本请把 `credentialRef` 写进插件配置 |
267
+ | **提示"当前环境不支持写入 DSH 凭据库"** | ≥0.3.6 起会自动回退到本机凭据文件 `~/.dsh/calibguard-credentials.json`(0600),面板会显示存储位置;想改回凭据库时在 DSH Models 页配置即可。也可用插件配置 `llmApiKey`(不推荐) |
262
268
  | **审计结果保存在哪 / 想清空** | 保存在引擎 `reports/` 目录(默认 `<engineRoot>/reports`),每次审计生成 JSON + Markdown;面板结果区显示路径并带「清空结果」按钮;AI 工具返回 `report_md_path` / `report_json_path` |
263
269
  | **审计时总提示"首次会加载模型"** | 0.3.3 起只在模型尚未加载时提示;加载后按钮只显示「审计中…」,顶部状态显示「模型 已加载」 |
264
270
  | **有 NVIDIA 显卡(如 RTX 2060)但一直显示 CPU** | 说明装的是 CPU 版 torch(旧版安装器行为)。升级到 ≥0.3.3 后面板会给出提示;在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装上 CUDA 版即可,之后重启 DSH |
@@ -282,7 +288,7 @@ dsh-CalibGuard/
282
288
  ├── engine/ 从 CG_GUI 复制来的引擎核心(代码/词表/校准参数,不含 475 MB 权重)
283
289
  ├── cordis.patch.yml 组合包 patch(把插件行插进 profile 配置树)
284
290
  ├── scripts/ link-dsh(开发期挂包)、bridge-selftest(引擎自检)
285
- ├── test/ node --test(95 个用例;缺引擎环境时自动 skip)
291
+ ├── test/ node --test(105 个用例;缺引擎环境时自动 skip)
286
292
  ├── .github/workflows/ publish.yml:npm 可信发布(OIDC,可选)
287
293
  └── docs/ 配置全表、性能、排错、插件契约、实现决策
288
294
  ```
@@ -30,7 +30,7 @@
30
30
 
31
31
  > **GPU 定档**:安装器检测到 NVIDIA 且 CUDA ≥ 12.8、算力 ≥ 7.5 时自动装 CUDA(cu128)版 torch,
32
32
  > 否则装 CPU 版。已经装成 CPU 版时:在插件目录运行 `node setup.mjs --reinstall-torch`。
33
- > 手动覆盖:`--force-cuda` / `--cpu`(或环境变量)决定**安装哪个 build**;插件配置 `device`
33
+ > 手动覆盖:`--force-cuda` / `--cpu`(或环境变量)决定**安装哪个 build**(面板顶部也有「一键重装 GPU 版 torch」按钮,走 `POST /install-torch`);插件配置 `device`
34
34
  > (auto/cpu/cuda)决定**运行时用哪个设备**。CPU 版与 CUDA 版 torch 不能共存于同一个 venv
35
35
  >(同名包互斥);CUDA 版在无 GPU 时也能跑 CPU。
36
36
 
@@ -46,11 +46,14 @@
46
46
 
47
47
  ## 三、云端复核的 Key 从哪来(不硬编码、不隐式继承)
48
48
 
49
- 1. `credentialRef`(推荐;插件配置或面板「凭据名」两者取其一)→ `ctx.credentials.resolve(ref)`,优先级
50
- **继承环境 > `~/.dsh/.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`**;
49
+ 1. `credentialRef`(推荐;插件配置或面板「凭据名」两者取其一,面板默认 `DEEPSEEK_API_KEY`)→
50
+ `ctx.credentials.resolve(ref)`,优先级 **继承环境 > `~/.dsh/.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`**;
51
51
  解析到的值随请求走 **stdin** 下发,**不进 argv、不进子进程环境变量**,且每次操作重新解析(换 Key 下一次审计即生效)。
52
52
  2. `llmApiKey`(字面值,次选)。
53
- 3. `CG_GUI/gui-settings.json` 里的 `deepseek_api_key`(`useGuiSettings: true` 时生效)。
53
+ 3. 同名环境变量(名字来自 `credentialRef`);
54
+ 4. 本地凭据回退文件 `$DSH_HOME/calibguard-credentials.json`(面板保存时若 DSH 版本不支持写凭据库,
55
+ 会自动写到这里;仅本机、权限 0600,值不落共享设置文件);
56
+ 5. `CG_GUI/gui-settings.json` 里的 `deepseek_api_key`(`useGuiSettings: true` 时生效)。
54
57
 
55
58
  三者都没有 → 云端复核不可用、灰区转人工(安全降级)。桥接子进程**不会**去读宿主的
56
59
  `DEEPSEEK_API_KEY`:它的环境已按 DSH 的 `scrubbedParentEnv` 清洗(`*KEY*` / `*PASSWORD*` /
@@ -9,10 +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
+ | **提示"当前环境不支持写入 DSH 凭据库"** | ≥0.3.6 会自动回退到 `~/.dsh/calibguard-credentials.json`(0600,仅本机),面板会显示存储位置;清空即删该文件或到 DSH Models 页重新配置 |
12
13
  | **面板保存 API Key 报「需要 credentialRef」** | 升级到 ≥0.3.3:面板「凭据名」直接填(如 `DEEPSEEK_API_KEY`)并保存,会记进共享设置;旧版本只能把 `credentialRef` 写进插件配置 |
13
14
  | **审计结果在哪里 / 怎么清空** | 引擎 `reports/` 目录(默认 `<engineRoot>/reports`):每次审计生成 JSON + Markdown;面板结果区显示路径并有「清空结果」,工具返回 `report_md_path` / `report_json_path` |
14
15
  | **每次审计都提示"首次会加载模型"** | 0.3.3 起仅在模型未加载时提示;加载后不再出现,顶部状态显示「模型 已加载」 |
15
16
  | **有 NVIDIA 显卡但引擎显示 CPU** | 装的是 CPU 版 torch。升级到 ≥0.3.3(面板顶部会提示),然后在插件目录运行 `node setup.mjs --reinstall-torch`(约 2.5 GB)装 CUDA 版;装完重启 DSH。`DSH_CALIBGUARD_CPU_ONLY=1` 可反向强制 CPU |
17
+ | **已经装了 CPU 版 torch,想换成 GPU 版** | 升级到 ≥0.3.5:面板顶部点「一键重装 GPU 版 torch(约 2.5 GB)」(先卸载再装,完成自动重启桥接);或命令行 `node "<插件目录>/setup.mjs" --reinstall-torch --force-cuda` |
16
18
  | **想强制装 GPU / CPU 版 torch** | 安装时加 `--force-cuda` / `--cpu`(可配合 `--reinstall-torch` 覆盖已装版本);CPU 版与 CUDA 版不能共存于同一 venv,CUDA 版在无 GPU 时会自动回落 CPU |
17
19
  | **首次安装特别慢 / 提示某个源不可用** | 模型默认 4 路并发分段下载(`DSH_CALIBGUARD_DOWNLOAD_CONNECTIONS` 可调),GitHub 失败自动换 gh-proxy/ghfast/ghproxy 镜像;pip 自动在 清华/阿里/中科大/腾讯/官方 之间回退。实在慢可手动下载 `calibguard-model-1.0.0.zip` 放到缓存目录(见下一条) |
18
20
  | **模型下载失败 / 想离线** | 手动下载 `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 并跳过下载 |
package/lib/client.js CHANGED
@@ -345,6 +345,11 @@ window.__ModuleLoader__.load({
345
345
  var credName = credNameState[0]
346
346
  var setCredName = credNameState[1]
347
347
 
348
+ // torch 重装提示(面板按钮/结果)
349
+ var torchMsgState = useState('')
350
+ var torchMsg = torchMsgState[0]
351
+ var setTorchMsg = torchMsgState[1]
352
+
348
353
  var busyState = useState(false)
349
354
  var busy = busyState[0]
350
355
  var setBusy = busyState[1]
@@ -382,6 +387,15 @@ window.__ModuleLoader__.load({
382
387
  loadSettings()
383
388
  }, [])
384
389
 
390
+ useEffect(function () {
391
+ if (!torchRunning) return undefined
392
+ var timer = setInterval(refreshStatus, 3000)
393
+ return function () { clearInterval(timer) }
394
+ }, [torchRunning])
395
+ useEffect(function () {
396
+ if (torchState === 'done') refreshStatus()
397
+ }, [torchState])
398
+
385
399
  function applyStatus(payload) {
386
400
  var data = (payload && payload.status) || {}
387
401
  var llm = data.llm || {}
@@ -392,7 +406,7 @@ window.__ModuleLoader__.load({
392
406
  writable: Boolean(payload && payload.credentialWritable),
393
407
  configured: Boolean(llm.configured),
394
408
  })
395
- setCredName(function (prev) { return prev || (payload && payload.credentialRef) || '' })
409
+ setCredName(function (prev) { return prev || (payload && payload.credentialRef) || 'DEEPSEEK_API_KEY' })
396
410
  setForm(function (current) {
397
411
  var next = update(current, 'llmEnabled', llm.enabled !== false)
398
412
  next = update(next, 'allowThreshold', gate.allowThreshold !== undefined ? gate.allowThreshold : current.allowThreshold)
@@ -476,7 +490,11 @@ window.__ModuleLoader__.load({
476
490
  }).then(function (outcome) {
477
491
  setApiKey('')
478
492
  if (outcome && outcome.credentialRef) setCredName(outcome.credentialRef)
479
- setSettingsMsg('已保存 ✓ 参数共用;API Key 已写入 DSH 凭据库(' + ((outcome && outcome.credentialRef) || credentialName) + ')')
493
+ var storage = outcome && outcome.credentialStorage
494
+ setSettingsMsg(storage === 'local-file'
495
+ ? '已保存 ✓ 参数共用;当前 DSH 版本不支持从插件写入凭据库,API Key 已保存到本机文件 '
496
+ + '(~/.dsh/calibguard-credentials.json,仅本机可见)'
497
+ : '已保存 ✓ 参数共用;API Key 已写入 DSH 凭据库(' + ((outcome && outcome.credentialRef) || credentialName) + ')')
480
498
  refreshStatus()
481
499
  loadSettings()
482
500
  }).catch(function (err) {
@@ -554,6 +572,19 @@ window.__ModuleLoader__.load({
554
572
  })
555
573
  }
556
574
 
575
+ function installTorch(mode) {
576
+ setTorchMsg('已启动重装 torch(后台进行,面板自动刷新进度)…')
577
+ apiFetch('/install-torch', {
578
+ method: 'POST',
579
+ headers: { 'content-type': 'application/json' },
580
+ body: JSON.stringify({ mode: mode }),
581
+ }).then(function () {
582
+ refreshStatus()
583
+ }).catch(function (err) {
584
+ setTorchMsg('重装启动失败:' + String((err && err.message) || err))
585
+ })
586
+ }
587
+
557
588
  function clearResult() {
558
589
  setResult(null)
559
590
  setReport(null)
@@ -604,15 +635,22 @@ window.__ModuleLoader__.load({
604
635
 
605
636
  var envInfo = status && status.environment ? status.environment : null
606
637
  var engineInfo = status && status.status ? status.status : null
638
+ var envPreparing = Boolean(envInfo) && !engineInfo && envInfo.state !== 'failed'
607
639
  var reportDir = (engineInfo && engineInfo.reportDir)
608
640
  || (status && status.settings && status.settings.reportDir)
609
641
  || (result && result.config && result.config.reportDir)
610
642
  || ''
611
643
  var devicePolicy = (engineInfo && engineInfo.devicePolicy) || null
612
644
  var setupScript = (status && status.setupScript) || '<插件目录>/setup.mjs'
613
- // 有 NVIDIA 显卡、但 torch 没启用 CUDA(装的是 CPU 版)→ 提示怎么开 GPU
645
+ var torchJob = (status && status.torchJob) || null
646
+ var torchRunning = Boolean(torchJob && torchJob.state === 'running')
647
+ var torchState = (torchJob && torchJob.state) || 'idle'
648
+ var gpuInfo = (status && status.gpuTorch) || null
649
+ // 有 NVIDIA 显卡、但 torch 没启用 CUDA(装的是 CPU 版)→ 提示并允许一键重装
614
650
  var gpuTorchHint = engineInfo && engineInfo.cudaAvailable === false
615
- && devicePolicy && devicePolicy.gpuName ? devicePolicy.gpuName : ''
651
+ && ((gpuInfo && gpuInfo.gpuName) || (devicePolicy && devicePolicy.gpuName))
652
+ ? ((gpuInfo && gpuInfo.gpuName) || (devicePolicy && devicePolicy.gpuName))
653
+ : ''
616
654
  var engineReady = Boolean(engineInfo)
617
655
  var envBusy = Boolean(envInfo) && !engineReady && envInfo.state !== 'failed'
618
656
  var envFailed = Boolean(envInfo) && !engineReady && envInfo.state === 'failed'
@@ -624,8 +662,16 @@ window.__ModuleLoader__.load({
624
662
  + ' | 云端复核 ' + (engineInfo.llm.enabled ? (engineInfo.llm.configured ? '可用' : '已启用但无凭据') : '关闭')
625
663
  : (envFailed ? '环境准备失败' : (envBusy ? '首次运行:正在准备引擎与依赖…' : '状态读取中…'))
626
664
 
627
- return jsx('div', {
628
- style: S.page,
665
+ // 环境准备中自动刷新(装完后无需手动点「刷新状态」)
666
+ useEffect(function () {
667
+ if (!envPreparing) return undefined
668
+ var timer = setInterval(refreshStatus, 4000)
669
+ return function () { clearInterval(timer) }
670
+ }, [envPreparing])
671
+
672
+ function renderPage() {
673
+ return jsx('div', {
674
+ style: S.page,
629
675
  children: jsxs(Fragment, {
630
676
  children: [
631
677
  // ---------- 顶部 ----------
@@ -646,13 +692,65 @@ window.__ModuleLoader__.load({
646
692
  + ' | ' + devicePolicy.reason,
647
693
  })
648
694
  : 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。',
695
+ gpuTorchHint && torchState !== 'running'
696
+ ? jsxs('div', {
697
+ style: S.card,
698
+ children: [
699
+ jsx('div', { style: S.cardTitle, children: '检测到 NVIDIA ' + gpuTorchHint + ',但当前 torch 是 CPU 版' }),
700
+ jsx('div', {
701
+ style: S.muted,
702
+ children: 'CPU 版与 GPU 版 torch 不能共存于同一个 venv,重装会先卸载当前的再装 CUDA(cu128)版'
703
+ + '(约 2.5 GB,下载走 PyTorch 官方源)。装完桥接会自动重启,无需重启 DSH。',
704
+ }),
705
+ jsxs('div', {
706
+ style: S.row,
707
+ children: [
708
+ jsx('button', {
709
+ style: S.buttonPrimary,
710
+ onClick: function () { installTorch('cuda') },
711
+ children: '一键重装 GPU 版 torch(约 2.5 GB)',
712
+ }),
713
+ jsx('button', {
714
+ style: S.button,
715
+ onClick: function () { installTorch('auto') },
716
+ children: '按自动定档重装',
717
+ }),
718
+ torchMsg ? jsx('span', { style: S.muted, children: torchMsg }) : null,
719
+ ],
720
+ }),
721
+ torchState === 'done'
722
+ ? jsx('div', {
723
+ style: { color: C.allow, fontSize: 12, marginTop: 6 },
724
+ children: 'torch 重装完成(面板已自动刷新;若顶部仍显示 CPU,点「刷新状态」或重启 DSH 即可)',
725
+ })
726
+ : null,
727
+ torchState === 'failed'
728
+ ? jsx('div', {
729
+ style: { color: C.error, fontSize: 12, whiteSpace: 'pre-wrap', marginTop: 6 },
730
+ children: '重装失败:' + ((torchJob && (torchJob.error || torchJob.message)) || '详见日志')
731
+ + '\n手动方式:node "' + setupScript + '" --reinstall-torch --force-cuda',
732
+ })
733
+ : null,
734
+ jsx('div', {
735
+ style: S.note,
736
+ children: '不想用面板也可以手动运行:node "' + setupScript + '" --reinstall-torch --force-cuda',
737
+ }),
738
+ ],
739
+ })
740
+ : null,
741
+ torchRunning
742
+ ? jsxs('div', {
743
+ style: S.card,
744
+ children: [
745
+ jsx('div', { style: S.cardTitle, children: '正在重装 torch…' }),
746
+ jsx('div', { style: S.muted, children: (torchJob && (torchJob.step || torchJob.message)) || '准备中…' }),
747
+ torchJob && torchJob.tail && torchJob.tail.length > 0
748
+ ? jsx('pre', {
749
+ style: Object.assign({}, S.pre, { maxHeight: 140, marginTop: 8 }),
750
+ children: torchJob.tail.slice(-5).join('\n'),
751
+ })
752
+ : null,
753
+ ],
656
754
  })
657
755
  : null,
658
756
 
@@ -941,7 +1039,8 @@ window.__ModuleLoader__.load({
941
1039
  style: S.note,
942
1040
  children: '设置保存在本地 ' + (settingsFile || '$DSH_HOME/calibguard-settings.json')
943
1041
  + '(GUI 与 AI 工具共用);API Key 写入 DSH 凭据库,不落此文件。'
944
- + '「凭据名」只是保存位置的名字(可留空=用插件配置 credentialRef);'
1042
+ + '「凭据名」只是保存位置的名字(默认 DEEPSEEK_API_KEY,也会记住你填的名字);'
1043
+ + 'API Key 优先写 DSH 凭据库,当前 DSH 版本不支持时自动保存到本机文件。'
945
1044
  + '审计结果(JSON + Markdown)保存在引擎的 reports 目录,见下方「审计结果」。',
946
1045
  }),
947
1046
  jsxs('div', {
@@ -1113,6 +1212,9 @@ window.__ModuleLoader__.load({
1113
1212
  ],
1114
1213
  }),
1115
1214
  })
1215
+ }
1216
+
1217
+ return renderPage()
1116
1218
  }
1117
1219
 
1118
1220
  // ============================== 插件入口 ==============================
package/lib/engine.js CHANGED
@@ -22,16 +22,19 @@
22
22
  * stdin 管道关闭 → 桥接读到 EOF 自行退出,不留孤儿进程。
23
23
  */
24
24
  import { spawn } from 'node:child_process'
25
- import { existsSync } from 'node:fs'
26
- import { join } from 'node:path'
25
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
26
+ import { dirname, join } from 'node:path'
27
27
  import { fileURLToPath } from 'node:url'
28
28
 
29
29
  import { buildChildEnv } from './env.js'
30
30
  import { toBridgeOptions } from './config.js'
31
31
  import { EnvironmentBootstrap, venvPythonPath } from './bootstrap.js'
32
+ import { dshHome } from './settings.js'
32
33
 
33
34
  /** 桥接脚本相对本文件的位置(spawn 需要真实路径,不能直接把 URL 对象交给子进程)。 */
34
35
  const BRIDGE_SCRIPT = fileURLToPath(new URL('../bridge/calibguard_bridge.py', import.meta.url))
36
+ /** 安装器脚本(面板「重装 GPU 版 torch」会调用它)。 */
37
+ const SETUP_SCRIPT = fileURLToPath(new URL('../setup.mjs', import.meta.url))
35
38
 
36
39
  /** 单次 status 请求的默认超时(毫秒):只做文件探测与 torch 导入,很快。 */
37
40
  const STATUS_TIMEOUT_MS = 60_000
@@ -42,6 +45,38 @@ const ENV_WAIT_MS = 60_000
42
45
  /** stderr 环形缓冲行数:崩溃时把最后几行回给调用方,便于定位。 */
43
46
  const STDERR_TAIL_LINES = 20
44
47
 
48
+ /**
49
+ * 本地凭据回退文件:某些 DSH 版本的 ctx.credentials 没有 「set」(无法从插件写入凭据库),
50
+ * 此时把 Key 写到 $DSH_HOME/calibguard-credentials.json(仅本机、权限尽量收紧),
51
+ * 避免「面板根本配不了 Key」。优先仍走 DSH 凭据库;文件只是兼容兜底。
52
+ */
53
+ function localCredentialFile() {
54
+ return join(dshHome(), 'calibguard-credentials.json')
55
+ }
56
+
57
+ function readLocalCredentials() {
58
+ try {
59
+ const raw = JSON.parse(readFileSync(localCredentialFile(), 'utf8'))
60
+ return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
61
+ } catch {
62
+ return {}
63
+ }
64
+ }
65
+
66
+ function readLocalCredential(ref) {
67
+ const value = readLocalCredentials()[ref]
68
+ return typeof value === 'string' && value ? value : undefined
69
+ }
70
+
71
+ function writeLocalCredential(ref, value) {
72
+ const file = localCredentialFile()
73
+ const data = { ...readLocalCredentials(), [ref]: value }
74
+ mkdirSync(dirname(file), { recursive: true })
75
+ writeFileSync(file, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
76
+ try { chmodSync(file, 0o600) } catch { /* Windows 上可能不支持,忽略 */ }
77
+ return file
78
+ }
79
+
45
80
  export class CalibGuardEngine {
46
81
  /**
47
82
  * @param {object} options
@@ -71,6 +106,9 @@ export class CalibGuardEngine {
71
106
  this.statusCache = null
72
107
  /** 最近一次凭据解析结果(只用于诊断,不缓存值给下次复用)。 */
73
108
  this.lastCredential = { source: 'none', configured: false }
109
+ /** 面板「重装 torch」任务状态(CPU 版 → GPU 版一键替换用)。 */
110
+ this.torchJob = { state: 'idle', step: '', message: '', tail: [], startedAt: null, finishedAt: null, error: null }
111
+ this.torchChild = null
74
112
  /**
75
113
  * 首次运行自动准备环境(对齐 CG_GUI 的 launcher 首启装环境):
76
114
  * 缺 `<engineRoot>/.venv` 时在后台跑 setup.mjs(内置引擎 + npm 模型分片 → 建 venv → 装依赖)。
@@ -159,16 +197,16 @@ export class CalibGuardEngine {
159
197
  return { key: undefined, source: 'none' }
160
198
  }
161
199
 
200
+ let lookedUpInStore = false
162
201
  if (this.credentials?.resolve) {
202
+ lookedUpInStore = true
163
203
  try {
164
204
  const resolved = await this.credentials.resolve(ref)
165
205
  if (resolved?.value) {
166
206
  this.lastCredential = { source: `credentials:${resolved.source ?? 'unknown'}`, configured: true }
167
207
  return { key: resolved.value, source: this.lastCredential.source }
168
208
  }
169
- this.logger?.warn?.(`[calibguard] 凭据引用 ${ref} 未配置(credentialRef),云端复核将不可用`)
170
- this.lastCredential = { source: 'credentials:missing', configured: false }
171
- return { key: undefined, source: this.lastCredential.source }
209
+ this.logger?.warn?.(`[calibguard] 凭据引用 ${ref} 未在 DSH 凭据库中找到,继续尝试环境变量/本地凭据文件`)
172
210
  } catch (error) {
173
211
  this.logger?.warn?.(`[calibguard] 解析凭据 ${ref} 失败:${error?.message ?? error}`)
174
212
  }
@@ -180,8 +218,16 @@ export class CalibGuardEngine {
180
218
  this.lastCredential = { source: `env:${ref}`, configured: true }
181
219
  return { key: fromEnv, source: this.lastCredential.source }
182
220
  }
183
- this.lastCredential = { source: 'unavailable', configured: false }
184
- return { key: undefined, source: 'unavailable' }
221
+
222
+ // 最后兜底:插件自己写的本地凭据文件(DSH 版本不支持从插件写凭据库时用)
223
+ const fromLocal = readLocalCredential(ref)
224
+ if (fromLocal) {
225
+ this.lastCredential = { source: 'local-file', configured: true }
226
+ return { key: fromLocal, source: 'local-file' }
227
+ }
228
+
229
+ this.lastCredential = { source: lookedUpInStore ? 'credentials:missing' : 'unavailable', configured: false }
230
+ return { key: undefined, source: this.lastCredential.source }
185
231
  }
186
232
 
187
233
  /**
@@ -204,10 +250,20 @@ export class CalibGuardEngine {
204
250
  throw new Error('还没有凭据名:请在面板「凭据名」里填一个(如 DEEPSEEK_API_KEY),'
205
251
  + '或在插件配置里设置 credentialRef。凭据名不是密钥本身,只是"保存位置"的名字。')
206
252
  }
207
- if (typeof this.credentials?.set !== 'function') {
208
- throw new Error('当前环境不支持写入 DSH 凭据库:请在 DSH Models 页配置 Key,或用插件配置 llmApiKey')
253
+ // 优先写入 DSH 凭据库;当前 DSH 版本不支持时,退化为本地凭据文件(仅本机、0600)
254
+ let storage = 'dsh-credentials'
255
+ if (typeof this.credentials?.set === 'function') {
256
+ try {
257
+ await this.credentials.set(ref, value.trim())
258
+ } catch (error) {
259
+ this.logger?.warn?.(`[calibguard] 写入 DSH 凭据库失败(${error?.message ?? error}),改用本地凭据文件`)
260
+ storage = 'local-file'
261
+ writeLocalCredential(ref, value.trim())
262
+ }
263
+ } else {
264
+ storage = 'local-file'
265
+ writeLocalCredential(ref, value.trim())
209
266
  }
210
- await this.credentials.set(ref, value.trim())
211
267
  // 记住这个名字(只存名字,不存密钥),下次启动后审计仍能找到它
212
268
  if (this.settingsStore) {
213
269
  try {
@@ -216,9 +272,9 @@ export class CalibGuardEngine {
216
272
  this.logger?.warn?.(`[calibguard] 记住凭据名失败(不影响本次写入):${error?.message ?? error}`)
217
273
  }
218
274
  }
219
- this.lastCredential = { source: 'credentials:set', configured: true }
220
- this.logger?.info?.(`[calibguard] 已写入凭据引用 ${ref}(值不落日志)`)
221
- return { ref, saved: true }
275
+ this.lastCredential = { source: storage, configured: true }
276
+ this.logger?.info?.(`[calibguard] 已写入凭据引用 ${ref}(存储:${storage},值不落日志)`)
277
+ return { ref, saved: true, storage }
222
278
  }
223
279
 
224
280
  /**
@@ -274,6 +330,121 @@ export class CalibGuardEngine {
274
330
  )
275
331
  }
276
332
 
333
+ /**
334
+ * GPU/CPU torch 状态(面板据此决定是否显示「重装 GPU 版」)。
335
+ * 数据来自最近一次 status 快照:gpuName 仅当检测到 NVIDIA;cudaAvailable=torch 是否启用 CUDA。
336
+ */
337
+ gpuTorchStatus() {
338
+ const status = this.statusCache
339
+ if (!status) return { gpuName: null, cudaAvailable: null, device: null, policyMode: null, reason: '' }
340
+ const policy = status.devicePolicy || {}
341
+ return {
342
+ gpuName: policy.gpuName ?? null,
343
+ cudaAvailable: status.cudaAvailable ?? null,
344
+ device: status.device ?? null,
345
+ policyMode: policy.mode ?? null,
346
+ reason: policy.reason ?? '',
347
+ }
348
+ }
349
+
350
+ /** 重装 torch 任务快照。 */
351
+ torchJobSnapshot() {
352
+ const job = this.torchJob
353
+ return {
354
+ state: job.state,
355
+ step: job.step,
356
+ message: job.message,
357
+ startedAt: job.startedAt,
358
+ finishedAt: job.finishedAt,
359
+ error: job.error,
360
+ tail: [...job.tail],
361
+ }
362
+ }
363
+
364
+ /**
365
+ * 重装 torch(CPU 版 ↔ GPU 版互斥,先卸载再装),完成后停掉桥接让下次请求用新的 torch。
366
+ * @param {'auto'|'cuda'|'cpu'} [mode] auto=按自动定档;cuda=强制 CUDA 版(跳过兼容检查);cpu=强制 CPU 版
367
+ */
368
+ async installTorch(mode = 'auto') {
369
+ if (this.torchJob.state === 'running') return this.torchJobSnapshot()
370
+ if (!['auto', 'cuda', 'cpu'].includes(mode)) throw new Error('mode 必须是 auto / cuda / cpu')
371
+ if (!existsSync(venvPythonPath(this.config.engineRoot))) {
372
+ throw new Error('环境还没准备好(缺 .venv):请先完成首次环境准备再重装 torch。')
373
+ }
374
+ this.torchJob = {
375
+ state: 'running', step: '', message: '正在重装 torch…', tail: [],
376
+ startedAt: Date.now(), finishedAt: null, error: null,
377
+ }
378
+ const extra = ['--reinstall-torch']
379
+ if (mode === 'cuda') extra.push('--force-cuda')
380
+ if (mode === 'cpu') extra.push('--cpu')
381
+ const args = [SETUP_SCRIPT, '--dest', this.config.engineRoot, '--skip-verify', ...extra]
382
+ this.logger?.info?.(`[calibguard] 重装 torch:node setup.mjs ${args.slice(1).join(' ')}`)
383
+
384
+ const finish = new Promise((resolvePromise) => {
385
+ const child = spawn(process.execPath, args, {
386
+ cwd: this.config.engineRoot,
387
+ stdio: ['ignore', 'pipe', 'pipe'],
388
+ windowsHide: true,
389
+ })
390
+ this.torchChild = child
391
+ const onLine = (line) => {
392
+ const text = String(line).trim()
393
+ if (!text) return
394
+ this.torchJob.tail.push(text)
395
+ if (this.torchJob.tail.length > 20) this.torchJob.tail.shift()
396
+ const stepMatch = /^==\s*(.+?)\s*==$/.exec(text)
397
+ if (stepMatch) {
398
+ this.torchJob.step = stepMatch[1]
399
+ this.torchJob.message = `${stepMatch[1]}…`
400
+ return
401
+ }
402
+ this.torchJob.message = text
403
+ }
404
+ const wire = (stream) => {
405
+ let buffer = ''
406
+ stream.setEncoding('utf8')
407
+ stream.on('data', (chunk) => {
408
+ buffer += chunk
409
+ const lines = buffer.split(/\r?\n/)
410
+ buffer = lines.pop() ?? ''
411
+ for (const line of lines) onLine(line)
412
+ })
413
+ stream.on('end', () => { if (buffer) onLine(buffer) })
414
+ }
415
+ wire(child.stdout)
416
+ wire(child.stderr)
417
+ child.on('error', (error) => {
418
+ this.torchJob.state = 'failed'
419
+ this.torchJob.error = String(error?.message ?? error)
420
+ this.torchJob.message = `重装失败:${this.torchJob.error}`
421
+ this.torchJob.finishedAt = Date.now()
422
+ this.torchChild = null
423
+ resolvePromise(this.torchJobSnapshot())
424
+ })
425
+ child.on('close', async (code) => {
426
+ this.torchChild = null
427
+ this.torchJob.finishedAt = Date.now()
428
+ if (code === 0) {
429
+ this.torchJob.state = 'done'
430
+ this.torchJob.step = ''
431
+ this.torchJob.message = 'torch 重装完成,桥接将重启以使用新的 torch'
432
+ this.statusCache = null
433
+ try { await this.stop('torch-reinstalled') } catch { /* 忽略 */ }
434
+ this.logger?.info?.('[calibguard] torch 重装完成,桥接已停止(下次调用重启)')
435
+ } else {
436
+ this.torchJob.state = 'failed'
437
+ this.torchJob.error = `setup.mjs 退出码 ${code}`
438
+ this.torchJob.message = `重装失败(${this.torchJob.error}):${this.torchJob.tail.slice(-3).join(' | ')}`
439
+ this.logger?.warn?.(`[calibguard] ${this.torchJob.message}`)
440
+ }
441
+ resolvePromise(this.torchJobSnapshot())
442
+ })
443
+ })
444
+ this.torchJob.promise = finish
445
+ return finish
446
+ }
447
+
277
448
  // ============================== 进程生命周期 ==============================
278
449
 
279
450
  /** 启动常驻桥接进程(已启动则直接返回)。子进程环境经过凭据清洗。 */
@@ -501,8 +672,11 @@ export class CalibGuardEngine {
501
672
  lastExit: this.lastExit,
502
673
  credential: { ...this.lastCredential },
503
674
  credentialRef: this.effectiveCredentialRef() || null,
504
- credentialWritable: typeof this.credentials?.set === 'function',
675
+ credentialWritable: true,
676
+ credentialStorage: typeof this.credentials?.set === 'function' ? 'dsh-credentials' : 'local-file',
505
677
  environment: this.bootstrap.snapshot(),
678
+ torchJob: this.torchJobSnapshot(),
679
+ gpuTorch: this.gpuTorchStatus(),
506
680
  settings: this.settings(),
507
681
  stderrTail: [...this.stderrTail],
508
682
  }
package/lib/routes.js CHANGED
@@ -148,7 +148,10 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
148
148
  credential: diagnostics.credential,
149
149
  credentialRef: diagnostics.credentialRef ?? null,
150
150
  credentialWritable: Boolean(diagnostics.credentialWritable),
151
+ credentialStorage: diagnostics.credentialStorage ?? null,
151
152
  setupScript,
153
+ torchJob: typeof engine.torchJobSnapshot === 'function' ? engine.torchJobSnapshot() : null,
154
+ gpuTorch: typeof engine.gpuTorchStatus === 'function' ? engine.gpuTorchStatus() : null,
152
155
  settings: engine.settings(),
153
156
  settingsFile: settingsStore?.file ?? null,
154
157
  })
@@ -162,6 +165,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
162
165
  credential: diagnostics.credential,
163
166
  credentialRef: diagnostics.credentialRef ?? null,
164
167
  credentialWritable: Boolean(diagnostics.credentialWritable),
168
+ credentialStorage: diagnostics.credentialStorage ?? null,
165
169
  setupScript,
166
170
  settings: engine.settings(),
167
171
  settingsFile: settingsStore?.file ?? null,
@@ -179,7 +183,10 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
179
183
  credential: diagnostics.credential,
180
184
  credentialRef: diagnostics.credentialRef ?? null,
181
185
  credentialWritable: Boolean(diagnostics.credentialWritable),
186
+ credentialStorage: diagnostics.credentialStorage ?? null,
182
187
  setupScript,
188
+ torchJob: typeof engine.torchJobSnapshot === 'function' ? engine.torchJobSnapshot() : null,
189
+ gpuTorch: typeof engine.gpuTorchStatus === 'function' ? engine.gpuTorchStatus() : null,
183
190
  settings: engine.settings(),
184
191
  settingsFile: settingsStore?.file ?? null,
185
192
  })
@@ -209,13 +216,43 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
209
216
  }
210
217
  const result = await engine.setCredential(apiKey, name || undefined)
211
218
  const ref = result && result.ref ? result.ref : null
212
- sendJson(res, 200, { ok: true, ...result, credentialRef: ref, settings: engine.settings() })
219
+ sendJson(res, 200, {
220
+ ok: true,
221
+ ...result,
222
+ credentialRef: ref,
223
+ credentialStorage: result && result.storage ? result.storage : null,
224
+ settings: engine.settings(),
225
+ })
213
226
  sendJson(res, 200, { ok: true, ...result })
214
227
  } catch (error) {
215
228
  sendJson(res, 400, { error: String(error?.message ?? error) })
216
229
  }
217
230
  }
218
231
 
232
+ /** POST /install-torch —— 面板「重装 GPU/CPU 版 torch」。启动后台任务,进度走 /status 的 torchJob。 */
233
+ async function handleInstallTorch(req, res) {
234
+ if (req.method !== 'POST') return sendJson(res, 405, { error: '仅支持 POST' })
235
+ try {
236
+ const body = await readBody(req, 1024 * 1024)
237
+ let payload
238
+ try {
239
+ payload = JSON.parse(body.toString('utf8') || '{}')
240
+ } catch {
241
+ return sendJson(res, 400, { error: '请求体必须是合法 JSON' })
242
+ }
243
+ const mode = typeof payload?.mode === 'string' ? payload.mode : 'auto'
244
+ if (typeof engine.installTorch !== 'function') {
245
+ return sendJson(res, 503, { error: '当前插件版本不支持重装 torch' })
246
+ }
247
+ engine.installTorch(mode).catch((error) => {
248
+ logger?.warn?.(`[calibguard] 重装 torch 失败:${error?.message ?? error}`)
249
+ })
250
+ sendJson(res, 200, { ok: true, started: true, mode })
251
+ } catch (error) {
252
+ sendJson(res, 400, { error: String(error?.message ?? error) })
253
+ }
254
+ }
255
+
219
256
  /** POST /audit */
220
257
  async function handleAudit(req, res) {
221
258
  if (req.method !== 'POST') return sendJson(res, 405, { error: '仅支持 POST' })
@@ -308,6 +345,7 @@ export function createCalibGuardRoutes({ engine, config, logger, settingsStore }
308
345
  state,
309
346
  routes: [
310
347
  { kind: 'exact', path: `${ROUTE_PREFIX}/status`, handler: handleStatus },
348
+ { kind: 'exact', path: `${ROUTE_PREFIX}/install-torch`, handler: handleInstallTorch },
311
349
  { kind: 'exact', path: `${ROUTE_PREFIX}/audit`, handler: handleAudit },
312
350
  { kind: 'exact', path: `${ROUTE_PREFIX}/report`, handler: handleReport },
313
351
  { kind: 'exact', path: `${ROUTE_PREFIX}/settings`, handler: async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calibguard",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },