# WASM 插件系统实施计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为 ERP 平台引入 WASM 运行时插件系统,使行业模块可动态安装/启用/停用。 **Architecture:** 基础模块(auth/config/workflow/message)保持 Rust 编译时,新增 `erp-plugin-runtime` crate 封装 Wasmtime 运行时。插件通过宿主代理 API 访问数据库和事件总线,前端使用配置驱动 UI 渲染引擎自动生成 CRUD 页面。 **Tech Stack:** Rust + Wasmtime 27+ / WIT (wit-bindgen 0.24+) / SeaORM / Axum 0.8 / React 19 + Ant Design 6 + Zustand 5 **Spec:** `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` --- ## File Structure ### 新建文件 ``` crates/erp-plugin-runtime/ ├── Cargo.toml ├── wit/ │ └── plugin.wit # WIT 接口定义 └── src/ ├── lib.rs # crate 入口 ├── manifest.rs # plugin.toml 解析 ├── engine.rs # Wasmtime 引擎封装 ├── host_api.rs # 宿主 API(db/event/config/log) ├── loader.rs # 插件加载器 ├── schema.rs # 动态建表逻辑 ├── error.rs # 插件错误类型 └── wasm_module.rs # ErpModule trait 的 WASM 适配器 crates/erp-server/migration/src/ └── m20260413_000032_create_plugins_table.rs # plugins + plugin_schema_versions 表 crates/erp-server/src/ └── handlers/ └── plugin.rs # 插件管理 + 数据 CRUD handler apps/web/src/ ├── api/ │ └── plugins.ts # 插件 API service ├── stores/ │ └── plugin.ts # PluginStore (Zustand) ├── pages/ │ ├── PluginAdmin.tsx # 插件管理页面 │ └── PluginCRUDPage.tsx # 通用 CRUD 渲染引擎 └── components/ └── DynamicMenu.tsx # 动态菜单组件 ``` ### 修改文件 ``` Cargo.toml # 添加 erp-plugin-runtime workspace member crates/erp-core/src/module.rs # 升级 ErpModule trait v2 crates/erp-core/src/events.rs # 添加 subscribe_filtered crates/erp-core/src/lib.rs # 导出新类型 crates/erp-auth/src/module.rs # 迁移到 v2 trait crates/erp-config/src/module.rs # 迁移到 v2 trait crates/erp-workflow/src/module.rs # 迁移到 v2 trait crates/erp-message/src/module.rs # 迁移到 v2 trait crates/erp-server/src/main.rs # 使用新注册系统 + 加载 WASM 插件 crates/erp-server/src/state.rs # 添加 PluginState crates/erp-server/migration/src/lib.rs # 注册新迁移 apps/web/src/App.tsx # 添加动态路由 + PluginAdmin 路由 apps/web/src/layouts/MainLayout.tsx # 使用 DynamicMenu ``` --- ## Chunk 1: ErpModule Trait v2 迁移 + EventBus 扩展 ### Task 1: 升级 ErpModule trait **Files:** - Modify: `crates/erp-core/src/module.rs` - Modify: `crates/erp-core/src/lib.rs` - [ ] **Step 1: 升级 ErpModule trait — 添加新方法(全部有默认实现)** 在 `crates/erp-core/src/module.rs` 中,保留所有现有方法签名不变,追加新方法: ```rust use std::collections::HashMap; // 新增类型 pub enum ModuleType { Native, Wasm, } pub struct ModuleHealth { pub status: String, pub details: Option, } pub struct ModuleContext { pub db: sea_orm::DatabaseConnection, pub event_bus: crate::events::EventBus, pub config: Arc, } // 在 ErpModule trait 中追加(不改现有方法): fn id(&self) -> &str { self.name() } // 默认等于 name fn module_type(&self) -> ModuleType { ModuleType::Native } async fn on_startup(&self, _ctx: &ModuleContext) -> crate::error::AppResult<()> { Ok(()) } async fn on_shutdown(&self) -> crate::error::AppResult<()> { Ok(()) } async fn health_check(&self) -> crate::error::AppResult { Ok(ModuleHealth { status: "ok".into(), details: None }) } fn public_routes(&self) -> Option { None } // 需要 axum 依赖 fn protected_routes(&self) -> Option { None } fn migrations(&self) -> Vec> { vec![] } fn config_schema(&self) -> Option { None } ``` > **注意:** `on_tenant_created/deleted` 的签名暂不改动(加 ctx 参数是破坏性变更),在 Task 2 中单独处理。 - [ ] **Step 2: 升级 ModuleRegistry — 添加索引 + 拓扑排序 + build_routes** 在同一个文件中扩展 `ModuleRegistry`: ```rust impl ModuleRegistry { pub fn get_module(&self, id: &str) -> Option<&Arc> { ... } pub fn build_routes(&self) -> (axum::Router, axum::Router) { // 遍历 modules,收集 public_routes + protected_routes } fn topological_sort(&self) -> crate::error::AppResult>> { // 基于 dependencies() 的 Kahn 算法拓扑排序 } pub async fn startup_all(&self, ctx: &ModuleContext) -> crate::error::AppResult<()> { // 按拓扑顺序调用 on_startup } pub async fn health_check_all(&self) -> HashMap { ... } } ``` - [ ] **Step 3: 更新 lib.rs 导出** `crates/erp-core/src/lib.rs` 追加: ```rust pub use module::{ModuleType, ModuleHealth, ModuleContext}; ``` - [ ] **Step 4: 更新 erp-core Cargo.toml 添加 axum 依赖** `crates/erp-core/Cargo.toml` 的 `[dependencies]` 添加: ```toml axum = { workspace = true } sea-orm-migration = { workspace = true } ``` - [ ] **Step 5: 运行 `cargo check --workspace` 确保现有模块编译通过(所有新方法有默认实现)** - [ ] **Step 6: 迁移四个现有模块的 routes** 对 `erp-auth/src/module.rs`、`erp-config/src/module.rs`、`erp-workflow/src/module.rs`、`erp-message/src/module.rs`: - 将 `pub fn public_routes()` 关联函数改为 `fn public_routes(&self) -> Option` trait 方法 - 同样处理 `protected_routes` - 添加 `fn id()` 返回与 `name()` 相同值 每个模块的改动模式: ```rust // 之前: pub fn public_routes() -> Router where ... { Router::new().route(...) } // 之后: fn public_routes(&self) -> Option { Some(axum::Router::new().route("/auth/login", axum::routing::post(auth_handler::login)).route("/auth/refresh", axum::routing::post(auth_handler::refresh))) } fn protected_routes(&self) -> Option { Some(...) } fn id(&self) -> &str { "auth" } // 与 name() 相同 ``` - [ ] **Step 7: 更新 main.rs 使用 build_routes** `crates/erp-server/src/main.rs`: ```rust // 替换手动 merge 为: let (public_mod, protected_mod) = registry.build_routes(); let public_routes = Router::new() .merge(handlers::health::health_check_router()) .merge(public_mod) // 替代 erp_auth::AuthModule::public_routes() .route("/docs/openapi.json", ...) ...; let protected_routes = protected_mod // 替代手动 merge 四个模块 .merge(handlers::audit_log::audit_log_router()) ...; ``` - [ ] **Step 8: 运行 `cargo check --workspace` 确认全 workspace 编译通过** - [ ] **Step 9: 运行 `cargo test --workspace` 确认测试通过** - [ ] **Step 10: Commit** ``` feat(core): upgrade ErpModule trait v2 with lifecycle hooks, route methods, and auto-collection ``` --- ### Task 2: EventBus subscribe_filtered 扩展 **Files:** - Modify: `crates/erp-core/src/events.rs` - [ ] **Step 1: 添加类型化订阅支持** 在 `events.rs` 中扩展 `EventBus`: ```rust use std::sync::RwLock; pub type EventHandler = Box; pub type SubscriptionId = Uuid; pub struct EventBus { sender: broadcast::Sender, handlers: Arc>>>, } impl EventBus { pub fn subscribe_filtered( &self, event_type: &str, handler: EventHandler, ) -> SubscriptionId { let id = Uuid::now_v7(); let mut handlers = self.handlers.write().unwrap(); handlers.entry(event_type.to_string()) .or_default() .push((id, handler)); id } pub fn unsubscribe(&self, id: SubscriptionId) { let mut handlers = self.handlers.write().unwrap(); for (_, list) in handlers.iter_mut() { list.retain(|(sid, _)| *sid != id); } } } ``` 修改 `broadcast()` 方法,在广播时同时分发给 `handlers` 中匹配的处理器。 - [ ] **Step 2: 运行 `cargo test --workspace`** - [ ] **Step 3: Commit** ``` feat(core): add typed event subscription to EventBus ``` --- ## Chunk 2: 数据库迁移 + erp-plugin-runtime Crate ### Task 3: 插件数据库表迁移 **Files:** - Create: `crates/erp-server/migration/src/m20260413_000032_create_plugins_table.rs` - Modify: `crates/erp-server/migration/src/lib.rs` - [ ] **Step 1: 编写迁移文件** 创建 `plugins`、`plugin_schema_versions`、`plugin_event_subscriptions` 三张表(DDL 参见 spec §7.1)。 - [ ] **Step 2: 注册到 lib.rs 的迁移列表** - [ ] **Step 3: 运行 `cargo run -p erp-server` 验证迁移执行** - [ ] **Step 4: Commit** ``` feat(db): add plugins, plugin_schema_versions, and plugin_event_subscriptions tables ``` --- ### Task 4: 创建 erp-plugin-runtime crate 骨架 **Files:** - Create: `crates/erp-plugin-runtime/Cargo.toml` - Create: `crates/erp-plugin-runtime/src/lib.rs` - Create: `crates/erp-plugin-runtime/src/error.rs` - Create: `crates/erp-plugin-runtime/src/manifest.rs` - Modify: `Cargo.toml` (workspace members) - [ ] **Step 1: 创建 Cargo.toml** ```toml [package] name = "erp-plugin-runtime" version.workspace = true edition.workspace = true [dependencies] erp-core = { workspace = true } wasmtime = "27" wasmtime-wasi = "27" wit-bindgen = "0.24" serde = { workspace = true } serde_json = { workspace = true } toml = "0.8" uuid = { workspace = true } chrono = { workspace = true } sea-orm = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } axum = { workspace = true } async-trait = { workspace = true } ``` - [ ] **Step 2: 更新根 Cargo.toml workspace members + dependencies** - [ ] **Step 3: 实现 manifest.rs — PluginManifest 类型 + 解析** 定义 `PluginManifest`、`PluginInfo`、`PermissionSet`、`EntityDef`、`FieldDef`、`PageDef` 等结构体,实现 `fn parse(toml_str: &str) -> Result`。 - [ ] **Step 4: 实现 error.rs** ```rust #[derive(Debug, thiserror::Error)] pub enum PluginError { #[error("Manifest 解析失败: {0}")] ManifestParse(String), #[error("WASM 加载失败: {0}")] WasmLoad(String), #[error("Host API 错误: {0}")] HostApi(String), #[error("插件未找到: {0}")] NotFound(String), #[error("依赖未满足: {0}")] DependencyUnmet(String), } ``` - [ ] **Step 5: 实现 lib.rs — crate 入口 + re-exports** - [ ] **Step 6: 运行 `cargo check --workspace`** - [ ] **Step 7: Commit** ``` feat(plugin): create erp-plugin-runtime crate with manifest parsing ``` --- ### Task 5: WIT 接口定义 **Files:** - Create: `crates/erp-plugin-runtime/wit/plugin.wit` - [ ] **Step 1: 编写 WIT 文件** 参见 spec 附录 D.1 的完整 `plugin.wit` 内容(host interface + plugin interface + plugin-world)。 - [ ] **Step 2: 验证 WIT 语法** ```bash cargo install wit-bindgen-cli wit-bindgen rust ./crates/erp-plugin-runtime/wit/plugin.wit --out-dir /tmp/test-bindgen ``` - [ ] **Step 3: Commit** ``` feat(plugin): define WIT interface for host-plugin contract ``` --- ## Chunk 3: Host API + 插件加载器 ### Task 6: 实现 Host API 层 **Files:** - Create: `crates/erp-plugin-runtime/src/host_api.rs` - [ ] **Step 1: 实现 PluginHostState 结构体** 持有 `db`、`tenant_id`、`plugin_id`、`event_bus` 等上下文。实现 `db_insert`、`db_query`、`db_update`、`db_delete`、`db_aggregate` 方法,每个方法都: 1. 自动注入 `tenant_id` 过滤 2. 自动注入标准字段(id, created_at 等) 3. 参数化 SQL 防注入 4. 自动审计日志 - [ ] **Step 2: 注册为 Wasmtime host functions** 使用 `wasmtime::Linker::func_wrap` 将 host_api 方法注册到 WASM 实例。 - [ ] **Step 3: 编写单元测试** 使用 mock 数据库测试 db_insert 自动注入 tenant_id、db_query 自动过滤。 - [ ] **Step 4: Commit** ``` feat(plugin): implement host API layer with tenant isolation ``` --- ### Task 7: 实现插件加载器 + 动态建表 **Files:** - Create: `crates/erp-plugin-runtime/src/engine.rs` - Create: `crates/erp-plugin-runtime/src/loader.rs` - Create: `crates/erp-plugin-runtime/src/schema.rs` - Create: `crates/erp-plugin-runtime/src/wasm_module.rs` - [ ] **Step 1: engine.rs — Wasmtime Engine 封装** 单例 Engine + Store 工厂方法,配置内存限制(64MB 默认)、fuel 消耗限制。 - [ ] **Step 2: schema.rs — 从 manifest 动态建表** `create_entity_table(db, entity_def)` 函数:生成 `CREATE TABLE IF NOT EXISTS plugin_{name} (...)` SQL,包含所有标准字段 + tenant_id 索引。 - [ ] **Step 3: loader.rs — 从数据库加载 + 实例化** `load_plugins(db, engine, event_bus) -> Vec`:查询 `plugins` 表中 status=enabled 的记录,实例化 WASM,调用 init(),注册事件处理器。 - [ ] **Step 4: wasm_module.rs — WasmModule(实现 ErpModule trait)** 包装 WASM 实例,实现 ErpModule trait 的各方法(调用 WASM 导出函数)。 - [ ] **Step 5: 集成测试** 测试完整的 load → init → db_insert → db_query 流程(使用真实 PostgreSQL)。 - [ ] **Step 6: Commit** ``` feat(plugin): implement plugin loader with dynamic schema creation ``` --- ### Task 8: 插件管理 API + 数据 CRUD API **Files:** - Create: `crates/erp-server/src/handlers/plugin.rs` - Modify: `crates/erp-server/src/main.rs` - Modify: `crates/erp-server/src/state.rs` - [ ] **Step 1: 实现 plugin handler** 上传(解析 plugin.toml + 存储 wasm_binary)、列表、详情、启用(建表+写状态)、停用、卸载(软删除)。 - [ ] **Step 2: 实现插件数据 CRUD** `GET/POST/PUT/DELETE /api/v1/plugins/{plugin_id}/{entity}` — 动态路由,从 manifest 查找 entity,调用 host_api 执行操作。 - [ ] **Step 3: 注册路由到 main.rs** - [ ] **Step 4: 添加 PluginState 到 state.rs** ```rust impl FromRef for erp_plugin_runtime::PluginState { ... } ``` - [ ] **Step 5: 运行 `cargo test --workspace`** - [ ] **Step 6: Commit** ``` feat(server): add plugin management and dynamic CRUD API endpoints ``` --- ## Chunk 4: 前端配置驱动 UI ### Task 9: PluginStore + API Service **Files:** - Create: `apps/web/src/api/plugins.ts` - Create: `apps/web/src/stores/plugin.ts` - [ ] **Step 1: plugins.ts API service** 接口类型定义 + API 函数:`listPlugins`、`getPlugin`、`uploadPlugin`、`enablePlugin`、`disablePlugin`、`uninstallPlugin`、`getPluginConfig`、`updatePluginConfig`、`getPluginData`、`createPluginData`、`updatePluginData`、`deletePluginData`。 - [ ] **Step 2: plugin.ts PluginStore** ```typescript interface PluginStore { plugins: PluginInfo[]; loading: boolean; fetchPlugins(): Promise; getPageConfigs(): PluginPageConfig[]; } ``` 启动时调用 `fetchPlugins()` 加载已启用插件列表及页面配置。 - [ ] **Step 3: Commit** ``` feat(web): add plugin API service and PluginStore ``` --- ### Task 10: PluginCRUDPage 通用渲染引擎 **Files:** - Create: `apps/web/src/pages/PluginCRUDPage.tsx` - [ ] **Step 1: 实现 PluginCRUDPage 组件** 接收 `PluginPageConfig` 作为 props,渲染: - **SearchBar**: 从 `filters` 配置生成 Ant Design Form.Item 搜索条件 - **DataTable**: 从 `columns` 配置生成 Ant Design Table 列 - **FormDialog**: 从 `form` 配置或自动推导的 `schema.entities` 字段生成新建/编辑 Modal 表单 - **ActionBar**: 从 `actions` 配置生成操作按钮 API 调用统一走 `/api/v1/plugins/{plugin_id}/{entity}` 路径。 - [ ] **Step 2: Commit** ``` feat(web): implement PluginCRUDPage config-driven rendering engine ``` --- ### Task 11: 动态路由 + 动态菜单 **Files:** - Create: `apps/web/src/components/DynamicMenu.tsx` - Modify: `apps/web/src/App.tsx` - Modify: `apps/web/src/layouts/MainLayout.tsx` - [ ] **Step 1: DynamicMenu 组件** 从 `usePluginStore` 读取 `getPageConfigs()`,按 `menu_group` 分组生成 Ant Design Menu.Item,追加到侧边栏。 - [ ] **Step 2: App.tsx 添加动态路由** 在 private routes 中,遍历 PluginStore 的 pageConfigs,为每个 CRUD 页面生成: ```tsx } /> ``` 同时添加 `/plugin-admin` 路由指向 `PluginAdmin` 页面。 - [ ] **Step 3: MainLayout.tsx 集成 DynamicMenu** 替换硬编码的 `bizMenuItems`,追加插件动态菜单。 - [ ] **Step 4: 运行 `pnpm dev` 验证前端编译通过** - [ ] **Step 5: Commit** ``` feat(web): add dynamic routing and menu generation from plugin configs ``` --- ### Task 12: 插件管理页面 **Files:** - Create: `apps/web/src/pages/PluginAdmin.tsx` - [ ] **Step 1: 实现 PluginAdmin 页面** 包含:插件列表(Table)、上传按钮(Upload)、启用/停用/卸载操作、配置编辑 Modal。使用 Ant Design 组件。 - [ ] **Step 2: Commit** ``` feat(web): add plugin admin page with upload/enable/disable/configure ``` --- ## Chunk 5: 第一个行业插件(进销存) ### Task 13: 创建 erp-plugin-inventory **Files:** - Create: `crates/plugins/inventory/Cargo.toml` - Create: `crates/plugins/inventory/plugin.toml` - Create: `crates/plugins/inventory/src/lib.rs` - [ ] **Step 1: 创建插件项目** `Cargo.toml` crate-type = ["cdylib"],依赖 wit-bindgen + serde + serde_json。 - [ ] **Step 2: 编写 plugin.toml** 完整清单(spec §4 的进销存示例):inventory_item、purchase_order 两个 entity,3 个 CRUD 页面 + 1 个 custom 页面。 - [ ] **Step 3: 实现 lib.rs** 使用 wit-bindgen 生成的绑定,实现 `init()`、`on_tenant_created()`、`handle_event()`。 - [ ] **Step 4: 编译为 WASM** ```bash rustup target add wasm32-unknown-unknown cargo build -p erp-plugin-inventory --target wasm32-unknown-unknown --release ``` - [ ] **Step 5: Commit** ``` feat(inventory): create erp-plugin-inventory as first industry plugin ``` --- ### Task 14: 端到端集成测试 - [ ] **Step 1: 启动后端服务** ```bash cd docker && docker compose up -d cd crates/erp-server && cargo run ``` - [ ] **Step 2: 通过 API 上传进销存插件** ```bash # 打包 cp target/wasm32-unknown-unknown/release/erp_plugin_inventory.wasm /tmp/ # 上传 curl -X POST http://localhost:3000/api/v1/admin/plugins/upload \ -F "wasm=@/tmp/erp_plugin_inventory.wasm" \ -F "manifest=@crates/plugins/inventory/plugin.toml" ``` - [ ] **Step 3: 启用插件 + 验证建表** ```bash curl -X POST http://localhost:3000/api/v1/admin/plugins/erp-inventory/enable docker exec erp-postgres psql -U erp -c "\dt plugin_*" ``` - [ ] **Step 4: 测试 CRUD API** ```bash curl -X POST http://localhost:3000/api/v1/plugins/erp-inventory/inventory_item \ -H "Authorization: Bearer $TOKEN" \ -d '{"sku":"ITEM001","name":"测试商品","quantity":100}' curl http://localhost:3000/api/v1/plugins/erp-inventory/inventory_item \ -H "Authorization: Bearer $TOKEN" ``` - [ ] **Step 5: 前端验证** 启动 `pnpm dev`,验证: - 侧边栏出现"进销存"菜单组 + 子菜单 - 点击"商品管理"显示 PluginCRUDPage - 可以新建/编辑/删除/搜索商品 - [ ] **Step 6: 测试停用 + 卸载** ```bash curl -X POST http://localhost:3000/api/v1/admin/plugins/erp-inventory/disable curl -X DELETE http://localhost:3000/api/v1/admin/plugins/erp-inventory # 验证数据表仍在 docker exec erp-postgres psql -U erp -c "\dt plugin_*" ``` - [ ] **Step 7: Commit** ``` test(inventory): end-to-end integration test for plugin lifecycle ``` --- ## 执行顺序 ``` Chunk 1 (Tasks 1-2) ← 先做,所有后续依赖 trait v2 和 EventBus 扩展 ↓ Chunk 2 (Tasks 3-5) ← 数据库表 + crate 骨架 + WIT ↓ Chunk 3 (Tasks 6-8) ← 核心运行时 + API(后端完成) ↓ Chunk 4 (Tasks 9-12) ← 前端(可与 Chunk 5 并行) ↓ Chunk 5 (Tasks 13-14) ← 第一个插件 + E2E 验证 ``` ## 关键风险 | 风险 | 缓解 | |------|------| | Wasmtime 版本与 WIT 不兼容 | 锁定 wasmtime = "27",CI 验证 | | axum Router 在 erp-core 中引入重依赖 | 考虑将 trait routes 方法改为返回路由描述结构体,在 erp-server 层构建 Router | | 动态建表安全性 | 仅允许白名单列类型,禁止 DDL 注入 | | 前端 PluginCRUDPage 覆盖不足 | 先支持 text/number/date/select/currency,custom 页面后续迭代 |