Files
erp/plans/temporal-gathering-stream.md
iven 841766b168
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(用户管理): 修复用户列表页面加载失败问题
修复用户列表页面加载失败导致测试超时的问题,确保页面元素正确渲染
2026-04-19 08:46:28 +08:00

358 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 关键差距详解
**差距 1ErpModule 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。需要设计新的路由注入机制。
**差距 2FromRef 状态模式与 WASM 插件的冲突**
当前每个模块有自己的 State 类型(`AuthState``ConfigState` 等),通过 `FromRef``AppState` 桥接。WASM 插件无法定义编译时的 `FromRef` 实现,需要运行时状态传递机制。
**差距 3EventBus 缺少类型化订阅**
`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()` 返回 OkHost 端日志记录初始化成功 |
| 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 |