你该写哪一种
启动器有两套完全独立的插件系统。选错了会白写一遍,所以先用三个问题确定。
三个问题
问题一:你是想给启动器增加「能装的东西」,还是增加「界面」?
- 增加能装的东西(一个新的下载站、一个新的内容来源)→ 内容源
- 增加界面(一张卡片、一个页面、一个按钮)→ UI 插件
问题二:你有一个现成的网站或 API 要接进来吗?
有,而且它提供的是模组 / 整合包 / 资源包 / 光影 / 数据包 / 世界 / 皮肤中的某几种 → 内容源。
问题三:你要做的事需要读启动器的内部状态吗?(实例列表、当前选中的实例、安装进度、日志)
需要 → UI 插件(内容源拿不到这些)。
对照表
| 你想做的 | 写哪个 | 为什么 |
|---|---|---|
| 接入「某某模组站」,让它的模组能在启动器里搜到并安装 | 内容源 | 这就是内容源的定义 |
| 接入一个地图站,用户能浏览并下载地图 | 内容源 | 世界(world)是内容类型之一 |
| 在侧边栏放一张显示「今日一言」的卡片 | UI 插件 | 需要往界面里塞东西 |
| 给启动器加一个「服务器列表」页面 | UI 插件 | 需要注册路由 |
| 主页显示当前实例的游玩时长 | UI 插件 | 需要读启动器内部状态 |
| 让用户能从 CurseForge 装整合包 | 内容源 | 整合包是内容类型之一 |
| 局域网联机(把世界广播到局域网) | UI 插件 | 需要 lan 权限,且要配界面 |
| 换一套主题配色 | UI 插件 | 需要注入 CSS |
| 把某个站的皮肤接进来 | 内容源 | 皮肤(skin)是内容类型之一 |
两者的硬性差异
| 内容源 | UI 插件 | |
|---|---|---|
manifest 里的 type | 没有这个字段(就是内容源) | "ui" 或 "sidecar" |
| 安装目录 | <数据目录>/content-sources/<id>/ | <数据目录>/plugins/<id>/ |
| 索引文件 | content-sources.json | plugins.json |
| 提交表单 | 「提交内容源」 | 「提交插件」 |
| 入口导出 | activate(api) 里调 api.register(provider) | activate(api) 直接用 api |
| 能力声明 | capabilities 对象(search/browse/…) | permissions 数组(style/slot:…/…) |
| 权限模型 | 只有 hosts 白名单,没有审批 | 逐项声明 + 用户审批 |
| 有界面吗 | 没有,一个 DOM 都碰不到 | 有,能放组件、注册页面、注入 CSS |
它们不是「同一个系统的两个模式」
两套 manifest 的字段名、目录、加载器、权限模型都不一样。你不能把一个内容源装到 plugins/ 里,反过来也一样。一个仓库通常只做一件事——官方的四个内容源和两个 UI 插件就是六个独立仓库。
可以同时写吗
可以,但要分成两个插件:一个内容源 + 一个 UI 插件,两个仓库、两份 manifest。
它们之间没有直接的通信方式。内容源拿不到 UI 插件的状态,UI 插件也调不了内容源的 provider。它们唯一的交汇点是启动器自己的数据:
- 内容源可以把东西写进
api.storage,UI 插件读不到(存储是按插件隔离的)。 - 内容源安装的内容会出现在实例的「内容」页里,UI 插件可以通过
hostapi:instance.list之类看到实例,但看不到内容源的细节。
所以如果你的想法需要「一个界面 + 一个下载源」紧耦合,通常说明只写 UI 插件就够了——UI 插件自己就能发网络请求(network:<host> 权限)、自己读文件(通过宿主接口),不需要内容源那一层。
内容源那一层存在的意义是:让内容出现在启动器统一的浏览页里,和 Modrinth 的内容混在一起被搜索、筛选、排序。 如果你的内容不需要这个,就别写内容源。
还是拿不准
问自己一个问题:「用户会怎么找到你提供的东西?」
- 在启动器的浏览页里搜到、点进去、装到某个实例 → 内容源
- 在启动器的某个界面位置看到、点开、在插件自己的页面里操作 → UI 插件
如果答案是「用户根本不需要看到界面,他只是想搜到并安装」,那就是内容源。