Skip to content

Core Framework

worable edited this page Oct 12, 2026 · 2 revisions

架构概览

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

内核六大职责

职责 实现位置 说明
用户与 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 只追加审计,覆盖全部管理操作

插件执行模型

插件定义的 execution 字段决定插件运行在哪个进程边界,取值为 isolated 或 trusted。第三方包默认 isolated,内置包始终 trusted;未声明该字段的旧包在生产环境按 isolated 加载,在开发环境按 trusted 加载。

isolated:独立子进程

包的入口在独立 Node 子进程中运行,API 进程只保留清单、路由代理与 RPC 桥。ctx.* 通过 IPC RPC 暴露给 worker,协议带版本与身份校验:

  • 每条消息携带 protocol: 1 与已认证的 pluginId,协议版本或插件 id 不匹配的消息被丢弃。
  • 双向 10 秒超时、单条消息 1 MiB 上限、每个 worker 64 个在途请求上限;超时拒绝调用方并发尽力发送 cancel。
  • Date、Buffer、bigint、Error 以 { "__spType": ... } 标记编码,循环引用被拒绝;类实例、函数、流、socket 不跨边界。
  • worker 退出会把插件置为 failed,清理其路由/模型/任务并拒绝在途请求;下次激活启动新 worker;退出累计达到 3 次后运行时熔断,必须重新注册修复后的包才能恢复。
  • 原始流式路由与无法表示为 JSON Schema 的自定义模型 schema 在 worker 启动时被拒绝。
  • ctx.events.intercept() 与 ctx.events.waterfall() 依赖同步 next() 与同步返回值语义,无法跨异步 IPC 保真,在 isolated 模式下调用会得到明确错误。

trusted:内核同进程

内置插件在所有环境下都是 trusted;非内置插件只能在非生产环境请求 execution: "trusted",生产环境校验会拒绝该模式。

隔离边界

permissions 与路由上的 permission 是路由准入与前端可见性控制,不是 OS 沙盒;isolated 也不是完整容器。PluginContext 不提供数据库句柄,插件只能通过 Extension 引擎读写自己声明的模型。

设计原则

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。全局 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:

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

数据库

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

Clone this wiki locally