项目初始化 - 创建完整项目结构(src/, data/, docs/, examples/, tests/) - 配置requirements.txt依赖 - 创建.gitignore P0基础框架 - 数据库模型:Record模型,6种分类类型 - 配置管理:YAML配置,支持AI/OCR/云存储/UI配置 - OCR模块:PaddleOCR本地识别,支持云端扩展 - AI模块:支持OpenAI/Claude/通义/Ollama,6种分类 - 存储模块:完整CRUD,搜索,统计,导入导出 - 主窗口框架:侧边栏导航,米白配色方案 - 图片处理:截图/剪贴板/文件选择/图片预览 - 处理流程整合:OCR→AI→存储串联,Markdown展示,剪贴板复制 - 分类浏览:卡片网格展示,分类筛选,搜索,详情查看 技术栈 - PyQt6 + SQLAlchemy + PaddleOCR + OpenAI/Claude SDK - 共47个Python文件,4000+行代码 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
410 lines
10 KiB
Python
410 lines
10 KiB
Python
"""
|
||
日志工具模块
|
||
|
||
提供统一的日志配置和管理功能
|
||
"""
|
||
|
||
import logging
|
||
import sys
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||
from typing import Optional
|
||
|
||
|
||
class ColoredFormatter(logging.Formatter):
|
||
"""
|
||
彩色日志格式化器
|
||
|
||
为不同级别的日志添加颜色
|
||
"""
|
||
|
||
# ANSI 颜色代码
|
||
COLORS = {
|
||
'DEBUG': '\033[36m', # 青色
|
||
'INFO': '\033[32m', # 绿色
|
||
'WARNING': '\033[33m', # 黄色
|
||
'ERROR': '\033[31m', # 红色
|
||
'CRITICAL': '\033[35m', # 紫色
|
||
}
|
||
RESET = '\033[0m'
|
||
|
||
def format(self, record):
|
||
# 添加颜色
|
||
if record.levelname in self.COLORS:
|
||
record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.RESET}"
|
||
|
||
return super().format(record)
|
||
|
||
|
||
class LoggerManager:
|
||
"""
|
||
日志管理器
|
||
|
||
负责配置和管理应用程序日志
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
name: str = "CutThenThink",
|
||
log_dir: Optional[Path] = None,
|
||
level: str = "INFO",
|
||
console_output: bool = True,
|
||
file_output: bool = True,
|
||
colored_console: bool = True
|
||
):
|
||
"""
|
||
初始化日志管理器
|
||
|
||
Args:
|
||
name: 日志器名称
|
||
log_dir: 日志文件目录
|
||
level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||
console_output: 是否输出到控制台
|
||
file_output: 是否输出到文件
|
||
colored_console: 控制台是否使用彩色输出
|
||
"""
|
||
self.name = name
|
||
self.log_dir = log_dir
|
||
self.level = getattr(logging, level.upper(), logging.INFO)
|
||
self.console_output = console_output
|
||
self.file_output = file_output
|
||
self.colored_console = colored_console
|
||
|
||
self.logger: Optional[logging.Logger] = None
|
||
|
||
def setup(self) -> logging.Logger:
|
||
"""
|
||
设置日志系统
|
||
|
||
Returns:
|
||
配置好的 Logger 对象
|
||
"""
|
||
if self.logger is not None:
|
||
return self.logger
|
||
|
||
# 创建日志器
|
||
self.logger = logging.getLogger(self.name)
|
||
self.logger.setLevel(self.level)
|
||
self.logger.handlers.clear() # 清除已有的处理器
|
||
|
||
# 日志格式
|
||
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
date_format = '%Y-%m-%d %H:%M:%S'
|
||
|
||
# 控制台处理器
|
||
if self.console_output:
|
||
console_handler = logging.StreamHandler(sys.stdout)
|
||
console_handler.setLevel(self.level)
|
||
|
||
if self.colored_console:
|
||
console_formatter = ColoredFormatter(log_format, datefmt=date_format)
|
||
else:
|
||
console_formatter = logging.Formatter(log_format, datefmt=date_format)
|
||
|
||
console_handler.setFormatter(console_formatter)
|
||
self.logger.addHandler(console_handler)
|
||
|
||
# 文件处理器
|
||
if self.file_output and self.log_dir:
|
||
# 确保日志目录存在
|
||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 主日志文件(按大小轮转)
|
||
log_file = self.log_dir / f"{self.name}.log"
|
||
file_handler = RotatingFileHandler(
|
||
log_file,
|
||
maxBytes=10 * 1024 * 1024, # 10MB
|
||
backupCount=5,
|
||
encoding='utf-8'
|
||
)
|
||
file_handler.setLevel(self.level)
|
||
file_formatter = logging.Formatter(log_format, datefmt=date_format)
|
||
file_handler.setFormatter(file_formatter)
|
||
self.logger.addHandler(file_handler)
|
||
|
||
# 错误日志文件(单独记录错误和严重错误)
|
||
error_file = self.log_dir / f"{self.name}_error.log"
|
||
error_handler = RotatingFileHandler(
|
||
error_file,
|
||
maxBytes=10 * 1024 * 1024, # 10MB
|
||
backupCount=5,
|
||
encoding='utf-8'
|
||
)
|
||
error_handler.setLevel(logging.ERROR)
|
||
error_formatter = logging.Formatter(log_format, datefmt=date_format)
|
||
error_handler.setFormatter(error_formatter)
|
||
self.logger.addHandler(error_handler)
|
||
|
||
return self.logger
|
||
|
||
def get_logger(self) -> logging.Logger:
|
||
"""
|
||
获取日志器
|
||
|
||
Returns:
|
||
Logger 对象
|
||
"""
|
||
if self.logger is None:
|
||
return self.setup()
|
||
return self.logger
|
||
|
||
def set_level(self, level: str):
|
||
"""
|
||
动态设置日志级别
|
||
|
||
Args:
|
||
level: 日志级别字符串
|
||
"""
|
||
log_level = getattr(logging, level.upper(), logging.INFO)
|
||
self.logger.setLevel(log_level)
|
||
for handler in self.logger.handlers:
|
||
handler.setLevel(log_level)
|
||
|
||
|
||
# 全局日志管理器
|
||
_global_logger_manager: Optional[LoggerManager] = None
|
||
|
||
|
||
def init_logger(
|
||
log_dir: Optional[Path] = None,
|
||
level: str = "INFO",
|
||
console_output: bool = True,
|
||
file_output: bool = True,
|
||
colored_console: bool = True
|
||
) -> logging.Logger:
|
||
"""
|
||
初始化全局日志系统
|
||
|
||
Args:
|
||
log_dir: 日志目录
|
||
level: 日志级别
|
||
console_output: 是否输出到控制台
|
||
file_output: 是否输出到文件
|
||
colored_console: 控制台是否彩色
|
||
|
||
Returns:
|
||
Logger 对象
|
||
"""
|
||
global _global_logger_manager
|
||
|
||
# 默认日志目录
|
||
if log_dir is None:
|
||
project_root = Path(__file__).parent.parent.parent
|
||
log_dir = project_root / "logs"
|
||
|
||
_global_logger_manager = LoggerManager(
|
||
name="CutThenThink",
|
||
log_dir=log_dir,
|
||
level=level,
|
||
console_output=console_output,
|
||
file_output=file_output,
|
||
colored_console=colored_console
|
||
)
|
||
|
||
return _global_logger_manager.setup()
|
||
|
||
|
||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||
"""
|
||
获取日志器
|
||
|
||
Args:
|
||
name: 日志器名称,如果为 None 则返回全局日志器
|
||
|
||
Returns:
|
||
Logger 对象
|
||
"""
|
||
if _global_logger_manager is None:
|
||
init_logger()
|
||
|
||
if name is None:
|
||
return _global_logger_manager.get_logger()
|
||
|
||
# 返回指定名称的子日志器
|
||
return logging.getLogger(f"CutThenThink.{name}")
|
||
|
||
|
||
class LogCapture:
|
||
"""
|
||
日志捕获器
|
||
|
||
用于捕获日志并显示在 GUI 中
|
||
"""
|
||
|
||
def __init__(self, max_entries: int = 1000):
|
||
"""
|
||
初始化日志捕获器
|
||
|
||
Args:
|
||
max_entries: 最大保存条目数
|
||
"""
|
||
self.max_entries = max_entries
|
||
self.entries = []
|
||
self.callbacks = []
|
||
|
||
def add_entry(self, level: str, message: str, timestamp: Optional[datetime] = None):
|
||
"""
|
||
添加日志条目
|
||
|
||
Args:
|
||
level: 日志级别
|
||
message: 日志消息
|
||
timestamp: 时间戳
|
||
"""
|
||
if timestamp is None:
|
||
timestamp = datetime.now()
|
||
|
||
entry = {
|
||
'timestamp': timestamp,
|
||
'level': level,
|
||
'message': message
|
||
}
|
||
|
||
self.entries.append(entry)
|
||
|
||
# 限制条目数量
|
||
if len(self.entries) > self.max_entries:
|
||
self.entries = self.entries[-self.max_entries:]
|
||
|
||
# 触发回调
|
||
for callback in self.callbacks:
|
||
callback(entry)
|
||
|
||
def register_callback(self, callback):
|
||
"""
|
||
注册回调函数
|
||
|
||
Args:
|
||
callback: 回调函数,接收 entry 参数
|
||
"""
|
||
self.callbacks.append(callback)
|
||
|
||
def clear(self):
|
||
"""清空日志"""
|
||
self.entries.clear()
|
||
|
||
def get_entries(self, level: Optional[str] = None, limit: Optional[int] = None) -> list:
|
||
"""
|
||
获取日志条目
|
||
|
||
Args:
|
||
level: 过滤级别,None 表示不过滤
|
||
limit: 限制数量
|
||
|
||
Returns:
|
||
日志条目列表
|
||
"""
|
||
entries = self.entries
|
||
|
||
if level is not None:
|
||
entries = [e for e in entries if e['level'] == level]
|
||
|
||
if limit is not None:
|
||
entries = entries[-limit:]
|
||
|
||
return entries
|
||
|
||
def get_latest(self, count: int = 10) -> list:
|
||
"""
|
||
获取最新的 N 条日志
|
||
|
||
Args:
|
||
count: 数量
|
||
|
||
Returns:
|
||
日志条目列表
|
||
"""
|
||
return self.entries[-count:]
|
||
|
||
|
||
# 全局日志捕获器
|
||
_log_capture: Optional[LogCapture] = None
|
||
|
||
|
||
def get_log_capture() -> LogCapture:
|
||
"""
|
||
获取全局日志捕获器
|
||
|
||
Returns:
|
||
LogCapture 对象
|
||
"""
|
||
global _log_capture
|
||
if _log_capture is None:
|
||
_log_capture = LogCapture()
|
||
return _log_capture
|
||
|
||
|
||
class LogHandler(logging.Handler):
|
||
"""
|
||
自定义日志处理器
|
||
|
||
将日志发送到 LogCapture
|
||
"""
|
||
|
||
def __init__(self, capture: LogCapture):
|
||
super().__init__()
|
||
self.capture = capture
|
||
|
||
def emit(self, record):
|
||
"""
|
||
发出日志记录
|
||
|
||
Args:
|
||
record: 日志记录
|
||
"""
|
||
try:
|
||
message = self.format(record)
|
||
level = record.levelname
|
||
timestamp = datetime.fromtimestamp(record.created)
|
||
|
||
self.capture.add_entry(level, message, timestamp)
|
||
|
||
except Exception:
|
||
self.handleError(record)
|
||
|
||
|
||
def setup_gui_logging(capture: Optional[LogCapture] = None):
|
||
"""
|
||
设置 GUI 日志捕获
|
||
|
||
Args:
|
||
capture: 日志捕获器,如果为 None 则使用全局捕获器
|
||
"""
|
||
if capture is None:
|
||
capture = get_log_capture()
|
||
|
||
# 创建处理器
|
||
handler = LogHandler(capture)
|
||
handler.setLevel(logging.INFO)
|
||
handler.setFormatter(logging.Formatter('%(message)s'))
|
||
|
||
# 添加到根日志器
|
||
logging.getLogger("CutThenThink").addHandler(handler)
|
||
|
||
|
||
# 便捷函数
|
||
def log_debug(message: str):
|
||
"""记录 DEBUG 日志"""
|
||
get_logger().debug(message)
|
||
|
||
|
||
def log_info(message: str):
|
||
"""记录 INFO 日志"""
|
||
get_logger().info(message)
|
||
|
||
|
||
def log_warning(message: str):
|
||
"""记录 WARNING 日志"""
|
||
get_logger().warning(message)
|
||
|
||
|
||
def log_error(message: str, exc_info: bool = False):
|
||
"""记录 ERROR 日志"""
|
||
get_logger().error(message, exc_info=exc_info)
|
||
|
||
|
||
def log_critical(message: str, exc_info: bool = False):
|
||
"""记录 CRITICAL 日志"""
|
||
get_logger().critical(message, exc_info=exc_info)
|