Skip to content

Plugin Events

worable edited this page Oct 12, 2026 · 1 revision

事件总线

StackPanel 提供进程内的事件发布/订阅机制,插件之间通过事件松耦合通信。

接口

interface EventBus {
  publish(topic: string, payload?: unknown): void;
  subscribe(topic: string, handler: (payload: unknown) => void): () => void;
  intercept<T>(topic: string, listener: (value: T, next: (value: T) => T) => T): () => void;
  waterfall<T>(topic: string, value: T, terminal: (value: T) => T): T;
}

事件总线通过 PluginContext 注入:ctx.events。

总线提供两种语义不同的分发模式,按用途选择。

publish / subscribe 是事实广播:监听者只观察,不能改变已发生的事。单个监听器抛错会被隔离,不影响发布者和其它监听器。

intercept / waterfall 是同步的 around-pipeline:拦截器可以改写值或短路,需要影响到调用方结果的场景用它,见下方「拦截事件(waterfall)」。

已声明的平台事件

PlatformEvents(packages/sdk/src/plugin.ts)内置声明了以下 topic,供类型化订阅;未声明的 topic 仍可用(payload 为 unknown):

事件 payload 发布方
plugin.activated { pluginId } 内核(激活时)
plugin.deactivated { pluginId } 内核(停用时)
notification.created { notification: NotificationView } 内核通知服务
payment.settled { orderId: string | null; paymentId: string } 内核支付服务
order.paid { orderId; channelCode?; status?; total?; currency? } store
ticket.created { ticketId: string; userId: string } ticket
ticket.message.added { ticketId: string; authorId: string } ticket
ticket.status.changed { ticketId: string; status: string } ticket
store.order.price OrderPriceContext(waterfall,非广播) store

发布事件

onActivate(ctx) {
  // 发布业务事件
  ctx.events.publish('order.created', {
    orderId: 'abc123',
    userId: 'user-456',
    total: 9900,
  });
}

publish 先写入事务性 outbox,再由 relay 投递到本地监听者并(配置了 Redis 时)广播给其他副本;outbox 绑定前(开发/测试/早期启动)退化为进程内即时投递。监听器以同步方式被调用,若为 async 函数需自行处理 Promise rejection。单个监听器抛错会被隔离,不影响发布者与其他监听器。

订阅事件

onActivate(ctx) {
  ctx.events.subscribe('order.created', (payload) => {
    const order = payload as { orderId: string; total: number };
    ctx.logger.info(`新订单 ${order.orderId}: ${order.total} 分`);
  });
}

subscribe 返回取消订阅函数。声明式 eventListeners 的订阅由运行时自动追踪和清理。通过 ctx.events.subscribe() 在 onActivate 中手动订阅时,把它放进 ctx.effect 即随插件停用自动取消:

onActivate(ctx) {
  ctx.effect(() => ctx.events.subscribe('order.created', (payload) => { /* ... */ }));
}

ctx.effect(fn) 会收集 fn 返回的所有 disposer,并在插件停用/卸载时逆序回卷——事件订阅、路由、扩展点、服务都适用同一条规则。

拦截事件(waterfall)

当一个事件的结果需要被下游策略改写时(例如下单计价、payload 整形),用 waterfall 而不是 publish。拦截器按注册顺序包裹成洋葱式的 next 链:

onActivate(ctx) {
  // 促销策略:对商店算出的单价打 9 折
  ctx.effect(() =>
    ctx.events.intercept<{ unit: number }>('store.order.price', (value, next) =>
      next({ ...value, unit: Math.round(value.unit * 0.9) }),
    ),
  );
}

内核/业务方运行 waterfall:

const priced = ctx.events.waterfall('store.order.price', { unit: 10000 }, (value) => value);
// priced.unit === 9000

规则:

  • 拦截器必须调用 next(value) 委托下游(可传入改写后的值),或直接 return 一个值短路整条链。
  • 同一个拦截器内重复调用 next() 会抛错。
  • terminal 是链尾的终值函数;没有拦截器时直接返回 terminal(value)。
  • waterfall 是同步的,由调用方直接取返回值——不适合需要 await 的副作用,那类用 publish 或扩展点。

intercept 返回 disposer,放进 ctx.effect 即随插件停用自动撤销。topic 通过 PlatformEvents 接口声明合并做类型化(见 @stackpanel/sdk),未声明的 topic 仍可用(payload 为 unknown)。

事件命名约定

建议使用 <domain>.<action> 格式:

事件 说明
order.created 订单创建
order.paid 订单支付完成
order.cancelled 订单取消
ticket.created 工单创建
ticket.message.added 工单回复
payment.settled 订单支付结算完成(内核发布)

集群行为

事件总线是事务性 outbox + Redis Pub/Sub:publish 落 outbox_events 表,relay 循环认领未投递行并在本地重投 + 广播到 Redis 频道(sp:pubsub:events),其他副本订阅后再本地投递。因此配置 Redis 时事件可跨副本传播,且进程崩溃不会丢事件(至少一次)。

  • 本地投递由 relay 负责(不再在 publish 内直接 emit),存在轻微延迟;relay 间隔默认 2s。
  • 未配置 Redis(开发回退)时仅在进程内传播。
  • intercept / waterfall 是同步、进程内的,不经 outbox,也不跨副本。
  • 至少一次投递意味着监听器应幂等;不适合强一致的状态同步,仍建议用于通知/异步副作用类场景。

eventListener 扩展点

除了在 onActivate 中手动订阅,插件还可以通过 eventListeners 声明式注册:

export default definePlugin({
  manifest: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
  eventListeners: [
    {
      topic: 'order.created',
      handler: (payload) => {
        console.log('订单创建:', payload);
      },
    },
  ],
});

声明式监听器在插件激活时自动注册,停用时自动取消。

Clone this wiki locally