feat(plugin): P2-P4 插件平台演进 — 通用服务 + 质量保障 + 市场
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

P2 平台通用服务:
- manifest 扩展: settings/numbering/templates/trigger_events/importable/exportable 声明
- 插件配置 UI: PluginSettingsForm 自动表单 + 后端校验 + 详情抽屉 Settings 标签页
- 编号规则: Host API numbering-generate + PostgreSQL 序列 + manifest 绑定
- 触发事件: data_service create/update/delete 自动发布 DomainEvent
- WIT 接口: 新增 numbering-generate/setting-get Host API

P3 质量保障:
- plugin_validator.rs: 安全扫描(WASM大小/实体数量/字段校验) + 复杂度评分
- 运行时监控指标: RuntimeMetrics (错误率/响应时间/Fuel/内存)
- 性能基准: BenchmarkResult 阈值定义
- 上传时自动安全扫描 + /validate API 端点

P4 插件市场:
- 数据库迁移: plugin_market_entries + plugin_market_reviews 表
- 前端 PluginMarket 页面: 分类浏览/搜索/详情/评分
- 路由注册: /plugins/market

测试: 269 全通过 (71 erp-plugin + 41 auth + 57 config + 34 core + 50 message + 16 workflow)
This commit is contained in:
iven
2026-04-19 12:16:24 +08:00
parent c4b1e9e56d
commit e429448c42
20 changed files with 1889 additions and 46 deletions

View File

@@ -29,6 +29,14 @@ impl PluginService {
// 解析 manifest
let manifest = parse_manifest(manifest_toml)?;
// 安全扫描
let validation = crate::plugin_validator::validate_plugin_security(&manifest, wasm_binary.len())?;
if !validation.valid {
return Err(PluginError::ValidationError(format!(
"插件安全校验失败: {}", validation.errors.join("; ")
)).into());
}
// 计算 WASM hash
let mut hasher = Sha256::new();
hasher.update(&wasm_binary);
@@ -403,6 +411,10 @@ impl PluginService {
events: None,
ui: None,
permissions: None,
settings: None,
numbering: None,
templates: None,
trigger_events: None,
}
});
let entities = entities_map.get(&model.id).cloned().unwrap_or_default();
@@ -439,6 +451,16 @@ impl PluginService {
erp_core::error::check_version(expected_version, model.version)?;
// 校验配置值是否符合 manifest settings 声明
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
if let Some(settings) = &manifest.settings {
validate_plugin_settings(config.as_object().ok_or_else(|| {
PluginError::ValidationError("config 必须是 JSON 对象".to_string())
})?, &settings.fields)?;
}
let now = Utc::now();
let mut active: plugin::ActiveModel = model.into();
active.config_json = Set(config);
@@ -446,9 +468,6 @@ impl PluginService {
active.updated_by = Set(Some(operator_id));
let model = active.update(db).await?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let entities = find_plugin_entities(plugin_id, tenant_id, db).await.unwrap_or_default();
Ok(plugin_model_to_resp(&model, &manifest, entities))
}
@@ -489,7 +508,7 @@ impl PluginService {
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
// 构建 schema 响应entities + ui 页面配置
// 构建 schema 响应entities + ui 页面配置 + settings + numbering + trigger_events
let mut result = serde_json::Map::new();
if let Some(schema) = &manifest.schema {
result.insert(
@@ -503,6 +522,24 @@ impl PluginService {
serde_json::to_value(ui).unwrap_or_default(),
);
}
if let Some(settings) = &manifest.settings {
result.insert(
"settings".to_string(),
serde_json::to_value(settings).unwrap_or_default(),
);
}
if let Some(numbering) = &manifest.numbering {
result.insert(
"numbering".to_string(),
serde_json::to_value(numbering).unwrap_or_default(),
);
}
if let Some(triggers) = &manifest.trigger_events {
result.insert(
"trigger_events".to_string(),
serde_json::to_value(triggers).unwrap_or_default(),
);
}
Ok(serde_json::Value::Object(result))
}
@@ -681,6 +718,15 @@ fn find_plugin(
}
}
/// 公开的插件查询 — 供 handler 使用
pub async fn find_plugin_model(
plugin_id: Uuid,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<plugin::Model> {
find_plugin(plugin_id, tenant_id, db).await
}
/// 批量查询多插件的 entities返回 plugin_id → Vec<PluginEntityResp> 映射。
async fn find_batch_plugin_entities(
plugin_ids: &[Uuid],
@@ -754,6 +800,77 @@ fn validate_status_any(actual: &str, expected: &[&str]) -> AppResult<()> {
Ok(())
}
/// 校验配置值是否符合 manifest settings 声明
fn validate_plugin_settings(
config: &serde_json::Map<String, serde_json::Value>,
fields: &[crate::manifest::PluginSettingField],
) -> AppResult<()> {
use crate::manifest::PluginSettingType;
for field in fields {
let value = config.get(&field.name);
// 必填校验
if field.required {
match value {
None | Some(serde_json::Value::Null) => {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' ({}) 为必填",
field.name, field.display_name
))
.into());
}
Some(serde_json::Value::String(s)) if s.is_empty() => {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' ({}) 不能为空",
field.name, field.display_name
))
.into());
}
_ => {}
}
}
// 类型校验
if let Some(val) = value {
if !val.is_null() {
let type_ok = match field.field_type {
PluginSettingType::Text => val.is_string(),
PluginSettingType::Number => val.is_number(),
PluginSettingType::Boolean => val.is_boolean(),
PluginSettingType::Select => val.is_string(),
PluginSettingType::Multiselect => val.is_array(),
PluginSettingType::Color => val.is_string(),
PluginSettingType::Date => val.is_string(),
PluginSettingType::Datetime => val.is_string(),
PluginSettingType::Json => true,
};
if !type_ok {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' 类型错误,期望 {:?}",
field.name, field.field_type
))
.into());
}
// 数值范围校验
if let Some((min, max)) = field.range {
if let Some(n) = val.as_f64() {
if n < min || n > max {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' ({}) 的值 {} 超出范围 [{}, {}]",
field.name, field.display_name, n, min, max
))
.into());
}
}
}
}
}
}
Ok(())
}
fn plugin_model_to_resp(
model: &plugin::Model,
manifest: &PluginManifest,