完善插件治理中心第一阶段

- 为 PluginManager 增加统一插件治理快照,补充配置概览、治理诊断、运行态记录与未加载模块展示\n- 更新插件管理后台页面,展示治理健康、能力类型、Feature Key、依赖关系与配置概览信息\n- 优化插件配置保存流程,保存前先做格式校验,并支持对未加载插件查看详情与重新尝试加载\n- 更新工程优化文档,记录插件治理中心第一阶段的当前进展
This commit is contained in:
liuwei
2026-04-30 16:07:02 +08:00
parent 97fc6dc2a4
commit b0e11fb9b5
4 changed files with 738 additions and 92 deletions

View File

@@ -26,28 +26,11 @@ def get_plugins():
"""获取所有插件列表""" """获取所有插件列表"""
try: try:
server = current_app.dashboard_server server = current_app.dashboard_server
# 获取插件注册表 # 统一改为消费 PluginManager 的标准治理快照:
plugins = server.plugin_registry.get_all_plugins() # 1. 这样既能覆盖“已加载插件”,也能覆盖“发现但加载失败/配置禁用”的模块;
# 2. 后台不必重复拼装版本、命令、依赖、配置健康等字段;
# 转换为前端需要的格式 # 3. 后续继续补错误统计、性能排名时,也只需要在快照层扩展。
plugin_list = [] plugin_list = server.plugin_manager.get_plugin_snapshots()
for name, plugin in plugins.items():
# 获取插件模块名
try:
module_name = plugin.__class__.__module__.split('.')[-2]
except (IndexError, AttributeError):
module_name = "unknown"
plugin_info = {
"name": plugin.name,
"module_name": module_name,
"version": getattr(plugin, 'version', 'N/A'),
"author": getattr(plugin, 'author', 'N/A'),
"description": getattr(plugin, 'description', 'N/A'),
"status": plugin.status.name if hasattr(plugin, 'status') else 'UNKNOWN'
}
plugin_list.append(plugin_info)
return jsonify({"success": True, "data": plugin_list}) return jsonify({"success": True, "data": plugin_list})
except Exception as e: except Exception as e:
LOG.error(f"获取插件列表失败: {str(e)}", exc_info=True) LOG.error(f"获取插件列表失败: {str(e)}", exc_info=True)
@@ -193,31 +176,10 @@ def get_plugin_info():
if not plugin_name: if not plugin_name:
return jsonify({"success": False, "message": "缺少插件名称参数"}) return jsonify({"success": False, "message": "缺少插件名称参数"})
# 获取插件管理器 plugin_info = server.plugin_manager.get_plugin_snapshot(plugin_name)
display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) if not plugin_info:
if not plugin:
return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"})
# 获取插件模块名
try:
module_name = plugin.__class__.__module__.split('.')[-2]
except (IndexError, AttributeError):
module_name = "unknown"
# 构建详细信息
plugin_info = {
"name": plugin.name,
"module_name": module_name,
"version": getattr(plugin, 'version', 'N/A'),
"author": getattr(plugin, 'author', 'N/A'),
"description": getattr(plugin, 'description', 'N/A'),
"status": plugin.status.name if hasattr(plugin, 'status') else 'UNKNOWN',
"command_prefix": getattr(plugin, 'command_prefix', ''),
"commands": getattr(plugin, 'commands', []),
"config": getattr(plugin, '_config', {})
}
return jsonify({"success": True, "data": plugin_info}) return jsonify({"success": True, "data": plugin_info})
except Exception as e: except Exception as e:
LOG.error(f"获取插件详情失败: {str(e)}", exc_info=True) LOG.error(f"获取插件详情失败: {str(e)}", exc_info=True)
@@ -235,9 +197,14 @@ def enable_plugin():
if not plugin_name: if not plugin_name:
return jsonify({"success": False, "message": "缺少插件名称参数"}) return jsonify({"success": False, "message": "缺少插件名称参数"})
# 获取插件管理器 # 已加载插件直接启动;尚未加载的插件则先尝试加载,再进入启动流程。
# 启用插件 display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name)
if server.plugin_manager.start_plugin(plugin_name): if not plugin:
plugin = server.plugin_manager.load_plugin(plugin_name)
if plugin:
display_name = plugin.name
if plugin and server.plugin_manager.start_plugin(display_name or plugin_name):
return jsonify({"success": True, "message": f"插件 {plugin_name} 启用成功"}) return jsonify({"success": True, "message": f"插件 {plugin_name} 启用成功"})
else: else:
return jsonify({"success": False, "message": f"插件 {plugin_name} 启用失败"}) return jsonify({"success": False, "message": f"插件 {plugin_name} 启用失败"})
@@ -278,8 +245,14 @@ def reload_plugin():
if not plugin_name: if not plugin_name:
return jsonify({"success": False, "message": "缺少插件名称参数"}) return jsonify({"success": False, "message": "缺少插件名称参数"})
# 载插件 # 已加载插件优先走重载;若当前未加载,则退化为“重新尝试加载并启动”。
reloaded_plugin = server.plugin_manager.reload_plugin(plugin_name) display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name)
if plugin:
reloaded_plugin = server.plugin_manager.reload_plugin(plugin_name)
else:
reloaded_plugin = server.plugin_manager.load_plugin(plugin_name)
if reloaded_plugin:
server.plugin_manager.start_plugin(reloaded_plugin.name)
if reloaded_plugin: if reloaded_plugin:
return jsonify({"success": True, "message": f"插件 {plugin_name} 重载成功"}) return jsonify({"success": True, "message": f"插件 {plugin_name} 重载成功"})
@@ -300,16 +273,11 @@ def get_raw_plugin_config():
if not plugin_name: if not plugin_name:
return jsonify({"success": False, "message": "缺少插件名称参数"}) return jsonify({"success": False, "message": "缺少插件名称参数"})
# 获取插件管理器 plugin_snapshot = server.plugin_manager.get_plugin_snapshot(plugin_name)
if not plugin_snapshot:
# 查找插件
display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name)
if not plugin:
return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"})
# 获取配置文件路径 config_path = str(plugin_snapshot.get("config_path", "") or "").strip()
config_path = plugin.get_config_path()
if not os.path.exists(config_path): if not os.path.exists(config_path):
return jsonify({"success": False, "message": f"配置文件不存在: {config_path}"}) return jsonify({"success": False, "message": f"配置文件不存在: {config_path}"})
@@ -349,15 +317,29 @@ def update_plugin_config():
if not plugin_name or config_text is None: if not plugin_name or config_text is None:
return jsonify({"success": False, "message": "缺少必要参数"}) return jsonify({"success": False, "message": "缺少必要参数"})
# 查找插件
# 获取插件管理器
display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name)
plugin_snapshot = server.plugin_manager.get_plugin_snapshot(plugin_name)
if not plugin: if not plugin_snapshot:
return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"})
# 获取配置文件路径 config_path = str(plugin_snapshot.get("config_path", "") or "").strip()
config_path = plugin.get_config_path() if not config_path:
return jsonify({"success": False, "message": "插件未声明配置路径,暂不支持在线编辑"})
# 保存前先做格式校验:
# 1. 避免把坏 TOML 先写回磁盘,再让插件进入“文件已坏但提示成功”的状态;
# 2. 校验通过后再真正落盘,失败则保留线上旧配置;
# 3. 这也是插件治理中心第一阶段的“配置校验底座”。
try:
if format_type == 'toml':
config_obj = toml.loads(config_text)
elif format_type == 'json':
config_obj = json.loads(config_text)
else:
return jsonify({"success": False, "message": f"不支持的配置格式: {format_type}"})
except Exception as parse_error:
LOG.error(f"解析配置失败: {str(parse_error)}", exc_info=True)
return jsonify({"success": False, "message": f"配置格式校验失败: {str(parse_error)}"})
# 确保配置目录存在 # 确保配置目录存在
os.makedirs(os.path.dirname(config_path), exist_ok=True) os.makedirs(os.path.dirname(config_path), exist_ok=True)
@@ -366,22 +348,11 @@ def update_plugin_config():
with open(config_path, 'w', encoding='utf-8') as f: with open(config_path, 'w', encoding='utf-8') as f:
f.write(config_text) f.write(config_text)
# 解析配置并更新插件内部配置 # 若插件当前已加载,则同步刷新内存中的配置镜像,减少“保存后详情弹窗仍是旧配置”的困惑。
try: if plugin:
if format_type == 'toml':
config_obj = toml.loads(config_text)
elif format_type == 'json':
config_obj = json.loads(config_text)
else:
return jsonify({"success": False, "message": f"不支持的配置格式: {format_type}"})
# 更新插件内部配置
plugin._config = config_obj plugin._config = config_obj
return jsonify({"success": True, "message": "配置已保存"}) return jsonify({"success": True, "message": "配置已保存并通过格式校验"})
except Exception as e:
LOG.error(f"解析配置失败: {str(e)}", exc_info=True)
return jsonify({"success": False, "message": f"配置已保存,但解析失败: {str(e)}"})
except Exception as e: except Exception as e:
LOG.error(f"更新插件配置失败: {str(e)}", exc_info=True) LOG.error(f"更新插件配置失败: {str(e)}", exc_info=True)

View File

@@ -41,9 +41,9 @@
</el-col> </el-col>
<el-col :xs="24" :sm="12" :md="6"> <el-col :xs="24" :sm="12" :md="6">
<el-card class="overview-card overview-card--soft" shadow="hover"> <el-card class="overview-card overview-card--soft" shadow="hover">
<div class="overview-label">作者数量</div> <div class="overview-label">治理告警</div>
<div class="overview-value">{% raw %}{{ authorsCount }}{% endraw %}</div> <div class="overview-value">{% raw %}{{ governanceRiskCount }}{% endraw %}</div>
<div class="overview-note">参与维护的作者规模</div> <div class="overview-note">存在配置、依赖或加载风险的插件</div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -74,11 +74,42 @@
<el-table-column prop="description" label="描述" min-width="280" show-overflow-tooltip></el-table-column> <el-table-column prop="description" label="描述" min-width="280" show-overflow-tooltip></el-table-column>
<el-table-column label="状态" width="120" align="center"> <el-table-column label="状态" width="120" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag :type="scope.row.status === 'RUNNING' ? 'success' : 'info'"> <el-tag :type="pluginStatusTagType(scope.row.status)">
{% raw %}{{ scope.row.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %} {% raw %}{{ pluginStatusLabel(scope.row) }}{% endraw %}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="治理健康" width="170" align="center">
<template slot-scope="scope">
<div class="governance-cell">
<el-tag :type="governanceTagType(scope.row.governance_status)" size="small">
{% raw %}{{ governanceLabel(scope.row.governance_status) }}{% endraw %}
</el-tag>
<div class="governance-note">
{% raw %}{{ governanceIssueSummary(scope.row) }}{% endraw %}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="能力类型" width="150" align="center">
<template slot-scope="scope">
<div class="command-tags command-tags--compact">
<el-tag v-for="pluginType in (scope.row.plugin_types || [])" :key="pluginType" size="mini" effect="plain">
{% raw %}{{ pluginType }}{% endraw %}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="命令 / 权限" min-width="180">
<template slot-scope="scope">
<div class="entity-subtitle">
{% raw %}{{ scope.row.command_count ? `命令 ${scope.row.command_count} 个` : '无命令声明' }}{% endraw %}
</div>
<div class="entity-subtitle">
{% raw %}{{ scope.row.feature_key ? `Feature: ${scope.row.feature_key}` : '未接入群级权限' }}{% endraw %}
</div>
</template>
</el-table-column>
<el-table-column label="操作" min-width="290"> <el-table-column label="操作" min-width="290">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="action-row"> <div class="action-row">
@@ -118,17 +149,21 @@
<div class="entity-subtitle">模块:{% raw %}{{ plugin.module_name }}{% endraw %}</div> <div class="entity-subtitle">模块:{% raw %}{{ plugin.module_name }}{% endraw %}</div>
</div> </div>
</div> </div>
<el-tag :type="plugin.status === 'RUNNING' ? 'success' : 'info'" size="small"> <el-tag :type="pluginStatusTagType(plugin.status)" size="small">
{% raw %}{{ plugin.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %} {% raw %}{{ pluginStatusLabel(plugin) }}{% endraw %}
</el-tag> </el-tag>
</div> </div>
<div class="mobile-plugin-card__meta"> <div class="mobile-plugin-card__meta">
<span>版本:{% raw %}{{ plugin.version || '未知' }}{% endraw %}</span> <span>版本:{% raw %}{{ plugin.version || '未知' }}{% endraw %}</span>
<span>作者{% raw %}{{ plugin.author || '未知' }}{% endraw %}</span> <span>治理{% raw %}{{ governanceLabel(plugin.governance_status) }}{% endraw %}</span>
</div> </div>
<div class="mobile-plugin-card__desc"> <div class="mobile-plugin-card__desc">
{% raw %}{{ plugin.description || '暂无描述' }}{% endraw %} {% raw %}{{ plugin.description || '暂无描述' }}{% endraw %}
</div> </div>
<div class="mobile-plugin-card__meta">
<span>{% raw %}{{ governanceIssueSummary(plugin) }}{% endraw %}</span>
<span>{% raw %}{{ plugin.feature_key ? `Feature: ${plugin.feature_key}` : '未接入群级权限' }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__actions"> <div class="mobile-plugin-card__actions">
<el-button <el-button
size="mini" size="mini"
@@ -159,14 +194,40 @@
<el-descriptions-item label="版本" :span="1">{% raw %}{{ selectedPlugin.version }}{% endraw %}</el-descriptions-item> <el-descriptions-item label="版本" :span="1">{% raw %}{{ selectedPlugin.version }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="作者" :span="1">{% raw %}{{ selectedPlugin.author }}{% endraw %}</el-descriptions-item> <el-descriptions-item label="作者" :span="1">{% raw %}{{ selectedPlugin.author }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="状态" :span="1"> <el-descriptions-item label="状态" :span="1">
<el-tag :type="selectedPlugin.status === 'RUNNING' ? 'success' : 'info'" size="small"> <el-tag :type="pluginStatusTagType(selectedPlugin.status)" size="small">
{% raw %}{{ selectedPlugin.status === 'RUNNING' ? '已启用' : '已禁用' }}{% endraw %} {% raw %}{{ pluginStatusLabel(selectedPlugin) }}{% endraw %}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="治理健康" :span="1">
<el-tag :type="governanceTagType(selectedPlugin.governance_status)" size="small">
{% raw %}{{ governanceLabel(selectedPlugin.governance_status) }}{% endraw %}
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="命令前缀" :span="1" v-if="selectedPlugin.command_prefix !== undefined"> <el-descriptions-item label="命令前缀" :span="1" v-if="selectedPlugin.command_prefix !== undefined">
{% raw %}{{ selectedPlugin.command_prefix || '无' }}{% endraw %} {% raw %}{{ selectedPlugin.command_prefix || '无' }}{% endraw %}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{% raw %}{{ selectedPlugin.description }}{% endraw %}</el-descriptions-item> <el-descriptions-item label="描述" :span="2">{% raw %}{{ selectedPlugin.description }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="能力类型" :span="2" v-if="selectedPlugin.plugin_types && selectedPlugin.plugin_types.length > 0">
<div class="command-tags">
<el-tag v-for="pluginType in selectedPlugin.plugin_types" :key="pluginType" size="mini" effect="plain">
{% raw %}{{ pluginType }}{% endraw %}
</el-tag>
</div>
</el-descriptions-item>
<el-descriptions-item label="Feature Key" :span="1">
{% raw %}{{ selectedPlugin.feature_key || '未声明' }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="群级开关" :span="1">
{% raw %}{{ selectedPlugin.supports_group_switch ? '支持' : '未接入' }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="依赖插件" :span="2">
<div v-if="selectedPlugin.dependencies && selectedPlugin.dependencies.length > 0" class="command-tags">
<el-tag v-for="dependency in selectedPlugin.dependencies" :key="dependency" size="mini" effect="plain">
{% raw %}{{ dependency }}{% endraw %}
</el-tag>
</div>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="命令列表" :span="2" v-if="selectedPlugin.commands && selectedPlugin.commands.length > 0"> <el-descriptions-item label="命令列表" :span="2" v-if="selectedPlugin.commands && selectedPlugin.commands.length > 0">
<div class="command-tags"> <div class="command-tags">
<el-tag v-for="cmd in selectedPlugin.commands" :key="cmd" size="mini" class="command-tag"> <el-tag v-for="cmd in selectedPlugin.commands" :key="cmd" size="mini" class="command-tag">
@@ -174,6 +235,46 @@
</el-tag> </el-tag>
</div> </div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="配置概览" :span="2" v-if="selectedPlugin.config_overview">
<div class="config-overview-grid">
<div class="config-overview-item">
<span class="config-overview-label">配置文件</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.exists ? '存在' : '缺失' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">解析状态</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.parse_ok ? '正常' : '失败' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">配置分组</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.section_count || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">敏感字段</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.sensitive_field_count || 0 }}{% endraw %}</span>
</div>
</div>
<div class="entity-subtitle" style="margin-top: 8px;">
{% raw %}{{ selectedPlugin.config_path || '未声明配置路径' }}{% endraw %}
</div>
<div class="entity-subtitle" v-if="selectedPlugin.config_overview.parse_error">
{% raw %}{{ `解析错误:${selectedPlugin.config_overview.parse_error}` }}{% endraw %}
</div>
</el-descriptions-item>
<el-descriptions-item label="治理诊断" :span="2" v-if="selectedPlugin.governance_diagnostics">
<div v-if="selectedPlugin.governance_diagnostics.length > 0" class="diagnostic-list">
<div
v-for="(diagnostic, index) in selectedPlugin.governance_diagnostics"
:key="`${diagnostic.code}-${index}`"
class="diagnostic-item">
<el-tag :type="governanceTagType(diagnostic.level)" size="mini">
{% raw %}{{ governanceLabel(diagnostic.level) }}{% endraw %}
</el-tag>
<span class="diagnostic-text">{% raw %}{{ diagnostic.message }}{% endraw %}</span>
</div>
</div>
<span v-else>暂无治理诊断项</span>
</el-descriptions-item>
<el-descriptions-item label="配置信息" :span="2" v-if="selectedPlugin.config"> <el-descriptions-item label="配置信息" :span="2" v-if="selectedPlugin.config">
<div class="config-container"> <div class="config-container">
<div class="config-actions"> <div class="config-actions">
@@ -370,8 +471,8 @@
stoppedPluginsCount() { stoppedPluginsCount() {
return this.plugins.filter(plugin => plugin.status !== 'RUNNING').length; return this.plugins.filter(plugin => plugin.status !== 'RUNNING').length;
}, },
authorsCount() { governanceRiskCount() {
return new Set((this.plugins || []).map(plugin => plugin.author).filter(Boolean)).size; return (this.plugins || []).filter(plugin => ['warning', 'error'].includes((plugin.governance_status || '').toLowerCase())).length;
}, },
// 弹窗宽度按视口分级收缩,保证手机上弹窗内容不会贴边或继续触发横向溢出。 // 弹窗宽度按视口分级收缩,保证手机上弹窗内容不会贴边或继续触发横向溢出。
pluginInfoDialogWidth() { pluginInfoDialogWidth() {
@@ -400,6 +501,55 @@
// 这里统一以 768px 作为移动端断点,和常见后台管理布局断点保持一致。 // 这里统一以 768px 作为移动端断点,和常见后台管理布局断点保持一致。
this.isMobileViewport = window.innerWidth <= 768; this.isMobileViewport = window.innerWidth <= 768;
}, },
pluginStatusTagType(status) {
const normalizedStatus = String(status || '').toUpperCase();
if (normalizedStatus === 'RUNNING') return 'success';
if (normalizedStatus === 'ERROR') return 'danger';
if (normalizedStatus === 'LOADED') return 'warning';
return 'info';
},
pluginStatusLabel(plugin) {
if (plugin && plugin.status_label) return plugin.status_label;
const normalizedStatus = String(plugin?.status || '').toUpperCase();
const mapping = {
RUNNING: '运行中',
STOPPED: '已停用',
LOADED: '已加载',
UNLOADED: '未加载',
ERROR: '异常',
DISCOVERED: '待处理'
};
return mapping[normalizedStatus] || '未知';
},
governanceTagType(level) {
const normalizedLevel = String(level || '').toLowerCase();
if (normalizedLevel === 'error') return 'danger';
if (normalizedLevel === 'warning') return 'warning';
if (normalizedLevel === 'healthy') return 'success';
return 'info';
},
governanceLabel(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);
if (errorCount > 0 || warningCount > 0) {
return `错误 ${errorCount} / 告警 ${warningCount}`;
}
if (infoCount > 0) {
return `提示 ${infoCount}`;
}
return '暂无治理问题';
},
loadPlugins() { loadPlugins() {
this.loading = true; this.loading = true;
axios.get('/api/plugins') axios.get('/api/plugins')
@@ -487,7 +637,6 @@
}, },
saveConfig() { saveConfig() {
try { try {
let configObj;
axios.post('/api/plugins/config/update', { axios.post('/api/plugins/config/update', {
plugin_name: this.selectedPlugin.module_name, plugin_name: this.selectedPlugin.module_name,
config_text: this.editedConfig, config_text: this.editedConfig,
@@ -498,7 +647,11 @@
this.$message.success('配置保存成功'); this.$message.success('配置保存成功');
this.isEditingConfig = false; this.isEditingConfig = false;
this.selectedPlugin.configText = this.editedConfig; this.selectedPlugin.configText = this.editedConfig;
this.selectedPlugin.config = configObj; // 保存成功后立即重新拉取详情:
// 1. 同步刷新治理诊断、配置概览和内存中的插件配置快照;
// 2. 避免页面上继续停留在旧的健康状态;
// 3. 这样后续是否重载插件,用户都能先看到“配置文本已通过校验并落盘”。
this.showPluginInfo(this.selectedPlugin);
this.$confirm('配置已保存,是否要重载插件以应用新配置?', '提示', { this.$confirm('配置已保存,是否要重载插件以应用新配置?', '提示', {
confirmButtonText: '重载插件', confirmButtonText: '重载插件',
cancelButtonText: '稍后手动重载', cancelButtonText: '稍后手动重载',
@@ -714,10 +867,66 @@
} }
.config-container pre { margin: 0; white-space: pre-wrap; word-break: break-word; } .config-container pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
.command-tags { display: flex; flex-wrap: wrap; gap: 6px; } .command-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.command-tags--compact { justify-content: center; }
.command-tag { margin: 0 !important; } .command-tag { margin: 0 !important; }
.config-actions { margin-bottom: 10px; display: flex; gap: 10px; } .config-actions { margin-bottom: 10px; display: flex; gap: 10px; }
.config-editor { font-family: monospace; font-size: 12px; } .config-editor { font-family: monospace; font-size: 12px; }
.config-error { color: #ef4444; font-size: 12px; margin-top: 5px; } .config-error { color: #ef4444; font-size: 12px; margin-top: 5px; }
.governance-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.governance-note {
font-size: 11px;
color: #94a3b8;
line-height: 1.4;
}
.config-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.config-overview-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.72);
border: 1px solid rgba(148,163,184,0.12);
}
.config-overview-label {
font-size: 12px;
color: #64748b;
}
.config-overview-value {
font-size: 14px;
font-weight: 600;
color: #0f172a;
}
.diagnostic-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.diagnostic-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(248,250,252,0.82);
border: 1px solid rgba(148,163,184,0.12);
}
.diagnostic-text {
flex: 1;
font-size: 13px;
color: #334155;
line-height: 1.6;
word-break: break-word;
}
.plugin-group-status-dialog { min-height: 240px; } .plugin-group-status-dialog { min-height: 240px; }
.mobile-plugin-list, .mobile-plugin-list,
.mobile-group-list { .mobile-group-list {

View File

@@ -6,6 +6,7 @@ import threading
import time import time
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple
import toml
from loguru import logger from loguru import logger
from base.plugin_common.plugin_interface import PluginInterface, PluginStatus from base.plugin_common.plugin_interface import PluginInterface, PluginStatus
@@ -56,6 +57,11 @@ class PluginManager:
self.module_to_display = {} # 模块名到显示名的映射 self.module_to_display = {} # 模块名到显示名的映射
self.system_context = {} # 系统上下文 self.system_context = {} # 系统上下文
self.current_bot: Optional[WechatAPIClient] = None self.current_bot: Optional[WechatAPIClient] = None
# 运行态记录用于给“插件治理中心”提供统一视图:
# 1. 不仅记录已成功加载的插件,也记录“加载失败 / 配置禁用 / 手动停用”等状态;
# 2. 后台治理页就不必再从日志里猜某个插件为什么没出现在列表里;
# 3. 这里按 module_name 维度存储,便于和 plugins 目录天然对齐。
self.plugin_runtime_records: Dict[str, Dict[str, Any]] = {}
# 热加载相关 # 热加载相关
self._watcher_thread: Optional[threading.Thread] = None self._watcher_thread: Optional[threading.Thread] = None
@@ -74,6 +80,438 @@ class PluginManager:
if self.plugin_dir not in sys.path: if self.plugin_dir not in sys.path:
sys.path.insert(0, self.plugin_dir) sys.path.insert(0, self.plugin_dir)
def _record_module_runtime_state(
self,
module_name: str,
state: str,
message: str = "",
detail: Optional[Dict[str, Any]] = None,
) -> None:
"""记录插件模块的运行态快照。"""
if not module_name:
return
self.plugin_runtime_records[module_name] = {
"state": str(state or "").strip().lower() or "unknown",
"message": str(message or "").strip(),
"detail": dict(detail or {}),
"updated_at": float(time.time()),
}
def _get_module_runtime_state(self, module_name: str) -> Dict[str, Any]:
"""读取插件模块的最近一次运行态记录。"""
return dict(self.plugin_runtime_records.get(module_name, {}) or {})
@staticmethod
def _is_sensitive_config_key(key: str) -> bool:
"""判断配置键是否属于敏感信息。"""
lowered_key = str(key or "").strip().lower()
return any(
keyword in lowered_key
for keyword in ["password", "secret", "token", "api_key", "apikey", "cookie", "client_secret"]
)
def _build_config_overview_from_mapping(
self,
config_obj: Optional[Dict[str, Any]],
*,
config_path: str,
file_exists: bool,
parse_ok: bool,
parse_error: str = "",
) -> Dict[str, Any]:
"""从配置对象构建统一的配置概览。"""
config_obj = dict(config_obj or {})
top_level_keys = list(config_obj.keys())
dict_section_names = []
enabled_sections = []
disabled_sections = []
sensitive_paths = []
def _walk_sensitive_fields(node, path: str) -> None:
if isinstance(node, dict):
for key, value in node.items():
next_path = f"{path}.{key}"
if isinstance(value, str) and self._is_sensitive_config_key(key) and str(value or "").strip():
sensitive_paths.append(next_path)
_walk_sensitive_fields(value, next_path)
return
if isinstance(node, list):
for index, value in enumerate(node):
_walk_sensitive_fields(value, f"{path}[{index}]")
for section_name, section_value in config_obj.items():
if not isinstance(section_value, dict):
continue
dict_section_names.append(section_name)
if "enable" not in section_value:
continue
if bool(section_value.get("enable", True)):
enabled_sections.append(section_name)
else:
disabled_sections.append(section_name)
_walk_sensitive_fields(config_obj, "config")
return {
"path": config_path,
"exists": bool(file_exists),
"parse_ok": bool(parse_ok),
"parse_error": str(parse_error or ""),
"top_level_keys": top_level_keys,
"top_level_key_count": len(top_level_keys),
"dict_section_names": dict_section_names,
"section_count": len(dict_section_names),
"enabled_sections": enabled_sections,
"enabled_section_count": len(enabled_sections),
"disabled_sections": disabled_sections,
"disabled_section_count": len(disabled_sections),
"sensitive_field_paths": sensitive_paths,
"sensitive_field_count": len(sensitive_paths),
}
def _read_plugin_config_overview(self, config_path: str) -> Dict[str, Any]:
"""读取插件配置文件并返回概览。"""
if not config_path:
return self._build_config_overview_from_mapping(
{},
config_path="",
file_exists=False,
parse_ok=False,
parse_error="配置路径为空",
)
if not os.path.exists(config_path):
return self._build_config_overview_from_mapping(
{},
config_path=config_path,
file_exists=False,
parse_ok=True,
)
try:
with open(config_path, "r", encoding="utf-8") as config_file:
config_obj = toml.load(config_file)
return self._build_config_overview_from_mapping(
config_obj,
config_path=config_path,
file_exists=True,
parse_ok=True,
)
except Exception as e:
return self._build_config_overview_from_mapping(
{},
config_path=config_path,
file_exists=True,
parse_ok=False,
parse_error=str(e),
)
@staticmethod
def _build_diagnostic(level: str, code: str, message: str) -> Dict[str, str]:
"""统一治理诊断项结构。"""
return {
"level": str(level or "").strip().lower() or "info",
"code": str(code or "").strip(),
"message": str(message or "").strip(),
}
def _collect_plugin_types(self, plugin: PluginInterface) -> List[str]:
"""识别插件能力类型。"""
plugin_types = []
if isinstance(plugin, MessagePluginInterface):
plugin_types.append("message")
if isinstance(plugin, ScheduledPluginInterface):
plugin_types.append("scheduled")
if not plugin_types:
plugin_types.append("generic")
return plugin_types
def _collect_plugin_commands(self, plugin: PluginInterface) -> List[str]:
"""统一读取插件声明的命令列表。"""
commands = getattr(plugin, "commands", []) or getattr(plugin, "_commands", []) or []
if isinstance(commands, (list, tuple, set)):
return [str(item).strip() for item in commands if str(item or "").strip()]
if isinstance(commands, str) and commands.strip():
return [commands.strip()]
return []
def _build_governance_diagnostics(
self,
*,
plugin: Optional[PluginInterface],
module_name: str,
config_overview: Dict[str, Any],
runtime_record: Dict[str, Any],
) -> List[Dict[str, str]]:
"""根据插件元信息、配置和运行态生成治理诊断。"""
diagnostics = []
runtime_state = str(runtime_record.get("state", "") or "").strip().lower()
runtime_message = str(runtime_record.get("message", "") or "").strip()
if runtime_state == "load_failed":
diagnostics.append(
self._build_diagnostic(
"error",
"load_failed",
runtime_message or f"插件模块 `{module_name}` 加载失败,请先排查导入、初始化或依赖问题。",
)
)
elif runtime_state == "disabled_by_config":
diagnostics.append(
self._build_diagnostic(
"info",
"disabled_by_config",
runtime_message or "插件已在配置中禁用,当前未进入运行态。",
)
)
if not config_overview.get("exists"):
diagnostics.append(
self._build_diagnostic(
"info",
"config_missing",
"未发现 config.toml当前插件将完全依赖默认参数或代码内置配置。",
)
)
elif not config_overview.get("parse_ok"):
diagnostics.append(
self._build_diagnostic(
"error",
"config_parse_failed",
f"配置文件解析失败:{config_overview.get('parse_error', '未知错误')}",
)
)
sensitive_count = int(config_overview.get("sensitive_field_count", 0) or 0)
if sensitive_count > 0:
diagnostics.append(
self._build_diagnostic(
"warning",
"config_contains_sensitive_fields",
f"配置文件中检测到 {sensitive_count} 个敏感字段,建议逐步迁移到全局配置或环境变量。",
)
)
if plugin is None:
return diagnostics
version = str(getattr(plugin, "version", "") or "").strip()
author = str(getattr(plugin, "author", "") or "").strip()
description = str(getattr(plugin, "description", "") or "").strip()
if not version or version.upper() == "N/A":
diagnostics.append(self._build_diagnostic("warning", "missing_version", "插件未声明版本号,不利于后续升级和兼容治理。"))
if not author or author.upper() == "N/A":
diagnostics.append(self._build_diagnostic("info", "missing_author", "插件未声明作者信息,后续定位维护人会比较困难。"))
if not description or description.upper() == "N/A":
diagnostics.append(self._build_diagnostic("info", "missing_description", "插件未声明描述信息,后台可读性较弱。"))
dependencies = list(getattr(plugin, "dependencies", []) or [])
for dependency_name in dependencies:
if dependency_name not in self.plugins:
diagnostics.append(
self._build_diagnostic(
"warning",
"missing_dependency",
f"声明依赖插件 `{dependency_name}` 当前未加载,存在运行时能力缺失风险。",
)
)
if isinstance(plugin, MessagePluginInterface):
commands = self._collect_plugin_commands(plugin)
if not commands:
diagnostics.append(
self._build_diagnostic(
"info",
"missing_commands",
"消息插件未声明命令列表,后台无法准确展示其触发入口。",
)
)
feature_key = str(getattr(plugin, "feature_key", "") or "").strip()
if not feature_key:
diagnostics.append(
self._build_diagnostic(
"info",
"missing_feature_key",
"消息插件未声明 feature_key将无法纳入统一群级权限治理。",
)
)
return diagnostics
@staticmethod
def _summarize_governance_status(diagnostics: List[Dict[str, str]]) -> Dict[str, Any]:
"""把诊断列表汇总为后台更容易消费的治理状态。"""
level_priority = {"healthy": 0, "info": 1, "warning": 2, "error": 3}
level_counts = {"error": 0, "warning": 0, "info": 0}
governance_status = "healthy"
for item in diagnostics or []:
level = str(item.get("level", "") or "info").strip().lower()
if level in level_counts:
level_counts[level] += 1
if level_priority.get(level, 0) > level_priority.get(governance_status, 0):
governance_status = level
if governance_status == "info" and level_counts["warning"] == 0 and level_counts["error"] == 0:
governance_status = "healthy"
return {
"status": governance_status,
"error_count": level_counts["error"],
"warning_count": level_counts["warning"],
"info_count": level_counts["info"],
}
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)
config_path = plugin.get_config_path()
config_overview = self._read_plugin_config_overview(config_path)
commands = self._collect_plugin_commands(plugin)
feature_key = str(getattr(plugin, "feature_key", "") or "").strip()
feature_description = str(getattr(plugin, "feature_description", "") or "").strip()
governance_diagnostics = self._build_governance_diagnostics(
plugin=plugin,
module_name=module_name,
config_overview=config_overview,
runtime_record=runtime_record,
)
governance_summary = self._summarize_governance_status(governance_diagnostics)
return {
"name": plugin.name,
"module_name": module_name,
"version": getattr(plugin, "version", "N/A"),
"author": getattr(plugin, "author", "N/A"),
"description": getattr(plugin, "description", "N/A"),
"status": plugin.status.name if hasattr(plugin, "status") else "UNKNOWN",
"status_label": self._status_to_label(plugin.status.name if hasattr(plugin, "status") else "UNKNOWN"),
"plugin_types": self._collect_plugin_types(plugin),
"commands": commands,
"command_count": len(commands),
"command_prefix": getattr(plugin, "command_prefix", ""),
"dependencies": list(getattr(plugin, "dependencies", []) or []),
"feature_key": feature_key,
"feature_description": feature_description,
"supports_group_switch": bool(getattr(plugin, "feature", None)),
"config": getattr(plugin, "_config", {}),
"config_path": config_path,
"config_overview": config_overview,
"governance_diagnostics": governance_diagnostics,
"governance_status": governance_summary["status"],
"governance_error_count": governance_summary["error_count"],
"governance_warning_count": governance_summary["warning_count"],
"governance_info_count": governance_summary["info_count"],
"runtime_state": runtime_record.get("state", "loaded"),
"runtime_message": runtime_record.get("message", ""),
}
def _build_unloaded_plugin_snapshot(self, module_name: str) -> Dict[str, Any]:
"""为未成功加载的插件模块生成治理快照。"""
runtime_record = self._get_module_runtime_state(module_name)
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")
config_overview = self._read_plugin_config_overview(config_path)
governance_diagnostics = self._build_governance_diagnostics(
plugin=None,
module_name=module_name,
config_overview=config_overview,
runtime_record=runtime_record,
)
governance_summary = self._summarize_governance_status(governance_diagnostics)
runtime_state = str(runtime_record.get("state", "") or "").strip().lower()
status = "DISCOVERED"
if runtime_state == "load_failed":
status = "ERROR"
elif runtime_state == "disabled_by_config":
status = "STOPPED"
return {
"name": module_name,
"module_name": module_name,
"version": "N/A",
"author": "N/A",
"description": runtime_record.get("message", "") or "插件模块已发现,但当前未进入加载态。",
"status": status,
"status_label": self._status_to_label(status),
"plugin_types": ["unknown"],
"commands": [],
"command_count": 0,
"command_prefix": "",
"dependencies": [],
"feature_key": "",
"feature_description": "",
"supports_group_switch": False,
"config": {},
"config_path": config_path,
"config_overview": config_overview,
"governance_diagnostics": governance_diagnostics,
"governance_status": governance_summary["status"],
"governance_error_count": governance_summary["error_count"],
"governance_warning_count": governance_summary["warning_count"],
"governance_info_count": governance_summary["info_count"],
"runtime_state": runtime_state or "discovered",
"runtime_message": runtime_record.get("message", ""),
}
@staticmethod
def _status_to_label(status: str) -> str:
"""把运行态状态码转换成中文展示文案。"""
status_map = {
"RUNNING": "运行中",
"STOPPED": "已停用",
"LOADED": "已加载",
"UNLOADED": "未加载",
"ERROR": "异常",
"DISCOVERED": "待处理",
"UNKNOWN": "未知",
}
return status_map.get(str(status or "").strip().upper(), "未知")
def get_plugin_snapshots(self) -> List[Dict[str, Any]]:
"""返回插件治理中心使用的统一快照列表。"""
snapshots = []
loaded_module_names = set()
discovered_module_names = set(self.discover_plugins())
for plugin in self.plugins.values():
snapshot = self._build_plugin_snapshot(plugin)
snapshots.append(snapshot)
loaded_module_names.add(snapshot["module_name"])
# 这里把“目录已存在但插件未成功加载”的模块也补进列表:
# 1. 否则后台只能看到成功插件,看不到真正需要排查的失败模块;
# 2. 这类插件往往正是治理中心最该暴露的问题;
# 3. 统一补成快照后,前端无需区分“已加载”与“未加载”两套数据源。
for module_name in sorted(discovered_module_names - loaded_module_names):
snapshots.append(self._build_unloaded_plugin_snapshot(module_name))
snapshots.sort(
key=lambda item: (
0 if item.get("status") == "RUNNING" else 1,
0 if item.get("governance_status") == "error" else 1 if item.get("governance_status") == "warning" else 2,
str(item.get("module_name", "")),
)
)
return snapshots
def get_plugin_snapshot(self, name: str) -> Optional[Dict[str, Any]]:
"""按模块名或展示名获取单个插件治理快照。"""
target_name = str(name or "").strip()
if not target_name:
return None
display_name, plugin = self.find_plugin_by_name(target_name)
if plugin:
return self._build_plugin_snapshot(plugin)
for snapshot in self.get_plugin_snapshots():
if snapshot.get("module_name") == target_name or snapshot.get("name") == target_name:
return snapshot
return None
def set_system_context(self, context: Dict[str, Any]): def set_system_context(self, context: Dict[str, Any]):
""" """
设置系统上下文 设置系统上下文
@@ -354,6 +792,7 @@ class PluginManager:
if module_name not in self.module_to_display: if module_name not in self.module_to_display:
self.module_to_display[module_name] = display_name self.module_to_display[module_name] = display_name
self.LOG.debug(f"PluginManager添加缺失的模块映射 {module_name} -> {display_name}") self.LOG.debug(f"PluginManager添加缺失的模块映射 {module_name} -> {display_name}")
self._record_module_runtime_state(module_name, "loaded", "插件已在内存中复用现有实例。")
self._inject_bot_to_plugin(plugin) self._inject_bot_to_plugin(plugin)
return plugin return plugin
except Exception as e: except Exception as e:
@@ -372,6 +811,7 @@ class PluginManager:
self.plugin_modules[module_name] = module self.plugin_modules[module_name] = module
except ImportError as e: except ImportError as e:
self.LOG.error(f"PluginManager导入插件模块 {module_path} 失败: {e}") self.LOG.error(f"PluginManager导入插件模块 {module_path} 失败: {e}")
self._record_module_runtime_state(module_name, "load_failed", f"导入插件模块失败: {e}")
return None return None
else: else:
# 单文件插件 # 单文件插件
@@ -381,6 +821,7 @@ class PluginManager:
self.plugin_modules[module_name] = module self.plugin_modules[module_name] = module
except ImportError as e: except ImportError as e:
self.LOG.error(f"PluginManager导入单文件插件 {module_name} 失败: {e}") self.LOG.error(f"PluginManager导入单文件插件 {module_name} 失败: {e}")
self._record_module_runtime_state(module_name, "load_failed", f"导入单文件插件失败: {e}")
return None return None
# 查找插件类 # 查找插件类
@@ -406,12 +847,14 @@ class PluginManager:
# 加载插件配置 # 加载插件配置
if not plugin.load_config(): if not plugin.load_config():
self.LOG.error(f"PluginManager插件模块 {module_name} 加载配置失败") self.LOG.error(f"PluginManager插件模块 {module_name} 加载配置失败")
self._record_module_runtime_state(module_name, "load_failed", "插件配置加载失败。")
async_job.remove_jobs_by_owner(plugin) async_job.remove_jobs_by_owner(plugin)
return None return None
# 初始化插件 # 初始化插件
if not plugin.initialize(self.system_context): if not plugin.initialize(self.system_context):
self.LOG.error(f"PluginManager插件模块 {module_name} 初始化失败") self.LOG.error(f"PluginManager插件模块 {module_name} 初始化失败")
self._record_module_runtime_state(module_name, "load_failed", "插件初始化失败。")
async_job.remove_jobs_by_owner(plugin) async_job.remove_jobs_by_owner(plugin)
return None return None
self._inject_bot_to_plugin(plugin) self._inject_bot_to_plugin(plugin)
@@ -428,13 +871,16 @@ class PluginManager:
# 添加模块名到显示名的映射 # 添加模块名到显示名的映射
self.module_to_display[module_name] = display_name self.module_to_display[module_name] = display_name
self._refresh_module_file_state(module_name) self._refresh_module_file_state(module_name)
self._record_module_runtime_state(module_name, "loaded", "插件已成功加载。")
# self.LOG.info(f"PluginManager添加模块映射 {module_name} -> {display_name}") # self.LOG.info(f"PluginManager添加模块映射 {module_name} -> {display_name}")
return plugin return plugin
else: else:
self.LOG.error(f"PluginManager插件模块 {module_name} 的 get_plugin() 返回的不是有效的插件实例") self.LOG.error(f"PluginManager插件模块 {module_name} 的 get_plugin() 返回的不是有效的插件实例")
self._record_module_runtime_state(module_name, "load_failed", "get_plugin() 未返回有效的插件实例。")
else: else:
self.LOG.error(f"PluginManager插件模块 {module_name} 中未找到有效的插件类或 get_plugin 函数") self.LOG.error(f"PluginManager插件模块 {module_name} 中未找到有效的插件类或 get_plugin 函数")
self._record_module_runtime_state(module_name, "load_failed", "未找到有效的插件类或 get_plugin 函数。")
return None return None
# 实例化插件 # 实例化插件
@@ -448,6 +894,7 @@ class PluginManager:
# 加载插件配置 # 加载插件配置
if not plugin.load_config(): if not plugin.load_config():
self.LOG.error(f"PluginManager插件模块 {module_name} 加载配置失败") self.LOG.error(f"PluginManager插件模块 {module_name} 加载配置失败")
self._record_module_runtime_state(module_name, "load_failed", "插件配置加载失败。")
async_job.remove_jobs_by_owner(plugin) async_job.remove_jobs_by_owner(plugin)
return None return None
@@ -455,12 +902,14 @@ class PluginManager:
for section in plugin._config.values(): for section in plugin._config.values():
if isinstance(section, dict) and not section.get("enable", True): if isinstance(section, dict) and not section.get("enable", True):
self.LOG.debug(f"PluginManager插件 {module_name} 已禁用,跳过加载") self.LOG.debug(f"PluginManager插件 {module_name} 已禁用,跳过加载")
self._record_module_runtime_state(module_name, "disabled_by_config", "插件在配置中已禁用,启动时已跳过加载。")
async_job.remove_jobs_by_owner(plugin) async_job.remove_jobs_by_owner(plugin)
return None return None
# 初始化插件 # 初始化插件
if not plugin.initialize(self.system_context): if not plugin.initialize(self.system_context):
self.LOG.error(f"PluginManager插件模块 {module_name} 初始化失败") self.LOG.error(f"PluginManager插件模块 {module_name} 初始化失败")
self._record_module_runtime_state(module_name, "load_failed", "插件初始化失败。")
async_job.remove_jobs_by_owner(plugin) async_job.remove_jobs_by_owner(plugin)
return None return None
self._inject_bot_to_plugin(plugin) self._inject_bot_to_plugin(plugin)
@@ -477,6 +926,7 @@ class PluginManager:
# 添加模块名到显示名的映射 # 添加模块名到显示名的映射
self.module_to_display[module_name] = display_name self.module_to_display[module_name] = display_name
self._refresh_module_file_state(module_name) self._refresh_module_file_state(module_name)
self._record_module_runtime_state(module_name, "loaded", "插件已成功加载。")
# self.LOG.info(f"PluginManager添加模块映射 {module_name} -> {display_name}") # self.LOG.info(f"PluginManager添加模块映射 {module_name} -> {display_name}")
return plugin return plugin
@@ -485,6 +935,7 @@ class PluginManager:
plugin_obj = locals().get("plugin") plugin_obj = locals().get("plugin")
if plugin_obj is not None: if plugin_obj is not None:
async_job.remove_jobs_by_owner(plugin_obj) async_job.remove_jobs_by_owner(plugin_obj)
self._record_module_runtime_state(module_name, "load_failed", f"插件加载异常: {e}")
self.LOG.exception(f"PluginManager加载插件模块 {module_name} 失败: {e}", exc_info=True) self.LOG.exception(f"PluginManager加载插件模块 {module_name} 失败: {e}", exc_info=True)
return None return None
@@ -609,10 +1060,14 @@ class PluginManager:
if plugin.start(): if plugin.start():
plugin.status = PluginStatus.RUNNING plugin.status = PluginStatus.RUNNING
module_name = self._get_module_name_from_plugin(plugin) or name
self._record_module_runtime_state(module_name, "running", "插件已启动并进入运行态。")
self.LOG.debug(f"PluginManager插件 {display_name} 状态变更为在运行") self.LOG.debug(f"PluginManager插件 {display_name} 状态变更为在运行")
return True return True
else: else:
plugin.status = PluginStatus.ERROR plugin.status = PluginStatus.ERROR
module_name = self._get_module_name_from_plugin(plugin) or name
self._record_module_runtime_state(module_name, "load_failed", "插件启动失败,状态已标记为异常。")
self.LOG.debug(f"PluginManager插件 {display_name} 状态变更为异常") self.LOG.debug(f"PluginManager插件 {display_name} 状态变更为异常")
return False return False
@@ -639,10 +1094,14 @@ class PluginManager:
if plugin.stop(): if plugin.stop():
plugin.status = PluginStatus.STOPPED plugin.status = PluginStatus.STOPPED
module_name = self._get_module_name_from_plugin(plugin) or name
self._record_module_runtime_state(module_name, "stopped", "插件已手动停用。")
self.LOG.debug(f"插件 {display_name} 状态变更为已停止") self.LOG.debug(f"插件 {display_name} 状态变更为已停止")
return True return True
else: else:
plugin.status = PluginStatus.ERROR plugin.status = PluginStatus.ERROR
module_name = self._get_module_name_from_plugin(plugin) or name
self._record_module_runtime_state(module_name, "load_failed", "插件停用失败,状态已标记为异常。")
self.LOG.debug(f"插件 {display_name} 状态变更为异常") self.LOG.debug(f"插件 {display_name} 状态变更为异常")
return False return False

View File

@@ -400,6 +400,13 @@
- 把插件系统从“可加载”升级为“可治理” - 把插件系统从“可加载”升级为“可治理”
当前进展:
- 第一阶段已完成:`PluginManager` 已输出统一插件治理快照,后台不再只展示“加载成功的插件”
- 第一阶段已完成后台插件管理页已补充治理健康、能力类型、Feature Key、依赖与配置概览信息
- 第一阶段已完成:插件配置保存前已增加格式校验,避免坏配置直接写回线上文件
- 后续可继续补充插件错误历史、性能排名、依赖图与熔断/隔离控制
建议内容: 建议内容:
- 插件元信息页面 - 插件元信息页面