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

@@ -15,9 +15,28 @@ use erp_core::events::EventBus;
use crate::PluginWorld;
use crate::dynamic_table::DynamicTableManager;
use crate::error::{PluginError, PluginResult};
use crate::host::{HostState, PendingOp};
use crate::host::{HostState, NumberingRule, PendingOp};
use crate::manifest::PluginManifest;
/// 从 manifest 的 numbering 声明构建 HostState 缓存映射
fn numbering_rules_from_manifest(manifest: &PluginManifest) -> HashMap<String, NumberingRule> {
let mut rules = HashMap::new();
if let Some(numbering) = &manifest.numbering {
for n in numbering {
rules.insert(
n.entity.clone(),
NumberingRule {
prefix: n.prefix.clone(),
format: n.format.clone(),
seq_length: n.seq_length,
reset_rule: format!("{:?}", n.reset_rule).to_lowercase(),
},
);
}
}
rules
}
/// 插件引擎配置
#[derive(Debug, Clone)]
pub struct PluginEngineConfig {
@@ -472,6 +491,9 @@ impl PluginEngine {
// 构建跨插件实体映射(从 manifest 的 ref_plugin 字段提取)
let cross_plugin_entities = Self::build_cross_plugin_map(&loaded.manifest, &self.db, exec_ctx.tenant_id).await;
// 加载插件配置(从数据库)
let plugin_config = Self::load_plugin_config(plugin_id, exec_ctx.tenant_id, &self.db).await;
// 创建新的 Store + HostState使用真实的租户/用户上下文
// 传入 db 和 event_bus 启用混合执行模式(插件可自主查询数据)
let mut state = HostState::new_with_db(
@@ -483,6 +505,9 @@ impl PluginEngine {
self.event_bus.clone(),
);
state.cross_plugin_entities = cross_plugin_entities;
// 注入编号规则和插件配置
state.numbering_rules = numbering_rules_from_manifest(&loaded.manifest);
state.plugin_config = plugin_config;
let mut store = Store::new(&self.engine, state);
store
.set_fuel(self.config.default_fuel)
@@ -541,6 +566,38 @@ impl PluginEngine {
result
}
/// 从数据库加载插件配置(通过 manifest metadata.id 匹配)
fn load_plugin_config(
plugin_id: &str,
tenant_id: Uuid,
db: &DatabaseConnection,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send + 'static>> {
let db = db.clone();
let pid = plugin_id.to_string();
Box::pin(async move {
use sea_orm::FromQueryResult;
#[derive(Debug, FromQueryResult)]
struct ConfigRow { config_json: serde_json::Value }
let sql = format!(
"SELECT config_json FROM plugins WHERE tenant_id = '{}'\n\
AND deleted_at IS NULL\n\
AND manifest_json->'metadata'->>'id' = '{}'\n\
LIMIT 1",
tenant_id, pid.replace('\'', "''")
);
ConfigRow::find_by_statement(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
sql,
))
.one(&db)
.await
.ok()
.flatten()
.map(|r| r.config_json)
.unwrap_or_default()
})
}
/// 从 manifest 的 ref_plugin 字段构建跨插件实体映射
/// 返回: { "erp-crm.customer" → "plugin_erp_crm__customer", ... }
async fn build_cross_plugin_map(