From 4daf07513865e82a64c8f4bd036564d4f7cb06b4 Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 27 Mar 2025 11:06:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E7=9C=8B=E6=9D=BF=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=EF=BC=8C=E6=96=B9=E4=BE=BF=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/dashboard/README.md | 30 ++ admin/dashboard/config.toml | 6 + admin/dashboard/server.py | 392 +++++++++++++++ admin/dashboard/static/favicon.ico | Bin 0 -> 15406 bytes admin/dashboard/templates/base.html | 132 +++++ admin/dashboard/templates/errors.html | 146 ++++++ admin/dashboard/templates/groups.html | 79 +++ admin/dashboard/templates/index.html | 386 ++++++++++++++ admin/dashboard/templates/login.html | 114 +++++ admin/dashboard/templates/plugins.html | 255 ++++++++++ .../dashboard/templates/robot_management.html | 473 ++++++++++++++++++ admin/dashboard/templates/users.html | 77 +++ admin/dashboard_start.bat | 5 + admin/dashboard_start.py | 80 +++ plugins/stats_dashboard/config.toml | 2 +- 15 files changed, 2176 insertions(+), 1 deletion(-) create mode 100644 admin/dashboard/README.md create mode 100644 admin/dashboard/config.toml create mode 100644 admin/dashboard/server.py create mode 100644 admin/dashboard/static/favicon.ico create mode 100644 admin/dashboard/templates/base.html create mode 100644 admin/dashboard/templates/errors.html create mode 100644 admin/dashboard/templates/groups.html create mode 100644 admin/dashboard/templates/index.html create mode 100644 admin/dashboard/templates/login.html create mode 100644 admin/dashboard/templates/plugins.html create mode 100644 admin/dashboard/templates/robot_management.html create mode 100644 admin/dashboard/templates/users.html create mode 100644 admin/dashboard_start.bat create mode 100644 admin/dashboard_start.py 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 0000000000000000000000000000000000000000..d0f92da3c30d2fd1c53f6927ebdd4ce9e745b7fa GIT binary patch literal 15406 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 literal 0 HcmV?d00001 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"