358 lines
12 KiB
Markdown
358 lines
12 KiB
Markdown
# 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<Vec<Arc<dyn ErpModule>>>`,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<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
|
||
pub fn protected_routes<S: Clone + Send + Sync + 'static>() -> Router<S> { ... }
|
||
}
|
||
```
|
||
|
||
设计将路由提升为 trait 方法,但这引入了泛型参数问题:`Router<S>` 中的 `S` 是 `AppState` 类型,而 trait 不能有泛型方法(会导致 trait 不是 object-safe)。需要设计新的路由注入机制。
|
||
|
||
**差距 2:FromRef 状态模式与 WASM 插件的冲突**
|
||
|
||
当前每个模块有自己的 State 类型(`AuthState`、`ConfigState` 等),通过 `FromRef` 从 `AppState` 桥接。WASM 插件无法定义编译时的 `FromRef` 实现,需要运行时状态传递机制。
|
||
|
||
**差距 3:EventBus 缺少类型化订阅**
|
||
|
||
`subscribe()` 返回 `broadcast::Receiver<DomainEvent>`,订阅者需要自行过滤。这会导致每个插件都收到所有事件,增加不必要的开销。设计中的 `subscribe_filtered()` 是必要的扩展。
|
||
|
||
---
|
||
|
||
## 3. 关键问题(Critical Issues)
|
||
|
||
### C1: 路由注入机制设计不完整
|
||
|
||
**问题:** 设计中的 `fn public_routes(&self) -> Option<Router>` 和 `fn protected_routes(&self) -> Option<Router>` 缺少泛型参数 `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<Router<()>> { None }
|
||
fn public_routes(&self) -> Option<Router<()>> { 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<u8>) -> result<list<u8>, string>;
|
||
log-write: func(level: string, message: string);
|
||
}
|
||
|
||
interface plugin {
|
||
init: func() -> result<_, string>;
|
||
handle-event: func(event-type: string, payload: list<u8>) -> 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<u8>)>,
|
||
}
|
||
|
||
// 实现 bindgen 生成的 Host trait
|
||
impl Host for HostState {
|
||
async fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, 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<u8>) -> 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 |
|