chore: apply cargo fmt across workspace and update docs

- Run cargo fmt on all Rust crates for consistent formatting
- Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions
- Update wiki: add WASM plugin architecture, rewrite dev environment docs
- Minor frontend cleanup (unused imports)
This commit is contained in:
iven
2026-04-15 00:49:20 +08:00
parent e16c1a85d7
commit 9568dd7875
113 changed files with 4355 additions and 937 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "erp-plugin-prototype"
version = "0.1.0"
edition = "2024"
description = "WASM 插件系统原型验证 — Host 端运行时"
[dependencies]
wasmtime = "43"
wasmtime-wasi = "43"
tokio = { workspace = true }
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
[[test]]
name = "test_plugin_integration"
path = "tests/test_plugin_integration.rs"

View File

@@ -0,0 +1,208 @@
//! WASM 插件原型验证 — Host 端运行时
//!
//! 验证目标:
//! - V1: WIT 接口定义 + bindgen! 宏编译通过
//! - V2: Host 调用插件导出函数init / handle_event
//! - V3: 插件调用 Host 导入函数db_insert / log_write
//! - V4: async 支持Host async 函数正确桥接)
//! - V5: Fuel + Epoch 资源限制
//! - V6: 从二进制动态加载
use anyhow::Result;
use wasmtime::component::{Component, HasSelf, Linker, bindgen};
use wasmtime::{Config, Engine, Store, StoreLimits, StoreLimitsBuilder};
/// Host 端状态,绑定到每个 Store 实例
pub struct HostState {
/// Store 级资源限制
pub(crate) limits: StoreLimits,
/// 模拟数据库操作记录
pub db_ops: Vec<DbOperation>,
/// 日志记录
pub logs: Vec<(String, String)>,
/// 发布的事件
pub events: Vec<(String, Vec<u8>)>,
/// 配置存储(模拟)
pub config_map: std::collections::HashMap<String, Vec<u8>>,
}
/// 数据库操作记录
#[derive(Debug, Clone)]
pub struct DbOperation {
pub op_type: String,
pub entity: String,
pub data: Option<Vec<u8>>,
pub id: Option<String>,
pub version: Option<i64>,
}
impl Default for HostState {
fn default() -> Self {
Self::new()
}
}
impl HostState {
pub fn new() -> Self {
Self {
limits: StoreLimitsBuilder::new().build(),
db_ops: Vec::new(),
logs: Vec::new(),
events: Vec::new(),
config_map: std::collections::HashMap::new(),
}
}
}
// bindgen! 生成类型化绑定(包含 Host trait 和 add_to_linker
bindgen!({
path: "./wit/plugin.wit",
world: "plugin-world",
});
// 实现 bindgen 生成的 Host trait — 插件调用 Host API 的入口
impl erp::plugin::host_api::Host for HostState {
fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, String> {
let id = format!("id-{}", self.db_ops.len() + 1);
let record = serde_json::json!({
"id": id,
"tenant_id": "tenant-default",
"entity": entity,
"data": serde_json::from_slice::<serde_json::Value>(&data).unwrap_or(serde_json::Value::Null),
});
let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?;
self.db_ops.push(DbOperation {
op_type: "insert".into(),
entity: entity.clone(),
data: Some(data),
id: Some(id),
version: None,
});
Ok(result)
}
fn db_query(
&mut self,
entity: String,
_filter: Vec<u8>,
_pagination: Vec<u8>,
) -> Result<Vec<u8>, String> {
let results: Vec<serde_json::Value> = self
.db_ops
.iter()
.filter(|op| op.entity == entity && op.op_type == "insert")
.map(|op| {
serde_json::json!({
"id": op.id,
"entity": op.entity,
})
})
.collect();
serde_json::to_vec(&results).map_err(|e| e.to_string())
}
fn db_update(
&mut self,
entity: String,
id: String,
data: Vec<u8>,
version: i64,
) -> Result<Vec<u8>, String> {
let record = serde_json::json!({
"id": id,
"tenant_id": "tenant-default",
"entity": entity,
"version": version + 1,
"data": serde_json::from_slice::<serde_json::Value>(&data).unwrap_or(serde_json::Value::Null),
});
let result = serde_json::to_vec(&record).map_err(|e| e.to_string())?;
self.db_ops.push(DbOperation {
op_type: "update".into(),
entity,
data: Some(data),
id: Some(id),
version: Some(version),
});
Ok(result)
}
fn db_delete(&mut self, entity: String, id: String) -> Result<(), String> {
self.db_ops.push(DbOperation {
op_type: "delete".into(),
entity,
data: None,
id: Some(id),
version: None,
});
Ok(())
}
fn event_publish(&mut self, event_type: String, payload: Vec<u8>) -> Result<(), String> {
self.events.push((event_type, payload));
Ok(())
}
fn config_get(&mut self, key: String) -> Result<Vec<u8>, String> {
self.config_map
.get(&key)
.cloned()
.ok_or_else(|| format!("配置项 '{}' 不存在", key))
}
fn log_write(&mut self, level: String, message: String) {
self.logs.push((level, message));
}
fn current_user(&mut self) -> Result<Vec<u8>, String> {
let user = serde_json::json!({
"id": "user-default",
"username": "admin",
"tenant_id": "tenant-default",
});
serde_json::to_vec(&user).map_err(|e| e.to_string())
}
fn check_permission(&mut self, _permission: String) -> Result<bool, String> {
Ok(true)
}
}
/// 创建 Wasmtime Engine启用 Fuel 限制)
pub fn create_engine() -> Result<Engine> {
let mut config = Config::new();
config.wasm_component_model(true);
config.consume_fuel(true);
Ok(Engine::new(&config)?)
}
/// 创建带 Fuel 限制的 Store
pub fn create_store(engine: &Engine, fuel: u64) -> Result<Store<HostState>> {
let state = HostState::new();
let mut store = Store::new(engine, state);
store.set_fuel(fuel)?;
store.limiter(|state| &mut state.limits);
Ok(store)
}
/// 从 WASM 二进制加载并实例化插件
pub async fn load_plugin(
engine: &Engine,
wasm_bytes: &[u8],
fuel: u64,
) -> Result<(Store<HostState>, PluginWorld)> {
let mut store = create_store(engine, fuel)?;
let component = Component::from_binary(engine, wasm_bytes)?;
let mut linker = Linker::new(engine);
// 注册 Host API 到 Linker使插件可以调用 host 函数
// HasSelf<HostState> 表示 Data<'a> = &'a mut HostState
PluginWorld::add_to_linker::<_, HasSelf<HostState>>(&mut linker, |state| state)?;
let instance = PluginWorld::instantiate_async(&mut store, &component, &linker).await?;
Ok((store, instance))
}

View File

@@ -0,0 +1,10 @@
use erp_plugin_prototype::*;
fn _discover_api(instance: PluginWorld, mut store: wasmtime::Store<HostState>) {
let api = instance.erp_plugin_plugin_api();
let _ = api.call_init(&mut store);
let _ = api.call_on_tenant_created(&mut store, "test-tenant");
let _ = api.call_handle_event(&mut store, "test.event", &[1u8]);
}
fn main() {}

View File

@@ -0,0 +1,220 @@
//! WASM 插件集成测试
//!
//! 验证目标:
//! V1 — WIT 接口 + bindgen! 编译通过
//! V2 — Host 调用插件 init() / handle_event()
//! V3 — 插件回调 Host db_insert / log_write
//! V4 — async 实例化桥接
//! V5 — Fuel 资源限制
//! V6 — 从二进制动态加载
use anyhow::Result;
use erp_plugin_prototype::{create_engine, load_plugin};
/// 获取测试插件 WASM Component 文件路径
fn wasm_path() -> String {
let candidates = [
// 预构建的 WASM Component通过 wasm-tools component new 生成)
"../../target/erp_plugin_test_sample.component.wasm".into(),
// 备选:绝对路径
format!(
"{}/../../target/erp_plugin_test_sample.component.wasm",
std::env::current_dir().unwrap().display()
),
];
for path in &candidates {
if std::path::Path::new(path).exists() {
return path.clone();
}
}
candidates[0].clone()
}
#[tokio::test]
async fn test_v6_load_plugin_from_binary() -> Result<()> {
let wasm_path = wasm_path();
let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| {
anyhow::anyhow!(
"读取 WASM 失败: {}。请先编译: cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release\n路径: {}",
e, wasm_path
)
})?;
assert!(!wasm_bytes.is_empty(), "WASM 文件不应为空");
println!("WASM 文件大小: {} bytes", wasm_bytes.len());
let engine = create_engine()?;
let (_store, _instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
Ok(())
}
#[tokio::test]
async fn test_v2_host_calls_plugin_init() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// V2: 调用插件 init()
instance
.erp_plugin_plugin_api()
.call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
// 验证 Host 端收到了日志
let state = store.data();
assert!(
state
.logs
.iter()
.any(|(_, m)| m.contains("测试插件初始化成功")),
"应收到初始化日志"
);
Ok(())
}
#[tokio::test]
async fn test_v3_plugin_calls_host_api() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// 先 init插件会调用 db_insert 和 log_write
instance
.erp_plugin_plugin_api()
.call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
let state = store.data();
// 验证 db_insert 被调用
assert!(
state
.db_ops
.iter()
.any(|op| op.op_type == "insert" && op.entity == "inventory_item"),
"应有 inventory_item 的 insert 操作"
);
// 验证 log_write 被调用
assert!(
state.logs.iter().any(|(_, m)| m.contains("插入成功")),
"应有插入成功的日志"
);
Ok(())
}
#[tokio::test]
async fn test_v3_plugin_handle_event_with_db_callback() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// 先 init
instance
.erp_plugin_plugin_api()
.call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
// 发送事件
let payload = serde_json::json!({"order_id": "PO-001", "action": "approve"}).to_string();
instance
.erp_plugin_plugin_api()
.call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())?
.map_err(|e| anyhow::anyhow!(e))?;
let state = store.data();
// 验证 db_update 被调用
assert!(
state
.db_ops
.iter()
.any(|op| op.op_type == "update" && op.entity == "purchase_order"),
"应有 purchase_order 的 update 操作"
);
// 验证事件被发布
assert!(
state
.events
.iter()
.any(|(t, _)| t == "purchase_order.approved"),
"应发布 purchase_order.approved 事件"
);
Ok(())
}
#[tokio::test]
async fn test_v5_fuel_limit_traps() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
// 给极少量的 fuel插件 init() 应该无法完成
let result = load_plugin(&engine, &wasm_bytes, 10).await;
match result {
Ok((mut store, instance)) => {
let init_result = instance.erp_plugin_plugin_api().call_init(&mut store);
assert!(
init_result.is_err() || init_result.unwrap().is_err(),
"低 fuel 应导致失败"
);
}
Err(_) => {
// 实例化就失败了,也是预期行为
}
}
Ok(())
}
#[tokio::test]
async fn test_full_lifecycle() -> Result<()> {
let wasm_bytes = std::fs::read(wasm_path())?;
let engine = create_engine()?;
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
// 1. init
instance
.erp_plugin_plugin_api()
.call_init(&mut store)?
.map_err(|e| anyhow::anyhow!(e))?;
// 2. on_tenant_created
instance
.erp_plugin_plugin_api()
.call_on_tenant_created(&mut store, "tenant-new-001")?
.map_err(|e| anyhow::anyhow!(e))?;
// 3. handle_event
let payload = serde_json::json!({"order_id": "PO-002"}).to_string();
instance
.erp_plugin_plugin_api()
.call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())?
.map_err(|e| anyhow::anyhow!(e))?;
let state = store.data();
// 完整生命周期验证
assert!(
state.logs.len() >= 3,
"应有多条日志记录,实际: {}",
state.logs.len()
);
assert!(
state.db_ops.len() >= 3,
"应有多次数据库操作,实际: {}",
state.db_ops.len()
);
assert!(state.events.len() >= 1, "应有事件发布");
println!("\n=== 完整生命周期验证 ===");
println!("日志: {}", state.logs.len());
println!("DB 操作: {}", state.db_ops.len());
println!("事件: {}", state.events.len());
Ok(())
}

View File

@@ -0,0 +1,48 @@
package erp:plugin;
/// 宿主暴露给插件的 API插件 import 这些函数)
interface host-api {
/// 插入记录(宿主自动注入 tenant_id, id, created_at 等标准字段)
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
/// 查询记录(自动注入 tenant_id 过滤 + 软删除过滤)
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
/// 更新记录(自动检查 version 乐观锁)
db-update: func(entity: string, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
/// 软删除记录
db-delete: func(entity: string, id: string) -> result<_, string>;
/// 发布领域事件
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
/// 读取系统配置
config-get: func(key: string) -> result<list<u8>, string>;
/// 写日志(自动关联 tenant_id + plugin_id
log-write: func(level: string, message: string);
/// 获取当前用户信息
current-user: func() -> result<list<u8>, string>;
/// 检查当前用户权限
check-permission: func(permission: string) -> result<bool, string>;
}
/// 插件导出的 API宿主调用这些函数
interface plugin-api {
/// 插件初始化(加载时调用一次)
init: func() -> result<_, string>;
/// 租户创建时调用
on-tenant-created: func(tenant-id: string) -> result<_, string>;
/// 处理订阅的事件
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
}
world plugin-world {
import host-api;
export plugin-api;
}