diff --git a/admin/dashboard/blueprints/plugin_routes.py b/admin/dashboard/blueprints/plugin_routes.py index d71f140..211b47b 100644 --- a/admin/dashboard/blueprints/plugin_routes.py +++ b/admin/dashboard/blueprints/plugin_routes.py @@ -6,6 +6,7 @@ import toml from flask import Blueprint, request, jsonify, render_template, current_app from admin.dashboard.blueprints.auth import login_required +from utils.robot_cmd.robot_command import GroupBotManager, PermissionStatus # 创建蓝图 plugin_routes = Blueprint('plugin_routes', __name__) @@ -53,6 +54,86 @@ def get_plugins(): return jsonify({"success": False, "message": f"获取插件列表失败: {str(e)}"}) +@plugin_routes.route('/api/plugins/group_status', methods=['GET']) +@login_required +def get_plugin_group_status(): + """获取单个插件在各群的启用状态(已启用/未启用)""" + try: + server = current_app.dashboard_server + plugin_name = request.args.get('plugin_name') + if not plugin_name: + return jsonify({"success": False, "message": "缺少插件名称参数"}) + + # 通过现有插件管理器查找插件实例,兼容传入模块名或展示名两种形式。 + display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) + if not plugin: + return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) + + # 统一构建群列表:优先使用通讯录中的群(覆盖面更全),并补齐机器人管理列表中的群。 + group_contacts = {} + try: + group_contacts = (server.contact_manager.get_group_contacts() or {}) + except Exception: + # 这里降级为空字典,后续仍会使用 GroupBotManager 群列表,不中断主流程。 + group_contacts = {} + + # 从群机器人管理缓存中取群,确保“已托管但通讯录暂缺”的群也会展示出来。 + managed_groups = set(GroupBotManager.get_group_list() or []) + contact_groups = set(group_contacts.keys()) + all_group_ids = sorted(contact_groups | managed_groups) + + # 再做一次兜底:极端情况下如果前两者都为空,尝试读取本地缓存中的群集合。 + if not all_group_ids and hasattr(GroupBotManager, "local_cache"): + all_group_ids = sorted(GroupBotManager.local_cache.get("group_list", set()) or []) + + # 插件是否支持“按群开关”依赖于插件是否注册了 feature(多数消息插件会注册)。 + plugin_feature = getattr(plugin, "feature", None) + supports_group_switch = plugin_feature is not None + feature_key = getattr(plugin_feature, "name", "") if plugin_feature else "" + feature_description = getattr(plugin_feature, "description", "") if plugin_feature else "" + + enabled_groups = [] + disabled_groups = [] + + for group_id in all_group_ids: + # 群名优先从通讯录获取,拿不到时降级为群ID,保证页面总能显示。 + group_name = group_contacts.get(group_id) or server.contact_manager.get_nickname(group_id) or group_id + group_item = { + "group_id": group_id, + "group_name": group_name, + } + + # 支持群开关的插件:按 feature 的真实权限划分已开启/未开启。 + if supports_group_switch: + permission_status = GroupBotManager.get_group_permission(group_id, plugin_feature) + if permission_status == PermissionStatus.ENABLED: + enabled_groups.append(group_item) + else: + disabled_groups.append(group_item) + else: + # 不支持群级开关的插件默认归入“未开启”列表,并通过 supports_group_switch 让前端给提示。 + disabled_groups.append(group_item) + + return jsonify({ + "success": True, + "data": { + "plugin_name": plugin.name, + "module_name": plugin_name, + "supports_group_switch": supports_group_switch, + "feature_key": feature_key, + "feature_description": feature_description, + "enabled_count": len(enabled_groups), + "disabled_count": len(disabled_groups), + "total_group_count": len(all_group_ids), + "enabled_groups": enabled_groups, + "disabled_groups": disabled_groups, + } + }) + except Exception as e: + LOG.error(f"获取插件群状态失败: {str(e)}", exc_info=True) + return jsonify({"success": False, "message": f"获取插件群状态失败: {str(e)}"}) + + @plugin_routes.route('/api/plugins/info', methods=['GET']) @login_required def get_plugin_info(): diff --git a/admin/dashboard/templates/plugins_manage.html b/admin/dashboard/templates/plugins_manage.html index 3613d5a..c6def9e 100644 --- a/admin/dashboard/templates/plugins_manage.html +++ b/admin/dashboard/templates/plugins_manage.html @@ -93,6 +93,9 @@ 详情 + + 群状态 + @@ -148,6 +151,75 @@ + + +
+
+
+ {% raw %}{{ pluginGroupStatusData.plugin_name || '未知插件' }}{% endraw %} + + {% raw %}{{ `共 ${pluginGroupStatusData.total_group_count || 0} 个群` }}{% endraw %} + +
+
+ 已开启 {% raw %}{{ pluginGroupStatusData.enabled_count || 0 }}{% endraw %} + 未开启 {% raw %}{{ pluginGroupStatusData.disabled_count || 0 }}{% endraw %} +
+
+ + + + + + + +
+ 已开启群 + {% raw %}{{ pluginGroupStatusData.enabled_count || 0 }}{% endraw %} +
+ + + + + + +
+
+ + +
+ 未开启群 + {% raw %}{{ pluginGroupStatusData.disabled_count || 0 }}{% endraw %} +
+ + + + + + +
+
+
+
+
{% endblock %} @@ -165,7 +237,10 @@ isEditingConfig: false, editedConfig: '', configError: '', - configFormat: 'toml' + configFormat: 'toml', + pluginGroupStatusVisible: false, + groupStatusLoading: false, + pluginGroupStatusData: null } }, computed: { @@ -344,6 +419,43 @@ console.error('获取插件详情出错:', error); this.$message.error('获取插件详情出错'); }); + }, + showPluginGroupStatus(plugin) { + // 打开弹窗前先进入加载态,避免用户在慢接口场景下看到旧数据。 + this.pluginGroupStatusVisible = true; + this.groupStatusLoading = true; + this.pluginGroupStatusData = null; + + // 统一使用插件模块名查询,和启用/禁用/重载接口参数保持一致。 + axios.get('/api/plugins/group_status', { + params: { + plugin_name: plugin.module_name + } + }) + .then(response => { + if (response.data.success) { + // 接口返回已按“已开启/未开启”拆分好,前端仅做展示。 + this.pluginGroupStatusData = response.data.data || { + enabled_groups: [], + disabled_groups: [], + enabled_count: 0, + disabled_count: 0, + total_group_count: 0, + supports_group_switch: false + }; + } else { + this.$message.error(response.data.message || '获取插件群状态失败'); + this.pluginGroupStatusVisible = false; + } + }) + .catch(error => { + console.error('获取插件群状态出错:', error); + this.$message.error('获取插件群状态出错'); + this.pluginGroupStatusVisible = false; + }) + .finally(() => { + this.groupStatusLoading = false; + }); } } }); @@ -421,5 +533,26 @@ .config-actions { margin-bottom: 10px; display: flex; gap: 10px; } .config-editor { font-family: monospace; font-size: 12px; } .config-error { color: #ef4444; font-size: 12px; margin-top: 5px; } + .plugin-group-status-dialog { min-height: 240px; } + .group-status-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; + gap: 10px; + flex-wrap: wrap; + } + .group-status-title { font-size: 16px; font-weight: 600; color: #0f172a; } + .group-status-subtitle { margin-left: 8px; font-size: 12px; color: #64748b; font-weight: 500; } + .group-status-summary { display: flex; align-items: center; gap: 8px; } + .group-status-alert { margin-bottom: 12px; } + .group-status-card { border-radius: 12px; } + .group-status-card-header { + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + color: #334155; + } {% endblock %}