DeepSeek Harness 插件

omdsh-shortcuts

Bind a chord to anything the harness can do: a menu the desktop shell renders natively, a switchboard the runtime dispatches through, and an in-page listener for the web — one document, two surfaces(英文原文)

跳到安装方式

来源信息

GitHub 仓库
omdsh-plugins/omdsh-shortcuts
最近更新
2026年8月20日
分类
工具与能力
GitHub stars
0
载体类型
plugin
目录证据
上游声明已找到 dsh.bundle
证据路径
package.json#dsh.bundle
核对版本
0.1.0-rc.8
上游核对日期
2026-08-20

该证据由上游目录提供。本站没有安装、运行或安全审核这个插件。

安装

默认先复制一段 Prompt,让 Agent 读 GitHub 仓库和源码;需要自己装时再切到命令。

复制这段 Prompt,发给 DSH、Codex 或其他 Agent,让它先读 GitHub 仓库和源码。

请先不要安装或执行任何命令。阅读这个插件的 GitHub 仓库、README 和关键源码,然后用清楚、直接的方式回答以下问题,帮助我判断它是否适合我的需求:

1. 这个插件是什么,解决什么问题;
2. 适合哪些用户和典型使用场景;
3. 安装后如何使用,并给出一个最小使用示例;
4. 有哪些已知限制,以及隐私、安全、兼容性或维护风险;
5. 给出“推荐 / 有条件推荐 / 不推荐”的明确建议和理由。

请区分仓库明确说明、根据源码推断和未知信息。证据不足时请明确说明,不要猜测或照抄 README。

GitHub:https://github.com/omdsh-plugins/omdsh-shortcuts
插件名:omdsh-shortcuts
作者:omdsh-plugins

检查来源文件

安装前先看这个插件目录里的 README 和其他文件。

文件资源管理器4 个文件
README.zh.md来源说明 · 只读预览
README 语言

omdsh-shortcuts

English | 中文

用一份文档,为 DeepSeek Harness 能做的任何事绑定组合键——两种形态通吃。

挂载这个插件,菜单出现、一组键开始生效;卸载它,两者一起消失;改它的配置,两者就地重建。这些都不需要重新构建或重启桌面外壳,也都不是对 harness 的改动。

它提供什么

界面从哪来
桌面外壳的原生菜单,以及上面的每一个加速键这份文档由 GET /api/desktop/menu 提供,每次修订时再经 GET /api/desktop/menu.events 推一遍
一次原生按键,送达真正在前台的那个页面外壳经 POST /api/desktop/menu.invoke 交回,再由 GET /api/desktop/shortcut.events?client=<id> 下发给最近报告过焦点的客户端
Web 端的页内组合键——那里没有菜单来认领它们浏览器半边自己的按键监听器,按同一份文档绑定这一形态的组合键
运行时和页面里都有的 shortcut 服务两个半边各自 ctx.reflect.provide('shortcut', …)registerbindings()onBindingschordLabel
十五个界面命令里的十二个,由它执行[src/client/builtins.ts](src/client/builtins.ts),调用 layoutsessionsworkspacessessionModes——另外三个归拥有它们的插件管
新窗口从空白开始浏览器半边用掉 ?new=1,那是 omdsh-desktop 给刚打开的窗口带上的地址参数
每个快捷键都写在执行它的那个按钮的 tooltip 里,harness 自带的按钮也算[src/client/hints.ts](src/client/hints.ts):把组合键追加进 ui-primitives 本来就会弹出的那块 tooltip;它不弹的地方,由本包自己画一块
插件中心里的改键表单omdsh-shortcuts 这个 settings 命名空间:一张扁平的 id → 组合键 字典,加上一个提示开关,即时生效

核心想法

一次按键,起点是键盘所在的地方,终点是命令真正住着的地方——而这是三个不同的地方:

command.kind由谁执行怎么到达
shellElectron 主进程原生菜单加速键
runtime宿主插件,在 Node 运行时里本插件的交换机
browserUI 插件,在页面里本插件的浏览器半边

插件只说自己能做什么;哪个组合键够得到它——或者当前形态下究竟有没有键——是配置。于是,想让 ⌘L 唤出侧边输入框的人只需要改一份文档,不必去查是哪个插件把键写死了。

两种形态,一份文档

同一个运行时会同时服务桌面窗口和浏览器标签页——用一次「在浏览器中打开」就正好造出这一对——而两者听见按键的方式并不一样。

  Web 端    ⌘K ─→ 本页面的监听器 ────────────────────→ handler
  桌面端    ⌘K ─→ Electron 菜单 ─→ 运行时 ─→ 本页面 ─→ handler

桌面端的组合键在页面存在之前就被原生层认领了,所以一次按键要走完全程:外壳把 id POST 给运行时,运行时再把它交给当前在前台的浏览器客户端。Web 端没有菜单,也没有原生认领,页面自己听见按键、自己跑 handler,谁也不用问。

这个差别不归配置管,所以绑定也不总能通用。浏览器把 ⌘N⌘T⌘W⌘Q 留给自己——页面根本不会被问到,preventDefault 也没有东西可拦。CmdOrCtrl+Nnew-window 当原生绑定完全没问题,标签页却永远拿不到这个键。

在桌面端,这个组合键会打开一扇新窗口。外壳给地址带上 ?new=1,因为不这样,harness UI 就会从 origin 级的 localStorage 里恢复上次选中的会话——同一 origin 下每扇窗共用那一格。页面在挂载时用掉这个参数:赶在会话列表到达、history 窗口打开之前,清掉恢复出来的选择,再把 new 从地址里去掉——这样这一扇窗再刷新就是一次普通的恢复。它不会自己开一场对话;开对话的仍然是 ⌘K。

webAccelerator 就是允许两边各说各话的地方:

webAcceleratorWeb 端的效果
不写accelerator 同一个键
写字符串改用这个键
null这里完全不绑键;菜单项还在,鼠标仍可达

webAccelerator 里写一个浏览器保留键,会在挂载时报错,而不是变成一个默默不响应的键:在标签页里讨要 ⌘W,是页面根本无法兑现的请求,拒绝是唯一诚实的回答。只写原生 accelerator 则不算错——Web 端只是把这条绑定报告为 unreachable,设置界面可以据此显示「仅桌面端」。

它持有的路由

通过 webServer 服务上的 ctx.effect 注册,所以卸载插件就会移除这些路由,外壳退回平台自带的菜单底座。

路由谁读它
GET /api/desktop/menu任何人;文档,读一次
GET /api/desktop/menu.events外壳;连接时给文档,此后每次修订都推
POST /api/desktop/menu.invoke外壳,把自己不执行的项交回来
GET /api/desktop/shortcut.events?client=<id>浏览器客户端;绑定表,以及它自己听不见的那些按键
POST /api/desktop/shortcut.focus浏览器客户端,声明自己在前台

外壳的流和客户端的流是刻意分开的。外壳那条流的载荷是一个裸文档——每个已发布的桌面端构建解析的都是这个形状;给它加帧、让它同时承载调用,会让所有已安装外壳的菜单栏变空。

桌面端的一次按键给谁

给最近一次报告过焦点的那个客户端。客户端通常不止一个——多个窗口、多个标签页,或者两者同时连着同一个运行时——而「人正在看的那个界面」是唯一永远正确的答案。

焦点靠上报,不靠推断,因为别人谁都看不见:外壳知道被按下的菜单属于哪个窗口,但它的窗口刻意不带 preload、保持沙箱,没有任何通到页面里的通道;一个 HTTP 请求也完全说不出人的注意力在哪。所以只能由页面自己来说,并且一直说。

文档

{
  version: 1,
  items: [
    {
      id: 'new-window',
      label: 'New Window',
      section: 'file',                              // app | file | view | window | help
      command: { kind: 'shell', name: 'new-window' },
      accelerator: 'CmdOrCtrl+N',                   // 仅原生;标签页永远拿不到 ⌘N
    },
    {
      id: 'sidechat.open',
      label: 'Ask Here',
      section: 'view',
      command: { kind: 'browser' },
      accelerator: 'CmdOrCtrl+L',
      webAccelerator: 'CmdOrCtrl+Alt+L',            // 浏览器保留 ⌘L,所以 Web 端换 Alt
    },
    { id: 'say-hello', label: 'Say Hello', section: 'help', command: { kind: 'runtime' } },
  ],
}

shell 只能点名主进程会执行的那套固定词汇——new-windowrestart-runtimereveal-logopen-in-browsertoggle-idle-suspend。这份清单是契约里唯一插件扩不了的部分,因为扩它就意味着要发一个新的 Electron 构建。凡是用户能随意绑键的,都是 runtimebrowser

checkbox: true 的项会渲染成复选框,但它的状态属于外壳而不属于这份文档:外壳构建菜单项时读的是自己存的设置,所以重建不会让勾选状态和它所描述的东西脱节。

以下情况在挂载时就会被拒绝,而不是照常发布:两项共用一个 id、两项争同一个原生组合键、两项在页面里绑同一个键,以及 webAccelerator 写错了或写了浏览器保留键。

注册一个命令

在运行时里注册的是 runtime 命令。服务按名字解析,不走环境里的 ctx.shortcut,因为本包的两个半边编译成同一个程序,只有浏览器半边扩展了 cordis 的 Context

shortcut 要在 apply 里面拿,绝不能写进顶层 inject:本插件在不在 profile 里,取决于使用者一条条 dsh plugin add;而 cordis 对被注入的服务会无限期 等下去,顶层写了它的 entry 会一直停在 pending,两道启动审计会把整个 页面判为失败。在 apply 里启动的 fiber 不是 loader entry,等多久都不要紧。

export function apply(ctx: Context): void {
  ctx.inject(['shortcut'], (sctx) => {
    const shortcut = sctx.get('shortcut') as unknown as IShortcut | undefined
    // 当这个名字由一个未激活的 fiber 提供时,这里是可达的。
    if (shortcut === undefined) return
    sctx.effect(() => shortcut.register('say-hello', () => { /* ... */ }))
  })
}

在浏览器里,注册 browser 命令:

export function apply(ctx: ClientContext): void {
  ctx.inject(['shortcut'], (sctx) => {
    if (sctx.get('shortcut') === undefined) return
    sctx.effect(() => sctx.shortcut.register('sidechat.open', () => { panel.open() }))
  })
}

把 effect 挂在 sctx 上而不是 ctx 上,这样本插件在运行时被卸载时,注 册也跟着撤回。

注册不等于认领任何键。文档从未声明的命令照样能注册,只是永远不会触发——对一个挂在没提到它的配置上的插件来说,这正是该有的结果。ctx.shortcut.bindings() 报告每个命令在当前形态下的真实状态,包括哪些在这里没有键、为什么没有;ctx.shortcut.onBindings(fn) 在每次文档修订后回调,所以要显示组合键的界面(tooltip、设置行)能跟着改绑走,不用刷新。

注册要挂在一个受限 fiber 上,而不是写进插件自己的 inject 列表——否则没有键盘层的组装会连功能本身一起丢掉,而不只是丢掉快捷键:

ctx.inject(['shortcut'], (sctx) => {
  const shortcut = sctx.get('shortcut') as unknown as IShortcutClient
  sctx.effect(() => shortcut.register('panel.files', () => { geometry.toggleRight() }))
})

让按钮把快捷键教给人

一个用鼠标找到的功能,应该能让人从此不用鼠标。所以按钮在 tooltip 里显示自己的组合键,取的就是 chordLabel

// "打开文件面板 · ⇧⌘E",在浏览器标签页里则是 "打开文件面板 · ⌥⌘E"
const chord = shortcut.chordLabel('panel.files')
const hint = chord === undefined ? t('files.open') : `${t('files.open')} · ${chord}`

这三件事由 chordLabel 一并处理,界面不用各自重做:

  • 拼法按平台走——mac 上是 ⇧⌘E,别处是 Ctrl+Shift+ECmdOrCtrl+Shift+E线上拼法,把它原样印出来是在教配置格式,不是在教按键。
  • 形态说了算——桌面端显示原生键 ⌘1,标签页里显示 ⌥⌘1,因为那才是各自真正能收到的键。
  • 没有键就返回 undefined——所以 tooltip 落回纯标题,而不是留一个后面什么都没有的分隔符。浏览器把键占走的那些命令,在标签页里就是这个状态。

配合 onBindings 使用:文档是推下来的,所以第一次读通常是空的;改绑之后也要跟着变。仓库里 omdsh-sidepanel 的两个面板开关、omdsh-chatmodeomdsh-codemode 的模式段都是这么做的。omdsh-sidechat 的召唤图标走的是更底一层的路——自己读 bindings(),再把 claim 格式化出来,因为它要知道这个键由谁持有,而不只是怎么印出来。

已经自己绑了键的 UI 插件,把键让出来即可——setSummonChord(null) 就是这个协议——然后改为注册一个命令。这样一来,就不会出现两个 handler 抢同一次按键的情况。仓库里有两个现成的例子:

  • omdsh-sidepanel 把两个面板交出来。它本来就没绑键,所以只是注册。
  • omdsh-sidechat 交出的是一个已经在用的键。它在受限 fiber 里 setSummonChord(null) 并注册 sidechat.open,然后用 onBindings 把文档发给这个命令的键读回来喂给 tooltip——交出按键不该意味着从此不再教人按什么。fiber 卸载时它把内置的 CmdOrCtrl+L 还回去,所以移除键盘层不会顺手把召唤功能一起移除。

harness 自带的按钮,从外面教

New Session、会话搜索、添加工作区、设置、折叠侧栏,这几个按钮做不到上面那件事:它们的组件住在本仓库不改的包里,而从 harness 外面来的键盘层,本来就不是它们被写出来时会去问的东西。所以提示由本插件挂上去,鼠标悬停时读到的是 New session · ⌘K

两套机制,用哪一套是当场判断出来的,不是配置出来的:

harness 自己弹了什么会发生什么
它自己的 tooltip组合键追加到那一块上——仍然只有一块,位置、动画、边界收拢都由拥有它的代码负责,只是结尾多了 · ⌘K
什么都没有(设置入口,以及侧栏展开时的 New Session)由本插件画一块,样式取自同一套主题变量

一次悬停会先等 600ms——晚于 harness 自己的 500ms 悬停延迟——再做判断,所以有 tooltip 的按钮永远是被追加,绝不会出现两块。写进去的只有追加的元素,绝不改动 React 已经放在那里的文本——那是外部代码写入 React 子树唯一安全的方向。卸载插件时,追加的组合键和那块自绘的提示一并撤走。

按钮是靠它本来就戴着的名字认出来的。aria-label,和按钮自己查过的同一条词典项比较——locale.bind('sidebar')('session.new.label')——也就是问 harness 自己怎么称呼这个按钮,而不是去刮一个渲染出来的字符串。语言切换天然跟随;万一某个 key 被改名,代价是提示消失,而不是提示出错。有两个控件,名字不是一条可查的词典项——设置入口的名字来自一个 slot,设置页那一行的名字来自它自己的注册——它们按 [src/client/anchors.ts](src/client/anchors.ts) 一贯的方式定位:设置入口用它内部的 slot 出口,设置对话框的 Plugins 行用 slot 账本里的次序。类名、可见文本、渲染顺序一概不用。

组装自己加的命令,可以指明它属于哪个按钮,这个选择器优先于本包内置的一切:

- id: omdsh-shortcuts
  config:
    items:
      - id: deploy.run
        label: Deploy
        section: view
        command: { kind: runtime }
        accelerator: CmdOrCtrl+Alt+D
        anchor: '[data-slot="conversation.session.header.actions"] button'

整套行为就是一个设置项 hints,默认开启,即时生效。关掉它,每个组合键照旧绑定,每块 tooltip 也照旧是 harness 自己写的那样。

默认项

外壳层:shell 命令

id组合键分区
New Windownew-windowCmdOrCtrl+Nfile
Restart Harness Runtimerestart-runtimeCmdOrCtrl+Alt+Rview
Open in Browseropen-in-browserCmdOrCtrl+Shift+Oview
Reveal Runtime Logreveal-logCmdOrCtrl+Shift+Lview
Release Memory When Idleidle-suspendCmdOrCtrl+Alt+Mapp

改键时写的是 id,而 idle-suspend 是唯一一个和命令名对不上的:菜单项是 idle-suspend,它向外壳要的能力叫 toggle-idle-suspend,它也是这一组里唯一的复选框。

五项全是 shell 命令,所以全都只在桌面端有效:标签页里没有 Electron 可让组合键抵达。分三个层级是为了让这份映射好记——单修饰键是标准窗口操作,Shift 通向外壳的界面或目的地,Alt 通向运行时进程,也就是 Electron 自己放开发者工具的那一层。整份映射刻意避开可打印字符,因为窗口里的 harness 界面占有着菜单不用的每一个键——下面这一层花的正是这些键。

界面层:browser 命令

id桌面端Web 端由谁执行
New Sessionsession.newCmdOrCtrl+K同左本插件
Fork Sessionsession.forkCmdOrCtrl+Shift+K同左本插件
Archive Sessionsession.archiveCmdOrCtrl+Shift+W本插件
Add Workspaceworkspace.addCmdOrCtrl+OCmdOrCtrl+Alt+O本插件
Remote Connectremdev.connectCmdOrCtrl+Shift+CCmdOrCtrl+Alt+Comdsh-remdev
Search Sessionssession.searchCmdOrCtrl+Shift+F同左本插件(DOM)
Toggle Sidebarsidebar.toggleCmdOrCtrl+Shift+BCmdOrCtrl+Alt+B本插件
Toggle File Panelpanel.filesCmdOrCtrl+Shift+E同左omdsh-sidepanel
Toggle Terminalpanel.terminal`Ctrl+ ``同左omdsh-sidepanel
Toggle Details Paneldetails.toggleCmdOrCtrl+Shift+DCmdOrCtrl+Alt+D本插件
Ask Heresidechat.openCmdOrCtrl+LCmdOrCtrl+Alt+Lomdsh-sidechat
Chat Modemode.chatCmdOrCtrl+1CmdOrCtrl+Alt+1本插件
Work Modemode.workCmdOrCtrl+2CmdOrCtrl+Alt+2本插件
Code Modemode.codeCmdOrCtrl+3CmdOrCtrl+Alt+3本插件
Settingssettings.openCmdOrCtrl+,CmdOrCtrl+Alt+,本插件(DOM)
Plugin Settingssettings.pluginsCmdOrCtrl+Shift+PCmdOrCtrl+Alt+P本插件(DOM)

Web 端那一列只有一条规则:把 Shift(或者什么都不带)换成 Alt 需要换的都是浏览器留给自己的键——⌘, 是偏好设置、⌘O 是打开文件、⌘1..3 是切标签、⌘⇧B 是书签栏、⌘L 是地址栏、⌘⇧D 是全部加书签。Alt 是主流浏览器都不花在窗口控件上的一层,[isReservedByBrowser](src/chord.ts) 也认这一点:按住它,组合键就整个跳出保留集合。于是一个修饰键就回答了整类冲突,用不上一张按浏览器分列的例外表。

没换的那些——⌘⇧F⌘⇧E、`Ctrl+ `⌘⇧K——在 Chrome、Safari、Firefox 里都能抵达页面,再写一遍只会多一个要记的键。remdev.connect 恰好相反:它的 ⌘⇧C 正是 Chrome 和 Safari 分给「检查元素」的键,所以按规则换成 ⌥⌘C,而不是去和浏览器争一个只有部分标签页才肯让出的键。唯一完全没有 Web 键的是 session.archive⌘W 的每种写法都属于浏览器,Safari 上连加了 Alt` 的也算,所以它诚实地只在桌面端有键,而不是绑一个永远不会响应的。

内置命令

上表「由谁执行」写着本插件的行,handler 就在这个包的浏览器半边([src/client/builtins.ts](src/client/builtins.ts))。这和本插件其余部分的姿态正好相反,值得解释一下:

大多数界面动作背后都有服务可调——ctx.layout 管两侧栏,ctx.sessionsctx.workspaces 管会话与工作区,omdsh-basemodesessionModes 管模式切换。harness 不给它们注册快捷键,是因为 harness 里根本没有快捷键服务可注册——这个服务是从外面来的。调用总得有人发起,而知道「键被按了」的只有这里。

三个例外只能走 DOM:设置弹窗它当前显示的页和标签页侧栏搜索框。它们的开关状态,用 harness 自己的话说,是 "component-local viewing state",住在本仓库不去改的包里。[src/client/anchors.ts](src/client/anchors.ts) 写明了这些地址为什么站得住脚——用的全是框架保证的契约(每个 slot 出口都会渲染的 data-slot、frame 自己的 data-sidebar-collapsed / data-details-collapsed、以及 role="dialog" 这类无障碍属性),而不是 CSS module 的哈希类名、会被本地化的可见文字或渲染顺序。唯一绕不开顺序的地方——从设置导航栏里挑出 Plugins 那一行——是从 slot 注册表里读出它的下标,所以匹配的仍然是 id,DOM 只提供位置。

⌘⇧P 最后停在插件中心那一页。 Plugins 页本身是一条标签栏,自带的两页——Configurable、All——只是一份清单;插件中心(omdsh-plughub)才是人打开 Plugins 真正要去的那一页:安装、更新、卸载,以及每个声明了配置的插件的表单,包括本插件自己那张改键表。所以这次按键先选中那一页,再选中那个标签。标签栏恰好是 DOM 里唯一保留注册 id 的地方——为了让按钮和面板能按 ARIA 的要求互相指认,section 把每个标签的元素 id 拼成 ` ${useId()}-tab-${id} `——所以这个地址比它上面那行导航更可靠,而不是更脆。插件中心在不在,是问 slot 注册表、再看 DOM 的:没装它的组装停在 Plugins 页,什么都不说;只有「注册了却没渲染出来」这一种情况,值得往控制台写一行。

每个 handler 在要驱动的东西不存在时,都是安静的空操作。模式注册表属于 omdsh-basemode,里面的各个段落来自各个模式插件:没装 omdsh-basemode 就根本没有注册表,⌘1 谁也够不到;装了 omdsh-basemode 但没装 omdsh-chatmode,注册表只是少了 Chat 和 Work 两段,⌘1⌘2 找不到可进入的对象。两种情况下,这次按键都该什么都不发生——不抛异常,也不能把整个按键监听器一起带走。

panel.filespanel.terminalsidechat.open 不在这个文件里:它们的主人能自己注册,也确实自己注册。

改一个快捷键

配置分成两层,归不同的人所有——正是这条分界让第二层能放进设置面板里改:

字段归属在哪里改
items组装层profile 的 cordis.patch.yml
bindings使用者设置 → 插件 → 插件中心
hints使用者设置 → 插件 → 插件中心

最后那一列自己也有一个键:⌘⇧P(标签页里是 ⌥⌘P)打开设置弹窗并停在插件中心——也就是说,改这两个字段的面板,离它所描述的那副键盘只有一个键的距离。

items 说的是有哪些命令、显示成什么、进哪个菜单、由谁执行。bindings 是盖在它上面的一张扁平 id → 快捷键 映射:

- id: omdsh-shortcuts
  config:
    bindings:
      open-in-browser: CmdOrCtrl+Shift+B
      reveal-log: ''            # 留在菜单里,但不绑定按键

覆盖只替换菜单项的原生 acceleratorwebAccelerator 保持组装层的原样,因为它说的是形态的事实——"标签页永远拿不到这个键"——而不是选了哪个组合键。

设置面板打开时不是空的。 注册时交给 settings 的 base 里,带的是每个命令当前生效的键,而不是「已经被改过的那些」。理由很实际:由裸覆盖映射生成的表单,一打开就是空的;而在读它的人看来,空映射和「这个插件什么都没绑」没有区别。所以面板里坐着的是一张键盘图,而不是一张 diff;改键是编辑一行已经在那儿的记录,而不是先猜一个 id 再敲进去。

把这一层盖回去是恒等操作——它本来就是从 items 里读出来的——所以播下这颗种子,不会让任何一次全新安装和自己的默认值产生分歧。组装层如果自己写了 items,种子就从那份 items 读,而不是从内置的那份读。

映射里出现文档中没有的 id 时会被忽略,而不是被拒绝:item 列表属于组装层,它会变,一条过期的覆盖绝不能让这个插件挂不上去。而解析不了的组合键被拒绝,在那次写入时就拒绝——因为再怎么改 item 列表,Ctrl+ 也不会变得合法。

hints 是使用者拥有的另一个字段:一个开关,默认开启,决定指针停在按钮上时它教不教自己的组合键。它走的是客户端那条流,而不是写在文档里——因为那份文档是外壳的,原生菜单把加速键写在菜单项旁边,没有需要教东西的 tooltip。

这个插件把两者一起注册为 settings 命名空间 omdsh-shortcuts(见 omdsh 插件约定),这就是 omdsh-plughub 为它生成配置页所需的全部。这段注册挂在受限 fiber 上,所以没有 settings provider 的组装照旧按 entry config 运行。

改绑即时生效:文档重建并推给已经开着的那几条流,每个连着的外壳重建原生菜单,每个连着的页面重绑按键,每块写着组合键的 tooltip 换成新键——都不用重启。

可达性

invoke 路由没有信任围栏。约束它的有两点:只接受已发布文档声明过的 id,只能跑已挂载插件注册过的 handler。它没有约束的是谁可以 POST——这条路由的可达范围就是 webserver 的监听地址,桌面端是回环,dsh web 下则未必。要注册破坏性操作的插件应当知道这一点。若要加围栏,接缝是 webRuntime,用法见 omdsh-sidepanel

安装

它是一个 dsh bundle,不是对 harness 的改动。[cordis.patch.yml](cordis.patch.yml) 在 profile 已组装好的内容之上加一行。

npx @omdsh-plugins/omdsh-plughub add omdsh-shortcuts

这就是插件中心的安装器,只是入 口从按钮换成了 argv。它从这套集合的 registry 里解析出这个插件、从它的 GitHub 仓库装上,并把那条 pnpm 构建白名单写好——裸的 dsh plugin add github:… 会把这一步留给你,而那条记录带着 pnpm 解析出来的 commit,只能从报错里抄,事先 写不出来。

dsh plugin --profile web add @omdsh-plugins/omdsh-shortcuts 现在还不是那条命令:这个 包不在 npm 上,pnpm 会回 ERR_PNPM_FETCH_404。同一次安装也可以是一个按钮—— 只要 profile 里已经有插件中心,它就在设置 → 插件 → 插件中心里这个插件的卡片 上。

或者从一份检出安装——改这个插件本身时要的就是这种:

pnpm install && pnpm run build                    # 本地路径安装不会执行 prepare
dsh plugin --profile web add <本包路径>

两个半边都在做实事,缺一不可:node 半边发布文档并转接按键,浏览器半边持有注册表并绑定本形态的组合键。没有浏览器的形态下,客户端半边根本不会被拉取;而在没有 Electron 的 dsh web 上,浏览器半边就是全部。

移除也是同一种写法:

dsh plugin --profile web remove @omdsh-plugins/omdsh-shortcuts

这一行卸载时,每条开着的流都会收到一份空文档,外壳立刻退回平台自带的菜单,每个页面立刻解绑按键,而不是等到某个已经没人应答的请求之后。

旁边不用装别的东西,它伸手去够的东西也都不是必需的。 宿主半边只 inject webServer,浏览器半边只 inject slots;其余用到的名字——omdsh-basemodesessionModesomdsh-sidepanelomdsh-sidechat 注册的 handler——全是在 apply 里面读的,缺了就自己兜底。只装了这个插件的 profile 能组装、能启动:菜单在、每个键都绑得上,只是主人不在场的命令是安静的空操作。反过来也一样——把这个插件拿掉的 profile 里,omdsh-sidepanelomdsh-sidechat 都还站着,各自把自己的键拿回去。

命令

pnpm install
pnpm run build       # tsc 出 lib/types,再由 tsdown 打包两个半边
pnpm run typecheck   # 源码与测试
pnpm run test        # vitest
pnpm run harness:local ../../deepseek-harness   # 对着一份 checkout 编译
pnpm run harness:npm                            # 切回提交下来的版本号
pnpm run check:harness-pin                      # 只要还链着就失败

它从哪里来

omdsh-desktop 拆出,原为 app/src/menu.ts。决策记录见 harness fork 的 legacy/all-in-one 分支上的 Agent Note 2026-08-13-electron-desktop-application

已知限制

  • shell 那一层只在桌面端。 那五项都要 Electron 主进程来执行,所以在 dsh web 下它们整体不存在——既没有菜单来渲染,也没有哪个组合键能够到任何东西。
  • session.archive 没有 Web 键。 ⌘W 的每一种写法都属于浏览器,加 Alt 也一样,所以标签页里它报告为 unreachable,而不是绑一个反而会关掉窗口的键。
  • 浏览器保留的组合键给不了标签页。 ⌘N⌘T⌘W⌘Q 永远到不了页面,所以 webAccelerator 在挂载时就会拒绝它们。解法是在 Web 端换一个键,而不是想办法绕过去。
  • invoke 路由没有信任围栏。 它的可达范围就是 webserver 的监听地址,桌面端是回环,dsh web 下则未必;要注册破坏性操作的插件应当知道这一点。
  • 三个内置命令要走 DOM。 设置弹窗、它的 Plugins 页和侧栏搜索框没有服务可调,只能靠框架保证的契约来驱动——data-slot、frame 自己的折叠属性、无障碍 role。上游改了标记,这个包就得跟着改选择器。
  • 挂在 harness 按钮上的提示,是本包要一直跟随的一个地址。 六个控件靠一条词典 key 或一个 slot id 认出来,而它们都属于本仓库不改的包。上游随时可能改名;一旦改了,提示就此安静——tooltip 退回 harness 自己写的样子,组合键照样能用,但没有任何地方会说明原因。在内置表跟上之前,菜单项上的 anchor 就是应急出口。
  • 只有已经存在按钮的地方,提示才说得出组合键。 在当前形态下没有按钮的命令——Fork Session、Archive Session、没装模式插件的组装里的模式段——只能靠菜单和设置表单来教。
  • items 属于组装层,不是一个设置项。 有哪些命令、显示成什么、由谁执行、由哪个按钮来教,都在 profile 的 cordis.patch.yml 里改;插件中心提供的是 bindingshints。往菜单里加一个命令,不是在面板里能做的事。
  • 如果本插件挂上时会话列表已经到了,新窗口仍可能先打开上次的会话。 harness 在任何插件运行之前,就从 origin 级 localStorage hydrate dsh.sessions.current;本包会尽快清掉那个选择,通常能赶在 history 拉取之前。若组装把本插件拖到第一次 list 快照之后,上一场对话仍会先打开一会儿。