509 lines
21 KiB
Python
509 lines
21 KiB
Python
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/<group_id>/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/<group_id>/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/<group_id>/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/<group_id>/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/<group_id>/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
|