['warning', 'error'].includes((plugin.governance_status || '').toLowerCase())).length;
},
+ executionRiskCount() {
+ // 这里把执行风险单独统计出来,和治理告警区分开:
+ // 治理告警偏配置/依赖/加载问题,执行风险偏运行过程中的失败、超时与熔断。
+ return (this.plugins || []).filter(plugin => ['warning', 'error'].includes((((plugin.execution_summary || {}).status) || '').toLowerCase())).length;
+ },
+ openCircuitCount() {
+ return (this.plugins || []).filter(plugin => ((((plugin.execution_summary || {}).circuit_state) || '').toLowerCase() === 'open')).length;
+ },
+ topRiskPlugins() {
+ // 风险排行优先按熔断状态、执行状态和连续失败次数排序,
+ // 让页面顶部尽量把“最值得先排查”的插件顶上来。
+ const statusPriority = {
+ error: 0,
+ warning: 1,
+ info: 2,
+ healthy: 3
+ };
+ return (this.plugins || [])
+ .filter(plugin => ['warning', 'error'].includes((((plugin.execution_summary || {}).status) || '').toLowerCase()))
+ .slice()
+ .sort((left, right) => {
+ const leftSummary = left.execution_summary || {};
+ const rightSummary = right.execution_summary || {};
+ const leftPriority = statusPriority[(leftSummary.status || 'info').toLowerCase()];
+ const rightPriority = statusPriority[(rightSummary.status || 'info').toLowerCase()];
+ return (
+ (typeof leftPriority === 'number' ? leftPriority : 9) - (typeof rightPriority === 'number' ? rightPriority : 9)
+ || Number(rightSummary.consecutive_failures || 0) - Number(leftSummary.consecutive_failures || 0)
+ || Number(rightSummary.failure_count_total || 0) - Number(leftSummary.failure_count_total || 0)
+ || Number(rightSummary.timeout_count_total || 0) - Number(leftSummary.timeout_count_total || 0)
+ );
+ })
+ .slice(0, 5);
+ },
+ slowestPlugins() {
+ // 慢插件排行只看有执行样本的插件,避免未执行插件把榜单冲掉。
+ return (this.plugins || [])
+ .filter(plugin => Number((plugin.execution_summary || {}).total_executions || 0) > 0)
+ .slice()
+ .sort((left, right) => {
+ return Number((right.execution_summary || {}).last_process_time_ms || 0) - Number((left.execution_summary || {}).last_process_time_ms || 0);
+ })
+ .slice(0, 5);
+ },
// 弹窗宽度按视口分级收缩,保证手机上弹窗内容不会贴边或继续触发横向溢出。
pluginInfoDialogWidth() {
return this.isMobileViewport ? '94%' : '64%';
@@ -530,7 +707,7 @@
},
pluginStatusLabel(plugin) {
if (plugin && plugin.status_label) return plugin.status_label;
- const normalizedStatus = String(plugin?.status || '').toUpperCase();
+ const normalizedStatus = String((plugin && plugin.status) || '').toUpperCase();
const mapping = {
RUNNING: '运行中',
STOPPED: '已停用',
@@ -558,10 +735,27 @@
};
return mapping[normalizedLevel] || '提示';
},
+ executionTagType(level) {
+ const normalizedLevel = String(level || '').toLowerCase();
+ if (normalizedLevel === 'error') return 'danger';
+ if (normalizedLevel === 'warning') return 'warning';
+ if (normalizedLevel === 'healthy') return 'success';
+ return 'info';
+ },
+ executionLabel(level) {
+ const normalizedLevel = String(level || '').toLowerCase();
+ const mapping = {
+ healthy: '稳定',
+ warning: '需关注',
+ error: '高风险',
+ info: '暂无样本'
+ };
+ return mapping[normalizedLevel] || '暂无样本';
+ },
governanceIssueSummary(plugin) {
- const errorCount = Number(plugin?.governance_error_count || 0);
- const warningCount = Number(plugin?.governance_warning_count || 0);
- const infoCount = Number(plugin?.governance_info_count || 0);
+ const errorCount = Number((plugin && plugin.governance_error_count) || 0);
+ const warningCount = Number((plugin && plugin.governance_warning_count) || 0);
+ const infoCount = Number((plugin && plugin.governance_info_count) || 0);
if (errorCount > 0 || warningCount > 0) {
return `错误 ${errorCount} / 告警 ${warningCount}`;
}
@@ -570,6 +764,16 @@
}
return '暂无治理问题';
},
+ formatPercent(value) {
+ const normalizedValue = Number(value || 0);
+ if (!Number.isFinite(normalizedValue)) return '0.00%';
+ return `${normalizedValue.toFixed(2)}%`;
+ },
+ formatDurationMs(value) {
+ const normalizedValue = Number(value || 0);
+ if (!Number.isFinite(normalizedValue) || normalizedValue <= 0) return '-';
+ return `${normalizedValue.toFixed(2)} ms`;
+ },
loadPlugins() {
this.loading = true;
axios.get('/api/plugins')
@@ -687,7 +891,7 @@
})
.catch(error => {
console.error('保存配置出错:', error);
- this.configError = '保存配置出错: ' + (error.response?.data?.message || error.message);
+ this.configError = '保存配置出错: ' + (((error.response || {}).data || {}).message || error.message);
});
} catch (e) {
this.configError = '处理配置时出错: ' + e.message;
@@ -854,6 +1058,7 @@
font-size: 14px;
}
.overview-grid .el-col { margin-bottom: 16px; }
+ .insight-grid .el-col { margin-bottom: 16px; }
.overview-card { min-height: 112px; }
.overview-card--primary {
background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important;
@@ -869,6 +1074,64 @@
}
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
+ .rank-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+ .rank-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(248,250,252,0.82);
+ border: 1px solid rgba(148,163,184,0.12);
+ }
+ .rank-item__index {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(79,70,229,0.10);
+ color: #4f46e5;
+ font-size: 12px;
+ font-weight: 700;
+ flex-shrink: 0;
+ }
+ .rank-item__content {
+ flex: 1;
+ min-width: 0;
+ }
+ .rank-item__title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
+ }
+ .rank-item__title,
+ .rank-item__value {
+ font-size: 14px;
+ font-weight: 700;
+ color: #0f172a;
+ }
+ .rank-item__summary {
+ font-size: 13px;
+ line-height: 1.7;
+ color: #475569;
+ word-break: break-word;
+ }
+ .rank-item__meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 14px;
+ margin-top: 8px;
+ font-size: 12px;
+ color: #94a3b8;
+ }
.entity-cell { display: flex; align-items: center; gap: 12px; }
.entity-badge {
width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center;
@@ -903,6 +1166,28 @@
color: #94a3b8;
line-height: 1.4;
}
+ .execution-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+ .execution-cell__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+ .execution-cell__metric {
+ font-size: 12px;
+ color: #64748b;
+ font-weight: 600;
+ }
+ .execution-cell__summary {
+ font-size: 12px;
+ color: #94a3b8;
+ line-height: 1.6;
+ word-break: break-word;
+ }
.config-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
@@ -1079,6 +1364,10 @@
.mobile-plugin-card__header {
flex-direction: column;
}
+ .rank-item__title-row {
+ flex-direction: column;
+ align-items: flex-start;
+ }
.mobile-plugin-card__actions .el-button,
.mobile-group-card__actions .el-button {
flex: 1 1 calc(50% - 8px);
diff --git a/base/plugin_common/plugin_manager.py b/base/plugin_common/plugin_manager.py
index f9f96b2..91fb48e 100644
--- a/base/plugin_common/plugin_manager.py
+++ b/base/plugin_common/plugin_manager.py
@@ -602,11 +602,107 @@ class PluginManager:
"info_count": level_counts["info"],
}
+ @staticmethod
+ def _format_runtime_timestamp(timestamp_value: Any) -> str:
+ """把运行态中的 unix 时间戳转成后台可读文本。"""
+ try:
+ normalized = float(timestamp_value or 0.0)
+ except (TypeError, ValueError):
+ return ""
+ if normalized <= 0:
+ return ""
+ try:
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(normalized))
+ except (OverflowError, OSError, ValueError):
+ return ""
+
+ @staticmethod
+ def _safe_percent(numerator: Any, denominator: Any) -> float:
+ """安全计算百分比,避免分母为空时抛异常。"""
+ try:
+ denominator_value = float(denominator or 0.0)
+ if denominator_value <= 0:
+ return 0.0
+ return round((float(numerator or 0.0) / denominator_value) * 100, 2)
+ except (TypeError, ValueError, ZeroDivisionError):
+ return 0.0
+
+ def _build_execution_summary(self, guard_snapshot: Dict[str, Any]) -> Dict[str, Any]:
+ """把执行保护记录转换成更适合后台页面展示的执行摘要。
+
+ 设计考虑:
+ 1. 原始 execution_guard 更偏底层状态,前端直接消费会充满规则判断;
+ 2. 这里统一补出成功率、总执行次数、最近成功/失败时间、最近错误摘要;
+ 3. 未来如果还要做“高风险插件排行”“慢插件排行”,也能直接复用该摘要。
+ """
+ guard_snapshot = dict(guard_snapshot or {})
+ success_count_total = int(guard_snapshot.get("success_count_total", 0) or 0)
+ failure_count_total = int(guard_snapshot.get("failure_count_total", 0) or 0)
+ timeout_count_total = int(guard_snapshot.get("timeout_count_total", 0) or 0)
+ consecutive_failures = int(guard_snapshot.get("consecutive_failures", 0) or 0)
+ consecutive_timeouts = int(guard_snapshot.get("consecutive_timeouts", 0) or 0)
+ last_process_time_ms = round(float(guard_snapshot.get("last_process_time_ms", 0.0) or 0.0), 2)
+ circuit_state = str(guard_snapshot.get("circuit_state", "closed") or "closed").strip().lower()
+ last_error_message = str(guard_snapshot.get("last_error_message") or "").strip()
+ if len(last_error_message) > 240:
+ last_error_message = f"{last_error_message[:237]}..."
+
+ total_executions = success_count_total + failure_count_total
+ success_rate = self._safe_percent(success_count_total, total_executions)
+ timeout_rate = self._safe_percent(timeout_count_total, total_executions)
+ last_success_at_text = self._format_runtime_timestamp(guard_snapshot.get("last_success_at"))
+ last_failure_at_text = self._format_runtime_timestamp(guard_snapshot.get("last_failure_at"))
+
+ status = "info"
+ summary = "暂无执行样本"
+ if total_executions > 0:
+ status = "healthy"
+ summary = (
+ f"累计执行 {total_executions} 次,成功率 {success_rate}%,"
+ f"最近耗时 {last_process_time_ms}ms"
+ )
+
+ # 熔断打开是最明确的高风险信号,应优先标记为 error。
+ if circuit_state == "open":
+ status = "error"
+ summary = (
+ f"插件当前处于熔断中,连续失败 {consecutive_failures} 次,"
+ f"恢复剩余 {int(guard_snapshot.get('open_remaining_seconds', 0) or 0)}s"
+ )
+ elif failure_count_total > 0 or timeout_count_total > 0 or consecutive_failures > 0 or consecutive_timeouts > 0:
+ status = "warning"
+ summary = (
+ f"累计失败 {failure_count_total} 次,超时 {timeout_count_total} 次,"
+ f"成功率 {success_rate}%"
+ )
+
+ return {
+ "status": status,
+ "summary": summary,
+ "total_executions": total_executions,
+ "success_count_total": success_count_total,
+ "failure_count_total": failure_count_total,
+ "timeout_count_total": timeout_count_total,
+ "success_rate": success_rate,
+ "timeout_rate": timeout_rate,
+ "consecutive_failures": consecutive_failures,
+ "consecutive_timeouts": consecutive_timeouts,
+ "last_process_time_ms": last_process_time_ms,
+ "last_success_at_text": last_success_at_text,
+ "last_failure_at_text": last_failure_at_text,
+ "last_error_message": last_error_message,
+ "last_failure_type": str(guard_snapshot.get("last_failure_type") or "").strip(),
+ "last_timeout_seconds": int(guard_snapshot.get("last_timeout_seconds", 0) or 0),
+ "circuit_state": circuit_state,
+ "open_remaining_seconds": int(guard_snapshot.get("open_remaining_seconds", 0) or 0),
+ }
+
def _build_plugin_snapshot(self, plugin: PluginInterface) -> Dict[str, Any]:
"""为已加载插件生成标准治理快照。"""
module_name = self._get_module_name_from_plugin(plugin) or "unknown"
runtime_record = self._get_module_runtime_state(module_name)
guard_snapshot = self.get_plugin_guard_snapshot(module_name)
+ execution_summary = self._build_execution_summary(guard_snapshot)
config_path = plugin.get_config_path()
config_overview = self._read_plugin_config_overview(config_path)
commands = self._collect_plugin_commands(plugin)
@@ -648,12 +744,14 @@ class PluginManager:
"runtime_state": runtime_record.get("state", "loaded"),
"runtime_message": runtime_record.get("message", ""),
"execution_guard": guard_snapshot,
+ "execution_summary": execution_summary,
}
def _build_unloaded_plugin_snapshot(self, module_name: str) -> Dict[str, Any]:
"""为未成功加载的插件模块生成治理快照。"""
runtime_record = self._get_module_runtime_state(module_name)
guard_snapshot = self.get_plugin_guard_snapshot(module_name)
+ execution_summary = self._build_execution_summary(guard_snapshot)
config_path = os.path.join(self.plugin_dir, module_name, "config.toml")
if not os.path.exists(config_path):
config_path = os.path.join(self.plugin_dir, f"{module_name}", "config.toml")
@@ -700,6 +798,7 @@ class PluginManager:
"runtime_state": runtime_state or "discovered",
"runtime_message": runtime_record.get("message", ""),
"execution_guard": guard_snapshot,
+ "execution_summary": execution_summary,
}
@staticmethod
diff --git a/docs/工程优化与Feature清单.md b/docs/工程优化与Feature清单.md
index 835d5b4..12cb132 100644
--- a/docs/工程优化与Feature清单.md
+++ b/docs/工程优化与Feature清单.md
@@ -405,6 +405,7 @@
- 第一阶段已完成:`PluginManager` 已输出统一插件治理快照,后台不再只展示“加载成功的插件”
- 第一阶段已完成:后台插件管理页已补充治理健康、能力类型、Feature Key、依赖与配置概览信息
- 第一阶段已完成:插件配置保存前已增加格式校验,避免坏配置直接写回线上文件
+- 第二阶段已完成:插件管理页已补充执行表现摘要、最近错误信息与高风险/慢插件排行,便于快速定位运行异常插件
- 后续可继续补充插件错误历史、性能排名、依赖图与熔断/隔离控制
建议内容: