This commit is contained in:
2026-04-19 16:29:59 +08:00
22 changed files with 2060 additions and 916 deletions

113
utils/logger.py Normal file
View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""
统一日志模块 - 替代全局 sys.stdout 劫持
提供线程安全的日志记录,支持同时输出到终端和文件。
每个会话拥有独立的日志文件,不会互相干扰。
"""
import logging
import os
import sys
from datetime import datetime
from typing import Optional
def create_session_logger(
session_id: str,
log_dir: str,
log_filename: str = "process.log",
level: int = logging.INFO,
) -> logging.Logger:
"""
为指定会话创建独立的 Logger 实例。
Args:
session_id: 会话唯一标识
log_dir: 日志文件所在目录
log_filename: 日志文件名
level: 日志级别
Returns:
配置好的 Logger 实例
"""
logger = logging.getLogger(f"session.{session_id}")
logger.setLevel(level)
# 避免重复添加 handler
if logger.handlers:
return logger
formatter = logging.Formatter(
fmt="%(asctime)s %(message)s",
datefmt="%H:%M:%S",
)
# 文件 handler — 写入会话专属日志
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, log_filename)
file_handler = logging.FileHandler(log_path, encoding="utf-8", mode="a")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# 终端 handler — 输出到 stderr不干扰 stdout
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# 不向父 logger 传播
logger.propagate = False
return logger
class PrintCapture:
"""
轻量级 print 捕获器,将 print 输出同时写入日志文件。
用于兼容现有大量使用 print() 的代码,无需逐行改造。
用法:
with PrintCapture(log_path) as cap:
print("hello") # 同时输出到终端和文件
# 退出后 sys.stdout 自动恢复
"""
def __init__(self, log_path: str, filter_patterns: Optional[list] = None):
self.log_path = log_path
self.filter_patterns = filter_patterns or ["[TOOL] 执行代码:"]
self._original_stdout = None
self._log_file = None
def __enter__(self):
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
self._original_stdout = sys.stdout
self._log_file = open(self.log_path, "a", encoding="utf-8", buffering=1)
sys.stdout = self._DualWriter(
self._original_stdout, self._log_file, self.filter_patterns
)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout = self._original_stdout
if self._log_file:
self._log_file.close()
return False
class _DualWriter:
"""同时写入两个流,支持过滤"""
def __init__(self, terminal, log_file, filter_patterns):
self.terminal = terminal
self.log_file = log_file
self.filter_patterns = filter_patterns
def write(self, message):
self.terminal.write(message)
# 过滤不需要写入日志的内容
if any(p in message for p in self.filter_patterns):
return
self.log_file.write(message)
def flush(self):
self.terminal.flush()
self.log_file.flush()