插件系统

更新于 2026-09-29在 GitHub 上编辑

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

插件从哪里来

来源 目录 说明

内置源码

panel/plugins/、daemon/plugins/

仓库中的内置插件;构建后分别进入 web/plugins/ 与 daemon/plugins/,入口改写为编译产物。

市场(生产)

web/plugins/、每个节点的 daemon/plugins/

与内置插件共用加载根目录,以 .market-install.json 标记市场所有权;Daemon 端只发送到用户选择的节点。

市场(源码开发)

panel/data/plugins/、daemon/data/plugins/

与仓库的内置插件源码隔离,安装后可在开发环境重新扫描。

开发工作区

仓库根目录的 external/

仅开发模式扫描,直接以 .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()。

阅读顺序

  1. 快速开始 —— 用脚手架创建并跑通第一个插件。

  2. 插件结构 —— 清单字段、上下文服务、注册与生命周期规则。

  3. 完整示例 —— 贯通面板后端、前端页面与节点协议的可运行插件。

  4. 发布到插件市场 —— 打包、账号绑定与上传。