基本介绍
插件系统是LinaPro承载业务能力的核心扩展机制。每个插件都是自包含模块,可以声明API路由、数据库资源、前端页面、菜单权限、语言包、定时任务和生命周期回调。
LinaPro同时支持两种交付模式:
- 源码插件:以
Go源码形式参与主框架编译,适合长期维护的业务能力。 - 动态插件:以
.wasm产物运行时上传和加载,适合二进制分发、热加载和临时扩展。
两种模式运行形态不同,但共享同一套插件治理面。管理端看到的是同一类插件生命周期、依赖、权限、状态、多租户策略、公开静态资源和插件自身配置。
源码插件和动态插件的目录结构也趋于一致:根目录包含plugin.yaml,后端能力位于backend/,前端资源位于frontend/,安装脚本、语言包、配置和插件自有资源位于manifest/。源码插件通过plugin_embed.go把这些资源嵌入主框架编译产物;动态插件通过构建工具把同类资源写入.wasm产物,并在运行时绑定到当前有效发布版本。
插件不必包含前端页面
LinaPro的插件系统没有强制要求每个插件都提供前端页面。插件能否解决问题,不取决于它是否有界面,而取决于它是否实现了主框架所需的扩展能力。
许多插件的全部价值在于扩展后端行为,而不需要任何用户界面。典型的例子包括:
- 存储后端插件:对接七牛云、
AWS S3或其他对象存储服务,接管主框架的文件上传与读取逻辑。插件启用后,主框架的文件存储行为自动切换,无需修改任何调用方代码,界面侧完全无感。 - 认证方式插件:对接
LDAP目录服务或OIDC身份提供商,扩展用户登录能力。主框架通过认证接口调用插件提供的具体实现,业务层对底层协议完全透明。
对于这类纯后端插件,plugin.yaml中的menus字段通常为空,插件只需通过pluginhost注册HTTP路由或实现主框架扩展点接口,主框架以接口调用的方式消费插件能力。前端和后端的模块化是同一插件模型下的两个自然选择,并非对所有插件的强制要求。
为什么需要双模式
单一插件形态很难同时满足研发效率、运行性能、热加载和商业分发。
| 需求 | 更适合的模式 | 原因 |
|---|---|---|
| 长期业务模块 | 源码插件 | 原生Go性能,工具链完整,易测试和维护 |
| 紧急修复或临时能力 | 动态插件 | 可在运行时上传和启用,减少部署影响 |
| 商业插件分发 | 动态插件 | 可以只分发二进制产物,不暴露源码 |
| 与主框架能力深度协作 | 源码插件 | 可通过稳定pluginhost契约使用主框架能力 |
在大多数业务开发中,源码插件是默认选择;当热加载、源码保护或最终用户自行上传插件成为硬要求时,再选择动态插件。
治理主链
插件系统不是简单扫描目录再注册路由,而是一条从发现到运行的治理主链:
| 源码组件 | 组件职责 |
|---|---|
catalog | 读取plugin.yaml或WASM自定义段,生成可审查的发布快照 |
dependency | 检查框架版本范围、插件依赖和循环依赖 |
lifecycle | 编排安装、启用、禁用、卸载和运行时升级 |
integration | 将菜单、权限、路由、钩子和定时任务投影到主框架运行时 |
plugin-runtime cache | 为请求路径提供低延迟的插件状态、路由和资源快照 |
关键公共契约
plugin.yaml
每个插件都必须提供plugin.yaml。它是插件身份、依赖、菜单、多租户策略、公开静态资源和动态插件主框架能力授权的统一入口。
id: content-article
name: 文章管理
version: v0.1.0
type: source
scope_nature: tenant_aware
supports_multi_tenant: true
default_install_mode: tenant_scoped
description: 提供文章内容的增删改查管理功能
author: linapro
homepage: https://example.com/plugins/content-article
license: Apache-2.0
i18n:
enabled: true
default: zh-CN
locales:
- locale: en-US
nativeName: English
- locale: zh-CN
nativeName: 简体中文
dependencies:
framework:
version: ">=0.1.0 <1.0.0"
public_assets:
- source: frontend/pages
mount: /
index: index.html
menus:
- key: plugin:content-article:list
name: 文章管理
path: content-article-list
component: system/plugin/dynamic-page
perms: content-article:article:view
type: M
插件ID必须是kebab-case,最长64个字符。菜单key必须使用plugin:<plugin-id>:...格式,菜单类型使用D、M、B分别表示目录、页面和按钮权限。supports_multi_tenant是必填语义字段,用来明确插件是否参与租户级安装和开通治理。
public_assets声明插件作者允许匿名访问的静态资源目录。主框架只会托管声明命中的资源,并统一映射到/x-assets/{plugin-id}/{version}/...。同一插件版本下的公开资源内容应保持稳定;资源变化应升级插件版本或引入等价的内容版本机制。
动态插件还可以声明hostServices,申请访问主框架服务。hostServices是授权申请清单,不是运行时自动拥有的能力;安装或升级时需要主框架确认并写入授权快照:
hostServices:
- service: data
methods: [list, get, create, update, delete]
resources:
tables:
- plugin_demo_dynamic_record
- service: storage
methods: [put, get, delete, list]
resources:
paths:
- plugin-demo-dynamic/
- service: config
methods: [get]
- service: hostConfig
methods: [get]
resources:
keys:
- workspace.basePath
- i18n.default
- service: manifest
methods: [get]
resources:
paths:
- profile.yaml
config、hostConfig和manifest都是只读服务,清单中只声明methods: [get]。String、Bool、Int、Duration、Scan等是源码插件或动态插件SDK在get之上的便捷方法,不应作为授权方法写入清单。
pluginhost
pluginhost是源码插件使用的稳定扩展接口。源码插件通过它注册:
| 接口 | 能力 |
|---|---|
Assets() | 嵌入插件清单、语言包、SQL和前端资源 |
HTTP() | 注册插件HTTP路由 |
Hooks() | 订阅主框架事件 |
Cron() | 注册插件任务处理器 |
Lifecycle() | 注册安装、升级、禁用、卸载等生命周期回调 |
Governance() | 声明菜单和权限过滤逻辑 |
HostServices() | 获取插件作用域的配置、缓存、租户、通知、清单资源等主框架服务 |
源码插件无法直接import主框架的internal/目录,只能使用主框架发布的稳定契约。各基础能力服务的架构设计和使用约束,参见插件基础能力。
pluginbridge
pluginbridge是动态插件的沙箱通信层。主框架将请求、身份、租户和权限快照封装为BridgeRequestEnvelopeV1传入WASM模块,插件返回BridgeResponseEnvelopeV1。
动态插件访问主框架能力时发起host_call,主框架按安装时确认的hostServices授权快照校验服务、方法和资源边界。
插件配置与manifest资源
插件业务配置不应写入主框架config.yaml。主框架为插件提供插件作用域配置服务,读取优先级如下:
| 优先级 | 配置位置 | 说明 |
|---|---|---|
| 1 | 生产配置根下plugins/<plugin-id>/config.yaml | 运维侧覆盖当前插件配置 |
| 2 | apps/lina-plugins/<plugin-id>/manifest/config/config.yaml | 开发期插件默认配置 |
| 3 | 动态插件产物中的manifest/config/config.yaml | 动态插件随发布版本携带的默认配置 |
manifest/config/config.example.yaml只是配置模板,不是运行时默认值。源码插件通过HostServices().Config()读取当前插件自己的配置,通过HostServices().HostConfig()读取宿主公开配置白名单键,通过HostServices().Manifest()读取插件manifest/下的原始资源。动态插件对应使用hostServices中的config、hostConfig和manifest授权。
manifest资源路径相对manifest/,可以读取profile.yaml、resources/policy.yaml、config/config.example.yaml、sql/*.sql或i18n/*.json等插件自有文件;读取原文不等于让配置、SQL或语言包生效。完整路径语义、读取优先级和使用示例参见插件配置与manifest资源。
生命周期状态
插件生命周期覆盖发现、安装、启用、禁用、卸载和升级,并包含租户级禁用、租户删除、安装模式调整等治理钩子:
插件文件更新不会自动切换有效版本。主框架启动或扫描发现更高版本后,将插件标记为pending_upgrade,管理员在插件管理页预览并显式执行运行时升级。升级流程会执行依赖预检、生命周期回调、升级SQL、治理资源同步、有效发布切换、缓存失效和集群通知。
动态插件升级如果涉及资源型hostServices变化,需要重新确认授权快照。源码插件升级会比较当前编译发现版本与数据库中的有效版本,避免把文件覆盖误认为运行时升级已经完成。
隔离机制
数据库命名空间
插件自有表必须使用插件ID转换后的snake_case前缀:
主框架表:sys_user、sys_role、sys_menu
插件表:content_notice_notice、org_center_dept、plugin_demo_dynamic_record
系统表使用sys_前缀,插件表使用<plugin_id>_前缀。主框架能力和插件能力的数据完全隔离,避免命名冲突和权限误用。
插件如果需要支持多租户,那么需要自行设计包含tenant_id列,并通过主框架发布的租户过滤能力追加过滤条件。
文件命名空间
插件文件存储应以插件ID作为路径命名空间,例如:
temp/upload/content-notice/
temp/upload/plugin-demo-dynamic/
沙箱隔离
WASM动态插件不能直接访问主框架文件系统、网络或数据库。所有访问都通过hostServices桥接,并受授权快照约束。
多租户字段
插件通过三个字段声明多租户边界:
| 字段 | 可选值 | 说明 |
|---|---|---|
scope_nature | platform_only / tenant_aware | 插件是平台级治理能力,还是可进入租户上下文 |
supports_multi_tenant | true / false | 是否支持租户级安装、开通和数据隔离 |
default_install_mode | global / tenant_scoped | 默认全局启用还是按租户独立启停 |
例如,multi-tenant插件本身是平台级治理插件,使用platform_only和global;内容、组织、审计类插件通常是tenant_aware。
主框架与插件边界
| 规则 | 原因 |
|---|---|
插件不直接依赖主框架的internal/包 | 主框架内部实现可演进,稳定契约由pkg/提供 |
插件菜单使用plugin:<plugin-id>:<key>格式 | 避免与主框架或其他插件冲突 |
安装SQL必须幂等 | 支持重复执行、保留数据后重新安装和升级恢复 |
插件服务逻辑放在backend/internal/service/ | 保持插件后端结构一致,避免包命名混乱 |
插件API使用/x/{plugin-id}/... | 源码插件和动态插件共享统一插件API命名空间,避免占用主框架/api/v1控制面 |
公开静态资源必须声明public_assets | 主框架只托管插件显式授权公开的资源目录 |
| 插件配置通过插件作用域配置服务读取 | 避免插件直接依赖宿主全局配置结构 |
| 插件卸载区分保留数据和清理数据 | 降低误删风险,允许后续重新安装复用数据 |