dsh-local-trim
IdentifiedOne-step local model onboarding plus a light harness for DSH. On load it probes loopback and private-network inference endpoints (llama.cpp, Ollama, vLLM, LM Studio), reads each model's context window
dsh-local-trim
给本地小模型一套轻量 harness:让 llama.cpp / Ollama / vLLM / LM Studio 这类本地端点也能在 DeepSeek Harness 里顺畅干活,而云端 provider 一个字节都不动。
它做什么:把工具面、系统提示词、工具 schema 三个面同时收窄,并把注入的整份系统提示词
换成一页纸。本地模型工具调用能力实测未退化(ops_bash / ops_glob / ops_read_files
两步串行都正确)。
这里不给具体的请求体数值。绝对值由你自己的 harness 决定 —— 装了多少插件、注册了多少工具、 系统提示词有哪几段,每个人的基线不同,裁剪空间也就不同。想知道自己省了多少, 用文末「验证裁剪真的生效了」那段,在装与不装之间各抓一次请求来对比。
为什么需要它
DSH 默认面向云端大模型:一个装满插件的 profile 打开就是几十上百项工具,加一整份长篇系统
提示词。本地模型常见窗口是 16K–64K,装得多一点就 CONTEXT_WINDOW_EXCEEDED;就算窗口够,
小模型被上百个工具描述和几万字规则围着,调用正确率也会掉。
具体多大取决于你装了什么 —— 这正是下面要做按 provider 裁剪的原因。
DSH 没有「按模型裁工具」的开关:
dsh-tools的 Config 只有mode/maxParallel;- agent-preset 只能声明插件,不列 ≠ 不注册;
dsh-purge也没有工具过滤字段。
唯一原语是 tools.restrict({ allow }),它把限制追加到调用者所在 scope 的层,只影响该 agent
及其子 agent —— 正好满足「只给本地模型精简」。本插件就是把这个原语和三个提示词层面的
裁剪组合起来。
四层裁剪
| 层 | 做什么 | 手段 |
|---|---|---|
| 1. 工具面 | 只留本地真用得上的工具 | tools.restrict({ allow }),allow = 实际可见名 ∩ 白名单 |
| 2. 提示词面 | 丢掉本地用不到的大段 | system-prompt/assemble 里按段名过滤 sections / contexts |
| 3. 注入面 | 整份系统提示词换成一页纸 | 替换 dsh-purge 注入段正文,保留头 80 / 尾 96 字符 |
| 4. schema 面 | 截断工具描述 | 递归截 description,type / properties / required 一字不动 |
第 3 层有个关键细节:宿主 dsh-agent-loop 在渲染完 prompt 后会检查
globalThis.__dshPurgeHungPrompt() 返回的原文的前 80 字符和后 96 字符是否都还在渲染结果里;
只要缺一个,就把整份原文再前置一遍。所以替换注入时必须原样保留这两段取样,否则省下的
token 会被立刻加回去。
安装
方式一:dsh plugin add(推荐,v0.1.1 起)
包已声明 dsh.bundle,装完自动生效,默认开启自动识别(routes: []):
dsh plugin add github:Leonlee114514/dsh-local-trim
不想要自动识别就改自己 profile 的 cordis.patch.yml,把 local-tool-trim 的 routes 写死:
- id: local-tool-trim
config:
routes: [my-local-provider] # 只裁这一个 provider
方式二:本地源码 + link:
适合改代码调试。
# 1) 进你的 DSH profile 目录
cd ~/.dsh/profiles/desktop # Windows: %USERPROFILE%\.dsh\profiles\desktop
# 2) 作为本地依赖登记(用 link: 指向源码目录,改完即时生效)
# 路径必须是相对 profile 目录的正确层数,写错会得到一个指向空目录的 junction
pnpm add link:../../../path/to/dsh-local-trim
# 3) 在 cordis.patch.yml 末尾 insert 插件(见下方配置)
踩过的坑:手工把插件拷进
node_modules/而不写进package.json,任何一次pnpm install都会把它当无主残留隔离删除(日志在.dsh-market/log.ndjson)。一定要用pnpm add link:...或写进dependencies。
一步到位(v0.2.0 起)
不想手写 llm-pi-ai 的 baseURL / 窗口 / 模型列表:插件启动时会自己探测本地端点,
探到能连上的就把 provider 写进你的 cordis.patch.yml(持久化,重启还在),
新 provider 立刻可选用,不需要重启 DSH。
- insert:
- id: local-tool-trim
name: dsh-local-trim
inject: [ tools, systemPrompt ]
config:
provision: true # 或写成对象,见下
provision 三态:
| 写法 | 行为 |
|---|---|
false | 完全不探测,插件只做裁剪 |
true(或不写) | 按顺序探 8090(llama.cpp) → 11434(Ollama) → 8000(vLLM) → 1234(LM Studio),命中第一个可用的 |
{ endpoint: 'http://host:port/v1' } | 只探这一个地址 |
{ endpoints: [...], provider: 'my-local', overwrite: true } | 自定义列表 / 名字 / 是否覆盖同名配置 |
行为约定:
-
已有可用的本地 provider 就完全不写入 —— 你手写的
local-qwen不会被覆盖。 -
provider id 按端口推断:
8090 → local-llamacpp、11434 → local-ollama、8000 → local-vllm、1234 → local-lmstudio;可用provider:指定。 -
窗口 / 输出长度从后端的
/v1/models的meta.n_ctx、meta.n_predict读; 读不到就退回保守值。 -
自动生成的 profile 带三样东西,少一样都可能拿不到回答:
compat.thinkingFormat: 'chat-template'+compat.chatTemplateKwargs.enable_thinking: false—— 本地 chat template 默认开思考模式,max_tokens会全花在reasoning_content上、content返回空串。但 pi-ai 只在thinkingFormat是chat-template一类的分支里 才把chatTemplateKwargs翻成请求体的chat_template_kwargs;默认的openai分支不翻,写了也发不出去。(qwen分支发的是顶层enable_thinking,llama.cpp 不认。)- 模型上的
reasoningEfforts: { off: null, low: low }—— 上面那个分支的前提 是model.reasoning === true,而它由这个字段推出来。 apiKeyEnv—— 本地端点不校验 key,但 provider 没有 credential 声明时 pi-ai 不带Authorization头,llama.cpp 会静默返回空 completion (不是 401,非常难查)。默认指向DSH_LOCAL_LLM_API_KEY,值为空也行。
不需要关思考就在
provision里写chatTemplateKwargs: null。 -
探测与写入失败都不会影响裁剪,把原因写进日志(默认
~/.dsh/local-llm/local-trim.log,可用DSH_LOCAL_TRIM_LOG改位置)。
插件热重载期间写配置会被 HMR 事务拒绝(
HMR transactions cannot be nested)。 这时插件会记下待办,等下一个真实请求(agent/created)再写 —— 不需要你做什么。 冷启动(装完插件重启 DSH)时第一次就成功。
最小配置
- insert:
- id: local-tool-trim
name: dsh-local-trim
inject: [ tools, systemPrompt ]
config:
enabled: true
routes: [] # 空数组 = 自动识别本地端点
routes 三态:
| 写法 | 含义 |
|---|---|
| 不写 | 早期默认:只认 local-qwen 这一个名字(建议显式写 [] 或写具体名字) |
[] | 自动识别:provider 的 baseURL 指向回环/私有网段,或 provider 名带 local/ollama/vllm/lmstudio 等标记 |
[my-provider, ollama] | 只认写出来的名字(精确匹配,my-provider-turbo 不会命中) |
自动识别的 baseURL 特征:127.0.0.1 / localhost / 0.0.0.0 / [::1] /
host.docker.internal / 10.x / 192.168.x / 172.16–31.x。
四种本地后端的接入
先把模型挂进 DSH 的 llm-pi-ai provider,本插件负责裁剪。四种后端都走 OpenAI 兼容接口,
差别只在 baseURL 和要不要 API key。
llama.cpp(llama-server)
llama-server -m model.gguf --mmproj mmproj.gguf \
--host 127.0.0.1 --port 8090 --alias my-local \
--jinja -c 65536 --n-predict 8192 \
--cache-type-k q8_0 --cache-type-v q8_0
# cordis.patch.yml
- id: llm-pi-ai
config:
providers:
my-local: # provider id 自己定,带 local 标记会被自动识别
displayName: 本地 llama.cpp
apiKeyEnv: DSH_LOCAL_LLM_API_KEY # 见上文第 3 点:缺了会静默返回空串
api: openai-completions
baseURL: http://127.0.0.1:8090/v1
defaultContextWindow: 65536 # 必须和 -c 一致
defaultMaxTokens: 8192 # 必须和 --n-predict 一致
timeoutMs: 1800000
streamIdleTimeoutMs: 600000
compat:
supportsUsageInStreaming: true
maxTokensField: max_tokens
thinkingFormat: chat-template # 见上文第 1 点:不写这一行,下面白写
chatTemplateKwargs:
enable_thinking: false
models:
- id: my-local
reasoningEfforts: { off: null, low: low } # 见上文第 2 点
contextWindow: 65536
maxTokens: 8192
input: [ text ]
reasoningEfforts: false
defaultContextWindow 只在 provider 重新解析时生效 —— 改完必须重启 DSH 实例,
HMR 不覆盖 llm-pi-ai。
Ollama
ollama serve # 默认 127.0.0.1:11434
ollama pull qwen2.5-coder:7b
providers:
ollama:
displayName: Ollama
api: openai-completions
baseURL: http://localhost:11434/v1
apiKeyEnv: OLLAMA_API_KEY # 随便填个值,Ollama 不校验
defaultContextWindow: 32768
defaultMaxTokens: 4096
Ollama 的 num_ctx 由模型 Modelfile 决定,得和这里的 defaultContextWindow 对齐;
ollama show <model> 能看到实际值。provider 名带 ollama 前缀,自动识别直接命中。
vLLM
vllm serve Qwen/Qwen2.5-Coder-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--max-model-len 32768
providers:
vllm:
displayName: vLLM
api: openai-completions
baseURL: http://192.168.1.9:8000/v1 # 局域网机器也自动识别(192.168.x)
apiKeyEnv: VLLM_API_KEY
defaultContextWindow: 32768
defaultMaxTokens: 4096
compat:
maxTokensField: max_tokens
LM Studio
GUI 里打开 Developer → Start Server(默认 127.0.0.1:1234),Context Length 在
模型加载设置里改。
providers:
lmstudio:
displayName: LM Studio
api: openai-completions
baseURL: http://127.0.0.1:1234/v1
apiKeyEnv: LMSTUDIO_API_KEY
defaultContextWindow: 16384
defaultMaxTokens: 4096
完整配置
- insert:
- id: local-tool-trim
name: dsh-local-trim
inject: [ tools, systemPrompt ]
config:
enabled: true
routes: []
promptTrim: true
provision: true # 探测本地端点并自动写 provider;false 关闭
# 本地模型用不到的段。名字取自各插件的 systemPrompt.section({ name })。
# 这份清单对应的是「装了很多插件」的 profile:你装了哪几个插件,就删哪几个插件
# 产出的段,别整套照抄。名字对不上的段会被静默忽略,不会报错。
dropSections:
- tool:mindmap
- free-search:engines
- dsh-purge:anchor
- dshpg-capabilities
- mnemon:routing
- plugin:dsh-imagegen
- ui:deliverable-file-references
- app:web-surface
- tool:goal
- tool:ralph
- tool:workflow
- tool:session-query
- tool:pty
- dsh-excel-chat:interaction
- dsh-ops:repository-tooling:mode-standard
- dsh-ops:repository-tooling:mode-ptc
- dsh-ops:repository-tooling:mode-cordis
- harness:source
dropContexts:
- mnemon:memory-view
- mnemon:runtime-memory
# 白名单同时写规范名和 dsh-ops 的 ops_* 名:不同 profile 装的工具包不一样,
# 有的注册 bash/read/glob/grep,有的注册 ops_bash/ops_read_files/…,
# 两种名字都列上,这份配置就能同时用在两种 profile 上。
keep:
- bash
- pwsh
- ops_bash
- read
- ops_read_files
- ops_inspect_local_file
- write
- edit
- glob
- ops_glob
- grep
- ops_grep
- working_directory
- job_output
- job_list
- job_kill
- todo_write
- present
- read_image
# 描述截断长度。再往下会把 "Omit or leave empty when using" 这类半句留下,
# 反而降低调用正确率 —— 半句话比长话更容易误导模型调用。
# 设 0 表示该面不截断(保留原文)。
toolDescLimit: 140
toolParamDescLimit: 90
换成自己的轻量注入
三层优先级:config.injectText > ~/.dsh/local-llm/light-inject.md > 内置默认。
写个文件就行:
mkdir -p ~/.dsh/local-llm
cat > ~/.dsh/local-llm/light-inject.md <<'EOF'
[SYSTEM]
你是本地运行的编码助手。工具集已裁剪,上下文有限。
- 直接执行,不要反问,缺参数用占位符
- 中文回复,代码原文
EOF
内置默认(DEFAULT_INJECT)是一页纸的版本:身份 + 5 条行为准则 + 工具速查 + 输出格式。
正文多长不影响正确性,只影响省下的量 —— 想更省就写更短,但别把工具用法删到模型认不出。
云端隔离
三重保证,test/autodetect.mjs 和 test/selfcheck.mjs 都覆盖了:
- 工具面:未命中路由时
release(agent)撤销该 agent 的全部限制(restrict 调用次数: 0)。 - 提示词面:
trimAssembly未命中时直接return assembly—— 同一个对象引用, 不新建、不原地改。 - 注入面:接管后的
assembleWithLocalTrim里 provider 判定不命中就直接返回下游结果。 - 唯一的全局副作用是重新定义了
prompt.assemble的访问器,但它的行为对非本地 provider 完全等价于原函数。
实测:同一个会话里交替切云端 provider 和本地 provider,云端返回正常、本地工具调用正常, 两边的工具面互不影响。
控制面板(v0.3.0 起,v0.4.0 改版)
插件在 DSH Web 的插件详情页里挂了一块面板:左侧栏进「插件」,列表里点开
dsh-local-trim,详情页底部就是它。面板直接读写这份 profile 的
cordis.patch.yml 里 local-tool-trim.config 那一节,改完点保存即生效,不用重启。
v0.4.0 起版面按「先看状态,再动手」重排,从上到下五块:
① 状态带 —— 一行徽章,每枚带颜色灯:
| 徽章 | 含义 |
|---|---|
| 接管 prompt.assemble | 生效方式(叠在 dsh-purge 外层 / 叠在既有访问器外层 / 接管原生),失败时直接把原因写在这里 |
| 注入正文 | 一页纸注入有多少字符、来源是 config / light-inject.md / 内置 |
| 一步到位 | 自动注册 provider 的结果 |
| 提示词裁剪 / schema 瘦身 | 两层各自的开关状态 |
| 设置可写 | 宿主有没有把本插件登记成可写设置条目 |
② 裁剪收益条 —— 最近一次装配的进→出字符数、省了百分之几、段数变化、命中的 provider 和时间。走云端 provider 时这里会显示「未裁剪」,那是正常的:云端一个字节都不动。
③ 模型 provider 表 —— 列出 settings 里所有 provider,本地的一个个点灯
(GET /v1/models,15 秒内复用结果,多路并发不串行),显示在线耗时或错误原文;每个本地
provider 后面有 试跑 按钮,发一条「只回一个字:好」的真实请求,回来显示毫秒数与回复内容。
拉起 按钮会在这台机器上找后端并起一个脱离宿主的进程;如果那个端口上确实有本插件
拉起的进程,还会多一个 停止 按钮(SIGTERM → 赖着不走再 SIGKILL)。
别人的 ollama serve 不会被这里点掉 —— 服务端只肯停自己 spawn 过、且登记在内存表里的 pid。
④ 本机模型 · 一键配置(v0.3.9 起,v0.4.0 加了资源预检)—— 上一块要求你先有端点,可刚下完模型的 时候服务还不存在。这一块把顺序倒过来:扫描本机 → 选一个 gguf → 一键配置并试跑。
点「扫描本机模型」后插件会去这几个地方找 .gguf(每层最多下探两级,跳过
.mmproj. 视觉投影,按文件大小降序,大的排前面):
~/.workbuddy/local-models/files ← WorkBuddy 下载目录,命中率最高
~/AI/models
~/models
~/.lmstudio/models
%LOCALAPPDATA%/LM-Studio/models
~/Downloads
选中一个模型后填端口(默认 8090)和上下文(默认 65536),点一键配置并试跑, 一条链走完三件事:
- 先做资源预检:拿模型文件体积比可用内存(
max(freemem, totalmem − 2GB))。- 比整机内存还大,或占了可用预算 80% 以上 → 判
risky,不替你起,直接回一句 「模型 15.8GB 占了可用预算的 110%,首次加载会让机器明显变卡(可用 14.4GB)」, 要你在面板上勾「我知道会很卡,还是要起」才继续; - 占 50%–80% → 判
warn,照起但提示; - 拿不到体积 →
unknown,不假装通过也不假装危险。 判risky时自动改用--no-mmap(分页读盘,慢但不吃满内存)。
- 比整机内存还大,或占了可用预算 80% 以上 → 判
- 用这个 gguf 起
llama-server(自动找~/.workbuddy/local-models/bin/下最新的 build, 参数带--n-gpu-layers auto --fit on --jinja --cache-type-k/v q8_0);端口上已经有服务 在应答就跳过拉起,不重复起; - 等
GET /v1/models真应答,以它报的模型 id 为准(不是文件名); - 复用「导入」的注册逻辑写进
llm-pi-ai,然后发一条真实请求试跑。
失败时面板会说卡在哪一步:precheck(资源预检拦住)/ launch(起不来,带查过的路径)/
probe(起来了但没报模型)/ register(写 settings 被拒)。拉起后端口超时不应答的话,
插件会把刚起的进程回收掉,不留孤儿。别自己猜。
首次加载 17GB 权重需要十几秒到几十秒,这期间机器会明显变卡 —— 那是权重在往显存里搬, 不是插件在空转。显存不够时 llama.cpp 会回落到 CPU,速度会掉一个数量级。 这台机器上真发生过一次:17GB / 27.2B MoE 把桌面直接拖到卡死,只能强制重启。 所以
risky那道确认不是形式主义。
⑤ 导入本地模型 —— 填端点(例如 http://127.0.0.1:8090,/v1 可省)→ 探测 →
下拉里选一个模型 → 导入并试跑。导入做四件事:
GET /v1/models(不通用时退/api/tags)拿到模型列表;- 按端口猜后端名,生成 provider id(
local-llamacpp-qwen3-4b这种),也可以自己填; - 写进
llm-pi-ai的providers,模型选好后在「模型」选择器里立刻能选到; - 立刻发一条真实请求,把端到端耗时、回复内容、token 数(或后端原文错误)贴在面板上。
试跑失败不会回收配置 —— 配置已经落地,把后端拉起或改好端点后点「试跑」即可,不用重新导入。
默认会关掉 chat template 的思考模式(enable_thinking: false):本地小模型开了思考会把
max_tokens 全花在 reasoning 上、content 直接空字符串。需要思考的模型勾上「保留思考模式」。
原来那些字段(开关、阈值、三个名单、注入正文、一步到位)都在最下面折叠的 高级配置 里,位置变了,能力没变。
⑥ 一键拉起(v0.3.4 起,v0.3.7 会自己捡模型,v0.4.0 能停) —— provider 表里每个本地 provider 后面都多一个 拉起 按钮。点它就按这个 provider 的端口去找后端并起起来,然后轮询等它应答:
| 端口 | 后端 | 怎么起 |
|---|---|---|
| 11434 | Ollama | 找到 ollama(.exe) 后 serve,并带上 OLLAMA_HOST=127.0.0.1:11434 |
| 8090 | llama.cpp | 先找 llama-server.exe,再在 WorkBuddy 的模型目录里捡现成的 gguf,参数自己拼 |
| 8000 | vLLM | 需要模型参数,插件不猜 —— 在高级配置里填完整命令 |
| 1234 | LM Studio | 同上(GUI 应用,通常自己开着) |
llama.cpp 这一条是 v0.3.7 的重点:模型已经下好了就别让人再下一次 20GB。插件按这个顺序找
llama-server.exe:~/.workbuddy/local-models/bin/<build>/(取最新 build)→LLAMA_SERVER_BIN→~/AI/llama.cpp/→ PATH;- gguf:
~/.workbuddy/local-models/files/*.gguf,优先local_model.gguf,跳过.mmproj.(那是视觉投影); - 拼参数:
-m <gguf> --host 127.0.0.1 --port 8090 --alias <provider 的模型 id> --n-gpu-layers auto --fit on --parallel 1 --jinja --cache-type-k q8_0 --cache-type-v q8_0 -c 65536 --n-predict 8192。
这套参数是按 32GB 统一内存实测出来的:权重 17GB 上卡,q8_0 KV 单 slot 才压得住 64K 上下文。 两个组件都找到才自动拼;缺模型文件才回「需要模型文件才能起」,缺可执行文件才回「本机没找到」, 错误里带着查过的路径。
顺序是:先探测,端口本来就在应答就直接告诉你「没有重复拉起」;没应答才找可执行文件
(OLLAMA_BIN 环境变量 → ~/AI/ollama/ollama.exe → %LOCALAPPDATA%\Programs\Ollama\ollama.exe
→ C:\Program Files\Ollama\ollama.exe → PATH),拉起后按 拉起后等端口(秒) 轮询
/v1/models,成功后顺手把点灯缓存清掉。
高级配置里的 一键拉起命令 留空就走上面的自动逻辑;填了(例如
C:\Users\you\AI\ollama\ollama.exe serve --verbose)就原样执行,优先于自动探测。
起进程用 detached + stdio:ignore,不挂在宿主进程的管道上,所以关掉 DSH 不会把它带走,
要开机自启得自己挂(Windows 计划任务或启动项)。
停止(v0.4.0 起):进程起来了就得能关。provider 那一行如果在「本插件起过的端口」表里,
就会多一个红色 停止 按钮,点它走 POST /api/dsh-local-trim/stop → SIGTERM,
1.5 秒内不退再补 SIGKILL,还活着就如实报失败(不谎报成功)。
这张表只存在宿主内存里:重启 DSH 之后表就空了,此时那些进程要自己关
(taskkill /PID <pid> /F,pid 在面板「已拉起」提示和启动日志里都有)。
拉起后等端口的上限是 拉起后等端口(秒),默认 20 秒,可设范围 3–600 秒 —— 超出会被夹回去。
17GB 权重冷加载实测 11.6 秒,接近默认上限;更慢的机器记得调大。
超时不会留孤儿:插件会把刚 spawn 的那个 pid 回收掉再报错。
其他按钮:探测本地端点(依次打 8090 / 11434 / 8000 / 1234)、看日志
(读 ~/.dsh/local-llm/local-trim.log 尾部)、重新检测(强制刷新点灯结果)。
右上角显示当前 revision;这份配置在别处被改过时,保存会提示冲突并自动重新读取。
宿主里跑的还是旧代码时(只有 describe / save / probe / log 四条路由,没有 status),
面板不会整块报错:它自动退回 describe 视图,并在顶部提示「重启一次 DSH 就能看到点灯、
收益条和模型导入」。插件是 JS 模块,装在宿主进程里,改完源码要让宿主重新 apply 一次。
面板走的是插件自建的 HTTP bridge,前缀 /api/dsh-local-trim,十二个端点
describe / save / probe / log / status / check / import / test / launch /
stop / models / adopt,全部只接受 POST,且只放行本机来源
(sec-fetch-site: cross-site 直接 403,Origin 必须与 Host 同源)。
重活路由(除 describe / save / log 之外)带两层节流:同一个路由上一次还没跑完时
直接回「等它结束再来一次」,跑完之后 800ms 内再来也被挡(bridgeThrottleMs 可调,设 0 关掉
间隔限制、只留并发去重)。前端连点不会在宿主机上堆叠起一堆 llama-server。
保存报「被 home patch 覆盖」怎么办
如果这个插件的配置整段写在 profile 的 cordis.patch.yml 里(很多人是手写进去的),
settings.update 会要求「合成后的 config 与要写的完全一致」,而那段 patch 里还有
routes / dropSections 这些面板不负责的键 —— 于是整段写入被拒,面板会提示
这份配置整段来自 profile 的 cordis.patch.yml(home patch 层),面板不能整段覆盖。
两条路:① 直接改那个文件里 local-tool-trim.config 的对应键;② 把 patch 里这个插件的
config: 段删掉(只留 id / name / inject),重启后配置归 settings 管,面板就能改。
面板的只读视图(状态带、收益条、点灯、试跑、拉起)在两种情况下都能用。
面板不出现怎么办
先看日志末行是「控制面板 UI 已进入 client 条目表」还是「需要完整重启一次 DSH」。
宿主的 dsh-client-modules 会把每个包第一次扫描的结果缓存起来(pkgMeta),
插件热重载不会清这个缓存 —— 所以如果先装插件、后补 dsh.client 声明,只重载插件
不够,必须完整重启一次 DSH。插件自己会在装载时清掉本包那一项并触发重扫,正常情况下
一次重载就够;日志里说还需要重启时,重启即可。
启动自检
插件装载时往 ~/.dsh/local-llm/local-trim.log 写 10 项检查。下面是形态示例,数字取自
某一份具体配置(你的会不一样,<…> 处随配置变化):
✓ tools 服务 可 restrict
✓ systemPrompt 服务 可用
✓ dsh-purge 已装(本插件在其外层收尾,无需改它源码)
✓ assemble 接管 accessor+purge-slot
✓ 轻量注入正文 <字符数>,来源 ~/.dsh/local-llm/light-inject.md
✓ 工具白名单 <keep 项数> 项
✓ 裁剪段落 <dropSections 项数> 段 + <dropContexts 项数> 个 context
✓ 生效路由 自动识别(baseURL 指向回环/私有网段,或 provider 名带 local/ollama 等标记)
✓ schema 瘦身 description 140 / 参数 90
✓ 一步到位 <探测结果,如「已有配置好的 local-llamacpp」>
控制面板 bridge 已挂载:/api/dsh-local-trim/{describe,save,probe,log}
控制面板 UI 已进入 client 条目表(刷新页面即可在插件详情页看到)
就绪:仅对 本地端点(自动识别) 生效,其它 provider 原样放行
想关掉:config.selfCheck: false。
验证裁剪真的生效了
llama.cpp 加 --log-prompts-dir 会把每次请求落盘:
llama-server ... --log-prompts-dir ~/.dsh/local-llm/prompts
cd ~/.dsh/local-llm/prompts
f=$(ls -t *.txt | head -1)
echo "SIZE=$(stat -c%s $f)"
for k in "LOCAL LIGHT" "PRIME DIRECTIVE" "Mindmap editing"; do
printf "%-20s %s\n" "$k:" "$(grep -c "$k" $f)"
done
期望:LOCAL LIGHT: 1、PRIME DIRECTIVE: 0、Mindmap editing: 0。SIZE 跟你自己的
配置有关,不看绝对值,看的是「注入换成了轻量版、那些大段不再出现」。
如果 PRIME DIRECTIVE: 1,说明结果被某一层重写覆盖了。两步排查:
- 看
local-trim.log里最新的assemble 接管行。理想值是accessor+purge-slot; 出现already意味着外层是别的实例留下的包装器(见坑位 6)。 - 在配置里临时打开
debugWrap: true,重载后再发一次请求,日志会多出成对的包装器进入: N 段 / X 字符与包装器返回: M 段 / Y 字符。如果「返回」的体积没有变小, 是trimAssembly判定这条请求不属于本地 provider(看routes/ baseURL 是否被识别); 如果「进入」的体积本身就很小而请求很大,说明别的层在我们之后又灌了内容进来。
七个踩过的坑
-
tools.restrict({ allow })只认全局工具名。 scoped 注册名(如dsh-ops的ops_glob) 混进 allow 会让整批校验失败,一条限制都装不上。本插件从错误消息里把被拒的名字抠出来 记住,下一轮跳过 —— 比依赖view()私有字段跨版本安全。 -
cordis 的 waterfall
next()不收参数。 下游拿到的是最初那份args,所以裁剪必须在await next()之后对返回值做。 -
别和 dsh-purge 的 400ms 心跳抢
slot.rewrite。 它每 400ms 用bindSlot覆盖一次, 反向重挂永远输在时序上。正确做法是接管prompt.assemble访问器并在 getter 上转发Symbol.for('dsh-purge.assembleGuard'),让它只刷自己的 slot、不重装访问器覆盖你。 -
注入段必须
interpolate: false,并且保留头 80 / 尾 96 字符。 否则renderPrompt会用变量改写正文,破坏宿主取样判定,整份原文立刻被重新前置。 -
llm-pi-ai改defaultContextWindow要重启实例。 HMR 不覆盖 provider 解析, 改了不重启的话 bili / 代理层拿到的还是旧窗口。 -
热重载会留下上一实例的包装器,不能看到标记就跳过。 重载后旧实例的
prompt.assemble包装器还在最外层,但它的ctx已失效 —— provider 判断失效 → 它自己 原样放行,而它内层仍会调dsh-purge把整份原文灌回去。所以接管时不能用全局布尔标记 判断「已经装过」,要带一个 每次apply都不同的实例 token;发现外层是别人的包装器时 叠加一层自己的,而不是放弃。进程重启后旧包装器自然消失,叠加的层数也就回到一。 -
.volatile()的配置字段不是值,是活引用。 加上volatile()之后宿主传进apply(ctx, config)的每个字段都是{ get(): T },config.enabled !== false会永远为真 (对象 !== false),裁剪开关直接失效。必须统一走一个resolveConfig(config)逐键.get(), 而且不能一次性解构成常量 —— 面板改了要能立刻生效,每次用时现读。
已知边界
- 只做本进程内的裁剪,不碰模型权重,不改任何第三方插件源码。
dsh-purge未安装时也能工作(接管原生prompt.assemble),只是没有头 80 / 尾 96 的 保留逻辑(没有那层兜底就不需要)。- 段名来自各插件的
systemPrompt.section({ name }),插件升级改段名后要在dropSections里同步。 - llama.cpp 侧的调参只到「一键拉起时给一组能跑的默认值」(
--n-gpu-layers auto --fit on --cache-type-k/v q8_0);要换量化、换 KV cache 精度、限并发,改高级配置里的启动命令,插件不替你优化推理性能。 - 资源预检是估算,不是保证。
checkModelFit拿os.freemem()/os.totalmem()和模型文件体积比, 三者都不含 GPU 侧显存、也不含 Windows 与驱动占掉的那部分(本机实测:32GB 机器 Windows 只报 16.37GB 可见,其中 GPU 拿走 14.5GB)。所以它只会拦住明显起不来的, 不会替你算出真实可用显存。预检判risky时插件会改用--no-mmap并要你手点确认; 判ok也不代表不会卡。 - 面板只能停自己起的进程。
stop路由走一张内存表(launched),表里只有本插件spawn过的 pid。宿重重启后这张表就空了,此时手动起的后端要自己关。
开发
npm run verify # = check + test,8 个文件语法 + 6 套测试
npm run check # 只跑 node --check(lib/*.js + client.js)
npm test # 只跑测试
node test/selfcheck.mjs # 两个场景:装了 / 没装 dsh-purge
node test/autodetect.mjs # 四个本地后端 + 三个云端 provider 的路由判定
node test/provision.mjs # 探测 → 写 settings 的一步到位
node test/panel.mjs # 面板视图模型 + 导入流程(离线,不起浏览器)
node test/panel-host.mjs # 真跑 apply(),假 settings / 假后端把 12 条 bridge 路由全打一遍
node test/launcher.mjs # 一键拉起 + 扫本机 gguf + 资源预检 + 停进程(不真起进程)
test/panel.mjs 只测纯逻辑:lib/status.js(接管文案、收益计算、provider 判定与点灯计数)
和 lib/import-model.js(入参校验、provider id 生成、profile 拼装、试跑请求与四类失败分支,
全部用假 fetch)。
test/panel-host.mjs 是宿主侧集成:用一份内存 settings、一个假 webServer、一个假
llama.cpp(/v1/models + /v1/chat/completions)跑真的 apply(ctx),让插件自己注册路由,
然后逐条断言 status / check / import / test / launch / stop / models / adopt 的返回 ——
包括同源守卫 403、同名不覆盖、端点连不上、模型不在列表里、provider 不存在、
资源预检拦截、以及「只肯停自己起的进程」这几种路径。它不需要真起 DSH。
这份用例显式传
bridgeThrottleMs: 0:它是在几毫秒内连着打同一批重活路由的, 默认 800ms 间隔会把它们全挡掉。节流本身由最后几组用例单独验。
test/launcher.mjs 覆盖一键拉起的纯逻辑:端口解析、命令行切分(认双引号)、
whichOnPath 补扩展名、四种后端的计划生成、找不到可执行文件与「需要模型参数」两条错误路径、
自定义命令优先、runDetached 的 detached/stdio/env 传参,以及 waitForEndpoint 的
命中与超时。spawnImpl 与 probe 全部注入,CI 上不会留下后台进程。
v0.3.9 起还覆盖 scanLocalModels(跳过 mmproj、按大小降序、目录下探)与「自选端口 +
指定 gguf」的拼装(端口和 alias 都得按用户填的走)。v0.4.0 补上了
mergeEnv(Windows 上 PATH / Path 大小写去重)、envPathValue、checkModelFit
(四档结论 + 拿不到体积时的 unknown)、--no-mmap 透传、stopProcess
(SIGTERM → SIGKILL → 报失败,幂等)、isProcessAlive(EPERM 算活着),
以及扫描的三道上限(maxModels / maxEntries / Downloads 只扫一层)与 truncated 标记。
本地不要为了验证去真起 17GB 的模型 —— planLaunch 已经把命令和参数摆出来了,看一眼就够;
真要端到端跑,挑个空闲时段。首次加载 17GB 权重会让整台机器明显变卡(这台机器上实测过,
直接把桌面拖到卡死)。
lib/config.js 顶层 import z from '@deepseek-ai/schemastery' —— 运行时由 DSH 宿主提供
(peerDependencies,optional,不会再给装插件的人拉第二份副本),跑测试前需要
npm ci(它同时也写在 devDependencies 里)。CI 就是这么做的。
scripts/restore-bash.ps1 修 pnpm 把 @dsh-ops/bash-win32-x64 当不匹配平台剪掉的问题
(arm64 Windows 上会遇到):重新 pack 该可选依赖并放回 node_modules/@dsh-ops/。
License
MIT
Comments
Loading…
Similar plugins
Auto-resumes dsh sessions whose turn was left mid-flight by a web process restart or by a transient upstream failure, by injecting a 「继续(自动)」 ("continue (auto)") user message so the agent picks up exa
★ 0
↓ 95/wk
dsh plugin --profile web add dsh-autoresumeKeep a DSH profile identical across machines through any WebDAV server. The plugin collects the five parts that make up a setup — models (the llm-pi-ai line of cordis.patch.yml), defaults (the agent-d
★ 0
MIT
TypeScript
dsh plugin --profile web add dsh-webdav-syncby a1141171521
DSH 自动记忆系统:本地优先、文本可读、失败可见的分层记忆。
★ 3
MIT
JavaScript
Oct 31, 2026
dsh plugin --profile web add dsh-memfuseby cayan0x
DSH Desktop enhancement plugin: task-execution discipline plus a real relationship, per session. Discipline: adaptive protocol tiers (short for chat / full for tasks / lean for reasoning models), inte
★ 4
↓ 766/wk
MIT
TypeScript
Oct 9, 2026
dsh plugin --profile web add lume-dsh-pluginby 18477514055
台账记忆(Ledger Memory)—— DSH 的工程化项目记忆:台账 + 活动日志(L0-L4) + 三层上下文压缩 + 逐字胶囊落盘 + 跨会话交接单。可回溯、可追查。
★ 2
MIT
JavaScript
Oct 27, 2026
dsh plugin --profile web add dsh-ledger-memoryby mrzturn
dsh-switchman who is a plugin for deepseek harness
★ 4
↓ 364/wk
MIT
JavaScript
Oct 23, 2026
dsh plugin --profile web add dsh-switchman