Files
abot/admin/dashboard/server.py
T

295 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""
统计看板服务器 - 使用Flask蓝图重构版
"""
import os
import sys
import threading
import toml
from flask import Flask, send_from_directory
from loguru import logger
from db.contacts_db import ContactsDBOperator
from db.admin_account_db import AdminAccountDBOperator
from db.emoji_asset_db import EmojiAssetDB
from db.member_context_db import MemberContextDBOperator
from db.message_storage import MessageStorageDB
from db.stats_db import StatsDBOperator
from db.task_db import TaskDBOperator
from db.fun_command_rule_db import FunCommandRuleDBOperator
from utils.fun_command_rule_service import FunCommandRuleService
from wechat_ipad import WechatAPIClient
# 添加项目根目录到系统路径,确保可以导入项目模块
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
class DashboardServer:
"""统计看板服务器"""
def __init__(self, host: str = None, port: int = None,
username: str = None, password: str = None,
robot_instance=None):
# 加载配置文件
self.config = self._load_dashboard_config()
# 优先使用传入的参数,其次使用配置文件中的参数
self.host = host or self.config.get("server", {}).get("host", "0.0.0.0")
self.port = port or self.config.get("server", {}).get("port", 8888)
self.username = username or self.config.get("auth", {}).get("username", "admin")
self.password = password or self.config.get("auth", {}).get("password", "admin123")
self.LOG = logger
self.LOG.info(f"Dashboard配置加载完成: 服务器将运行在 {self.host}:{self.port}")
# 如果提供了robot实例,则使用其对象
if robot_instance:
self.db_manager = robot_instance.db_manager
self.stats_db = StatsDBOperator(self.db_manager)
# Dashboard 启动可能早于 iPad 登录完成:
# 1. 此时 Robot 上的 message_storage 还没来得及绑定真实 bot;
# 2. 但后台很多页面仍然依赖消息存储与表情资产库;
# 3. 因此这里优先复用 Robot 已初始化的 message_storage,没有则再安全回退到 DB 层对象。
self.message_storage = getattr(robot_instance, "message_storage", None) or MessageStorageDB(self.db_manager)
self.emoji_asset_db = getattr(self.message_storage, "emoji_asset_db", None) or EmojiAssetDB(self.db_manager)
self.contact_db: ContactsDBOperator = ContactsDBOperator(self.db_manager)
self.member_context_db = MemberContextDBOperator(self.db_manager)
self.task_db: TaskDBOperator = TaskDBOperator(self.db_manager)
# 后台管理员账号数据层:用于登录鉴权与修改密码。
self.admin_account_db = AdminAccountDBOperator(self.db_manager)
self.system_job_db = robot_instance.system_job_db
self.system_job_loader = robot_instance.system_job_loader
self.plugin_schedule_db = robot_instance.plugin_schedule_db
self.plugin_schedule_manager = robot_instance.plugin_schedule_manager
self.group_plugin_config_db = robot_instance.group_plugin_config_db
self.llm_catalog_db = robot_instance.llm_catalog_db
self.group_plugin_config_service = robot_instance.group_plugin_config_service
# 趣味指令规则服务:用于“文案/事件触发多媒体玩法回复”后台配置与缓存。
# 这里统一在 Dashboard 启动时初始化,保证管理端可直接读写规则。
self.fun_command_rule_db = FunCommandRuleDBOperator(self.db_manager)
self.fun_command_rule_service = FunCommandRuleService(
db_operator=self.fun_command_rule_db,
redis_client=self.db_manager.get_redis_connection(),
local_ttl_seconds=30,
)
self.fun_command_rule_service.init_tables()
self.fun_command_rule_service.refresh_cache()
# 获取联系人管理器实例
self.contact_manager = robot_instance.contact_manager
self.plugin_manager = robot_instance.plugin_manager
self.plugin_registry = robot_instance.plugin_registry
self.client: WechatAPIClient = robot_instance.ipad_bot
self.robot = robot_instance
self.member_context_plugin = self.plugin_manager.plugins.get("成员交互摘要")
self.member_context_service = getattr(self.member_context_plugin, "service", None)
self.LOG.info("使用Robot实例的对象进行初始化")
# 初始化后台管理员账号表,并将旧配置中的默认账号平滑迁移进数据库。
try:
table_ok = self.admin_account_db.init_tables()
if not table_ok:
self.LOG.warning("初始化后台账号表失败,将回退旧配置账号模式")
else:
seed_ok = self.admin_account_db.ensure_default_admin(self.username, self.password, "系统管理员")
if seed_ok:
self.LOG.info("后台账号体系初始化完成(数据库账号模式已可用)")
else:
self.LOG.warning("后台账号种子初始化失败,请检查配置中的默认账号信息")
except Exception as e:
self.LOG.error(f"初始化后台账号体系失败,将回退旧配置账号模式: {e}")
else:
self.LOG.error("未提供Robot实例,Dashboard无法正常工作")
raise ValueError("必须提供Robot实例")
self.app = self._create_app()
self._stop_event = threading.Event()
self._server = None # 存储服务器实例
def _load_dashboard_config(self):
"""加载Dashboard配置文件"""
try:
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
return toml.load(f)
else:
# 如果配置文件不存在,创建默认配置
default_config = {
"server": {"host": "0.0.0.0", "port": 8888},
"auth": {"username": "admin", "password": "admin123"}
}
with open(config_path, 'w', encoding='utf-8') as f:
toml.dump(default_config, f)
return default_config
except Exception as e:
self.LOG.error(f"加载Dashboard配置文件失败: {e}")
# 返回默认配置
return {
"server": {"host": "0.0.0.0", "port": 8888},
"auth": {"username": "admin", "password": "admin123"}
}
def _create_app(self) -> Flask:
"""创建Flask应用"""
# 指定模板文件夹路径
template_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
app = Flask(__name__, template_folder=template_folder)
app.secret_key = "stats_dashboard_secret_key"
# 禁用模板缓存,使修改HTML文件后立即生效 False =重启才生效
app.config['TEMPLATES_AUTO_RELOAD'] = True
# 设置Werkzeug日志级别为DEBUG
import logging
logging.getLogger('werkzeug').setLevel(logging.ERROR)
# 将dashboard_server实例设置为app的属性
app.dashboard_server = self
# 配置静态文件访问
static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory(static_folder, filename)
# 获取项目根目录下的static/images目录
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
images_dir = os.path.join(project_root, "static", "images")
# 确保目录存在
os.makedirs(images_dir, exist_ok=True)
@app.route('/static/images/<path:filename>')
def serve_images(filename):
return send_from_directory(images_dir, 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')
# 注册蓝图
self._register_blueprints(app)
return app
def _register_blueprints(self, app):
"""注册所有蓝图"""
# 在函数内部导入蓝图,避免循环导入
from admin.dashboard.blueprints.auth import auth_bp
from admin.dashboard.blueprints.contacts import contacts_bp
from admin.dashboard.blueprints.robot import robot_bp
from admin.dashboard.blueprints.messages import messages_bp
from admin.dashboard.blueprints.stats import stats_bp
from admin.dashboard.blueprints.system import system_bp
from admin.dashboard.blueprints.main import main_bp
from admin.dashboard.blueprints.plugin_routes import plugin_routes
from admin.dashboard.blueprints.virtual_group import virtual_group_bp
from admin.dashboard.blueprints.file_browser import file_browser_bp
from admin.dashboard.blueprints.message_push import message_push_bp
from admin.dashboard.blueprints.friend_circle import friend_circle_bp
from admin.dashboard.blueprints.system_jobs import system_jobs_bp
from admin.dashboard.blueprints.plugin_schedules import plugin_schedules_bp
from admin.dashboard.blueprints.group_plugin_config import group_plugin_config_bp
from admin.dashboard.blueprints.fun_command_rules import fun_command_rules_bp
from admin.dashboard.blueprints.trendradar_webhook import trendradar_webhook_bp
# 在app.register_blueprint部分添加
app.register_blueprint(virtual_group_bp, url_prefix='/virtual_group')
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(contacts_bp)
app.register_blueprint(robot_bp)
app.register_blueprint(messages_bp)
app.register_blueprint(stats_bp)
app.register_blueprint(system_bp)
app.register_blueprint(plugin_routes)
app.register_blueprint(file_browser_bp)
app.register_blueprint(message_push_bp)
app.register_blueprint(friend_circle_bp)
app.register_blueprint(system_jobs_bp)
app.register_blueprint(plugin_schedules_bp)
app.register_blueprint(group_plugin_config_bp)
app.register_blueprint(fun_command_rules_bp)
app.register_blueprint(trendradar_webhook_bp)
self.LOG.info("所有蓝图已注册")
def run(self):
"""运行服务器"""
from werkzeug.serving import make_server
# 设置Werkzeug日志级别为DEBUG
import logging
logging.getLogger('werkzeug').setLevel(logging.ERROR)
self.LOG.info(f"启动服务器: {self.host}:{self.port}")
try:
# Dashboard 存在文件浏览、统计查询等慢请求,单线程 WSGI 一旦被占住会导致整个后台无响应。
# 改为 threaded server,避免某个接口阻塞后拖死所有页面访问。
self._server = make_server(self.host, self.port, self.app, threaded=True)
self._server.serve_forever()
except Exception as e:
self.LOG.error(f"服务器运行失败: {e}")
self._stop_event.set()
def stop(self):
"""停止服务器"""
self.LOG.info("正在停止服务器...")
self._stop_event.set()
# 使用werkzeug服务器的关闭方法
if self._server:
self._server.shutdown()
self.LOG.info("服务器已停止")
def get_current_user_info(self):
"""获取当前登录的微信用户信息"""
try:
if not self.client:
self.LOG.error("client实例不可用,无法获取当前用户信息")
return {"success": False, "message": "实例不可用"}
# 获取当前登录的微信ID
# 从新的resp格式中获取用户信息
try:
if self.robot is None:
raise ValueError("机器人对象未初始化")
user_data = {
"wxid": getattr(self.robot, "wxid", ""),
"nickName": getattr(self.robot, "nickname", ""),
"mobile": getattr(self.robot, "phone", ""),
"smallHeadImgUrl": getattr(self.robot, "head_image", ""),
"signature": getattr(self.robot, "signature", "")
}
except (AttributeError, ValueError) as e:
print(f"获取用户信息出错: {str(e)}")
user_data = {
"wxid": self.robot.wxid,
"nickName": self.robot.nickname,
"mobile": self.robot.phone,
"smallHeadImgUrl": self.robot.head_image,
"signature": self.robot.signature
}
if not user_data:
return {"success": False, "message": "未获取到用户数据"}
return {
"success": True,
"data": {
"wx_id": user_data.get("wxid", ""),
"nickname": user_data.get("nickName", "未知用户"),
"avatar": user_data.get("smallHeadImgUrl", "logo.png"), # 使用小头像URL
"mobile": user_data.get("mobile", ""),
"home": f"{user_data.get('province', '')}-{user_data.get('city', '')}", # 组合省市信息
"signature": user_data.get("signature", "")[:10]
}
}
except Exception as e:
self.LOG.error(f"获取当前用户信息失败: {e}")
return {"success": False, "message": f"获取用户信息出错: {str(e)}"}