feat(plugin): P2-P4 插件平台演进 — 通用服务 + 质量保障 + 市场
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user