类型
信息
问题与证据
插件互相爆炸
希望得到的结果
RFC: dsh 统一插件系统 —— Plugin Manifest、Capability 与事件模型
状态: Draft / 征求意见
发起: dsh 社区生态开发者群(gui / webui / tui / 启动器 / 分发渠道 维护者共同讨论)
参考实现: fabric
讨论方式: 直接在本 RFC 的 issue / discussion 下留言,或提 PR 修改本文档
0. 一句话摘要
给 dsh 生态定一套与 dsh 上游版本解耦的插件标准:插件通过 manifest 声明自己是谁、需要什么能力(capability);宿主(gui / webui / tui / 启动器)通过统一的事件与生命周期 hook 加载和驱动插件。做一件事只有一种明确的方法。
类比一句话就能懂:我们要做的是 Chrome 扩展那套(manifest + 权限声明 + 统一 API),而不是每家浏览器自己发明一遍插件机制。
1. 背景:我们现在的生态有多割裂
dsh 火了之后,社区自发长出了多个终端和工具链:
- 三个主要 UI 形态:gui、webui、tui,各自有自己的插件生态,互相不兼容。tui 上跑不了需要图形能力的插件,但目前没有机制提前知道一个插件"需要图形能力",只能装上炸了才知道。
- loader 混战:早期大家各写各的 loader,谁都能做一个。后来官方引入了 plugin 注册方式,当时所有第三方 loader 一夜全废。这个教训说明:基于"加载方式"做生态是沙子上盖楼,基于"标准"做生态才能活过上游更新。
- patch 满天飞:不少插件靠直接 patch 源码实现功能。dsh 一更新,patch 全崩。目前的共识是 patch 越少越好,最好全部换成标准调用。
- 版本地狱:分发渠道(启动器/组合包)没法追着 dsh 的更新节奏做包管理,只能锁死"插件 A 0.1 + B 0.3 + C 0.7 + dsh 0.5.0"这样的版本组合包。这能用,但本质是在用锁版本对抗接口不稳定,治标不治本。
- 插件互炸:和 MC 社区早期一样,插件之间冲突频发,没有权限边界,也没有"干一件事的唯一方法",两个插件用两种野路子改同一个东西,必然打架。
上游(dsh 官方)改内核的意愿和节奏我们控制不了,社区也明确不接受激进的内核改动。所以这套标准的第一原则是:
标准的存续不依赖 dsh 上游的任何决定。
2. 设计目标
来自群内讨论的三条硬性要求,作为本 RFC 的验收标准:
- 基于 manifest / capability 声明,与 dsh 上游无关。 插件描述"我需要什么",而不是"我怎么被加载"。
- 接口抽象,行为可控。 插件只能通过声明过的接口做事,宿主对插件的行为有完整的知情权和拒绝权。
- 有开发标准。 哪怕有了框架,也不能让插件想干什么就干什么。干一件事情应该有明确的唯一方法。
外加两条用户侧目标(降低两个摩擦):
- 降低安装摩擦:统一市场 + 经过实测的版本组合包。
- 降低使用摩擦:插件在任意宿主上行为一致,不兼容的能优雅拒载而不是崩溃。
非目标(Non-Goals)
- 不要求 dsh 官方立刻采纳。 本标准先在社区侧跑通,成为事实标准后官方支持只是水到渠成(fabric 支持起来也不是难事)。
- 不统一 UI 实现。 gui / webui / tui 各自的渲染方式不管,只统一"插件如何声明和请求 UI 能力"。
- 不做包管理器。 分发/组合包/启动器是另一层的事,本 RFC 只保证它们有稳定的 manifest 可以消费。
3. 核心概念
整个体系只有四个概念,按依赖顺序:
Manifest(我是谁、我要什么)
↓
Capability(宿主有什么、给不给)
↓
Lifecycle & Events(什么时候叫醒我)
↓
Host API(叫醒之后我能干什么)
3.1 Manifest:插件的身份证
每个插件必须携带一个 plugin.json(或 plugin.toml,格式待议,见开放问题),这是插件与世界交互的唯一入口声明。宿主、市场、启动器、组合包工具全部只读 manifest,不猜、不扫源码。
{
"id": "com.example.better-sidebar",
"name": "Better Sidebar",
"version": "1.2.0",
"apiVersion": "1.x",
"entry": "dist/main.js",
"capabilities": {
"required": ["ui.panel", "session.read", "events.message"],
"optional": ["ui.tray", "storage.local"]
},
"contributes": {
"panels": [{ "id": "sidebar", "slot": "left", "title": "Sidebar" }],
"commands": [{ "id": "sidebar.toggle", "title": "Toggle Sidebar" }]
},
"compat": {
"hosts": ["gui>=2.0", "webui>=1.4"]
}
}
关键设计:
apiVersion 对齐的是本标准的版本,不是 dsh 的版本。 这就是"和 dsh 上游解耦"的具体落点:dsh 更新只影响标准的适配层(fabric),不影响千百个插件。
capabilities.required vs optional:required 缺一个就拒载(干净地拒,给用户明确提示),optional 缺了则降级运行。这直接解决"tui 装上需要 gui 能力的插件然后爆炸"的问题——tui 看一眼 manifest 就说"这个我跑不了",装都不让装。
contributes 是声明式扩展点:插件不是"拿到句柄之后为所欲为",而是提前声明"我要在左侧 slot 贡献一个面板"。宿主决定怎么渲染、放不放。
3.2 Capability:能力即权限
Capability 是一份由标准定义、宿主实现的能力清单。初版建议的命名空间(对应群里提到的分类):
| 命名空间 |
内容 |
说明 |
ui.* |
panel / tray / statusbar / dialog / theme |
gui、webui 全量实现;tui 实现子集 |
session.* |
read / write / history |
会话上下文的读写 |
events.* |
message / tool-call / lifecycle |
订阅业务事件流 |
storage.* |
local / shared |
插件私有存储与跨插件共享存储 |
net.* |
fetch |
网络访问(默认关闭,需用户确认) |
fs.* |
read / write |
文件访问(默认关闭,需用户确认) |
规则只有两条:
- 没声明的 capability,调用直接抛错。 不存在"偷偷能用"。
- 每个 capability 在标准里有且只有一套 API。 想画面板只有
ui.panel 一条路,没有第二种野路子——这就是"唯一方法"原则的落地。
3.3 Lifecycle & Events:forge 思路,不是 loader 思路
这是讨论里最重要的一个转向,值得单独说清楚:
loader 思路:对单个文件/模块做内容转换(像 webpack loader)。问题:谁都能写一个,官方一统一就全废,而且根本表达不了"扩展"。
forge 思路:在运行时的特定生命周期阶段自动触发扩展逻辑(像 Minecraft Forge / Fabric 的 hook)。性能可能略低一点,但这才是"扩展"该有的样子。
所以本标准的插件没有"加载脚本"这个概念,只有"注册到生命周期和事件上的回调":
生命周期事件(宿主保证的顺序)
host:ready → plugin:activate → [运行期业务事件...] → plugin:deactivate → host:shutdown
业务事件("业务事件化")
把 dsh 的核心业务流程抽象成标准事件,插件订阅事件而不是 hack 内部函数:
session:created / session:closed
message:before-send / message:sent / message:received
tool:before-call / tool:result
ui:panel-mounted / ui:panel-destroyed
before-* 类事件支持可取消/可修改语义(返回修改后的 payload 或 cancel),这是插件影响行为的唯一合法方式。事件清单本身随标准 apiVersion 演进,走 RFC 流程增删。
3.4 Host API:插件视角的世界
插件代码里只 import 一个东西:
import { definePlugin } from "@dsh-std/api";
export default definePlugin((ctx) => {
// ctx 上只有 manifest 里声明过的能力
ctx.events.on("message:received", async (msg) => {
await ctx.storage.local.set("last", msg.id);
});
ctx.ui.panel("sidebar", (panel) => {
panel.render(/* 声明式 UI 描述,由宿主决定如何渲染 */);
});
return {
deactivate() { /* 清理 */ }
};
});
注意 panel.render 接受的是声明式 UI 描述而不是 DOM 操作——这样 gui 用原生控件渲染、webui 用 React 渲染、tui 用字符界面渲染,插件一份代码三端跑(前提是它只用了三端都有的 capability)。
4. 宿主的义务
一个终端(gui / webui / tui / 其他)想成为"标准兼容宿主",需要做到:
- 只通过 manifest 加载插件,不支持任何旁路加载。
- 实现 capability 协商:对 required 缺失的插件明确拒载并给出人话提示("该插件需要图形界面能力,当前终端不支持")。
- 保证生命周期事件的顺序和送达。
- 公布自己的 capability 实现清单(机器可读),供市场和启动器做兼容性匹配。
宿主可以有自己的私有扩展 capability(如 x-tui.keymap),但必须带 x- 前缀,且插件依赖私有 capability 时市场会明确标注"仅限 XX 终端"。
5. 分发与版本策略
- 市场(marketplace):统一索引 manifest,按 capability 需求 × 宿主实现清单自动算兼容性,用户在 tui 里根本刷不到纯 gui 插件。
- 组合包(modpack 模式):继续保留,且升级为一等公民。组合包 = 一份锁定的
{标准版本, 宿主版本, [插件@版本...]} 清单 + 实测通过标记。启动器只消费这份清单。现在锁版本是无奈,将来锁版本是发行版(类比 Linux distro:滚动更新给折腾党,锁定组合包给普通用户)。
- semver 纪律:标准 API 的 breaking change 必须升 major;宿主对旧
apiVersion 至少保留一个 major 的兼容窗口。
6. 迁移路径
- fabric 作为参考实现先行:把 dsh 与 fabric 的启动解耦(进行中),fabric 实现本标准 v0 的事件层和 capability 协商。
- 现有插件迁移:把所有 patch 点替换成标准事件/API 调用(这一步大部分可以让 AI 批量做,然后人工跑测试),补一份 manifest。
- starter 框架:官方出一套插件脚手架(模板 + 类型定义 + 本地调试宿主),把"写一个 hello world 插件"压到 10 分钟以内。
- 三端认领:gui / webui / tui 各出一名代表认领宿主侧适配(讨论中 better sidebar 等项目已有人认领)。
7. 开放问题(欢迎在评论区认领)
- manifest 格式:JSON or TOML?要不要支持 JS 动态生成(倾向不支持,保持静态可分析)?
- 事件清单 v0 的最小集:上文列出的事件哪些进第一版?
before-* 的取消语义边界在哪?
- 声明式 UI 的表达力:三端公共子集能覆盖多少真实需求?复杂 UI 是否允许 webui-only 的
x- capability 逃生舱?
- 性能预算:事件总线相比直接 hook 的开销,需要一个 benchmark 基线(有同学已有 benchmark 经验,求接手)。
- 权限 UX:
net.* / fs.* 这类敏感能力的用户确认流程长什么样?
- 治理:标准的修改流程——谁有 merge 权,RFC 多久一个周期?
8. 为什么现在做、为什么是我们做
- 现在参与讨论的群体已经覆盖了生态 top 插件作者 + 三端维护者 + 主要分发渠道。我们不定标准,就没有人有能力定标准了;我们各自为战,用户就继续在版本地狱里炸。
- 历史已经验证过一次:loader 时代的所有投入在官方统一注册方式后一夜归零。依赖实现的生态会死,依赖标准的生态才能穿越上游的更新周期。
- Chrome 用"强内核 + manifest 扩展生态"证明了这条路;MC 社区用 Forge/Fabric 证明了即使官方不配合,社区标准也能成为事实标准。我们两个先例都有,抄作业就行。
下一步:对本 RFC 有意见的老师直接开 issue 或评论;一周后汇总修订为 v0.1,随后 fabric 侧开始按 v0.1 实现事件层原型。
涉及资产与授权
所有插件
受影响群体及安全、权利、速度风险
无
替代方案、可逆性与回滚
无
利益冲突
无
建议负责人和复审日
无
公开记录
类型
信息
问题与证据
插件互相爆炸
希望得到的结果
RFC: dsh 统一插件系统 —— Plugin Manifest、Capability 与事件模型
0. 一句话摘要
给 dsh 生态定一套与 dsh 上游版本解耦的插件标准:插件通过 manifest 声明自己是谁、需要什么能力(capability);宿主(gui / webui / tui / 启动器)通过统一的事件与生命周期 hook 加载和驱动插件。做一件事只有一种明确的方法。
类比一句话就能懂:我们要做的是 Chrome 扩展那套(manifest + 权限声明 + 统一 API),而不是每家浏览器自己发明一遍插件机制。
1. 背景:我们现在的生态有多割裂
dsh 火了之后,社区自发长出了多个终端和工具链:
上游(dsh 官方)改内核的意愿和节奏我们控制不了,社区也明确不接受激进的内核改动。所以这套标准的第一原则是:
2. 设计目标
来自群内讨论的三条硬性要求,作为本 RFC 的验收标准:
外加两条用户侧目标(降低两个摩擦):
非目标(Non-Goals)
3. 核心概念
整个体系只有四个概念,按依赖顺序:
3.1 Manifest:插件的身份证
每个插件必须携带一个
plugin.json(或plugin.toml,格式待议,见开放问题),这是插件与世界交互的唯一入口声明。宿主、市场、启动器、组合包工具全部只读 manifest,不猜、不扫源码。{ "id": "com.example.better-sidebar", "name": "Better Sidebar", "version": "1.2.0", "apiVersion": "1.x", "entry": "dist/main.js", "capabilities": { "required": ["ui.panel", "session.read", "events.message"], "optional": ["ui.tray", "storage.local"] }, "contributes": { "panels": [{ "id": "sidebar", "slot": "left", "title": "Sidebar" }], "commands": [{ "id": "sidebar.toggle", "title": "Toggle Sidebar" }] }, "compat": { "hosts": ["gui>=2.0", "webui>=1.4"] } }关键设计:
apiVersion对齐的是本标准的版本,不是 dsh 的版本。 这就是"和 dsh 上游解耦"的具体落点:dsh 更新只影响标准的适配层(fabric),不影响千百个插件。capabilities.requiredvsoptional:required 缺一个就拒载(干净地拒,给用户明确提示),optional 缺了则降级运行。这直接解决"tui 装上需要 gui 能力的插件然后爆炸"的问题——tui 看一眼 manifest 就说"这个我跑不了",装都不让装。contributes是声明式扩展点:插件不是"拿到句柄之后为所欲为",而是提前声明"我要在左侧 slot 贡献一个面板"。宿主决定怎么渲染、放不放。3.2 Capability:能力即权限
Capability 是一份由标准定义、宿主实现的能力清单。初版建议的命名空间(对应群里提到的分类):
ui.*session.*events.*storage.*net.*fs.*规则只有两条:
ui.panel一条路,没有第二种野路子——这就是"唯一方法"原则的落地。3.3 Lifecycle & Events:forge 思路,不是 loader 思路
这是讨论里最重要的一个转向,值得单独说清楚:
所以本标准的插件没有"加载脚本"这个概念,只有"注册到生命周期和事件上的回调":
生命周期事件(宿主保证的顺序)
业务事件("业务事件化")
把 dsh 的核心业务流程抽象成标准事件,插件订阅事件而不是 hack 内部函数:
before-*类事件支持可取消/可修改语义(返回修改后的 payload 或 cancel),这是插件影响行为的唯一合法方式。事件清单本身随标准apiVersion演进,走 RFC 流程增删。3.4 Host API:插件视角的世界
插件代码里只 import 一个东西:
注意
panel.render接受的是声明式 UI 描述而不是 DOM 操作——这样 gui 用原生控件渲染、webui 用 React 渲染、tui 用字符界面渲染,插件一份代码三端跑(前提是它只用了三端都有的 capability)。4. 宿主的义务
一个终端(gui / webui / tui / 其他)想成为"标准兼容宿主",需要做到:
宿主可以有自己的私有扩展 capability(如
x-tui.keymap),但必须带x-前缀,且插件依赖私有 capability 时市场会明确标注"仅限 XX 终端"。5. 分发与版本策略
{标准版本, 宿主版本, [插件@版本...]}清单 + 实测通过标记。启动器只消费这份清单。现在锁版本是无奈,将来锁版本是发行版(类比 Linux distro:滚动更新给折腾党,锁定组合包给普通用户)。apiVersion至少保留一个 major 的兼容窗口。6. 迁移路径
7. 开放问题(欢迎在评论区认领)
before-*的取消语义边界在哪?x-capability 逃生舱?net.*/fs.*这类敏感能力的用户确认流程长什么样?8. 为什么现在做、为什么是我们做
下一步:对本 RFC 有意见的老师直接开 issue 或评论;一周后汇总修订为 v0.1,随后 fabric 侧开始按 v0.1 实现事件层原型。
涉及资产与授权
所有插件
受影响群体及安全、权利、速度风险
无
替代方案、可逆性与回滚
无
利益冲突
无
建议负责人和复审日
无
公开记录