dsh-llamacpp-bridge
把本地 llama.cpp(llama-server) 变成 DeepSeek Harness 的一等模型 Provider:进程与路由管理、模型目录同步、按需自动启动、先停旧再起新的模型切换,以及侧边栏「llama.cpp 终端输出监控」面板。
English | 中文
这是什么
DeepSeek Harness(DSH)默认使用云端模型。本插件让你把本机的 llama.cpp 接入进来:在 DSH 的模型选择器里直接选 llamacpp 分组下的本地 GGUF 模型,发消息时自动拉起 llama-server,并把视觉投影文件(mmproj)、上下文长度、GPU 层数等一并交代清楚。
插件是标准的 DSH 双包插件(host + client):
- host(Node ESM):注册
llamacpp模型路由、管理llama-server子进程、同步模型目录、提供同源 HTTP/SSE 数据面; - client(浏览器 bundle):侧边栏入口行 + 自持面板 + 设置页图形引导。
特性
| 能力 | 说明 |
|---|---|
| 模型路由 | 注册 llamacpp provider,与云端 provider 并存;只拥有自己的路由,不干扰其它 provider 的模型订阅 |
| 按需自动启动 | 调用前 ensure(model):服务器没跑就先跑起来,就绪后才走 OpenAI 兼容端口 |
| 先停旧再起新 | 切换模型时先把先前模型停下来,再启动目标模型(日志会写明这两步),避免显存里同时挂着两个模型 |
| 两种运行模式 | 单模型直启(-m <model>)与 router 模式(--models-dir + /models/load、/models/unload 动态装卸) |
| 目录同步 | 启动全量重扫 + 读取时按目录指纹按需重扫 + fs.watch + 兜底轮询;目录稍后才出现也能自愈监听 |
| mmproj 视觉配对 | 三级配对:手动绑定 → 后缀约定 <模型名>-mmproj.gguf → 前缀模糊配对 mmproj-*.gguf(按归一化名打分);配上就自动带 --mmproj |
| 终端输出监控面板 | 侧边栏入口行(与任务看板同级)→ 自持面板:实时 SSE 日志、流别标签、时间戳、关键字过滤、清屏、模型切换、刷新 |
| 优雅退出 | 面板按钮走真终端交互:Ctrl+C(SIGINT 到前台进程组)→ 发送 Y 确认 → 等退出;不直接强杀(详见下文) |
| 图形化引导 | 设置页自动探测 ~/llama.cpp/build/bin 等位置,列出候选可执行文件与模型目录,也可手动浏览选择 |
| 显示名截断 | 模型显示名超过 30 字符自动截断为前 30 字符 + ...(id 保持完整,不影响选择与调用) |
P0–P3 需求覆盖
| 项 | 状态 | 实现位置 |
|---|---|---|
| ① 后端模型切换(非启动脚本全量列表) | ✅ | model-store 扫描整个模型目录,与“启动脚本里的列表”解耦 |
| ② Web UI 侧边栏入口(Ollama 风格图标 + 模型切换 + 终端输出) | ✅ | DOM 注入入口行 → 自持面板:模型切换 + SSE 实时日志 |
| ③ 对话开始时自动启动 llama.cpp | ✅ | adapter.stream() → server.ensure()(真正连接端口前等待就绪) |
| ④ 切换模型前先卸载旧模型 | ✅ | ensure():先 stop() 旧模型并确认停止,再启动目标模型 |
| ⑤ 不挂钩其它模型订阅 | ✅ | 仅 llm.registerAdapter(['llamacpp'], …) |
| ⑥ 安装即生成模型配置 | ✅ | 自有设置命名空间 llamacpp-bridge + 发现命名空间(含模型/投影器配对情况) |
| P1 目录新增模型自动同步 | ✅ | 启动重扫 + 指纹按需重扫 + fs.watch + 兜底轮询 |
| P1 mmproj 与模型一同加载 | ✅ | 三级配对 + --mmproj 参数 |
| P2 依显存设置上下文长度 | ✅ | autoContext + pickContextLength 启发式 |
| P3 长上下文提示(会话内询问) | ⚠️ | 上下文长度可在设置页设定;“对话中询问”尚无官方交互缝,未实现 |
行为测试(模拟真实子进程/文件系统):模型切换顺序、目录增删即时反映、mmproj 配对与 --mmproj 参数、Ctrl+C → Y 退出流程、30 字符截断,均已逐项验证。
环境要求
| 项目 | 要求 |
|---|---|
| DSH | web profile。注意:0.1.7-rc.2 该版本本身有缺陷——DSH 不会加载插件的客户端 bundle(界面不可见),与本插件无关 |
| Node | ≥ 20 |
| llama.cpp | 提供 llama-server(router 模式需较新版本;单模型模式老版本亦可) |
| 平台 | macOS / Linux(Apple Silicon 已验证);Windows 未验证 |
安装
方式 A:从 Release 安装(推荐)
到 Releases 页面下载 dsh-llamacpp-bridge-<版本>.tgz,然后:
dsh plugin --profile web add ./dsh-llamacpp-bridge-1.5.0.tgz
方式 B:从源码构建
dist/已随仓库提供,只想用的话 clone 后npm pack即可,不必构建。
需要重新构建时(@deepseek-ai/* 类型包不在 npm registry 上,随 DSH 本体提供,由脚本链接):
git clone https://github.com/b8yg7vjstj-ctrl/dsh-llamacpp-bridge.git
cd dsh-llamacpp-bridge
npm install # 1) 先装 devDependencies
npm run link:dsh # 2) 再链接本机 DSH 的类型包(顺序不可颠倒:npm install 会清掉未声明的链接)
npm run check # 3) 类型检查 + 构建
npm pack # 4) 产出 dsh-llamacpp-bridge-<版本>.tgz
dsh plugin --profile web add ./dsh-llamacpp-bridge-*.tgz
脚本探测不到 DSH 位置时可手动指定:
DSH_INSTALL=/opt/homebrew/lib/node_modules/@deepseek-ai/dsh npm run link:dsh
安装后必做
- 重启 host:
dsh plugin add只改磁盘,运行中的 host 不会热加载新插件lsof -nP -iTCP:3080 -sTCP:LISTEN # 取 PID kill <PID> cd ~ && dsh web - 浏览器强制刷新:
Cmd/Ctrl + Shift + R(旧页面缓存会继续请求已被替换的 bundle,表现为bundle script ... failed to load)
dsh plugin add会自动维护~/.dsh/profiles/web/package.json的dsh.profile.bundles,无需手工添加。卸载:dsh plugin --profile web remove dsh-llamacpp-bridge。
快速开始
- 按上面步骤安装并重启,打开 DSH。
- 侧边栏「新会话」下方出现入口行 「llama.cpp 终端输出监控」(线条终端图标,紧跟任务看板之后)。
- 首次使用进入 设置 → llama.cpp:确认自动探测到的
llama-server可执行文件与模型目录(探测不到就手动选)。 - 在模型选择器里选
llamacpp分组下的本地模型并直接发消息——插件会自动启动服务器,就绪后开始推理。 - 点侧边栏入口行打开面板:看实时日志、切换模型、点「刷新」重扫目录、点「优雅退出」走 Ctrl+C → Y 流程。
模型切换语义(重要)
切换模型时不会在旧模型还在跑的时候直接调用 OpenAI 端口。实际顺序:
[manager] 切换模型:先停止先前模型 <旧模型>(pid <pid>)
[manager] 先前模型已停止,开始启动目标模型 <新模型>
…(服务器就绪)…
[manager] ready on http://127.0.0.1:8080 (single)
即 先停旧、再起新,然后才发起请求。router 模式下用 /models/unload + /models/load 完成等价流程。
配置
设置页(图形化引导)
设置 → llama.cpp 提供:
- 可执行文件候选(自动探测
~/llama.cpp/build/bin/release、build/bin、build、bin、~/llama.cpp及PATH),可手动选择或浏览; - 模型目录候选(含
.gguf计数),可手动选择或浏览; - ③ 视觉投影文件(mmproj):列出目录内所有投影文件及其配对结果,未配对的可在该行填入目标模型 id 完成绑定;
- 高级项:端口、运行策略、GPU 层数、上下文长度/自动推断、mmproj 后缀、附加参数、启动超时、调试日志。
设置命名空间 llamacpp-bridge
| 字段 | 默认 | 说明 |
|---|---|---|
displayName |
Local llama.cpp (bridge) |
provider 显示名 |
executable |
'' |
llama-server 路径;留空 = 自动探测 |
modelsDir |
'' |
模型目录;留空 = 自动探测 |
host / port |
127.0.0.1 / 8080 |
服务监听地址与端口 |
strategy |
auto |
auto / single / router |
contextLength |
— | -c 上下文长度 |
autoContext |
false |
按显存自动推断上下文长度 |
gpuLayers |
-1 |
-ngl,-1 = 交给 llama.cpp |
mmprojSuffix |
-mmproj.gguf |
后缀约定式投影文件名 |
mmprojOverrides |
{} |
手动绑定 { 模型id: 投影文件名 }(优先级最高) |
additionalArgs |
[] |
追加给 llama-server 的参数 |
startTimeoutMs |
120000 |
启动就绪超时 |
debug |
false |
打印实际执行参数等调试信息 |
发现命名空间 llamacpp-bridge-discovery(只读)
启动探测结果,供设置页展示:executables[]、modelsDirs[]、models[]、projectors[](含 file 与已配对的 modelId)。
侧边栏面板与数据面
入口行由 DOM 注入(锚定侧边栏「新会话」区域,MutationObserver 自愈),点击开关自持面板(createRoot 挂进会话列;激活时隐藏会话列其它子元素以避免重叠)。返回方式有三条:面板头部「← 返回会话」、点侧边栏任一身份行、按 Esc;与其它插件面板通过 dsh-panel-activate 事件互斥。
面板内容:插件作用回显 → 状态行(phase / 模型 / pid / 上下文)→ 模型切换(带 👁 视觉标记,名称超 30 字符截断)→「刷新」→ 工具栏(优雅退出、清屏、跟随、过滤)→ 终端输出(时间戳 + 流别标签 stdout / stderr / system / terminal)。
数据面(同源 HTTP;栅栏:回环套接字 + 回环 Host + 同源标记):
| 路由 | 方法 | 说明 |
|---|---|---|
/api/llamacpp-bridge/state |
GET | 快照:provider、插件作用、状态、模型清单、日志尾部 |
/api/llamacpp-bridge/events |
GET | SSE:snapshot 首帧 + log 增量 + status 差分 + 15s 心跳 |
/api/llamacpp-bridge/action |
POST | {"action":"graceful-stop"} → {ok, steps} |
为什么不用 cordis 事件把 host 日志推给客户端:host→client 的
remote-event帧是白名单制(由dsh-api-remotes拥有),自定义事件发不出去。因此改用官方webServer.register的 SSE 通道。
优雅退出(Ctrl+C → Y)
面板上的「优雅退出(Ctrl+C → Y)」不是强杀:
- 启动形态:优先
ctx.subprocess.spawnTerminal分配真实 PTY(日志:已分配终端(支持 Ctrl+C / Y 交互));终端不可用时回退管道模式(stdin: 'pipe')。 - Ctrl+C:终端模式
signalForeground('SIGINT')——把 SIGINT 送到前台进程组,与在终端按 Ctrl+C 等价;管道模式process.kill(pid, 'SIGINT')(仍是 SIGINT,不是 SIGKILL)。 - Y 确认:等 800ms 让服务器打印确认提示,再写
Y\n(终端write()/ 管道 stdin)。 - 升级顺序:SIGINT → Y → 等 8s → 再补一次 SIGINT → 等 5s → 最后才回退
terminate()(SIGTERM → grace → SIGKILL)。全程不直接强杀。 - 服务器打印的确认提示会原样进入面板日志(例如
Press Y to confirm exit)。
实测调用序列:spawnTerminal → signalForeground:SIGINT → write:"Y\n",步骤回显 ["已发送 Ctrl+C","已发送 Y 确认","确认后已退出"]。
架构
| 文件 | 职责 |
|---|---|
src/host/index.ts |
插件入口:环境探测、设置注册、目录仓库、服务器管理器、provider 注册、路由注册 |
src/host/adapter.ts |
llamacpp 路由的 LlmAdapter:调用前 ensure()、DSH ↔ OpenAI SSE 双向翻译 |
src/host/llama-server.ts |
llama-server 进程管理:终端/管道双模式、单模型与 router、就绪探测、优雅退出 |
src/host/model-store.ts |
目录扫描、mmproj 三级配对、指纹按需重扫、watcher 自愈 |
src/host/discover.ts |
可执行文件/模型目录探测,投影文件配对结果上报 |
src/host/terminal-routes.ts |
同源 HTTP/SSE 数据面(state / events / action) |
src/host/log-hub.ts |
环形日志总线(序号、时间戳、流别、订阅、增量读取) |
src/host/config.ts |
配置默认值、设置 schema、发现 schema |
src/client/index.tsx |
客户端入口:服务注入、设置页注册、挂载入口行与面板 |
src/client/terminal-mount.ts |
DOM 注入入口行 + 自持面板(开合、返回路径、互斥、自愈) |
src/client/terminal-panel.tsx |
面板 UI:状态、模型切换、日志流、优雅退出、过滤 |
src/client/setup.tsx |
设置页:探测候选、目录浏览、mmproj 绑定、高级项 |
src/client/ollama-icon.ts |
线条终端图标(内联 SVG) |
scripts/wrap-client.mjs |
把 esbuild 的 CJS 产物包成 window.__ModuleLoader__.load({...}) |
scripts/link-dsh-types.mjs |
从本机 DSH 安装链接类型包,供源码构建 |
关键实现说明(踩坑记录)
以下都是实际踩过的坑,改代码前请先读:
- cordis 服务注入的两个相反陷阱
- 访问未在
inject声明的服务 → 抛cannot get property "X" without inject; - 把当前作用域不可用的服务写进导出的
inject→ fiber 永久等待,插件静默不加载(没有任何报错)。 - 正确做法:核心服务放导出的
inject,其余用ctx.inject([...], cb)局部等待。
- 访问未在
list类座位必须带id(sidebar.footer.action/settings.section/conversation.view),否则报list slot ... requires options.id。- 不要再回退到「DOM 覆盖层 + CSS 隐藏原生会话」方案:会造成控件重叠与「切不回会话」。
- 原生
conversation.view标签不适合当入口:会话壳只在「已打开会话且标签数 > 1」时渲染标签,在首页/hero 点击会找不到目标(表现为「点不动」)。故改为入口行自持面板。 ctx.sessions是 Service,快照在ctx.sessions.list(传 Service 会getSnapshot is not a function,座位被错误边界替换成崩溃提示)。- 客户端 bundle 契约:必须
window.__ModuleLoader__.load({ id: '<包名>', factory });react 是 external,classic JSX 需import * as React(默认导入编译成require('react').default= undefined)。 - esbuild 参数:用
--jsx=transform(--jsx=classic在 0.24 报错),且必须--external:react-dom/client。 - 类型跨版本:插件只对 DSH 实际用到的接口做最小结构类型(消息块、设置作用域、子进程句柄 pid 等),避免 DSH 版本演进导致编译中断。
- pnpm 构建门禁:
ERR_PNPM_IGNORED_BUILDS(fsevents/sharp 等)会阻断任何dsh plugin add,需在 profile 的pnpm-workspace.yaml里用allowBuilds放行。 - DSH 单实例:再起一个
dsh web会报task-board ledger is already owned by process <pid>。
兼容性
- 环境:DSH
webprofile + macOS(Apple Silicon)+llama.cpp的llama-server(单模型与 router 两种模式)。 - 已知 DSH 侧问题:
0.1.7-rc.2下 DSH 不加载插件的客户端 bundle(/plugins/<包名>/client.js返回 404,宿主日志无插件侧报错),因此界面不可见;同一个包里 host 半包工作正常(路由可用)。请使用无此缺陷的 DSH 版本。 - 类型层:对 DSH 接口采用最小结构类型,可在 DSH 类型修订之间继续编译;类型包由
npm run link:dsh从本机安装链接。 - 运行期:host 入口必须能在 DSH profile 内解析(用
dsh plugin add安装即可满足)。 - Windows:未验证;终端原语与信号语义不同(
gracefulStop会自动回退管道模式)。
故障排查
| 现象 | 处理 |
|---|---|
| 0.1.7-rc.2 上完全看不到入口/设置页 | 该版本 DSH 未加载客户端 bundle(/plugins/dsh-llamacpp-bridge/client.js 为 404),属 DSH 侧问题,非本插件报错;换用可用版本 |
| 侧边栏没有入口行 | 确认插件已安装(dsh plugin add 已自动写入 bundles)→ 重启 host → 浏览器 Cmd/Ctrl+Shift+R |
bundle script /plugins/... failed to load |
旧页面缓存:强制刷新,或 Cmd+Q 整退出后重开 |
| 入口行点了没反应 | 打开 F12 Console,找 [llamacpp-bridge] 或 slot entry crashed 开头的行 |
| 模型列表不随目录变化 | 面板点「刷新」;host 每次启动都会重扫(日志 scanned N model(s) ... (startup rescan)),运行中靠 fs.watch + 30s 兜底轮询 |
| 视觉模型不生效 | 看 host 日志的 vision: <模型> ← <投影文件> 与 unmatched mmproj: ...;未配对的到 设置 → llama.cpp →「③ 视觉投影文件」手动绑定 |
| 启动即失败 | 日志会打印实际执行参数与错误;确认 executable / modelsDir 正确(设置页可浏览选择) |
| 端口被占用 | 改 port,或先停掉占用 8080 的进程 |
ERR_PNPM_IGNORED_BUILDS |
在 ~/.dsh/profiles/web/pnpm-workspace.yaml 添加 allowBuilds: { fsevents: true, sharp: true } |
ledger is already owned by process ... |
DSH 单实例限制:先关掉另一个 dsh web |
开发
npm install # devDependencies
npm run link:dsh # 链接本机 DSH 类型包(必须在 npm install 之后)
npm run typecheck # host + client 类型检查
npm run build # 构建 dist/(host 走 tsc,client 走 esbuild + 包装脚本)
npm run check # typecheck + build
npm pack # 产出可安装的 tgz
目录结构:
src/
shared.ts 共享常量与工具(provider id、路由前缀、名称截断等)
host/ Node ESM 侧(provider、进程、目录、HTTP/SSE)
client/ 浏览器 bundle 侧(入口行、面板、设置页)
scripts/
wrap-client.mjs esbuild 产物 → __ModuleLoader__ 包装
link-dsh-types.mjs 从本机 DSH 安装链接类型包
dist/ 构建产物(仓库内已包含,便于直接安装)
许可证
MIT © 2026 b8yg7vjstj-ctrl
No comments yet. Be the first to write one.