Files
abot/admin/dashboard/blueprints/robot.py
2026-04-13 11:21:00 +08:00

509 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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