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:
iven
2026-04-19 12:16:24 +08:00
parent c4b1e9e56d
commit e429448c42
20 changed files with 1889 additions and 46 deletions

View File

@@ -0,0 +1,304 @@
use crate::error::{PluginError, PluginResult};
use crate::manifest::{parse_manifest, PluginManifest};
/// 插件上传时校验报告
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub metrics: PluginMetrics,
}
/// 插件质量指标
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct PluginMetrics {
pub entity_count: usize,
pub field_count: usize,
pub page_count: usize,
pub permission_count: usize,
pub relation_count: usize,
pub has_import_export: bool,
pub has_settings: bool,
pub has_numbering: bool,
pub has_trigger_events: bool,
pub wasm_size_bytes: usize,
pub complexity_score: f64,
}
/// 运行时监控指标
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct RuntimeMetrics {
pub error_count: u64,
pub total_invocations: u64,
pub avg_response_ms: f64,
pub fuel_consumption_avg: f64,
pub memory_peak_bytes: u64,
pub last_error: Option<String>,
pub last_error_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl RuntimeMetrics {
pub fn error_rate(&self) -> f64 {
if self.total_invocations == 0 {
return 0.0;
}
self.error_count as f64 / self.total_invocations as f64
}
}
/// 上传时安全扫描
pub fn validate_plugin_security(manifest: &PluginManifest, wasm_size: usize) -> PluginResult<ValidationReport> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
// 1. WASM 大小检查(上限 10MB
if wasm_size > 10 * 1024 * 1024 {
errors.push(format!("WASM 文件过大: {} bytes (上限 10MB)", wasm_size));
} else if wasm_size > 5 * 1024 * 1024 {
warnings.push(format!("WASM 文件较大: {} bytes (>5MB)", wasm_size));
}
// 2. 实体数量检查(上限 20
if let Some(schema) = &manifest.schema {
if schema.entities.len() > 20 {
errors.push(format!("实体数量过多: {} (上限 20)", schema.entities.len()));
}
for entity in &schema.entities {
// 字段数量检查
if entity.fields.len() > 50 {
errors.push(format!(
"实体 '{}' 字段数量过多: {} (上限 50)",
entity.name, entity.fields.len()
));
}
// 索引数量检查
if entity.indexes.len() > 10 {
warnings.push(format!(
"实体 '{}' 索引数量较多: {} (>10 可能影响写入性能)",
entity.name, entity.indexes.len()
));
}
// 检查字段中有无潜在 SQL 注入风险的字段名
for field in &entity.fields {
if field.name.len() > 64 {
errors.push(format!(
"字段名过长: '{}.{}' (上限 64 字符)",
entity.name, field.name
));
}
if !field.name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
errors.push(format!(
"字段名包含非法字符: '{}.{}' (只允许字母、数字、下划线)",
entity.name, field.name
));
}
}
}
}
// 3. 权限码命名规范检查
if let Some(permissions) = &manifest.permissions {
for perm in permissions {
if !perm.code.contains('.') {
warnings.push(format!(
"权限码 '{}' 建议使用 'entity.action' 格式",
perm.code
));
}
}
}
// 4. 依赖检查
if manifest.metadata.dependencies.len() > 5 {
warnings.push(format!(
"依赖数量较多: {} (>5 可能增加安装复杂度)",
manifest.metadata.dependencies.len()
));
}
// 5. 计算复杂度分数
let mut metrics = collect_metrics(manifest, wasm_size);
metrics.complexity_score = calculate_complexity_score(&metrics);
if metrics.complexity_score > 80.0 {
warnings.push(format!(
"插件复杂度较高: {:.1} (>80 建议拆分)",
metrics.complexity_score
));
}
let valid = errors.is_empty();
Ok(ValidationReport {
valid,
errors,
warnings,
metrics,
})
}
/// 收集插件指标
fn collect_metrics(manifest: &PluginManifest, wasm_size: usize) -> PluginMetrics {
let mut metrics = PluginMetrics {
wasm_size_bytes: wasm_size,
..Default::default()
};
if let Some(schema) = &manifest.schema {
metrics.entity_count = schema.entities.len();
for entity in &schema.entities {
metrics.field_count += entity.fields.len();
metrics.relation_count += entity.relations.len();
if entity.importable == Some(true) || entity.exportable == Some(true) {
metrics.has_import_export = true;
}
}
}
if let Some(ui) = &manifest.ui {
metrics.page_count = count_pages(&ui.pages);
}
if let Some(permissions) = &manifest.permissions {
metrics.permission_count = permissions.len();
}
metrics.has_settings = manifest.settings.is_some();
metrics.has_numbering = manifest.numbering.as_ref().map_or(false, |n| !n.is_empty());
metrics.has_trigger_events = manifest.trigger_events.as_ref().map_or(false, |t| !t.is_empty());
metrics
}
fn count_pages(pages: &[crate::manifest::PluginPageType]) -> usize {
let mut count = 0;
for page in pages {
count += 1;
if let crate::manifest::PluginPageType::Tabs { tabs, .. } = page {
count += count_pages(tabs);
}
}
count
}
/// 计算复杂度分数0-100
fn calculate_complexity_score(metrics: &PluginMetrics) -> f64 {
let entity_score = (metrics.entity_count as f64 / 20.0) * 30.0;
let field_score = (metrics.field_count as f64 / 100.0) * 20.0;
let page_score = (metrics.page_count as f64 / 20.0) * 15.0;
let relation_score = (metrics.relation_count as f64 / 30.0) * 15.0;
let size_score = (metrics.wasm_size_bytes as f64 / (10.0 * 1024.0 * 1024.0)) * 20.0;
(entity_score + field_score + page_score + relation_score + size_score).min(100.0)
}
/// 性能基准测试结果
#[derive(Debug, Clone, serde::Serialize)]
pub struct BenchmarkResult {
pub create_avg_ms: f64,
pub read_avg_ms: f64,
pub update_avg_ms: f64,
pub delete_avg_ms: f64,
pub list_avg_ms: f64,
pub passed: bool,
pub details: String,
}
impl BenchmarkResult {
/// 创建操作的阈值: 500ms
pub const CREATE_THRESHOLD_MS: f64 = 500.0;
/// 读取操作的阈值: 200ms
pub const READ_THRESHOLD_MS: f64 = 200.0;
/// 列表查询的阈值: 1000ms
pub const LIST_THRESHOLD_MS: f64 = 1000.0;
pub fn check(&self) -> bool {
self.create_avg_ms <= Self::CREATE_THRESHOLD_MS
&& self.read_avg_ms <= Self::READ_THRESHOLD_MS
&& self.list_avg_ms <= Self::LIST_THRESHOLD_MS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_security_basic() {
let toml = r#"
[metadata]
id = "test"
name = "Test"
version = "0.1.0"
[schema]
[[schema.entities]]
name = "product"
display_name = "商品"
[[schema.entities.fields]]
name = "sku"
field_type = "string"
required = true
"#;
let manifest = parse_manifest(toml).unwrap();
let report = validate_plugin_security(&manifest, 1024).unwrap();
assert!(report.valid);
assert!(report.errors.is_empty());
}
#[test]
fn reject_oversized_wasm() {
let toml = r#"
[metadata]
id = "test"
name = "Test"
version = "0.1.0"
"#;
let manifest = parse_manifest(toml).unwrap();
let report = validate_plugin_security(&manifest, 15 * 1024 * 1024).unwrap();
assert!(!report.valid);
assert!(report.errors.iter().any(|e| e.contains("WASM 文件过大")));
}
#[test]
fn complexity_score_calculation() {
let metrics = PluginMetrics {
entity_count: 5,
field_count: 30,
page_count: 5,
relation_count: 3,
wasm_size_bytes: 500_000,
..Default::default()
};
let score = calculate_complexity_score(&metrics);
assert!(score > 0.0 && score < 50.0, "score = {}", score);
}
#[test]
fn runtime_metrics_error_rate() {
let metrics = RuntimeMetrics {
error_count: 5,
total_invocations: 100,
..Default::default()
};
assert!((metrics.error_rate() - 0.05).abs() < 0.001);
}
#[test]
fn benchmark_threshold_check() {
let result = BenchmarkResult {
create_avg_ms: 300.0,
read_avg_ms: 100.0,
update_avg_ms: 200.0,
delete_avg_ms: 150.0,
list_avg_ms: 800.0,
passed: true,
details: String::new(),
};
assert!(result.check());
}
}