Skip to content

Core Framework

worable edited this page Oct 20, 2026 · 3 revisions

架构概览

StackPanel 采用 微内核(Microkernel)架构。内核提供六项基础设施职责,其余一切功能均为插件。除这六项外,内核还拥有少量平台服务(支付编排、钱包、汇率、站内通知、共享状态),供任意插件通过 ctx.* 复用——这些是横向能力,而非业务功能(见 ADR-0006 与 ADR-0020)。

内核六大职责

职责 实现位置 说明
用户与 RBAC plugins/auth.ts、lib/rbac.ts、auth/session-store.ts 本地认证、服务端会话(Redis/进程内)、角色权限
插件运行时 plugins/runtime.ts、plugins/dispatcher.ts 生命周期管理、路由分发、扩展点
主题引擎 lib/themes.ts、routes/theme.ts ZIP 主题包、CSS Token 校验、激活/预览
事件总线 plugins/events.ts DB Outbox + Redis Pub/Sub,本地即时投递
设置与密钥 lib/platform-info.ts、lib/crypto.ts JSON 设置存储、AES-256-GCM 加密密钥
审计日志 plugins/audit.ts、routes/admin/audit.ts 只追加审计,覆盖全部管理操作

设计原则

API-first

所有功能先有 REST API,后台(Next.js Web)只是 API 的客户端(BFF 模式)。Web 不含业务逻辑,仅负责渲染和 API 转发。

路由隔离

内核路由和插件路由物理隔离:

  • 内核路由:注册为 Fastify 原生路由,由 radix router 匹配,优先级最高
  • 插件路由:存在内存路由表(PluginDispatcher),由 catch-all /* 处理器分发

因为 catch-all 最后注册,内核路由永远优先匹配。

热加载

插件路由表可运行时变更。安装、升级、启停插件无需重启服务,路由即时生效。

类型安全

TypeScript strict 模式(当前 typescript@6)。@stackpanel/sdk 提供完整的类型契约,插件开发者和 API 消费者共享同一套类型定义;@stackpanel/spec 是内核 API 版本与 OpenAPI 投影的单一来源。

请求流转

内核路由请求

HTTP 请求
  → Fastify radix router 匹配内核路由
  → preHandler 链(requireAuth → requireRole)
  → 路由 handler
  → onSend 钩子(安全头注入)
  → 响应

插件路由请求

HTTP 请求
  → Fastify radix router 无匹配(落到 catch-all)
  → PluginDispatcher.match(method, path)
  → runGuards(isActive → requireAuth → admin 时 requirePermission('platform.admin') → permission 校验)
  → 构造 HttpRequest(框架无关视图)
  → 插件 handler(request, reply)
  → reply.send(result)
  → 响应

错误处理

统一错误模型为 RFC 7807 application/problem+json(ADR-0012)。全局 onSend 钩子把所有 >=400 的 JSON 响应归一化,setErrorHandler 处理异常:

  • 业务错误(SDK PluginError):按插件给定的 code/status 输出 problem+json,info 级日志
  • 4xx:{ type, title, status, detail, instance, requestId },响应头回传 x-request-id
  • 5xx:internal.unexpected + 服务器内部错误,详情只入日志
  • 校验失败(Fastify validation):422 validation.invalid

启动流程

initInfra()                     ← 校验基础设施、连接 Redis、确保对象存储桶(server.ts 中先于 buildApp)
buildApp({ redis })
  ├── 注册 Fastify 插件(cookie/cors/multipart/rateLimit/swagger/swagger-ui)
  ├── 全局 onSend 安全头 + RFC 7807 归一化 + setErrorHandler / setNotFoundHandler
  ├── 创建 PluginDispatcher → app.decorate
  ├── createPluginHost() → 内核服务 + PluginRuntime → app.decorate(payments/wallet/fx/auth/notifications/state)
  ├── registerKernelJobs()      ← 内核 sweep 注册为 BullMQ 周期任务
  ├── installExtensionRoutes()  ← Extension 引擎自定义资源路由
  ├── 注册内核路由(health/media/auth/nav/notifications/seo/open-api-v1/sp-v1/mcp/plugin/theme/admin/*)
  ├── registerApiVersioning() + installObservability()
  ├── onReady: initThemes() + seedDefaultBrand()    ← 种子默认主题与品牌
  ├── onReady: host.registerPlugins()               ← seed 内置插件 + 扫描 data/plugins 注册
  └── installBodyRouting() + dispatcher.install()   ← catch-all(最后注册)

start()
  ├── ensureBootstrapAdmin()                  ← 首启创建管理员
  ├── app.ready()                             ← 触发 onReady hooks
  ├── app.pluginHost.syncEnabledPlugins()     ← 按 DB enabled 标志激活插件
  ├── startRuntimeCoherence()                 ← 订阅跨副本运行时失效
  └── app.listen()

事件总线

领域事件走事务性 Outbox + Redis Pub/Sub(ADR-0013):

  • publish 先把事件写入 outbox(至少一次投递,进程崩溃不丢),由 relay 循环认领并本地投递 + 跨副本经 Redis Pub/Sub 分发。
  • 未绑定 outbox 时(开发/测试/早期启动)退化为进程内 EventEmitter,同步投递。
  • EventBus 接口是集群接缝:publish/subscribe 广播事实,intercept/waterfall 是同步环绕管线。
interface EventBus {
  publish(topic: string, payload?: unknown): void;
  subscribe(topic: string, handler: (payload: unknown) => void): () => void;
}

数据库

存储引擎唯一为 PostgreSQL(Prisma provider=postgresql + @prisma/adapter-pg,ADR-0019)。连接串非 postgres:// / postgresql:// 时启动即失败。迁移一律手写 migration.sql 并用 migrate deploy,禁用 migrate dev。应用层无状态,集群一致性依赖 Redis 与对象存储(ADR-0017)。

Clone this wiki locally