Skip to content

插件与内核 ​

星云启动器的插件系统要解决一件事:让第三方扩展启动器,而不用改启动器代码。这一页讲这套系统由哪几块组成、彼此怎么解耦。

两个概念 ​

  • 内核(Kernel)——Starcloud.Core/Kernel/。最小本地地:插件生命周期、契约解析、事件总线、贡献点注册表。它不含任何业务。
  • 插件(Plugin)——一份描述(id / 提供什么契约 / 依赖谁 / 要什么权限)加一段实现。内置插件和外部插件共用同一个描述形状(PluginDescriptor),差别只有「在哪跑」和「有多少权限」。

内核里最硬的一条纪律是:内核代码里不许出现具体功能的名字。下载、Nova、版本目录、存档——这些词一旦出现在 Kernel/ 目录里,就是分层错误。内核是所有插件共用的底座,一旦它认识了某个业务,第三方插件就再也换不掉那个实现。

三个注册表 ​

插件之间不直接互相引用,内核提供三条通道:

服务注册表(ServiceRegistry)解决「我要用某个能力,但不知道谁实现」。插件注册的是契约(接口),不是具体类:

csharp
context.Services.Add<IVersionCatalog>(new VersionCatalog(service));

注册具体类会直接抛异常。同一契约允许多份实现,取的时候按 priority 选,优先级相同时后注册的赢——第三方想覆盖内置实现,只要注册得晚一点。

贡献点注册表(ContributionRegistry)解决「我要往界面上加东西」。它只存「谁在哪个挂载位放了什么」:导航项、页面、首页卡片、设置分区、版本详情分区、页面内插槽、浮层。渲染不归它管——WinUI 外壳读它建导航栏,Nebula 外壳读它建自己的导航。 事件总线(EventBus)解决「我要知道一件已经发生的事」。插件只广播事实,不直接调别人。分发是同步的,但单个订阅者抛异常不影响其他人。

万物皆插件 ​

下载、版本目录、搜索翻译、图标缓存、投影引擎——在启动器里它们全是以内置插件的形式存在的:

csharp
public static PluginDescriptor Descriptor { get; } = new()
{
    Id = "starcloud.versions",
    Kind = PluginKind.Feature,
    Provides = [typeof(IVersionCatalog)],
    Depends = new Dictionary<string, string> { [BuiltinDownloadPlugin.PluginId] = "^1.0.0" },
};

图标缓存插件依赖下载插件,它自己不 new 一个下载服务,而是从内核按契约取:

csharp
var service = new IconCacheService(context.Kernel.Get<DownloadService>(), settings);

内置插件和外部插件的差别因此只剩两点:跑在进程内还是沙箱里,权限是全量信任还是白名单裁决。把老服务包成内置插件,一行代码没改就获得「可被替换」的能力——以后想换下载实现,改的是插件注册,不是启动器核心。

外壳只认贡献点 ​

这是整个解耦的关键。UI 外壳不认识页面类型:它只知道「这里有个 id 为 main 的面板」,不知道插件交上来的是 HTML 还是声明式 JSON。

csharp
public sealed record PanelRegistration(
    string PluginId, string PanelId, string Title, string EntryPath, bool Declarative = false);

EntryPath 是包内相对路径,Declarative 决定渲染方式。校验发生在装配时而不是渲染时:路径穿越在这一层就被拒掉。代价是外壳要多写一套「装配」逻辑,收益是换一套外壳不需要碰任何插件。

常见的误解

「插件系统 = 用户能装东西」。其实启动器自己的大部分功能也是插件。用户插件的特殊之处只在于它们来自第三方、要过白名单。

相关链接:内核的生命周期 · 插件清单格式 · 权限模型

基于 MIT 许可证(有修改)开源