跳过主要内容
X

dsh-input-optimizer

xxsttb/dsh-input-optimizer

在输入框模型选择器左侧加一个四角星按钮,发送前就地改写尚未发出的草稿——语法、用词、标点与制作顺序——并逐字节保留代码、命令、路径、标识符、数字与 URL;它不回答请求,也不会覆盖改写期间新输入的文本。每条用户消息下的铅笔可在不碰草稿的前提下重新打开该消息编辑;因为 Harness 的会话日志只追加,编辑是在该消息之前派生分支,而不是改写原消息。每条回复下的圆形重启图标会在一个不含此前任何轮次的全新会话里重新问同一个问题。一轮还在作答时两者都不允许;当这条回复用过会留下痕迹的工具,或该消息提到的文件此后被改过时,两者也会被禁用。右侧栏的记忆树把对话按轮次画出来,显示真正被问和被答的内容,同一个问题的每个回答各占一行并标注为原始、编辑所得或无记忆重问,最新的在最上方,箭头在原处换读该回答;右键可跳转到某个分支,或把它从插件自己的历史中移除。

安装

dsh plugin --profile web add github:xxsttb/dsh-input-optimizer

README

dsh-input-optimizer

English | 中文

一个 DeepSeek Harness 插件,做五件事:

  1. 重写你的草稿。 输入框的模型选择器左侧有一个四角星按钮。按下去,你已经打好、还没发出的草稿会先被重写:修正语法与措辞,并调整那些会悄悄削弱结果的制作顺序偏差。
  2. 编辑你已经发出去的消息。 每条用户消息旁边有一支铅笔。它把输入框临时换成编辑区,不碰你的草稿;发送后从这条消息之前切出一条新分支。
  3. 不继承记忆地重新提问。 每条模型回复下方有一个圆形重启箭头:它在一个全新会话里重新回答这一条消息,而那个会话不带任何此前的轮次。
  4. 把对话的分支历史画成一棵记忆树,放在右侧栏,两种模式。同一组 ‹ N/M › 箭头在同一个问题得到过的每一个回答之间切换,无论那个回答是怎么来的。
  5. 挡住会误导模型的编辑。 如果那一轮改过文件、或跑过命令,铅笔与重新生成都会禁用并说明原因。
[ ★ ] [ 模型选择器 ] [ 发送 ]
  ^
  按钮在这里

重写不是摘要器,也不是回答生成器。它从不回应你的请求,也从不添加事实。它交还的是同一个请求,只是写得更清楚。

所有东西都在插件自己的设置页上可配:重写指令、是否跟随当前对话的模型、要固定使用的提供方与模型、推理强度,以及用哪个快捷键发送编辑。

关于作者。 代码、注释、图标、语言文件和本文档,全部是在用户要求下由 AI 助手生成的。见 AI 生成。


它做什么

按下模型选择器旁边的四角星。Host 半侧会把你当前的草稿送到当前对话选中的模型(默认 low 推理强度),再把重写后的文本放回输入框,让你在发送前先过目。

重写瞄准五个目标,按优先级排列:

#目标实际含义
1保留意图不添加你没写的东西。没有回答、没有观点、没有举例、没有开场白。
2修正语言语法、拼写、标点、用词、指代不清、主谓不一致、机翻腔。保留你原本的语言与语域。
3修正执行顺序步骤写反了、前置条件排在需要它的动作之后、目标被背景埋住、约束与它约束的对象被拆开、两件无关的事缠在一句里。
4点明明显的隐含前提如果你列的步骤显然必须按顺序执行,它可以点出来。它绝不发明需求。
5收紧删除废话、重复和空洞的铺垫。

指令强制执行的硬规则:代码、命令、文件路径、标识符、数字、单位、URL、报错信息和引用文本逐字节保留;已有的 Markdown 结构与列表符号原样保留;原文语言(包括刻意混用)保留;已经清楚的文字原样返回;回复里只有重写后的文本 —— 没有 Rewritten: 标题、没有外层引号、没有任何附注。

你应该知道的行为

  • 你的编辑优先。 如果重写在飞的时候你还在打字,结果不会被写入,按钮会报告草稿变了。模型的输出永远不会悄悄盖掉更新的输入。
  • 整份草稿被替换。 结果作为一次编辑写回,所以按一次撤销就能恢复你原来的内容。
  • 空草稿会被拒绝,在联系任何提供方之前。
  • 重写是一次旁路调用。 它不带 session id、不带 purpose,所以它永远不会进入任何对话日志,也不会触发那些监听 llm/stream 的 agent-loop 流量监听器。

编辑一条已经发出去的消息

鼠标悬停在用户消息上,复制按钮旁边会出现一支铅笔。点击它不会把文本放进输入框 —— 你的草稿原地不动。取而代之的是输入框被换成编辑区,而原消息留在对话里,一字未改,作为你所在分支的起点。

  • 编辑区从这条消息本身的文本开始,并随文本增长,最高到略超半个窗口高度;它永远不会在一个固定高度的框里滚动。
  • 默认用 Ctrl+Enter(或 Cmd+Enter)发送,或点 发送 按钮。单独的 Enter 是换行,所以多行编辑不会在不小心的时候一发而不可收。
  • 快捷键可配置,在设置页:Ctrl+Enter、Ctrl+Shift+Enter、Alt+Enter,或完全不用快捷键。它是一个浏览器端偏好,存在本地,不是 Harness 的快捷键注册 —— Harness 已经把 Ctrl+Enter 绑给了它自己的互补动作,插件不能占用已经被拿走的键位。
  • Esc 取消。取消按钮也一样。
  • 如果 Harness 拒绝了这次文本,编辑区会保持打开并留着你写的内容,什么都不会丢。

这里的「编辑」是什么意思

Harness 的会话日志是严格只追加的:没有截断、回退或回滚 API,本插件也不假装有。发送一次编辑会**分叉(fork)**对话 —— 分叉点在原消息之前那个事件 —— 并把编辑后的文本送进新分支。原消息和模型给出的一切留在它原来的对话里;你正在编辑的这条分支加入同一条谱系,并以标题递增的形式出现在会话列表里。

这就是铅笔只出现在用户消息上的原因。编辑模型的回复意味着换一个问题,而不是修正你问的那个。

在版本之间切换

一旦某条消息被编辑过 —— 或者某个回复被重新生成过 —— 动作行会长出 ‹ 1/3 › 箭头与 N/M 计数。箭头在这条消息存在过的回答之间切换:原版在前,之后按切出的顺序排列。整组的 tooltip 会说明你正在看哪一个。这份记录存在浏览器本地。

箭头就在你所在的地方读另一个回答,在那个提问的轮次下方 —— 它不会把你扔进另一段对话。消息下方的计数、轮次下方的内联块、以及记忆树里的高亮,读的是同一份存储的选择,所以这三处永远不可能对「你在读哪一个回答」产生分歧。要前往某个分支 —— 在那里继续对话 —— 用记忆树里的打开按钮(↗),或者回答块上的那个。

回复下方的箭头通向那个问题收到过的每一个回答,无论每一个是来自编辑问题还是来自重新生成,因为两条路写的是同一份记录。

只有一个回答的问题不画箭头也不画块:在一个问题真的被重新问过之前,什么都不变。

其他回答在哪里

一个问题的每一个回答都是真实的对话 —— 它有自己的工作目录、自己的工具、模型可以在其中被继续的自己那份记忆 —— 但它从会话列表里隐藏。一个分支一旦答完、且读者已不在其中,就会被归档,所以同一个问题问十次,会话列表仍然和原来一样长。

操作顺序是有意为之的:

  • 分支还在作答时不被隐藏。Harness 会拒绝已归档会话里的每一次模型步进,所以过早归档只会留下一个永远无法回复的对话。
  • 分支在你身处其中时不被隐藏。归档屏幕上正在显示的那个会话会清空屏幕。
  • 打开一个分支时先还原、再打开,因为自带的会话浏览器根本拒绝打开一个已归档的会话。

所以被隐藏的回答从不会丢:在树里点它,它就回到列表里,可以像任何其他对话一样打开与继续。

铅笔什么时候被禁用

只有当重新提问不会误导模型时,一条消息才可编辑。会检查两件事,任何一件成立就禁用铅笔,并在气泡下方显示原因。

回复用过的工具。 编辑会在你的消息之前切出分支,于是那个分支里的模型面对的是一个可能已经被上一条回复改写过的工作区 —— 而它毫不知情,因为做出那些改写的轮次已经不在了。所以只有当一个轮次用过的每一个工具都只是读取时,它才可编辑:

允许阻止
read、read_image、glob、grepwrite、edit、str_replace_editor
web_search、web_fetchpwsh、bash,以及任何其他 shell
job_list、job_output、get_goalskill、todo_write、present
acp_status、acp_cache、search_contextdecompress(带 toFile 时会写文件)
load_workspace_dependencies任何这个版本不认识的工具

判据是「这个工具能不能留下痕迹」,而不是「听起来是否被动」—— 这就是为什么 decompress 被放在阻止的一侧,尽管它只是读上下文;也是为什么 shell 被阻止,即使它跑的命令碰巧只是个 ls。

纯粹聊天的回复没有用过任何工具,所以它保持可编辑。这份名单是白名单:任何未被识别的工具都会禁用铅笔,因为一个无法归类的工具就无法被证明无害。还在书写中的回复同样被阻止,因为编辑它会与已经在飞行中的回答赛跑。

一段对话的第一条消息前面没有东西可分叉,所以它的铅笔画成禁用而不是隐藏 —— 一个消失的按钮看起来像 bug,一个禁用的按钮自己会解释。

这条消息提到过的文件。 用 @路径 提到过文件的消息会对着那个文件检查。如果它变了、没了、或读不到,铅笔被禁用并显示原因。

检查走 Harness 自己的 workspaceFiles 服务(remote.workspaceFiles.stat),所以指纹是 Host 的,在会话的工作目录内解析 —— 本插件自己不读任何文件。

这个限制值得说清楚:Harness 不保留文件历史,所以「这个文件自消息发出以来是否变过」是无法回答的。守卫会在插件第一次看到该消息时记录一份指纹,一旦文件不再匹配那份指纹、或消失、或不可读,就阻止编辑。如果一个文件在被看到之前就已经变过,守卫无从得知。

重新生成的同一套条件

回复下方的重新生成按钮与铅笔用完全相同的判据。那一轮如果改过文件、写过文件、跑过命令,或者还在作答,圆形箭头就会禁用,悬停时给出的也是同一条理由 —— 因为「不继承记忆地重新提问」同样会让新会话面对一个已被改写的、它浑然不知的工作区。只有纯聊天、或只读过东西的那一轮才允许重新生成。


不继承记忆地重新提问

在每条模型回复下方、复制按钮旁边,有一个圆形重启箭头。它在完全不记得任何东西的对话里重新回答同一个问题。

按它不会分叉。分叉会把整个前缀一起带走,而那恰恰是这里要甩掉的记忆。取而代之的是在同一个工作目录里创建一个全新的会话,把你那一条消息送进去,Harness 切换过去。模型看到的就是那一条消息,前面什么都没有 —— 没有你更早的轮次,没有你正在读的那条回复,也没有那条回复刚刚改写过的任何文件。原回答留在原处,一动没动。

  • 它对任何一条回复都有效,不只最后一条。重新生成较早的一条,会把之后的轮次留在它们自己的对话里,原封不动。
  • 原回答永远不会被改写,理由和编辑过的消息一样:会话日志只会增长。
  • 新对话正在启动时,图标会旋转、按钮被禁用,所以第二次按不会开启第二个会话。

因为重新生成的回答落在一个既不共享谱系、也不共享消息 seq 的对话里,它被归档在铅笔写的那份版本记录下 —— 锚定在问题第一次被问起的那段对话。这就是它下方的箭头能通向那个问题收到过的每一个回答的原因,无论每一个是怎么产生的。


记忆树

右侧栏多出一个 记忆树 标签页。它逐轮画出对话记住了什么 —— 不是对话的谱系,而是每一个问题与那个问题收到过的每一个回答。

  • 流式 —— 通向你在读的这段对话的那串对话,随它运行实时生长。这是默认。
  • 完整 —— 版本记录连起来的每一段对话,所以在更早的对话里切出的分支也会画出来。

最新的在最上面。 树自上而下读,最近的轮次排在第一行;一段对话内部也是新的在上。

每个被记住的轮次是一个节点,显示完整的提问文本,不再缩略(标签、悬停提示与树中的全文都取自会话事件本身,而不是 Harness 那份只给 50 字的预览投影)。悬停它会显示那个问题以及它得到的回答:

  • 有回答的轮次是实心圆点;
  • 已经中断、没有回答的轮次是红色圆点(而不是蓝色)—— 蓝点只表示「此刻正在作答」;
  • 正在作答的轮次是蓝点。

在一个被问过不止一次的轮次下方,每个回答各挂一行,并标出它的来历 —— 原始回答、你改写了这个问题、或 不继承记忆重新提问 —— 以及它的位置:正在被阅读、就在本对话、或在另一段对话里。同一组 ‹ N/M › 箭头也出现在轮次旁边与回答块旁边,它们一起移动。

  • 点击一个回答就在原地读它,在它的轮次下方。
  • 点击回答旁边的 ↗ 会真正打开那段对话,先把它还原回会话列表。

右键菜单

在回答行(以及会话行)上点右键会打开一个小菜单:

  • 跳转到这段对话 —— 与 ↗ 相同。
  • 删除这个分支 —— 仅对分支回答出现(通过编辑问题或重新生成得到的那些),原始回答没有这一项。

删除做的是插件能做到的事,说清楚免得误会:Harness 没有任何删除会话的 API,会话日志是只能追加的,本插件也不碰磁盘。删除会把这条分支从版本记录里剔掉,于是它从树里和箭头里消失;那段会话本身保持归档,安静地留在盘上。要真正清掉数据,用 Harness 自己的会话管理。

数据来自 Harness 自己的 turnOutline 投影加上插件自己的版本记录 —— 它读的是会话列表已经知道的东西,不另建第二份日志。


环境要求

  • DeepSeek Harness 0.2.0-rc.2 或更新(package.json 里的 dsh.engines.dsh)。
  • 一个包含 Harness Web UI 与设置服务的 profile —— 也就是任何正常的 desktop 或 web profile。设置页需要 @deepseek-ai/dsh-settings 与 @deepseek-ai/dsh-config-editor,出厂 desktop 与 web profile 都已带有。
  • 至少一个可用的 LLM 提供方。插件调用 ctx.llm.stream(),所以 Harness 自己能谈的都能用。

安装

在这个仓库的检出目录里:

dsh plugin --profile <profile> add /absolute/path/to/dsh-input-optimizer

打好的 tarball 用法相同,而且那才是 profile 实际存储的形态:

pnpm pack
dsh plugin --profile <profile> add ./dsh-input-optimizer-1.0.0.tgz

然后重新加载应用(或者就等一会 —— Harness 会实时调和变化了的 dsh.profile.bundles 列表)并确认:

curl http://127.0.0.1:19387/plugins/input-optimizer/models

{"ok":true,"providers":[…]} 表示 Host 半侧已经活了。星形按钮会在下一次页面加载时出现。

卸载

dsh plugin --profile <profile> remove dsh-input-optimizer

在插件页面上保存的设置留在 profile 的 cordis.patch.yml 里一个 input-optimizer 条目下;卸载插件会留下那个条目,想彻底清干净可以手动删掉它。

不要先用 link: 安装再换成 tarball。 pnpm 的 link:(junction)安装把 profile 指向你的工作副本;替换它会删掉那个 junction,而 pnpm 会跟着重解析点走,把源目录清空。开发用的检出目录最好也以 tarball 安装,或者在一个用完就丢的 profile 上测试。

不要手工安装。 自己把包加进 profile 的 package.json 与 node_modules 会让运行中的进程持有一张过期的模块解析表,插件会以一个死行加载,且没有任何可见错误。让 dsh plugin add 去做。如果你已经手工改过,重启一次应用。


设置

打开 设置 → 输入优化器。页面通过 Harness 设置服务读写本插件的配置,所以改动会保存进 profile 的 cordis.patch.yml 并在重启后保留。

字段默认值用途
重写指令内置提示词每次重写都会送出的系统提示词。清空该字段并保存即可恢复默认。
跟随对话的模型开开:用当前对话选中的模型重写。关:始终用下面固定的模型。
提供方(空)关闭跟随时使用 —— 或作为跟随的模型不可用时的回退。
模型(空)同上。
推理强度low模型思考的力度。none 表示完全不发送该字段。列表读自所选模型自己声称支持的强度,所以你选不到它不支持的。
最大输出 Token4096单次重写的上限。0 表示本插件不设上限:请求不带 maxTokens 字段发出。
超时60000 毫秒单次重写的墙钟上限。
发送编辑用Ctrl+Enter发送一次编辑消息的快捷键。也提供 Ctrl+Shift+Enter、Alt+Enter 与 不用快捷键(只按按钮)。

内置的重写指令就是 它做什么 里的五目标提示词。它由 AI 写成,可以编辑,替换它正是重新瞄准这个优化器的正规做法。

开关类设置使用胶囊滑块而不是复选框,与 Harness 自己的开关一致:视觉状态以 aria-checked 为准,所以画出来的样子与辅助技术读到的状态不可能不一致。插件自己画这个控件,因为客户端插件不允许 require Harness 的客户端包。

模型解析

每次重写按下面的顺序解析一条路由,并在响应里报告用了哪一条:

  1. session —— 跟随对话的模型 打开、且当前对话选中的模型仍然可用时,用那个模型。
  2. config —— 在本页固定下来的提供方/模型。
  3. 失败 —— 一个 NO_MODEL 错误,告诉你要么为这次对话选一个模型,要么在设置里指定一个。

推理强度会对解析出的模型声称支持的强度做校验。如果模型不支持所请求的强度,请求会不带该字段发出,而不是直接失败。


HTTP 接口

Client 半侧通过 /plugins/input-optimizer 下的三条路由与 Host 半侧通讯。它们只接受同源请求。

路由方法Body / 查询结果
/optimizePOST{ text, session?: { provider, model } }{ ok, text, provider, model, routeSource, reasoningEffort, truncated }
/modelsGET—{ ok, providers: [{ id, name, models: [{ id, name }] }] }
/models/effortsGET?provider=&model=该模型声称支持的强度

铅笔背后的文件检查不走本插件:它用 Harness 自己的 workspaceFiles 服务(remote.workspaceFiles.stat),所以一条消息提到过的文件是由 Host 自己、在会话的工作区内取指纹的,不需要额外路由。

错误是 JSON:{ ok: false, error: { code, message } }。你可能见到的 code:

CodeHTTP含义
BAD_JSON400Body 不是合法 JSON。
EMPTY_INPUT400草稿为空或只有空白。
CROSS_ORIGIN403请求不是来自 Harness UI。
BODY_TOO_LARGE413Body 超过 512 KiB。
INPUT_TOO_LARGE413草稿超过 200,000 字符。
NO_MODEL400没有可用模型 —— 见 模型解析。
TIMEOUT504重写超出了配置的超时。
LLM_ERROR502提供方拒绝了。带有提供方自己的 llmCode。
MAX_TOKENS502回复在写完之前撞到了输出上限。
EMPTY_RESPONSE502模型没有返回任何可用的东西。

目录结构

dsh-input-optimizer/
├── package.json         manifest:exports、dsh.bundle、dsh.client、engines
├── cordis.patch.yml     插入行;它的 `id` 就是设置命名空间
├── icon.svg             四角星,用于插件列表
├── locale/
│   ├── en.json          插件列表里的标题/描述
│   └── zh.json
└── lib/
    ├── index.js         Host 半侧:Config schema、HTTP 路由、LLM 调用
    └── client.js        Client 半侧:星形按钮、铅笔、编辑器、
                         设置页、记忆树

lib/index.js 是 Host 半侧:用 schemastery 声明可配置字段,打开三条路由,并通过 ctx.llm.stream() 跑重写。lib/client.js 是 Client 半侧:把星形按钮注册进 conversation.input.right —— 紧挨 conversation.input.model 左侧的那个座位 —— 设置页注册进 settings.section,影子用户气泡注册进 conversation.chat.node,编辑器注册进 conversation.composer 链,重新生成/版本动作注册进 conversation.chat.assistant-actions,记忆树注册进右侧栏。它不把任何 Harness 包当作模块加载(遵守客户端规则),而是向 Harness 的 remote.workspaceFiles 服务询问提到过的文件,而不是自己开一条路由。

其中几处注册如果你要 fork 的话值得知道:

  • 消息铅笔遮蔽了 Harness 自己的用户气泡。 Harness 没有给用户消息留出放额外动作的槽位 —— 动作行硬编码在它的 UserMessageNodeView 里 —— 所以唯一的入口是以更低的优先级注册同一个 conversation.chat.node 的 key(user,以及同理的 steering)并画出整个气泡。最低优先级渲染,所以这能拿下那个格子;又因为抛错的槽位条目会被弃权(abdicate),影子里的 bug 会退回 Harness 的气泡,而不是留下一个洞。CSS 类名是插件自己的,气泡用 Harness 使用的同一批 token 重画,而不是 require 它的样式。
  • 编辑器通过 conversation.composer 链替换输入框。 这条链把真正的输入框留在下层挂载(data-chain-overlay-fallback),并挑选第一个 select 返回非 null 的条目,所以插件的编辑器只在它自己的 pending interaction 存在时出现,其余一切 —— 审批、提问、子代理 —— 都照常工作。
  • 重新生成按钮与版本箭头共用一个座位。 两者都住在 conversation.chat.assistant-actions,Harness 把它渲染在一条回复的动作行里、复制按钮与分支按钮之间。这个座位只给插件一个 prop —— 那条已完成回答的 messageId —— 所以插件从它走回轮次,再走回开启该轮次的用户消息,从而知道要重新问什么、以及把这个回答归档进哪份版本记录。

cordis.patch.yml 里的 id(input-optimizer)是承重的。Harness 设置服务按插件的profile 条目 id 来寻址它的 volatile 字段,所以 Client 半侧硬编码了同一个字面量,而不是 import Host 半侧去获知它。


开发

这个插件是一个普通的两半式 Harness bundle。它的 Host 半侧把 @deepseek-ai/schemastery 与 @deepseek-ai/dsh-llm 作为 peer dependency import;profile 提供它们。它的 Client 半侧声明 remote 与 remote.workspaceFiles(后者由 dsh-api-remotes 承载),好让 workspace-file 命名空间在插件激活之前就在模块图里。要在 profile 之外试验 Host 半侧,把本地 node_modules/@deepseek-ai/ 指向 Harness 安装自带的那几份。

# 用 Harness 运行时作为 Node 可执行文件,对两半侧做语法检查
ELECTRON_RUN_AS_NODE=1 "$DSH_INSTALL/DeepSeek Harness.exe" --check lib/index.js
ELECTRON_RUN_AS_NODE=1 "$DSH_INSTALL/DeepSeek Harness.exe" --check lib/client.js

# 打出 profile 安装用的 tarball
pnpm pack

Client 半侧由 Harness 的浏览器加载器加载,不是 Node:它用 window.__ModuleLoader__.load({ id, factory }) 把自己包起来,并且只能 require react 与 react/jsx-runtime。


AI 生成

这个仓库的每一部分都是 AI 助手应一位用户的请求生成的:

  • lib/index.js、lib/client.js —— 实现,包括所有注释;
  • icon.svg —— 四角星;
  • locale/en.json、locale/zh.json —— 插件列表文本;
  • cordis.patch.yml、package.json —— bundle 接线;
  • README.md、README.en.md —— 本文档及其英文版。

AI 没有从任何已有插件抄写。它读了 Harness 发布的插件作者技能、安装里附带的槽位与设置契约,以及已安装第三方插件的源码来学习惯例,然后写下这些。本文引用的关于 Harness 内部的事实都对着运行中的安装核实过。

在你依赖它之前请先审阅。它是针对 Harness 0.2.0-rc.2 写的,没有任何东西保证它跟得上之后的版本。

许可证

MIT © 2026 dsh-input-optimizer contributors

相关插件