diff --git a/admin/dashboard/README.md b/admin/dashboard/README.md new file mode 100644 index 0000000..a2ca728 --- /dev/null +++ b/admin/dashboard/README.md @@ -0,0 +1,30 @@ +# WeChatRobot 管理后台 + +## 简介 + +WeChatRobot 管理后台是一个独立的Web应用,用于管理和监控WeChatRobot的运行状态、使用情况和配置。 + +## 功能 + +- 群组管理:查看和管理群组,设置群组功能权限 +- 插件统计:查看插件使用情况统计 +- 用户统计:查看用户活跃度统计 +- 错误日志:查看系统错误日志 +- 系统配置:管理系统配置参数 + +## 安装与配置 + +1. 确保已安装WeChatRobot主程序 +2. 复制`config.yaml.template`为`config.yaml` +3. 根据需要修改配置参数 + +## 启动方式 + +### Windows + +双击`dashboard_start.bat`文件启动管理后台。 + +### 命令行 + +```bash +python dashboard_start.py [--host HOST] [--port PORT] [--username USERNAME] [--password PASSWORD] \ No newline at end of file diff --git a/admin/dashboard/config.toml b/admin/dashboard/config.toml new file mode 100644 index 0000000..60f4901 --- /dev/null +++ b/admin/dashboard/config.toml @@ -0,0 +1,6 @@ +enable = true +host = "0.0.0.0" +port = 8888 +username = "admin" +password = "admin123" +auto_start = "True" \ No newline at end of file diff --git a/admin/dashboard/server.py b/admin/dashboard/server.py new file mode 100644 index 0000000..25f6afa --- /dev/null +++ b/admin/dashboard/server.py @@ -0,0 +1,392 @@ +import logging +import os +import sys +import threading +import time +from datetime import datetime + +# 添加项目根目录到系统路径,确保可以导入项目模块 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from flask import Flask, render_template, request, jsonify, redirect, url_for, session, send_from_directory +from werkzeug.security import check_password_hash, generate_password_hash + +from db.connection import DBConnectionManager +from db.message_storage import MessageStorageDB +from db.stats_db import StatsDBOperator +from utils.wechat.contact_manager import ContactManager +from robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus + +class DashboardServer: + """统计看板服务器""" + + def __init__(self, host: str = "0.0.0.0", port: int = 8888, + username: str = "admin", password: str = "admin123"): + self.host = host + self.port = port + self.username = username + self.password = password + self.logger = logging.getLogger("DashboardServer") + + # 使用单例模式获取数据库连接 + self.db_manager = DBConnectionManager.get_instance() + self.stats_db = StatsDBOperator(self.db_manager) + self.message_storage = MessageStorageDB(self.db_manager) + # 获取联系人管理器实例 + self.contact_manager = ContactManager.get_instance() + self.app = self._create_app() + self._stop_event = threading.Event() + self._server = None # 存储服务器实例 + + def _create_app(self) -> Flask: + """创建Flask应用""" + app = Flask(__name__) + app.secret_key = "stats_dashboard_secret_key" + + # 实现基本的身份验证 + def check_auth(): + auth = request.authorization + if not auth or auth.username != self.username or auth.password != self.password: + return False + return True + + # 静态文件目录 + static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') + + @app.route('/static/') + def serve_static(filename): + return send_from_directory(static_folder, filename) + + # 添加一个路由处理favicon请求 + @app.route('/favicon.ico') + def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + # 登录页面 + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + if username == self.username and password == self.password: + session['logged_in'] = True + return redirect(url_for('index')) + else: + error = '用户名或密码错误' + + return render_template('login.html', error=error) + + # 登出 + @app.route('/logout') + def logout(): + session.pop('logged_in', None) + return redirect(url_for('login')) + + # 登录检查装饰器 + def login_required(f): + def decorated_function(*args, **kwargs): + if not session.get('logged_in'): + return redirect(url_for('login')) + return f(*args, **kwargs) + decorated_function.__name__ = f.__name__ + return decorated_function + + @app.route('/') + @login_required + def index(): + return render_template('index.html') + + @app.route('/plugins') + @login_required + def plugins(): + return render_template('plugins.html') + + @app.route('/users') + @login_required + def users_page(): + return render_template('users.html') + + @app.route('/groups') + @login_required + def groups(): + return render_template('groups.html') + + @app.route('/errors') + @login_required + def errors(): + return render_template('errors.html') + + # 在_create_app方法中添加新的路由 + @app.route('/robot_management') + @login_required + def robot_management(): + return render_template('robot_management.html') + + @app.route('/api/robot/groups') + @login_required + def api_robot_groups(): + # 获取所有群组列表 + groups = GroupBotManager.get_group_list() + group_data = [] + + for group_id in groups: + group_name = self.contact_manager.get_nickname(group_id) + robot_status = GroupBotManager.get_group_permission(group_id, Feature.ROBOT) + group_data.append({ + "group_id": group_id, + "group_name": group_name, + "robot_status": robot_status.value + }) + + return jsonify({"success": True, "data": group_data}) + + @app.route('/api/robot/group//permissions') + @login_required + def api_robot_group_permissions(group_id): + 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}) + + @app.route('/api/robot/group//permissions', methods=['POST']) + @login_required + def api_update_robot_permissions(group_id): + # 更新群组功能权限 + 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 = self.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: + self.logger.error(f"更新群组权限失败: {e}") + return jsonify({"success": False, "error": str(e)}), 400 + + @app.route('/api/robot/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: + self.logger.error(f"批量操作失败: {e}") + return jsonify({"success": False, "error": str(e)}), 400 + + @app.route('/api/user_stats') + @login_required + def api_user_stats(): + days = request.args.get('days', 7, type=int) + limit = request.args.get('limit', 10, type=int) + stats = self.stats_db.get_user_stats(days, limit) + + # 将用户ID转换为名称 + for item in stats: + if 'user_id' in item: + user_id = item['user_id'] + item['user_name'] = self.contact_manager.get_nickname(user_id) + + return jsonify({"success": True, "data": stats}) + + @app.route('/api/group_stats') + @login_required + def api_group_stats(): + days = request.args.get('days', 7, type=int) + limit = request.args.get('limit', 10, type=int) + stats = self.stats_db.get_group_stats(days, limit) + + # 将群ID转换为名称 + for item in stats: + if 'group_id' in item: + group_id = item['group_id'] + item['group_name'] = self.contact_manager.get_nickname(group_id) + + return jsonify({"success": True, "data": stats}) + + @app.route('/api/plugin_stats') + @login_required + def api_plugin_stats(): + days = request.args.get('days', 7, type=int) + stats = self.stats_db.get_plugin_stats(days) + return jsonify({"success": True, "data": stats}) + + @app.route('/api/error_logs') + @login_required + def api_error_logs(): + days = request.args.get('days', 7, type=int) + limit = request.args.get('limit', 100, type=int) + logs = self.stats_db.get_error_logs(days, limit) + return jsonify({"success": True, "data": logs}) + + @app.route('/api/system_info') + @login_required + def api_system_info(): + # 获取系统信息 + import platform + import psutil + + system_info = { + "os": platform.system(), + "os_version": platform.version(), + "python_version": platform.python_version(), + "cpu_usage": psutil.cpu_percent(), + "memory_usage": psutil.virtual_memory().percent, + "disk_usage": psutil.disk_usage('/').percent, + "uptime": time.time() - psutil.boot_time(), + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + + return jsonify({"success": True, "data": system_info}) + + return app + + def run(self): + """运行服务器""" + from waitress import serve + + self.logger.info(f"启动服务器: {self.host}:{self.port}") + try: + serve(self.app, host=self.host, port=self.port) + except Exception as e: + self.logger.error(f"服务器运行失败: {e}") + self._stop_event.set() + + def stop(self): + """停止服务器""" + self.logger.info("正在停止服务器...") + self._stop_event.set() + + # 如果使用了waitress,需要额外的停止逻辑 + # 这里可能需要根据实际情况调整 + if self._server: + # 某些服务器可能有shutdown方法 + if hasattr(self._server, 'shutdown'): + self._server.shutdown() + # 或者需要关闭socket + elif hasattr(self._server, 'socket'): + self._server.socket.close() + + self.logger.info("服务器已停止") + + +# 在 _create_app 方法中添加以下路由 + +@app.route('/api/dashboard_summary') +@login_required +def api_dashboard_summary(): + try: + days = request.args.get('days', 7, type=int) + summary = self.stats_db.get_dashboard_summary(days) + + # 转换用户和群组ID为名称 + if 'top_users' in summary: + for user in summary['top_users']: + if 'user_id' in user: + user['user_name'] = self.contact_manager.get_nickname(user['user_id']) + + if 'top_groups' in summary: + for group in summary['top_groups']: + if 'group_id' in group: + group['group_name'] = self.contact_manager.get_nickname(group['group_id']) + + self.logger.info(f"看板主页统计数据: {summary}") + return jsonify({"success": True, "data": summary}) + except Exception as e: + self.logger.error(f"获取仪表盘摘要数据出错: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/plugin_trend') +@login_required +def api_plugin_trend(): + days = request.args.get('days', 7, type=int) + plugin_name = request.args.get('plugin_name', '') + trend = self.stats_db.get_plugin_trend(plugin_name, days) + + # 如果趋势数据中包含用户或群组ID,也进行转换 + if isinstance(trend, list): + for item in trend: + if 'user_id' in item: + item['user_name'] = self.contact_manager.get_nickname(item['user_id']) + if 'group_id' in item: + item['group_name'] = self.contact_manager.get_nickname(item['group_id']) + + self.logger.info(f"看板主页/api/plugin_trend: {trend}") + return jsonify({"success": True, "data": trend}) + +@app.route('/api/robot/group//message_trend', methods=['GET']) +@login_required +def get_group_message_trend(group_id): + """获取群组消息趋势数据""" + try: + days = request.args.get('days', default=7, type=int) + # 获取消息存储实例 + trend_data = self.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: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@app.route('/api/error_detail/') +@login_required +def api_error_detail(error_id): + detail = self.stats_db.get_error_detail(error_id) + return jsonify({"success": True, "data": detail}) \ No newline at end of file diff --git a/admin/dashboard/static/favicon.ico b/admin/dashboard/static/favicon.ico new file mode 100644 index 0000000..d0f92da Binary files /dev/null and b/admin/dashboard/static/favicon.ico differ diff --git a/admin/dashboard/templates/base.html b/admin/dashboard/templates/base.html new file mode 100644 index 0000000..796fd56 --- /dev/null +++ b/admin/dashboard/templates/base.html @@ -0,0 +1,132 @@ + + + + + + {% block title %}机器人管理后台{% endblock %} + + + + + + + + {% block head %}{% endblock %} + + +
+ + + + + + + 首页概览 + + + + 插件统计 + + + + 用户统计 + + + + 群组统计 + + + + 错误日志 + + + + 群机器人管理 + + + + + +
{% block header_title %}机器人管理后台{% endblock %}
+
+ + + 管理员 + + + 退出登录 + + +
+
+ + + + + + + + + + + + + + + 刷新数据 + + + + + + + {% block content %}{% endblock %} + + + + +
+
+
+ + + + + + + + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/admin/dashboard/templates/errors.html b/admin/dashboard/templates/errors.html new file mode 100644 index 0000000..60b6b78 --- /dev/null +++ b/admin/dashboard/templates/errors.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} + +{% block title %}错误日志 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 错误日志 +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + +
+

插件名称: {% raw %}{{ errorDetail.plugin_name }}{% endraw %}

+

命令: {% raw %}{{ errorDetail.command }}{% endraw %}

+

用户ID: {% raw %}{{ errorDetail.user_id }}{% endraw %}

+

群组ID: {% raw %}{{ errorDetail.group_id || '无' }}{% endraw %}

+

时间: {% raw %}{{ errorDetail.created_at }}{% endraw %}

+

错误信息: {% raw %}{{ errorDetail.error_message }}{% endraw %}

+
+

堆栈跟踪:

+
{% raw %}{{ errorDetail.stack_trace }}{% endraw %}
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard/templates/groups.html b/admin/dashboard/templates/groups.html new file mode 100644 index 0000000..1932d11 --- /dev/null +++ b/admin/dashboard/templates/groups.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block title %}群组统计 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 群组活跃度排行 +
+ + + + + + + + + + + + + + + + +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard/templates/index.html b/admin/dashboard/templates/index.html new file mode 100644 index 0000000..584c62e --- /dev/null +++ b/admin/dashboard/templates/index.html @@ -0,0 +1,386 @@ +{% extends "base.html" %} + +{% block title %}首页概览 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 总调用次数 +
+
+ {% raw %}{{ totalCalls }}{% endraw %} +
+
+
+ + +
+ 成功率 +
+
+ {% raw %}{{ successRate.toFixed(2) }}{% endraw %}% +
+
+
+ + +
+ 活跃用户数 +
+
+ {% raw %}{{ activeUsers }}{% endraw %} +
+
+
+ + +
+ 活跃群组数 +
+
+ {% raw %}{{ activeGroups }}{% endraw %} +
+
+
+ + +
+ 平均响应时间 +
+
+ {% raw %}{{ avgResponseTime.toFixed(2) }}{% endraw %} ms +
+
+
+
+ + + + + +
+ 热门用户 +
+ + + + + + + + +
+
+ + +
+ 热门群组 +
+ + + + + + + +
+
+ + +
+ 热门插件 +
+ + + + +
+
+
+ + + +
+

插件使用排行

+ +
+
+ +
+

成功率分析

+ +
+
+
+ + + +
+

使用趋势

+ +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block styles %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard/templates/login.html b/admin/dashboard/templates/login.html new file mode 100644 index 0000000..c983a3a --- /dev/null +++ b/admin/dashboard/templates/login.html @@ -0,0 +1,114 @@ + + + + + + 登录 - WeChatRobot管理后台 + + + + + + + + + + + \ No newline at end of file diff --git a/admin/dashboard/templates/plugins.html b/admin/dashboard/templates/plugins.html new file mode 100644 index 0000000..06a8845 --- /dev/null +++ b/admin/dashboard/templates/plugins.html @@ -0,0 +1,255 @@ +{% extends "base.html" %} + +{% block title %}插件统计 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 插件使用统计 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+

{% raw %}{{ selectedPlugin ? selectedPlugin.plugin_name : '' }}{% endraw %} 使用趋势

+ +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard/templates/robot_management.html b/admin/dashboard/templates/robot_management.html new file mode 100644 index 0000000..984a6c4 --- /dev/null +++ b/admin/dashboard/templates/robot_management.html @@ -0,0 +1,473 @@ +{% extends "base.html" %} + +{% block title %}群机器人管理 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 群机器人管理 + + 添加群组 + + + +
+ + + + + + + + + + + + + + + + +
+ + 已选择 {% raw %}{{ selectedGroups.length }}{% endraw %} 个群组 + +
+ 批量启用 + 批量关闭 + 批量移除 +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + +
+

{% raw %}{{ currentGroupName }}{% endraw %} 消息趋势

+ +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard/templates/users.html b/admin/dashboard/templates/users.html new file mode 100644 index 0000000..9561555 --- /dev/null +++ b/admin/dashboard/templates/users.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block title %}用户统计 - 机器人管理后台{% endblock %} + +{% block content %} + +
+ + + +
+ 用户活跃度排行 +
+ + + + + + + + + + + + + + +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/admin/dashboard_start.bat b/admin/dashboard_start.bat new file mode 100644 index 0000000..1058892 --- /dev/null +++ b/admin/dashboard_start.bat @@ -0,0 +1,5 @@ +@echo off +cd /d %~dp0 +echo 正在启动WeChatRobot管理后台... +python dashboard_start.py +pause \ No newline at end of file diff --git a/admin/dashboard_start.py b/admin/dashboard_start.py new file mode 100644 index 0000000..ad1426b --- /dev/null +++ b/admin/dashboard_start.py @@ -0,0 +1,80 @@ +import os +import sys +import logging +import tomli +import argparse + +# 添加项目根目录到系统路径 +sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) + +from admin.dashboard.server import DashboardServer + +def setup_logging(): + """设置日志""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler(os.path.join(os.path.dirname(__file__), 'dashboard.log')) + ] + ) + +def load_config(): + """加载TOML配置""" + config_path = os.path.join(os.path.dirname(__file__), 'dashboard', 'config.toml') + if not os.path.exists(config_path): + print(f"配置文件不存在: {config_path}") + return None + + try: + with open(config_path, 'rb') as f: + config = tomli.load(f) + return config + except Exception as e: + print(f"加载配置文件失败: {e}") + return None + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description='启动WeChatRobot管理后台') + parser.add_argument('--host', type=str, help='服务器主机地址') + parser.add_argument('--port', type=int, help='服务器端口') + parser.add_argument('--username', type=str, help='管理员用户名') + parser.add_argument('--password', type=str, help='管理员密码') + args = parser.parse_args() + + # 设置日志 + setup_logging() + logger = logging.getLogger("Dashboard") + + # 加载配置 + config = load_config() + if not config: + logger.error("无法加载配置,使用默认配置") + config = {} + + # 命令行参数优先级高于配置文件 + host = args.host or config.get('host', '0.0.0.0') + port = args.port or config.get('port', 8888) + username = args.username or config.get('username', 'admin') + password = args.password or config.get('password', 'admin123') + + # 检查是否启用 + if not config.get('enable', True): + logger.info("管理后台已禁用,退出程序") + return + + # 创建并启动服务器 + server = DashboardServer( + host=host, + port=port, + username=username, + password=password + ) + + logger.info(f"启动管理后台服务器,访问地址: http://{host}:{port}") + server.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/plugins/stats_dashboard/config.toml b/plugins/stats_dashboard/config.toml index 60f4901..35eae07 100644 --- a/plugins/stats_dashboard/config.toml +++ b/plugins/stats_dashboard/config.toml @@ -1,4 +1,4 @@ -enable = true +enable = false host = "0.0.0.0" port = 8888 username = "admin"