omdsh-plugins
GitHub
DeepSeek Harness · 插件集合DeepSeek Harness · plugin collection dsh 0.1.2-rc.1

omdsh-plugins

12 个插件、3 个应用、1 份注册表 —— 全部在 harness 之外,全部只用它已经开好的接缝。 Twelve plugins, three applications, one registry — all outside the harness, all built on seams it already publishes.

DeepSeek Harness 是一个编码智能体。这套集合把它变成一张自己组装的工作台:聊天模式、代码模式、侧边面板、快捷键、远程开发、用量统计、项目状态。每一项都是一个独立仓库、一个 profile 图层 —— 装上就有,卸掉就没,harness 本身一行不改。 The harness is a coding agent. This collection turns it into a workbench you assemble yourself: chat mode, code mode, side panels, chords, remote development, spend at a glance, project status. Each is its own repository and its own profile layer — add it and it is there, remove it and it is gone, with nothing in the harness edited.

下载 macOS 版Download for macOS Apple silicon · 218 MB 下载 Windows 版Download for Windows x64 · 134 MB 运行时和插件中心都在安装包里,机器上不用再装别的 · 全部版本 The runtime and the plugin hub are inside the installer · all releases
12插件plugins
3应用(桌面 · 终端 · 网页)applications
4分类categories
0处 harness 改动harness edits
PROFILE
omdsh-status 插件图层plugin layer
omdsh-usage 插件图层plugin layer
omdsh-sidepanel 插件图层plugin layer
omdsh-chatmode 插件图层plugin layer
omdsh-basemode 插件图层plugin layer
dsh-web-app surface bundle · 一个 profile 只能有一个surface bundle · exactly one per profile
dsh-base harness 自己的组合the harness's own composition
dsh plugin --profile web add <pkg>
→ 往这个栈顶加一层。每一层就是一个包,连同它的 cordis.patch.yml。 → appends one layer to this stack. Each layer is a package and its cordis.patch.yml.
理念The idea

四条决定了其余一切的取舍Four choices that decide everything else

这套集合的规矩没几条,因为它们几乎都是同一条:用 harness 已经开好的接缝。 The rules are short because they are almost all the same rule: use the seam the harness already has.

OUT-OF-TREE

不动 harness 一行代码Never edit the harness

每个功能都是一个 out-of-tree bundle:一个 npm 包,声明 dsh.bundle.patch,用 dsh plugin add 追加到 profile 的图层栈上。harness 是一个保持干净、跟得住上游的 fork —— 打在里面的本地补丁,下次同步就会丢失或冲突。 Every feature ships as an out-of-tree bundle: an npm package declaring dsh.bundle.patch, appended to a profile's layer stack by dsh plugin add. The harness is a tracked fork kept clean so it can follow upstream; a local patch there is lost or conflicts on the next sync.

SEAMS

缺接缝就从邻居那里取A missing seam comes from a neighbour

slot、service、settings、workspaces —— 能用现成的就用现成的。真缺一个,就从同目录的另一个插件那里拿,而不是绕回去改 harness。omdsh-editor 就是这么长出 host 那一半的。 Slots, services, settings, workspaces — take what is published. When a seam is genuinely missing, take it from another plugin in this directory rather than reaching around the harness. That is how omdsh-editor grew a host half of its own.

SETTINGS

配置就是一个命名空间Configuration is a namespace

往 settings 注册一个 schemastery schema,插件中心就能把它渲染成表单。校验、分层、密钥遮蔽、修订冲突、热提交,全是 harness 早就写好的 —— 明年写的插件,装上当天就有配置页。 Register one schemastery schema with settings and the hub renders a form from it. Validation, layering, secret redaction, revision conflicts and hot commits are all the harness's, already written — so a plugin written next year gets a configuration page the day it is installed.

ONE REPO EACH

一个插件一个仓库One repository per plugin

每个包都能单独 clone、单独安装、单独发版,各带自己的 workspace 文件和锁文件。这个 monorepo 只是把它们放在一起开发的地方,不是它们运行的前提。 Every package is set up to be cloned, installed and released on its own, each with its own workspace file and lockfile. This monorepo is only where they are developed together — never a precondition for running one.

组成Composition

一个 profile 是怎么装起来的How a profile is put together

底座是 dsh-base,上面压一个 surface bundle(网页或终端,二选一),再往上是任意多个插件图层。插件之间通过 service 和 slot 协作,从不互相 import 值。 A base composition, one surface bundle over it — web or terminal, never both — and then any number of plugin layers. Plugins collaborate through services and slots, and never import each other's values.

一个包,两半,同时编译One package, two halves, compiled together

大多数插件有两个半边:host 半边跑在 Node 运行时里,browser 半边跑在页面里。同一个包把两半当成一个程序一起编译,类型因此是共享的 —— 也正因为如此,service 必须按名字取(ctx.get('settings')),不能靠 ctx.settings:它的类型取决于编译器先看到哪个 Context 声明,赶上哪个算哪个。 Most plugins have two halves: a host half in the Node runtime, a browser half in the page. One package typechecks both as a single program, which is exactly why a service must be resolved by name — ctx.get('settings'), never ctx.settings, whose type is whichever Context declaration the compiler saw first.

host half · node

找到装了哪些编辑器,把它拉起来Find the installed editors, launch one

进程、文件系统、SSH、HTTP 监听、settings schema 的注册,都在这一半。Processes, the filesystem, SSH, HTTP listeners, and the settings schema all live here.

browser half · page

会话头部那个分裂按钮The split control in the session header

slot 注册、React 组件、快捷键监听。它拿不到进程,只能通过 RPC 请另一半动手。Slot registrations, components, key listeners. It owns no process; it asks the other half over RPC.

@omdsh-plugins/omdsh-editor

依赖是加法,不是前提Depending on a plugin is additive, never a precondition

另一个插件是否存在,是 profile 的属性 —— 而 profile 是人靠一次次 dsh plugin add 攒出来的。cordis 的 inject 等待没有超时:在顶层 inject 里写了别人的 service,只要那个插件没装,这一项就永远停在 pending。 Whether another plugin exists is a property of the profile, and a profile is assembled by a person one dsh plugin add at a time. cordis's inject wait has no timeout, so a top-level inject naming another plugin's service sits at pending forever when that plugin is absent.

代价the cost
web boot: 1 entry did not activate
@omdsh-plugins/omdsh-codemode: pending (waiting for service: sessionModes)

任何一项没激活,两个启动审计都会判整个应用失败 —— 那是一整页死掉的 UI,不是一个被禁用的功能,跟它毫无关系的插件也得一起陪葬。所以:别人的 service 只在 apply 里面取,永远不进 inject。 Both boot audits fail the whole app for any entry that is not active. That is a dead UI, not a disabled feature — and it takes down every plugin that had nothing to do with it. So: reach for another plugin's service from inside apply, never from inject.

谁需要谁Who needs whom

omdsh-chatmode · omdsh-codemode → omdsh-basemode 模式插件把自己的段注册进 sessionModes。没有 omdsh-basemode,就没有那个开关。Mode plugins register their segment into sessionModes. Without omdsh-basemode there is no switch to register into.
omdsh-codemode → omdsh-tui Code 模式那一列跑的就是这个终端本身 —— 一行都没有重新实现。The column in Code mode is that terminal, running as a process; nothing of it is reimplemented.
omdsh-sidepanel · omdsh-sidechat ⇢ omdsh-shortcuts 可选:把命令交出去换一个和弦。没装快捷键插件,面板也不占任何键 —— 头部那个开关照样是全部入口。Optional: hand a command over in exchange for a chord. With no keybinding layer composed, the panels simply cost no key — the header switches are still the whole entry point.
omdsh-remdev ⇢ 任何读文件、起进程的插件anything reading files or starting processes 一句 ctx.get('remdev')?.remoteFor(cwd),本地那条分支原封不动。One ctx.get('remdev')?.remoteFor(cwd), and the local branch stays exactly as it was.
插件目录 · 12 个仓库Catalog · 12 repositories

装上就有,卸掉就没Add it and it is there; remove it and it is gone

卡片上的标题、摘要、分类和文档链接,都是插件自己 package.json 里的 dsh.plughub 声明的 —— 和插件中心真正读的是同一份。 Every title, summary, category and docs link below is the plugin's own dsh.plughub manifest section — the same fields the hub reads.

system0.2.7

插件中心Plugin HubPlugin Hub

@omdsh-plugins/omdsh-plughub

在设置里安装、停用并配置 omdsh 插件。Install, disable, and configure omdsh plugins from inside Settings.

设置页里多出来的一个标签页:上半是可以装的插件目录,一张卡片一个按钮 —— 没装是安装,装了是更新,有新版本才点亮。下半是这个 profile 里已有的插件,装上的和停用的都在;停用只把图层从组装栈拿掉,文件还在,再启用不用再装一遍。模板自带的 bundle、插件中心和模式系统都留在栈上 —— 中心是唯一能把插件装回去的界面,模式系统则是没有谁会自动带上的 peer。表单直接按插件注册的 schema 生成,所以这个中心不认识任何一个具体插件 —— 这正是它存在的意义。装、卸、启、停都会弹出一条重启横幅,因为插件图层是在启动时组合的。 One more tab beside the shipped Plugins pages: a catalog on top, one button per card — Install when it is missing, Update when it is there, lit only when a newer version exists. Below, every plugin this profile has, composed or parked. Disable takes a layer off the stack without touching node_modules, so Enable is not another install. Template bundles, the hub, and the mode system stay on: the hub is the only UI that puts plugins back, and the mode system is the peer nothing auto-installs. The form is generated from the schema the plugin already registered, so the hub knows nothing about any plugin in particular — which is the whole point. Install, remove, enable and disable all raise a restart banner, because plugin layers are composed at boot.

settings: omdsh-plughub repo ↗
input0.2.5

快捷键ShortcutsShortcuts

@omdsh-plugins/omdsh-shortcuts

为每个命令绑定一个快捷键,桌面菜单与网页端共用一份配置。One chord per command, on the desktop menu and in the page alike.

一份文档,两个界面:shell 类命令由 Electron 主进程通过原生菜单加速键执行,runtime 类走这个插件的交换机,browser 类由页内监听器接住。插件只声明"我能做什么",哪个键找到它由这里决定 —— 改绑当场生效,外壳不用重启,harness 一行不动。 One document, two surfaces: a shell command is performed by the Electron main process through a native accelerator, a runtime command through this plugin's switchboard, a browser command by its in-page listener. A plugin says only what it can do; which chord reaches it is decided here — and editing that rebuilds both in place, with no shell restart and no harness edit.

CmdOrCtrl+Shift+E → panel.files Ctrl+` → panel.terminal
settings: omdsh-shortcuts 参考实现reference impl repo ↗
system0.2.4

远程控制Remote ControlRemote Control

@omdsh-plugins/omdsh-remctrl

把你的 dsh 界面开放到公网地址,前面挡一道通行码。Your dsh window, on a public address, behind a passcode.

只有一扇门 —— 一个反向代理:请求打到这个插件绑定的端口上,通行码决定它能不能往里走,能走的原样交给 harness 自己的回环端口 —— HTTP 和两条 WebSocket 下行,一字不改。所以手机上不是一个功能子集,就是 dsh。代价写在明处:代理把 Host 改写成回环地址,这正是 harness 信任围栏放行的原因,也意味着一个已登录的浏览器能拿到桌面端能拿到的一切。于是通行码周围那几样不是功能,是设计本身 —— 默认关;Cookie 落定前,一个字节都不转发;每个地址每分钟只能试六次,所以十个字符就够用;来源围栏在公网这一侧,按浏览器真正用的地址再问一遍;走隧道时,非 HTTPS 的请求在读取凭证之前就以 421 挡回。默认出口是一条 cloudflared quick tunnel,所以没有公网地址的笔记本也能用:不用映射端口,不用开防火墙,不用配证书。 One door, and it is a reverse proxy: requests arrive on a port this plugin binds, the passcode decides whether they go further, and the ones that do are handed to the harness's own loopback port — HTTP and both WebSocket downlinks, verbatim. So a phone gets no subset of anything; it is dsh. The cost is stated rather than hidden: the proxy rewrites Host to loopback, which is what makes the harness's trust fence pass, and which means a signed-in browser reaches everything the desktop can. The controls around the passcode are therefore the design and not features — off by default; nothing forwarded before the cookie resolves; six tries a minute per address, which is what makes ten characters enough; the provenance fence re-imposed at the public boundary, about the address the browser actually used; and under the tunnel a non-HTTPS request refused with 421 before a credential is read off it. The default way out is a supervised cloudflared quick tunnel, so a laptop with no public address forwards no port, opens no firewall, and arranges no certificate.

settings: omdsh-remctrl repo ↗
insight0.2.4

用量UsageUsage

@omdsh-plugins/omdsh-usage

在会话顶栏显示本次会话花费、本项目花费与账户余额。Session spend, project spend, and account balance, in the conversation's top row.

三个数字放进你本来就在看的那一行。Chat 和 Work 里,它坐在模式芯片右边;Code 模式整列是终端,根本没有头部,它就挪到对角去。低于一个可配置的下限(默认 10,按账户计价货币),三个图标全部转红 —— 在那之上,一点颜色都不加。 Three figures in the row you are already reading. In Chat and Work the badge sits right of the mode chip; in Code the whole column is a terminal with no header to sit in, so it moves to the opposite corner. Under a configurable floor — 10 by default, in the account's own currency — all three icons turn red. Above it, nothing is tinted at all.

Help ⚙ Code mode 💬 0.023 ¥ 📁 1.03 ¥ 👛 964.15 ¥
settings: omdsh-usage repo ↗
insight0.2.5

状态StatusStatus

@omdsh-plugins/omdsh-status

在会话顶栏的 Session log 按钮左边,显示当前项目名、git 分支和变更计数。Project name and git branch with change counts, in the conversation header left of the Session log button.

哪个项目、哪个分支、动了多少 —— 一眼的事。项目名取自已注册的 workspace;旁边是当前分支,加上领先/落后、已暂存、已修改、未跟踪、冲突里非零的那几项计数 —— 有冲突时只有它一个数字是红的,因为一行里每个符号都喊,等于什么都没说。host 半边按会话解出工作目录,跑一次 git status;浏览器只收到要画的那几个数字,没有路径清单、没有 diff;目录不在 git 仓库里,就只显示项目名。聊天不是项目:装了模式系统时,被判成 chat 的那条对话既不出读数,也不轮询 —— 它的工作区是 harness 自己留的目录,不是谁挑的;把它摆在分支旁边,等于报告一个读者根本没有的项目。这条规则是从 sessionModes 读来的,不是自己再推一遍,所以读数和模式开关不会各说各的。 Which project, which branch, how much has moved — at a glance. The name comes from the workspace you registered; beside it the branch, and whichever of ahead/behind, staged, modified, untracked and conflicts are non-zero — conflicts are the one count drawn in the alarm colour, because a row where every glyph shouts says nothing. The host half resolves the session's directory and runs one git status; the browser receives only the counts it draws, with no path list and no diff, and a directory outside any repository shows the name alone. A chat is not a project: where a mode system is composed, a conversation it classifies as a chat gets no readout and no poll — its workspace is the one the harness keeps rather than one anybody chose, so naming it beside a branch would report a project the reader does not have. That rule is read from sessionModes rather than derived a second time, which is what keeps the readout and the mode switch from disagreeing on screen.

⬇ 导出 omdsh-plugins ⎇ main ↑1 +2 ~3 ?1
system0.2.5

基础模式Base ModeBase Mode

@omdsh-plugins/omdsh-basemode

所有模式插件共用的模式开关,以及按模式给侧栏会话上色的圆点。The switch every mode plugin registers into, and the sidebar dots that colour their conversations.

它自己不发明任何模式 —— 这正是重点。只装了它一个的 profile 能正常启动,也完全看不到那个开关 —— 没有东西可切的控件不是控件。它提供 sessionModes 注册表、悬浮在对话列上方的开关、每一行侧栏前面那颗按模式着色的圆点,以及那次覆盖:把 New Session 先递给正占着这一列的模式。它唯一贡献的姿态,是本来就在屏幕上的那个 —— Work,harness 自己的那一列 —— 好让开关永远有地方切回去;装了 chatmode 之后,由它那个会记事的 Work 顶替。 It invents no mode of its own — that is the point. A profile with this plugin and nothing else composes, boots, and shows no switch at all, because a control with nothing to switch between is not a control. It provides the sessionModes registry, the floating switch above the conversation, the coloured leading dot on every sidebar row, and the override that offers New Session to whichever segment is holding the column. The one posture it does contribute is the one already on screen — Work, the harness's own column — so a switch always has somewhere to switch back to; chatmode's Work, which remembers where you were, replaces it.

被 chatmode / codemode 需要needed by chatmode / codemode repo ↗
workspace0.2.5

聊天模式Chat ModeChat Mode

@omdsh-plugins/omdsh-chatmode

不用选项目目录就能开始对话,这些对话统一放在侧栏的 Chat 下面。Start a conversation without picking a project directory, kept together under Chat.

harness 是编码智能体,新建会话前先问你要哪个工作区 —— 合理,因为编码总要发生在某处。但不是每个问题都是活儿。这个插件在旁边补上另一种姿势:对话上方一个 Chat / Work 开关,加上一个受管的 Chat 工作区(真实目录 <dshHome>/chat)。会话跑在哪里就是它的全部 —— 用哪套 agent,仍由部署的默认预设和 harness 自己的芯片决定。Chat 里那枚工作区 chip 会从新建会话那一行拿掉,只留下预设选择。模式是推导出来的而不是存下来的:一个会话是不是 chat,取决于它算在哪个工作区名下 —— 存一个标志位,迟早会和屏幕上的事实打架。 The harness is a coding agent, and its New Session screen asks for a workspace first — reasonably, because a coding agent works somewhere. But not every question is work. This adds the other posture beside it: a Chat / Work switch above the conversation, and a managed Chat workspace (a real directory at <dshHome>/chat). Where the conversation lives is the whole of it — a chat runs the deployment's default preset, and the harness's own chip is where a reader picks another. In Chat the workspace chip comes off that row, leaving the preset picker. The mode is derived, not stored: a session is a chat exactly when it is accounted under that workspace — a stored flag would eventually disagree with the screen.

需要 omdsh-basemodeneeds omdsh-basemode repo ↗
workspace0.2.3

代码模式Code ModeCode Mode

@omdsh-plugins/omdsh-codemode

在对话所属的工作区里开一个 harness 终端,与 Chat、Work 并列。A harness terminal in the conversation's workspace, beside Chat and Work.

网页界面和终端是同一个 harness 的两道前门,这个插件就是让其中一道开进另一道里的那条缝:按下 Code,整个对话列换成一个真的 dsh --profile omdsh-tui,banner 都在,跑在这个对话本来就归属的目录里。终端不是重新实现的 —— 屏幕上是 omdsh-tui 自己的前门,这个插件只负责起进程、转发字节。在终端里 /rename 一下,侧栏那一行跟着改。 The web GUI and the terminal are two front doors onto the same harness; this is the seam that lets one be shown inside the other. Pressing Code replaces the conversation column with a real dsh --profile omdsh-tui, banner and all, in the directory the conversation is already accounted under. Nothing of the terminal is reimplemented — this plugin starts that program and relays bytes. A /rename typed inside it moves the sidebar row.

需要 omdsh-basemode · omdsh-tuineeds omdsh-basemode · omdsh-tui repo ↗
workspace0.2.3

外部编辑器External EditorExternal Editor

@omdsh-plugins/omdsh-editor

用你真正在用的编辑器、终端或文件管理器打开当前会话的目录。Open the conversation's directory in the editor, terminal, or file manager you actually use.

会话头部的一个分裂控件:按左半边,当前对话的目录用你上次选的编辑器打开;点箭头,换一个。harness 既没有这个能力,也没有哪条接缝能让人绕到它外面去实现 —— 所以这个包两半都自己写:host 那半找出机器上装了哪些应用、把它拉起来,browser 那半负责把它们摆出来。 A split control in the session header: press the left half and the conversation's directory opens in the editor you chose last time; press the chevron and pick a different one. The harness has no such capability and no seam that would let one be reached around it, so this package adds both halves — the host side that finds the applications and starts one, and the browser side that offers them.

◧ VS Code ◧ Cursor ▤ Finder ▸ Terminal ▸ iTerm2
input0.2.3

侧边对话Side ChatSide Chat

@omdsh-plugins/omdsh-sidechat

在任何位置召唤一条独立的对话,带上你正看着的东西作为锚点。Summon a conversation of its own anywhere, anchored to whatever you were looking at.

harness 只有一个输入框,在对话列的底部,而它属于你正在跑的那件事。于是每一句"等等,这个函数是干嘛的"都要付两次钱:手离开你正在读的地方,问题挤进那条对话的上下文,然后就一直待在那儿,占着窗口、影响之后每一轮。按 ⌘L,就地开一条属于它自己的会话 —— 多轮、有历史、存在 Chat 工作区里;保存之前不占侧边栏,点了面板右下角的保存按钮,它才归进自己的工作区。界面上只有一个问题、一个答案,和它干活时的一行字。 The harness has one input, at the bottom of the conversation column, and it belongs to the thing you are running. So every "wait, what does this function do?" costs you twice: your hands leave what you were reading, and the question moves into that conversation's context, where it stays — taking up window, shaping every turn after it. Press ⌘L and a conversation of its own opens right where you are: multi-turn, with history, kept out of the sidebar until the Save control in its corner files it under its workspace. It shows a question, an answer, and one line while it works.

可选 omdsh-shortcutsoptional omdsh-shortcuts repo ↗
input0.2.4

文档DocumentsDocuments

@omdsh-plugins/omdsh-document

把 Word、PPT、Excel、PDF 和文本文件添加到消息里,模型直接读到它们的内容。Attach Word, PowerPoint, Excel, PDF and text files to a message; the agent reads their text.

输入框里什么也不会出现。附件是宿主端为这个对话维护的队列 —— 输入框卡片里那条轨道就是它,紧挨 harness 自己的图片缩略图 —— 宿主在 agent/pre-step 把队列插到消息正下方,作为一条 context 消息:模型紧接着问题读到文档,聊天记录在这条消息底下画一行可折叠的 Context injection。浏览器插件唯一的发送时钩子,是草稿里的一枚 chip;而 chip 是输入框里一个看得见的格子,代表的却不是用户写下的内容。换成宿主端这条缝,输入框就还是输入框。队列归宿主所有,所以它熬得过刷新;轨道清空就是回执 —— 只有真正带着用户话语的那一步才能认领队列,还留着的一行就是没送出去的文件。 Nothing appears in the message box. An attachment is a queue the host keeps for the conversation — the rail inside the composer card is that queue, next to the harness's own image thumbnails — and the host splices it in directly under the message at agent/pre-step, as a context message: the model reads the documents right after the question, and the transcript draws one collapsed Context injection row beneath it. A browser plugin's only send-time hook is a chip in the draft, and a chip is a visible cell in the textarea standing for something the user did not write; the host's seam leaves the message box a message box. The queue being the host's is also why it survives a reload — and why the rail emptying is the receipt: only a step actually carrying the user's words may claim it, so a row that stays is a file that did not go.

settings: omdsh-document repo ↗
workspace0.2.5

侧边面板Side PanelsSide Panels

@omdsh-plugins/omdsh-sidepanel

右边一列文件树,下边一条终端,只在 Work 模式下出现。A file tree down the right edge and a terminal along the bottom, in Work mode.

harness 出厂是三列 —— 会话列表、对话、详情 —— 没有任何地方可以看一个文件或者跑一条命令。这个插件补上这两样,然后就停手:右边列出这个对话自己的目录,一次预览一个文件;下边是同一个目录里的 shell。没有编辑器、没有 git 面板、没有浏览器、没有标签栏 —— 在这里,一个容易被忽略的面板,胜过一个想当第二个应用的面板。 The harness ships a three-column frame — session list, conversation, details — and no place to look at a file or run a command. This adds the two, and stops there. The right panel lists the conversation's own directory and shows one file at a time; the bottom panel is a shell in that same directory. There is no editor, no git surface, no browser, no tab strip: a panel that is easy to ignore is worth more here than one that is a second application.

settings: omdsh-sidepanel 可选 omdsh-shortcutsoptional omdsh-shortcuts repo ↗
应用 · 3 个仓库Applications · 3 repositories

三个不是插件的仓库The three repositories that are not plugins

它们各自带着自己的 pnpm workspace、自己的 catalog 和自己的 lockfile,所以不在这个 workspace 的成员里,也不在注册表中 —— 它们不是 profile 图层,而是跑 profile 的地方。 Each carries a pnpm workspace of its own, with its own catalog and lockfile — which is why none is a member of this workspace, and why none appears in the registry: none is a profile layer. They are where a profile runs.

Electron 桌面外壳Electron desktop shell

omdsh-desktop

把 harness 运行时当作子进程监督起来,通过 loopback HTTP 说话。运行时崩了,或者堆爆了,按外壳自己的策略重启 —— 窗口不跟着走。窗口、菜单、活动指示、重启策略,还有那块"运行时正在启动"的开机屏,都归它。随外壳交付的插件中心卸不掉、停不了:下次启动会把它放回组装栈;通过中心做的 Update 会写成 profile 里的真实依赖,外壳不再去覆盖那份副本。 An Electron shell that supervises a harness runtime as a child process and talks to it over loopback HTTP. A runtime that crashes or exhausts its heap is restarted under a policy the shell owns, without taking the window with it. Windows, menus, activity, restart policy, and the boot screen a starting runtime is watched from all live here. The shipped hub cannot be uninstalled or disabled: the next launch puts it back on the stack. An Update through the hub writes a real profile dependency, and this shell then leaves that copy alone.

runtime/package.json 是唯一一处记录它所搭载 harness 版本的地方 —— 从源码跑和打包出来的 .app 因此监督的是同一个运行时。外壳绑的那些和弦不在这儿,在 omdsh-shortcuts;这个仓库只拥有那些和弦做什么。 runtime/package.json is the one place recording the release it ships, which is what makes running from source representative of a packaged build. Which chords the shell claims lives in omdsh-shortcuts; this repository owns what those chords run.

macOS.dmg · 218 MB Windows.exe · 134 MB 未签名,首次打开:macOS 右键 →「打开」,Windows「更多信息」→「仍要运行」 Unsigned — first open: right-click → Open on macOS, More info → Run anyway on Windows
dsh 0.1.2-rc.1 electron-builder · dmg · nsis
终端界面 · profile bundleInteractive terminal · profile bundle

omdsh-tui

上游把 TUI 包删掉了,这个仓库自己带一个 —— 而且构建在已发布的 harness release 上,不是构建在它的 fork 上。所以这个终端跟随 harness 发版,只需要改一个版本号,而不是合并一整个 monorepo。omdsh-codemode 在网页里显示的就是它。 Upstream removed its TUI package; this repository carries one, built against a published harness release rather than a fork of it — which is what lets the terminal follow harness releases by changing a version number instead of merging a monorepo. It is what omdsh-codemode shows inside the web GUI.

每个会话由一个 agent preset 组合出来 —— 决定模型有哪些工具、人设说什么、看得到哪些 prompt 段落 —— /mode 就是这四个 preset 的界面。bundle 的 patch 关掉了 base 在 host 半边的工具行,所以"少一个工具"是真的少一个,不只是个标签。 Each session is composed from one agent preset — the composition deciding which tools the model has, what its persona says, which prompt sections it sees — and /mode is the surface over the four dsh ships. The bundle patch disables the base's host-plane tool rows, so a preset that removes a tool actually removes it.

模式Mode preset 模型拿到什么What the model gets
Standardstandard完整编码智能体:文件编辑、shell、文件与网络搜索、技能、计划、目标、子智能体、工作流The full coding agent: file editing, shell, search, skills, planning, goals, subagents, workflows
Codecode同样的能力,改由 Code Mode SDK 呈现 —— 五次调用变成一段 TypeScriptThe same capabilities through the Code Mode SDK, so a five-call sequence becomes one TypeScript program
Minimalminimal常驻 bash 和 str_replace_editor,一句话人设,再无其他Persistent bash and str_replace_editor, a one-sentence persona, and nothing else
CreatorcordisStandard 加上自指的 Cordis 工具集与写 preset 的技能 —— 让智能体给你写另一个智能体Standard plus the self-referential Cordis toolset and a preset-authoring skill, for asking an agent to write you another agent
macOS 应用打包器macOS application packager

omdsh-webapp

dsh web 从终端启动,打印一个 URL —— 于是用网页界面就意味着:服务活多久,终端就得开多久,还得去点那行地址。这个仓库把它写成一个可双击的 DSH Web.app:点 Dock 图标启动服务并打开页面,之后每次唤起它,都是把已经显示着它的那个标签页提到前台,而不是再开一个;退出时,停掉它自己启动的那个服务。 dsh web starts from a terminal and prints a URL, so using the browser UI means keeping a terminal open for the server's lifetime and clicking a printed line. This repository writes a double-clickable DSH Web.app instead: the Dock icon starts the server and opens the page, and every activation afterwards raises the tab already showing it rather than opening another. Quitting stops the server it started.

它是一个启动器,不是 harness 的副本:烘焙进去的只有构建时解析到的 dsh 路径和一条 PATH —— Finder 启动不继承任何环境,连登录 shell 的 PATH 都不继承。profile、它的插件、harness 版本,全都在启动时读取,所以升级 harness 不需要重新打包。它也因此不是 profile 图层:它包装的,就是你已经组好的那个 profile。 It is a launcher, not a copy of the harness: all that is baked in is the dsh resolved at build time and a PATH, because a Finder launch inherits no environment at all — not even the login shell's. The profile, its plugins, and the harness release are read at launch, so upgrading the harness needs no rebuild. Which is also why it is no profile layer: what it wraps is the profile you already assembled.

当你When you 它It
启动它Launch it起 dsh --profile web,等它打印出 URL,然后打开那个 URLboots dsh --profile web, waits for the URL it prints, and opens that URL
再次触及它Reach it again调起已经显示该 URL 的标签页 —— 只问已经在运行的浏览器,所以绝不会拉起一个你已经关掉的raises the tab already showing that URL — only browsers that are already running are asked, so it never starts one you had closed
端口已被别的东西占着Something else holds the port呈现那个端口上的会话并就此让位,因为「到达它」正是这次点击要的surfaces the session on that port and steps aside, because reaching it is what the click asked for
退出它Quit it停掉它自己启动的服务,不留下被占用的端口stops the server it started, so no port is left held
macOS 13+ swift shim · applescript · dmg
注册表Registry

一份文件,一次请求,一个说清推荐什么的机会One file, one request, and a chance to say what is recommended

插件中心默认读的目录就是它。registryUrl 默认指向这份清单的 CDN 地址,GitHub 枚举默认关闭,所以一个刚装好、还没配置过的安装,也能直接看到整个集合。 The curated catalog every hub installation reads by default. registryUrl points at this file on a CDN, and GitHub enumeration stays off, so a fresh install lists the whole collection without being configured.

https://cdn.jsdmirror.com/gh/omdsh-plugins/registry/registry.json

枚举本来就能用,为什么还要它Why it exists when enumeration already works

插件中心还有第二个远程源,一个文件都不需要:直接问 GitHub 这个账号有哪些仓库,再读每个仓库的 package.json。那个源永远不会过期,也一直保留 —— 推上去的东西,不管列没列在这儿,都装得上。但它有三件事做不到: The hub has a second remote source that needs no file at all: it asks GitHub what the account owns and reads each repository's package.json. That source never goes stale, and it stays. What it cannot do is any of the following:

  • 一次请求,而不是一个仓库一次。One request instead of one per repository. 未认证的枚举一小时只有 60 次,光这个集合就要十几次。Enumeration is rate-limited to 60 requests an hour unauthenticated, and the collection alone costs a dozen.
  • 说出推荐什么。Say what is recommended. 枚举报告的是"存在什么"。一个草稿仓库和一个正式发布的插件,在它眼里一模一样。Enumeration reports what exists. A scratch repository and a released plugin look identical to it.
  • 钉住版本。Pin a version. 枚举读的是默认分支,HEAD 上有什么就广告什么;这里的一行,对应一次正式发布。Enumeration reads the default branch, so it advertises whatever is on HEAD; a row here advertises a release.

三个源按包名合并,优先级是 local > registry > github:配置了 localSources 的开发者看到的是自己的工作副本,而不是这里的一行;没列在这份清单里的插件,仍然能从账号枚举里装上。 The three sources are merged on the package name, with precedence local > registry > github: a developer who configured localSources sees their own working copy rather than the row here, and a plugin absent from this manifest is still installable from the account.

远程内容当作远程内容对待remote content is treated as remote content

spec 是要交给 pnpm 的参数,所以每一个都先过白名单:registry 规格、github:owner/repo,或 https 的 git / tarball 地址。开头是 - 的、含空白的,以及任何文件系统路径,一律拒绝 —— 一份远程清单里的路径,装上的会是读到它的那台机器上碰巧存在的东西。 spec is an argument to pnpm, so every specifier is validated against an allowlist first: a registry specifier, github:owner/repo, or an https git or tarball URL. A leading -, whitespace, and any filesystem path are refused — a path from a remote manifest would install whatever the reader's machine happens to have at it.

它是生成的,不是手写的It is generated, not written

build.mjs 读旁边那些插件检出的 package.json,把清单打印出来。每个字段本来就住在那里;手工维护的副本,在有人第一次改摘要时就开始漂移。 build.mjs reads the plugin checkouts beside it and prints the manifest from their package.json files. Every field already lives there, and a hand-kept copy would drift the first time somebody edited a summary.

# rewrite registry.json
node registry/build.mjs
# fail when it is stale
node registry/build.mjs --check

哪些包会出现,由包自己决定:一个目录当且仅当声明了 dsh.bundle.patch 才被收进来 —— 这和插件中心判断"这东西装得上"用的是同一个事实。所以一次发布是两次推送:先发插件,再跑一遍生成器,推注册表。 Which packages appear is decided by the packages themselves: a directory is included exactly when it declares dsh.bundle.patch, the same fact the hub reads to decide something is installable at all. So a release is two pushes: the plugin, then the generator and this repository.

更新按钮的四种状态The four states of the Update button

state 卡片显示The card shows 按钮The button
available0.1.0 → 0.2.0高亮Highlighted
current单个版本the one version灰:已是最新Grey: up to date
linked单个版本the one version灰:装自本机的一个目录,文件本身就是源Grey: installed from a directory on this machine
unknown已知的版本whatever is known灰:这个源不广告版本,无从比较Grey: this source publishes no version to compare against
公约 · 九条规则Conventions · the nine rules

一个插件在这里答应的事What a plugin here agrees to

这些是插件中心能在完全不认识某个插件的前提下,把它列出来、装上、配好的全部前提。一个需要每个插件教它点什么的中心,每来一个新插件就得改一次代码;这个不用 —— 它渲染的每一样东西,插件本来就声明过。 This is all it takes for the hub to list, install and configure a plugin without knowing anything about it in particular. A hub that needed each plugin to teach it something would need editing before every new plugin could appear; this one needs nothing.

配置是一个 settings 命名空间Configuration is a settings namespace

有东西要配的插件注册一个命名空间,名字就是它的无 scope 包名,带一份 schemastery schema,base 设成组合入口的配置。A plugin with anything to configure registers one namespace, named for its unscoped package name, with a schemastery schema and base set to the composition entry.

schema 自带它的措辞The schema carries its own words

每个字段用 .description() 写成句子,再用 .i18n({ zh: … }) 本地化。没有任何插件向中心注册字典,中心也不会为每个插件长出一条词条 —— 正是这条性质,让明年写的插件装上当天就有两种语言的正确标签。Label every field with .description(), written as a sentence, and localize with .i18n({ zh: … }). No plugin registers a dictionary with the hub, and no dictionary in the hub grows an entry per plugin.

密钥要声明,不能指望Secrets are declared, not hoped for

装凭据的字段挂 .role('secret'):线路上会把它从每个响应里剥掉,只报告"存了没有",中心渲染成只写控件。没声明的密钥,会以明文送到每一个打开过这个面板的浏览器。A field holding a credential gets .role('secret'). The wire strips it from every response and reports only whether a value is stored. A secret that is not declared travels in plaintext to every browser that opens the panel.

说清楚改动什么时候生效Say when a change takes effect

applies: 'live'(默认)是提交即生效,'restart' 则不是。中心会在卡片上标出"要重启",免得有人改完了坐在那里干等。优先选 live —— 通常只是多一个 watch 回调。applies: 'live' (the default) means a change is acted on as it commits; 'restart' means it is not, and the hub marks that on the card so nobody is left wondering. Prefer live — it usually costs one watch callback.

展示信息写在 package.json 里Declare display metadata in package.json

dsh.plughub 里的 displayName、summary、category、settings、docs、order,全部可选。标题一律 Title Case、写全不缩写 —— 包名是拿来敲的,所以缩(omdsh-remctrl);标题是拿来读的,所以不缩(Remote Control)。displayName, summary, category, settings, docs, order — every field optional. Titles are Title Case and spelled out: a package name is typed, so it contracts (omdsh-remctrl); a title is read, so it does not (Remote Control).

表单画不出来的控件,给它一张卡A control the form cannot draw gets a card

通用表单画字符串、数字、布尔、封闭联合、字符串列表与字典、嵌套对象;画不了的它拒绝,而不是猜 —— 猜出来的控件会写进错误的形状,还能通过校验。需要"按下一个键"而不是"输入 Ctrl+K"的插件,往中心的 slot 里注册一张自己的卡。The generic form draws strings, numbers, booleans, closed unions, string lists and dictionaries, and nested objects. It refuses anything else rather than guessing — a guessed control writes the wrong shape and passes validation. A plugin needing to capture a keystroke registers a card in the hub's slot instead.

version 保持 semver,发版就 bumpKeep version semver, and bump it on release

点亮更新按钮的比较只有一次:目录源广告的版本 对 磁盘上的版本,按 semver 排序。所以 version 不是记账 —— 它是任何人收到"有新版本了"的唯一信号。The Update button is lit by one comparison: the version the catalog advertises against the version on disk, ordered by semver. So version is not bookkeeping — it is the only signal anybody gets that a new release exists.

三件永远不做的事Three things a plugin here never does

不改 deepseek-harness;不提交 link: 规格(pnpm 按声明它的那份清单解析,等于把某一台机器的目录结构写死,而且是静默失败:悬空符号链接、"安装成功",然后每一个 harness import 都 TS2307);浏览器那半绝不 import 另一个插件的值 —— 要么内联出第二份运行时,要么向冻结的模块表要一个它答不上来的规格。Never edit deepseek-harness. Never commit a link: specifier — pnpm resolves it against the declaring manifest, so it hard-codes one machine's layout, and it fails silently: dangling symlink, "successful" install, then TS2307 on every harness import. And never import another plugin's values in the browser half — it either inlines a second copy of that runtime or asks the frozen module table for a specifier it cannot answer.

别人的 service 永远不进你的 injectAnother plugin's service never goes in your inject

harness 自己的 service(slots、sessions、workspaces、settings…)由 base 和 surface bundle 组合,写在顶层 inject 里总能解析。另一个插件发布的 service 不是同一类事实 —— 它在不在,取决于这个 profile。所以在 apply 里面拿:依赖有生命周期就用受限 fiber,只在调用那一刻需要就惰性读取。The harness's own services — slots, sessions, workspaces, settings — are composed by base and the surface bundle, so naming them at the top level always resolves. A service published by another plugin is a different kind of fact: whether it exists is a property of the profile. So reach for it inside apply — a restricted fiber when the dependency has a lifetime, a lazy read when you only need it at the moment something is called.

→ 读全文:九条规则逐条展开,附代码示例、新插件自查表和参照实现。 → Read the full text: all nine rules with their code, a checklist for a new plugin, and the worked example.

开始使用Getting started

两条路,装的是同一样东西Two paths to the same thing

插件中心的安装按钮跑的就是 dsh plugin add;区别只在于你是在页面里按,还是在终端里敲。 The hub's Install button runs the same dsh plugin add; the only difference is whether you press it or type it.

路线 A · 推荐Path A · recommended

先装插件中心,剩下的在页面里点Install the hub once, click the rest

dsh plugin --profile web add @omdsh-plugins/omdsh-plughub
dsh --profile web

# 或者一条命令装完,页面都不用开
npx @omdsh-plugins/omdsh-plughub add omdsh-basemode omdsh-chatmode
  1. 打开 设置 → 插件 → 插件中心Open Settings → Plugins → Plugin hub
  2. 目录会直接列出整个集合 —— 默认读的就是这份清单的 CDN 地址The catalog lists the collection immediately — the default is this manifest, on a CDN
  3. 按 Install,然后按顶部的横幅重启Press Install, then restart from the banner at the top
  4. 在同一页展开任何已装插件,配置它Expand any installed plugin on the same page to configure it
路线 B · 从源码Path B · from source

克隆、构建、加进 profileClone, build, add to a profile

pnpm install && pnpm run build
dsh plugin --profile web add \
  "$PWD/omdsh-basemode" "$PWD/omdsh-chatmode" "$PWD/omdsh-codemode"

# Code 模式跑的是另一个 profile,装法也不同
cd omdsh-tui && pnpm install && pnpm run install:profile

dsh plugin add <路径> 记录的是 link: 依赖,装上的文件就是这份检出 —— 所以必须先 build,没有 lib/ 的检出加载不了;插件中心把这种情况报告为 linked 而不是"已是最新",因为根本没有东西可以去取。 dsh plugin add <path> records a link: dependency, so the installed files are the checkout — build first, because a checkout with no lib/ cannot be loaded. The hub reports such an install as linked rather than "up to date", because there was never anything to fetch.

顺序只是可读性偏好,不是要求:一个插件如果先于它想要的 service 被组合,会挂在受限 fiber 上等着,而不是失败。但 Code 模式确实需要那个 profile —— 没有它,按下 Code 会在终端列里显示 dsh: profile "omdsh-tui" does not exist,应用其余部分不受影响。 Order is a readability preference, not a requirement: a plugin composed before the service it wants waits on a restricted fiber rather than failing. But Code mode does need that profile — without it, pressing Code renders dsh: profile "omdsh-tui" does not exist inside the terminal column, and the rest of the app is unaffected.