AIRI 插件平台架构解析:Eventa 传输、双平面模型与多设备编排
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
AIRI(Project AIRI)是一个面向多运行时的插件化陪伴平台,其插件系统围绕「一个 API 表面、多种传输、多设备协同」构建:插件、桥接(Bridge)与查看器(Viewer)通过 Eventa 消息传输通信,插件宿主(Plugin Host)统一装载插件并路由控制与数据流量。本文基于仓库中的架构设计文档 packages/plugin-sdk/docs/design/architecture.md,并结合 plugin-sdk 源码 深入讲解双平面分离、插件生命周期、能力模型、部署模式与验证策略,读完你将掌握 AIRI 插件平台从设计意图到运行时实现的全貌,并能在自己的插件或宿主接入中复用这套思路。
背景:为什么 AIRI 需要一套插件平台
AIRI 需要同时运行在桌面(Electron)、Web 与移动端(Pocket),并保持一套干净的插件 API 表面。插件既要能注册 UI、声明能力,也要能与设备特定的桥接交换数据。为了让系统具备可扩展性,架构文档明确要求:高频数据流必须与生命周期、配置类流量分离。
同时,运行时的依赖编排被有意拆分到独立设计文档(capability-orchestration.md)中,让本文聚焦于平台的「形态」与「平面」划分,避免架构讨论被编排细节稀释。
设计目标与非目标
Goals
- 在多个运行时之间提供统一的插件 API 表面;
- 将生命周期与配置流量从高频数据流中分离;
- 以同一套协议支持本地插件与远程插件;
- 允许多个查看器与桥接通过共享控制平面协同;
- 保持部署灵活性:嵌入式、外部进程或远程插件宿主均可。
Non-goals
- 不在本文中定义完整的插件生命周期状态机(交给 capability-orchestration.md);
- 不规定具体 UI 布局或查看器实现;
- 不实现 Eventa 适配器之外的新传输层。
总体提案:控制平面与数据平面分离
架构的核心提案非常清晰:
- 所有控制与数据流量统一走Eventa;
- 用专用控制平面承载配置、权限、UI 注册与路由策略;
- 用数据平面承载音频、视觉、遥测等高吞吐流;
- 插件运行在插件宿主内,宿主负责装载插件入口点并暴露 SDK;
- 桥接被视为仅提供数据与动作的设备侧集成,不拥有 UI。
控制平面(Control Plane)
控制平面承担生命周期、配置、路由策略、权限与 UI 贡献。文档列出的典型控制消息包括:
control:hellocontrol:announcecontrol:plugin:registercontrol:plugin:config:getcontrol:plugin:config:setcontrol:capability:grantcontrol:capability:revokecontrol:ui:register
数据平面(Data Plane)
数据平面承担实时、高吞吐的流式数据。典型数据消息包括:
data:context:updatedata:vision:framedata:audio:streamdata:transcriptdata:character:output
传输选项
两个平面都使用 Eventa 消息,传输层提供两种可选形态:
- 两个 WebSocket 端点(控制与数据各自独立);
- 一个多路复用连接 + 命名空间(namespace 隔离)。
这种设计的动机在文档 Q&A 中直接给出:生命周期流量与高频数据流对可靠性(reliability)和服务质量(QoS)的需求不同,物理或逻辑上隔离可以各自按需优化。
插件宿主与查看器
插件宿主(Plugin Host)是一个 Node 进程,职责包括:
- 装载插件入口点(entrypoint);
- 暴露 AIRI SDK;
- 注册 UI 贡献;
- 协商能力(capabilities);
- 连接控制平面与数据平面。
在源码层面,这一角色由 core.ts 中的ExtensionHost类承担。从类定义与注释可以推断,宿主内部组合了多个服务:ExtensionSessionService(会话管理)、DependencyService(能力注册表)、KitRegistryService(Kit 注册)、PermissionService(权限校验)、ResourceService(资源提供)、KitApiBindingRegistryService(模块绑定管理)。文档注释给出的调用链为:
caller -> ExtensionHost.start -> FileSystemLoader.resolveEntrypointFor -> FileSystemLoader.loadExtensionFor -> ExtensionHost.startExtension查看器(Viewer)负责渲染 UI 与角色输出,文档给出的示例包括:
- 带 Configurator 特性的 Electron Stage;
- Web Configurator 客户端;
- Pocket Stage 客户端。
插件生命周期概述
架构文档给出了与 core.ts 生命周期注释 相呼应的生命周期图,覆盖模块声明(announcement)、配置与能力阶段:
在源码实现中,startExtension(core.ts)揭示了对应的运行时细节:
- 每个扩展会话拥有
phase状态,取值'setting-up' | 'ready' | 'failed' | 'stopped'; - 会话上下文
ctx提供kits(扩展级 Kit 注册表)与modules.register(...)(模块级注册,返回带独立订阅与清理回调的ExtensionModuleContext); - 权限采用双层模型:扩展授权是包/会话级「天花板」,模块级 Kit 使用按「扩展授权 ∩ 模块请求」推导的模块授权进行校验(见 core.ts 头部注释);
- 失败路径会把会话标记为
failed并执行完整清理(cleanupExtensionSession),避免泄漏绑定、订阅与模块资源。
更细的能力依赖编排、等待阶段与就绪门控(readiness gate)在 capability-orchestration.md 中定义。
桥接与远程插件
桥接(Bridge)把外部设备与服务接入 AIRI。它们不拥有 UI,只提供数据与动作。文档示例包括:
- VS Code 扩展:提供编辑器上下文与命令;
- 浏览器扩展:提供页面上下文;
- Minecraft 服务:提供游戏事件与命令。
远程插件(Remote Plugin)是任意语言编写的服务,通过 Eventa 连接并注册能力。文档明确它们更适用于服务端集成与非 JS/TS 技术栈——远程插件只要求能讲 WebSocket 上的 Eventa 协议,不需要 npm 或 JS 运行时。
传输抽象
所有 SDK 调用都是传输无关的。宿主决定通信走本地 IPC 还是远程 RPC,而插件 API 保持不变。
这一点在 channels/index.ts 中有实现支撑:createExtensionChannelScope创建「扩展身份 + Eventa context」的通道作用域,createModuleChannelScope从扩展作用域派生出模块身份,并复用同一 Eventa context。身份信息(id、sessionId、version)随上下文绑定,使协议流量天然携带归属信息。
配套设计文档 multi-transport.md 进一步规划了PluginTransport联合类型:
export type PluginTransport = | { kind: 'in-memory' } | { kind: 'websocket', url: string, protocols?: string[] } | { kind: 'web-worker', worker: Worker } | { kind: 'node-worker', worker: import('node:worker_threads').Worker } | { kind: 'electron', target: 'main' | 'renderer', webContentsId?: number }其核心原则是:每个插件实例一个 Eventa context,由宿主的createPluginContext(transport)在生命周期方法调用前创建,并通过createApis(ctx)把 API 绑定到该 context——本地插件(in-memory / worker)与远程插件(WebSocket)共享同一 API 表面,互不串扰。
能力模型(Capability Model)
每个节点在注册时声明能力,控制平面根据策略授予/拒绝权限并路由请求。文档给出的能力示例:
context.readcontext.writeui.panelui.widgetvision.capturevision.streamdevice.mobile.sensors
源码层面,能力由 dependencies.ts 中的DependencyService维护,它是一个带快照与等待原语的内存能力注册表:
announce(key, metadata):声明能力,状态置为announced;markReady(key, metadata):状态置为ready并唤醒所有等待者;markDegraded/withdraw:置为degraded/withdrawn;waitFor(key, timeoutMs = 15000):若已 ready 立即返回,否则注册等待回调,超时抛出Capability ... is not ready after ...错误;waitForMany(keys, timeoutMs):并行等待多个能力。
协议侧,capabilities/index.ts 定义了跨边界的 RPC 契约:protocolCapabilityWait(proj-airi:plugin-sdk:apis:protocol:capabilities:wait)与protocolCapabilitySnapshot(...capabilities:snapshot),返回可序列化的CapabilityDescriptor:
interface CapabilityDescriptor { key: string state: 'announced' | 'ready' | 'degraded' | 'withdrawn' metadata?: Record<string, unknown> updatedAt: number }这与 capability-orchestration.md 的「快照权威、事件增量」原则一致:等待者先查快照,能力已就绪则立即放行,避免「错过就绪信号」的竞态。
部署模式
插件宿主支持三种部署形态:
- 嵌入式:内嵌在 Electron 主进程中,安装即用(install-and-go);
- 外部 Node 进程:支持热重载与进程隔离;
- 远程服务器:实现跨设备连续性。
这与 package.json 的导出结构 相印证:./plugin-host子路径通过条件导出区分node(runtimes/node/index.mjs)与default(runtimes/web/index.mjs)实现,且运行时字面量被限定为electron | node | web(见 types.ts)。
清单与入口点(Manifest & Entrypoints)
插件通过清单文件声明元数据,并提供运行时入口点。文档给出的示例:
{ "id": "airi.vscode", "name": "AIRI VS Code", "version": "1.0.0", "capabilities": ["context.read", "ui.panel", "commands"], "entrypoints": { "node": "./dist/node/index.js" } }实际宿主代码中,清单格式由ExtensionManifestV1(types.ts)定义,比文档示例更严格、更完整:
export interface ExtensionManifestV1 { apiVersion: 'v1' entrypoints: { default?: string electron?: string node?: string web?: string } id: string kind: 'manifest.extension.airi.moeru.ai' permissions: ModulePermissionDeclaration }要点说明:
kind固定为manifest.extension.airi.moeru.ai,作为清单类型判别器;- 入口点可按运行时细分,
FileSystemLoader.resolveEntrypointFor(fs.ts)的解析顺序为:entrypoints.<runtime>→entrypoints.default→entrypoints.electron(兼容遗留清单); permissions声明包级/会话级权限天花板,其 schema(permissionDeclarationSchema)将权限划分为五类区域,每类各有动作白名单:apis:invoke/emitcapabilities:wait/snapshotpipelines:hook/process/emit/manageprocessors:register/execute/manageresources:read/write/subscribe
插件入口点本身通过defineExtension(...)(define.ts)导出,约定id必须与清单中的id一致(startExtension会做一致性校验),setup(ctx)是通用的编写入口,通过ctx.kits使用宿主安装的 Kit。
Kit API 命名与 Eventa 契约
仓库 README(packages/plugin-sdk/README.md)补充了 Kit 开发的命名约定,与架构的「SDK 调用传输无关」原则直接呼应:
| 名称 | 含义 |
|---|---|
gameletKitApis | Kit 包导出的共享 Eventa API 契约(通常是defineInvokeEventa(...)条目组成的映射) |
gameletKitService | 宿主侧 Kit 行为实现,拥有真实副作用(如 UI 挂载/更新/清理) |
gameletKit | 供ctx.kits.use(...)消费的 Kit 定义,拥有身份、版本、可用性策略与客户端创建 |
gamelets | 返回给插件作者的客户端实例(多操作时建议复数命名空间) |
createGameletKit(...) | 把依赖注入gameletKit的工厂,支持本地客户端与远程 Eventa 客户端 |
关键约束:共享的产物是 Eventa API 契约,而不是实现函数。本地客户端可直接调用gameletKitService,远程客户端通过 Eventa 调用同一 API,两者对外暴露相同的编写形态;跨进程/网络时需要复用共享的 Eventa invoke 契约,不得发明invokeGamelet之类的 Kit 专属传输方法名。
验证与测试
验收标准(Criteria)
- 插件可被宿主装载,并注册 UI 与能力;
- 桥接可连接并被控制平面发现;
- 数据平面流与控制平面流量保持隔离;
- 同一插件 API 表面在桌面、Web、移动端表现一致。
测试与 QA(Test & QA)
- 集成测试:host + viewer + bridge,验证控制平面路由;
- 集成测试:数据平面以高频数据源流式传输;
- 兼容性测试:同一插件入口点在多个运行时下运行。
从源码结构看,这些验收标准已被测试工程承接:plugin-host目录下存在 core.test.ts、runtimes/shared/services/下的dependencies.test.ts、permissions.test.ts、kits.test.ts、bindings.test.ts等单测,以及testdata/中覆盖正常插件、错误插件、注入宿主 API 插件、无连接插件、可停止入口点等场景的测试夹具。
进度与后续规划
- Status:Active design(活跃设计阶段);
- Next Steps:
- 让运行时文档与最新的插件上下文、传输策略对齐;
- 从 capability-orchestration.md 集成能力注册表与就绪门控生命周期迁移;
- 按语言扩充远程插件示例。
常见问题(Q&A)
Q: 为什么拆分控制平面与数据平面?A: 生命周期流量与高频数据流对可靠性和 QoS 的需求不同,物理或逻辑隔离可以各自按需优化,避免高频流拖垮配置/权限等关键控制消息。
Q: 桥接会渲染 UI 吗?A: 不会。UI 通过控制平面贡献给查看器,桥接只提供数据与动作。
Q: 远程插件可以不使用 JS 或 npm 吗?A: 可以。远程插件只需通过 WebSocket 讲 Eventa 协议,语言无关。
相关文档
- 多传输插件上下文 Multi-Transport Plugin Contexts
- 能力导向的模块编排 Capability-Oriented Module Orchestration
- Plugin SDK 源码
- Plugin SDK 说明文档
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考