插件系统
ElementsPanel 的面板与节点内核都只保留三样东西:cordis 容器、插件加载器、进程日志器。其余全部功能——配置、Web 服务、节点连接、鉴权、监控、实例管理——都以插件形式提供。因此"给面板加功能"就是"写一个插件"。
当前三端统一使用 Cordis 3.18.1。插件的运行时机制见 cordis 文档;本页只讲 ElementsPanel 的选择。
三个宿主端
同一个概念在三个地方各有一套独立的插件:
| 宿主端 | 运行环境 | 能做什么 |
|---|---|---|
Panel 后端 |
Node 进程(面板) |
注册 HTTP 路由、配置表单、向总览追加字段、连接节点、暴露服务给其他插件。 |
Panel 前端 |
浏览器(Vite 模块图) |
注册页面与路由、全局组件、菜单项、实例/终端/计划任务按钮、桌面应用。 |
Daemon 后端 |
Node 进程(节点) |
注册协议事件、异步任务、实例预设、计划任务类型、文件与实例能力。 |
三套插件形状一致:一个导出 apply 的模块,外加可选的 inject。
import type { PanelPluginContext } from "../../../panel/src/app/plugin";
export const inject = ["koa", "i18n", "middleware", "roles"];
export function apply(ctx: PanelPluginContext) {
ctx.i18n.define(localeMessages);
const router = ctx.koa.router("/api/example");
router.get("/", ctx.middleware.permission({ level: ctx.roles.ADMIN }), async (requestCtx) => {
requestCtx.body = "hello";
});
}
没有 setup、没有 dispose、没有需要实现的生命周期方法。apply 可以是 async。每个宿主端的上下文声明都是权威清单:
-
Panel 后端:
panel/src/app/plugin/context.ts -
Panel 前端:
frontend/src/plugin/context.ts -
Daemon 后端:
daemon/src/plugin/context.ts
插件从哪里来
| 来源 | 目录 | 说明 |
|---|---|---|
内置源码 |
|
仓库中的内置插件;构建后分别进入 |
市场(生产) |
|
与内置插件共用加载根目录,以 |
市场(源码开发) |
|
与仓库的内置插件源码隔离,安装后可在开发环境重新扫描。 |
开发工作区 |
仓库根目录的 |
仅开发模式扫描,直接以 |
加载器仍会扫描旧版 market_plugins/,但不会静默移动或删除它。加载器按 plugin.json → manifest.json → package.json 的顺序查找运行时清单,第一个存在的生效;市场发布包则要求每一端都有 plugin.json。external/ 里的插件若要发布,走 发布到插件市场。
加载、依赖与顺序
-
插件按
priority升序加载。priority只用于给互不依赖的插件定一个确定顺序(Koa 中间件顺序、路由匹配顺序、翻译合并优先级)。 -
真正的依赖用
inject表达:cordis 会保持插件处于非激活状态,直到所有被注入的服务都存在;其中任何一个消失,插件会被重新卸载。 -
一个插件加载失败会被记录并跳过,宿主进程继续运行,其余插件不受影响。
作用域与卸载
你在 apply 里注册的每样东西,都属于该插件自己的作用域,卸载时自动撤销。 路由、中间件、定时器、翻译、配置表单、协议事件、服务皆然。
由此得出的实践规则:
-
不要写清理代码,也不要从
apply返回 disposer —— cordis v3 会忽略它; -
需要撤销 cordis 不知道的东西时,用
ctx.effect()包裹; -
用
ctx.setInterval/ctx.setTimeout等宿主提供的定时器,卸载即取消,采样器不必写stop()。
开关插件
插件页面为每个插件提供一个开关,它写入 data/plugin-overrides.json(见 数据与配置),不改动插件包本身:
-
面板插件的后端与浏览器端共用面板清单和开关;Daemon 插件在每个节点上单独管理;
-
普通插件可返回
applied,仍在等待依赖时为pending,失败为failed;基础或管理插件可能返回restart-required; -
后端插件代码发生变化时,仍需要重启对应进程——这是模块热替换,不是状态保留的代码热更新;
-
浏览器通过
/plugins/events的 SSE 图更新通知刷新插件图;基础外壳类插件的变化仍需要刷新页面。
版本兼容
编译插件时会写入兼容元数据:浏览器插件为 elements: { api: 1, sdk: 1 },仅后端的插件为 elements: { api: 1 }。运行时与安装器都会在执行代码前拒绝声明了不支持版本的包。缺失该元数据的包被识别为旧包。不要给手工拼装的、不兼容的包盖上版本戳。
前端插件通过 @elements-panel/sdk 引用宿主服务(ctx、usePluginService、serviceRevision 以及上下文类型)。SDK 主版本 1 是兼容边界,破坏性的公开改动需要升主版本。
生产环境的前端资源使用 /plugins/<folder>@<revision>/frontend/…,旧 revision 会返回 404,避免不同版本的分块与样式混用。插件可通过 ctx.slots.register() 使用当前的 shell.overlay、shell.header.leading、shell.header.actions 插槽;新代码优先使用插槽,而不是兼容包装 ctx.ui.globalComponent()。