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

12 KiB
Raw Blame History

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 的一部分,而是每个模块的关联函数:

// 当前(静态关联函数,非 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> 中的 SAppState 类型,而 trait 不能有泛型方法(会导致 trait 不是 object-safe。需要设计新的路由注入机制。

差距 2FromRef 状态模式与 WASM 插件的冲突

当前每个模块有自己的 State 类型(AuthStateConfigState 等),通过 FromRefAppState 桥接。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> 缺少泛型参数 SAppState。Axum 的 Router 依赖状态类型,而 trait object 不能携带泛型。

影响: 这是路由自动收集的基础。如果无法解决,整个 registry.build_routes() 的设计就不能实现。

建议方案:

// 方案:使用 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 接口(验证用最小子集)

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 端实现(验证要点)

// 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 测试插件

// 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 验证测试用例

// 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