omdsh-plughub
English | 中文
DeepSeek Harness 设置里的插 件中心:在自带的 Plugins 页签旁边再加一个,列出可以从上游安装的插件,并 把已经装上的插件都配置好——或者先停用,文件还留着。插件中心和模式系统始 终保持启用。
以前装一个插件,是在终端里敲 dsh plugin --profile web add <path>;配一 个插件,是手改 profile 的 cordis.patch.yml。现在这两件事都在页面上。
它提供什么
| 界面 | 从哪来 |
|---|---|
| 设置 → 插件 下的第三个页签 插件中心 | settings.plugins.tab 里的一个条目——ui-settings 为"清单类"和"配置类"插件留的那个座位 |
| 合并后的目录,以及每个来源的结果 | GET /api/plughub/catalog,由 local、registry、github 三个来源解析而来 |
| 可安装里的安装或更新;已安装里的更新和卸载 | POST /api/plughub/install、/update、/uninstall,各自 shell 出去执行 dsh plugin --profile <name> |
| 已安装里的启用/停用 | POST /api/plughub/enabled,改写 dsh.profile.bundles 和一份停用名单;包仍留在 node_modules。插件中心和模式系统可以更新,不能停用,也不能卸载。 |
| 每个已安装插件的配置表单 | GET/POST /api/plughub/settings,传递 ctx.settings.describe({ redactSecrets: true }) 与 ctx.settings.mutate |
| 操作进度与重启提示 | GET /api/plughub/events,一条事件流 |
omdsh.plugin.card slot | ctx.slots——通用表单画不出它的控件的插件,在这里注册自己的那张脸 |
它自己的 settings 命名空间 omdsh-plughub | ctx.settings.register,用的就是每个插件都用的那套通用表单 |
终端上的 omdsh-plughub | 一个 bin,解析的是同一份目录,跑的是路由用的同一个 Installer |
这个页面上有两个字符串指向这个包:中文里写法相同,英文里只差一个字母, 而这个差别出自约定,不是疏忽。页签叫 插件中心(英文 Plugin hub): 它属于设置界面的 chrome,所以和 harness 摆在它旁边的页签 Plugin list 一样用 sentence case。另一个 Plugin Hub 是这个包自己的 dsh.plughub.displayName,只给已安装列表里的一张卡片当标题——就是这个 插件自己的那张。所有卡片的标题都由同一段代码、从同一个字段取出,而 Title Case 正是规则 5 对每个 displayName 的要求。harness 本身没有任何改动:那个页签是一个已 经发布的座位,移除这一行就原样交还。
思路
插件中心难的不是那个"安装"按钮,而是后半截:插件装上之后,它的配置界面 从哪儿来?
有两种答案。要么每个插件都为这个面板附一张卡片——但那样一来,比这个包 晚发布的插件装上后什么也点不了,而这个包还得随着插件数量一直长下去。要 么,面板去读插件本来就已经声明的东西。
harness 让第二种答案成为可能,因为它本来就有一条 user-settings 接缝。插 件用一份 [schemastery] schema 注册一个命名空间; ctx.settings.describe({ redactSecrets: true }) 会把这份 schema 连同当 前取值、组装层 base、原始 user 层、脱敏后的密钥槽位和一个 revision 一起 交出来。这正是一个配置表单需要的全部输入。
所以这个包只做两件事——渲染表单、安装包——并且不认识任何一个具体的插 件。明年才写出来的插件,只要遵守约定, 装上的当天就有配置页。
插件(host 半边) plughub(host 半边) plughub(浏览器半边)
──────────────── ─────────────────── ────────────────────
ctx.settings.register( describe(脱敏) ──────────→ rehydrateSchema
'omdsh-shortcuts', ─→ plan ─→ 控件
Config, settings.mutate ←───────── 一次按路径寻址的修改
{ base: entryConfig })左边一列没有提到这个包,右边一列没有提到快捷键。
中间那一列,以及它为什么还在
那一步曾经必须存在:harness 自己的 settings 线路被一张写死的命名空间名单 挡住,任何 out-of-tree 插件都过不去。0.1.0-rc.7 把那道闸拆了:Host 现 在会把每一个已注册的命名空间都送到浏览器,想在官方「可配置」页签上放一 张卡片的插件,往 settings.plugin.item 里注册一张即可。
这条路由还在,是为了另一件事。官方页签只渲染认领了那个槽位的命名空间; 这个中心则会从每个 omdsh 插件已经注册好的 schema 画出一张通用表单,包括 那些从没写过卡片的插件。而且它画出的边界比 Host 的更窄——一个命名空间只 有在某个已安装的 bundle 用 dsh.plughub.settings 声明了它时才可达, 所以同一个进程里注册着的 shell 和 agent-loop 在这里够不到。
它只承担传输,别的什么都不做:校验、分层、脱敏、并发冲突、提交,全都还 在 ctx.settings 里。
这个页签里有什么
目录来源——这个插件自己的配置,放在最上面,因为这些字段回答的正是 "下面这个列表从哪儿来"。它就是一个普通的 settings 命名空间,用的是每个 插件都用的那套通用表单;只是它待在这里,而不是待在已安装列表里——这样 盯着空目录的人,不用去翻找那个能修好它的开关。当所有来源都失败、或者什 么都没拉到时,它会自己展开一次。
可安装——合并后的目录,每张卡片一个按钮。没装的时候是"安装";装了 且没有新版本时是灰色的"更新";有了新版本,同一个按钮就亮起来。卡片的标 题、简介和文档链接都是插件自己的,从它的 dsh.plughub 声明里读出来,按 当前语言解析。
已安装——这个 profile 里的每个插件一行,无论当前是否在组装栈上。启 用和停用共用一个按钮:停用把依赖从组装栈上拿下来,但不碰 node_modules,再想用时按启用即可,不必重新安装。模板自带的 bundle、插 件中心和模式系统都不能从这里停用。展开后是按该插件的 settings schema 生 成的表单;没有注册命名空间的插件会明确说"没有可配置项"——这是一个真实 的回答,而不是一个空盒子。
profile 有变化时,顶部会出现重启提示。插件层是在启动时组装的,被监听的 只有用户 patch 层,所以新装的 bundle 确实没法热挂载——说"需要重启"是实 话,不是拿话术盖住一个限制。
更新
"可安装"里已安装的卡片上只有"更新"这一个按钮,没东西可拉的时候是灰的。 到底是哪种状态,由 Host 用它手上已有的两个数字判断——获胜来源声明的版 本,和磁盘上那个包的版本——按 semver 比,不是按字符串比(0.10.0 比 0.9.0 新,1.0.0 比 1.0.0-rc.2 新)。
| 状态 | 卡片上显示 | 按钮 |
|---|---|---|
available | 0.1.0 → 0.2.0 | 高亮 |
current | 单个版本号 | 灰:已是最新 |
linked | 单个版本号 | 灰:从本机目录链接安装,文件本身就是来源 |
unknown | 已知的那个版本号 | 灰:该来源没有提供可比较的版本号 |
更新跑的就是安装跑的那条 dsh plugin add,只是 specifier 会说清楚要去 哪儿。git specifier 原样不动,因为重新解析 ref 本来就是它要做的全部事 情;registry specifier 则会补上目录里那个版本—— pnpm add @scope/name@0.2.0——因为对一个清单已经满足的依赖跑光秃秃的 pnpm add <name>,pnpm 会打印 Already up to date,什么都不改,然后以 0 退出:那次操作会报成功,卡片上却照旧挂着同一个更新。
点名版本除了正确,还换来两件事。按钮装的是它上面印着的那个版本,而不是 按下去那一刻 latest 恰好指向的东西;而且显式版本不受 pnpm minimumReleaseAge 约束——它会把新发布藏上一整天,否则发布之后第一次 按下按钮,就又是那个静默的空操作。代价是 profile 里记的是确切版本,而不 是一个范围;对一个"更新靠按按钮"的清单来说,这反而是诚实的。
它单独占一条路由,是因为前置条件正好相反:安装拒绝 profile 已经有的包, 更新则要求它已经有。
更新完会出现重启提示。更新不会改变 bundle 列表——同一个包名,只是 后面换了一份代码——所以按列表比对会漏掉这个唯一换掉运行中代码的操作, runtime 因此额外记了一笔。这里故意保守:更新拉到同一个版本时,代价是白 重启一次;反过来的代价,是有人以为自己换了代码,其实跑的还是旧的。
linked 就是 checkout 安装的样子,dsh plugin add <路径> 记的是 link:。所以一个全部由本地目录拼起来的 profile,每个更新按钮都会是灰的 ——而这是对的:改 checkout 就已经是在改插件了。
目录从哪里来
三个来源,按包名合并,优先级从高到低:
| 来源 | 是什么 | 为什么存在 |
|---|---|---|
local | 本地插件 checkout 目录 | 你正在改的那份,胜过别人发布的那份 |
registry | 一份人工维护的 JSON 清单 | 一次请求拿到全部元数据,也是上游表达"我推荐哪些"的地方 |
github | 枚举账号下的仓库 | 零维护:把插件仓库推上去它就出现了 |
优先级低的来源仍然会补上赢家缺少的 repo——本地 checkout 很少知道自己 发布在哪里,卡片上的链接因此更好用。
开箱即用时,目录就是这个集合发布的那份策展清单,从缓存 GitHub 的 CDN 上 取:
https://cdn.jsdmirror.com/gh/omdsh-plugins/registry/registry.json
GitHub 枚举默认关掉——upstream 是空的——因为那一份文件已经列出了每 一个插件;而先问 GitHub 这个账号有哪些仓库、再对每个仓库去拉 package.json,正是页签第一次打开会变慢的原因。把 upstream 指到一个 账号,就会连同枚举一起打开;把 registryUrl 清空,则会改由账号推导 https://raw.githubusercontent.com/<account>/registry/HEAD/registry.json。 两个都清空,则只用 localSources。
本地来源只往下扫一层目录,所以把可安装的那一半放在 packages/ 里的 monorepo 不会被提供。这通常是对的——这里的 monorepo 装着的多半是另一 种 surface 的 bundle,而一个 profile 只组装一种 surface。确实想让它出 现时,把 localSources 直接指向里面那一层目录。
失败的来源会被报告出来,而不是藏起来。空列表上,"这里没有插件"和 "GitHub 对这个账号限流了"长得一模一样,但只有其中一个会自己恢复。
只有一个例外,而且是反方向的:推导出来的清单地址返回 404 算"没有", 不算"坏了"。上游账号不发布策展清单是常态,仓库枚举本来就覆盖这种情况; 要是每个默认安装底下都挂一行红字,指着一个谁也没承诺过的文件,只会训练 人忽略那个报告失败的位置。而人手填进 registryUrl 的地址是相反的情形: 他本来就认为那里该有一份清单,所以它的 404 和其他失败一样会被报告出来。
清单格式是 { "plugins": [...] }(裸数组也行):
{
"plugins": [
{
"name": "@omdsh-plugins/omdsh-shortcuts",
"repo": "omdsh-plugins/omdsh-shortcuts",
"version": "0.1.0",
"plughub": { "displayName": { "": "Shortcuts", "zh": "快捷键" }, "order": 10 }
}
]
}spec 可以显式写;不写时按 github:<repo> 推导。
这个账号发布的清单在 omdsh-plugins/registry,由 各插件自己的 package.json 生成,而不是手工维护。
它持有的路由
| 路由 | 方法 | 作用 |
|---|---|---|
/api/plughub/catalog | GET | 合并后的目录,?refresh=1 重新拉取每个来源 |
/api/plughub/installed | GET | 这个 profile 的插件,哪些可以卸载,哪些在组装栈上 |
/api/plughub/install | POST | { id }——安装一个目录条目 |
/api/plughub/update | POST | { name }——按目录当前提供的 specifier 重装一个已安装插件 |
/api/plughub/uninstall | POST | { name }——卸载一个依赖管理的 bundle |
/api/plughub/enabled | POST | { name, enabled }——组装或停用一个依赖管理的插件 |
/api/plughub/events | GET | 操作进度、重启标记与设置失效通知,事件流 |
/api/plughub/settings | GET | 已安装插件持有的每个命名空间,已脱敏 |
/api/plughub/settings | POST | { ns, ops, expectedRevision }——一次按路径寻址的修改 |
可达范围
读路由用的是 /api 同款栅栏:Host 头指向我们(loopback,或这个部署被明 确告知要服务的 authority),加上同源的浏览器标记。它们和渲染它们的设置 面板一样可达,不多不少。
写路由是 loopback only,不管 --trusted-host 说了什么。它们每一个 都会改变这台机器:安装会执行那个包的 prepare 脚本,写设置会落盘到 Host 的文档。"这个部署把 /api 开放到了局域网",不等于同意了这两件事 里的任何一件。真的想在一个已发布的 dsh web 上装插件的人,仍然可以从 终端装——在那里,这个决定明显是他自己做的。
而且写请求指的都是 Host 已经解析出来的东西。安装指的是一个目录条目, 永远不是包 specifier——Host 在自己解析出来的目录里查 specifier,所以任 何请求都够不到所配置上游没有提供的包,也根本不存在能携带 specifier 的 请求形式。写设置指的是某个已安装插件声明自己持有的命名空间。两张白 名单都是结构性的,而不是"需要有人记得写"的检查。
一次安装实际怎么跑
它 shell 出去执行 dsh plugin --profile <name> add <spec>。
pnpm add 只是安装的一半;另一半是把 dsh.profile.bundles 与磁盘上的 现状对账,而这份对账逻辑属于启动这个 runtime 的那个 launcher。在这里重 新实现一遍,就得维护一份必须紧跟 launcher 的副本——而 launcher 是用户 自己独立升级的;一旦写错,一个"刚装好"的插件下次启动时就不在树里。
有一件事这个包必须知道:pnpm ≥10 在未加入白名单前拒绝执行依赖的安装脚 本,而一个 git 来源的 dsh 插件是在 prepare 里构建自己的——它发布 出去的树里没有 lib/。于是一次没加白名单的 git 安装会"成功",写入依 赖,完成 bundle 对账,然后下次启动死在 Cannot find module .../lib/index.js。所以 allowBuilds 条目是在安装 之前写进 profile 的 pnpm-workspace.yaml 的——因为那个错误要比犯 错晚一次重启才出现。
写包名对 registry 依赖是对的,对 git 依赖则不够。pnpm 给 git 来源的 包用的 key 是它实际解析到的那个 tarball——@scope/name@https://codeload.github.com/ owner/repo/tar.gz/<sha>——而且拒绝任何别的写法,所以那条提前写好的条 目形式正确、实际无效。那个 commit 在安装前不可知,除非把 pnpm 自己的解 析重写一遍,而且插件每推一次它就变。
所以先写名字;如果 pnpm 仍然拒绝,就去问它。它的拒绝信息里印着它想要的 那个精确 key,把这个 key 读出来写进去,再跑一次安装。
只要还在学到新东西,就继续跑——因为 pnpm 报的是它实际撞到的那 个拒绝,而不是接下来会撞到的那些。一个有原生依赖的插件,会先卡在那个依 赖上,等它被放行之后才轮到插件自己的 prepare:omdsh-remdev 要三轮, 正是这个原因。循环的条件是"有没有进展",而不是次数:一个已经是 true 的条目没有新东西可写,所以某一轮什么都没教给它时,循环就结束;上限四次 只是兜底,而不是会拦住一次正常安装的东西。
有一件事必须"回答"而不是"读取":pnpm 会自己把被拦的包写进那个文 件,值是 set this to true or false。那是一个问句,而按下 Install 的人 已经回答过了,所以那个值会被改写,而不是被当成"条目已存在"。
操作串行执行:同一个目录里两个 pnpm 会抢同一把 lockfile,输的那个给出 的报错描述的是这场竞争,而不是人做错了什么。
同样的安装,在终端里
这个包带一个 bin。它就是上面那条安装路径,只是入口从路由换成了 argv:
omdsh-plughub list # 目录里有什么,已经装了什么
omdsh-plughub add omdsh-status # 装一个
omdsh-plughub update omdsh-status # 挪到目录里那个版本
omdsh-plughub remove omdsh-status # 卸一个它存在的理由就是上一节。集合里从 npm 装的有两个——这个包,以及 omdsh-basemode。其余每个插件都从 GitHub 装,而 git 安装没有一条能用的 dsh plugin add——pnpm 要的那个 allowlist key 里带着它解析出来的 commit,只能从报错里抄,事先写不出来。这个包一直知道该怎么应付,只是在 此之前,它只应答一个按钮。
这里没有第二套实现。命令解析的是同一份目录,从里面取出同一个 specifier, 交给同一个 Installer——所以从终端装上的插件和从页签装上的插件,是同 一条依赖、同一行 bundle、同一次重启。
是名字,还是 specifier
一个参数属于哪一种,决定了要不要查目录;也正因为有这个区分,你可以点名 某个账号下的仓库,而不必把整份目录搬过去:
| 你敲的 | 它装的 |
|---|---|
omdsh-status | 目录里名字以这一段结尾的那个条目;匹配到两个会如实报出来,而不是替你猜 |
@omdsh-plugins/omdsh-status | 就是那个条目,点名点全 |
github:someone/omdsh-status | 就那个仓库,照字面装,完全不查目录 |
@omdsh-plugins/omdsh-status@0.1.2 | 同上,并锁到某个版本 |
/checkouts/omdsh-status | 同上,从一份 checkout 装——这里允许,而路由里拒绝,这正是 isInstallableSpec 的 allowPath 一直以来的用途:一条在键盘上敲出来的路径,和一条从别人清单里送进来的路径,不是一回事 |
--upstream <账号> 把整份目录挪到另一个账号上,只对这一次运行有效。这 是同一个问题的另一半——参数说的是取什么,upstream 说的是目录去哪 儿找——也正因为如此,一个光秃秃的名字不必自己背上一个账号。
它不读这个插件存下来的设置。 命名空间是由 harness 的 settings 服务在 一棵运行中的树里解析的,而这个程序不是那样一棵树,所以 --upstream、 --github-token、--registry-url 和几个超时都是命令行参数,默认值与 schema 里声明的那一套相同。--help 会把它们列出来。
表单会画哪些控件
| schema 节点 | 控件 |
|---|---|
string | 文本框 |
带 role('secret') 的 string | 只写输入框,存过值以后显示为掩码;Host 只报告有没有存过 |
number | 数字框,遵守 min / max / step |
boolean | 复选框 |
常量 union | 下拉选择 |
array(string) | 可编辑列表 |
dict(string) | 可编辑键值行 |
object | 标题加缩进的子项,最多三层 |
| 其他 | 只读 JSON,并提示去改设置文件 |
最后一行是刻意的。一个对任意 schema 硬猜的通用表单,产出的控件会悄悄写 进错误的形状;而一次通过了校验、含义却已经变了的设置写入,比没有控件更 糟。如果插件需要的控件这个表单画不出来,就改为往 omdsh.plugin.card 注 册一张卡片——见约定第 6 条。
每次写入都是一次按路径寻址的修改,并带上这个面板读到的 revision。按路径 而不是整体替换,是因为面板收到的内容是脱敏过的:用屏幕上的内容重建一个 replace,会把线上从来没送过来的密钥全部删掉。带 revision,是因为同一 个面板可能被两个界面同时打开:没有它,第二个写入者会静默覆盖第一个;有 了它,第二个会被拒绝、重读,然后显示当前的取值。
字段的标题由属性名推导(maxRepos → Max repos),schema 的 description 放在它下面。schemastery 的 description 是一句话,拿一句话当 标签并不好读;这样 schema 作者只写一份,它落在读起来合适的位置上。
而属性名是个英文标识符——中文界面上的表单因此总是只翻译了一半。所以 schema 可以自己写标题,写在 meta.extra 里(schemastery 自己留给表单渲 染器的那个槽位),形式和本地化的 description 一样,是一张语言映射表:
Schema.string().extra('extra', { label: { '': 'Model route', zh: '模型路由' } })
Schema.string().role('secret', { label: { '': 'API key', zh: '密钥' } })第二种写法不是花样:role(text, extra) 写的是同一个槽位,而且只传一个 参数时会把 undefined 写进去,所以带 role 的字段必须通过 role 声明 标题。写了标题之后,属性名会作为一枚 code 小标签回到标题旁边——那正是 要去设置文档里改的人需要的。这个集合里每一个拥有设置命名空间的插件都这 样声明标题,包括下面这张表里的字段:一个渲染表单的插件,自己的面板却只 翻译了一半,那是在跟自己较劲。
配置它自己
这个插件遵守它自己定的约定,所以它在自己的面板里就能配——在页签顶部的 目录来源里。除了一个字段以外,下面这些都能在那里改,不用碰文件;同 样也仍然可以写在 cordis.patch.yml 的组装配置里,那就是面板写入所覆盖 的 base 层。命名空间 omdsh-plughub:
| 字段 | 默认值 | 作用 |
|---|---|---|
upstream | (空) | 作为兜底来源被枚举的 GitHub 账号;留空则关闭 |
registryUrl | https://cdn.jsdmirror.com/gh/omdsh-plugins/registry/registry.json | 人工维护的清单;留空则由 upstream 推导 |
localSources | [] | 作为可安装条目提供的本地 checkout 目录 |
githubToken | — | 解除匿名枚举每小时 60 次的限流(密钥) |
maxRepos | 100 | 枚举时最多检查的仓库数 |
timeoutMs | 10000 | 远程来源的单次请求超时 |
cacheTtlMs | 300000 | 已解析的目录复用多久 |
profileDir | 推导 | 要管理的 profile;留空则取当前运行的那个。只在组装处生效——见下 |
launcher | 推导 | dsh 可执行文件路径;留空则取当前运行的 runtime,再走 PATH |
pnpmPath | 推导 | pnpm 可执行文件路径;留空则依次在 runtime、profile 和常见安装位置里找 |
profileDir 就是面板不提供的那一个字段。这个 runtime 管哪个 profile, 在插件挂载时就定下来了——installer、路由、以及判断是否需要重启所比对 的 bundle 列表,全都绑在它上面,而 settings 层是在那之后才解析的。所以 它对表单 .hidden(),只在组装这个插件的地方设置——这正是 omdsh-shortcuts 在 items 和 bindings 之间画的那条线。
如果这套组装里根本没有 settings 服务——无头的 surface、一个测试台—— 插件中心就只按组装配置跑:目录来源会直接说明这一点,而不是画出表 单;每个已安装插件都显示为没有声明可配置项;目录、安装和卸载都和平时一 样。注册走的是 ctx.inject(['settings'], …),所以"可配置"在这里是附加 的,而不是前提。
安装
dsh plugin --profile web add @omdsh-plugins/omdsh-plughub
dsh web然后打开 设置 → 插件 → 插件中心,集合里其余插件已经列在那里了—— 目录清单是默认值,所以需要在终端里装的只有这一个插件。也可以按插件中心 自己在卡片上用的写法,直接从账号装一个发布版:
dsh plugin --profile web add github:omdsh-plugins/omdsh-plughub这一条第一次跑一定失败,而且失败的是 pnpm,不是这个包:git 依赖靠 prepare 自建,而 pnpm ≥10 在包进入 allowBuilds 之前不会跑任何这类脚 本。pnpm 和 dsh 都会把该往 $DSH_HOME/profiles/web/pnpm-workspace.yaml 里加的那一条打印出来,而且 是完整的 specifier——'@omdsh-plugins/omdsh-plughub@https://codeload.github.com/…/<sha>': true——不 是包名;一旦有过一次被拒绝的尝试,光写包名就不够了。上面那条 npm 写法 完全不需要这些,通过插件中心装的东西也不需要:这个包在跑安装之前会 自己把那一条写好,这就是按一个按钮和粘一段 YAML 的区别。
或者从一份 checkout 装,改插件中心本身时要的就是这种:
pnpm install && pnpm run build
dsh plugin --profile web add /path/to/omdsh-plughub想让本地 checkout 和上游并列出现,把 localSources 设成存放它们的目 录;同名包下,checkout 胜过任何已发布的版本。
omdsh-plughub 这条命令从哪儿来
bin 是随这个包一起发的,所以把它装进一个 profile,命令会落在那个 profile 的 node_modules/.bin 里——你 PATH 上的任何东西都找不到它, 下面第一种写法就是为这个准备的:
npx @omdsh-plugins/omdsh-plughub add omdsh-status # 什么都不用先装
npm install -g @omdsh-plugins/omdsh-plughub # 然后:omdsh-plughub add …
"$DSH_HOME"/profiles/web/node_modules/.bin/omdsh-plughub add omdsh-status三种写法跑的是同一个程序、对着同一个 profile:它读的是 $DSH_HOME 和 --profile,而不是自己是怎么被启动的,所以二进制来自哪里,从不改变它 写进哪个 profile。
移除也是同一种写法:
dsh plugin --profile web remove @omdsh-plugins/omdsh-plughub这会同时带走页签、路由和 settings 网关。Plugins 区回到插件配置和 插件列表两个页签,而通过插件中心装的每个插件都还在——那些是 profile 自己的 bundle 行,由 launcher 写入,并不由这个包持有。
它旁边不需要再组装别的东西。宿主半边只 inject webServer,settings 注 册又挂在 ctx.inject(['settings'], …) 上,所以一个完全没有 settings provider 的 profile 照样有页签、有目录、能装能卸——只是每个已安装插件 都显示为没有声明可配置项。
命令
pnpm install
pnpm run build # tsc → lib/types,再 tsdown → lib/{index,contract,client,cli}.js
pnpm test
pnpm run typecheck
pnpm run harness:local ../../deepseek-harness # 对着一份 checkout 编译
pnpm run harness:npm # 切回提交下来的版本号
pnpm run check:harness-pin # 只要还链着就失败它从哪儿来
harness 声明 settings.plugins.tab 的原话,就是为了让"清单类插件和配置 类插件在互不依赖的前提下协作" (packages/client/ui-settings/src/client/contract/slots.ts)。这个包是 那个座位上的第三位占用者,和 harness 自带的两个页签并列:插件配置 (configurable 条目,出厂的 Bash、Agent loop、Web search 三张卡片就归 它)和插件列表(all 条目,把组装进来的每个 bundle 列一遍)。它没 有给 harness 加任何 slot,没有打任何补丁;把它移除,Plugins 区就回到这 两个页签。
已知限制
- 每一次安装、更新、卸载、启用和停用都需要重启。 插件层是在启动时组
装的,被监听的只有用户 patch 层,所以新装的或刚停用的 bundle 没法热挂 载。提示条说的就是这件事,它背后没有一个"以后版本会悄悄修好"的东 西。
- 本地来源只往下扫一层目录。 配置的根目录里放的是插件 checkout,凡
是自己的 package.json 里没有 dsh.bundle.patch 的都会被跳过——所 以把可安装的那一半放在 packages/ 里的 monorepo 不会被提供。想让它出 现,就把 localSources 指向里面那一层。
- 匿名的 GitHub 枚举有限流。 不带令牌时每小时 60 次,而且无论如何
maxRepos 最多只看 100 个仓库。失败会显示在来源那一行,而不是藏起 来;githubToken 可以解除限流。
profileDir不能在面板里改。 这个 runtime 管哪个 profile,在插件
挂载时、settings 层解析之前就定下来了,所以这个字段对表单 .hidden(),只属于组装配置。
- 写路由只限回环。 一个开放到局域网的
dsh web可以浏览目录、读面
板,但安装、更新、卸载、启用、停用和写设置都会被拒绝——把 /api 发 布出去,并不等于同意在这台机器上执行某个包的 prepare 脚本。
- 一个命名空间只有在被某个已安装 bundle 声明时才可达。 网关是按
dsh.plughub.settings 判断归属的,所以那些注册者并非 profile 所含 bundle 的命名空间——比如 harness 自己的 shell 和 agent-loop——在 这里天然看不到。
- 通用表单画不出来的 schema 会被拒绝。 字符串、数字、布尔、常量
union、字符串列表、字符串字典和嵌套对象之外的东西,一律渲染成只读 JSON,并提示去改设置文件。需要更多控件的插件,改为往 omdsh.plugin.card 注册一张卡片。
[schemastery]: https://github.com/shigma/schemastery