← Back to home@xiex16070-jpg

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
GitHub repo

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 根本不会回调。所以按优先级依次试,第一个被声明的胜出:

顺序座位渲染在哪
1conversation.input.right模型选择器左侧(目标位置)
2conversation.composer.dock输入框底栏,与上下文计量同排
3conversation.input.left完全权限 左侧,仍在输入框内
4conversation.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
注册 UIctx.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.dockdsh 自己的 app.asar:@deepseek-ai/dsh-client-ui-conversation/lib/client.js(声明 + renderSlot 调用点)
i18nctx.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 输出卫生

模型被明确告知"只输出改写后的提示词本身",但它仍可能加壳。宿主半侧只剥两种无歧义的壳,其余一律原样保留:

  1. 包住整个答案的代码块(``` 或 ~~~,可带语言标记);
  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),每次请求都重新读取——所以改完立刻生效,不需要重启。

键默认含义
enabledtrue关掉后所有改写请求返回 disabled
modestructured默认改写模式:structured / concise / detailed / translate-en / spec
languageauto输出语言:auto(跟随草稿)/ zh / en
temperature0.3改写任务,故意压低
timeoutMs45000单次改写超时
maxDraftChars8000草稿上限,超了拒绝而非截断
maxOutputChars12000输出上限,超了丢弃(判为没按指令改写)
offerUndotrue写回后是否显示「撤销」
showModeMenutrue芯片是否显示 ▾ 模式菜单
systemPrompt""非空则整体替换内置系统提示词(高级用法)

示例:

{
  "mode": "spec",
  "language": "zh",
  "temperature": 0.2,
  "maxDraftChars": 4000
}

写坏了也不要紧:JSON 解析失败会退回默认值而不是让插件报错。

5.4 怎么确认它真的装上了

  1. 打开一个新会话,看输入框选择器行有没有 ✦ 增强。
  2. 打开浏览器控制台,fetch('prompt-enhance/health').then(r=>r.json()) —— 应返回 {ok:true, value:{enabled:true, model:"…", llm:true}}。
  3. 试 /enhance 帮我写个爬虫:命令回复里应出现改写后的文本。

六、已知边界(重要,请先读这一段)

这个插件有一处依赖没有公开 API:读写输入框草稿。

DSH 的插槽、inputTriggers、session / workspace 服务都是有公开契约、且在装插件里被真实使用的;但"输入框里那段文字"不在其中。在装插件里没有任何一个读写 composer 草稿的接口(dsh-context 能拿到输入框的 / 触发词与 token span,但那是"消费一个 token",不是"读出整段并写回")。

所以 lib/client.js 里的 ComposerAdapter 被单独隔离出来,写法是启发式 + 写后校验:

  1. 先向上找,再全局兜底。 从芯片自己的 DOM 节点往上走(最远 14 层),找可见、可编辑的 textarea / [contenteditable];走不到就退到全文档搜索,并在多个候选里选最宽的那个——输入框稳定地是页面上最大的文本输入,而"第一个匹配"在设置页里会是别人的搜索框。
  2. 每次用都重新解析,绝不缓存。 这一条是上一版真正的事故原因:模型回答期间输入框会重渲染,回话到达时之前抓到的那个节点已经被摘掉了。往一个已脱离文档的节点写值,"读回来比对"居然会通过(节点自己还留着那个值),但屏幕上一个字都不会变。所以读写各解析一次,写之前再解析一次。
  3. 四种写法依次尝试,每种都读回校验:① 原生 value setter(React 会忽略直接赋值);② setRangeText;③ 全选后 execCommand('insertText')(富文本编辑器靠这条同步内部模型);④ 直接写 textContent。
  4. 全部失败就报失败,芯片退化为"面板展示 + 一键复制"。

这条边界的实际含义:

  • 如果 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