From 369b74e834085bec8f7ac009b7c05377cfd5a37d Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 30 Apr 2026 17:35:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=8F=92=E4=BB=B6=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=8B=93=E6=89=91=E4=B8=8E=E7=BC=BA=E5=A4=B1=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E9=A3=8E=E9=99=A9=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/dashboard/templates/plugins_manage.html | 184 ++++++++++++++++++ base/plugin_common/plugin_manager.py | 81 ++++++++ docs/工程优化与Feature清单.md | 1 + 3 files changed, 266 insertions(+) diff --git a/admin/dashboard/templates/plugins_manage.html b/admin/dashboard/templates/plugins_manage.html index 367e132..69cc002 100644 --- a/admin/dashboard/templates/plugins_manage.html +++ b/admin/dashboard/templates/plugins_manage.html @@ -120,6 +120,70 @@ + + + +
+
+

依赖核心插件

+

优先保护被多个插件依赖的基础能力节点,避免单点异常扩散。

+
+
+
+
暂无依赖关系数据
+
+
{% raw %}{{ index + 1 }}{% endraw %}
+
+
+
{% raw %}{{ plugin.name }}{% endraw %}
+ + {% raw %}{{ `${(plugin.dependency_summary || {}).dependent_count || 0} 个上游` }}{% endraw %} + +
+
+ {% raw %}{{ buildDependencyCoreSummary(plugin) }}{% endraw %} +
+
+ 执行:{% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %} + 治理:{% raw %}{{ governanceLabel(plugin.governance_status) }}{% endraw %} +
+
+
+
+
+
+ + +
+
+

缺失依赖风险

+

快速查看声明了依赖但当前目标未加载的插件,优先处理运行链断裂问题。

+
+
+
+
当前没有缺失依赖风险
+
+
{% raw %}{{ index + 1 }}{% endraw %}
+
+
+
{% raw %}{{ plugin.name }}{% endraw %}
+ + {% raw %}{{ `${(plugin.dependency_summary || {}).missing_count || 0} 个缺失` }}{% endraw %} + +
+
+ {% raw %}{{ buildMissingDependencySummary(plugin) }}{% endraw %} +
+
+ 模块:{% raw %}{{ plugin.module_name }}{% endraw %} +
+
+
+
+
+
+
+
@@ -258,6 +322,9 @@ {% raw %}{{ governanceIssueSummary(plugin) }}{% endraw %} {% raw %}{{ plugin.feature_key ? `Feature: ${plugin.feature_key}` : '未接入群级权限' }}{% endraw %}
+
+ 依赖:{% raw %}{{ buildDependencySummaryText(plugin) }}{% endraw %} +
+ +
+
+ 声明依赖 + {% raw %}{{ selectedPlugin.dependency_summary.declared_count || 0 }}{% endraw %} +
+
+ 已解析依赖 + {% raw %}{{ selectedPlugin.dependency_summary.resolved_count || 0 }}{% endraw %} +
+
+ 缺失依赖 + {% raw %}{{ selectedPlugin.dependency_summary.missing_count || 0 }}{% endraw %} +
+
+ 下游依赖 + {% raw %}{{ selectedPlugin.dependency_summary.dependent_count || 0 }}{% endraw %} +
+
+
+
+
已解析依赖
+
+ + {% raw %}{{ `${dependency.name} (${dependency.status_label || dependency.status || '未知'})` }}{% endraw %} + +
+
+
+
+
缺失依赖
+
+ + {% raw %}{{ dependency.name }}{% endraw %} + +
+
+
+
+
下游依赖插件
+
+ + {% raw %}{{ `${dependency.name} (${dependency.status_label || dependency.status || '未知'})` }}{% endraw %} + +
+
+
+
+
@@ -661,6 +777,29 @@ }) .slice(0, 5); }, + topDependencyCorePlugins() { + // 核心依赖插件优先按“被多少插件依赖”排序, + // 这样最容易形成单点影响的基础插件会排在前面。 + return (this.plugins || []) + .filter(plugin => Number(((plugin.dependency_summary || {}).dependent_count) || 0) > 0) + .slice() + .sort((left, right) => { + return ( + Number(((right.dependency_summary || {}).dependent_count) || 0) - Number(((left.dependency_summary || {}).dependent_count) || 0) + || Number(((right.dependency_summary || {}).declared_count) || 0) - Number(((left.dependency_summary || {}).declared_count) || 0) + ); + }) + .slice(0, 5); + }, + pluginsWithMissingDependencies() { + return (this.plugins || []) + .filter(plugin => Number(((plugin.dependency_summary || {}).missing_count) || 0) > 0) + .slice() + .sort((left, right) => { + return Number(((right.dependency_summary || {}).missing_count) || 0) - Number(((left.dependency_summary || {}).missing_count) || 0); + }) + .slice(0, 5); + }, slowestPlugins() { // 慢插件排行只看有执行样本的插件,避免未执行插件把榜单冲掉。 return (this.plugins || []) @@ -774,6 +913,33 @@ if (!Number.isFinite(normalizedValue) || normalizedValue <= 0) return '-'; return `${normalizedValue.toFixed(2)} ms`; }, + buildDependencySummaryText(plugin) { + const dependencySummary = (plugin && plugin.dependency_summary) || {}; + const declaredCount = Number(dependencySummary.declared_count || 0); + const missingCount = Number(dependencySummary.missing_count || 0); + const dependentCount = Number(dependencySummary.dependent_count || 0); + if (declaredCount <= 0 && dependentCount <= 0) { + return '无依赖关系'; + } + if (missingCount > 0) { + return `声明 ${declaredCount} 个,缺失 ${missingCount} 个`; + } + if (dependentCount > 0) { + return `被 ${dependentCount} 个插件依赖`; + } + return `已解析 ${declaredCount} 个依赖`; + }, + buildDependencyCoreSummary(plugin) { + const dependencySummary = (plugin && plugin.dependency_summary) || {}; + return `当前被 ${(dependencySummary.dependent_count || 0)} 个插件依赖,自身声明 ${(dependencySummary.declared_count || 0)} 个依赖。`; + }, + buildMissingDependencySummary(plugin) { + const missingDependencies = ((plugin && plugin.missing_dependencies) || []).map(item => item.name).filter(Boolean); + if (!missingDependencies.length) { + return '当前没有缺失依赖。'; + } + return `缺失依赖:${missingDependencies.join('、')}`; + }, loadPlugins() { this.loading = true; axios.get('/api/plugins') @@ -1193,6 +1359,24 @@ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; } + .dependency-panels { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin-top: 12px; + } + .dependency-panel { + padding: 12px; + border-radius: 14px; + background: rgba(248,250,252,0.82); + border: 1px solid rgba(148,163,184,0.12); + } + .dependency-panel__title { + font-size: 13px; + font-weight: 700; + color: #334155; + margin-bottom: 8px; + } .config-overview-item { display: flex; flex-direction: column; diff --git a/base/plugin_common/plugin_manager.py b/base/plugin_common/plugin_manager.py index 91fb48e..37e1f65 100644 --- a/base/plugin_common/plugin_manager.py +++ b/base/plugin_common/plugin_manager.py @@ -801,6 +801,85 @@ class PluginManager: "execution_summary": execution_summary, } + @staticmethod + def _normalize_snapshot_dependency_key(value: Any) -> str: + """把依赖引用统一规整成可匹配的 key。""" + return str(value or "").strip().lower() + + def _enrich_dependency_relationships(self, snapshots: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """为插件快照补齐依赖关系摘要。 + + 设计目标: + 1. 后台既要看“我依赖谁”,也要看“谁依赖我”; + 2. 依赖声明有可能写插件名,也可能写模块名,因此这里做双 key 兼容; + 3. 缺失依赖需要单独产出,便于页面上做风险聚合与高亮。 + """ + normalized_snapshots = [dict(item or {}) for item in (snapshots or [])] + lookup_by_key: Dict[str, Dict[str, Any]] = {} + + for snapshot in normalized_snapshots: + module_name = str(snapshot.get("module_name") or "").strip() + display_name = str(snapshot.get("name") or "").strip() + if module_name: + lookup_by_key[self._normalize_snapshot_dependency_key(module_name)] = snapshot + if display_name: + lookup_by_key[self._normalize_snapshot_dependency_key(display_name)] = snapshot + + for snapshot in normalized_snapshots: + snapshot["resolved_dependencies"] = [] + snapshot["missing_dependencies"] = [] + snapshot["dependent_plugins"] = [] + + for snapshot in normalized_snapshots: + dependencies = list(snapshot.get("dependencies", []) or []) + resolved_dependencies = [] + missing_dependencies = [] + + for dependency_name in dependencies: + normalized_key = self._normalize_snapshot_dependency_key(dependency_name) + target_snapshot = lookup_by_key.get(normalized_key) + if target_snapshot: + dependency_row = { + "name": str(target_snapshot.get("name") or "").strip(), + "module_name": str(target_snapshot.get("module_name") or "").strip(), + "status": str(target_snapshot.get("status") or "").strip(), + "status_label": str(target_snapshot.get("status_label") or "").strip(), + "governance_status": str(target_snapshot.get("governance_status") or "").strip(), + } + resolved_dependencies.append(dependency_row) + target_snapshot.setdefault("dependent_plugins", []).append( + { + "name": str(snapshot.get("name") or "").strip(), + "module_name": str(snapshot.get("module_name") or "").strip(), + "status": str(snapshot.get("status") or "").strip(), + "status_label": str(snapshot.get("status_label") or "").strip(), + "governance_status": str(snapshot.get("governance_status") or "").strip(), + } + ) + else: + missing_dependencies.append( + { + "name": str(dependency_name or "").strip(), + } + ) + + snapshot["resolved_dependencies"] = resolved_dependencies + snapshot["missing_dependencies"] = missing_dependencies + snapshot["dependency_summary"] = { + "declared_count": len(dependencies), + "resolved_count": len(resolved_dependencies), + "missing_count": len(missing_dependencies), + "dependent_count": len(snapshot.get("dependent_plugins", []) or []), + "has_missing": len(missing_dependencies) > 0, + } + + for snapshot in normalized_snapshots: + dependent_plugins = list(snapshot.get("dependent_plugins", []) or []) + dependent_plugins.sort(key=lambda item: (str(item.get("name") or ""), str(item.get("module_name") or ""))) + snapshot["dependent_plugins"] = dependent_plugins + + return normalized_snapshots + @staticmethod def _status_to_label(status: str) -> str: """把运行态状态码转换成中文展示文案。""" @@ -833,6 +912,8 @@ class PluginManager: for module_name in sorted(discovered_module_names - loaded_module_names): snapshots.append(self._build_unloaded_plugin_snapshot(module_name)) + snapshots = self._enrich_dependency_relationships(snapshots) + snapshots.sort( key=lambda item: ( 0 if item.get("status") == "RUNNING" else 1, diff --git a/docs/工程优化与Feature清单.md b/docs/工程优化与Feature清单.md index 12cb132..f32d204 100644 --- a/docs/工程优化与Feature清单.md +++ b/docs/工程优化与Feature清单.md @@ -406,6 +406,7 @@ - 第一阶段已完成:后台插件管理页已补充治理健康、能力类型、Feature Key、依赖与配置概览信息 - 第一阶段已完成:插件配置保存前已增加格式校验,避免坏配置直接写回线上文件 - 第二阶段已完成:插件管理页已补充执行表现摘要、最近错误信息与高风险/慢插件排行,便于快速定位运行异常插件 +- 第二阶段已完成:插件快照已补充依赖拓扑摘要,后台可直接查看核心依赖插件、缺失依赖风险与上下游关系 - 后续可继续补充插件错误历史、性能排名、依赖图与熔断/隔离控制 建议内容: