DSH 插件优化集合(dsh-prompt-tuner)
一个个人向的 DSH Web 插件集合:把日常真正用得上的优化做进输入框旁边,而不是只做一件事。目前六件:
- ✨ 提示词改写(单一模式):把输入框里已经写好但还不够明确的内容,用一套内置默认提示词(或你自己写的那一份)改写为指向清楚、更有逻辑、可直接执行的提示词,再交给 agent;可把最近 n 条会话消息按时间正序一起拼进提示词,可选改写结果用中文还是英文,并可指定用哪个模型、思考到什么档位(默认跟随当前会话、不思考)(见历史改进记录 · 提示词改写与改写自己的模型与强度)。
- 💬 旁路提问(
/btw):主任务跑着的时候,顺手问一个小问题——答案只浮在输入框上方,不写进主对话、不打断 agent(见历史改进记录 · 旁路提问)。
- 🗜 上下文压缩阈值:给每个模型单独设一条固定 token 数的压缩线(不是百分比),插件把它换算成 DSH 的
compaction-basic 需要的窗口占比并写进 DSH 配置(见历史改进记录 · 上下文压缩阈值)。
- 🔔 任务完成通知:每个对话任务结束时弹一条系统桌面通知,标题取会话标题,正文先由你指定的模型把本轮回答压缩成一句能完整显示的话(这次调用的思考强度可配,默认关闭;也绝不把最后一条消息原样推出去)——压不进设置的长度才以
... 结尾;宿主按平台分发——Linux 用 notify-send、Windows 用 PowerShell 通知、WSL 走 Windows(见历史改进记录 · 任务完成通知)。
- 🏷 会话标题重总结:初始标题仍是会话的第一句话(DSH 自己写的,插件不动);此后每积累 N 条你的消息,用你指定的模型根据最近 N 条消息重写一次标题,长度上限与 N 都在设置页里调(见历史改进记录 · 会话标题)。
- 🛣 供应商熔断与顺序切换(已拆成本地独立服务):某个供应商任何失败都在原路由上重试 N 次,用完即熔断,按顺序表切到下一个可用供应商(不必等原供应商的退避);冷却时长随连续熔断次数递增,成功一次归零;到期后按所选方式恢复——默认 probe:先放行一次探测请求,成功才真正切回,也可选
immediate。这一半不再跑在插件里:插件只负责把它作为独立进程拉起来,顺序表、供应商与密钥、熔断参数都在服务自己的页面上改,插件设置页的「路由」页签变成只读状态 + 跳转链接。服务对外是一个 OpenAI 兼容的本地代理(默认 http://127.0.0.1:8790/v1),所以本机任何 agent 都能用,不限于 DSH。它还带一个可注册的协议转换器接口,内置的 maas 转换器把 ZTE 网关那两个模型(maas-dsv4/deepseek-v4-flash、maas-coclaw/co-claw)的出入参对齐到 api.deepseek.com 的契约(见路由服务与历史改进记录 · 路由服务与 maas 转换器)。
所有模型调用都在宿主进程里发生:浏览器不接触凭据,也不直接调用任何 provider;桌面通知同样由宿主按平台派发;熔断状态也只活在宿主进程内存里(不落盘、不写会话)。
包名、路由前缀(/dsh-prompt-optimizer/*)、设置与旁路历史文件名、dspo- 类名前缀等标识符保持不变:它们是已装实例与用户设置的引用点,改名会丢配置或需要重装,不属于定位更名。
入口
插件占四个座位(五个注册入口),没有自己的面板(集合里的每个功能各占一格):
| 座位 | 内容 |
|---|
conversation.input.left | 工具行里的 ✨ 优化提示词 按钮与一行状态(Alt+O 等价);同排的 💬 旁路提问 按钮(Alt+B、斜杠命令 /btw 等价)。 |
conversation.input.dock | 改写预览卡片:原文与结果并排、模型标注的待确认、采用/撤销/再改一次/关闭。 |
conversation.input.overlay | 旁路提问浮层(刻意不是模态框,主对话仍在后面继续跑)与一个不可见的完成通知座位。 |
settings.section | 「插件优化集合」设置页(6 个页签)。 |
界面示意(浮层与卡片的实际样子)见历史改进记录 · 界面。
设置
设置页按功能模块分成 6 个页签:优化提示词 / 旁路提问 / 标题 / 压缩 / 通知 / 路由。改写功能只占「优化提示词」一个页签:它没有任何模式开关,页签里只有该功能真正暴露的五项(优化用的模型 + 改写的思考强度 + 自定义提示词 + 改写结果的语言 + 携带最近会话消息);模型默认「跟随当前会话」、强度默认 off,不填就是零配置行为。四个用到模型的功能(改写 / 旁路提问 / 标题 / 通知)都各自可选模型并各自可选思考档位,互不影响;固定了改写模型后,改写也不再走「路由」页签的熔断切换(那条链路只接管会话自己的模型调用)。另外五个页签分别是旁路提问、标题、压缩、通知与路由的设置。「路由」页签不再编辑任何东西:配置与熔断状态都在独立服务里,这个页签只显示服务的地址、存活状态、实时的熔断表与最近的切换记录,并提供跳转到服务自身页面的入口——两处都能改同一份配置就会漂移,所以这里只读。
配置文件:$DSH_HOME/prompt-optimizer.json(默认 ~/.dsh/prompt-optimizer.json),设置页底部显示实际路径。
{
"systemPrompt": null,
"recentMessages": 8,
"outputLang": null,
"provider": null,
"model": null,
"reasoningEffort": "off",
"btwContextTurns": "all",
"btwContextCount": 8,
"btwSaveHistory": true,
"btwProvider": null,
"btwModel": null,
"btwReasoningEffort": "off",
"compactionTokens": {},
"notifyOnComplete": true,
"notifyMaxChars": 120,
"notifyProvider": null,
"notifyModel": null,
"notifyReasoningEffort": "off",
"titleProvider": null,
"titleModel": null,
"titleReasoningEffort": "off",
"titleRerollTurns": 100,
"titleMaxChars": 24
}
每个设置项的逐条说明在历史改进记录 · 设置页,已移除的旧键如何处理见历史改进记录 · 配置文件与已移除的旧键。
思考档位发到网关上是什么:插件自己只声明标准的 reasoningEffort,不发任何网关私有字段(profile 里那两条 MaaS 路由的 reasoningEfforts 仍然有效)。而经过路由服务的调用,转换器会进一步把入参按参照契约翻译:thinking: {type: 'disabled'} 落成 reasoning_effort: "none",thinking: {type:'enabled', budget_tokens} 落成就近的档位,max_completion_tokens 折成 max_tokens,response_format: json_object 按官方的前置条件校验;出参则把 reasoning 改名为 reasoning_content、摘掉网关常驻的空字段、补齐 usage 与 system_fingerprint。判据是实测而不是文档,逐条对照见路由服务。
路由服务(独立进程)
熔断与路由不再属于插件运行时,而是一个本地独立服务:lib/service/。插件只做三件事——在插件激活时把它 fork 起来(没在跑的话)、
把设置页的「路由」页签指向它、把三个按钮(探测 / 清空熔断 / 刷新)转达过去。没有任何熔断状态留在插件进程里,也没有第二份实现。
它对外长什么样
| 面 | 地址 | 说明 |
|---|
| OpenAI 兼容 | POST http://127.0.0.1:8790/v1/chat/completions(GET /v1/models) | 把任何 OpenAI 客户端/agent 的 baseURL 指过来即可;入参出参按参照契约(见下) |
| 管理页 | http://127.0.0.1:8790/ | 配置供应商与密钥、顺序表、熔断参数;看实时熔断表、最近切换与统计 |
| 管理 API | POST/GET http://127.0.0.1:8790/admin/api/{state,config,reset,probe,models,keys/restore,converters} | 管理页用的接口,需要 X-Router-Token(页面自带,令牌在配置文件里) |
| 存活 | GET /healthz | 插件用它判断该接入还是该拉起 |
只监听 127.0.0.1,不做鉴权面之外的暴露。/admin/api/* 需要令牌:一个用户随手打开的网页可以对 127.0.0.1 发跨源 POST,
但不该能改路由或读到(掩码的)凭据。/v1/* 不需要,因为它只对本机开放,而它持有的凭据本来就已经在这台机器上。
接入方式
自己把本地模型地址指过去就行,插件不会替你改 profile:
// 以 DSH 的 pi-ai provider 为例(也可以改用任何 OpenAI 客户端)
"local-router": {
"api": "openai-completions",
"baseURL": "http://127.0.0.1:8790/v1",
"models": [{ "id": "deepseek-v4-flash" }]
}
北向的 model 用来决定从顺序表哪一行开始试:写 deepseek-v4-flash 从第一个模型名匹配的行开始,写 maas-dsv4/deepseek-v4-flash
从那一行开始,都不匹配就从第一行开始;之后按顺序表往后走并回绕——顺序表是一个池子加一个偏好,不是单向链表。切换对调用方是透明的:
只要上游还没吐出头一个字节,失败的那一跳不会出现在调用方的响应里。
转换器
服务提供一个注册接口,转换器是代码模块(lib/service/converters/),在配置里按 id 列出来即可:
defineConverter({ id, label, match, toUpstream, fromUpstream, fromUpstreamChunk, errorBody, listModels })
只有 id/label 是必填,其余缺省都是原样透传——所以「没注册转换器的路由」行为不变。这么设计是因为转换器要改的是流式 SSE 的增量、
要合成的 usage、要重写的错误体:那是状态机,不是替换表,声明式规则表达不了。内置一个 maas 转换器:
| 轴 | 参照契约(api.deepseek.com) | ZTE 网关 | 转换器怎么做 |
|---|
| 思考开关 | thinking: {type: 'disabled'|'enabled', budget_tokens} + effort | 忽略 thinking;认 reasoning_effort(none 真的关) | 翻译成 reasoning_effort,并把 thinking/effort 从上游请求里删掉 |
| 输出上限 | max_tokens(含思考 token) | 两种都认 | max_completion_tokens 折进 max_tokens |
response_format: json_object | 提示词里没有 "json" 就 400 | 无条件接受 | 强制参照契约的前置条件(这是唯一一处比上游更严) |
| 思考正文 | message.reasoning_content | message.reasoning | 改名 |
| 思考 token 数 | 总是上报 | co-claw 上报,deepseek-v4-flash 不上报 | 没思考就写 0;思考了但上游没报就省略,不估算 |
model 回显 | 请求的名字 | 后端名(DeepSeek-V4-Flash-0731) | 回显调用方请求的名字 |
usage 细节 | prompt_cache_hit_tokens / miss_tokens | prompt_tokens_details.created_cache_tokens 等 | 归一到参照契约;网关多出来的计数器丢掉 |
system_fingerprint | 有 | 有(vllm-…) | 原样透传,没有时按 provider+后端名合成 |
| 错误体 | {error: {message, type, param, code}} | vLLM 自己的形状 | 按状态码重写 |
| 未知字段 | — | — | 一律原样透传,不丢数据 |
转换器不会自作主张加参数:调用方什么都没说时不发 reasoning_effort,由网关自己的默认值决定(co-claw 默认为开、deepseek-v4-flash 默认为关)。
它保证的是「显式控制两边同义」。逐条实测见历史改进记录 · 路由服务与 maas 转换器。
配置与密钥
配置在 $DSH_HOME/router-service.json(默认 ~/.dsh/router-service.json,0600),由服务的页面读写。首次运行会从插件的旧设置里
把顺序表搬过来,并预置两条 MaaS 路由的地址——但不预置任何密钥,密钥要你自己在页面上填。服务不会去读 DSH 的凭据库。
一个供应商可以配多把 key,每把有自己的稳定 id、标签和密钥。稳定 id 是刻意的:熔断、失败计数、拉黑都按这个 id 记账,
所以轮换密钥不会丢掉它已有的熔断状态,也不会把两把 key 的状态混在一起(按明文或按掩码认领都会——掩码会撞)。旧配置里的
单个 apiKey 会被读成一条 key(id 固定为 k1),所以老文件照常能用。
页面上留空即保留已存密钥:服务从不把明文回传给页面,只回掩码,所以"不改密钥"只能是留空;填入新值即覆盖,从列表里删掉即删除。
请求头仍然是一个字符串值的扁平 JSON 对象,直接合并进上游请求。
熔断:颗粒度在 key,不在供应商
- 切换颗粒度是 key:某个 key 失败时,先在同一供应商的其它 key 之间切换;只有该供应商所有 key 都不可用,才轮到顺序表里的下一家。
无密钥的供应商(本地 Ollama 这类)仍然是一个单位,行为与从前一致。
- 只有该记账的失败才记账:失败先被分类(
lib/service/failure.js)。请求本身写错(400/404/422…)、模型不存在、调用方自己取消——
这些是调用方的问题,一次都不计,否则一个坏客户端就能把健康的供应商熔断掉。5xx/超时/传输错误算 retryable;
429/503/529 算 overloaded,当次即熔断(上游已经在说"慢点",再等一次确认只是多花一个请求)。
- 凭据死了就摘掉,不是冷却:401 / 402 / 403 是回答而不是抽风,它们进拉黑表——该 key 之后被完全跳过(不再向上游发请求),
直到你手动恢复、或上游给了恢复时间到期、或那把密钥被轮换/删除。把它们交给冷却式熔断,只会得到"永远在重试一把废掉的 key"。
- 双阈值:连续失败到
failureThreshold(默认 2,且"连续"限定在窗口内,隔一周的两次失败不算)或窗口内失败率到
failureRateThreshold(默认 0.7,且至少要有 minSamples 个样本才让比例说话)。前者在低频使用下也能生效,后者抓"时好时坏"。
- half-open 是租约:冷却到期只放行一次探测,
halfOpenSuccesses 次连续成功才闭合;探测失败立刻重新熔断且冷却升级
(cooldownMs 起、每轮乘 cooldownFactor、封顶 cooldownMaxMs)。上游给了 retry-after 就用它的时长,但仍受同一个上限约束。
探测一结束就交还租约(成功、被忽略的失败、转换器拒绝、调用方中途取消都算),否则"两次确认"会变成"两次确认相隔一个冷却"。
- 候选全部用完时,回写最后一个上游答案:上游真的答了 4xx/5xx,就把它自己的状态码与错误体(经转换器对齐契约)交给调用方——
"这个模型不存在"、"余额不足"是可行动的,换成服务自造的 503 恰好抹掉了唯一能用的信息。反过来,超时、连接断开、200 但 body
不是 JSON 这些不是"上游的答案",它们拿到的是服务自己的 502 错误体(带具体原因),绝不把不可解析的字节透传给调用方。
真正没有候选(没有路由、供应商未配置、所有凭据都被拉黑、所有候选都被熔断跳过)才回 503。
- 顺序表里的模型列是可手填的下拉:选项来自该供应商的手工模型列表 ∪ 从
GET {baseURL}/models 拉到的列表。页面上的「刷新模型」
按钮会真的去问上游(零 token,顺便验证凭据),结果记在运行态里,重启后仍在。手工列表可以留空(只有一条不拦保存的提醒):
留空的含义是"以上游拉到的为准",这也正是新建供应商唯一合理的第一步——一个还没保存的供应商根本无处可拉。
运行态与配置是两件事
配置是人的意图(页面保存时不可接受就拒绝,不悄悄改),运行态是运行时的事实(拉黑表、已发现的模型),放在
$DSH_HOME/router-service.state.json(0600,原子写)。分开的理由很实际:运行态由服务自己写,如果和配置同文件,服务就会去改一份
你可能正在手编的文件;而且一份坏掉的运行态绝不应该让服务起不来——配置那边"写时拒绝"的规则在这里不适用。
改供应商,路由表跟着改
顺序表是针对某一个供应商集合写的,所以增删改供应商时服务会对账这张表:provider 已经不在、或它所用的模型已经不在该供应商
可用模型里的行会被移除,并在保存的返回值里报告删了哪些行(页面会提示,不静默)。反过来,供应商只声明了空模型列表时不删任何行——
空是"未知"而不是"没有",在那里猜会删掉用户有理由写的行。
生命周期
服务由插件 fork,随 DSH 退出而停。这是刻意的取舍:换来的是零手工步骤。想让它活过 DSH 重启,本机已有现成的约定——
systemd-run --user --unit=<name> node <插件目录>/lib/service/main.js(dsh-remote-gateway 就是这么做的);
服务本身不关心是谁拉起它的,插件发现端口上已经有服务在跑时会接入而不是再拉起一个。
自检
npm run check:service # 292 项:桩上游 + 真服务 + 真 socket,离线、零 token
npm run check:ui # 76 项:管理页(假 DOM 跑真脚本),离线、零 token
npm run check:live # 28 项:对真实网关的端到端实测,需网络与密钥,非离线自检
覆盖转换器的入参/出参映射、熔断与切换、重试、坏响应、管理面鉴权、配置校验与迁移、转换器注册表、SSE 解析、以及插件真的能把它拉起来并转达。
另有 npm run check:live:对真实网关的端到端实测(28 项,需要网络、需要你自己在凭据库里配好那两个 ref,会花真实 token),所以它故意不进 npm run check;结论记在历史改进记录里。
安装
dsh plugin add github:lzyyzznl/dsh-prompt-tuner
仓库根有 cordis.patch.yml(一条 insert),package.json 声明 dsh.bundle.patch 与 dsh.client.platform=web;不含构建步骤,lib/ 即源码。
npm run check # 自检(843 项),不需要本机装 DSH
npm run eval:prompt # 三臂盲评:只打印计划,加 --live / --judge 才发真实请求
npm run eval:attribution # 归因审计:读已有运行记录,量「改写加了什么用户没提的东西」
文档
- 历史改进记录:每个功能的设计取舍与固定行为、实测延迟与失败阶梯、与其他同类插件的差异、架构与文件职责、自检覆盖范围,以及全部已知限制。
- 内置提示词的依据与实测:默认提示词为什么是这六步、三臂盲评的方法与数字、归因审计(改写里有多少东西是用户没提的)的口径与结果,以及中英两条输出路径的对照与偏差。
- 通知路径的实测记录:Windows toast 派发的实测与推断边界。
许可
BSD-3-Clause。