dsh-enter-send
English | 简体中文
DeepSeek Harness(dsh)客户端插件:在 设置 → 常规 页面新增"发送快捷键"选项,重映射聊天输入框的发送 / 换行键位。
| 选项 | Enter | Ctrl/Cmd+Enter |
|---|---|---|
| Enter 发送(默认,保持 dsh 原生行为) | 发送 | 换行 |
| Ctrl+Enter 发送 | 换行 | 发送 |
- Shift+Enter 两种模式下都是换行(浏览器默认行为,不拦截)。
- 中文输入法(IME)组合输入中的 Enter 一律放行,不会误发。
- 默认值
enter下普通 Enter 行为与官方完全一致,升级无感知。 - 设置写入当前 profile 的配置文档(本插件条目
enter-send的mode字段),随桌面端 / Web 端各自的配置一起持久化。
安装
前置条件
- dsh
0.2.1-rc.2(web 平台):本版按该版本的客户端 API 复核;它和0.2.0-rc.2之间只有增量改动,两个版本都能跑。0.1.x不再兼容(原因见下方「兼容性说明」)。 - 桌面版自带这套运行时,直接可装;
- CLI / web profile 需先把
@deepseek-ai/dsh升到0.2.x一线——npm 上已发布0.2.1-rc.2;在0.1.x下面装本版会复现"装了不激活"。 - 从 git 安装时会运行
prepare脚本自动构建:构建工具链是 esbuild(npm 生态,只需 Node.js,随 devDependencies 自动安装);本机有 bun 时回退用 bun 构建。
方式一:桌面版(推荐,官方入口)
桌面版不是用 CLI 的 web profile,而是 Electron 自有的保留 profile $DSH_HOME/profiles/desktop——在 CLI 里装的插件桌面版看不到,而且 npm 版 dsh CLI 会拒绝针对 desktop profile 的插件管理请求。桌面版请走它自己的入口:
- 打开 设置 → 插件 → 添加插件;
- 在包名/地址输入框里粘贴本仓库地址:
github:Nalleyer/dsh-enter-send
想锁定版本可以写 github:Nalleyer/dsh-enter-send#;
3. 点「安装」,等它把依赖装好并执行 prepare 构建;
4. 重启桌面版(或按提示重载 profile),设置 → 常规 里会出现「发送快捷键」一行。
> 安装对话框会提示:插件装入后不会自动更新,升级需要先卸载再安装新版本。
方式二:CLI(web profile)
dsh plugin --profile web add github:Nalleyer/dsh-enter-send
git 安装拉取的是源码,不附带构建产物,因此 pnpm ≥ 10 在得到显式允许前会拒绝运行 prepare 构建脚本——首次 add 会失败,dsh 会打印修法:把 pnpm 提示的确切包键写进该 profile 的 pnpm-workspace.yaml:
allowBuilds:
dsh-enter-send: true
然后重新执行 add。注意:该授权允许包代码在安装时于你的机器上执行(不在沙箱内),只对源码可信的包授权,并建议锁定 commit。
本包声明了 dsh.bundle 清单,add 后加载行会自动写入 profile 的 patch 层,无需手动编辑;重启 dsh web 即可生效。
本地开发(--patch overlay,不写入 profile)
# 注意:--patch 必须放在转发给 web app 的未知选项(如 --port)之前:
# commander 遇到未知选项就将其及后续 token 当作位置参数,--patch 放后面
# 会报 unknown option;也不能写成父级形式(dsh --patch ... web)。
dsh web --patch ./cordis.patch.yml --port 8091
卸载
# CLI / web profile:
dsh plugin --profile web remove dsh-enter-send
# 桌面版:设置 → 插件 → 插件列表里卸载
卸载后插件条目残留的配置无害,可手动删除。
> 插件打包与安装的官方说明见 dsh 开发文档。
构建与测试
pnpm install # 安装开发依赖(esbuild、typescript、tsx、0.2.1-rc.2 类型包)
node scripts/build.mjs # 产出 lib/index.js(host 半)+ lib/client.js(浏览器半)
pnpm test # keymap 单测 + composer 契约回归 + 提交策略单测 + bundle 形态冒烟
pnpm typecheck # tsc --noEmit,按真实 0.2.1-rc.2 类型检查
测试用 node --import tsx --test(Node 24 自带 test runner,tsx 负责 TS 转译);pnpm test:bun 保留了 bun 入口。
工作原理
- 拦截层:
document捕获阶段 keydown 监听(addEventListener(..., true)),先于 React 委托的 composeronKeyDown。命中条件:目标是 composer 的可编辑区域(带data-composer-input、data-phase且contenteditable="true"的 Lexical 编辑器)、非 disabled、非 IME 组合(isComposing || keyCode === 229)。 - 发送:拦截后向 composer 编辑区域派发合成的无修饰 Enter
keydown(bubbles: true),冒泡到 React root 触发 composer 原生提交路径——草稿 / 附件 / 队列 / busy 仲裁全部复用,不重复实现。合成事件会再次经过捕获阶段,由同步标志防重入。 - 换行:向 composer 编辑区域派发合成的 Shift+Enter
keydown(bubbles: true),走 composer 自身的原生换行路径(KEY_ENTER 对 Shift+Enter 放行,Lexical 的 Enter 处理插入换行符),草稿同步与真实 Shift+Enter 完全一致。早期版本依赖的document.execCommand("insertText", "\n")在当前 Lexical composer 上已实测失效——返回成功但不派发beforeinput,Lexical 把未变化的 DOM 回滚,因此替换为上述合成 keydown 方案。 - 偏好持久化:浏览器半通过
ctx.configForms.get("enter-send")取本插件条目的配置表单(getSnapshot / subscribe / set),host 半导出Configschema(mode为 volatile 字段)并关掉自动生成的配置页(settings.configure({ auto: false }));选择先落到本地 snapshot store(键盘与设置行读它),再排队写回 Host 文档。
目录结构
├── package.json # dsh.bundle + dsh.client 清单(platform: web)
├── cordis.patch.yml # bundle 配置层(dsh.bundle.patch),行 id = enter-send
├── scripts/build.mjs # 构建脚本(esbuild + __ModuleLoader__.load 包装)
├── src/
│ ├── submission-settings.ts # 双端共用:条目 id、字段、模式常量与类型
│ ├── locales.ts # 中/英字典 + LocaleNamespaceMap 声明
│ ├── node/
│ │ ├── index.ts # host 半:导出 Config schema + 关闭自动配置页
│ │ └── contract.d.ts # 仅类型:引入 ctx.settings 声明
│ └── client/
│ ├── index.ts # apply 入口:设置行 + configForms + keymap + 样式
│ ├── submission.ts # 提交策略:本地 store + Host 表单读写
│ ├── EnterSendRow.tsx # 设置行组件(仿官方 EnterBehaviorRow)
│ ├── keymap.ts # 按键重映射核心(纯逻辑,可单测)
│ ├── styles.ts # 设置行样式(运行时注入 ,随卸载清理)
│ └── contract.d.ts # 仅类型:引入 slots / locale / settings 声明
├── lib/ # 构建产物(.gitignore,不入库)
└── test/ # keymap + composer 契约 + 提交策略 + bundle 冒烟
兼容性说明
本版是一次 API 迁移,不是清单微调。 0.1.2 版依赖的三个上游契约在 0.2.0-rc.2 上全部变了,旧版装上去会静默不激活(客户端 fiber 等不到 settingsScope 服务),表现就是"装了没反应":
| 0.1.2 写法(失效) | 0.2.x(本版) |
|---|---|
host 侧 ctx.settings.register(ns, schema) |
服务只提供 configure / describe / update / replace / mutate;改为导出 Config schema(profile 条目自带配置) |
客户端 inject 含 settingsScope |
inject = ["slots", "locale", "configForms"],偏好走 ctx.configForms.get(entryId) |
| — | 服务访问按 inject 门禁:ctx. 未列入 inject 即抛 cannot get property "…" without inject,注册字典的 ctx.locale.register() 因此必须声明 "locale" |
settingsScope.bind({ namespace }).set(field, v) |
ctx.configForms.get(id).set(field, v),读取用 getSnapshot()/subscribe() |
| — | 设置行注册需要 store / inject / locale;hooks 隔间 → useXxx 选择器 Hook |
IconChevronDownOutline14 |
IconChevronDownOutlineRegular(图标命名改为 Regular/Medium) |
仍成立的契约(已逐项复核 0.2.1-rc.2 产物):
- 设置行插槽:
settings.general.item(列表型,id+order+locale+inject)与官方EnterBehaviorRow(id: composer-enter,order: 20)同构;本插件用order: 21紧随其后。 - 条目 id 对齐:
configForms.get()的 key 是 profile 里的插件条目 id,因此cordis.patch.yml的插入行写成id: enter-send,与客户端里的SETTINGS_NAMESPACE一致。若将来安装器改用别的行 id,改这一处即可(src/submission-settings.ts)。 - composer DOM 标记:
data-composer-input(ComposerContentEditable)与data-phase(InputBar以input?.phase ?? "inert"透传)仍落在同一个 contenteditable div 上,contenteditable随editable、aria-disabled随禁用态同步——isComposerTarget/findComposerTarget无需调整。上游若拆开这两个属性或改名,需同步更新。 - composer 键位:
registerComposerKeymap对shiftKey === true的 Enter 仍返回false(放行给 Lexical 原生换行),其余 Enter 提交;因此合成 Shift+Enter 换行、合成无修饰 Enter 发送两条路径都仍然成立。上游若改动 Enter / Shift+Enter 的处理,需同步更新newlineViaCursor/sendViaComposer(见src/client/keymap.ts)。 - 客户端模块表:
lib/client.js以require取用的@deepseek-ai/dsh-client-store与@deepseek-ai/dsh-client-ui-primitives在 shell 的静态模块表内,且已写入package.json的dsh.client.inject;两者一致性由test/bundle-smoke.test.ts守住。
0.2.1-rc.2 复核结果
按桌面版 0.2.1-rc.2 的实装产物(app.asar 内 dsh/package.json 版本 0.2.1-rc.2,cordis 4.0.5,schemastery 3.18.5)逐项复核,并用 npm 上真实的 0.2.1-rc.2 类型包做了 tsc --noEmit:插件代码无需改动,只补了一处健壮性处理。
- 依赖实现没变:
dsh-client-store、dsh-client-locale、dsh-settings的lib输出与 0.2.0-rc.2 逐字节相同;schemastery(3.18.4 → 3.18.5)与cordis(4.0.4 → 4.0.5)只有版本号与依赖范围变化,lib相同。 dsh-client-ui-slots/dsh-client-ui-renderer只有增量:新增inline插槽选项与 keyed hook 的member形参,本插件用的register/inject/hooks形态不变。dsh-client-ui-settings移除了"浏览器本地 memory 表单模式"(ConfigFormSnapshot.mode、ConfigForms.persistence):本插件只读getSnapshot().value、写set(field, value),不受影响。副作用是所有客户端表单写入都跨网络(set()在传输失败时 reject),因此setMode给写入补了.catch(),避免断线/被拒时冒出 unhandled rejection。- composer 契约不变:
data-composer-input+data-phase仍由DraftEditor透传到同一个 contenteditable div,KEY_ENTER仍对shiftKey === true放行、其余走submit(ctrlKey || metaKey);isComposing || keyCode === 229的组合输入判断也没变。 - 设置行插槽未变:0.2.1 官方
settings.general.item的 order 为-20/0/10/11/12/15/16/17/20/30/90/100,本插件的21仍紧随官方composer-enter(order 20,管的是 busy 时 Enter 是排队还是打断),无冲突。 - 客户端投递契约未变:仍是
window.__ModuleLoader__.load({ id, factory }),bundle 也只requireshell 静态模块表里的@deepseek-ai/dsh-client-store与@deepseek-ai/dsh-client-ui-primitives(Menu、IconChevronDownOutlineRegular都还在表内),因此不需要dsh.client.external。 - 清单字段未变:
dsh.bundle.patch、dsh.client.platform、dsh.client.inject照旧读取;0.2.1 的类型注释明确inject是"包名依赖信息,不是 Cordis 服务注入"。本版顺带补了dsh.manifestVersion: 1与engines.dsh,把兼容范围写成声明(当前 dsh 不强制校验这两项)。
License
MIT