READMESource: master@cf6bd217
正在开发中,还不能使用
dsh-hotkeys-platform
DeepSeek Harness(DSH)Web 插件:快捷键设定平台。
把「每个插件各自拦截键盘、各写各的设置」升级为一个注册表服务 ctx.hotkeys:
- 任何插件通过
ctx.hotkeys.registerAction(...)注册「动作 → 处理器」; - 动作的默认键、触发范围(scope)、描述由插件声明;
- 用户的显式改键统一持久化在平台;
- 设置页(DSH 设置 → 快捷键)与
/hotkeys命令提供统一配置。
架构与 dsh-better-sidebar 同构:平台 ctx.provide('hotkeys', service),业务动作由消费插件注册(吃狗粮),能力对等。
使用示例:docs/example.md
快速开始(消费插件)
export const inject = ['hotkeys']
export function apply(ctx: Context): void {
ctx.effect(() =>
ctx.hotkeys.registerAction({
id: 'my-plugin:send-message',
title: () => t('发送'),
description: '发送当前消息',
group: '消息',
scope: 'input',
defaultKey: 'Control+Enter',
handler: (event, h) => {
// 命中后的处理;返回 false 表示「不消费,放行给 DSH 原生行为」。
h.dispatchEnter(h.target ?? event.target)
},
})
)
}
要点:
inject = ['hotkeys']让 Cordis 在平台就绪后才激活你的插件(ctx.hotkeys恒存在);registerAction返回 disposer,必须包在ctx.effect(...)里(HMR-safe);id建议包前缀(my-plugin:xxx),避免冲突。
关键点说明
| 点 | 说明 |
|---|---|
ctx.get('hotkeys') + 判空 |
平台是可选项,读不到就跳过;不要用 inject 之外的 ctx.hotkeys 直接访问 |
ctx.effect 包裹 |
registerAction 返回 disposer,fiber 卸载(HMR/禁用)时自动撤销注册,否则下次激活抛 already registered |
id 用包前缀 |
dsh-better-archive:open-archived,避免与别的插件冲突 |
scope: 'global' |
任意处可触发;若动作只在输入框有意义,改 scope: 'input';提问卡内用 question-card;精确判定用 custom + target(el) |
handler 返回 false |
表示「不消费这次按键,放行给 DSH 原生/后续动作」——例如你想让某键只在特定条件下生效,条件不满足时 return false |
需要 host 能力的动作
如果某个快捷键动作要触发 host 侧能力(读写文件、执行命令、归档会话等),平台不承载这些逻辑——由你的插件自带 host 路由,handler 里 fetch 即可。例如 better-archive 的「清空全部归档」动作可以这样写:
handler: async () => {
await fetch('/archived/delete-all', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ confirm: true }),
})
}
这正是平台「host 仅可选扩展」的设计:host 半只留一个可选的
hotkeys设置命名空间,业务 host 能力一律由各插件自己的/路由提供。
验证
- 重启
dsh web(peer 依赖新增需重启),浏览器强刷; - 打开「设置 → 快捷键」,应看到「归档」分组下出现「打开已归档」动作,当前键显示
Ctrl+Shift+A; - 在任意界面按下
Ctrl+Shift+A,应跳转到「已归档」设置页; - 点「录制」把键改成别的组合键,或点「禁用」整体关闭。
动作描述符
| 字段 | 说明 |
|---|---|
id |
唯一 id,建议包前缀 |
title |
标题(字符串或 () => string,i18n 友好) |
description |
描述,显示在设置页 |
group |
分组(设置页分区展示) |
defaultKey |
默认组合键,如 'Control+Enter';空串 = 默认不绑 |
scope |
global / input / question-card / command-palette / custom |
target |
scope='custom' 时的精确判定 (el) => boolean |
handler |
`(event, ctx) => void |
hidden |
设置页隐藏(默认 false) |
order |
排序(升序,默认 100) |
内置 scope:
| scope | 判定 |
|---|---|
global |
任意处 |
input |
可编辑文本区(textarea / input[type=text]) |
question-card |
DeepSeek 提问卡([data-question-key])内 |
command-palette |
命令菜单/下拉打开时 |
custom |
自定义 target(el) 判定 |
Handler 上下文 ctx:target(事件目标)、dispatchEnter(el)(合成裸 Enter,防重入)、insertTextAtCaret(el, text)、hasOpenOverlay()、isEditableTarget(el)。
服务 API
interface HotkeyService {
registerAction(d: HotkeyActionDescriptor): () => void
getActions(): readonly HotkeyActionDescriptor[]
getBindings(): Readonly<Record<string, string>> // 只存用户改过的
effectiveKey(actionId: string): string | null // 用户绑定 ?? 默认 ?? null(禁用)
setBinding(actionId: string, combo: string): void // combo='' 表示显式禁用
clearBinding(actionId: string): void // 删除绑定,回落 defaultKey
record(actionId: string): Promise<string | null> // 录制
subscribe(listener: () => void): () => void
readonly features: readonly string[] // 'record' | 'subscribe' | 'effectiveKey' | 'clearBinding'
readonly version: string
}
持久化与语义
- localStorage 键
dsh.hotkeys.v1,只存用户显式改过的{ actionId: combo }; - 三态:未出现(回落
defaultKey)/ 具体串(用户绑定)/''(显式禁用,连默认也关掉); - 迁移:首次启动自动从旧
dsh-hotkeys的dsh.sendkeys.v1迁非默认绑定(保留旧键)。
安装
dsh plugin --profile web add dsh-hotkeys-platform
# 或本地开发
dsh plugin --profile web add link:<本仓库路径>
重启 dsh web(host 改动需重启;client 改动浏览器强刷)。
如何配置快捷键(用户教程)
入口一:设置页(推荐)
- 打开 DSH 设置(左下角齿轮图标)。
- 左侧导航选择「快捷键」分区。
- 页面按分组列出所有插件注册的动作,每个动作显示标题、描述、动作 id 与当前生效键。
- 配置操作(可视化点选 + 键盘录制,两条路写同一份绑定):
- 编辑(可视化):点动作右侧「编辑」展开组合键编辑器——
- 修饰键:勾选
Ctrl/⌘/Win/Alt/⌥/Shift(多选); - 主键:下拉选常用键(Enter、Space、Tab、Esc、方向键、F1-F12…),或直接输入任意键名;
- 预览实时显示拼出的组合键,点「保存」生效,「清除」回到默认键;
- 好处:能配「浏览器保留键」(如
Cmd+W、Ctrl+T录不到但可手工拼)、能精确微调单个修饰键。
- 修饰键:勾选
- 录制(快捷):点「录制」→ 提示「请按下新的组合键」→ 直接按键即完成。
Esc取消;15 秒无操作自动取消。 - 禁用:点「禁用」,该动作完全关闭(连默认键也不再触发)。
- 已改过的动作会出现「禁用」按钮;未改过的动作使用默认键。
- 编辑(可视化):点动作右侧「编辑」展开组合键编辑器——
入口二:/hotkeys 命令
- 在消息输入框输入
/hotkeys。 - 弹出菜单列出所有动作及当前键。
- 选「🎙 录制「xxx」键」→ 按下组合键。
组合键语法
- 格式:修饰键 + 主键,用
+连接,如Control+Shift+Enter。 - 修饰键别名(任选一种写法):
| 修饰键 | 别名 |
|---|---|
| Ctrl | Ctrl / Control / ⌃ |
| Meta | Meta / Cmd / Command / Win / ⌘ |
| Shift | Shift / ⇧ |
| Alt | Alt / Option / ⌥ |
- 单字符主键自动大写;空格键写作
Space。
键位三态
| 状态 | 含义 |
|---|---|
| 未改过 | 使用动作声明的默认键(defaultKey) |
| 已录制 | 使用你设置的键 |
| 已禁用 | 完全不触发 |
冲突说明
当多个动作绑定了同一个组合键,先注册的动作优先触发;冲突的动作在设置页会红色高亮,请改绑其中一个避免歧义。
构建与测试
pnpm install
pnpm typecheck
pnpm test # vitest 单测(combo / scope / store / dispatcher)
pnpm build # tsc 声明 + tsdown(host ESM + client.js + client-registry.js)
目录
src/
├── index.ts # host half(可选设置命名空间,其余留空)
├── combo.ts # 组合键解析/匹配/描述(纯函数)
├── scope.ts # 内置 scope 判定(纯函数)
├── store.ts # bindings 持久化 + 迁移(纯函数)
├── context-types.ts # HotkeyService 类型 + Context 增补
└── client/
├── index.tsx # apply:ctx.provide + settings.section + /hotkeys 命令
├── service.ts # HotkeyService 实现工厂
├── dispatcher.ts # 全局 keydown 分发(纯决策可测)
├── record.ts # 录制
├── dom-utils.ts # overlay/合成 Enter/toast
└── HotkeysSettingsPanel.tsx # 设置页
License
MIT
No comments yet. Be the first to write one.