Repository navigation
Plugin Lifecycle
StackPanel 插件有四个生命周期阶段,每个阶段对应一个回调。
register → activated → deactivated → unregistered
↓ ↓ ↓ ↓
onRegister onActivate onDeactivate (清理)
| 阶段 | 回调 | 触发时机 |
|---|---|---|
| registered | onRegister(ctx) |
插件被发现并注册到运行时(路由绑定,但不可访问) |
| active | onActivate(ctx) |
插件被启用(路由可访问,扩展点生效) |
| registered | onDeactivate(ctx) |
插件被停用(路由 404,扩展点移除) |
| 移除 | — | 插件被卸载/升级(路由和扩展点全部清理) |
插件被 runtime.register() 注册后进入此状态。此时:
- 路由已绑定到 Dispatcher,但
isActive()返回 false,请求会得到 404。 -
onRegister回调已执行。 - 扩展点未注册。
- 事件订阅未生效。
- 路由不可访问。
插件被 runtime.activate() 激活后进入此状态。此时:
- 路由可访问(
isActive()返回 true)。 -
onActivate回调已执行。 - 扩展点已注册(其他插件可消费)。
- 事件订阅已生效。
- 角色权限模板已合并到 RBAC。
插件被 runtime.deactivate() 停用后回到 registered 状态。此时:
-
onDeactivate回调已执行。 - 事件订阅已取消。
- 扩展点已注销。
- 角色权限模板已移除。
- 路由仍绑定但返回 404。
插件被 runtime.unregister() 完全移除。此时:
- 如果仍为 active,先执行 deactivate。
- 扩展点全部注销。
- 路由从 Dispatcher 移除。
- 从运行时 Map 中删除。
激活插件时,运行时会执行以下检查:
-
依赖循环检测 — 深度优先遍历
requires,发现循环则拒绝激活 -
依赖存在性与版本 — 非 optional 依赖必须已注册且满足
range,否则拒绝 -
递归激活依赖 — 依赖插件会被递归
activate() -
consumes 校验 — 非 optional 的消费项,其提供方必须已激活且
provides包含该扩展点 -
inject 校验 —
inject声明的命名服务必须已由其他插件provide - 前端路由冲突(管理端启用/安装路径)— 插件的 frontend pages 不能与已激活插件的页面路径重叠
第 1–5 项在 PluginRuntime.activate() 内;第 6 项由管理端 /admin/plugins 的上传/启用处理在调用 activate() 前执行(assertNoConflictingFrontendPages)。
任一检查失败,激活会回滚(effects 逆序回卷,模型表注销),插件保持 registered 状态。
停用插件时,运行时检查是否有其他已激活的插件依赖本插件。如果有,拒绝停用并返回依赖者列表。
interface PluginDefinition {
onRegister?: (ctx: PluginContext) => void | Promise<void>;
onActivate?: (ctx: PluginContext) => void | Promise<void>;
onDeactivate?: (ctx: PluginContext) => void | Promise<void>;
}PluginContext 提供:
interface PluginContext {
manifest: PluginManifest;
logger: PluginLogger;
events: EventBus;
extensions: ExtensionClient; // 本插件声明模型(customModels)的类型化数据访问
tx<T>(fn: (tx: ExtensionTransaction) => Promise<T>): Promise<T>; // 单事务,禁止嵌套
secrets: PluginSecrets; // 插件命名空间加密密钥
// 内核平台服务
payments: PaymentService; // 支付记录/发起/结算/清扫
wallet: WalletService; // 钱包账户/流水/调整
fx: FxService; // 多币种汇率与换算
auth: AuthService; // 校验密码、签发会话、权限
notifications: NotificationsService; // 站内通知读写
state: StateService; // 共享状态:计数器 / TTL 键 / 锁(见下)
jobs: JobContext; // 内核任务:enqueue / schedule / handle(集群只跑一次)
/** 注册一个可逆副作用;`collect` 收到的一切随插件停用逆序回卷。 */
effect(fn: (collect: (disposable: Disposable) => void) => EffectResult): Disposable;
registerExtension<T>(pointId: string, impl: T): () => void;
getExtensions<T>(pointId: string): T[];
getExtensionsWithOwner<T>(pointId: string): Array<{ pluginId: string; implementation: T }>;
/** 发布一个命名能力服务供其它插件注入。 */
provide<T>(name: string, implementation: T): () => void;
/** 解析服务;缺失返回 undefined。 */
getService<T>(name: string): T | undefined;
/** 解析服务;缺失抛错(`服务 <name> 不可用`)。 */
requireService<T>(name: string): T;
}ctx.state 提供跨请求协调原语。内核实现为路由器:默认进程内 MemoryStateService,initInfra() 连上 Redis 后切换到 RedisStateService(键前缀 sp:state:)。生产环境强制要求 Redis(缺 REDIS_URL 启动即失败),因此多副本下计数器/锁天然共享;开发无 Redis 时回落进程内实现,插件代码无需改动。
interface StateService {
get(key: string): Promise<string | null>;
set(key: string, value: string, ttlMs?: number): Promise<void>;
del(key: string): Promise<void>;
incr(key: string, ttlMs?: number): Promise<number>;
decr(key: string): Promise<number>;
acquire(key: string, ttlMs: number): Promise<boolean>;
release(key: string): Promise<void>;
withLock(key: string, ttlMs: number, fn: () => Promise<void>): Promise<boolean>;
}用于限速窗口、并发上限、key→账号粘性、调度锁等。计数器在首个 incr 时设置 TTL,decr 以 0 为下限。
export default definePlugin({
manifest: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
// 声明本插件依赖的命名服务;缺失时激活直接报错,而非运行期静默失败
inject: ['platform.analytics'],
onRegister(ctx) {
ctx.logger.info('插件已注册');
},
onActivate(ctx) {
ctx.logger.info('插件已激活');
// 订阅事件:放进 ctx.effect,停用时自动取消
ctx.effect(() =>
ctx.events.subscribe('order.created', (payload) => {
ctx.logger.info(`新订单: ${JSON.stringify(payload)}`);
}),
);
// 注册扩展点
ctx.registerExtension('web.nav', {
surface: 'admin',
label: '我的插件',
href: '/admin/my-plugin',
});
// 发布能力服务,供其它插件 ctx.requireService('greeter') 使用
ctx.provide('greeter', { greet: (name: string) => `hi ${name}` });
},
onDeactivate(ctx) {
ctx.logger.info('插件已停用');
// 通过 ctx.effect / registerExtension / provide 注册的一切都会自动回卷,无需手动清理
},
});可逆 effect 是统一规则:
ctx.effect(fn)、ctx.registerExtension(...)、ctx.provide(...)以及声明式eventListeners注册的东西,都由运行时统一追踪,在插件停用/卸载时逆序回卷。 只有当你绕过这些 API 自建外部资源(裸setInterval、连接池、手写new EventEmitter)时才需要自己清理——那种情况请把清理函数交给ctx.effect。
扩展点是「一对多」的能力列表(getExtensions 返回数组);命名服务是「一对一」的能力句柄。当一个插件要为另一个插件提供唯一实现(而非可叠加列表)时,用服务:
// 提供方
ctx.provide('store.catalog', { findProduct: (id) => ... });
// 消费方:声明 inject,激活前内核校验服务存在
export default definePlugin({
manifest: { id: 'my-consumer', name: 'Consumer', version: '1.0.0', requires: ['store'] },
inject: ['store.catalog'],
onActivate(ctx) {
const catalog = ctx.requireService<Catalog>('store.catalog');
},
});- 服务名全局唯一,重复注册(不同插件)会报错。
-
inject在激活前校验:服务缺失时抛插件 X 缺少注入服务 Y,不会进入半激活状态。 - 激活顺序由
manifest.requires表达——提供方插件先激活并provide,消费方再激活。
插件的 enabled 状态持久化在数据库 plugins 表(id/name/version/enabled)。API 与 worker 启动时,syncEnabledPlugins() 读取 enabled=true 的插件并调用 activate()。
注册和注销是运行时行为,不持久化。每次 API 启动时会重新扫描 data/plugins/ 并注册所有已安装插件。
核心开发
插件开发
主题开发
前端包开发
RESTful API