from flask import Blueprint, render_template, jsonify, request, current_app, redirect from .auth import login_required from loguru import logger from utils.robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus from datetime import datetime, timedelta # 创建群管理兼容蓝图(旧入口保留做跳转,实际功能已并入通讯录) robot_bp = Blueprint('robot', __name__, url_prefix='/robot') LOG = logger # 旧群权限页入口兼容跳转 @robot_bp.route('/') @login_required def robot_management(): return redirect('/contacts') # API路由 @robot_bp.route('/api/groups') @login_required def api_robot_groups(): try: server = current_app.dashboard_server # 获取所有群组列表 groups = GroupBotManager.get_group_list() # 如果方法返回None或发生异常,使用本地缓存 if groups is None and hasattr(GroupBotManager, "local_cache"): groups = GroupBotManager.local_cache.get("group_list", set()) # 如果仍然为None,则初始化为空集合 if groups is None: groups = set() LOG.info(f"获取到 {len(groups)} 个群组") group_data = [] for group_id in groups: try: # 获取群名称,如果失败则使用默认值 group_name = server.contact_manager.get_nickname(group_id) if not group_name: group_name = f"未知群组({group_id})" # 获取机器人状态,如果失败则使用默认值 try: robot_status = GroupBotManager.get_group_permission(group_id, Feature.ROBOT) except: robot_status = PermissionStatus.DISABLED group_data.append({ "group_id": group_id, "group_name": group_name, "robot_status": robot_status.value if robot_status else "unknown" }) except Exception as e: LOG.warning(f"处理群组 {group_id} 信息时出错: {e}") # 添加基本信息,避免单个群组错误影响整个列表 group_data.append({ "group_id": group_id, "group_name": "获取失败", "robot_status": "unknown" }) return jsonify({"success": True, "data": group_data}) except Exception as e: LOG.error(f"获取群组列表失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @robot_bp.route('/api/group//permissions') @login_required def api_robot_group_permissions(group_id): try: permissions = GroupBotManager.list_group_permissions(group_id) permission_data = [] for feature, status in permissions.items(): permission_data.append({ "feature_id": feature.value, "feature_name": feature.name, "feature_description": feature.description, "status": status.value }) return jsonify({"success": True, "data": permission_data}) except Exception as e: LOG.error(f"获取群组权限失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @robot_bp.route('/api/group//permissions', methods=['POST']) @login_required def api_update_robot_permissions(group_id): # 更新群组功能权限 server = current_app.dashboard_server data = request.json feature_id = data.get('feature_id') status = data.get('status') try: feature = Feature(int(feature_id)) new_status = PermissionStatus(status) # 特殊处理ROBOT功能 if feature == Feature.ROBOT: r = server.db_manager.get_redis_connection() if new_status == PermissionStatus.ENABLED: GroupBotManager.local_cache["group_list"].add(group_id) r.sadd("group:list", group_id) else: GroupBotManager.local_cache["group_list"].remove(group_id) r.srem("group:list", group_id) GroupBotManager.set_group_permission(group_id, feature, new_status) return jsonify({"success": True}) except Exception as e: LOG.error(f"更新群组权限失败: {e}") return jsonify({"success": False, "error": str(e)}), 400 @robot_bp.route('/api/batch_operation', methods=['POST']) @login_required def api_robot_batch_operation(): # 批量操作接口 data = request.json operation = data.get('operation') group_ids = data.get('group_ids', []) results = {} try: if operation == 'remove_groups': for group_id in group_ids: result = GroupBotManager.remove_group(group_id) results[group_id] = result return jsonify({"success": True, "results": results}) else: return jsonify({"success": False, "error": "不支持的操作类型"}), 400 except Exception as e: LOG.error(f"批量操作失败: {e}") return jsonify({"success": False, "error": str(e)}), 400 @robot_bp.route('/api/add_group', methods=['POST']) @login_required def api_add_group(): try: server = current_app.dashboard_server data = request.json group_id = data.get('group_id') if not group_id or not group_id.strip(): return jsonify({"success": False, "error": "群组ID不能为空"}), 400 group_id = group_id.strip() # 如果group_id 不是@chatroom 结尾,这提示错误 if not group_id.endswith("@chatroom"): return jsonify({"success": False, "error": "群组ID必须以 @chatroom 结尾"}), 400 # 检查群组是否已存在 if group_id in GroupBotManager.local_cache["group_list"]: return jsonify({"success": False, "error": "该群组已存在"}), 400 # 添加群组到列表并启用机器人功能 GroupBotManager.local_cache["group_list"].add(group_id) r = server.db_manager.get_redis_connection() r.sadd("group:list", group_id) # 设置ROBOT功能为启用状态 GroupBotManager.set_group_permission(group_id, Feature.ROBOT, PermissionStatus.ENABLED) # 获取群组名称(如果可能) group_name = server.contact_manager.get_nickname(group_id) return jsonify({ "success": True, "message": f"群组 {group_id} 已成功添加", "group": { "group_id": group_id, "group_name": group_name, "robot_status": "enabled" } }) except Exception as e: LOG.error(f"添加群组失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @robot_bp.route('/api/group//message_trend') @login_required def api_group_message_trend(group_id): try: server = current_app.dashboard_server days = request.args.get('days', 7, type=int) trend_data = server.message_storage.get_message_trend(group_id, days) # 格式化数据为前端需要的格式 dates = [] counts = [] for item in trend_data: # 将日期转换为字符串 if isinstance(item['date'], datetime): date_str = item['date'].strftime('%Y-%m-%d') else: date_str = str(item['date']) dates.append(date_str) counts.append(item['message_count']) return jsonify({ 'success': True, 'data': { 'dates': dates, 'counts': counts } }) except Exception as e: LOG.error(f"获取群组消息趋势数据出错: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @robot_bp.route('/api/group//detail') @login_required def api_group_detail(group_id): """群组详情聚合接口""" try: server = current_app.dashboard_server now = datetime.now() last_24h = now - timedelta(days=1) last_7d = now - timedelta(days=7) last_30d = now - timedelta(days=30) group_name = server.contact_manager.get_nickname(group_id) or group_id permission_map = GroupBotManager.list_group_permissions(group_id) enabled_features = [ feature.description for feature, status in permission_map.items() if status == PermissionStatus.ENABLED ] member_summary = server.contact_db.get_group_member_summary(group_id, inactive_days=30) inactive_members = server.contact_db.get_inactive_members_rank(group_id, days=30, limit=8) message_stats_24h = server.message_storage.get_message_stats_by_date_range(group_id, last_24h, now) message_stats_7d = server.message_storage.get_message_stats_by_date_range(group_id, last_7d, now) message_stats_30d = server.message_storage.get_message_stats_by_date_range(group_id, last_30d, now) speaker_ranking = server.message_storage.get_group_member_message_ranking(group_id, last_30d, now, limit=10) plugin_summary = server.stats_db.get_group_plugin_summary(group_id, days=30) plugin_stats = server.stats_db.get_group_plugin_stats(group_id, days=30, limit=8) last_message = server.message_storage.get_group_last_message(group_id) trend_data = server.message_storage.get_message_trend(group_id, 14) hourly_distribution = server.message_storage.get_group_hourly_distribution(group_id, 30) for item in inactive_members: wxid = item.get("wxid") item["display_name"] = server.contact_manager.get_group_name(group_id, wxid) or item.get("nick_name") or wxid for item in speaker_ranking: sender = item.get("sender") item["display_name"] = server.contact_manager.get_group_name(group_id, sender) or sender total_members = member_summary.get("in_group_members") or member_summary.get("total_members") or 0 active_30d = member_summary.get("active_30d_members", 0) zombie_count = member_summary.get("inactive_members", 0) msg_30d_total = message_stats_30d.get("total_count", 0) plugin_total_calls = plugin_summary.get("total_calls", 0) health_score = 100 if total_members > 0: health_score -= min(35, round((zombie_count / total_members) * 35)) health_score -= min(25, round((1 - min(active_30d / total_members, 1)) * 25)) if msg_30d_total == 0: health_score -= 25 elif msg_30d_total < 50: health_score -= 12 if plugin_total_calls == 0: health_score -= 10 health_score = max(0, min(100, health_score)) trend_dates = [] trend_counts = [] for item in trend_data: date_val = item.get("date") if isinstance(date_val, datetime): date_str = date_val.strftime('%Y-%m-%d') else: date_str = str(date_val) trend_dates.append(date_str) trend_counts.append(int(item.get("message_count") or 0)) hourly_distribution_sorted = sorted( hourly_distribution, key=lambda x: x.get("message_count", 0), reverse=True ) peak_hours = [ { "hour": item.get("hour", 0), "label": f"{int(item.get('hour', 0)):02d}:00-{(int(item.get('hour', 0)) + 1) % 24:02d}:00", "message_count": item.get("message_count", 0), } for item in hourly_distribution_sorted[:3] if item.get("message_count", 0) > 0 ] plugin_success_rate = ( round(plugin_summary.get("success_calls", 0) / plugin_total_calls * 100, 1) if plugin_total_calls else 0 ) zombie_ratio = round((zombie_count / total_members) * 100, 1) if total_members else 0 active_ratio = round((active_30d / total_members) * 100, 1) if total_members else 0 operation_suggestions = [] if zombie_ratio >= 50: operation_suggestions.append({ "level": "danger", "title": "僵尸成员偏多,建议分层清理或召回", "desc": f"当前僵尸成员占比 {zombie_ratio}% ,建议优先筛查 30 天未发言成员,结合群定位决定清理、激活或转移。" }) elif zombie_ratio >= 30: operation_suggestions.append({ "level": "warning", "title": "沉默成员较多,建议做激活动作", "desc": f"当前僵尸成员占比 {zombie_ratio}% ,可以通过签到、抽奖、任务或话题互动做一次激活。" }) if active_ratio < 20: operation_suggestions.append({ "level": "danger", "title": "活跃覆盖率偏低,群内容可能只被少数人消费", "desc": f"近30天活跃覆盖率仅 {active_ratio}% ,建议增加低门槛互动,如投票、签到、接龙、关键词触发。" }) elif active_ratio < 40: operation_suggestions.append({ "level": "warning", "title": "活跃度中等,可继续扩大参与面", "desc": f"近30天活跃覆盖率为 {active_ratio}% ,建议在高峰时段安排固定互动,提高更多成员参与率。" }) if plugin_total_calls == 0: operation_suggestions.append({ "level": "info", "title": "插件渗透率为 0,建议补一次功能引导", "desc": "这个群近30天没有插件调用,建议置顶菜单、欢迎语或群公告里给出可用指令示例。" }) elif plugin_summary.get("plugin_count", 0) <= 2: operation_suggestions.append({ "level": "info", "title": "插件使用面偏窄,可以扩展场景", "desc": f"近30天仅使用了 {plugin_summary.get('plugin_count', 0)} 个插件,建议根据群定位补充娱乐、工具或运营型插件。" }) if plugin_total_calls > 0 and plugin_success_rate < 85: operation_suggestions.append({ "level": "warning", "title": "插件成功率偏低,建议检查指令体验", "desc": f"近30天插件成功率 {plugin_success_rate}% ,可排查失败插件、异常命令和提示文案是否清晰。" }) if msg_30d_total < 30: operation_suggestions.append({ "level": "info", "title": "群消息基数偏低,建议降低互动门槛", "desc": f"近30天消息量仅 {msg_30d_total} 条,更适合短反馈、轻任务、固定时间提醒等低成本互动。" }) if not operation_suggestions: operation_suggestions.append({ "level": "success", "title": "群整体状态健康,可持续做精细化运营", "desc": "当前活跃、消息量、插件调用都比较均衡,建议继续围绕高峰时段和高频插件做强化。" }) return jsonify({ "success": True, "data": { "group_id": group_id, "group_name": group_name, "health_score": health_score, "overview": { "member_count": total_members, "active_member_count_7d": member_summary.get("active_7d_members", 0), "active_member_count_30d": active_30d, "zombie_member_count": zombie_count, "never_spoken_count": member_summary.get("never_spoken_members", 0), "message_count_24h": message_stats_24h.get("total_count", 0), "message_count_7d": message_stats_7d.get("total_count", 0), "message_count_30d": msg_30d_total, "plugin_call_count_30d": plugin_total_calls, "plugin_count_30d": plugin_summary.get("plugin_count", 0), }, "diagnosis": [ { "title": "活跃覆盖率", "value": f"{round((active_30d / total_members) * 100, 1) if total_members else 0}%", "desc": "近30天发言成员占全群人数比例" }, { "title": "僵尸用户占比", "value": f"{round((zombie_count / total_members) * 100, 1) if total_members else 0}%", "desc": "30天未发言或从未发言成员占比" }, { "title": "人均消息量", "value": round(msg_30d_total / active_30d, 1) if active_30d else 0, "desc": "近30天活跃成员的人均发言量" }, { "title": "插件渗透度", "value": f"{plugin_summary.get('plugin_count', 0)} 个插件", "desc": "近30天在本群真实发生调用的插件数" } ], "permissions": { "enabled_count": len(enabled_features), "enabled_features": enabled_features, }, "member_summary": member_summary, "message_summary": { "last_message": last_message, "last_24h": message_stats_24h, "last_7d": message_stats_7d, "last_30d": message_stats_30d, "type_mix_30d": [ {"label": "文本", "count": message_stats_30d.get("text_count", 0)}, {"label": "图片", "count": message_stats_30d.get("image_count", 0)}, {"label": "视频", "count": message_stats_30d.get("video_count", 0)}, {"label": "链接", "count": message_stats_30d.get("link_count", 0)}, {"label": "表情", "count": message_stats_30d.get("emoji_count", 0)}, ], "trend_14d": { "dates": trend_dates, "counts": trend_counts }, "hourly_distribution_30d": hourly_distribution, "peak_hours_30d": peak_hours, }, "inactive_members": inactive_members, "speaker_ranking": speaker_ranking, "plugin_summary": plugin_summary, "plugin_stats": plugin_stats, "operation_suggestions": operation_suggestions, } }) except Exception as e: LOG.error(f"获取群组详情失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 # 添加缺失的群组状态更新接口 @robot_bp.route('/api/group//status', methods=['POST']) @login_required def api_update_group_status(group_id): try: server = current_app.dashboard_server data = request.json status = data.get('status') if status == 'disabled': # 禁用该群组的所有功能 LOG.info(f"正在禁用群组 {group_id} 的所有功能") # 获取所有功能并禁用 for feature in Feature: GroupBotManager.set_group_permission(group_id, feature, PermissionStatus.DISABLED) # 特殊处理ROBOT功能,从群组列表中移除 if group_id in GroupBotManager.local_cache["group_list"]: GroupBotManager.local_cache["group_list"].remove(group_id) # 从Redis中移除 r = server.db_manager.get_redis_connection() r.srem("group:list", group_id) return jsonify({ "success": True, "message": f"群组 {group_id} 的所有功能已禁用" }) elif status == 'enabled': # 启用该群组的基本功能 LOG.info(f"正在启用群组 {group_id} 的基本功能") # 添加到群组列表 if group_id not in GroupBotManager.local_cache["group_list"]: GroupBotManager.local_cache["group_list"].add(group_id) # 添加到Redis r = server.db_manager.get_redis_connection() r.sadd("group:list", group_id) # 启用ROBOT基本功能 GroupBotManager.set_group_permission(group_id, Feature.ROBOT, PermissionStatus.ENABLED) return jsonify({ "success": True, "message": f"群组 {group_id} 的基本功能已启用" }) else: return jsonify({ "success": False, "error": "不支持的状态值,只接受 'enabled' 或 'disabled'" }), 400 except Exception as e: LOG.error(f"更新群组状态失败: {e}") return jsonify({"success": False, "error": str(e)}), 500