@@ -1,52 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
统计看板服务器。
|
||||
|
||||
这里负责:
|
||||
1. 读取 Dashboard 配置并创建 Flask 应用;
|
||||
2. 初始化后台依赖对象、蓝图和静态资源路由;
|
||||
3. 提供后台登录限流、弱密码提醒等通用安全能力。
|
||||
统计看板服务器 - 使用Flask蓝图重构版
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
|
||||
import toml
|
||||
from flask import Flask, send_from_directory
|
||||
from loguru import logger
|
||||
|
||||
from db.admin_account_db import AdminAccountDBOperator
|
||||
from db.contacts_db import ContactsDBOperator
|
||||
from db.admin_account_db import AdminAccountDBOperator
|
||||
from db.emoji_asset_db import EmojiAssetDB
|
||||
from db.fun_command_rule_db import FunCommandRuleDBOperator
|
||||
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__), "..", "..")))
|
||||
# 添加项目根目录到系统路径,确保可以导入项目模块
|
||||
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,
|
||||
):
|
||||
# 先加载配置文件,再用传入参数覆盖,保证 CLI/外部调用具备最高优先级。
|
||||
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")
|
||||
@@ -54,108 +45,105 @@ class DashboardServer:
|
||||
|
||||
self.LOG = logger
|
||||
self.LOG.info(f"Dashboard配置加载完成: 服务器将运行在 {self.host}:{self.port}")
|
||||
|
||||
# 登录失败限流的进程内兜底缓存:
|
||||
# 1. 优先用 Redis 做共享限流;
|
||||
# 2. 如果 Redis 暂时不可用,至少本进程内仍有基本防护;
|
||||
# 3. 这里会被并发访问,因此需要配合线程锁使用。
|
||||
# 登录失败限流兜底缓存:
|
||||
# 1. 优先尝试 Redis,但为了兼容 Redis 暂不可用的场景,这里保留进程内兜底;
|
||||
# 2. 字典内容只保存短期登录失败窗口,不用于持久化;
|
||||
# 3. 线程化 WSGI 会并发访问,因此需要显式加锁。
|
||||
self._auth_runtime_lock = threading.Lock()
|
||||
self._auth_failures = {}
|
||||
# 如果提供了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)
|
||||
|
||||
if not robot_instance:
|
||||
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.db_manager = robot_instance.db_manager
|
||||
self.stats_db = StatsDBOperator(self.db_manager)
|
||||
|
||||
# Dashboard 启动可能早于 iPad 登录完成:
|
||||
# 1. 此时 Robot 上的 message_storage 可能尚未绑定完成;
|
||||
# 2. 但后台页面仍然依赖消息存储与表情资产库;
|
||||
# 3. 因此优先复用 Robot 现成对象,没有时再安全回退到 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
|
||||
|
||||
# 趣味指令规则服务:用于“文案/事件触发多媒体玩法回复”的后台配置与缓存。
|
||||
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()
|
||||
|
||||
# 其余运行时对象直接复用 Robot 已初始化实例,避免重复构造。
|
||||
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}")
|
||||
|
||||
self.app = self._create_app()
|
||||
self._stop_event = threading.Event()
|
||||
self._server = None # Werkzeug 服务实例会在 run() 时写入这里。
|
||||
self._server = None # 存储服务器实例
|
||||
|
||||
def _load_dashboard_config(self):
|
||||
"""加载 Dashboard 配置文件。"""
|
||||
"""加载Dashboard配置文件"""
|
||||
try:
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.toml")
|
||||
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:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return toml.load(f)
|
||||
|
||||
# 如果配置文件不存在,则创建默认配置,方便本地快速启动。
|
||||
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
|
||||
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"},
|
||||
"auth": {"username": "admin", "password": "admin123"}
|
||||
}
|
||||
|
||||
def _create_app(self) -> Flask:
|
||||
"""创建 Flask 应用。"""
|
||||
template_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
||||
"""创建Flask应用"""
|
||||
# 指定模板文件夹路径
|
||||
template_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
|
||||
app = Flask(__name__, template_folder=template_folder)
|
||||
|
||||
auth_config = self.config.get("auth", {}) or {}
|
||||
session_timeout_minutes = int(auth_config.get("session_timeout_minutes", 480) or 480)
|
||||
cookie_secure = bool(auth_config.get("cookie_secure", False))
|
||||
@@ -164,54 +152,57 @@ class DashboardServer:
|
||||
or auth_config.get("secret_key", "")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
if configured_secret:
|
||||
app.secret_key = configured_secret
|
||||
else:
|
||||
# 未显式配置 secret_key 时,用进程级随机密钥兜底:
|
||||
# 1. 安全性优于硬编码固定值;
|
||||
# 2. 服务重启后旧 session 会失效,但这是可接受的安全代价;
|
||||
# 3. 同时输出 warning,提醒后续通过配置或环境变量固定注入。
|
||||
# 若未显式配置 secret_key,则每次进程启动生成随机值:
|
||||
# 1. 这比固定硬编码密钥安全得多;
|
||||
# 2. 代价是服务重启后旧 session 会失效,作为安全兜底是可接受的;
|
||||
# 3. 同时输出 warning,提醒后续最好通过配置或环境变量固定注入。
|
||||
app.secret_key = secrets.token_hex(32)
|
||||
self.LOG.warning("未配置 Dashboard secret_key,已使用进程级随机密钥,重启后现有登录会失效")
|
||||
|
||||
# 关闭模板缓存,便于开发时实时看到页面修改结果。
|
||||
app.config["TEMPLATES_AUTO_RELOAD"] = True
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = str(auth_config.get("cookie_samesite", "Lax") or "Lax")
|
||||
app.config["SESSION_COOKIE_SECURE"] = cookie_secure
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=max(15, session_timeout_minutes))
|
||||
# 禁用模板缓存,使修改HTML文件后立即生效 False =重启才生效
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = str(auth_config.get("cookie_samesite", "Lax") or "Lax")
|
||||
app.config['SESSION_COOKIE_SECURE'] = cookie_secure
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=max(15, session_timeout_minutes))
|
||||
|
||||
# 设置Werkzeug日志级别为DEBUG
|
||||
import logging
|
||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
|
||||
# 将 DashboardServer 实例挂到 app 上,方便蓝图在请求期取用。
|
||||
# 将dashboard_server实例设置为app的属性
|
||||
app.dashboard_server = self
|
||||
|
||||
static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||
# 配置静态文件访问
|
||||
static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
||||
|
||||
@app.route("/static/<path:filename>")
|
||||
@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>")
|
||||
@app.route('/static/images/<path:filename>')
|
||||
def serve_images(filename):
|
||||
return send_from_directory(images_dir, filename)
|
||||
|
||||
@app.route("/favicon.ico")
|
||||
# 添加一个路由处理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",
|
||||
)
|
||||
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 get_auth_policy(self) -> dict:
|
||||
@@ -248,19 +239,12 @@ class DashboardServer:
|
||||
if expire_at > 0 and expire_at <= time.time():
|
||||
self._auth_failures.pop(guard_key, None)
|
||||
record = {}
|
||||
|
||||
return {
|
||||
"count": int(record.get("count") or 0),
|
||||
"first_failed_at": float(record.get("first_failed_at") or 0.0),
|
||||
}
|
||||
|
||||
def _save_login_failure_record(
|
||||
self,
|
||||
guard_key: str,
|
||||
count: int,
|
||||
first_failed_at: float,
|
||||
ttl_seconds: int,
|
||||
) -> None:
|
||||
def _save_login_failure_record(self, guard_key: str, count: int, first_failed_at: float, ttl_seconds: int) -> None:
|
||||
"""保存登录失败记录,优先 Redis,失败时回退到进程内缓存。"""
|
||||
payload = f"{int(count)}|{float(first_failed_at)}"
|
||||
try:
|
||||
@@ -322,48 +306,41 @@ class DashboardServer:
|
||||
|
||||
def should_force_password_change(self, username: str) -> bool:
|
||||
"""判断当前管理员是否应该被强制提示修改密码。"""
|
||||
normalized_username = str(username or "").strip()
|
||||
admin_db = getattr(self, "admin_account_db", None)
|
||||
if admin_db:
|
||||
try:
|
||||
admin_row = admin_db.get_admin_by_username(normalized_username)
|
||||
# 只要数据库里已经存在该账号,就完全以数据库中的当前密码状态为准。
|
||||
# 这样可以避免用户已经在后台把密码改强后,仍被 config.toml 中的旧默认密码反复误判。
|
||||
if admin_row:
|
||||
return bool(admin_db.is_using_risky_password(normalized_username))
|
||||
except Exception as e:
|
||||
self.LOG.error(f"判断后台弱密码状态时出现异常,将回退配置判断: {e}")
|
||||
if admin_db and admin_db.is_using_risky_password(username):
|
||||
return True
|
||||
|
||||
# 数据库体系不可用或该账号尚未迁移进数据库时,再回退配置值判断。
|
||||
# 数据库体系不可用时,再回退配置值判断,至少把默认 admin/admin123 识别出来。
|
||||
fallback_username = str(self.username or "").strip()
|
||||
fallback_password = str(self.password or "").strip()
|
||||
return (
|
||||
normalized_username == fallback_username
|
||||
str(username or "").strip() == fallback_username
|
||||
and fallback_password in getattr(admin_db, "RISKY_PASSWORDS", {"admin123", "admin"})
|
||||
)
|
||||
|
||||
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.file_browser import file_browser_bp
|
||||
from admin.dashboard.blueprints.friend_circle import friend_circle_bp
|
||||
from admin.dashboard.blueprints.fun_command_rules import fun_command_rules_bp
|
||||
from admin.dashboard.blueprints.group_plugin_config import group_plugin_config_bp
|
||||
from admin.dashboard.blueprints.main import main_bp
|
||||
from admin.dashboard.blueprints.message_push import message_push_bp
|
||||
from admin.dashboard.blueprints.messages import messages_bp
|
||||
from admin.dashboard.blueprints.plugin_routes import plugin_routes
|
||||
from admin.dashboard.blueprints.plugin_schedules import plugin_schedules_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.system_jobs import system_jobs_bp
|
||||
from admin.dashboard.blueprints.trendradar_webhook import trendradar_webhook_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(virtual_group_bp, url_prefix="/virtual_group")
|
||||
# 在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)
|
||||
@@ -384,16 +361,16 @@ class DashboardServer:
|
||||
self.LOG.info("所有蓝图已注册")
|
||||
|
||||
def run(self):
|
||||
"""运行服务器。"""
|
||||
"""运行服务器"""
|
||||
from werkzeug.serving import make_server
|
||||
|
||||
# 设置Werkzeug日志级别为DEBUG
|
||||
import logging
|
||||
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||
self.LOG.info(f"启动服务器: {self.host}:{self.port}")
|
||||
try:
|
||||
# Dashboard 存在文件浏览、统计查询等慢请求,单线程 WSGI 一旦被占住会拖死整个后台。
|
||||
# 改为 threaded server 可以避免某个接口阻塞时所有页面一起无响应。
|
||||
# Dashboard 存在文件浏览、统计查询等慢请求,单线程 WSGI 一旦被占住会导致整个后台无响应。
|
||||
# 改为 threaded server,避免某个接口阻塞后拖死所有页面访问。
|
||||
self._server = make_server(self.host, self.port, self.app, threaded=True)
|
||||
self._server.serve_forever()
|
||||
except Exception as e:
|
||||
@@ -401,22 +378,26 @@ class DashboardServer:
|
||||
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("机器人对象未初始化")
|
||||
@@ -425,7 +406,7 @@ class DashboardServer:
|
||||
"nickName": getattr(self.robot, "nickname", ""),
|
||||
"mobile": getattr(self.robot, "phone", ""),
|
||||
"smallHeadImgUrl": getattr(self.robot, "head_image", ""),
|
||||
"signature": getattr(self.robot, "signature", ""),
|
||||
"signature": getattr(self.robot, "signature", "")
|
||||
}
|
||||
except (AttributeError, ValueError) as e:
|
||||
print(f"获取用户信息出错: {str(e)}")
|
||||
@@ -434,7 +415,7 @@ class DashboardServer:
|
||||
"nickName": self.robot.nickname,
|
||||
"mobile": self.robot.phone,
|
||||
"smallHeadImgUrl": self.robot.head_image,
|
||||
"signature": self.robot.signature,
|
||||
"signature": self.robot.signature
|
||||
}
|
||||
|
||||
if not user_data:
|
||||
@@ -445,11 +426,11 @@ class DashboardServer:
|
||||
"data": {
|
||||
"wx_id": user_data.get("wxid", ""),
|
||||
"nickname": user_data.get("nickName", "未知用户"),
|
||||
"avatar": user_data.get("smallHeadImgUrl", "logo.png"), # 使用小头像 URL。
|
||||
"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],
|
||||
},
|
||||
"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}")
|
||||
|
||||
Reference in New Issue
Block a user