Repository navigation
Plugin Events
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,并在插件停用/卸载时逆序回卷——事件订阅、路由、扩展点、服务都适用同一条规则。
当一个事件的结果需要被下游策略改写时(例如下单计价、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,也不跨副本。 - 至少一次投递意味着监听器应幂等;不适合强一致的状态同步,仍建议用于通知/异步副作用类场景。
除了在 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);
},
},
],
});声明式监听器在插件激活时自动注册,停用时自动取消。
核心开发
插件开发
主题开发
前端包开发
RESTful API