Skip to content

Plugin Lifecycle

worable edited this page Oct 12, 2026 · 1 revision

插件生命周期

StackPanel 插件有四个生命周期阶段,每个阶段对应一个回调。

生命周期阶段

register → activated → deactivated → unregistered
   ↓           ↓            ↓              ↓
onRegister  onActivate   onDeactivate    (清理)
阶段 回调 触发时机
registered onRegister(ctx) 插件被发现并注册到运行时(路由绑定,但不可访问)
active onActivate(ctx) 插件被启用(路由可访问,扩展点生效)
registered onDeactivate(ctx) 插件被停用(路由 404,扩展点移除)
移除 — 插件被卸载/升级(路由和扩展点全部清理)

状态说明

registered(已注册)

插件被 runtime.register() 注册后进入此状态。此时:

  • 路由已绑定到 Dispatcher,但 isActive() 返回 false,请求会得到 404。
  • onRegister 回调已执行。
  • 扩展点未注册。
  • 事件订阅未生效。
  • 路由不可访问。

active(已激活)

插件被 runtime.activate() 激活后进入此状态。此时:

  • 路由可访问(isActive() 返回 true)。
  • onActivate 回调已执行。
  • 扩展点已注册(其他插件可消费)。
  • 事件订阅已生效。
  • 角色权限模板已合并到 RBAC。

回到 registered(已停用)

插件被 runtime.deactivate() 停用后回到 registered 状态。此时:

  • onDeactivate 回调已执行。
  • 事件订阅已取消。
  • 扩展点已注销。
  • 角色权限模板已移除。
  • 路由仍绑定但返回 404。

移除(已注销)

插件被 runtime.unregister() 完全移除。此时:

  • 如果仍为 active,先执行 deactivate。
  • 扩展点全部注销。
  • 路由从 Dispatcher 移除。
  • 从运行时 Map 中删除。

激活安全检查

激活插件时,运行时会执行以下检查:

  1. 依赖循环检测 — 深度优先遍历 requires,发现循环则拒绝激活
  2. 依赖存在性与版本 — 非 optional 依赖必须已注册且满足 range,否则拒绝
  3. 递归激活依赖 — 依赖插件会被递归 activate()
  4. consumes 校验 — 非 optional 的消费项,其提供方必须已激活且 provides 包含该扩展点
  5. inject 校验 — inject 声明的命名服务必须已由其他插件 provide
  6. 前端路由冲突(管理端启用/安装路径)— 插件的 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(共享状态)

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。

命名服务(inject / provide)

扩展点是「一对多」的能力列表(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/ 并注册所有已安装插件。

Clone this wiki locally