dsh-enter-send
DSH 插件:在输入框工具行「权限」按钮右侧加一个发送键开关,可在 Enter 发送与 Ctrl+Enter 发送之间切换,另一个键自动变为换行(Send-key toggle chip for DeepSeek Harness)
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 19, 2026
- Updated
- Oct 19, 2026
Introduction
dsh-enter-send
给 DeepSeek Harness 的输入框工具行加一个发送键开关:放在「权限(工作区内修改)」控件的右侧, 点一下就能在 Enter 发送 与 Ctrl+Enter 发送 之间切换。

(截图来自 node tools/browser-check.mjs --shot,是本机 desktop profile 真实渲染的工具行:
+ / 工作区内修改 / 电脑操控:全自动 / ↵ Ctrl+Enter 发送 / … / 模型 / 发送。
conversation.input.left 是 list 席位,别的插件也可能在这条行里占一格——这里
dsh-computer-use 用 order 0 排在前面;本控件与相邻控件同高、同中线,左右间距就是工具行
自己的 column-gap(12px)。)
用法
| 操作 | 结果 |
|---|---|
点工具行里的 ↵ Enter 发送 | 弹出菜单:「发送方式」下两项,当前项带对勾 |
| 选 Enter 发送(默认) | Enter 提交(DSH 原生);Ctrl/Cmd+Enter 换行、Shift+Enter 换行 |
| 选 Ctrl+Enter 发送 | Ctrl/Cmd+Enter 提交(与按 Enter、点发送按钮同一条路径);Enter 换行、Shift+Enter 换行 |
两种模式都是「发送键之外的另一个键负责换行」:Enter 发送模式下是 Ctrl+Enter(以及 DSH 原生的 Shift+Enter),Ctrl+Enter 发送模式下是 Enter(以及 Shift+Enter)。
细节:
- 选择保存在浏览器
localStorage(键dsh-enter-send.mode.v1,v1 存{version:1,mode}),跨刷新保留; 换浏览器/清站点数据会回到默认,DSH 本身不受影响。 - 只在输入框正文里改写按键:队列里的小编辑器、重命名框、终端、其它 contenteditable 一律不碰。
- 输入法组字中的 Enter 原样放行;
Alt/AltGraph组合原样放行。 - 补全菜单(
/、@)已高亮候选时,Enter 仍然归菜单去选中候选,不会被改成换行。 - 长按不会连发:重放事件保留
repeat标志,交给 DSH 自己的「长按不重复发送」判断。 - 窄卡片(容器宽度 ≤ 460px)下与权限控件一样只留图标,不额外占宽。
- 默认值就是 Enter 发送:装上插件、不动开关,Enter 提交仍然完全是 DSH 原生行为 (唯一变化是 Ctrl+Enter 从 DSH 的「互补 Queue/Steer 提交」变成换行,见「已知限制」)。
安装(本机 desktop profile 已装好)
# link 方式装进 desktop profile(改代码即时生效;desktop profile 只能由桌面应用管理,
# 用应用里的「插件」面板安装,或在应用内执行等价的 pnpm 操作)
dsh plugin --profile <profile> add <本目录路径>
dsh plugin --profile <profile> remove dsh-enter-send # 卸载
装好后宿主会热加载(dsh-client-modules 增量扫描 Loader 条目),已经打开的窗口刷新一次页面(Ctrl+R)
即可看到新控件。
验证
node --test # 25 项单元测试:偏好读写/迁移、Enter 判定表(两种模式 ×
# 修饰键组合)、DOM 事实提取、捕获阶段改写与重放、注册契约、
# 宿主入口契约(无需浏览器)
node tools/verify.mjs # 宿主端 7 项:profile 清单/link/包契约 + 启动图收录 +
# bundle 可取回 + /api 诊断路由
node tools/browser-check.mjs # 真浏览器 20 项:席位/相邻/对齐/菜单/持久化/窄卡片折叠
# + 两种模式下 Enter 与 Ctrl+Enter 的真实按键行为
# (会发送 2 条测试消息)
node tools/browser-check.mjs --no-keys # 只跑非破坏性检查(不发送任何消息)
node tools/browser-check.mjs --shot # 额外把证据截图存到 docs/
三个脚本都用 ~/.dsh/.credentials.yaml 的 browser-session secret 现场铸签名 cookie(与浏览器同构),
不读取、不打印其它凭据;browser-check.mjs 用临时 --user-data-dir 起 headless Chrome,
与你正在用的窗口完全隔离,没有 Chrome/Edge 时会明确报错退出。
排障时可以在 DSH 窗口的 DevTools 控制台里看这几处:
localStorage.getItem('dsh-enter-send.mode.v1') // 当前选择
document.querySelector('.dsh-es-trigger')?.innerText // 工具行里的控件
document.querySelector('[data-slot="conversation.input.left"]') // 它注册到的席位
架构
DSH 插件是「宿主端 + 浏览器端」双半结构,本插件零 npm 依赖、零构建:
| 文件 | 角色 |
|---|---|
| src/client.js | 浏览器端 bundle(window.__ModuleLoader__.load 手写格式):chip UI、菜单、样式、Enter 改写 |
| src/index.js | 宿主端 Cordis 插件:让包成为 Loader 条目(浏览器 bundle 的发现前提)+ /api/dsh-enter-send.status 只读诊断路由 |
| cordis.patch.yml | 安装时自激活宿主端入口 |
| package.json | dsh.bundle.patch 自激活 + dsh.client.platform = web 浏览器 bundle 声明 |
| test/ | 单元测试(把 bundle 用 ModuleLoader 桩加载后直接断言) |
| tools/ | 验证脚本:verify.mjs(宿主)、browser-check.mjs(真浏览器,可 --shot 截图) |
界面:为什么用 conversation.input.left
@deepseek-ai/dsh-client-ui-conversation 的输入框工具行是固定结构(InputBar.tsx):
<div class="row">
<div class="tools"> ← 左组,gap 12px
[+] <div class="modes"> 权限控件 / 计划控件 </div>
{renderSlot('conversation.input.left', {})} ← 本插件在这里
</div>
<div class="trailing"> …模型 / 麦克风 / 发送… </div>
</div>
conversation.input.left 是 list 席位,渲染位置就在权限与计划控件之后、工具行内,
正是截图里红框那一格——不需要 DOM 注入,也不需要 portal。chip 的规格直接照抄
PermissionSelect.module.css 的触发器(28px 高、border-radius: var(--dsw-radius-sm)、
13/20 medium、gap:4px、padding:0 4px 0 8px、--dsw-alias-label-secondary、
悬停 --dsw-alias-interactive-bg-hover、14px 前置图标 + 16px 折叠箭头),
菜单用 ui-primitives 的 Menu(与权限控件同一个组件、同一套样式),所以外观与相邻控件天然一致。
行为:为什么是「捕获阶段改写 + 重放」
DSH 的输入框是 Lexical 编辑器,键位在 KEY_ENTER_COMMAND(critical 优先级)里判定
(ui-conversation/src/client/input/editor/keymap.ts):
if (event?.shiftKey === true) return false // Shift+Enter → 原生换行
if (handlers.arbitrate('enter', false) !== 'pass') … // 菜单高亮 → 归菜单
event?.preventDefault()
handlers.submit(event?.ctrlKey === true || event?.metaKey === true) // 其余 → 提交
本插件不调用任何 DSH 内部 API(也不 import 它的私有模块),而是在 document 捕获阶段先拿到按键, 把语义重放成等价的那一个:
| 选择 | 真实按键 | 重放成 |
|---|---|---|
| Enter 发送(默认) | Ctrl/Cmd+Enter | Shift+Enter(→ DSH 原生的换行路径;替代 DSH 原本的「互补」提交) |
| Enter 发送(默认) | Enter / Shift+Enter | 不拦,完全交给 DSH |
| Ctrl+Enter 发送 | Ctrl/Cmd+Enter | 普通 Enter(→ 与按 Enter、点发送按钮完全同一条提交路径) |
| Ctrl+Enter 发送 | Enter | Shift+Enter(→ DSH 原生的换行路径) |
重放用的是同元素上的合成 KeyboardEvent:Lexical 的 keydown 监听挂在编辑器根元素上、
不检查 isTrusted,所以合成事件与真实按键走同一条链路;repeat 标志跟着原事件走,长按不会连发。
补全菜单高亮时([role="listbox"][aria-activedescendant])Enter 交给 DSH 的菜单仲裁去选候选,
不会被改成换行。
与 DSH 的耦合点(都有降级)
| 依赖 | 变化时的表现 |
|---|---|
conversation.input.left 席位 | 席位消失 → ctx.slots.inject 等待/摘除,插件静默失效,不影响 DSH 其它功能 |
[data-composer-input] / contenteditable="true" | 改名 → 按键改写不生效(退化成「只有开关,没有行为」),控制台无异常 |
Lexical 的 KEY_ENTER_COMMAND 语义(Shift=换行、其余=提交) | 语义变化 → 重放结果跟着变;单元测试会先失败 |
[role="listbox"][aria-activedescendant] | 补全菜单改名 → 菜单打开时 Enter 会被改成换行(可接受的降级) |
localStorage 不可用(隐私模式) | 降级为「本次会话有效」 |
任何一处失败都只降级本插件自己;插槽渲染器会把崩溃的条目隔离在 [data-slot-error],不会拖累外壳。
已知限制
- 开关是浏览器本地偏好(
localStorage),不写宿主设置,所以同一台机器的另一个浏览器/另一份 profile 数据不会同步;DSH 官方设置里也不会出现这一项。 - 只覆盖输入框正文的 Enter:队列里那条小编辑器(Enter 保存)按自己的语义走,不受开关影响。
conversation.input.left是 list 席位:别的插件也在用(例如dsh-computer-use,order 0), 它们按各自order排列;本插件用 order 100,所以排在它们之后。想让开关更靠左(比如紧贴权限控件), 把src/client.js里ctx.slots.register({ … order: 100 … })的 order 调小即可。- 「Ctrl+Enter 发送」模式下,Ctrl+Enter 走的是普通提交语义(与 Enter 相同,遵守 DSH 自己的 队列/转向偏好),不是 DSH 原生的「互补」提交;这正是「Ctrl+Enter 就是发送键」的含义。
- 「Enter 发送」模式下,Ctrl/Cmd+Enter 被改成换行,因此 DSH 原生的「互补 Queue/Steer 提交」
(固定按键
fixed.complementary)在这个模式下不可用;需要它时切到「Ctrl+Enter 发送」即可 (那里 Ctrl+Enter 是发送键,Enter 是换行键)。快捷键帮助里由 DSH 自己登记的那条说明不会随之变化。 - 桌面端与 Web 端一致:Windows/macOS 桌面只把可配置快捷键拦在原生层,Enter/Ctrl+Enter 属于 固定动作,仍然走 DOM,所以本插件在桌面应用与浏览器里行为相同。
本机开发备注
.ref/是本机 DSH 内置包源码摘录与探查脚本(仅本地参考,已 gitignore,不随包发布); 摘录用dsh-remind里的tools/asar.mjs,源码可在E:\deepseek-harness查看。- 改了
src/client.js后:宿主会重新计算 bundle 版本(按文件 mtime/size),页面刷新即生效; 宿主端src/index.js改动需要重启 DSH(或插件页重载)。