poc/project/llmclipboard/llmclipboard/app.py

968 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import time
import configparser
from pynput import mouse
import win32clipboard
import win32con
from html2text import HTML2Text
import keyboard
import threading
import sys
import logging
from logging.handlers import RotatingFileHandler
from PyQt6.QtWidgets import QApplication, QWidget
from llmclipboard.gui import MainWindow
from llmclipboard.document_processor import DocumentProcessor
import io
from PIL import Image
import uuid
import re
import requests
# 重定向标准输出和标准错误到日志
class StreamToLogger:
def __init__(self, logger, level):
self.logger = logger
self.level = level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.level, line.rstrip())
def flush(self):
pass
class TextCaptureService:
def __init__(self):
# 设置日志记录
self.setup_logging()
# 加载配置
self.load_config()
self.running = True
self.last_right_click_time = 0
self._mouse_listener = None
self._keyboard_listener = None
# 加载AI配置
self.ai_config = self.load_ai_config()
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
# 初始化文档处理器,传入配置路径
self.doc_processor = DocumentProcessor(
ai_config=self.ai_config,
config_path=os.path.join(config_dir, 'categories.json')
)
self.logger.info(f"文档处理器已初始化,分类配置路径: {os.path.join(config_dir, 'categories.json')}")
def load_config(self):
try:
# 使用 ConfigParser 的 BasicInterpolation 而不是 ExtendedInterpolation
# 这样可以避免 % 符号的问题
self.config = configparser.ConfigParser(interpolation=configparser.BasicInterpolation())
# 确定配置文件路径
if getattr(sys, 'frozen', False):
# 打包环境 - 使用可执行文件所在目录
config_dir = os.path.dirname(sys.executable)
else:
# 开发环境 - 使用项目根目录
config_dir = os.path.dirname(os.path.dirname(__file__))
self.config_file = os.path.join(config_dir, 'config.ini')
self.logger.info(f"配置文件路径: {self.config_file}")
if os.path.exists(self.config_file):
self.config.read(self.config_file, encoding='utf-8')
self.logger.info(f"成功读取配置文件")
else:
self.logger.warning(f"配置文件不存在,将使用默认配置")
# 使用默认配置
if not self.config.has_section('Settings'):
self.config.add_section('Settings')
self.config.set('Settings', 'double_click_threshold', '0.3')
self.config.set('Settings', 'save_location', os.path.expanduser('~/Documents'))
# 保存默认配置
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
self.logger.info(f"已创建默认配置文件: {self.config_file}")
except Exception as save_error:
self.logger.error(f"创建默认配置文件失败: {save_error}")
# 获取设置值,处理可能的插值语法错误
try:
self.double_click_threshold = self.config.getfloat('Settings', 'double_click_threshold', fallback=0.3)
except (configparser.InterpolationError, ValueError) as e:
self.logger.error(f"读取double_click_threshold设置时出错: {e}")
self.double_click_threshold = 0.3
try:
save_location = self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents'))
# 检查路径是否存在,如果不存在则使用默认路径
if not os.path.exists(save_location):
self.logger.warning(f"保存路径不存在: {save_location},将使用默认路径")
save_location = os.path.expanduser('~/Documents')
self.save_location = save_location
except (configparser.InterpolationError, ValueError) as e:
self.logger.error(f"读取save_location设置时出错: {e}")
self.save_location = os.path.expanduser('~/Documents')
except Exception as e:
self.logger.error(f"加载配置时出错: {e}")
import traceback
traceback.print_exc()
# 使用默认值
self.double_click_threshold = 0.3
self.save_location = os.path.expanduser('~/Documents')
def load_ai_config(self):
try:
if not self.config.has_section('AI'):
self.config.add_section('AI')
self.config.set('AI', 'model_type', 'none')
self.config.set('AI', 'api_key', '')
self.config.set('AI', 'base_url', 'https://api.openai.com/v1')
self.config.set('AI', 'model', 'gpt-3.5-turbo')
# 保存默认AI配置
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
except Exception as e:
self.logger.error(f"保存默认AI配置失败: {e}")
# 读取AI配置处理可能的插值语法错误
try:
self.ai_model_type = self.config.get('AI', 'model_type', fallback='none')
except configparser.InterpolationError as e:
self.logger.error(f"读取model_type设置时出错: {e}")
self.ai_model_type = 'none'
try:
self.ai_api_key = self.config.get('AI', 'api_key', fallback='')
except configparser.InterpolationError as e:
self.logger.error(f"读取api_key设置时出错: {e}")
self.ai_api_key = ''
try:
self.ai_base_url = self.config.get('AI', 'base_url', fallback='https://api.openai.com/v1')
except configparser.InterpolationError as e:
self.logger.error(f"读取base_url设置时出错: {e}")
self.ai_base_url = 'https://api.openai.com/v1'
try:
self.ai_model = self.config.get('AI', 'model', fallback='gpt-3.5-turbo')
except configparser.InterpolationError as e:
self.logger.error(f"读取model设置时出错: {e}")
self.ai_model = 'gpt-3.5-turbo'
self.logger.info(f"AI配置已加载: 模型类型={self.ai_model_type}, 模型={self.ai_model}")
except Exception as e:
self.logger.error(f"加载AI配置时出错: {e}")
import traceback
traceback.print_exc()
# 使用默认值
self.ai_model_type = 'none'
self.ai_api_key = ''
self.ai_base_url = 'https://api.openai.com/v1'
self.ai_model = 'gpt-3.5-turbo'
ai_config = {
'model_type': self.ai_model_type,
'api_key': self.ai_api_key,
'base_url': self.ai_base_url,
'model': self.ai_model
}
return ai_config
def update_ai_config(self, new_config):
"""更新AI配置"""
try:
if not self.config.has_section('AI'):
self.config.add_section('AI')
# 更新配置
for key, value in new_config.items():
self.config.set('AI', key, str(value))
# 保存到文件
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
# 更新实例变量
self.ai_model_type = new_config.get('model_type', 'none')
self.ai_api_key = new_config.get('api_key', '')
self.ai_base_url = new_config.get('base_url', 'https://api.openai.com/v1')
self.ai_model = new_config.get('model', 'gpt-3.5-turbo')
self.logger.info(f"AI配置已更新: 模型类型={self.ai_model_type}, 模型={self.ai_model}")
return True
except Exception as e:
self.logger.error(f"更新AI配置时出错: {e}")
import traceback
traceback.print_exc()
return False
def setup_logging(self):
try:
# 获取应用程序根目录
if getattr(sys, 'frozen', False):
# 如果是打包后的环境
app_root = os.path.dirname(sys.executable)
else:
# 如果是开发环境
app_root = os.path.dirname(os.path.dirname(__file__))
# 设置日志目录
log_dir = os.path.join(app_root, 'logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 设置日志文件路径
log_file = os.path.join(log_dir, 'llmclipboard.log')
# 创建 RotatingFileHandler
file_handler = RotatingFileHandler(
log_file,
maxBytes=5*1024*1024, # 5MB
backupCount=3,
encoding='utf-8'
)
# 设置日志格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
# 移除所有现有的处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 添加新的处理器
root_logger.addHandler(file_handler)
# 创建应用程序日志记录器
self.logger = logging.getLogger(__name__)
# 重定向标准输出和标准错误到日志
sys.stdout = StreamToLogger(self.logger, logging.INFO)
sys.stderr = StreamToLogger(self.logger, logging.ERROR)
self.logger.info('日志系统初始化完成')
self.logger.info(f'日志文件路径: {log_file}')
except Exception as e:
# 如果日志设置失败,尝试写入到临时目录
import tempfile
temp_dir = tempfile.gettempdir()
fallback_log = os.path.join(temp_dir, 'llmclipboard_error.log')
with open(fallback_log, 'a', encoding='utf-8') as f:
f.write(f'{time.strftime("%Y-%m-%d %H:%M:%S")} - 日志初始化失败: {str(e)}\n')
def is_valid_content(self, content):
"""验证内容是否有效"""
if not content:
return False
# 检查是否是空的JSON对象
if content.strip().startswith('{') and content.strip().endswith('}'):
try:
import json
data = json.loads(content)
# 检查是否是空的excalidraw内容
if data.get('type') == 'excalidraw/clipboard' and not data.get('elements'):
self.logger.info("跳过空的excalidraw内容")
return False
except:
pass
# 检查内容是否全是空白字符
if not content.strip():
return False
# 检查内容是否太短
if len(content.strip()) < 5:
return False
return True
def extract_images_from_html(self, html_content):
"""从HTML内容中提取图片"""
import re
import requests
from io import BytesIO
from PIL import Image
# 查找所有图片标签
img_tags = re.findall(r'<img[^>]+src="([^"]+)"[^>]*>', html_content)
images = []
for img_url in img_tags:
try:
# 处理相对路径和绝对路径
if img_url.startswith('data:image'):
# 处理base64编码的图片
try:
import base64
# 提取MIME类型和base64数据
mime_type, base64_data = img_url.split(',', 1)
mime_type = mime_type.split(':')[1].split(';')[0]
# 解码base64数据
binary_data = base64.b64decode(base64_data)
# 创建PIL图像
img = Image.open(BytesIO(binary_data))
images.append(img)
self.logger.info(f"成功提取base64图片大小: {img.size}")
except Exception as e:
self.logger.error(f"处理base64图片失败: {e}")
elif img_url.startswith(('http://', 'https://')):
# 下载网络图片
response = requests.get(img_url, stream=True, timeout=5)
if response.status_code == 200:
img = Image.open(BytesIO(response.content))
images.append(img)
self.logger.info(f"成功下载网络图片: {img_url}")
else:
self.logger.warning(f"下载图片失败: {img_url}, 状态码: {response.status_code}")
else:
self.logger.warning(f"不支持的图片URL格式: {img_url}")
except Exception as e:
self.logger.error(f"处理图片URL失败: {img_url}, 错误: {e}")
return images
def get_clipboard_content(self):
"""获取剪贴板内容"""
try:
win32clipboard.OpenClipboard()
content = None
# 检查是否有图片格式
try:
if win32clipboard.IsClipboardFormatAvailable(win32con.CF_DIB):
self.logger.info("检测到图片格式内容")
# 获取图片数据
image_data = win32clipboard.GetClipboardData(win32con.CF_DIB)
# 转换为PIL图像
try:
from io import BytesIO
# 创建BMP文件头
offset = 14 # BMP文件头大小
header_size = 40 # BITMAPINFOHEADER大小
# 从DIB数据中提取宽度和高度
width = int.from_bytes(image_data[4:8], byteorder='little')
height = int.from_bytes(image_data[8:12], byteorder='little')
# 计算文件大小
file_size = offset + len(image_data)
# 创建BMP文件头
bmp_header = b'BM' + \
file_size.to_bytes(4, byteorder='little') + \
b'\x00\x00\x00\x00' + \
offset.to_bytes(4, byteorder='little')
# 组合BMP文件
bmp_data = bmp_header + image_data
# 转换为PIL图像
image = Image.open(BytesIO(bmp_data))
# 返回图片内容
return {"type": "image", "data": image}
except Exception as e:
self.logger.error(f"处理图片失败: {e}")
# 如果图片处理失败,继续尝试其他格式
except Exception as e:
self.logger.debug(f"获取图片失败: {e}")
# 首先尝试获取Unicode文本
try:
content = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT)
self.logger.info("获取Unicode文本格式内容")
if content and self.is_valid_content(content):
return {"type": "text", "data": content}
except Exception as e:
self.logger.debug(f"获取Unicode文本失败: {e}")
# 尝试获取HTML格式
try:
CF_HTML = win32clipboard.RegisterClipboardFormat("HTML Format")
html_content = win32clipboard.GetClipboardData(CF_HTML)
self.logger.info("获取HTML格式内容")
if html_content:
# 从HTML字符串中提取实际的HTML内容
try:
if isinstance(html_content, bytes):
html_content = html_content.decode('utf-8')
# 查找HTML内容的开始和结束
start = html_content.find('<html>')
if start == -1:
start = html_content.find('<!--StartFragment-->')
if start != -1:
start = html_content.find('<', start + 20)
end = html_content.find('</html>')
if end == -1:
end = html_content.find('<!--EndFragment-->')
if start != -1:
if end != -1:
html_content = html_content[start:end]
else:
html_content = html_content[start:]
# 提取HTML中的图片
images = self.extract_images_from_html(html_content)
if images:
self.logger.info(f"从HTML中提取到 {len(images)} 张图片")
# 如果只有一张图片且没有其他内容,直接返回图片
if len(images) == 1:
return {"type": "image", "data": images[0]}
# 否则返回混合内容
else:
# 解析HTML格式的内容为文本
h = HTML2Text()
h.body_width = 0 # 禁用自动换行
h.single_line_break = True # 使用单行换行
h.ignore_emphasis = False
h.ignore_images = False
h.ignore_links = False
h.ignore_tables = False
text_content = h.handle(html_content).strip()
if text_content and self.is_valid_content(text_content):
return {
"type": "mixed",
"data": text_content,
"images": images
}
else:
# 如果文本内容无效,但有多张图片,返回第一张图片
return {"type": "image", "data": images[0]}
except Exception as e:
self.logger.debug(f"HTML内容提取失败: {e}")
# 如果没有提取到图片或提取失败使用传统方法处理HTML
h = HTML2Text()
h.body_width = 0 # 禁用自动换行
h.single_line_break = True # 使用单行换行
h.ignore_emphasis = False
h.ignore_images = False
h.ignore_links = False
h.ignore_tables = False
content = h.handle(html_content).strip()
if content and self.is_valid_content(content):
return {"type": "text", "data": content}
except Exception as e:
self.logger.debug(f"获取HTML格式失败: {e}")
# 如果HTML获取失败尝试获取普通文本
try:
text_content = win32clipboard.GetClipboardData(win32con.CF_TEXT)
self.logger.info("获取普通文本格式内容")
if text_content:
if isinstance(text_content, bytes):
text_content = text_content.decode('gbk', errors='ignore')
content = text_content
if self.is_valid_content(content):
return {"type": "text", "data": content}
except Exception as e:
self.logger.debug(f"获取普通文本失败: {e}")
if not content or not self.is_valid_content(content):
self.logger.warning("剪贴板内容为空或无效")
if hasattr(self, 'gui') and self.gui:
self.gui.show_message("剪贴板内容为空或无效", error=True)
return None
return {"type": "text", "data": content}
except Exception as e:
self.logger.error(f"获取剪贴板内容失败: {e}")
if hasattr(self, 'gui') and self.gui:
self.gui.show_message(f"获取剪贴板内容失败: {str(e)}", error=True)
return None
finally:
try:
win32clipboard.CloseClipboard()
except:
pass
def process_content_in_thread(self, content):
"""在线程中处理内容避免阻塞UI"""
def worker():
try:
if content.get('type') == 'image':
self.save_image(content['data'])
elif content.get('type') == 'mixed':
# 保存文本内容
md_path = self.save_to_markdown(content['data'])
# 保存图片并添加到Markdown文件
if md_path and content.get('images'):
self.append_images_to_markdown(md_path, content['images'])
else:
self.save_to_markdown(content['data'])
except Exception as e:
self.logger.error(f"处理内容失败: {e}")
if hasattr(self, 'gui') and self.gui:
self.gui.show_message(f"处理内容失败: {str(e)}", error=True)
# 创建并启动线程
thread = threading.Thread(target=worker)
thread.daemon = True
thread.start()
def on_click(self, x, y, button, pressed):
if not pressed or not self.running:
return True
if button == mouse.Button.right:
current_time = time.time()
if (current_time - self.last_right_click_time) < self.double_click_threshold:
self.logger.info("检测到双击右键")
# 先执行复制操作
self.simulate_copy()
time.sleep(0.1) # 等待复制完成
# 获取剪贴板内容
content = self.get_clipboard_content()
if content:
self.logger.info(f"开始保存内容,类型: {content.get('type', 'unknown')}")
# 在线程中处理内容避免阻塞UI
self.process_content_in_thread(content)
else:
self.logger.warning("没有可保存的内容")
self.last_right_click_time = current_time
return True
def save_image(self, image):
"""保存图片并创建Markdown引用"""
try:
# 确保保存目录存在
image_dir = os.path.join(self.save_location, "图片")
if not os.path.exists(image_dir):
os.makedirs(image_dir)
self.logger.info(f"创建图片保存目录: {image_dir}")
# 生成文件名
timestamp = time.strftime("%Y%m%d_%H%M%S")
image_filename = f"image_{timestamp}.png"
image_path = os.path.join(image_dir, image_filename)
# 保存图片
image.save(image_path)
# 获取图片文件大小和尺寸信息
file_size = os.path.getsize(image_path)
file_size_str = self.format_file_size(file_size)
img_width, img_height = image.size
self.logger.info(f"图片已保存到: {image_path}")
self.logger.info(f"图片大小: {file_size_str}")
self.logger.info(f"图片尺寸: {img_width}x{img_height}")
# 创建Markdown内容
md_content = f"""---
title: 图片 {timestamp}
date: {time.strftime("%Y-%m-%d %H:%M:%S")}
tags: 图片
category: 图片
size: {file_size_str}
dimensions: {img_width}x{img_height}
---
![图片](./{image_filename})
"""
# 保存Markdown文件
md_path = os.path.join(image_dir, f"image_{timestamp}.md")
with open(md_path, 'w', encoding='utf-8') as f:
f.write(md_content)
self.logger.info(f"Markdown文件已保存到: {md_path}")
# 构建详细的通知消息
notification_message = f"图片尺寸: {img_width}x{img_height}\n"
notification_message += f"文件大小: {file_size_str}\n"
notification_message += f"保存到: {md_path}"
# 在GUI中显示保存成功的消息
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_notification(
"图片已保存",
notification_message,
"info",
["打开文件", "打开文件夹"],
md_path
)
except Exception as e:
# 如果通知显示失败,记录错误并尝试使用基本消息
self.logger.error(f"显示图片保存通知失败: {str(e)}")
try:
self.gui.show_message(f"图片已保存到: {md_path}")
except Exception as e2:
self.logger.error(f"显示基本消息也失败: {str(e2)}")
return md_path
except Exception as e:
error_msg = f"保存图片失败: {str(e)}"
self.logger.error(error_msg)
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_message(error_msg, error=True)
except Exception as e2:
self.logger.error(f"显示错误消息失败: {str(e2)}")
return None
def append_images_to_markdown(self, md_path, images):
"""将图片添加到已有的Markdown文件中"""
try:
if not os.path.exists(md_path):
self.logger.error(f"Markdown文件不存在: {md_path}")
return
# 获取Markdown文件所在目录
md_dir = os.path.dirname(md_path)
# 读取原始内容
with open(md_path, 'r', encoding='utf-8') as f:
original_content = f.read()
# 准备添加的图片内容
image_content = "\n\n## 相关图片\n\n"
# 图片信息汇总
total_size = 0
image_info_list = []
# 保存每张图片并添加引用
for i, image in enumerate(images):
try:
# 生成图片文件名
timestamp = time.strftime("%Y%m%d_%H%M%S")
image_filename = f"image_{timestamp}_{i+1}.png"
image_path = os.path.join(md_dir, image_filename)
# 保存图片
image.save(image_path)
# 获取图片信息
file_size = os.path.getsize(image_path)
total_size += file_size
img_width, img_height = image.size
# 记录图片信息
image_info = {
'path': image_path,
'size': file_size,
'size_str': self.format_file_size(file_size),
'dimensions': f"{img_width}x{img_height}"
}
image_info_list.append(image_info)
self.logger.info(f"附加图片已保存到: {image_path}")
self.logger.info(f"图片大小: {image_info['size_str']}")
self.logger.info(f"图片尺寸: {image_info['dimensions']}")
# 添加图片引用
image_content += f"![图片{i+1}](./{image_filename}) *{image_info['dimensions']} - {image_info['size_str']}*\n\n"
except Exception as e:
self.logger.error(f"保存附加图片失败: {e}")
# 更新Markdown文件
with open(md_path, 'w', encoding='utf-8') as f:
f.write(original_content + image_content)
self.logger.info(f"已将 {len(images)} 张图片添加到Markdown文件: {md_path}")
self.logger.info(f"总图片大小: {self.format_file_size(total_size)}")
# 构建通知消息
if image_info_list:
notification_message = f"已添加 {len(images)} 张图片到文档\n"
notification_message += f"总大小: {self.format_file_size(total_size)}\n"
for i, info in enumerate(image_info_list[:3]): # 只显示前3张图片的信息
notification_message += f"图片{i+1}: {info['dimensions']} - {info['size_str']}\n"
if len(image_info_list) > 3:
notification_message += f"... 以及 {len(image_info_list) - 3} 张其他图片\n"
notification_message += f"文件: {md_path}"
# 显示通知
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_notification(
"图片已添加到文档",
notification_message,
"info",
["打开文件", "打开文件夹"],
md_path
)
except Exception as e:
self.logger.error(f"显示图片添加通知失败: {str(e)}")
try:
self.gui.show_message(f"已添加 {len(images)} 张图片到: {md_path}")
except Exception as e2:
self.logger.error(f"显示基本消息也失败: {str(e2)}")
except Exception as e:
error_msg = f"添加图片到Markdown文件失败: {e}"
self.logger.error(error_msg)
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_message(error_msg, error=True)
except Exception as e2:
self.logger.error(f"显示错误消息失败: {str(e2)}")
def save_to_markdown(self, content):
"""保存内容到Markdown文件"""
try:
# 确保保存目录存在
if not os.path.exists(self.save_location):
os.makedirs(self.save_location)
self.logger.info(f"创建保存目录: {self.save_location}")
# 使用文档处理器处理内容
result = self.doc_processor.process_document(content, self.save_location)
# 保存处理后的内容
with open(result['path'], 'w', encoding='utf-8') as f:
f.write(result['content'])
# 获取文件大小
file_size = os.path.getsize(result['path'])
file_size_str = self.format_file_size(file_size)
self.logger.info(f"文件保存成功: {result['path']}")
self.logger.info(f"文件大小: {file_size_str}")
self.logger.info(f"标题: {result['title']}")
self.logger.info(f"分类: {result['category']}")
self.logger.info(f"标签: {', '.join(result['tags'])}")
# 构建详细的通知消息
notification_message = f"标题: {result['title']}\n"
notification_message += f"分类: {result['category']}\n"
notification_message += f"大小: {file_size_str}\n"
notification_message += f"标签: {', '.join(result['tags'])}\n"
notification_message += f"保存到: {result['path']}"
# 在GUI中显示保存成功的消息和操作选项
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_notification(
"内容已保存",
notification_message,
"info",
["打开文件", "打开文件夹"],
result['path']
)
except Exception as e:
# 如果通知显示失败,记录错误并尝试使用基本消息
self.logger.error(f"显示通知失败: {str(e)}")
try:
self.gui.show_message(f"内容已保存到: {result['path']}")
except Exception as e2:
self.logger.error(f"显示基本消息也失败: {str(e2)}")
return result['path']
except Exception as e:
error_msg = f"保存文件失败: {str(e)}"
self.logger.error(error_msg)
if hasattr(self, 'gui') and self.gui:
try:
self.gui.show_message(error_msg, error=True)
except Exception as e2:
self.logger.error(f"显示错误消息失败: {str(e2)}")
return None
def format_file_size(self, size_bytes):
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes/1024:.1f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes/(1024*1024):.1f} MB"
else:
return f"{size_bytes/(1024*1024*1024):.1f} GB"
def simulate_copy(self):
keyboard.press('ctrl')
keyboard.press('c')
keyboard.release('c')
keyboard.release('ctrl')
time.sleep(0.1) # 等待复制操作完成
def start(self):
"""启动文本捕获服务"""
try:
if self.running and self._mouse_listener is not None:
self.logger.warning("服务已经在运行中,忽略重复启动请求")
return
self.logger.info("正在启动文本捕获服务...")
self.running = True
# 创建并启动鼠标监听器
try:
self._mouse_listener = mouse.Listener(on_click=self.on_click)
self._mouse_listener.start()
self.logger.info("鼠标监听器启动成功")
except Exception as e:
self.logger.error(f"启动鼠标监听器失败: {e}")
self.running = False
raise Exception(f"启动鼠标监听器失败: {e}")
self.logger.info("文本捕获服务启动成功")
except Exception as e:
self.logger.error(f"启动文本捕获服务失败: {e}")
self.running = False
# 确保清理任何可能已创建的资源
if self._mouse_listener:
try:
self._mouse_listener.stop()
except:
pass
self._mouse_listener = None
raise Exception(f"启动文本捕获服务失败: {e}")
def stop(self):
"""停止文本捕获服务"""
try:
if not self.running:
self.logger.warning("服务已经停止,忽略重复停止请求")
return
self.logger.info("正在停止文本捕获服务...")
self.running = False
# 停止鼠标监听器
if self._mouse_listener:
try:
self._mouse_listener.stop()
self.logger.info("鼠标监听器停止成功")
except Exception as e:
self.logger.error(f"停止鼠标监听器失败: {e}")
finally:
self._mouse_listener = None
# 停止键盘监听器(如果存在)
if self._keyboard_listener:
try:
self._keyboard_listener.stop()
self.logger.info("键盘监听器停止成功")
except Exception as e:
self.logger.error(f"停止键盘监听器失败: {e}")
finally:
self._keyboard_listener = None
self.logger.info("文本捕获服务停止成功")
except Exception as e:
self.logger.error(f"停止文本捕获服务失败: {e}")
# 确保设置状态为已停止,即使发生异常
self.running = False
self._mouse_listener = None
self._keyboard_listener = None
# 全局变量
app = None
window = None
service = None
tray_icon = None # 添加全局变量保存托盘图标引用
def main():
global app, window, service, tray_icon
try:
# 创建QApplication实例
app = QApplication(sys.argv)
# 设置应用程序属性,确保所有窗口关闭后应用程序不会退出
app.setQuitOnLastWindowClosed(False)
# 创建主窗口
from llmclipboard.gui import MainWindow
window = MainWindow()
# 保存服务实例的引用
service = window.service
# 保存系统托盘图标的引用
tray_icon = window.tray_icon
# 确保系统托盘图标可见
if tray_icon is not None and not tray_icon.isVisible():
print("确保系统托盘图标可见")
tray_icon.show()
# 确保tray_icon不会被垃圾回收在应用程序退出前调用cleanup
app.aboutToQuit.connect(cleanup)
# 显示主窗口
window.show()
# 运行应用程序
return app.exec()
except Exception as e:
print(f"Application error: {str(e)}")
logging.error(f"Application error: {str(e)}", exc_info=True)
# 在打包环境中,确保错误信息被记录
if getattr(sys, 'frozen', False):
import traceback
error_log_path = os.path.join(os.path.dirname(sys.executable), 'error_log.txt')
with open(error_log_path, 'a', encoding='utf-8') as f:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Error: {str(e)}\n")
traceback.print_exc(file=f)
return 1
def cleanup():
"""清理资源,确保应用程序正确退出"""
global tray_icon, service
print("Cleaning up resources...")
# 停止服务
if service is not None and hasattr(service, 'running') and service.running:
print("Stopping service...")
service.stop()
# 隐藏并销毁系统托盘图标
if tray_icon is not None:
print("Hiding tray icon...")
tray_icon.hide()
tray_icon.setVisible(False)
tray_icon = None
if __name__ == "__main__":
# 使用try-except块捕获可能的异常
try:
sys.exit(main())
except Exception as e:
print(f"Application error: {e}")
sys.exit(1)