# ==================== 日志模块 ==================== # 统一的日志输出,支持控制台和文件日志,带日志轮转 import os import sys import logging from datetime import datetime from pathlib import Path from logging.handlers import RotatingFileHandler # ==================== 日志配置 ==================== LOG_DIR = Path(__file__).parent / "logs" LOG_FILE = LOG_DIR / "app.log" LOG_MAX_BYTES = 10 * 1024 * 1024 # 10MB LOG_BACKUP_COUNT = 5 # 保留 5 个备份 def _ensure_log_dir(): """确保日志目录存在""" LOG_DIR.mkdir(exist_ok=True) class ColoredFormatter(logging.Formatter): """带颜色的控制台日志格式化器""" COLORS = { logging.DEBUG: "\033[90m", # 灰色 logging.INFO: "\033[0m", # 默认 logging.WARNING: "\033[93m", # 黄色 logging.ERROR: "\033[91m", # 红色 logging.CRITICAL: "\033[91m", # 红色 } RESET = "\033[0m" GREEN = "\033[92m" # 用于 success BLUE = "\033[94m" # 用于 highlight def format(self, record): # 自定义 level 颜色 color = self.COLORS.get(record.levelno, self.RESET) # 处理自定义的 success level if hasattr(record, 'is_success') and record.is_success: color = self.GREEN # 处理自定义的 highlight level (蓝色) if hasattr(record, 'is_highlight') and record.is_highlight: color = self.BLUE # 格式化时间 timestamp = datetime.fromtimestamp(record.created).strftime("%H:%M:%S") # 获取图标 icon = getattr(record, 'icon', '') if icon: icon = f"{icon} " # 构建消息 message = f"[{timestamp}] {color}{icon}{record.getMessage()}{self.RESET}" return message class FileFormatter(logging.Formatter): """文件日志格式化器 (不带颜色)""" def format(self, record): timestamp = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S") level = record.levelname.ljust(8) icon = getattr(record, 'icon', '') if icon: icon = f"{icon} " return f"[{timestamp}] [{level}] {icon}{record.getMessage()}" class TelegramLogHandler(logging.Handler): """Telegram 实时日志处理器""" def __init__(self, callback, level=logging.INFO): """初始化 Telegram 日志处理器 Args: callback: 回调函数,接收 (message: str, level: str) 参数 level: 日志级别 """ super().__init__(level) self.callback = callback self.setFormatter(FileFormatter()) def emit(self, record): """发送日志记录到 Telegram""" try: msg = self.format(record) # 映射日志级别到 BotNotifier 的级别 level_map = { logging.DEBUG: "debug", logging.INFO: "info", logging.WARNING: "warning", logging.ERROR: "error", logging.CRITICAL: "error" } level = level_map.get(record.levelno, "info") self.callback(msg, level) except Exception: self.handleError(record) class Logger: """统一日志输出 (基于 Python logging 模块)""" # 日志级别 LEVEL_DEBUG = logging.DEBUG LEVEL_INFO = logging.INFO LEVEL_WARNING = logging.WARNING LEVEL_ERROR = logging.ERROR # 日志级别图标 ICONS = { "info": "", "success": "", "warning": "", "error": "", "debug": "", "start": "", "browser": "", "email": "", "code": "", "save": "", "time": "", "wait": "", "account": "", "team": "", "auth": "", } def __init__(self, name: str = "app", use_color: bool = True, level: int = None, enable_file_log: bool = True): """初始化日志器 Args: name: 日志器名称 use_color: 是否使用颜色 (仅控制台) level: 日志级别 enable_file_log: 是否启用文件日志 """ self.name = name self.use_color = use_color self.enable_file_log = enable_file_log self._telegram_handler = None # Telegram 日志处理器 # 从环境变量读取日志级别,默认 INFO if level is None: env_level = os.environ.get("LOG_LEVEL", "INFO").upper() level_map = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR} level = level_map.get(env_level, logging.INFO) self.level = level self._setup_logger() def _setup_logger(self): """设置日志器""" self._logger = logging.getLogger(self.name) self._logger.setLevel(self.level) self._logger.handlers.clear() # 清除已有的处理器 # 控制台处理器 console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(self.level) if self.use_color: console_handler.setFormatter(ColoredFormatter()) else: console_handler.setFormatter(FileFormatter()) self._logger.addHandler(console_handler) # 文件处理器 (带轮转) if self.enable_file_log: try: _ensure_log_dir() file_handler = RotatingFileHandler( LOG_FILE, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT, encoding='utf-8' ) file_handler.setLevel(self.level) file_handler.setFormatter(FileFormatter()) self._logger.addHandler(file_handler) except Exception as e: # 文件日志初始化失败时继续使用控制台日志 print(f"[WARNING] 文件日志初始化失败: {e}") def _get_icon(self, icon: str = None) -> str: """获取图标""" if icon: return self.ICONS.get(icon, icon) return "" def enable_telegram_logging(self, callback, level: int = logging.INFO): """启用 Telegram 实时日志 Args: callback: 回调函数,接收 (message: str, level: str) 参数 level: 日志级别,默认 INFO """ if self._telegram_handler is None: self._telegram_handler = TelegramLogHandler(callback, level) self._logger.addHandler(self._telegram_handler) def disable_telegram_logging(self): """禁用 Telegram 实时日志""" if self._telegram_handler is not None: self._logger.removeHandler(self._telegram_handler) self._telegram_handler = None def is_telegram_logging_enabled(self) -> bool: """检查 Telegram 实时日志是否启用""" return self._telegram_handler is not None def info(self, msg: str, icon: str = None, indent: int = 0): """信息日志""" prefix = " " * indent extra = {'icon': self._get_icon(icon)} self._logger.info(f"{prefix}{msg}", extra=extra) def success(self, msg: str, indent: int = 0): """成功日志""" prefix = " " * indent extra = {'icon': self._get_icon("success"), 'is_success': True} self._logger.info(f"{prefix}{msg}", extra=extra) def highlight(self, msg: str, icon: str = None, indent: int = 0): """高亮日志 (蓝色)""" prefix = " " * indent extra = {'icon': self._get_icon(icon), 'is_highlight': True} self._logger.info(f"{prefix}{msg}", extra=extra) def warning(self, msg: str, indent: int = 0): """警告日志""" prefix = " " * indent extra = {'icon': self._get_icon("warning")} self._logger.warning(f"{prefix}{msg}", extra=extra) def error(self, msg: str, indent: int = 0): """错误日志""" prefix = " " * indent extra = {'icon': self._get_icon("error")} self._logger.error(f"{prefix}{msg}", extra=extra) def debug(self, msg: str, indent: int = 0): """调试日志""" prefix = " " * indent extra = {'icon': self._get_icon("debug")} self._logger.debug(f"{prefix}{msg}", extra=extra) def step(self, msg: str, indent: int = 0): """步骤日志 (INFO 级别)""" prefix = " " * indent extra = {'icon': ''} self._logger.info(f"{prefix}-> {msg}", extra=extra) def verbose(self, msg: str, indent: int = 0): """详细日志 (DEBUG 级别)""" prefix = " " * indent extra = {'icon': ''} self._logger.debug(f"{prefix}. {msg}", extra=extra) def progress(self, current: int, total: int, msg: str = ""): """进度日志""" pct = (current / total * 100) if total > 0 else 0 bar_len = 20 filled = int(bar_len * current / total) if total > 0 else 0 bar = "=" * filled + "-" * (bar_len - filled) extra = {'icon': ''} self._logger.info(f"[{bar}] {current}/{total} ({pct:.0f}%) {msg}", extra=extra) def progress_inline(self, msg: str): """内联进度 (覆盖当前行)""" print(f"\r{msg}" + " " * 10, end='', flush=True) def progress_clear(self): """清除内联进度""" print("\r" + " " * 50 + "\r", end='', flush=True) def countdown(self, seconds: int, msg: str = "等待"): """倒计时显示 (同一行更新) Args: seconds: 倒计时秒数 msg: 提示消息 """ import time for remaining in range(seconds, 0, -1): timestamp = datetime.now().strftime("%H:%M:%S") print(f"\r[{timestamp}] {msg} {remaining}s... ", end='', flush=True) time.sleep(1) self.progress_clear() def separator(self, char: str = "=", length: int = 60): """分隔线""" extra = {'icon': ''} self._logger.info(char * length, extra=extra) def header(self, title: str): """标题""" self.separator() extra = {'icon': ''} self._logger.info(f" {title}", extra=extra) self.separator() def section(self, title: str): """小节标题""" extra = {'icon': ''} self._logger.info("#" * 40, extra=extra) self._logger.info(f"# {title}", extra=extra) self._logger.info("#" * 40, extra=extra) # ==================== 配置日志辅助函数 ==================== def log_config_error(source: str, error: str, details: str = None): """记录配置加载错误 Args: source: 配置来源 (如 config.toml, team.json) error: 错误类型 details: 详细信息 """ msg = f"配置加载失败 [{source}]: {error}" if details: msg += f" - {details}" log.warning(msg) def log_config_warning(source: str, message: str): """记录配置警告 Args: source: 配置来源 message: 警告信息 """ log.warning(f"配置警告 [{source}]: {message}") def log_config_info(source: str, message: str): """记录配置信息 Args: source: 配置来源 message: 信息内容 """ log.info(f"配置 [{source}]: {message}") # 全局日志实例 log = Logger()