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

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 · 217 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-justchat 插件图层plugin layer
omdsh-base 插件图层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

一个 schemastery schema 注册进 settings,插件中心就能把它渲染成表单。校验、分层、密钥遮蔽、修订冲突、热提交,全是 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-code: 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-justchat · omdsh-code omdsh-base 模式插件把自己的段注册进 sessionModes。没有 base,就没有那个开关。Mode plugins register their segment into sessionModes. Without base there is no switch to register into.
omdsh-code 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.1.3

插件中心Plugin HubPlugin Hub

@omdsh-plugins/omdsh-plughub

在设置里安装、卸载并配置 omdsh 插件。Install, remove, and configure omdsh plugins from inside Settings.

设置页里多出来的一个标签页:上半是可以装的目录,下半是已装插件的配置表单。表单直接由插件注册的 schema 生成,所以这个中心不认识任何一个具体插件 —— 这正是它存在的意义。更新按钮由 semver 比较点亮;装完给一条重启横幅,因为插件图层是在启动时组合的,热挂载本来就做不到,说"重启"是诚实的汇报。 One more tab beside the shipped Plugins pages: a catalog on top, a form per installed plugin below. 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. Update is lit by one semver comparison, and an install raises a restart banner, because plugin layers are composed at boot and saying so is the honest report.

settings: omdsh-plughub repo ↗
input0.1.1

快捷键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 ↗
workspace0.1.1

远程开发Remote DevelopmentRemote Development

@omdsh-plugins/omdsh-remdev

把工作区接到一台 SSH 服务器上,文件、终端、智能体和项目技能都在那台机器上。Attach a workspace to an SSH server: its files, terminals, agents, and project skills all live there.

侧栏 Workspaces 那一行末尾多一个 Remote Connect:描述一台机器、连上去、在上面装好 .dsh-server、选一个目录 —— 回来的是一个普通工作区,只是文件夹上带个地球。workspaces.create 要求目录真实存在,所以这个插件在 $DSH_HOME/remotes/ 下给它一个真的镜像目录;于是会话分组、注册表、投影、用量汇总全都一行不用改。 A Remote Connect button at the trailing end of the sidebar's Workspaces bar: describe a machine, connect, provision a .dsh-server there, pick a directory. What comes back is an ordinary workspace with a globe on its folder. workspaces.create requires a real directory, so this plugin gives it one — a mirror under $DSH_HOME/remotes/ — and everything downstream works untouched.

settings: omdsh-remdev repo ↗
system0.1.0

远程控制Remote ControlRemote Control

@omdsh-plugins/omdsh-remctrl

在 tailnet 上用手机查看进度、批准工具调用、下发新任务。Watch, approve, and dispatch from a phone on your tailnet.

harness 的第二道前门,跑在自己的端口上,前面是设备配对和分级方法白名单。绑定策略从设计上就不能被配置到公网接口;配对是六位数字码,同时只有一个、五分钟、五次机会;设备令牌以哈希存储,重启后仍在,可撤销。当前进度 M0 —— 门和锁已经装好了,门后面的会话读取、实时流、批准和派活是 M1–M4。 A second front door on its own port, behind device pairing and a tiered method allowlist. The bind policy cannot be configured onto a public interface; pairing is a six-digit code, one outstanding at a time, five minutes, five guesses; device tokens are stored as hashes, survive a restart, and are revocable. Status M0 — the door and the lock; reading sessions, the live stream, approvals and dispatch are M1 through M4.

M0 settings: omdsh-remctrl repo ↗
insight0.1.0

用量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.1.0

状态StatusStatus

@omdsh-plugins/omdsh-status

在 Work 窗口右上角显示当前项目名称与 git 分支和变更计数。Project name and git branch with change counts, at the top-right of the Work window.

哪个项目、哪个分支、动了多少 —— 一眼的事。项目名取自已注册的 workspace;旁边是当前分支,和领先/落后、已暂存、已修改、未跟踪、冲突里非零的那几个计数 —— 有冲突时只有它一个数字是红的,一行里每个符号都喊叫的读数等于什么都没说。host 半边按会话解出工作目录跑一次 git status,浏览器只收到要画的那几个数字,没有路径清单、没有 diff;目录不在 git 仓库里时就只显示项目名。 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.

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

模式系统Mode SystemMode System

@omdsh-plugins/omdsh-base

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

它自己不发明任何模式 —— 这正是重点。只装它的 profile 能正常启动,并且完全看不到那个开关,因为没有东西可切的控件不是控件。它提供 sessionModes 注册表、悬浮在对话列上方的开关、每一行侧栏前面那颗按模式着色的圆点,以及把 New Session 先递给"正占着这一列的那个模式"的那次覆盖。它唯一贡献的那个姿态是本来就在屏幕上的那个 —— Work,harness 自己的那根列 —— 好让开关永远有地方可以切回去;装了 justchat 之后,由它那个会记事的 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; justchat's Work, which remembers where you were, replaces it.

被 justchat / code 需要needed by justchat / code repo ↗
workspace0.1.1

聊天模式Chat ModeChat Mode

@omdsh-plugins/omdsh-justchat

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

harness 是编码智能体,新建会话前先问你要哪个工作区 —— 合理,因为编码总要发生在某处。但不是每个问题都是活儿。这个插件在旁边补上另一种姿势:对话上方一个 Chat / Work 开关,一个受管的 Chat 工作区(真实目录 <dshHome>/chat),背后是一套一个工具都没有的 agent 组合。模式是推导出来的而不是存下来的:一个会话是不是 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, a managed Chat workspace (a real directory at <dshHome>/chat), and an agent composition with no tools at all behind them. 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-baseneeds omdsh-base repo ↗
workspace0.1.1

代码模式Code ModeCode Mode

@omdsh-plugins/omdsh-code

在对话所属的工作区里开一个 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-base · omdsh-tuineeds omdsh-base · omdsh-tui repo ↗
workspace0.1.0

外部编辑器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.1.1

侧边对话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, stored in the Chat workspace. It shows a question, an answer, and one line while it works.

可选 omdsh-shortcutsoptional omdsh-shortcuts repo ↗
workspace0.1.0

侧边面板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.

可选 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 说话。运行时崩了,或者堆爆了,按外壳自己的策略重启 —— 窗口不跟着走。窗口、菜单、活动指示、重启策略,还有那块"运行时正在启动"的开机屏,都归它。 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.

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

omdsh-tui

上游把 TUI 包删掉了,这个仓库自己带一个 —— 而且是构建在已发布的 harness release 上,不是构建在它的 fork 上。这就是为什么这个终端跟随 harness 发版只需要改一个版本号,而不是合并一整个 monorepo。omdsh-code 在网页里显示的就是它。 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-code 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常驻 bashstr_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 itdsh --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

插件中心默认读的目录就是它。upstream 设置默认是 omdsh-plugins,所以刚装好、还没配置过的一份安装也能直接看到整个集合。 The curated catalog every hub installation reads by default. Its upstream setting defaults to omdsh-plugins, so a fresh install lists the whole collection without being configured.

https://raw.githubusercontent.com/omdsh-plugins/registry/HEAD/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 里的 displayNamesummarycategorysettingsdocsorder,全部可选。标题一律 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(slotssessionsworkspacessettings…)由 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

# 尚未发布到 npm,所以从检出装
pnpm install && pnpm run build
dsh plugin --profile web add "$PWD/omdsh-plughub"
dsh --profile web
  1. 打开 设置 → 插件 → OMDSH 插件Open Settings → Plugins → OMDSH Plugins
  2. 目录会直接列出整个集合 —— upstream 默认就是 omdsh-pluginsThe catalog lists the collection immediately — upstream already defaults to omdsh-plugins
  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-base" "$PWD/omdsh-justchat" "$PWD/omdsh-code"

# 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.