dsh-prompt-enhance
One-click prompt enhancement for the DeepSeek Harness composer: a chip beside the model selector rewrites the draft into a structured prompt using the session's own model.
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 6, 2026
- Updated
- Oct 6, 2026
Introduction
dsh-prompt-enhance
一键增强提示词 —— 一个 DeepSeek Harness 插件:在输入框的模型 / 工作区选择器旁加一颗芯片,点一下就把你正在写的草稿改写成一条更清晰、更可执行的提示词,并写回输入框。
┌──────────────────────────────────────────────────────────────┐
│ 输入框 │
│ │
│ 帮我写个爬虫,要快 │
│ │
│ [工作区 ▾] [✦ 增强 ▾] [模型 deepseek-flash ▾] [发送] │
│ ↑ │
│ 本插件注册在这里 │
└──────────────────────────────────────────────────────────────┘
一、功能定位
它只做一件事:把"随手写的一句需求"变成"一条能直接发给模型的提示词"。
- 不替代你思考,只把你已经说清的意思补齐结构:目标、约束、上下文、交付物、验收标准。
- 不改你的原意。专有名词、路径、命令、代码、版本号逐字保留;模型自己补的背景必须标成「假设:…」。
- 不新建对话、不写会话历史。增强是输入框里的一次文本替换,不是一条消息——所以它不会污染上下文,也不会消耗一次对话轮次。
- 用你自己的模型。取当前会话的
provider/model,不额外配 key、不换模型、不引入第二种计费口径。
一句话区分:它不是"帮我写提示词"的助手,是"把你写的这句改好"的编辑器。
二、集成位置与触发方式
2.1 位置:输入框底部动作行,模型选择器旁
┌──────────────────────────────────────────────────┐
│ 发消息或创建任务,/ 调用指令,@ 文件或对话 │
│ │
│ [+] [🛡 完全权限 ⌄] [✦ 增强 ▾] [模型 ⌄] [发送] │
│ ↑ │
│ 本插件落在这里 │
└──────────────────────────────────────────────────┘
用一个真正的插槽,不是 DOM 搬家。
芯片注册进 conversation.input.right —— DSH 的 @deepseek-ai/dsh-client-ui-conversation 里,这一行就是它:
children: [
renderSlot("conversation.input.right", {}), // ← 本插件
renderSlot("conversation.input.model", { locked }) // ← 模型选择器
]
声明是 { kind: "list", scope: "session" }:列表座位(可以多个插件共存),会话作用域(有会话时渲染)。它就在模型选择器紧左边,正是要的位置。
座位是探测出来的,不是猜的。 一个运行中的 shell 没声明的座位名会静默失效——slots.inject 根本不会回调。所以按优先级依次试,第一个被声明的胜出:
| 顺序 | 座位 | 渲染在哪 |
|---|---|---|
| 1 | conversation.input.right | 模型选择器左侧(目标位置) |
| 2 | conversation.composer.dock | 输入框底栏,与上下文计量同排 |
| 3 | conversation.input.left | 完全权限 左侧,仍在输入框内 |
| 4 | conversation.input.dock | 输入框上方的 hero 行(保底:看得见,但位置不对) |
每个座位给 400ms 的声明期限;都没声明就什么都不注册,也不抛错。胜出的座位会打到控制台:[prompt-enhance] chip seated in conversation.input.right。
上一版为什么错了:它注册进
conversation.input.selector.context——这个名字在当前 dsh 里根本不存在(grep -c为 0),所以静默回退到conversation.input.dock,也就是输入框上方的 hero 行;然后为了挪到模型名旁边,用 React portal +MutationObserver把 DOM 节点搬进输入框的动作行。视觉上对,但形状是错的:往 React 托管的行里插自己的节点,并在全应用的每次 DOM 变更上重跑一遍全文档查询。现在这些都删掉了:没有 portal、没有
MutationObserver、没有insertBefore。自测里有两条源码级断言盯着它们不许回来。
2.2 三种触发方式
| 触发 | 操作 | 结果 |
|---|---|---|
| 点芯片 | 单击 ✦ 增强 | 用当前模式改写草稿并写回输入框 |
| 换模式 | 点芯片右侧 ▾ | 展开模式菜单(结构化 / 精简 / 详尽 / 译成英文 / 规格化),点任一项立即按该模式改写 |
| 命令行 | /enhance <你的提示词> | 走宿主斜杠命令,结果作为命令回复给出——不碰输入框,适合键盘流与无鼠标环境 |
改完之后芯片会短暂显示「已增强」,并出现 撤销:一次点击把原文写回。原文只保存在页面内存里,刷新即消失。
三、与 DeepSeek Harness 的对接方式
3.1 一个包,两个半侧
dsh-prompt-enhance
├── lib/index.js ← 宿主半侧(Cordis 插件:name / inject / apply)
│ · 挂 HTTP 路由 /prompt-enhance/*
│ · 调宿主 LLM 做改写
│ · 读写配置
│ · 注册 /enhance 斜杠命令
└── lib/client.js ← 浏览器半侧(普通副作用脚本,经 __ModuleLoader__ 加载)
· 注册芯片到输入框插槽
· 读写输入框草稿
· 只负责搬文本,不碰模型
两半靠 package.json 的 dsh 字段声明:
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" }, // 宿主半侧挂载声明
"client": { "platform": "web", "inject": [ … ] } // 浏览器半侧:DSH 会把
} // exports["./client"] 提供给页面
cordis.patch.yml 把插件行插进配置树:
- insert:
- id: prompt-enhance
name: 'dsh-prompt-enhance'
为什么改写放在宿主半侧:模型调用、凭据、提示词模板都不该进页面。页面只发一条 POST,拿回文本。这条界线同时让宿主半侧可以在没有浏览器的情况下被测试。
3.2 用到的宿主能力,以及每一处的依据
本插件只使用已被在装插件实际使用过的 API,没有发明接口:
| 能力 | API | 依据(在装插件中的实例) |
|---|---|---|
| 浏览器半侧加载 | window.__ModuleLoader__.load({ id, factory }),factory 返回 { name, inject, apply } | dsh-pet src/client/index.ts |
| 注册 UI | ctx.slots.inject(座位, cb) / ctx.slots.register({name,id,order,locale,inject}, 组件) | dsh-pet app.ts、git-graph index.ts |
| 插槽名与布局 | conversation.input.right / .model / .left / .dock、conversation.composer.dock | dsh 自己的 app.asar:@deepseek-ai/dsh-client-ui-conversation/lib/client.js(声明 + renderSlot 调用点) |
| i18n | ctx.locale.register(NS, {zh,en}) / ctx.locale.bind(NS) | dsh-pet、git-graph |
| 条件挂载 | ctx.inject([服务…], scope => …)(等 conversation 就绪再注册) | git-graph index.ts |
| 生命周期 | ctx.effect(fn, label),fn 返回 disposer | 三者皆用 |
| HTTP 路由 | ctx.webServer.register({ kind: 'prefix', path, handler }) | git-graph host/routes.ts、dsh-pet host/index.ts |
| 浏览器 → 宿主 | 同源文档相对路径 fetch,信封 {ok,value} / {ok,error} | git-graph client/api.ts |
| 当前模型 | ctx.agentDefaultModel.currentSelection() → {provider, model} | dsh-pet host/chat.ts |
| 调模型 | ctx.llm.stream({provider, model, messages, system, temperature, signal}) + BlockAssembler + createUserMessage(@deepseek-ai/dsh-llm) | dsh-pet host/chat.ts |
| 斜杠命令 | ctx.commands.register({name, description, input:{hint}, handler}) | dsh-pet host/index.ts |
路径必须文档相对:DSH 用 <base href="./"> 提供 GUI,根绝对路径(/prompt-enhance/...)会逃出子路径部署的前缀,永远打不到宿主路由。所以浏览器半侧用的是 prompt-enhance/rewrite(无前导斜杠)。
3.3 数据流
用户点芯片
│
├─ 浏览器半侧:从芯片自己的 DOM 位置向上找到输入框编辑器,读出草稿
│
├─ POST prompt-enhance/rewrite { draft, mode, locale }
│ │
│ └─ 宿主半侧:
│ · 校验(空 / 超长 / 含分隔标记)
│ · 取 agentDefaultModel.currentSelection()
│ · 组装 system + user(草稿被 <<<草稿开始>>> 定界)
│ · ctx.llm.stream(…) → BlockAssembler 拼回文本
│ · 剥掉模型多加的包裹(整段代码块 / 引导句)
│ · 校验结果(空 / 超长 / 与原文相同)
│ ← { ok:true, value:{ text, mode, model, provider, elapsedMs } }
│ 或 { ok:false, error:{ reason, message, … } }
│
└─ 浏览器半侧:写回编辑器 → 读回校验 → 成功则显示「已增强 + 撤销」
→ 失败则弹面板展示结果 + 一键复制
四、输入输出行为
4.1 路由契约
| 方法 | 路径 | 请求 | 成功响应 value |
|---|---|---|---|
GET | /prompt-enhance/health | — | { enabled, routePrefix, provider, model, llm } |
GET | /prompt-enhance/config | — | 公开配置(见下) |
PUT/POST | /prompt-enhance/config | 配置补丁对象 | 保存后的公开配置 |
POST | /prompt-enhance/rewrite | { draft, mode?, locale? } | { text, mode, model, provider, elapsedMs } |
信封统一:成功 { ok: true, value };失败 { ok: false, error: { reason, message, … } }。
拒绝是 200,不是 HTTP 错误——"模型按规矩拒绝了这次改写"是一次成功的请求,浏览器需要拿到 reason 和 message 去渲染。只有请求本身有问题才是 4xx:空/非法 body → 400,方法不对 → 405,路径不存在 → 404,处理函数内部异常 → 500。
rewrite 的拒绝理由(error.reason):
| reason | 含义 |
|---|---|
disabled | 插件被配置停用 |
empty-draft | 输入框是空的 |
draft-too-long | 草稿超过 maxDraftChars(拒绝而不是静默截断) |
draft-has-fence | 草稿里含插件自己的分隔标记 <<<草稿开始>>>,无法安全定界 |
no-model | 当前会话没有 provider/model |
no-llm | 宿主 LLM 服务不可用 |
timeout | 超过 timeoutMs(与下面的失败区分开:这条是"再试一次") |
generate-error | 模型调用本身失败(附带 detail) |
empty-answer | 模型没返回可用文本 |
answer-too-long | 输出超过 maxOutputChars,判定为没按指令改写,丢弃而不是塞给用户 |
unchanged | 结果与原文逐字相同(不是错误,但不会谎报"已完成";error.text 里仍带着结果) |
4.2 输出卫生
模型被明确告知"只输出改写后的提示词本身",但它仍可能加壳。宿主半侧只剥两种无歧义的壳,其余一律原样保留:
- 包住整个答案的代码块(``` 或 ~~~,可带语言标记);
- 一行引导句(
以下是增强后的提示词:/Here is the enhanced prompt:)——要求该行 ≤60 字、以冒号结尾、且以以下/下面/这是/改写后/增强后/here/below/sure/当然/好的开头。
不做更多猜测:再往下猜就变成替用户改他的提示词了。
4.3 芯片的 UI 状态机
| 状态 | 表现 | 进入条件 |
|---|---|---|
idle | ✦ 增强 | 初始 / 闪示结束 |
busy | ✦ 增强中,禁用点击 | 请求进行中 |
done | ✦ 已增强 + 撤销,2.6 秒后回 idle | 写回成功 |
error | 展开面板:理由 + (若有)结果文本 + 复制 | 任何拒绝,或改写成功但写回失败 |
"改写成功但写回失败"是一个独立结果,不会被说成成功:芯片显示 没找到输入框,已把结果放在这里,请手动复制。 并把结果放进面板。这是这套设计里最重要的一条:按钮要么真的改了,要么明说没改。
五、安装、启用与配置
5.1 安装
DSH 的插件是 npm 包:装进 profile 的 node_modules,并在 profile 的 package.json 里登记为 bundle。profile 默认在 ~/.dsh/profiles/<名字>/(本机是 desktop)。
方式 A:官方命令(推荐)
# 从 registry / 市场安装
dsh plugin --profile desktop add dsh-prompt-enhance
# 从本地目录安装(开发时用;注意下面的 `link:` 陷阱)
dsh plugin --profile desktop add file:~/code/dsh-prompt-enhance
# 卸载
dsh plugin --profile desktop remove dsh-prompt-enhance
dsh plugin add 会做两件事:把包装进 <profile>/node_modules/(内部走 pnpm),并把它写进 profile 的 dsh.profile.bundles。
方式 B:手工登记(命令不可用时)
编辑 ~/.dsh/profiles/desktop/package.json,两个地方都要改:
{
"dependencies": {
"dsh-prompt-enhance": "file:C:/Users/admin/code/dsh-prompt-enhance"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-prompt-enhance"
]
}
}
}
然后在 profile 目录用宿主自带的 pnpm 安装:
cd ~/.dsh/profiles/desktop
~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/node/bin/node.exe \
~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/pnpm/bin/pnpm.cjs install
只写
dependencies不写bundles(或反之)都不会生效:前者让它可解析,后者才让它进配置树。
5.1.1 本地开发用 file:,不要用 link:(实测)
本机 profile 的 pnpm-workspace.yaml 设了 nodeLinker: hoisted。在这个组合下 link: 协议不会在 node_modules 里生成条目:lockfile 里会正确写下 version: link:…,pnpm install 也报成功(Packages: +1),但 node_modules/<包名> 始终不存在 —— 于是插件根本不加载,而且没有任何报错。改用 file: 协议即可。
file: 的行为值得知道:pnpm 把包 硬链接 进 node_modules,所以
| 改动 | 是否立刻生效 |
|---|---|
用编辑器就地改写已有文件(lib/client.js 等) | ✅ 立刻生效(同一个 inode) |
新增 / 删除文件,或用 mv/cp 整体替换某个文件 | ❌ 需要重新同步 |
实测 pnpm 对这个 file: 依赖很固执:pnpm install(含 --force)只会说「Already up to date」什么都不做,即使你把 node_modules/<包名> 删掉它也不重建;pnpm add "file:…" 只在包不存在时才会重新落地。
所以最省事的重新同步是直接把硬链接接回去(pnpm 当初就是这么装的,手动做一遍即可,幂等且不需要动 lockfile):
// node scripts/relink.mjs (或直接在 node -e 里跑)
const fs = require('fs');
const dev = 'C:/Users/admin/code/dsh-prompt-enhance/';
const ins = 'C:/Users/admin/.dsh/profiles/desktop/node_modules/dsh-prompt-enhance/';
for (const f of ['lib/client.js', 'lib/config.js', 'lib/enhance.js', 'lib/index.js',
'package.json', 'cordis.patch.yml', 'README.md']) {
fs.rmSync(ins + f, { force: true });
fs.linkSync(dev + f, ins + f); // 同一个 inode:以后就地编辑立刻生效
}
接回去之后,用就地改写(编辑器保存、Edit 工具)改文件就永久生效,不用再管同步。只有 mv/cp/整文件重写会打断硬链接,那时再跑一次上面的循环。
装完必须重启 DSH(见下一节)。
5.2 启用
包的 cordis.patch.yml 会作为 bundle 层自动叠加,不需要手工改 profile 的 cordis.patch.yml。装完重启 DSH 即可(宿主半侧在启动时注册路由;浏览器半侧在页面加载时注册座位)。
要停用,在 ~/.dsh/profiles/desktop/cordis.patch.yml 里按行 id 关掉:
- id: prompt-enhance
disabled: true
(该文件是 profile 的补丁层——一个顶层 YAML 数组,按 id 覆盖配置、停用行、或 insert 新行。本机已有先例:dsh-pet 就是这样被停用的。)
5.3 配置
配置是可选的:默认值即可用。用户层写在 $DSH_HOME/prompt-enhance/config.json(默认 ~/.dsh/prompt-enhance/config.json),每次请求都重新读取——所以改完立刻生效,不需要重启。
| 键 | 默认 | 含义 |
|---|---|---|
enabled | true | 关掉后所有改写请求返回 disabled |
mode | structured | 默认改写模式:structured / concise / detailed / translate-en / spec |
language | auto | 输出语言:auto(跟随草稿)/ zh / en |
temperature | 0.3 | 改写任务,故意压低 |
timeoutMs | 45000 | 单次改写超时 |
maxDraftChars | 8000 | 草稿上限,超了拒绝而非截断 |
maxOutputChars | 12000 | 输出上限,超了丢弃(判为没按指令改写) |
offerUndo | true | 写回后是否显示「撤销」 |
showModeMenu | true | 芯片是否显示 ▾ 模式菜单 |
systemPrompt | "" | 非空则整体替换内置系统提示词(高级用法) |
示例:
{
"mode": "spec",
"language": "zh",
"temperature": 0.2,
"maxDraftChars": 4000
}
写坏了也不要紧:JSON 解析失败会退回默认值而不是让插件报错。
5.4 怎么确认它真的装上了
- 打开一个新会话,看输入框选择器行有没有
✦ 增强。 - 打开浏览器控制台,
fetch('prompt-enhance/health').then(r=>r.json())—— 应返回{ok:true, value:{enabled:true, model:"…", llm:true}}。 - 试
/enhance 帮我写个爬虫:命令回复里应出现改写后的文本。
六、已知边界(重要,请先读这一段)
这个插件有一处依赖没有公开 API:读写输入框草稿。
DSH 的插槽、inputTriggers、session / workspace 服务都是有公开契约、且在装插件里被真实使用的;但"输入框里那段文字"不在其中。在装插件里没有任何一个读写 composer 草稿的接口(dsh-context 能拿到输入框的 / 触发词与 token span,但那是"消费一个 token",不是"读出整段并写回")。
所以 lib/client.js 里的 ComposerAdapter 被单独隔离出来,写法是启发式 + 写后校验:
- 先向上找,再全局兜底。 从芯片自己的 DOM 节点往上走(最远 14 层),找可见、可编辑的
textarea/[contenteditable];走不到就退到全文档搜索,并在多个候选里选最宽的那个——输入框稳定地是页面上最大的文本输入,而"第一个匹配"在设置页里会是别人的搜索框。 - 每次用都重新解析,绝不缓存。 这一条是上一版真正的事故原因:模型回答期间输入框会重渲染,回话到达时之前抓到的那个节点已经被摘掉了。往一个已脱离文档的节点写值,"读回来比对"居然会通过(节点自己还留着那个值),但屏幕上一个字都不会变。所以读写各解析一次,写之前再解析一次。
- 四种写法依次尝试,每种都读回校验:① 原生 value setter(React 会忽略直接赋值);②
setRangeText;③ 全选后execCommand('insertText')(富文本编辑器靠这条同步内部模型);④ 直接写textContent。 - 全部失败就报失败,芯片退化为"面板展示 + 一键复制"。
这条边界的实际含义:
- 如果 DSH 换了输入框实现,最坏结果是芯片变成"增强结果展示器",不会静默丢内容、不会写错地方、不会谎报成功。
- 面板 + 复制是已验证的退化路径,不是应急补丁。
- 若将来 DSH 暴露了 composer 读写 API,替换的只有
ComposerAdapter这一个对象,其余代码不动。
座位问题已经查清、不再靠猜:DSH 的客户端包就在本机 dsh 自己的 app.asar 里(%LOCALAPPDATA%\Programs\DeepSeek Harness\resources\app.asar,不是 WorkBuddy 那个 asar)。conversation.input.right 的声明与渲染位置都是从那里读出来的,不是从第三方插件的注释里推的。芯片当前落在哪个座位会打到控制台,装完看一眼即可。
仍然只能靠启发式的只有一件事:输入框草稿的读写(本节上半部分)。这条边界不会让功能失效——最坏是退化成"结果展示 + 一键复制"。
七、自测
npm test # 零依赖、不需要 dsh、不需要网络、不需要 DOM
覆盖:配置面(默认值 / 钳制 / 落盘 / 坏文件降级)、改写契约(提示词形状、剥壳、每一个拒绝理由、超时与生成失败分开报)、路由信封(200/400/404/405、拒绝走 200)、斜杠命令、浏览器半侧的模块形状 / 座位注册 / 回退 / 前缀自检 / composer 适配器 / 搬家到输入框动作行。
其中三条是这次修复的验收用例,都跑在一个手搭的假 DOM 上:
- 端到端(已搬家):芯片的 portal 宿主确实落在动作行、就在发送按钮之前;点击后草稿被读出、答案写回真输入框。
- 输入框中途被换掉:请求发出后 shell 换了一个全新的编辑器节点,答案必须落进新节点,而不是那个已经脱离文档的旧节点。这条直接对应"生成完成后找不到对话框"。
- 退化:拿不到输入框时芯片说"没找到输入框",且不去调模型(不浪费一次调用)。
它不能证明的:芯片在真机上长什么样,以及上面第六节说的那两点。那两件事只能靠装上看一眼。
八、目录结构
dsh-prompt-enhance/
├── package.json # dsh.bundle.patch + dsh.client 声明
├── cordis.patch.yml # 宿主配置树的挂载行
├── icon.svg
├── lib/
│ ├── index.js # 宿主半侧:路由 / 命令 / 配置
│ ├── config.js # 配置表 + 模式定义 + 读写
│ ├── enhance.js # 改写引擎:提示词、剥壳、校验
│ └── client.js # 浏览器半侧:芯片 + composer 适配器
└── scripts/selftest.mjs
License
MIT