feat: 实现CutThenThink P0阶段核心功能

项目初始化
- 创建完整项目结构(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>
This commit is contained in:
congsh
2026-02-11 18:21:31 +08:00
commit c4a77f8aa4
79 changed files with 19412 additions and 0 deletions

409
src/utils/logger.py Normal file
View File

@@ -0,0 +1,409 @@
"""
日志工具模块
提供统一的日志配置和管理功能
"""
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)