Skip to content

[Proposal] 统一插件 api 和事件、sdk 讨论 #23

Description

@hikariming

类型

信息

问题与证据

插件互相爆炸

希望得到的结果

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 的验收标准:

  1. 基于 manifest / capability 声明,与 dsh 上游无关。 插件描述"我需要什么",而不是"我怎么被加载"。
  2. 接口抽象,行为可控。 插件只能通过声明过的接口做事,宿主对插件的行为有完整的知情权和拒绝权。
  3. 有开发标准。 哪怕有了框架,也不能让插件想干什么就干什么。干一件事情应该有明确的唯一方法。

外加两条用户侧目标(降低两个摩擦):

  1. 降低安装摩擦:统一市场 + 经过实测的版本组合包。
  2. 降低使用摩擦:插件在任意宿主上行为一致,不兼容的能优雅拒载而不是崩溃。

非目标(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 文件访问(默认关闭,需用户确认)

规则只有两条:

  1. 没声明的 capability,调用直接抛错。 不存在"偷偷能用"。
  2. 每个 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 / 其他)想成为"标准兼容宿主",需要做到:

  1. 只通过 manifest 加载插件,不支持任何旁路加载。
  2. 实现 capability 协商:对 required 缺失的插件明确拒载并给出人话提示("该插件需要图形界面能力,当前终端不支持")。
  3. 保证生命周期事件的顺序和送达
  4. 公布自己的 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. 迁移路径

  1. fabric 作为参考实现先行:把 dsh 与 fabric 的启动解耦(进行中),fabric 实现本标准 v0 的事件层和 capability 协商。
  2. 现有插件迁移:把所有 patch 点替换成标准事件/API 调用(这一步大部分可以让 AI 批量做,然后人工跑测试),补一份 manifest。
  3. starter 框架:官方出一套插件脚手架(模板 + 类型定义 + 本地调试宿主),把"写一个 hello world 插件"压到 10 分钟以内。
  4. 三端认领:gui / webui / tui 各出一名代表认领宿主侧适配(讨论中 better sidebar 等项目已有人认领)。

7. 开放问题(欢迎在评论区认领)

  1. manifest 格式:JSON or TOML?要不要支持 JS 动态生成(倾向不支持,保持静态可分析)?
  2. 事件清单 v0 的最小集:上文列出的事件哪些进第一版?before-* 的取消语义边界在哪?
  3. 声明式 UI 的表达力:三端公共子集能覆盖多少真实需求?复杂 UI 是否允许 webui-only 的 x- capability 逃生舱?
  4. 性能预算:事件总线相比直接 hook 的开销,需要一个 benchmark 基线(有同学已有 benchmark 经验,求接手)。
  5. 权限 UXnet.* / fs.* 这类敏感能力的用户确认流程长什么样?
  6. 治理:标准的修改流程——谁有 merge 权,RFC 多久一个周期?

8. 为什么现在做、为什么是我们做

  • 现在参与讨论的群体已经覆盖了生态 top 插件作者 + 三端维护者 + 主要分发渠道。我们不定标准,就没有人有能力定标准了;我们各自为战,用户就继续在版本地狱里炸。
  • 历史已经验证过一次:loader 时代的所有投入在官方统一注册方式后一夜归零。依赖实现的生态会死,依赖标准的生态才能穿越上游的更新周期。
  • Chrome 用"强内核 + manifest 扩展生态"证明了这条路;MC 社区用 Forge/Fabric 证明了即使官方不配合,社区标准也能成为事实标准。我们两个先例都有,抄作业就行。

下一步:对本 RFC 有意见的老师直接开 issue 或评论;一周后汇总修订为 v0.1,随后 fabric 侧开始按 v0.1 实现事件层原型。

涉及资产与授权

所有插件

受影响群体及安全、权利、速度风险

替代方案、可逆性与回滚

利益冲突

建议负责人和复审日

公开记录

  • 我理解相关控制者确认前它可能始终只是提案。

Metadata

Metadata

Assignees

No one assigned

    Labels

    proposalCommunity governance proposal / 社区治理提案

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions