feat: add group detail dashboard insights

This commit is contained in:
liuwei
2026-04-13 11:04:20 +08:00
parent e2b19c0614
commit ec6c1308db
5 changed files with 962 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ from flask import Blueprint, render_template, jsonify, request, current_app
from .auth import login_required
from loguru import logger
from utils.robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus
from datetime import datetime
from datetime import datetime, timedelta
# 创建机器人管理蓝图
robot_bp = Blueprint('robot', __name__, url_prefix='/robot')
@@ -222,6 +222,233 @@ def api_group_message_trend(group_id):
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