🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI
广告☁️ 云服务器特惠阿里云首购 8 折 · 腾讯云合作特惠
DeepSeek Harness Hub
← 返回列表

DZQJOKER/dsh-plugin-local-model

DeepSeek Harnessspec-screened扫描:中风险在 GitHub 查看 ↗
⚠ 装前注意

"DeepSeek Harness 本地模型插件:设置里独立的「本地模型」页,首条对话自动拉起…

基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/9/22 · 已提供中文文档

"DeepSeek Harness 本地模型插件:设置里独立的「本地模型」页,首条对话自动拉起 llama.cpp,空闲 5 分钟自动卸载释放资源。"

综合分
30.7
GitHub 分
30.7
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add DZQJOKER/dsh-plugin-local-model
未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
信任档位:已验证本站已于 2 天前真实安装成功
是什么
dsh 原生插件 · tool
装得上吗
本站已真实安装成功(非静态推断)
安全吗
本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
还在维护吗
活跃:最近一次提交在 3 天前

档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →

🟢实装验证通过· 2026/9/24
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/23(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包dsh-plugin-local-model(未发布到 npm,仅可源码安装)
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

未发布到 npm registry,仅可从源码安装

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/23 19:04:36

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-tools@deepseek-ai/schemastery
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

由 DeepSeek 最新模型翻译生成
dsh-cost-meter 预览图

dsh-plugin-local-model

给 DeepSeek Harness(dsh) 用的本地模型插件:在设置里管理本地 GGUF 模型,
第一条对话自动拉起 llama.cpp 载入模型,连续 5 分钟无交互自动卸载并释放显存;
设置页顶部还有一块参数预设,把整套参数存成带名字的条目,一键切换。

模型和 llama 工具由用户自己下载,放进插件规定的目录即可 —— 插件不联网拉模型、不碰工作区文件、
除本机回环地址外不监听任何端口。

更新日志

- 0.7.0 — ① 修掉「装上视觉投影文件后一直报 400 Failed to load image or audio file」。根因不在插件、也不在模型:llama.cpp 内置的图像解码器只认 png / jpeg / gif / bmp 这些老格式,不认识 WebP;遇到 WebP 它会去起外部的 ffprobe 探测、ffmpeg 转码,而这两个是从 PATH 里找的(这个构建砍掉了 --ffmpeg-path,只剩 PATH 一条路)。机器上没装 ffmpeg 时,服务端会打 probe: failed to launch ffprobe / failed to decode webp buffer,客户端拿到 400 —— 而同一批里的 JPEG 却能正常识别(dsh 转码时两种格式都会产出,实测 .dsh/attachments/v1/request-images/ 下两种都有),所以表现是「时好时坏」,极易被当成插件或模型坏了。现在:插件会自动找到 ffmpeg 并把它的目录前置进 llama-server 子进程的 PATH(不必改系统 PATH、也不必为此重启 dsh),找不到且已开视觉时会打出一条说清「哪个格式会失败、为什么只有它、怎么修」的警告。装法:winget install Gyan.FFmpeg。
② MTP 与视觉投影从「互斥」改为「可共存」(新增设置项 mtpWithVision,默认开启)。原规则来自上游 llama.cpp(两者同时下发会加载失败),但 kvmem 分支的 llama-kvmem-server 实测支持:同时给 --mmproj 与 --spec-type draft-mtp 时 clip_model_loader: has vision encoder 与 creating MTP draft context 会同时出现、服务正常起来、看图对话也能识别(2026-09-21 本机验证)。换回官方 llama.cpp 的用户把这个开关关掉即恢复旧的互斥行为(MTP 生效、--mmproj 被忽略并给出说明)。
③ 设置页顶部新增「本次启动参数」面板:把这次真正下发给 llama-server 的完整命令行逐项一行列出来(不是设置页里填的那一份 —— 两者之间隔着门控跳过、构建默认值与自动收敛),配一张参数摘要(含派生的「GPU KV 合计 = 工作集 + 解码预留」)、一条「复制命令行」按钮、以及被跳过的选项与识别表没覆盖到的选项(后者过去只躺在日志里)。刷新跟随运行状态轮询,换模型/改参数后自动更新。

- 0.6.0 — ① 补齐 kvmem 分支(kvmem/kvmem-llama.cpp)的全部缺失启动参数,共 31 项:KVMem 家族 18 项(--kvmem / --kvmem-budget / --kvmem-gen-reserve / --kvmem-block-tokens / --kvmem-sink-tokens / --kvmem-recent-tokens / --kvmem-method / --kvmem-query-last / --kvmem-query-max-tokens / --kvmem-query-replay / --kvmem-query-policy / --kvmem-mtp-state / --kvmem-gpu-ratio / --kvmem-cpu-gb / --kvmem-nvme-gb / --kvmem-nvme-dir / --kvmem-harvest-v / --kvmem-raw-k-nvme)、外加 -n/--n-predict、-lm/--load-mode、--kv-dtype、--spec-kv-dtype、--spec-draft-n-max、--spec-draft-p-min、--frequency-penalty、--mmproj-offload、--chat-template-file、--chat-template-kwargs、--reasoning-effort、--reasoning-budget-message。默认值一律是「不下发」(数字项用 -1 当哨兵、字符串用空串),所以装官方 llama.cpp 的部署行为一字不变;kvmem 专有项走严格门控,探测不到就整体跳过并在日志里说明。
② 新增「输出上限溢出保护」(guardContextOverflow,默认开启)。上游 llama.cpp 遇到 prompt + max_tokens > n_ctx 只打一条 warning 再自行收敛,而 kvmem 那个独立 server 是硬拒绝:HTTP 400 {"error":"prompt + max_tokens exceeds n_ctx"} —— 报错里既没有 prompt 长度也没有 n_ctx,用户只能乱试。现在代理会先把「必然失败」(max_tokens >= n_ctx,任何非空提示词都放不下)的上限预防性压到 n_ctx − 1024,再对服务端明确拒绝的请求逐级减半重试(只缓冲 400,其余状态码照旧流式直通,正常对话零额外开销);实在装不下时把原文换成一句带上真实 n_ctx 与最可能原因的中文说明。
③ 新增加载期配置一致性提醒:maxTokens 不小于 ctxSize(本轮线上故障的根因)、超过一半、或 --kvmem-gen-reserve 小于单次输出上限时,加载日志里直接点明后果与建议值。
④ 修掉一处静默缺陷:被跳过选项的汇总提示原先在采样参数之后就结算了,导致后面新增的每一项(--kvmem- / -n / -lm …)被跳过后都不会出现在日志里。
⑤ Added two new settings groups: "KVMem Chunked Cache" and "Multi-Token Prediction (MTP) Details".

- 0.5.3 — Fixed "switching to a different llama branch breaks the plugin startup". The user replaced the executable with the llama-kvmem-server.exe from kvmem-v0.16.0-rc2 (a standalone OpenAI-compatible server whose option table is only a true subset of llama.cpp), and loading failed immediately:
unknown flag: --alias + usage + exit 1. The root cause is that among the set of options the plugin unconditionally sends, some do not exist in this branch at all — the detection mechanism previously only gated the 4 categories of options that "only newer versions have"
(--kv-unified / --kv-stream-stage-mib / --image--tokens / --reasoning-budget), while the rest (--alias, -t, --threads-batch, -ub, --repeat-last-n, --no-mmap,
--mlock, --api-key, the sampling parameter group) were all sent as-is. And this kind of branch does not ignore unknown options — it exits with 1 directly,
and only reports one at a time — so "just fixing --alias" is not enough; the next one would be -ub.
Fix: Extend the existing contract of "only send options the build acknowledges" to all options, and split it into two tiers of semantics:
① Options that only newer builds/specific branches have → strictly gated (if detection fails, still not sent, same as before);
② The rest → loosely gated (if detection fails, still send everything as before, behavior fully consistent with history; only skip an item when the build explicitly does not expose it).
Anything skipped is always written to the log (已跳过(不影响加载):--alias、-ub、…), never silently.
Also fixed a related failure of the same origin: the default value of --seed is -1 (= random), but this branch validates it as uint32,
and receiving -1 directly exits with invalid --seed: seed out of range [0, 4294967295] — now negative numbers are never sent for this parameter (equivalent to "random": llama.cpp's default value is already -1),
one rule simultaneously satisfies standard builds (semantics unchanged) and builds with a narrowed value range.
Real-machine acceptance (using the user's real config, with the model swapped to a sentinel path):
① kvmem build — 63 options recognized, no more unknown items in the command line, all actual parameters pass;
② llama.mtp加速优化版 (405 options) — the command line is verbatim identical to before the fix (only the semantically equivalent --seed -1 is missing),
all parameters pass. In other words, this version has zero behavior change for standard/near-upstream builds.
New regression cases: in scripts/selftest.mjs, a set of assertions was built based on the original --help text of kvmem (including "every option sent must be publicly exposed"),
and scripts/verify-llama.mjs added two categories of criteria: unknown flag / invalid --xxx: (previously it would miss this line and judge it as "no parameter errors").

- 0.5.2 — Fixed transparent overlap on the settings page. The 0.5.1 diagnosis was wrong — those three geometry issues did exist and were indeed fixed,
but they were not the reason the user saw "text on top of text". The real root cause is that the plugin referenced CSS variables that do not exist in DSH at all:
the plugin's entire set used --color-background-primary / --color-border-tertiary / --color-text-primary
(a set of generic tokens in Claude / Cursor style), while the host DSH only provides a set of --dsw-
(scanning all 21810 files in app.asar, the definition count for the above --color- is 0).
var(--x, fallback) silently falls back to the fallback when the variable does not exist, so:
① The fallback for card and the bottom save bar was written as 'transparent' → they really are transparent,
and the text of the "运行状态" card below (current model / process / entry / total models / inference tier / multi-token prediction / visual projection)
shows straight through; ② The fallback for the pinned preset bar is '#fff' → under the light theme, a white background equals the panel's white background,
so visually it still looks "transparent"; ③ The --color-background-secondary of input boxes / dropdowns also falls through to transparent,
appearing on the card as an area with "no visible bottom"; ④ Under the dark theme, --color-text-primary → #111,
and the primary button becomes black text on a black background. Fix: Migrate the entire color system to --dsw-
(panel background --dsw-alias-bg-layer-2, same color as the host .MI-_Aa_panel; borders --dsw-alias-border-l1..l4;
text --dsw-alias-label-; semantic colors --dsw-alias-state-), and give all containers that "would block the content below"
an opaque background. Also fixed two related issues: ⑤ The horizontal negative margin of the pinned bar and the save bar —
.options has 24px padding on each side, and sticky elements by default only cover the content area width, so content scrolling through the 24px gaps on both sides is directly exposed
(in the old version, when the save bar had padding: '10px 0', there were even 10px gaps above and below, and "当前模型" showed through from this gap);
⑥ The status badge background color now uses color-mix to derive from the foreground color, so "light background with light text" no longer appears under the dark theme.
client/src/layoutCheck.js added two categories of assertions: variable whitelist and transparency,
and added a reverse verification (using a buggy bad sample must report an error) — because the biggest risk of this kind of detection is
the false negative of "the rule never triggers", which is more dangerous than having no rule at all.
- 0.5.1 — Fixed the geometric causes of element overlap on the settings page (three places, all layout issues that "only show up at certain screen widths").
The root cause is that the Host settings panel actually has only 564px of available width (panel 800 − left navigation 188 − padding 48),
while the client wrote the content area as maxWidth: 720, 156px wider than the available width:
① The field row is a grid with three columns, and the second column is written as a bare 1fr — grid's automatic minimum size defaults to auto,
and it refuses to be compressed below the input box's min-content, so when the available width is insufficient, the second column is pushed to the next line,
the "默认" button returns to the right side, and the two rows of content visually overlap (≤800px panels always reproduce this, i.e. any laptop);
② 置顶预设条的 position: sticky 包含块是滚动容器 .options 的内边距盒,
top:0 正好落在面板 32px 圆角上,滚起来会切掉卡片上沿、压住面板标题与关闭按钮;
③ .options 的上内边距为 0 而 sticky 不认领它,卡片上方留下一条能滚动的透明缝,
下层卡片从缝里透出来。现在:内容区改为 maxWidth: 100%;三列全部 minmax(0, …)
并把控件都补上 minWidth: 0(字段行最小需求降到约 204px,480px 宽的窗口都富余);
置顶条用实色背景 + 顶部内边距 + isolation: isolate 把自己封住,不再和面板抢层级。
另修:超长预设名会让 chip 把 ✎ / × 挤出卡片(现封顶 240px 并省略),
元信息网格的 minmax(220px, 1fr) 硬下限在窄面板下整块溢出(现 minmax(0, 220px))。
只改样式,不动任何组件结构、路由与交互;新增 client/src/layoutCheck.js 把这几条
几何约束做成断言,client-check 会在常见分辨率下逐个验算「设置项一行排得下」。
⚠️ 透明背景问题不在这一版的修复范围内,见 0.5.2。
- 0.5.0 — 新增参数预设:把整套加载/推理参数(含选中的模型与视觉投影)存成带名字的条目,
可存多组、可重命名、可覆盖、可删除,点一下即切换。预设条固定在设置页顶部,
滚到参数区也能直接切。预设只收「参数」不收「环境」—— 端口、监听地址、模型/运行时目录、
密钥、日志级别这 9 项不进预设,切换预设不会动它们(否则切个参数预设会把端口也换掉)。
存储在 /local-model/state/presets.json(与 config.json 同目录,独立成文件,
不污染既有配置)。新增功能不动既有行为:所有原设置项、按钮、请求改写、加载/卸载逻辑一字未改,
预设的应用路径就是「保存设置」那条路径(写入用户层 → 按新参数卸载 → 下次对话重载)。
- 0.4.2 — 修掉「推理档位拨了会 500」。0.4.1 让 dsh 把档位写进 chat_template_kwargs 之后暴露了两个新问题:① 模型模板对不认识的档位是直接 raise_exception(实测这个模型只认 xhigh/medium/low),而插件把面板档位原样透传、dsh 还会把 off 发成布尔 false —— 一次对话直接 500;② 插件仍会在某些情形下覆盖请求自己的档位选择(开关关闭时把 High 也压成不思考)。现在:加载后读模型模板解析出它支持的档位表(日志与状态卡都会显示),面板档位按表重映射(high → xhigh);映射不出来就不发这个字段;false/null/true 等形态一律归一化;开/关以请求为准,插件开关只在请求什么都没说时生效。
- 0.4.1 — 修掉「dsh 的推理等级滑杆形同虚设」。两处成因,都修了:① 插件注册进 dsh 的路由 profile 没有声明这是推理模型(缺 reasoningEfforts),pi-ai 因此什么思考参数都不发,滑杆只是个摆设;现在会声明 compat.thinkingFormat = chat-template 与七档映射,让 dsh 把档位写进 chat_template_kwargs(llama.cpp 唯一认的通道)。② 插件的「启用思考」开关无条件把 enable_thinking 改写成 true,把 dsh 选的 Off 也顶掉了 —— 现在开关开启时只当默认值:请求里已经显式写了 enable_thinking 或带了档位就原样放行;关闭时仍是硬覆盖。另外代理会把顶层的 reasoning_effort 下沉进 chat_template_kwargs —— 顶层字段会被 llama-server 静默丢掉(无报错、无日志),这是社区踩过的坑。
- 0.4.0 — ① 新增 13 项设置:KV 缓存策略 2 项(kvUnified、kvStreamStageMib,其中流式暂存是特定 llama.cpp 分支的私有参数)、采样参数 8 项(temp / topK / topP / minP / presencePenalty / repeatPenalty / repeatLastN / seed)、多模态与推理预算 3 项(imageMinTokens / imageMaxTokens / reasoningBudget)。这些参数现在会在每次加载时显式下发并覆盖 llama.cpp 自身的默认值(temp 0.8→0.75、top-k 40→20、min-p 0.05→0、repeat-penalty 1.1→1.0),设置页里逐项注明了差异。② 删除「接入 dsh」整组设置(routeName / modelAlias / routeModelId / contextWindow / registerRoute / exposeTool / allowModelControl):这 7 项改为 configResolve.ts 里的常量,取值沿用原默认值,行为与默认安装完全一致但从此不可配置。③ maxTokens(单次最大输出 tokens)保留,从「接入 dsh」挪进「推理参数」。④ 顺带修掉一处会静默毁掉小数设置的 bug:clamp() 内部有四舍五入,temp 0.75 会被它变成 1;小数项改用新的 clampFloat()。
- 0.3.1 — 新增「多 Token 预测(MTP)」开关(mtp,默认关闭)。开启后加载时下发 --spec-type draft-mtp,用模型自带的预测头做投机解码,本地生成速度通常提升 1.2~2 倍。开启 MTP 会自动禁用视觉投影文件(--mmproj) —— 两者在 llama.cpp 里不能共存,强行一起下发会导致加载失败;这条互斥规则写在参数拼装层(src/llama/args.ts),只要 mtp 为真 --mmproj 就不可能漏下去,界面上对应的下拉框会立即置灰并在状态卡里说明原因。关掉 MTP 后用户选的 mmprojFile 不会被清空,只是暂时不生效。默认关闭,因此升级后老部署的行为一字不变。
- 0.3.0 — 两项推理侧能力:① 推理参数新增「启用思考」「保留历史 think」两个开关,按 chat_template_kwargs(enable_thinking / preserve_thinking)在每次请求的请求体上下发,关闭「保留历史 think」时还会顺手剥掉历史 assistant 消息里的 reasoning_content / think 文本,让多轮上下文中不再堆积思考内容;② 模型与目录新增「视觉投影文件」选择项,可直接挑选 mmproj(加载时作为 --mmproj 下发),留空即保持原有的「同目录自动关联」行为。这两项开关走请求体而不是 llama-server 启动参数:旧构建只会忽略它,绝不会影响模型加载,也不必为切一次开关重启模型。
- 0.2.3 — 发布准备:补 repository / homepage / engines.dsh;peerDependencies 从 "" 改为显式的预发布分支(原来的  会静默匹配不到 harness 的 -rc.x 构建);loader entry id 由裸 local-model 改为 dzqjoker-local-model 避免与其他插件撞车;去掉 private 与 prepare(产物随仓库发布,安装时不再需要构建);补 LICENSE、.gitignore、.gitattributes;PLUGIN_VERSION 与 package.json 对齐。另修 npm run verify:它过去不读 DSH Desktop 的 activeHome,数据目录被搬走后会漏扫真正在用的 profile,报出「未安装本插件」的假结论。
- 0.2.2 — 修复 CUDA OOM:新增 gpuLayersMode(auto / all / custom)。默认 auto 会下发 -ngl auto,让 llama.cpp 的 --fit 按可用显存自适应卸载 —— 之前默认把 -ngl 钉成 -1 跳过了这层保护,模型放不下时从「少放几层」变成「直接 OOM」。同时把失败日志翻译成可读诊断(fit-blocked-by-pinned-layers / cuda-oom / 形状不匹配 / 模型缺失 等十几种模式)。
- 0.2.1 — 修复 --flash-attn 形状不匹配:老构建按裸 flag 发,参数解析器把下一个 token 当成它的值吃掉;新增能力探测与三态配置(auto / on / off),auto 永远不下发最安全。
- 0.2.0 — 修复 dsh.bundle 声明形状 + 设置侧栏面板;新增浏览器半侧 bundle + 同源数据桥。
- 0.1.0 — 初版:设置项 / 拉起 / 空闲卸载 / 路径约定 / 接入 dsh 路由。

1. 需求 → 实现对照

| 需求 | 落点 |
| --- | --- |
| ① 设置里新增独立「本地模型」配置项,可选模型、可填启动参数 | 设置侧栏一级页面:src/config.ts 的 schemastery Config(唯一真源)+ src/webBridge.ts(数据面)+ client/(浏览器半侧面板)。字段说明见 第 4 节 |
| ② 用户自行下载 llama 工具与模型,存入插件规定目录 | src/paths.ts 定义目录约定并在首次运行时写入放置说明;scripts/fetch-llama.mjs 可选一键下载 |
| ③ 选定模型后首条对话自动拉起并载入 | src/proxy.ts(常驻轻量入口)+ src/lifecycle.ts 的 ensureReady() 单飞加载 |
| ④ 连续 5 分钟无交互自动卸载释放资源 | idleUnloadMinutes(默认 5);判定规则抽成纯函数 shouldUnload(),src/lifecycle.ts |
| ⑤ 参数预设:存多组带名字的参数、固定在页面顶部、一键应用 | src/presets.ts(独立落盘 state/presets.json + 作用域定义)+ webBridge.ts 的 5 条 /presets/ 路由 + 浏览器侧 client/src/presets.jsx(顶部预设条)。见 第 4 节 |

「不占资源」是硬指标:没有对话时磁盘上只有一个常驻 HTTP 代理进程(不加载模型、不占显存),
llama-server 只在第一个请求进来时才被 spawn。这一条有端到端测试兜底(见第 9 节)。

2. 安装

1) 装进 profile —— 这一步同时写入依赖 + 把它登记进 dsh.profile.bundles
dsh plugin --profile desktop add github:DZQJOKER/dsh-plugin-local-model
本地开发时改成本地路径:
dsh plugin --profile desktop add D:/path/to/dsh-plugin-local-model

2) 重启 dsh —— bundle 组合在启动时固定,装/卸插件都必须重启(刷浏览器不生效)

装完打开 设置 → 本地模型,就能看到独立的配置组。

从 GitHub 安装不需要在用户机器上构建

本仓库已包含预构建产物 —— lib/(宿主半侧)与 client/client.js(浏览器半侧)。
包里刻意没有 prepare 脚本:pnpm 对 git 依赖默认拒绝执行构建脚本,会以
ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED(needs to execute build scripts but is not in the
"allowBuilds" allowlist)在物化之前就中止安装。产物入库 + 去掉 prepare,这条路才是通的。

代价是改了 src/ 或 client/src/ 之后必须重新 npm run build 再提交,
否则用户装到的是上一次的产物。改完用 npm test 跑一遍(它会先 build 再自查)。

装完先自查一次

npm run verify

它会检查两件事:本包是不是一个合法的 dsh bundle,以及它在哪些 profile 里装了却没生效。
后者正是插件管理页显示「已安装,未生效」的成因,输出长这样:

A. bundle 清单
✓ dsh.bundle.patch = ./cordis.patch.yml
✓ patch 的 name 是包名:dsh-plugin-local-model
B. profile 挂载状态
✗ desktop → 已安装为依赖,但不在 dsh.profile.bundles 里(这就是「已安装,未生效」)

为什么必须声明 dsh.bundle

dsh 的安装机制建立在两个 manifest 概念上,都写在 package.json 的 dsh 字段里:

| 概念 | 是什么 | 声明方式 |
| --- | --- | --- |
| bundle | 一个附带配置层的 npm 包,说明「这个包贡献什么」 | "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } |
| client | 浏览器半侧(设置界面、面板、卡片) | "dsh": { "client": { "inject": [...], "platform": "web" } } + exports["./client"] |
| profile | $DSH_HOME/profiles/ 下的目录,说明「这套配置由哪些 bundle 按什么顺序组成」 | profile 自己的 dsh.profile.bundles |

没有 dsh.bundle 的包照样能装上,但只会作为普通依赖存在,dsh 不会激活它贡献的任何层。
这个设计是给「被别的插件包 import、而不是供用户直接启用」的库用的;对一个要直接启用的插件来说,
漏掉这个字段就等于白装。声明形状写错(比如写成字符串 "bundle": "包名")和没写是同一个后果。

宿主侧的 Config schema 只负责配置的读写,不会自己长出界面。 dsh 设置侧栏的每一项都是
一个客户端 UI 插件贡献的,所以要让「本地模型」出现在设置里,必须另外提供浏览器半侧
(dsh.client + lazy-CJS factory 形式的 bundle)。官方 dsh-client-ui-settings-plugins 的文档把这句
写得很直白:

卡片仍然需要一份浏览器 bundle:浏览器半侧必须是按客户端模块系统的 lazy-CJS factory 格式构建的
dsh.client 包,而产出它的 clientBundle 预设位于 packages/client/tsdown.client.ts,
并非已发布的包,因此本仓库之外的插件得自行复刻该构建。

本插件的复刻方式是 scripts/build-client.mjs(esbuild 打包 + 手工套上 window.__ModuleLoader__.load
外壳),并由 scripts/client-check.mjs 用假 loader 真跑一遍来保证格式没跑偏。

配置层的叠加顺序是:profile 里各 bundle 的 patch(按声明顺序)→ profile 自己的 cordis.patch.yml
→ $DSH_HOME/cordis.patch.yml → 命令行 --patch。后一层覆盖前一层,且是整块替换 config 而不是深合并。

3. 放模型和 llama 工具

首次运行插件会建好目录并放入说明文件:

$DSH_HOME/local-model/            # Windows 默认 C:\Users\\.dsh\local-model
├── models/                       # ← 把 GGUF 放这里(可建子目录)
│   └── PUT_GGUF_MODELS_HERE.txt
├── runtime/                      # ← 把 llama.cpp 解压到这里(可放在一层子目录里)
│   └── PUT_LLAMA_RUNTIME_HERE.txt
└── state/                        # 插件自管:pid、日志、设置与预设,不用管
├── config.json               # 设置页保存的用户层配置
└── presets.json              # 参数预设(0.5.0 起;一组都没存过时这个文件不存在)

llama 工具:去  下载对应平台的包,解压到 runtime/
(NVIDIA 选 win-cuda-x64,无独显选 win-cpu-x64,macOS 选 macos-arm64)。
也可以让脚本代劳:

node scripts/fetch-llama.mjs                 # 自动判断平台与后端
node scripts/fetch-llama.mjs --variant=cuda  # 指定后端:cuda / vulkan / hip / cpu
node scripts/fetch-llama.mjs --dry-run       # 只预览要下载哪个包

模型:下载 .gguf 放进 models/。插件支持三种布局,全自动识别:

- 单文件:Qwen3-8B-Q4_K_M.gguf
- 分片:xxx-00001-of-00003.gguf ×3 —— 会自动归并成一个模型,缺片会明确提示而不是加载半截模型
- 视觉:同目录放 mmproj-.gguf —— 只在能确定归属时自动关联(宁可漏配,不可错配);
目录里有多个模型/多个投影文件而自动关联不出来时,去 设置 → 本地模型 → 视觉投影文件 手动选一个即可。

放好后回 设置 → 本地模型 点「重新扫描」,在下拉框里选模型。

4. 设置页与设置项

面板在哪

重启 dsh 后,打开 设置,侧栏会出现一级条目 「本地模型」(排在「通用设置 / 模型」之后)。
面板里依次是:

- 参数预设(0.5.0 起,置顶那块):点名字即应用,✎ 改名,× 删除,输入框 + 「保存为预设」新建,
应用过之后还能「用当前参数更新「X」」。见 下面单独一节。
- 运行状态:状态徽标(待机 / 加载中 / 已就绪 / 已卸载 / 失败)、当前模型、进程 pid、入口地址、
预计自动卸载时间、最近一次错误;配「重新扫描 / 立即加载 / 卸载 / 刷新状态」按钮。
- 模型下拉框:列出模型目录里扫到的全部 GGUF,带量化、参数量、体积、是否分片完整、是否含视觉投影。
分片不完整的会置灰并在文案里说明。
- 六个参数分组:模型与目录 / 服务与端口 / 推理参数 / 采样与 KV 缓存 / 加载与卸载 / 诊断与高级。
每个字段都带中文说明,文案与默认值直接来自 src/config.ts 的 schema,字段被改过的会标「已覆盖」,
旁边有「默认」按钮可单独恢复。
- 目录约定:模型目录、运行时目录、用户配置文件三个绝对路径,照着放文件即可。
- 底部:保存 / 放弃修改 / 恢复默认;未保存项数会实时标出。

面板是数据驱动的:字段列表、类型、默认值、说明全部由宿主把 schema 序列化后送过来
(Config.toJSON() → src/schemaForm.ts),因此往 config.ts 加一个字段,界面上会自动出现。
两个例外是需要下拉框的字段(selectedModel 选模型、mmprojFile 选视觉投影文件)——
选项来自扫描结果、schema 表达不了,所以由客户端特判渲染,其余字段一律自动出现。

布局约束(0.5.1 起,改样式前必读)

设置面板可用宽度只有 564px,这是量出来的、不是估的:

宿主 .panel    width: 800px(max-width: calc(100vw - 48px))
左导航 .nav    width: 188px(flex:none)
.options        padding: 0 24px 24px
────────────────────────────────
内容区          800 − 188 − 48 = 564px

因此客户端样式有两条硬约束,scripts/client-check.mjs 会逐条断言(client/src/layoutCheck.js
里是规则本体,新增样式遇到同类问题时往里加一条):

1. 不写死超过可用宽度的 maxWidth。 内容区用 maxWidth: '100%'。
2. 凡是 flex / grid 里的容器,一律 minWidth: 0,列宽用 minmax(0, …)。
grid 的自动最小尺寸默认是 auto(= 不小于内容最小宽度),裸 1fr 于是拒绝压缩 ——
可用宽度不足时会把格子挤到下一行,和相邻元素在视觉上重叠。这条踩过一次:
字段行 minmax(140px, 190px) 1fr auto 的最小需求约 420px,面板 ≤800px 时必现。

置顶那条 position: sticky 另有三点必须一起满足(原因写在 styles.js 的 cardPinned 注释里):
不透明背景(遮住下层内容)、横向盖满 .options 的 24px 内边距(两侧不留缝)、
isolation: isolate(自建层叠上下文,不与面板标题栏抢层级)。

颜色变量:只能用 --dsw-(0.5.2 起,血泪条款)

DSH 里不存在任何 --color- 变量。 宿主 @deepseek-ai/dsh-client-ui-theme 只定义一套 --dsw-:

--dsw-alias-bg-base / bg-layer-1|2|3 / bg-overlay / bg-mask-1
--dsw-alias-border-l1|l2|l3|l4
--dsw-alias-label-primary|secondary|tertiary|dimmed|caption
--dsw-alias-button-primary-fill|hover / button-ghost-active-fill|border
--dsw-alias-interactive-bg-hover
--dsw-alias-state-error|success|warn-primary / -secondary / -tertiary
--dsw-static-neutral-bluish-{00,50,60,75,100,…,1000} 等静态色板

写完 var(--color-background-primary, 'transparent') 这种代码时,不会有任何报错、任何警告 ——
变量不存在,var() 直接退回 fallback,而 fallback 是手写的、很容易写成 transparent。
症状就是「元素是透明的、下层文字透上来」,然后你会去怀疑 z-index、position、
层叠上下文……全都不对,因为根本没东西被画出来。

三条规则(client-check.mjs 会拦):

1. 只用 --dsw- / --font- / --dsh-,其余前缀一律视为宿主不提供。
2. 会挡住下层内容的容器(card / cardPinned / footer)背景必须不透明。
面板底统一用 --dsw-alias-bg-layer-2 —— 宿主 .MI-_Aa_panel 自己就是这个值,
两边同色才能让 sticky 元素「融进」面板、看不出接缝。
3. sticky 条要盖满 .options 的横向内边距:marginLeft/Right: -24 + paddingLeft/Right: 24。
否则卡片两侧各留 24px 缝,滚动时下层内容会从缝里穿过去。

排查这类问题的通用手法:从 app.asar 全量扫变量定义(find.mjs / pick.mjs 在
D:\日常工作区\_dev-tools\dsh-asar-tools\)。只要某变量在 21810 个文件里出现次数为 0,
它就是不存在,代码里每一次 var() 都在走 fallback。

参数预设(0.5.0 新增)

设置页最上面那一块,用 position: sticky 钉在顶部:下面几十项参数是用来调的,
切换整套参数时不该还让你滚回页面顶端去找按钮。

| 操作 | 怎么做 |
| --- | --- |
| 新建 | 输入名字(回车或点「保存为预设」)。名字最长 40 字,重名会被拒绝(不区分大小写),最多 100 组 |
| 应用 | 点预设名。按新参数卸载模型,下次对话重新加载 —— 与「保存设置」同一条路径、同一个后果 |
| 重命名 / 删除 | 预设上的 ✎ / ×。删一组参数不影响当前生效的设置 |
| 更新某一组 | 应用过之后,会出现「用当前参数更新「X」」——把界面上这套参数写回它,名字不变 |
| 看差异 | 每个预设上标着「N 项不同」,与当前生效配置完全一致的那个标「当前生效」并高亮 |

哪些设置会进预设,哪些不会:

| | 字段 | 为什么 |
| --- | --- | --- |
| ✅ 进预设(41 项) | 当前模型、视觉投影文件、MTP、上下文长度、GPU 层数、线程/批大小、Flash Attention、KV 精度与策略、8 项采样参数、图像 token 预算、推理预算、三个思考开关、Jinja/模板、mmap/mlock、附加参数、环境变量、空闲卸载与超时重试、单次最大输出 | 这些就是「一套参数」的内容。像「看图」这种预设,本来就该连同模型一起切过去 |
| ❌ 不进预设(9 项) | enabled、modelsDir、runtimeDir、llamaServerPath、host、port、llamaPort、apiKey、logLevel | 这些属于这台机器而不是这套参数:端口和别的服务是否占用有关、路径是部署环境、密钥不该在预设文件里再存一份明文、日志级别是排查时的临时开关。切个参数预设却把端口换掉,只会是事故 |

作用域由 src/presets.ts 的 PRESET_EXCLUDED_KEYS 唯一定义,界面上的说明文案也来自同一处
(宿主把 keys / excluded 一起送进 /state),不存在两边各写一份而漂移的可能。

两条交互规则值得知道:

1. 保存的是界面上当前这套参数,含还没保存的修改 —— 界面会写明「含 N 项还没保存的修改」。
数字框被清空时按「未设置」处理(回落成已保存的值),不会写成 0;
2. 应用预设会丢弃未保存的草稿,所以有草稿时会先弹一次确认;应用成功后草稿清空 ——
否则界面上会留下一堆「未保存」的假象。

预设落在 /local-model/state/presets.json,与 config.json 同目录但独立成文件:
一份配置 + 若干套可切换的参数,互不干扰。文件损坏、内容离谱(缺名字、缺 id、重复 id、一项参数都没有)
都只会降级成「没有预设」并在面板上说明,绝不影响模型加载这条主线 —— 它只是便利功能。

设置的三个层级

沿用 dsh 自己的三层语义,用户层落在插件目录里:

schema 默认值  →  组合层(cordis.patch.yml / 部署配置)  →  /local-model/state/config.json

面板保存写入的是最上层那个文件;「恢复默认」清空它,于是回落到组合层 + schema 默认值。
浏览器发来的 JSON 只按 src/configStore.ts 里的类型表做强制转换,表里没有的键一律丢弃 ——
界面写不进一条会污染 llama-server 命令行的脏数据。

「应用预设」写的是同一个文件、过的是同一道闸门:预设值先按作用域过滤(见上一节),
再走 ConfigStore.update() —— 于是「点预设」与「点保存」在落盘、卸载、提示三件事上完全一致,
不存在两套逻辑各跑各的可能。预设文件本身也不豁免:presets.json 里的值读回来时同样过一遍闸门,
手改配置文件塞进去的脏数据不会跑到命令行上。

为什么不用 dsh 的 settings 命名空间:当前 dsh 版本的 settings apiproxy 只服务硬编码的命名空间白名单,
第三方插件的命名空间一律答复 settings-not-exposed,浏览器侧既读不到也写不进。
自建一条同源 HTTP 桥(/api/local-model,注册在宿主 webServer 上)是这一版宿主上唯一可靠的通道。

桥的安全姿态

本机工具、没有账号体系,所以靠三条硬约束而不是鉴权:

1. 只接受回环来源的请求 —— 配置变更与进程启停不该被局域网里的谁触发(/presets/ 也一样);
2. 写操作要求 content-type: application/json —— 普通表单跨站提交做不到这一点,
于是即便有恶意页面在浏览器里跑,也发不出能改配置的请求;
3. 请求体有大小上限。

因此:通过局域网/远程 Web UI 访问 dsh 时,这个面板会被拒绝(这是刻意的)。本地模型本来就是本机能力。

字段一览

类型、默认值与说明都写在设置面板里,这里只列关键分组:

| 分组 | 关键字段 |
| --- | --- |
| 模型与目录 | enabled、selectedModel、mmprojFile(视觉投影文件)、mtp(多 Token 预测,开启后自动禁用视觉投影)、imageMinTokens / imageMaxTokens(每张图的 token 预算下限/上限)、modelsDir、runtimeDir、llamaServerPath、preload |
| 服务与端口 | host(默认只听回环)、port(默认 18080)、llamaPort(默认 0 = 每次自动挑空闲端口) |
| 推理参数 | ctxSize、maxTokens(单次最大输出 tokens)、nPredict(服务端默认输出上限)、guardContextOverflow(输出上限溢出保护,默认开)、gpuLayers、loadMode、threads、batchSize/ubatchSize、flashAttention、mmprojOffload、mtpWithVision(MTP 与视觉共存,默认开)、reasoningBudget(推理 token 预算)、reasoningBudgetMessage、reasoningEffort、jinja、chatTemplate、chatTemplateFile、chatTemplateKwargs、enableThinking、preserveThinking、mmap、mlock |
| 多 Token 预测(MTP)细节 | specKvDtype、specDraftNMax、specDraftPMin(只在开了上面「多 Token 预测(MTP)」时生效) |
| KVMem 分块缓存(kvmem 分支专有) | kvmemEnabled、kvmemBudget、kvmemGenReserve(解码预留 = 单次生成上限,构建默认仅 256)、kvmemBlockTokens、kvmemSinkTokens、kvmemRecentTokens、kvmemMethod、kvmemQueryLast、kvmemQueryMaxTokens、kvmemQueryReplay、kvmemQueryPolicy、kvmemMtpState、kvmemGpuRatio、kvmemCpuGb、kvmemNvmeGb、kvmemNvmeDir、kvmemHarvestV、kvmemRawKNvme。整组默认值都是「不下发」(数字 -1、字符串留空);官方 llama.cpp 构建上会被整体跳过并记日志 |
| 采样与 KV 缓存 | kvUnified(统一 KV 缓存)、kvStreamStageMib(KV 主机内存暂存 MiB)、temp、topK、topP、minP、presencePenalty、repeatPenalty、repeatLastN、seed |
| 加载与卸载 | idleUnloadMinutes(默认 5)、startupTimeoutMs、shutdownGraceMs、autoRestart、maxRestarts |
| 诊断与高级 | apiKey、extraArgs、envOverrides、logLevel(排查加载问题设 debug) |

原先还有一组「接入 dsh」(routeName / modelAlias / routeModelId / contextWindow /
registerRoute / exposeTool / allowModelControl)。0.4.0 起这一组已从 schema 删除,
改为 src/configResolve.ts 里的常量(取值 = 原默认值),因此行为不变但不再可配置。
详见 第 4 节「接入 dsh 那些设置去哪了」。

保存后模型会被卸载一次,下次对话按新参数重新加载 —— 用户刚改完设置,期望的就是这个行为。

插件会先探测你的 llama.cpp 再拼参数

同一个参数在不同版本的 llama.cpp 里形状会变。最典型的是 --flash-attn:

| 构建 | 形状 | 插件怎么发 |
| --- | --- | --- |
| 新(如 b10822) | -fa, --flash-attn [on|off|auto] 带值 | --flash-attn on / --flash-attn off |
| 老 | -fa, --flash-attn 裸开关 | --flash-attn(仅当设为 on) |

如果不探测就直接按老形状发裸 flag,新构建的参数解析器会把下一个 token 当成它的值吃掉,
报出来的却是 unknown value for --flash-attn: '--mmproj' —— 错误信息指向被吞掉的那个参数,
排查方向会完全跑偏。

所以插件在起进程前先跑一次 --help,问清楚两件事:这个构建认识哪些选项、
--flash-attn 是哪种形状(结果按可执行文件缓存,每次加载只付一次代价)。顺带得到两个好处:

- 不认识的选项不会被发出去(见「换了一个 llama 分支也能起来」一节)——
不少分支对未知选项是直接 unknown flag: xxx + exit 1,不是忽略;
- 万一探测结果与实际不符(或探测失败导致下发形状不对),服务报错后插件会自动去掉这个参数重试一次,
并把「已改为不下发」写进日志 —— 不必让用户去猜。

flashAttention 因此是三态而不是开关:auto(默认)永远不下发该参数。因为 auto 在所有支持三态的构建里
都等于默认值,在只支持裸开关的老构建里又根本表达不出来 —— 不下发是唯一在两种构建上语义都正确的做法。
历史配置里的 true/false 会自动收敛成 on/off。

另外:较新的 llama.cpp 已把 --mlock / --no-mmap 标记为 DEPRECATED(建议改用 --load-mode),
但两者仍然可用,插件继续沿用;将来若被移除(或换了不带它们的构建),门控会直接跳过并点名。

多 Token 预测(MTP)

0.3.1 新增,默认关闭。开启后加载时下发 --spec-type draft-mtp,让模型用它自带的预测头
一次猜测并校验多个 token(投机解码),本地生成速度通常能提升 1.2~2 倍,回复越长越明显。

MTP 与视觉投影的关系见下一节「MTP 与视觉共存」 —— 0.7.0 起两者默认可以同时开启,
互斥规则只有在关掉 mtpWithVision 时才恢复(上游 llama.cpp 需要那样)。

MTP 与视觉共存(0.7.0)

设置项 mtpWithVision,默认开启。上游 llama.cpp 里 MTP 与图像输入不能同时下发(会加载失败),
但 kvmem 分支的 llama-kvmem-server 支持:同时给 --mmproj 与 --spec-type draft-mtp 时
clip_model_loader: has vision encoder 与 creating MTP draft context 会同时出现、服务正常起来、
看图对话也能识别(2026-09-21 本机实测)。所以默认允许「开着 MTP 用图片」。

关掉它即恢复旧的互斥:MTP 生效、--mmproj 被忽略 —— 界面上下拉框会置灰、状态卡说明原因,
你选的 mmprojFile 不会被清空(免得来回切开关时丢配置)。换回官方 llama.cpp 的用户请关掉本项。

本次启动参数面板(0.7.0)

设置页最顶部会显示这次真正下发给 llama-server 的完整命令行(逐项一行),外加一张参数摘要
(含派生的「GPU KV 合计 = 工作集 + 解码预留」)和一个「复制命令行」按钮。

两个刻意的取舍:

- 数据源是下发的 args,不是设置页里的配置。 两者之间隔着门控跳过、构建默认值与自动收敛
(-ub  最容易踩的坑是第 4 条:普通 GGUF 打开开关不会有任何加速,而且不报错。如果你开了 MTP 却感觉没变快,
先确认模型文件名里有没有 MTP 字样,再看 llama.cpp 的构建日期。

采样参数与 KV 缓存策略(0.4.0 新增)

这一组每次加载都会显式下发,因此会覆盖 llama.cpp 自身的默认值。差异也逐项写在设置页的说明里,
这里汇总成表:

| 参数 | 插件默认 | llama.cpp 默认 | 说明 |
| --- | --- | --- | --- |
| --temp | 0.75 | 0.80 | 温度:越高越随机 |
| --top-k | 20 | 40 | 只从概率最高的 K 个 token 里采样;0 = 不过滤 |
| --top-p | 0.95 | 0.95 | 核采样阈值(一致) |
| --min-p | 0.0 | 0.05 | 0 = 关掉最小概率过滤 |
| --presence-penalty | 0.0 | 0.0 | 存在惩罚(一致) |
| --repeat-penalty | 1.0 | 1.10 | 1.0 = 不做重复惩罚 |
| --repeat-last-n | 64 | 64 | 检查最近多少个 token(一致);构建不公开它时跳过(见「分支构建」一节) |
| --seed | -1 | -1 | -1 = 每次启动都用随机种子(一致)。负数不下发:负号只是「随机」的写法,而不下发就是随机 —— 但有些分支把取值校验成 uint32,收到 -1 会直接报错退出 |

所以升级到 0.4.0 之后生成风格会变:更确定(temp 更低)、候选更窄(top-k 减半)、
不再惩罚重复、关掉了 min-p 过滤。想退回原样,把上表右列的值填回设置页即可。

KV 两项用于长上下文:

| 参数 | 插件默认 | 作用 |
| --- | --- | --- |
| --kv-unified | 关闭 | 用一整块统一缓冲管理 KV 缓存,减少「按层分配」造成的显存碎片,长上下文更容易装下。代价是这块缓冲在加载时就按完整上下文预留 —— 即使很少跑满也占着显存 |
| --kv-stream-stage-mib | 1024 | 把这么多 MiB 的 KV 缓存暂存到主机内存,以减轻显存压力。0 = 不下发。这是「自适应 KV 流式」那个 llama.cpp 分支的私有参数,上游构建不认识它。开启时插件会自动追加 -np 1(见下方说明) |

⚠️ --kv-stream-stage-mib 有个前置条件:块级 KV 流式要求「恰好一个序列」。
而 llama.cpp 的 -np 默认是 -1(自动),会落到多序列 —— 于是「一开暂存就加载不了」:

E llama_init_from_model: failed to initialize the context:
block KV streaming requires exactly one sequence (-np 1)

所以只要暂存值 > 0 且构建支持它,插件就自动追加 -np 1(并在日志里说明),
不会把这个坑留给你去猜。副作用是并发槽位变成 1 —— 本地单人使用没有影响。
如果你在「附加参数」里自己写过 -np / --parallel,插件不会覆盖你的显式设置,
但会在日志里警告:那里不是 1 就会加载失败。

图像两项(--image-min-tokens / --image-max-tokens)只对动态分辨率的视觉模型生效,且要先挂上视觉投影文件;
max 小于 min 时插件会就地把它抬到 min(否则 llama.cpp 拒绝启动)。
--reasoning-budget 限制思考链的最大长度,0 = 关掉思考、-1 = 不限。

dsh 的「推理等级」滑杆怎么连到本地模型

对话框里的 Off / Low / Medium / High 要真正生效,得跨过三道关。0.4.1 及以前只走通了一半,
所以出现「拨了没反应」;0.4.2 把三道都走通了:

| 环节 | 谁负责 | 说明 |
| --- | --- | --- |
| ① dsh 知道这是推理模型 | 你的路由配置 | 模型条目里要有 reasoningEfforts。没有它,pi-ai 认为这不是推理模型,一个思考参数都不发 |
| ② 档位送到 llama-server | 代理层 | 档位必须落在 chat_template_kwargs.reasoning_effort。顶层的 reasoning_effort 会被静默丢掉(无报错、无日志),插件会把它挪进去 |
| ③ 发给模板的值必须是它认的 | 代理层 | 插件读模型的对话模板,把档位重映射成模板真正支持的那几个 |

第 ③ 条是关键,也是 0.4.1 踩的坑:模型模板对不认识的档位是直接 throw,不是忽略。
实测这个模型的模板里写着:

{%- if resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
{{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~ ...) }}

它只认 xhigh / medium / low(默认 xhigh)。于是面板上选个 High、或者 dsh 把 off 写成布尔 false,
都会让整次对话 500。0.4.2 起插件的做法是:

- 加载后读一次模型模板,解析出它支持的档位表(日志里会打出来,设置页状态卡也显示);
- 面板档位 → 映射到表里最接近的一个(距离相同时取更高,所以 high → xhigh);
- 映射不出来、或压根没解析到档位表 → 干脆不发这个字段,让模板用自己的默认值。

off 的传递也一并修好了:dsh 会把「档位 = off」经 settings.yaml 往返后变成布尔 false
(YAML 把 off 读成布尔)。插件现在认得这种形态,会把它翻译成 enable_thinking: false。

开关保持开启时,最终发给 llama-server 的内容:

| 对话框档位 | 实际下发 |
| --- | --- |
| Off | {"enable_thinking": false, "preserve_thinking": true} |
| Low | {"enable_thinking": true, "reasoning_effort": "low", "preserve_thinking": true} |
| Medium | {"enable_thinking": true, "reasoning_effort": "medium", "preserve_thinking": true} |
| High | {"enable_thinking": true, "reasoning_effort": "xhigh", "preserve_thinking": true} ← 按模板改写 |

想知道你这台机器实际解析出的档位表:看日志里的「模型模板支持的推理档位」那一行,
或看 设置 → 本地模型 状态卡里的「推理档位」。它决定了面板档位最后会变成什么。

另一个更可靠的杠杆是「推理 token 预算」(--reasoning-budget,引擎级硬上限):
档位是「请模型自己想多深」,预算是「想超过 N 个 token 就强制打断」。两者互补,不冲突。

三个和「思考」有关的设置,怎么配合

设置页里有三项都会影响模型的思考行为。它们不冲突,但作用层不同 —— 混着调很容易得出错误结论:

| 设置 | 作用层 | 生效时机 | 干什么 |
| --- | --- | --- | --- |
| 启用思考 | 模板级 | 每次请求 | 告诉对话模板:要不要进入思考模式。只在请求什么都没说时才生效 —— 请求带了 enable_thinking 或推理档位就听请求的 |
| 保留历史 think | 模板级 | 每次请求(preserve_thinking,关闭时还会剥掉历史里的 think 文本) | 多轮对话中,历史 assistant 的思考内容要不要留在上下文里 |
| 推理 token 预算 | 引擎级 | 加载时(--reasoning-budget) | 数着思考 token,超了强制结束思考 |

为什么「启用思考」走请求体、而预算走启动参数:前者是每轮都可能变的偏好,改在请求体里切一次立即生效,
且旧构建最坏只是忽略它;后者是采样阶段的约束,llama.cpp 只提供了启动参数这个入口。

两个建议避开的组合:

| 组合 | 会发生什么 |
| --- | --- |
| 启用思考 关 + 预算 > 0 | 预算永远不会被触发 —— 模板都不进思考了,哪来的思考 token。看上去「预算没起作用」,其实是被架空 |
| 启用思考 开 + 预算 0 | 模型先进思考、再被立刻掐断。通常给出一个空的思考块,个别模型上还会直接跑偏 |

要彻底不思考,用「启用思考 = 关」(快、稳、旧构建也认);要给思考链设上限,才用预算。

预算还有个副作用值得知道:耗尽时模型是被硬掐断的,而 llama.cpp 的过渡提示语
(--reasoning-budget-message)插件不下发。官方维护者用 HumanEval 实测:预算设得太紧又没有提示语时,
得分从 94% 掉到 78%。需要提示语就用「附加参数」自己加 --reasoning-budget-message "…"。
另外 --reasoning-budget 需要较新的构建(插件会先探测,构建不认就跳过并在日志里说明)。

换了一个 llama 分支也能起来:只下发构建承认的选项

--kv-unified、--kv-stream-stage-mib、--image--tokens、--reasoning-budget 都比较新,
其中流式暂存更是特定分支独有。而不认识的选项会让服务直接启动失败 ——
那等于「换了个分支 / 加了个开关,插件反而起不来了」。

所以插件在起进程前先跑一次 --help(本节开头介绍过这套探测),并且只下发构建承认的选项。
两档严格程度,分别解决两个具体问题:

| 哪一类选项 | 探测失败时 | 构建明确不公开时 | 例子 |
| --- | --- | --- | --- |
| 只有新构建 / 特定分支才有的 | 不下发(宁可退回构建默认值,也不赌它认) | 不下发 | --kv-unified、--kv-stream-stage-mib、--image--tokens、--reasoning-budget |
| 其余(别名、线程、批大小、采样参数…) | 照旧下发(与历史行为完全一致,一次 --help 失败不该丢掉你的采样设置) | 不下发 | --alias、-t、--threads-batch、-ub、--repeat-last-n、--no-mmap、--mlock、--api-key、--temp 那一组 |

跳过的一律写进日志(这个 llama-server 不认识以下选项,已跳过(不影响加载):…),
绝不静默 —— 你能看到的是「哪几个选项没发出去」,而不是「命令行里少了一项」。

实测反例(kvmem-v0.16.0-rc2 那个独立的 llama-kvmem-server.exe,它的选项表是 llama.cpp 的真子集):

unknown flag: --alias
usage: ...\llama-kvmem-server.exe -m model.gguf [options]

它一次只报一个未知选项然后 exit 1 —— 所以「只修 --alias 一个」是不够的:
-t / -ub / --repeat-last-n / --no-mmap / --api-key 会是下一个。
--seed -1 则是另一种形态:选项认识,但值超出它校验的 uint32 范围(见上一节的表格)。

想知道你的这份 llama.cpp 认哪些选项、插件实际会下发什么?跑 npm run verify:llama:
它用你的实际配置拼出完整命令行(模型换成不存在的哨兵路径,不占显存),真的起一次服务并报告结论;
它会同时报出「哪些选项被跳过了」,以及实测里有没有出现 unknown flag 这类参数错误。

「接入 dsh」那些设置去哪了

0.4.0 按用户要求删掉了整组「接入 dsh」设置。它们改成了常量(src/configResolve.ts),
取值就是原来的默认值:

| 原设置 | 现在的常量 | 影响 |
| --- | --- | --- |
| routeName | local-llama | 路由名固定,dsh 侧 settings.yaml 不用改 |
| modelAlias / routeModelId | local | 模型选择器里的 id 固定为 local(由插件自己的 /v1/models 回答);--alias 只是顺便让上游也叫这个名字,构建不认它时跳过,dsh 侧 id 不受影响 |
| contextWindow | 跟随 ctxSize | 见下 |
| registerRoute | 恒为真 | 始终尝试注册路由;宿主没接口时照旧降级为打印可粘贴的 YAML |
| exposeTool | 恒为真 | local_model 工具照旧注册 |
| allowModelControl | 恒为假 | 能力取舍:local_model 的 start / stop 从此恒被拒绝,无法再打开 |

contextWindow 是唯一一个取值发生变化的:它过去独立可配(默认 32768),而 ctxSize 默认 8192 ——
两者不一致正是插件一直警告的「声明比实际大 → 长会话中途崩」。现在声明的窗口直接取 ctxSize,
两者同源、不可能再错位,启动后的对账警告也因此更准。

副作用提醒:local_model 工具的 start / stop 动作现在恒被拒绝(这本是原默认行为,
但过去能用 allowModelControl 打开)。要恢复可控,得改 src/configResolve.ts 里的常量。

5. 接入模型路由

插件启动时会尝试把本地端点注册进 dsh 的 llm 缝。注册成功(日志里能看到
已通过 llm.() 注册本地模型路由)就可以直接在模型选择器里选 local。

如果宿主没暴露注册接口,插件不会报错、也不会影响模型加载,只在日志里打印一段
可以直接粘贴的配置;把 examples/settings-route.yaml 的内容并进 $DSH_HOME/settings.yaml
即可(已存在 providers 时是并入,不要整体覆盖):

llm-pi-ai:
providers:
local-llama:
displayName: 本地模型(llama.cpp)
api: openai-completions
baseURL: http://127.0.0.1:18080/v1
streamIdleTimeoutMs: 600000      # 本地/CPU 推理慢,这个必须放宽
models:
- id: local
contextWindow: 8192          # 跟随「设置 → 上下文长度」,改 ctxSize 时这里也要跟着改
maxTokens: 8192

上面是最小可连通版本。完整版(含「推理档位」的 compat 与 reasoningEfforts)见
examples/settings-route.yaml —— 少了那一段,dsh 的「推理等级」滑杆会失灵;
插件自动注册时会带上它,/local-model route 打出来的也是完整版。

0.4.0 起 local-llama 与 local 这两个名字、以及 contextWindow = ctxSize 的规则都是固定的
(原 routeName / modelAlias / routeModelId / contextWindow 设置已删除)。
手写配置时照抄上面的 id 即可;contextWindow 是唯一需要你自己跟上 ctxSize 的值。
插件自己注册的那条路由会用 ctxSize 自动填这个字段,所以只有手写配置时才需要操心它。

会话里也可以用 /local-model route 随时打印这段配置。

6. 运行机制

为什么要有一个常驻代理? 最直接的做法是让 dsh 连 llama-server 的端口。但模型卸载后那个端口
就没人监听了,下一条对话必然连接失败 —— 而「按需加载」又要求端点平时不在。这是个死结。

解法是让插件常驻一个极轻量的 HTTP 入口(不加载模型、不占显存),由它消化掉「模型还没加载」这件事:

第一条对话 ──▶ 插件代理 :18080 ──ensureReady()──▶ 拉起 llama-server(内部随机空闲端口)
│                                   │
│◀────── 等 /health 就绪 ────────────┘
└──原样转发(含 SSE 流式)──▶ llama-server
│
连续 5 分钟无交互 ◀────────┘ 卸载 → 端口释放 → 回到待机

对 dsh 来说端点始终在线;对用户来说显存只在真正用的时候才被占。

代理会改一种请求体:对话补全

「原样转发」有一个例外 —— POST /v1/chat/completions 的请求体会按两个思考开关改写:

请求体 ──▶ 合并 chat_template_kwargs(enable_thinking / preserve_thinking)
└─▶ 关闭「保留历史 think」时:剥掉历史 assistant 消息的 reasoning_content 与 think 文本

为什么改在请求体而不是 llama-server 启动参数(--chat-template-kwargs):

1. 启动参数在旧构建上根本不存在,一旦下发就是「模型起不来」;请求体里多一个字段,
旧构建的 JSON 解析器直接忽略,最坏也只是开关无效,模型加载这条主线绝不受影响;
2. 这两个开关是「每次请求」的语义,改完即生效,不必为切一次开关重启模型;
3. 「不保留历史 think」要动的是 messages 本身,启动参数表达不了。

其余路径(/v1/models、/health、/v1/embeddings…)连请求体都不读,仍是零改动的流式直通;
认不出的请求体(非 JSON、非对象)一律原样放行 —— 一个可以降级的设置问题,不该变成一次请求失败。

状态机

disabled ──启用──▶ idle ──首条对话──▶ starting ──就绪──▶ ready
▲                                    │
└──── 空闲 5 分钟 / 切换模型 / 手动 ────┘
│
崩溃且未超上限 ─┘──▶ starting(退避重试)

7. 稳定性设计

这些都是「启动与卸载逻辑稳定可靠」的具体承诺,且多数有测试覆盖:

- 加载单飞:并发请求共享同一次加载,不会重复 spawn 出多个进程抢显存。
- 残留进程回收:dsh 被强杀会留下握着显存的孤儿 llama-server。插件下次启动时按 state/llama-server.pid
先回收它,再启动新的 —— 顺序反了就是 OOM。
- 崩溃自愈:非预期退出时按 1s/2s 退避重启,超过 maxRestarts 进入 failed 并停止重试(不无限重启打满 CPU)。
- 端口自适应:llamaPort=0 每次自动挑空闲端口;对外端口被占用会向后找 20 个,仍不行则交给内核分配并提示。
- 进程树回收:超时后强制结束整棵进程树(taskkill /T /F / SIGKILL),避免后端 worker 变成孤儿。
- 不打断生成:requestTimeout/timeout 置 0;客户端中断(点停止)会同步掐掉上游推理,不继续空烧显存。
- 卸载保护:有活跃请求时绝不卸载;加载未完成时的卸载请求会先等加载结束。
- 参数自纠:-ub > -b 这类会让 llama.cpp 直接启动失败的组合,在拼参数阶段就收敛掉。
- 密钥不落日志:命令行日志里 --api-key 一律显示为 *;设置页标记为敏感字段。

8. 故障排查

| 现象 | 原因与处理 |
| --- | --- |
| 插件管理里显示「已安装,未生效」 | 两种成因:① package.json 没声明 dsh.bundle,或声明形状不对(正确形状见第 2 节);② 该包没被登记进 profile 的 dsh.profile.bundles。跑 npm run verify 直接定位,再 dsh plugin --profile  add  一次补全 |
| 插件已生效,但设置里没有「本地模型」 | ① 改完 manifest 后没重启 dsh(bundle 组合在启动时固定);② dsh.client 缺失或 exports["./client"] 没指向产物 —— 跑 npm run verify 与 npm run test:client 自查 |
| 设置面板打开是空白 / 一直「正在读取配置…」 | 浏览器控制台看 /api/local-model/state 的返回。若是 403,说明你是通过局域网地址访问的 —— 面板刻意只服务本机(见第 4 节) |
| 面板里改了设置不生效 | 保存后模型会卸载一次,下次对话按新参数加载;确认点的是「保存」而不是只改了输入框(底部会显示未保存项数) |
| 改完代码 / 重新 build 后行为没变 | bundle 组合在 dsh 启动时固定:装、卸、更新都必须重启 profile。客户端 bundle 也一样,改完要 npm run build:client 再重启 |
| 加载失败,日志里出现 error while handling argument "--xxx": unknown value for --xxx: '--yyy' | 参数形状不匹配:解析器把下一个参数当成前一个的值吃掉了。插件已内置探测与自动重试(见第 4 节),若你仍在旧版本上遇到,升级到 0.2.1+ 即可;诊断用 npm run verify:llama |
| 换了 llama 分支后加载失败,日志里出现 unknown flag: --alias(或 -ub / --repeat-last-n / -t / --no-mmap / --api-key) | 这个分支不是 llama.cpp 的 llama-server(例如 llama-kvmem-server.exe 这类独立 OpenAI 兼容 server),它的选项表只是 llama.cpp 的子集,且对未知选项是直接退出 1。插件会在启动前按 --help 探测并跳过它不认识的选项(0.5.3+),日志里能看到「已跳过:--alias、-ub、…」;升级到该版本即可。若你在旧版本上,临时办法是在「设置 → 本地模型 → llama-server 路径」换回官方 llama.cpp 构建 |
| 加载失败,日志里出现 invalid --seed: seed out of range [0, 4294967295] | 这个分支把 --seed 校验成 uint32,而插件默认值是 -1(= 随机)。0.5.3+ 起负数一律不下发该参数(与「随机」等价:llama.cpp 的默认值本来就是 -1),因此不会再出现;旧版本上把「随机数种子」填成 0 或任意正数即可绕过 |
| 对话每一条都失败,返回 400 {"error":"prompt + max_tokens exceeds n_ctx"} | 这是 dsh 侧路由声明与启动参数对不上,不是模型坏了。kvmem 那个独立 server 会把 prompt + max_tokens > n_ctx 判成硬错误(上游 llama.cpp 只会 warning 后自行收敛),而报错里既没有 prompt 长度也没有 n_ctx。最常见的成因是 settings.yaml 里手写的路由把 contextWindow / maxTokens 填得比实际 -c 大(实测:声明 maxTokens: 128000、实际 -c 32768,于是 128000 > 32768 恒成立,每条消息必失败)。三步解决:① 看设置 → 本地模型 → 上下文长度(或插件日志里「已拉起:… -c N」那一行)拿到真实 n_ctx;② 把该模型的 contextWindow 改成这个值、maxTokens 改成它的 1/4 左右(例如 32768 / 8192);③ 0.6.0+ 已经内置兜底:guardContextOverflow(默认开)会预防性压掉必然失败的超大上限,并在服务端拒绝后逐级减半重试,日志里能看到「已把输出上限从 X 压到 Y 后重试」 |
| 模型「话说一半就停了」/ 每次回复都特别短(明明没到上下文上限) | 看 设置 → 本地模型 → KVMem 分块缓存 → 解码预留(--kvmem-gen-reserve)。它是 kvmem 里单次生成的上限(含思考),而这个构建的默认值只有 256 —— 不设置它,每次回复最多只能写 256 个 token,且不会有任何报错。把它设成不小于「单次最大输出 tokens」(例如 8192~16384)。加载日志里有一条专门的提醒,看到「解码预留…当前为 256」就是这个问题 |
| 想调 KVMem 的分块检索(budget / 块大小 / 检索算法 / NVMe 溢场…) | 0.6.0+ 在设置页新增了「KVMem 分块缓存(kvmem 分支专有)」整组,共 18 项,对应 --kvmem- 家族;另有「多 Token 预测(MTP)细节」组(--spec-kv-dtype / --spec-draft-n-max / --spec-draft-p-min)。整组默认值都是「不下发」(数字项填 -1、字符串留空),不动它们就等于用构建自己的默认行为。这些是 kvmem 分支专有参数,官方 llama.cpp 构建上会被整体跳过并在日志里说明 |
| 想知道「我的 llama.cpp 会不会接受插件下发的参数」 | 跑 npm run verify:llama。它会读你的实际配置、拼出参数、真的起一次 llama-server(把模型换成不存在的哨兵路径,因此不会占显存),并给出通过/失败与完整命令行 |
| 请求返回 503「还没有选择本地模型」 | 去 设置 → 本地模型 选模型;模型要放在 modelsDir 下 |
| 503「没有找到 llama-server 可执行文件」 | 错误信息里会列出查找过的三个位置;用 scripts/fetch-llama.mjs 或手填 llamaServerPath |
| 503「选中的模型已不在模型目录里」 | 模型被移动/删除,重新选一下 |
| 一直卡在「正在加载模型…」 | 看插件日志里 llama-server 的输出(logLevel=debug);多半是显存不足或 -ngl 太大 |
| 加载失败,日志里出现 n_gpu_layers already set by user to N, abort + cudaMalloc failed: out of memory | OOM + 自适应被钉住:把「设置 → 本地模型 → GPU 层数策略」改成「自动」(这是 0.2.2+ 的默认)。--fit 只会调整「用户没显式设置」的参数;一旦把 -ngl 钉成具体数字,自适应就被跳过,模型放不下时从「少放几层」变成「直接 OOM」。同时把上下文长度调小、换更低比特量化 |
| 模型看不到工具、不调用工具 | 检查 jinja=true;再试 chatTemplate=chatml 或指定模板文件 |
| 切了「启用思考 / 保留历史 think」但输出没变化 | 两条前提:① llama.cpp 要支持请求体里的 chat_template_kwargs(2025-06 之后的构建,旧构建会忽略该字段,表现为开关无效);② 模型模板要认 enable_thinking / preserve_thinking(带思维链的模板如 Qwen3 / Qwen3.6 才认)。logLevel=debug 会把「未能改写请求体」的原因打出来 |
| 加载失败,日志里出现 failed to load mmproj / clip_model_load | 视觉投影文件与模型不配套,或路径不对。去 设置 → 本地模型 → 视觉投影文件,把选项恢复成「自动」让插件重新按同目录关联;纯文本模型保持「自动」即可 |
| 对话框报 400 "Failed to load image or audio file",而同一批里有些图又能正常识别 | 失败的是 WebP 图片。dsh 转码时会产出 WebP 与 JPEG 两种(实测 request-images/ 下两种都有),而 llama.cpp 内置的图像解码器不认识 WebP —— 它需要外部的 ffprobe + ffmpeg 来转码,而这两个是从 PATH 里找的(这个构建砍掉了 --ffmpeg-path)。判断方法:看插件日志(logLevel=debug)里有没有 probe: failed to launch ffprobe / failed to decode webp buffer。修法:winget install Gyan.FFmpeg。0.7.0 起插件会自动找到 ffmpeg 并把它的目录前置进 llama-server 子进程的 PATH(装完立刻生效,不必改系统 PATH、也不必为此重启 dsh);找不到而你又开着视觉投影时,加载日志里会有一条说清「哪个格式会失败、为什么只有它、怎么修」的警告 |
| 开了 MTP 之后,视觉投影设置变成了灰色 / 状态卡说「视觉投影已被 MTP 顶掉」 | 0.7.0 起默认不会再发生:MTP 与视觉投影现在可以共存(mtpWithVision 默认开启,kvmem 分支实测支持同时加载视觉头与 MTP 草稿上下文)。看到它变灰只说明你把这个开关关掉了 —— 那时恢复旧的互斥行为(上游 llama.cpp 需要这样)。想同时用 MTP 和看图就把它打开;你选的 mmprojFile 不会被清空(见第 4 节「多 Token 预测(MTP)」) |
| 开了 MTP,但生成速度没有任何变化 | 两条前提没同时满足:① 模型必须是带 MTP 头的 GGUF(文件名常带 MTP 字样),普通 GGUF 打开开关零效果且不报错;② llama.cpp 构建要支持 MTP(2026-05 之后的 PR #22673)。先看模型文件名,再看构建日期。另外草稿深度用默认值即可,调得过大反而更慢 |
| 加载失败,日志里出现 unknown argument: --spec-type / --spec-type: invalid value | 这个 llama.cpp 构建不支持 MTP(早于 2026-05),或该构建只认别的取值。升级 llama.cpp;插件在启动前会通过 --help 探测并打一条「不认识这个选项」的警告,看到警告就说明是这个原因 |
| 加载失败,日志里同时出现 MTP 与 mmproj 相关字样 | 说明命令行里同时下发了 --spec-type 和 --mmproj。0.7.0 起这是默认行为(kvmem 分支的 llama-kvmem-server 支持共存,实测两者同时加载没有问题);如果你换回了官方 llama.cpp,它会因此加载失败 —— 把「MTP 与视觉共存」(mtpWithVision)关掉即恢复互斥。另外顺手检查 设置 → 本地模型 → 附加参数 里有没有手写这两个参数(会和插件下发的重复) |
| 升级到 0.4.0 后生成风格变了(更保守 / 更容易重复 / 候选更单调) | 这是预期内的:新增的 8 个采样参数每次加载都会显式下发,并覆盖 llama.cpp 自身默认值 —— temp 0.8→0.75、top-k 40→20、min-p 0.05→0、repeat-penalty 1.1→1.0。想退回原样,去 设置 → 本地模型 → 采样与 KV 缓存 把这些值填成 llama.cpp 的默认值(见第 4 节的对照表) |
| 对话框里的「推理等级」拨哪个档位都没反应 | 先看 设置 → 本地模型 状态卡里的「推理档位」和日志里的「模型模板支持的推理档位」:解析不出档位表,插件就只会按开关控制思考与否,不碰档位。解析出来了还不行,就是模型模板没定义你选的那一档 —— 换个档位试试 |
| 对话直接报 500,错误里有 Unexpected reasoning effort xxx | 模型模板不认这个档位值。0.4.2 起插件会读模板、按它支持的档位重映射,正常不会再出现;若仍出现,把日志里「模型模板支持的推理档位」那一行发出来 |
| 选了 Off 但模型还是在思考 | 三种可能:① 用的是 0.4.1 及以前(会把 Off 顶回 true,或把 false 原样发给模板导致 500);② 模型模板不认 enable_thinking(0.4.2 起选 Off 会下发 {"enable_thinking": false},模板忽略它就仍会思考);③ dsh 侧的档位压根没传过来。后者可以用「推理 token 预算 = 0」强制掐断思考 |
| 升级后模型在 dsh 里「消失」或提示路由注册失败 | 新增的 compat / reasoningEfforts 声明若不被这个 dsh 版本接受,注册会失败 —— 插件不会因此影响本地端点(照旧降级为打印可粘贴的 YAML),把日志里的 llm.() 注册失败 那一行发出来即可 |
| 设置页里找不到「路由名 / 模型别名 / 模型 ID / 声明上下文 / 自动注册路由」了 | 0.4.0 按需求删掉了整组「接入 dsh」设置,它们变成了 src/configResolve.ts 里的常量(路由名 local-llama、id local,其余恒为真/假)。功能不变,只是不可配置。其中「声明上下文」现在直接跟随 ctxSize,见第 4 节「接入 dsh 那些设置去哪了」 |
| 日志说「这个 llama-server 不认识以下选项,已跳过:--kv-stream-stage-mib…」 | 这是预期行为,不是故障:这个构建没公开那一项,插件就不发过去(对未知选项直接退出的分支尤其如此)。日志里的名单可能同时包含两类:① 只有新构建才有的(--kv-unified / --kv-stream-stage-mib / --image--tokens / --reasoning-budget);② 这个分支干脆没有的(--alias、-ub、--repeat-last-n 之类)。想用哪项就升级到支持它的构建,加载本身不受影响 |
| 加载失败,日志里出现 block KV streaming requires exactly one sequence (-np 1) | KV 流式暂存要求单序列,而 llama-server 拿到的序列数不是 1。插件在开启暂存时已自动追加 -np 1,所以出现这条通常说明「附加参数」里有 -np / --parallel 覆盖了它 —— 把它删掉,或把「KV 主机内存暂存」设为 0。插件已内置这条诊断,会直接给出结论而不用你读英文日志 |
| local_model 工具的 start / stop 一直返回「不可用」 | 0.4.0 之后恒如此:控制开关 allowModelControl 已删除、固定为假。要恢复可控,需把 src/configResolve.ts 里的 ALLOW_MODEL_CONTROL 改成 true 并重新构建 |
| 设了 kvStreamStageMib 之后启动变慢 / 反而更容易 OOM | 这个值是「暂存到主机内存」的上限,取值取决于模型、上下文、显卡与其它显存占用,没有通用最优解。设得过大可能让主机内存吃紧。从保守值起步,逐步加大并观察启动情况与峰值显存;填 0 即完全不下发该参数 |
| 长会话中途崩 | contextWindow 比 ctxSize 大;把两者对齐并留余量 |
| 回复一卡一卡然后断开 | 设置页或路由里的 streamIdleTimeoutMs 太短(本地推理慢),参考第 5 节调到 600000 |
| 显存没释放 | 看 local_model 工具或 /local-model status 的状态;确认 idleUnloadMinutes 不是 0 |
| 改完设置不生效 | 端口/目录类改动需重启 dsh;模型/参数类改动被空闲卸载后或 /local-model reload 后会按新值生效 |
| 找不到「参数预设」那一块,或点了预设没反应 | 它在 设置 → 本地模型 页最上面(置顶那条)。一片预设都没有时只有输入框 —— 输入名字点「保存为预设」。升级后没出现先看状态卡里的版本号是否为 v0.5.2,再跑 npm run verify |
| 点预设提示「找不到这个预设,可能已被删除」 | 另一个标签页/窗口把这组预设删掉了。点「刷新状态」重新拉一次即可 |
| 预设里怎么没有端口 / 模型目录 / 密钥 | 设计如此,不是漏了:这 9 项属于「这台机器」而不属于「这套参数」,切换预设时保持原样(否则切一次参数就把端口换了)。作用域见第 4 节「参数预设」 |
| 应用预设之后模型被卸载并重新加载了一次 | 预期行为:它与「保存设置」走同一条路径 —— 参数变了就卸载,下次对话按新参数加载 |
| 设置页有元素叠在一起 / 输入框被「默认」按钮压住 | 0.5.1 已修(面板可用宽度只有 564px,旧的 maxWidth: 720 + 裸 1fr 三列在窄窗口下会换行重叠)。若升级后仍有,先确认状态卡里的版本号是 v0.5.2;再跑 node scripts/client-check.mjs,两套「布局」用例会直接指出是哪条样式约束被破坏了 |
| 预设条滚起来上边缘被切 / 盖住面板标题 | 同上,0.5.1 已修。相关的三条约束(不透明背景、横向盖满 24px 内边距、isolation: isolate)见 client/src/styles.js 的 cardPinned |
| 整块设置项是半透明的,下层卡片的文字透上来叠字 | 0.5.2 已修。根因是插件引用了宿主不存在的 --color- 变量,var() 静默落到 transparent 兜底(详见「颜色变量」一节)。先确认版本号是 v0.5.2;再跑 node scripts/client-check.mjs,「主题」三条用例会直接报出是哪个变量或哪个容器背景不对 |
| 深色主题下主按钮黑底黑字 / 输入框看不见底 | 同上,0.5.2 已修(--color-text-primary 落空到 #111、--color-background-secondary 落空到 transparent) |
| 预设建不出来(说名称重复 / 为空) | 名称不能为空、不能重复(不区分大小写),最长 40 字,最多 100 组。换个名字或先删掉旧的 |
| 面板提示「预设文件解析失败,已按「没有预设」处理」 | state/presets.json 被外部改坏了。插件不会因此起不来、模型加载也不受影响,只是那批预设读不回来;删掉该文件即可从头再来 |

9. 开发与测试

npm run build       # tsc → lib/,再打包客户端 bundle → client/client.js
npm run build:client # 只重打浏览器半侧(改 client/src 后用它)
npm run verify      # 清单自检:bundle 合法性 + 客户端半侧合规 + 在哪些 profile 装了却没生效
npm run verify:llama # 参数验收:拿本机的 llama-server 真起一次,确认它接受插件下发的参数
npm test            # 下面五套全跑(共 180 项:单测 118 + e2e 13 + 加载 32 + 客户端 17 + 清单 12 项检查)
npm run test:client # 浏览器 bundle:用假 loader 真加载一遍,校验格式、插槽注册与预设条契约
npm run test:load   # 类宿主跑 apply():目录骨架、代理端口、设置面板数据面、参数预设与安全约束
npm run test:unit   # 参数拼装 / 能力探测 / 分片归并 / 路径与配置解析 / 参数预设读写
npm run test:e2e    # 真进程跑完整加载与空闲卸载生命周期

参数验收(verify-llama.mjs)是排查「加载不起来」最快的入口:它按你的实际配置拼参数、
真的把 llama-server 起一次(模型换成一个不存在的哨兵路径,因此不会占显存),
从而把「参数被拒绝」和「模型/显存问题」这两类故障一次分开:

构建能力:识别到 417 个选项,flash-attn 形状为 value
将要下发的参数:… --flash-attn on --jinja
✓ 下发的选项这个构建全都认识
✓ 参数被接受(失败点已经走到读模型这一步,说明参数解析全部通过)

客户端校验(client-check.mjs)用假的 window.__ModuleLoader__ 与假的 react 把产物跑一遍:

✓ 产物能被 dsh 的模块加载器接收              ✓ 工厂 id 等于包名
✓ 外部依赖都走注入的 require(react 没被打进包) ✓ 导出 apply / inject,且没有 default 导出
✓ apply 注册了 id 为 local-model 的 settings.section ✓ 侧栏标题取词为「本地模型」
✓ 注册的一切都可逆(mounted / disposer)
✓ 预设:客户端调的 5 条路由宿主侧全都有(防两边路径悄悄错位)
✓ 预设:预设条排在状态卡之前(「固定在顶部」这条需求由测试钉住)
✓ 预设快照:草稿优先 / 空数字回落 / 字符串空值保留

加载验证(load-check.mjs)在临时目录里搭一个假 profile,让 @deepseek-ai/schemastery
指回本机真实的 schemastery(不是自造的桩),把编译产物拷进去加载,钉死 dsh 的硬约定并顺带
把设置面板的数据面与参数预设也验一遍:

✓ 导出符号齐全 / 没有默认导出 / schema 能产出默认值     ✓ 宿主服务全缺失时 apply() 仍能跑起来
✓ 目录骨架与放置说明被创建                              ✓ 代理端口可监听、状态端点可访问
✓ 表单分组 ≥5 组、字段 ≥30 且文案来自 schema             ✓ 保存落盘并即时生效;恢复默认清空用户层
✓ 预设:存 / 读 / 应用 / 改名 / 覆盖 / 删除全链路        ✓ 预设:环境字段被挡在预设之外(端口不受影响)
✓ 预设:重名与空值有可读报错,失败不留半条记录            ✓ 预设:重启后仍能读回同一个文件
✓ 拒绝非本机来源(403,含预设接口)                      ✓ 写操作要求 JSON content-type(415)
✓ 只接受 GET/POST(405);非法 JSON 有明确报错           ✓ 卸载时端口被释放

端到端测试用 scripts/fixtures/fake-llama-server.mjs 顶替 llama-server,但走的是真实的
spawn / 健康探测 / 进程树 kill 流程:

✓ 初始化后不拉起进程(不占显存)        ✓ 首条对话自动拉起 → 等就绪 → 原样转发
✓ 4 个并发请求只 spawn 1 次(单飞)      ✓ SSE 流式逐块透传
✓ 空闲后进程被回收、端口被释放           ✓ 卸载后能再次拉起(可反复)
✓ 未选模型 / 缺 llama-server 的报错可操作 ✓ 卸载插件时清理进程与端口

目录结构

package.json         dsh.bundle.patch → cordis.patch.yml;dsh.client → client/client.js
cordis.patch.yml     bundle patch:把插件行 insert 进 profile 的插件树
src/                 宿主侧(Node)
├── index.ts          插件入口:name / inject / Config / apply(只用具名导出)
├── config.ts         设置项的唯一真源(schemastery schema,50 个字段)
├── configResolve.ts  配置解析:路径占位符、区间收敛(无宿主依赖,可单独测)
├── configStore.ts    用户层配置读写 + 写入白名单/类型闸门
├── presets.ts        参数预设:作用域定义 + 落盘(独立于 schemastery,可单独测)
├── paths.ts          $DSH_HOME 与目录约定、首次初始化
├── schemaForm.ts     把 schema 序列化成设置面板的表单描述(含分组)
├── webBridge.ts      同源 HTTP 数据面(状态 / 配置 / 操作 / 预设),回环 + JSON 约束
├── registry.ts       模型扫描:GGUF 分片归并、量化/参数量识别、mmproj 关联与选择
├── requestRewrite.ts 请求体改写:思考开关(chat_template_kwargs + 历史 think 剥离)
├── lifecycle.ts      状态机:单飞加载、空闲卸载、崩溃自愈、热改配置
├── proxy.ts          常驻入口:首请求触发加载、SSE 透传、状态端点、按开关改写请求体
├── llmBridge.ts      接入 dsh llm 缝(探测 + 降级)
├── tools.ts          local_model 工具(status / list / start / stop)
└── commands.ts       /local-model 命令(status / list / start / stop / reload / route)
client/src/          浏览器半侧
├── index.jsx         注册 settings.section 一级页面 + 词条
├── section.jsx       面板:预设条、状态、模型/视觉投影下拉(MTP 开启时置灰)、按 schema 渲染的分组表单
├── presets.jsx       置顶的「参数预设」条:保存 / 应用 / 改名 / 覆盖 / 删除
├── presetSnapshot.js 预设快照的纯逻辑(可被 node 直接单测)
├── layoutCheck.js    布局几何 + 主题变量自检规则(防元素重叠 / 防宿主变量用错,被 client-check 调用)
├── api.js            同源 fetch
└── styles.js         内联样式(不引 CSS modules,少一个加载失败面)
scripts/
├── verify-bundle.mjs 清单自检(bundle + client + profile 挂载状态)
├── verify-llama.mjs  参数验收(拿真 llama-server 跑一次参数解析)
├── build-client.mjs  esbuild 打包 + 套 lazy-CJS factory 外壳
├── client-check.mjs  假 loader 加载校验 + 预设契约、快照逻辑与布局几何自检
├── load-check.mjs    类宿主环境加载验证
├── selftest.mjs      纯逻辑单元自检(含参数预设的读写与容错)
├── e2e.mjs           真进程端到端
└── fetch-llama.mjs   下载 llama.cpp 运行时

10. 已知限制与版本适配

dsh 目前处于开发者预览期,接口仍在变动,因此所有宿主集成点都做了显式降级,宁可少做也不报错:

- 本插件 inject 为空:llm / tools / commands / timer 全部通过 ctx.get() 读取,
缺失或接口对不上时只跳过对应增强,模型加载与空闲卸载这条主线始终可用。
- llmBridge 会依次探测 registerProvider / upsertProvider / registerRoute / addRoute / setRoute,
全都不匹配时打印可直接粘贴的 settings.yaml 片段(第 5 节),而不是失败。
- /local-model 命令同时挂了 execute 和 run 两种入口名,以适配不同版本的 commands 服务。
- src/commands.ts 与 /local-model status 的输出格式可能随宿主调整而调整 —— 它们只是便利功能,
真实状态以 GET http://127.0.0.1:18080/local-model/status 为准。
- types/dsh-ambient.d.ts 是离线编译用的环境声明(只覆盖本插件用到的接口)。
在 dsh 仓库内开发时以主仓库的真实类型为准;若出现重复声明告警,把它从 tsconfig.json
的 include 里去掉即可。
- 未做:多模型同时常驻(严格一次一个,符合"释放资源"的目标)、图像输入透传的端到端验证、
Windows 上 llama-server 的优雅退出(llama.cpp 无状态可刷,直接结束进程树)。

浏览器半侧是复刻的构建,不是官方预设

dsh 官方明确说明产出 dsh.client bundle 的 clientBundle 预设不在已发布的包里,
仓库之外的插件必须自己复刻。本项目的复刻落在 scripts/build-client.mjs:esbuild 打成 CJS
(react / react/jsx-runtime 全部 external,由宿主提供),再手工套上
window.__ModuleLoader__.load({ id, factory }) 外壳;形状是拿本机三个已经在生效的
dsh.client 包的产物逐字节对照出来的,并由 npm run test:client 用假 loader 真跑一遍守住。
升级 dsh 后如果设置页不出现,先跑这两个脚本,再检查 dsh.client.inject 里的客户端包名是否仍然存在。

面板的数据面是自建的同源 HTTP 桥而不是 dsh 的 settings 命名空间(原因见第 4 节),
因此设置写在插件自己的 /local-model/state/config.json 里。这是刻意的取舍:
它不依赖宿主内部接口的版本,代价是这些设置不出现在 settings.yaml 中,也就不能跟着
dsh 的凭据/同步机制走。桥只服务回环来源 —— 通过局域网访问 dsh 时面板不可用。

界面文案目前是中文硬编码(只额外注册了中英两份分区标题词条)。

本包的 manifest 是按实机对照确认的

dsh.bundle 与 dsh.client 的形状不是照文档猜的,是拿本机几个已经在生效的第三方插件
逐个字段对出来的(dshmarket、@linxin666/dsh-web-all、dsh-better-sidebar):

"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"client": { "inject": ["@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-settings"], "platform": "web" }
}

engines.dsh 是给市场/网站读的版本闸门(市场会把 engines.dsh 与 @deepseek-ai/ 的
peer 范围一起收进兼容性卡片)。写它有个坑:不带预发布分支的范围会静默匹配不到 harness
的 -rc.x 构建 —— node-semver 只在范围里某个比较符与该版本的 major.minor.patch
元组完全一致、且自身带预发布标签时才放行预发布版本。所以这里用显式的 || 分支:

"engines": { "dsh": "^0.1.0-rc.1 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0" }

harness 出一个新的 minor 预发布(例如 0.1.6-rc.1)时需要补一个分支,这是这类声明
固有的维护成本。peerDependencies 同一处理:官方 @deepseek-ai/ 一律写进
peerDependencies(而不是 dependencies),并全部标 optional。原来写的 "" 看着最宽,
实际受同一条 semver 规则影响匹配不到任何 -rc.x。

cordis.patch.yml 里的 loader entry id 带作者前缀(dzqjoker-local-model),不用裸的
local-model:cordis 拒绝加载含重复 entry id 的插件树,而插件市场一旦发现新装插件的 id
与已加载的相撞,会直接把该插件卸载(否则下次启动整个 profile 都起不来)。

exports 只声明了 . / ./client / ./cordis.patch.yml / ./package.json 四个入口,
与上述包的风格保持一致。

repository 字段必须指回本仓库 —— 插件市场用它把 npm 包与列表条目关联起来。
本包不再有 private 字段:它是要被公开安装的插件。

License

MIT

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入社群

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

DPharness QQ 群二维码,QQ 扫码进群
QQ 扫码进群
DPharness 飞书群二维码,飞书扫码进群
飞书扫码进群