# WASM 插件系统设计 — 可行性分析与原型验证计划 > 基于 `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` 的全面审查 --- ## 1. 总体评估 **结论:技术可行,但需要对设计做重要调整。** 设计方案的核心思路(WASM 沙箱 + 宿主代理 API + 配置驱动 UI)是正确的架构方向。Wasmtime v43.x + Component Model + WASI 0.2/0.3 已达到生产就绪状态。但设计中有 **7 个关键问题** 和 **5 个改进点** 需要在实施前解决。 **可行性评分:** | 维度 | 评分 | 说明 | |------|------|------| | 技术可行性 | 8/10 | Wasmtime 成熟,核心 API 可用;动态表和事务支持需额外封装 | | 架构兼容性 | 6/10 | 与现有 FromRef 状态模式、静态路由模式存在结构性冲突 | | 安全性 | 9/10 | WASM 沙箱 + 权限模型 + 租户隔离设计扎实 | | 前端可行性 | 7/10 | PluginCRUDPage 概念正确但实现细节不足 | | 实施复杂度 | 高 | 估计 3 个 Phase、6-8 周工作量 | --- ## 2. 与现有代码的兼容性分析 ### 2.1 现有架构快照 | 组件 | 现状 | 设计目标 | 差距 | |------|------|----------|------| | ErpModule trait | 7 个方法(name/version/dependencies/register_event_handlers/on_tenant_created/on_tenant_deleted/as_any) | 15+ 方法(新增 id/module_type/on_startup/on_shutdown/health_check/public_routes/protected_routes/migrations/config_schema) | **重大差距** | | ModuleRegistry | `Arc>>`,Builder 模式注册,无路由收集 | 需要 build_routes()、topological_sort()、load_wasm_plugins()、health_check_all() | **重大差距** | | EventBus | `tokio::broadcast`,仅有 subscribe()(全量订阅) | 需要 subscribe_filtered() + unsubscribe() | **中等差距** | | 路由 | main.rs 手动 merge 静态方法 | registry.build_routes() 自动收集 | **中等差距** | | 状态注入 | FromRef 模式(编译时桥接 AppState → 各模块 State) | WASM 插件需要运行时状态注入 | **结构性冲突** | | 前端菜单 | MainLayout.tsx 硬编码 3 组菜单 | 从 PluginStore 动态生成 | **中等差距** | | 前端路由 | App.tsx 静态定义,React.lazy 懒加载 | DynamicRouter 根据插件配置动态生成 | **中等差距** | ### 2.2 关键差距详解 **差距 1:ErpModule trait 路由方法缺失** 当前路由不是 trait 的一部分,而是每个模块的关联函数: ```rust // 当前(静态关联函数,非 trait 方法) impl AuthModule { pub fn public_routes() -> Router { ... } pub fn protected_routes() -> Router { ... } } ``` 设计将路由提升为 trait 方法,但这引入了泛型参数问题:`Router` 中的 `S` 是 `AppState` 类型,而 trait 不能有泛型方法(会导致 trait 不是 object-safe)。需要设计新的路由注入机制。 **差距 2:FromRef 状态模式与 WASM 插件的冲突** 当前每个模块有自己的 State 类型(`AuthState`、`ConfigState` 等),通过 `FromRef` 从 `AppState` 桥接。WASM 插件无法定义编译时的 `FromRef` 实现,需要运行时状态传递机制。 **差距 3:EventBus 缺少类型化订阅** `subscribe()` 返回 `broadcast::Receiver`,订阅者需要自行过滤。这会导致每个插件都收到所有事件,增加不必要的开销。设计中的 `subscribe_filtered()` 是必要的扩展。 --- ## 3. 关键问题(Critical Issues) ### C1: 路由注入机制设计不完整 **问题:** 设计中的 `fn public_routes(&self) -> Option` 和 `fn protected_routes(&self) -> Option` 缺少泛型参数 `S`(AppState)。Axum 的 Router 依赖状态类型,而 trait object 不能携带泛型。 **影响:** 这是路由自动收集的基础。如果无法解决,整个 `registry.build_routes()` 的设计就不能实现。 **建议方案:** ```rust // 方案:使用 Router<()>, 由 ModuleRegistry 在 build_routes() 时添加 .with_state() pub trait ErpModule: Send + Sync { fn protected_routes(&self) -> Option> { None } fn public_routes(&self) -> Option> { None } } impl ModuleRegistry { pub fn build_routes(&self, state: AppState) -> (Router, Router) { let public = self.modules.iter() .filter_map(|m| m.public_routes()) .fold(Router::new(), |acc, r| acc.merge(r)) .with_state(state.clone()); let protected = self.modules.iter() .filter_map(|m| m.protected_routes()) .fold(Router::new(), |acc, r| acc.merge(r)) .with_state(state); (public, protected) } } ``` ### C2: 事务支持缺失 **问题:** Host API 每次 db 调用都是独立事务。但 ERP 业务经常需要多步骤原子操作。 **建议:** 增加声明式事务,插件提交一组操作,宿主在单事务中执行。 ### C3: 插件间依赖未解决 **建议:** Phase 7 暂不实现,在 plugin.toml 中预留 `plugins = [...]` 字段。 ### C4: 版本号严重过时 **建议:** 锁定 `wasmtime = "43"` + `wit-bindgen = "0.55"`。 ### C5: 动态表 Schema 迁移策略缺失 **建议:** 添加 schema_version + 迁移 SQL 机制。 ### C6: PluginCRUDPage 实现细节不足 **建议:** 扩展关联数据、主从表、文件上传支持。 ### C7: 错误传播机制不完整 **建议:** 定义结构化插件错误协议。 --- ## 4. 替代方案比较 **结论:WASM 方案优于 Lua 脚本和进程外 gRPC。** dylib 因安全性排除。 --- ## 5. Wasmtime 原型验证计划 ### 5.1 验证目标 验证 Wasmtime Component Model 与 ERP 插件系统核心需求的集成可行性: | # | 验证项 | 关键问题 | |---|--------|---------| | V1 | WIT 接口定义 + bindgen! 宏 | C4: 版本兼容性 | | V2 | Host 调用插件导出函数 | init / handle_event 能否正常工作 | | V3 | 插件调用 Host 导入函数 | db_insert / log_write 能否正常回调 | | V4 | async 支持 | Host async 函数(数据库操作)能否正确桥接 | | V5 | Fuel + Epoch 资源限制 | 是否能限制插件 CPU 时间和内存 | | V6 | 从二进制动态加载 | 从数据库/文件加载 WASM 并实例化 | ### 5.2 原型项目结构 在 workspace 中创建独立的原型 crate(不影响现有代码): ``` crates/ erp-plugin-prototype/ ← 新增原型 crate Cargo.toml wit/ plugin.wit ← WIT 接口定义 src/ lib.rs ← Host 端:运行时 + Host API 实现 main.rs ← 测试入口:加载插件并调用 tests/ test_plugin_integration.rs ← 集成测试 erp-plugin-test-sample/ ← 新增测试插件 crate Cargo.toml src/ lib.rs ← 插件端:实现 Guest trait ``` ### 5.3 WIT 接口(验证用最小子集) ```wit package erp:plugin; interface host { db-insert: func(entity: string, data: list) -> result, string>; log-write: func(level: string, message: string); } interface plugin { init: func() -> result<_, string>; handle-event: func(event-type: string, payload: list) -> result<_, string>; } world plugin-world { import host; export plugin; } ``` ### 5.4 Host 端实现(验证要点) ```rust // crates/erp-plugin-prototype/src/lib.rs use wasmtime::component::*; use wasmtime::{Config, Engine, Store}; use wasmtime::StoreLimitsBuilder; // bindgen! 生成类型化绑定 bindgen!({ path: "./wit/plugin.wit", world: "plugin-world", async: true, // ← 验证 V4: async 支持 imports: { default: async | trappable }, exports: { default: async }, }); struct HostState { fuel_consumed: u64, logs: Vec<(String, String)>, db_ops: Vec<(String, Vec)>, } // 实现 bindgen 生成的 Host trait impl Host for HostState { async fn db_insert(&mut self, entity: String, data: Vec) -> Result, String> { // 模拟数据库操作 self.db_ops.push((entity, data.clone())); Ok(br#"{"id":"test-uuid","tenant_id":"tenant-1"}"#.to_vec()) } async fn log_write(&mut self, level: String, message: String) { self.logs.push((level, message)); } } ``` ### 5.5 测试插件 ```rust // crates/erp-plugin-test-sample/src/lib.rs wit_bindgen::generate!({ path: "../../erp-plugin-prototype/wit/plugin.wit", world: "plugin-world", }); struct TestPlugin; impl Guest for TestPlugin { fn init() -> Result<(), String> { host::log_write("info", "测试插件初始化成功"); Ok(()) } fn handle_event(event_type: String, payload: Vec) -> Result<(), String> { // 调用 Host API 验证双向通信 let result = host::db_insert("test_entity", br#"{"name":"test"}"#.to_vec()) .map_err(|e| format!("db_insert 失败: {}", e))?; host::log_write("info", &format!("处理事件 {} 成功", event_type)); Ok(()) } } export!(TestPlugin); ``` ### 5.6 验证测试用例 ```rust // crates/erp-plugin-prototype/tests/test_plugin_integration.rs #[tokio::test] async fn test_plugin_lifecycle() { // V1: WIT 接口 + bindgen 编译通过(隐式验证) // V6: 从文件加载 WASM 二进制 let wasm_bytes = std::fs::read("../erp-plugin-test-sample/target/.../test_plugin.wasm") .expect("请先编译测试插件"); let engine = setup_engine(); // V5: 启用 fuel + epoch let module = Module::from_binary(&engine, &wasm_bytes).unwrap(); let mut store = setup_store(&engine); // V5: 设置资源限制 let instance = instantiate(&mut store, &module).await.unwrap(); // V2: Host 调用插件 init() instance.plugin().call_init(&mut store).await.unwrap(); // V3: Host 调用插件 handle_event(),插件回调 Host API instance.plugin().call_handle_event( &mut store, "test.event".to_string(), vec![], ).await.unwrap(); // 验证 Host 端收到了插件的操作 let state = store.data(); assert!(state.logs.iter().any(|(l, m)| m.contains("测试插件初始化成功"))); assert_eq!(state.db_ops.len(), 1); } #[tokio::test] async fn test_fuel_limit() { // V5: 验证 fuel 耗尽时正确 trap let mut store = setup_store_with_fuel(100); // 极低 fuel let result = instance.plugin().call_init(&mut store).await; assert!(result.is_err()); // 应该因 fuel 耗尽而失败 } ``` ### 5.7 验证步骤 ``` 步骤 1: 添加 crate 和 Cargo.toml 依赖 - crates/erp-plugin-prototype/Cargo.toml wasmtime = "43", wasmtime-wasi = "43", tokio, anyhow - crates/erp-plugin-test-sample/Cargo.toml wit-bindgen = "0.55", serde, serde_json, crate-type = ["cdylib"] 步骤 2: 编写 WIT 接口文件 - crates/erp-plugin-prototype/wit/plugin.wit 步骤 3: 实现 Host 端(bindgen + Host trait) - crates/erp-plugin-prototype/src/lib.rs 步骤 4: 实现测试插件 - crates/erp-plugin-test-sample/src/lib.rs 步骤 5: 编译测试插件为 WASM - cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release 步骤 6: 运行集成测试 - cargo test -p erp-plugin-prototype 步骤 7: 验证资源限制 - fuel 耗尽 trap - 内存限制 - epoch 中断 ``` ### 5.8 验证成功标准 | 标准 | 衡量方式 | |------|---------| | V1 编译通过 | Host 和插件 crate 均能 `cargo check` 通过 | | V2 Host→插件调用 | `init()` 返回 Ok,Host 端日志记录初始化成功 | | V3 插件→Host回调 | `handle_event()` 中调用 `host::db_insert()` 成功返回数据 | | V4 async 正确 | Host 的 async db_insert 在 tokio runtime 中正确执行 | | V5 资源限制 | 低 fuel 时 init() 返回错误而非无限循环 | | V6 动态加载 | 从 .wasm 文件加载并实例化成功 | | 编译大小 | 测试插件 WASM < 2MB | | 启动耗时 | 单个插件实例化 < 100ms | ### 5.9 关键文件清单 | 文件 | 用途 | |------|------| | `crates/erp-plugin-prototype/Cargo.toml` | 新建 - Host 端 crate 配置 | | `crates/erp-plugin-prototype/wit/plugin.wit` | 新建 - WIT 接口定义 | | `crates/erp-plugin-prototype/src/lib.rs` | 新建 - Host 运行时 + API 实现 | | `crates/erp-plugin-prototype/src/main.rs` | 新建 - 手动测试入口 | | `crates/erp-plugin-prototype/tests/test_plugin_integration.rs` | 新建 - 集成测试 | | `crates/erp-plugin-test-sample/Cargo.toml` | 新建 - 插件 crate 配置 | | `crates/erp-plugin-test-sample/src/lib.rs` | 新建 - 测试插件实现 | | `Cargo.toml` | 修改 - 添加两个新 workspace member |