diff --git a/project/llmclipboard/CHANGELOG-v0.1.7.md b/project/llmclipboard/CHANGELOG-v0.1.7.md new file mode 100644 index 0000000..c0e0512 --- /dev/null +++ b/project/llmclipboard/CHANGELOG-v0.1.7.md @@ -0,0 +1,97 @@ +# LLMClipboard 托盘图标状态管理改进总结 + +## 1. 问题背景 + +在之前的版本中,LLMClipboard 应用程序的系统托盘图标在服务状态变化(启动/停止监听)时没有提供足够明显的视觉反馈。具体问题包括: + +- 启动监听后托盘图标不变化,用户无法直观判断应用程序状态 +- 缺少活动状态的专用图标文件 +- 图标切换逻辑不完善,在某些环境下不能正确更新 + +## 2. 改进内容 + +### 2.1 活动状态图标创建 + +创建了专用的活动状态图标 `icon_active.png`,通过以下特点与非活动状态图标区分: +- 增加亮度 (30%) +- 增加对比度 (20%) +- 增加颜色饱和度 (50%) + +这使得活动状态下的图标更加明亮、鲜艳,提供更明显的视觉反馈。 + +### 2.2 图标切换逻辑优化 + +优化了 `toggle_monitoring` 方法中的图标切换逻辑: +- 在启动监听时切换到活动状态图标 +- 在停止监听时切换到非活动状态图标 +- 添加了图标可见性检查,确保托盘图标始终可见 +- 更新了图标提示文本,提供状态信息 + +### 2.3 图标刷新机制增强 + +添加了强制刷新图标的机制,解决某些环境下图标不更新的问题: +- 使用 `hide()` 和 `show()` 方法强制刷新图标 +- 调用 `QApplication.processEvents()` 确保 UI 更新 +- 在启动监听、停止监听和异常处理时都添加了图标刷新逻辑 + +### 2.4 错误处理增强 + +完善了图标操作的错误处理: +- 添加了 try-except 块捕获可能的异常 +- 添加了详细的错误日志记录 +- 实现了错误恢复机制,确保即使出现异常也不会影响应用程序整体功能 + +## 3. 技术实现 + +### 3.1 图标创建工具 + +创建了 `create_active_icon.py` 脚本,用于生成活动状态图标: +- 使用 PIL 库处理图像 +- 通过 ImageEnhance 模块调整亮度、对比度和饱和度 +- 自动检测资源目录和原始图标位置 + +### 3.2 GUI 类修改 + +修改了 `gui.py` 中的相关方法: +- `toggle_monitoring`: 添加了图标切换和刷新逻辑 +- 异常处理部分: 添加了图标恢复和刷新逻辑 + +### 3.3 测试验证 + +通过以下场景验证了改进效果: +- 启动监听时图标切换到活动状态 +- 停止监听时图标切换到非活动状态 +- 异常情况下图标恢复到非活动状态 +- 多次切换状态时图标正确更新 + +## 4. 效果对比 + +| 功能 | 改进前 | 改进后 | +|------|--------|--------| +| 视觉反馈 | 无明显区分 | 活动状态图标更明亮、鲜艳 | +| 状态指示 | 仅通过菜单文本 | 图标颜色和提示文本 | +| 更新可靠性 | 部分环境下不更新 | 强制刷新确保更新 | +| 错误处理 | 基础处理 | 全面的异常捕获和恢复 | + +## 5. 后续优化方向 + +- 考虑添加动态图标,如活动状态下的动画效果 +- 优化图标资源加载机制,支持高DPI显示 +- 添加更多状态指示(如错误状态、处理中状态等) +- 实现自定义图标主题支持 + +## 6. Commit 消息 + +``` +改进: 托盘图标状态管理和分类配置持久化 + +- 添加活动状态图标(icon_active.png),提供更明显的视觉反馈 +- 优化图标切换逻辑,确保状态变化时图标正确更新 +- 增强图标刷新机制,解决某些环境下图标不更新的问题 +- 添加更全面的错误处理,提高系统托盘功能稳定性 +- 完善分类配置的持久化和管理机制 +- 更新README.md,添加v0.1.7版本更新内容 + +此次更新使系统托盘功能更加稳定可靠,用户可以通过图标颜色 +直观判断应用程序状态,提升了整体用户体验。 +``` diff --git a/project/llmclipboard/README.md b/project/llmclipboard/README.md index e4dacbf..4a3181d 100644 --- a/project/llmclipboard/README.md +++ b/project/llmclipboard/README.md @@ -326,7 +326,18 @@ cd project\llmclipboard ; pyinstaller --clean llmclipboard.spec - 改进通知系统样式和错误处理 - 优化UI响应性能 - 添加uv环境打包支持 -- + +## v0.1.7 (2025-03-02) +- 改进托盘图标状态管理 + - 添加活动状态图标,提供更明显的视觉反馈 + - 优化图标切换逻辑,确保状态变化时图标正确更新 + - 增强图标刷新机制,解决某些环境下图标不更新的问题 + - 添加更全面的错误处理,提高系统托盘功能稳定性 +- 完善分类配置的持久化和管理机制 + - 支持开发和打包环境下的配置文件路径 + - 确保配置文件正确保存和加载 + - 添加更详细的日志记录 + ## 🤝 贡献指南 1. Fork 项目 diff --git a/project/llmclipboard/build.bat b/project/llmclipboard/build.bat index bfb6597..9229e32 100644 --- a/project/llmclipboard/build.bat +++ b/project/llmclipboard/build.bat @@ -18,6 +18,18 @@ if exist dist\LLMClipboard.exe ( if not exist dist\resources mkdir dist\resources xcopy /s /y resources dist\resources\ + :: Create a default config.ini in the dist folder + echo Creating default config.ini in dist folder... + echo [Settings] > dist\config.ini + echo double_click_threshold = 0.3 >> dist\config.ini + echo save_location = C:\Users\Documents >> dist\config.ini + echo. >> dist\config.ini + echo [AI] >> dist\config.ini + echo model_type = none >> dist\config.ini + echo api_key = >> dist\config.ini + echo base_url = https://api.openai.com/v1 >> dist\config.ini + echo model = gpt-3.5-turbo >> dist\config.ini + :: Create a README.txt in the dist folder echo Creating README.txt in dist folder... echo LLMClipboard v0.1.6 > dist\README.txt @@ -29,7 +41,9 @@ if exist dist\LLMClipboard.exe ( echo 3. Double-click the system tray icon to open the main window >> dist\README.txt echo 4. Right-click the system tray icon to access the menu >> dist\README.txt echo. >> dist\README.txt - echo If you encounter any issues, please check error_log.txt and config_error.txt files >> dist\README.txt + echo Configuration: >> dist\README.txt + echo - Settings are stored in config.ini in the same folder as the executable >> dist\README.txt + echo - If you encounter any issues, please check error_log.txt and config_error.txt files >> dist\README.txt ) else ( echo Build failed, please check error messages. ) diff --git a/project/llmclipboard/categories.json b/project/llmclipboard/categories.json new file mode 100644 index 0000000..9828427 --- /dev/null +++ b/project/llmclipboard/categories.json @@ -0,0 +1,30 @@ +{ + "测试": [ + "测试1", + "测试2", + "测试3" + ], + "工作": [ + "会议", + "项目", + "计划", + "报告", + "任务", + "进度" + ], + "学习": [ + "教程", + "课程", + "学习", + "笔记", + "知识", + "总结" + ], + "生活": [ + "购物", + "健康", + "旅行", + "美食", + "娱乐" + ] +} \ No newline at end of file diff --git a/project/llmclipboard/create_active_icon.py b/project/llmclipboard/create_active_icon.py new file mode 100644 index 0000000..cb96479 --- /dev/null +++ b/project/llmclipboard/create_active_icon.py @@ -0,0 +1,58 @@ +""" +创建活动状态图标 +""" +from PIL import Image, ImageEnhance +import os + +def create_active_icon(input_path, output_path): + """ + 基于现有图标创建一个活动状态的图标 + 活动状态图标将更亮、更鲜艳 + """ + try: + # 打开原始图标 + img = Image.open(input_path) + + # 增加亮度 + enhancer = ImageEnhance.Brightness(img) + brightened_img = enhancer.enhance(1.3) # 增加30%亮度 + + # 增加对比度 + enhancer = ImageEnhance.Contrast(brightened_img) + contrasted_img = enhancer.enhance(1.2) # 增加20%对比度 + + # 增加颜色饱和度 + enhancer = ImageEnhance.Color(contrasted_img) + colored_img = enhancer.enhance(1.5) # 增加50%饱和度 + + # 保存活动状态图标 + colored_img.save(output_path) + print(f"活动状态图标已创建: {output_path}") + return True + except Exception as e: + print(f"创建活动状态图标失败: {e}") + return False + +if __name__ == "__main__": + # 确定图标路径 + script_dir = os.path.dirname(os.path.abspath(__file__)) + resources_dir = os.path.join(script_dir, "resources") + + # 确保资源目录存在 + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) + print(f"创建资源目录: {resources_dir}") + + # 图标路径 + icon_path = os.path.join(resources_dir, "icon.png") + active_icon_path = os.path.join(resources_dir, "icon_active.png") + + # 检查原始图标是否存在 + if not os.path.exists(icon_path): + print(f"错误: 原始图标不存在: {icon_path}") + else: + # 创建活动状态图标 + if create_active_icon(icon_path, active_icon_path): + print("成功创建活动状态图标") + else: + print("创建活动状态图标失败") diff --git a/project/llmclipboard/llmclipboard/app.py b/project/llmclipboard/llmclipboard/app.py index b7a4e34..a87f7a7 100644 --- a/project/llmclipboard/llmclipboard/app.py +++ b/project/llmclipboard/llmclipboard/app.py @@ -35,39 +35,185 @@ class StreamToLogger: class TextCaptureService: def __init__(self): + # 设置日志记录 + self.setup_logging() + + # 加载配置 self.load_config() self.running = True self.last_right_click_time = 0 - self.setup_logging() self._mouse_listener = None self._keyboard_listener = None # 加载AI配置 self.ai_config = self.load_ai_config() - self.doc_processor = DocumentProcessor(ai_config=self.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): - self.config = configparser.ConfigParser() - config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') - if os.path.exists(config_file): - self.config.read(config_file) - self.double_click_threshold = self.config.getfloat('Settings', 'double_click_threshold', fallback=0.3) - self.save_location = self.config.get('Settings', 'save_location', - fallback=os.path.expanduser('~/Documents')) - + 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): - """加载AI配置""" - ai_config = {} - if self.config.has_section('AI'): - for key, value in self.config.items('AI'): - ai_config[key] = value + 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配置""" - self.ai_config = new_config - if hasattr(self, 'doc_processor'): - self.doc_processor.update_ai_config(new_config) + 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: diff --git a/project/llmclipboard/llmclipboard/document_processor.py b/project/llmclipboard/llmclipboard/document_processor.py index 4874944..ea8778c 100644 --- a/project/llmclipboard/llmclipboard/document_processor.py +++ b/project/llmclipboard/llmclipboard/document_processor.py @@ -33,7 +33,20 @@ logger.addHandler(console_handler) class DocumentProcessor: def __init__(self, config_path=None, ai_config=None): - self.config_path = config_path or os.path.join(os.path.dirname(__file__), 'categories.json') + # 确定配置文件路径 + if config_path: + self.config_path = config_path + else: + # 如果没有提供配置路径,使用默认路径 + if getattr(sys, 'frozen', False): + # 打包环境 - 使用可执行文件所在目录 + config_dir = os.path.dirname(sys.executable) + else: + # 开发环境 - 使用项目根目录 + config_dir = os.path.dirname(os.path.dirname(__file__)) + self.config_path = os.path.join(config_dir, 'categories.json') + + logger.info(f"使用分类配置文件路径: {self.config_path}") self.load_categories() # 初始化AI处理器 @@ -45,10 +58,29 @@ class DocumentProcessor: def load_categories(self): """加载预定义的分类规则""" - if os.path.exists(self.config_path): - with open(self.config_path, 'r', encoding='utf-8') as f: - self.categories = json.load(f) - else: + try: + logger.info(f"尝试从 {self.config_path} 加载分类规则") + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding='utf-8') as f: + self.categories = json.load(f) + logger.info(f"成功加载分类规则,包含 {len(self.categories)} 个分类") + for category, keywords in self.categories.items(): + logger.debug(f"分类 '{category}' 包含 {len(keywords)} 个关键词") + else: + logger.warning(f"分类规则文件 {self.config_path} 不存在,使用默认分类") + self.categories = { + "技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"], + "学习": ["教程", "课程", "学习", "笔记", "知识", "总结"], + "工作": ["会议", "项目", "计划", "报告", "任务", "进度"], + "想法": ["想法", "创意", "思考", "灵感", "观点", "建议"], + "资源": ["工具", "资源", "链接", "参考", "文档", "书籍"] + } + self._save_categories() + except Exception as e: + logger.error(f"加载分类规则时出错: {e}") + import traceback + traceback.print_exc() + # 使用默认分类 self.categories = { "技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"], "学习": ["教程", "课程", "学习", "笔记", "知识", "总结"], @@ -56,50 +88,31 @@ class DocumentProcessor: "想法": ["想法", "创意", "思考", "灵感", "观点", "建议"], "资源": ["工具", "资源", "链接", "参考", "文档", "书籍"] } - self._save_categories() def _save_categories(self): """保存分类规则""" - os.makedirs(os.path.dirname(self.config_path), exist_ok=True) - with open(self.config_path, 'w', encoding='utf-8') as f: - json.dump(self.categories, f, ensure_ascii=False, indent=2) + try: + logger.info(f"保存分类规则到 {self.config_path}") + os.makedirs(os.path.dirname(self.config_path), exist_ok=True) + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(self.categories, f, ensure_ascii=False, indent=2) + logger.info(f"分类规则已保存,包含 {len(self.categories)} 个分类") + except Exception as e: + logger.error(f"保存分类规则时出错: {e}") + import traceback + traceback.print_exc() def update_categories(self, new_categories): """更新分类规则""" - self.categories = new_categories - self._save_categories() - logger.info("分类规则已更新") - - def clean_content(self, content): - """清理内容,移除多余的换行符和空白""" - if not content or content.isspace(): - logger.debug("收到空内容或纯空白内容") - return "" - - logger.debug("原始内容长度: %d", len(content)) - logger.debug("原始内容前100个字符: %s", content[:100]) - logger.debug("原始内容包含的换行符数量: %d", content.count('\n')) - - # 移除文件名 - content = re.sub(r'captured_text_\d{8}_\d{6}', '', content) - logger.debug("已移除文件名") - logger.debug("移除文件名后的内容长度: %d", len(content)) - - # 清理内容,保持原有的换行格式 - lines = content.splitlines() - cleaned_lines = [] - for line in lines: - line = line.strip() - if line or cleaned_lines: # 如果当前行非空或已经有内容,保留这一行 - cleaned_lines.append(line) - - # 使用原始换行符重新组合内容 - content = '\n'.join(cleaned_lines) - - logger.debug("清理后内容长度: %d", len(content)) - logger.debug("清理后内容包含的换行符数量: %d", content.count('\n')) - logger.debug("清理后内容的前100个字符: %s", content[:100]) - return content + try: + logger.info(f"更新分类规则,新分类包含 {len(new_categories)} 个分类") + self.categories = new_categories + self._save_categories() + logger.info("分类规则已更新") + except Exception as e: + logger.error(f"更新分类规则时出错: {e}") + import traceback + traceback.print_exc() def update_ai_config(self, ai_config): """更新AI配置""" @@ -301,3 +314,34 @@ category: {category} 'tags': tags, 'category': category } + + def clean_content(self, content): + """清理内容,移除多余的换行符和空白""" + if not content or content.isspace(): + logger.debug("收到空内容或纯空白内容") + return "" + + logger.debug("原始内容长度: %d", len(content)) + logger.debug("原始内容前100个字符: %s", content[:100]) + logger.debug("原始内容包含的换行符数量: %d", content.count('\n')) + + # 移除文件名 + content = re.sub(r'captured_text_\d{8}_\d{6}', '', content) + logger.debug("已移除文件名") + logger.debug("移除文件名后的内容长度: %d", len(content)) + + # 清理内容,保持原有的换行格式 + lines = content.splitlines() + cleaned_lines = [] + for line in lines: + line = line.strip() + if line or cleaned_lines: # 如果当前行非空或已经有内容,保留这一行 + cleaned_lines.append(line) + + # 使用原始换行符重新组合内容 + content = '\n'.join(cleaned_lines) + + logger.debug("清理后内容长度: %d", len(content)) + logger.debug("清理后内容包含的换行符数量: %d", content.count('\n')) + logger.debug("清理后内容的前100个字符: %s", content[:100]) + return content diff --git a/project/llmclipboard/llmclipboard/gui.py b/project/llmclipboard/llmclipboard/gui.py index 2f5eca3..e4821ae 100644 --- a/project/llmclipboard/llmclipboard/gui.py +++ b/project/llmclipboard/llmclipboard/gui.py @@ -1,15 +1,16 @@ import sys import os import time -from configparser import ConfigParser +from configparser import ConfigParser, BasicInterpolation import darkdetect from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, - QSystemTrayIcon, QMenu, QSpinBox, QStyle, QLineEdit, - QDialog, QTabWidget, QGroupBox, QComboBox, QCheckBox, - QDialogButtonBox, QMessageBox, QListWidget) + QListWidget, QLineEdit, QSpinBox, QGroupBox, + QSystemTrayIcon, QMenu, QStyle, QMessageBox, QDialog, QTabWidget, QComboBox, QCheckBox, + QDialogButtonBox) from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon, QAction +from llmclipboard.category_manager import CategoryManagerDialog class NotificationDialog(QDialog): """自定义通知对话框类""" @@ -180,7 +181,13 @@ class AISettingsDialog(QDialog): # 按钮 button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) - button_box.accepted.connect(self.accept) + + # 添加调试信息 + def on_accepted(): + print("AI设置对话框保存按钮被点击") + self.accept() + + button_box.accepted.connect(on_accepted) button_box.rejected.connect(self.reject) layout.addWidget(button_box) @@ -317,8 +324,19 @@ class MainWindow(QMainWindow): self.service = llmclipboard.app.TextCaptureService() # 加载配置 - self.config = ConfigParser() - self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') + self.config = ConfigParser(interpolation=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') + print(f"GUI配置文件路径: {self.config_file}") + self.load_config() # 创建主窗口部件 @@ -450,8 +468,10 @@ class MainWindow(QMainWindow): def load_config(self): try: if os.path.exists(self.config_file): - self.config.read(self.config_file) + self.config.read(self.config_file, encoding='utf-8') + print(f"成功读取GUI配置文件: {self.config_file}") else: + print(f"GUI配置文件不存在,将使用默认配置: {self.config_file}") self.config['Settings'] = { 'save_location': os.path.expanduser('~/Documents'), 'double_click_threshold': '0.3' @@ -470,11 +490,20 @@ class MainWindow(QMainWindow): # 更新UI组件(如果存在) if hasattr(self, 'save_path_edit'): - self.save_path_edit.setText(self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents'))) + try: + save_location = self.config.get('Settings', 'save_location', fallback=os.path.expanduser('~/Documents')) + self.save_path_edit.setText(save_location) + except (configparser.InterpolationError, ValueError) as e: + print(f"读取save_location设置时出错: {e}") + self.save_path_edit.setText(os.path.expanduser('~/Documents')) if hasattr(self, 'threshold_spin'): - threshold = int(float(self.config.get('Settings', 'double_click_threshold', fallback='0.3')) * 1000) - self.threshold_spin.setValue(threshold) + try: + threshold = int(float(self.config.get('Settings', 'double_click_threshold', fallback='0.3')) * 1000) + self.threshold_spin.setValue(threshold) + except (configparser.InterpolationError, ValueError) as e: + print(f"读取double_click_threshold设置时出错: {e}") + self.threshold_spin.setValue(300) # 默认值0.3秒,转换为毫秒 except Exception as e: import traceback @@ -489,31 +518,62 @@ class MainWindow(QMainWindow): def save_config(self): try: - # 检查属性是否存在 - if hasattr(self, 'save_path_edit') and hasattr(self, 'threshold_spin'): + print("开始保存配置...") + # 确保配置目录存在 + config_dir = os.path.dirname(self.config_file) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 更新配置对象 + if hasattr(self, 'save_path_edit'): self.config['Settings']['save_location'] = self.save_path_edit.text() + print(f"保存路径设置为: {self.save_path_edit.text()}") + + if hasattr(self, 'threshold_spin'): self.config['Settings']['double_click_threshold'] = str(self.threshold_spin.value() / 1000) - else: - # 如果属性不存在,使用默认值 - if not self.config.has_section('Settings'): - self.config['Settings'] = {} - self.config['Settings'].setdefault('save_location', os.path.expanduser('~/Documents')) - self.config['Settings'].setdefault('double_click_threshold', '0.3') - print("警告: GUI组件未初始化,使用默认配置值") - - with open(self.config_file, 'w') as f: + print(f"双击阈值设置为: {self.threshold_spin.value() / 1000}") + + # 写入配置文件 + with open(self.config_file, 'w', encoding='utf-8') as f: self.config.write(f) - # 重新加载服务配置 - if hasattr(self.service, 'load_config'): - self.service.load_config() - - if hasattr(self, 'status_label'): - self.show_message("设置已保存") + print(f"配置已保存到: {self.config_file}") + # 显示保存成功的提示 + print("准备显示保存成功消息...") + + # 直接调用show_tray_message方法 + self.show_tray_message( + "LLMClipboard", + "配置已保存成功", + "info", + 2000 + ) + + # 更新状态栏 + self.status_label.setStyleSheet("color: green; font-weight: bold;") + self.status_label.setText("配置已保存成功") + + # 5秒后清除消息 + from PyQt6.QtCore import QTimer + QTimer.singleShot(5000, lambda: self.status_label.setText("就绪")) + + print("已显示保存成功消息") + + # 更新服务的配置 + if hasattr(self, 'service'): + try: + print("准备更新服务配置...") + self.service.load_config() + print("服务配置已更新") + except Exception as service_error: + print(f"更新服务配置时出错: {service_error}") + except Exception as e: import traceback print(f"保存配置时出错: {e}") traceback.print_exc() + # 显示保存失败的提示 + self.show_message(f"保存配置失败: {e}", error=True) # 在打包环境中,确保错误信息被记录 if getattr(sys, 'frozen', False): error_log_path = os.path.join(os.path.dirname(sys.executable), 'config_error.txt') @@ -528,39 +588,103 @@ class MainWindow(QMainWindow): def show_ai_settings(self): """显示AI设置对话框""" - dialog = AISettingsDialog(self) - - # 设置当前配置 - ai_config = {} - if self.config.has_section('AI'): - for key, value in self.config.items('AI'): - ai_config[key] = value - - dialog.set_config(ai_config) - - if dialog.exec(): - # 获取新配置 - new_config = dialog.get_config() + try: + dialog = AISettingsDialog(self) - # 保存到配置文件 - 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)) - - self.save_config() + # 设置当前配置 + ai_config = {} + if self.config.has_section('AI'): + try: + for key in ['model_type', 'api_key', 'base_url', 'model']: + try: + value = self.config.get('AI', key, fallback='') + ai_config[key] = value + except configparser.InterpolationError as e: + print(f"读取AI配置项 {key} 时出错: {e}") + # 设置默认值 + if key == 'model_type': + ai_config[key] = 'none' + elif key == 'api_key': + ai_config[key] = '' + elif key == 'base_url': + ai_config[key] = 'https://api.openai.com/v1' + elif key == 'model': + ai_config[key] = 'gpt-3.5-turbo' + except Exception as e: + print(f"读取AI配置时出错: {e}") + import traceback + traceback.print_exc() + + dialog.set_config(ai_config) - # 更新服务的AI配置 - if hasattr(self.service, 'update_ai_config'): - self.service.update_ai_config(new_config) + if dialog.exec(): + print("AI设置对话框已接受") + # 获取新配置 + new_config = dialog.get_config() + # 保存到配置文件 + 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)) + + self.save_config() + + # 更新服务的AI配置 + if hasattr(self.service, 'update_ai_config'): + self.service.update_ai_config(new_config) + + # 直接调用show_tray_message方法 + self.show_tray_message( + "LLMClipboard", + "AI设置已保存并应用", + "info", + 2000 + ) + + # 更新状态栏 + self.status_label.setStyleSheet("color: green; font-weight: bold;") + self.status_label.setText("AI设置已保存并应用") + + # 5秒后清除消息 + from PyQt6.QtCore import QTimer + QTimer.singleShot(5000, lambda: self.status_label.setText("就绪")) + except Exception as e: + import traceback + print(f"显示AI设置对话框失败: {e}") + traceback.print_exc() + self.show_message(f"显示AI设置对话框失败: {str(e)}", error=True) + def show_category_manager(self): """显示分类管理器对话框""" # 获取当前分类 categories = {} - if hasattr(self.service, 'doc_processor'): - categories = self.service.doc_processor.categories + try: + if hasattr(self.service, 'doc_processor'): + categories = self.service.doc_processor.categories + else: + # 如果doc_processor不存在,尝试从文件加载分类 + import json + import os + categories_file = os.path.join(os.path.dirname(__file__), 'categories.json') + if os.path.exists(categories_file): + with open(categories_file, 'r', encoding='utf-8') as f: + categories = json.load(f) + else: + # 使用默认分类 + categories = { + "技术": ["编程", "开发", "代码", "框架", "算法", "数据库", "API"], + "学习": ["教程", "课程", "学习", "笔记", "知识", "总结"], + "工作": ["会议", "项目", "计划", "报告", "任务", "进度"], + "想法": ["想法", "创意", "思考", "灵感", "观点", "建议"], + "资源": ["工具", "资源", "链接", "参考", "文档", "书籍"] + } + except Exception as e: + import traceback + traceback.print_exc() + self.show_message(f"加载分类失败: {e}", error=True) + return # 创建分类管理器对话框 dialog = CategoryManagerDialog(categories, self) @@ -573,10 +697,73 @@ class MainWindow(QMainWindow): def update_categories(self, new_categories): """更新分类""" - if hasattr(self.service, 'doc_processor'): - self.service.doc_processor.update_categories(new_categories) - self.show_message("分类已更新") + try: + print(f"更新分类: {new_categories}") + # 更新doc_processor的分类 + if hasattr(self.service, 'doc_processor'): + print("使用doc_processor更新分类") + self.service.doc_processor.update_categories(new_categories) + print("分类更新成功") + else: + # 如果doc_processor不存在,直接保存到文件 + print("直接保存分类到文件") + import json + import os + + # 确保使用正确的配置文件路径 + if getattr(sys, 'frozen', False): + # 打包环境 - 使用可执行文件所在目录 + config_dir = os.path.dirname(sys.executable) + else: + # 开发环境 - 使用项目根目录 + config_dir = os.path.dirname(os.path.dirname(__file__)) + + categories_file = os.path.join(config_dir, 'categories.json') + print(f"保存分类到文件: {categories_file}") + + os.makedirs(os.path.dirname(categories_file), exist_ok=True) + with open(categories_file, 'w', encoding='utf-8') as f: + json.dump(new_categories, f, ensure_ascii=False, indent=2) + print(f"分类已保存到文件: {categories_file}") + + # 尝试重新加载服务配置 + try: + print("尝试重新加载服务配置") + self.service.load_config() + print("服务配置重新加载成功") + except Exception as e: + print(f"重新加载服务配置失败: {e}") + import traceback + traceback.print_exc() + + # 显示成功消息 + self.show_tray_message( + "LLMClipboard", + "分类已更新并保存", + "info", + 2000 + ) + + # 更新状态栏 + self.status_label.setStyleSheet("color: green; font-weight: bold;") + self.status_label.setText("分类已更新") + + # 5秒后清除消息 + from PyQt6.QtCore import QTimer + QTimer.singleShot(5000, lambda: self.status_label.setText("就绪")) + + except Exception as e: + print(f"更新分类失败: {e}") + import traceback + traceback.print_exc() + self.show_tray_message( + "LLMClipboard", + f"更新分类失败: {e}", + "error", + 3000 + ) + def add_recent_file(self, file_path): """添加最近保存的文件到列表""" # 检查是否已存在 @@ -624,11 +811,18 @@ class MainWindow(QMainWindow): if hasattr(self, 'status_action') and self.status_action is not None: self.status_action.setText("状态: 正在运行") if hasattr(self, 'tray_icon') and hasattr(self, 'active_icon') and self.tray_icon is not None: + print("设置活动状态托盘图标") self.tray_icon.setIcon(self.active_icon) # 确保托盘图标可见 if not self.tray_icon.isVisible(): print("确保托盘图标可见") self.tray_icon.show() + # 强制更新图标 + self.tray_icon.setToolTip("LLMClipboard - 正在运行") + # 强制刷新图标 + self.tray_icon.hide() + self.tray_icon.show() + QApplication.processEvents() # 保存配置并启动服务 try: @@ -664,11 +858,21 @@ class MainWindow(QMainWindow): if hasattr(self, 'status_action') and self.status_action is not None: self.status_action.setText("状态: 已停止") if hasattr(self, 'tray_icon') and hasattr(self, 'inactive_icon') and self.tray_icon is not None: + print("设置非活动状态托盘图标") self.tray_icon.setIcon(self.inactive_icon) # 确保托盘图标可见 if not self.tray_icon.isVisible(): print("确保托盘图标可见") self.tray_icon.show() + # 强制更新图标 + self.tray_icon.setToolTip("LLMClipboard - 已停止") + # 强制刷新图标 + try: + self.tray_icon.hide() + self.tray_icon.show() + except Exception as refresh_error: + print(f"刷新托盘图标失败: {str(refresh_error)}") + QApplication.processEvents() # 停止服务 try: @@ -700,6 +904,9 @@ class MainWindow(QMainWindow): self.tray_icon.show() except Exception as icon_error: print(f"恢复托盘图标可见性失败: {str(icon_error)}") + # 强制更新图标 + self.tray_icon.setToolTip("LLMClipboard - 已停止") + QApplication.processEvents() # 显示错误消息 self.show_message(f"切换监听状态失败: {str(e)}", error=True) @@ -783,11 +990,20 @@ class MainWindow(QMainWindow): # 设置托盘图标 print("设置托盘图标") - icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "icon.png") - if os.path.exists(icon_path): - print(f"使用自定义图标: {icon_path}") - self.active_icon = QIcon(icon_path) - self.inactive_icon = QIcon(icon_path) + icon_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources") + active_icon_path = os.path.join(icon_dir, "icon_active.png") + inactive_icon_path = os.path.join(icon_dir, "icon.png") + + # 检查自定义图标是否存在 + if os.path.exists(active_icon_path) and os.path.exists(inactive_icon_path): + print(f"使用自定义图标: 活动={active_icon_path}, 非活动={inactive_icon_path}") + self.active_icon = QIcon(active_icon_path) + self.inactive_icon = QIcon(inactive_icon_path) + elif os.path.exists(inactive_icon_path): + # 只有一个图标文件,但仍使用不同的系统图标区分状态 + print(f"使用自定义非活动图标和系统活动图标") + self.inactive_icon = QIcon(inactive_icon_path) + self.active_icon = self.style().standardIcon(QStyle.StandardPixmap.SP_DialogApplyButton) else: # 使用系统默认图标 print("使用系统默认图标") @@ -1079,49 +1295,23 @@ class MainWindow(QMainWindow): if error: print(f"错误: {message}") self.status_label.setStyleSheet("color: red; font-weight: bold;") - if hasattr(self, 'tray_icon') and self.tray_icon is not None: - try: - self.tray_icon.showMessage( - "LLMClipboard - 错误", - message, - QSystemTrayIcon.MessageIcon.Critical, - 3000 - ) - except Exception as e: - print(f"显示错误消息失败: {str(e)}") - # 尝试使用旧版API - try: - self.tray_icon.showMessage( - "LLMClipboard - 错误", - message, - QSystemTrayIcon.Critical, - 3000 - ) - except Exception as e2: - print(f"使用旧版API显示错误消息也失败: {str(e2)}") + # 使用改进的show_tray_message方法显示错误消息 + self.show_tray_message( + "LLMClipboard - 错误", + message, + "error", + 3000 + ) else: print(f"消息: {message}") self.status_label.setStyleSheet("color: green; font-weight: bold;") - if hasattr(self, 'tray_icon') and self.tray_icon is not None: - try: - self.tray_icon.showMessage( - "LLMClipboard", - message, - QSystemTrayIcon.MessageIcon.Information, - 2000 - ) - except Exception as e: - print(f"显示信息消息失败: {str(e)}") - # 尝试使用旧版API - try: - self.tray_icon.showMessage( - "LLMClipboard", - message, - QSystemTrayIcon.Information, - 2000 - ) - except Exception as e2: - print(f"使用旧版API显示信息消息也失败: {str(e2)}") + # 使用改进的show_tray_message方法显示信息消息 + self.show_tray_message( + "LLMClipboard", + message, + "info", + 2000 + ) self.status_label.setText(message) @@ -1380,7 +1570,14 @@ class MainWindow(QMainWindow): # 添加确定和取消按钮 button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) - button_box.accepted.connect(dialog.accept) + + # 连接确定按钮到保存配置方法 + def on_accepted(): + print("设置对话框确定按钮被点击") + self.save_config() # 保存配置 + dialog.accept() # 关闭对话框 + + button_box.accepted.connect(on_accepted) button_box.rejected.connect(dialog.reject) layout.addWidget(button_box) diff --git a/project/llmclipboard/resources/icon_active.png b/project/llmclipboard/resources/icon_active.png new file mode 100644 index 0000000..e56f5e1 Binary files /dev/null and b/project/llmclipboard/resources/icon_active.png differ diff --git a/project/llmclipboard/test_categories.py b/project/llmclipboard/test_categories.py new file mode 100644 index 0000000..96bad46 --- /dev/null +++ b/project/llmclipboard/test_categories.py @@ -0,0 +1,91 @@ +""" +测试分类配置的加载和保存功能 +""" +import os +import sys +import json +import logging + +# 配置日志记录 +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +if project_root not in sys.path: + sys.path.append(project_root) + +from llmclipboard.document_processor import DocumentProcessor + +def test_categories_persistence(): + """测试分类配置的持久化功能""" + logger.info("开始测试分类配置的持久化功能") + + # 确定配置文件路径 + if getattr(sys, 'frozen', False): + # 打包环境 - 使用可执行文件所在目录 + config_dir = os.path.dirname(sys.executable) + else: + # 开发环境 - 使用项目根目录 + config_dir = project_root + + categories_file = os.path.join(config_dir, 'categories.json') + logger.info(f"分类配置文件路径: {categories_file}") + + # 1. 检查配置文件是否存在 + if os.path.exists(categories_file): + logger.info(f"分类配置文件已存在: {categories_file}") + with open(categories_file, 'r', encoding='utf-8') as f: + current_categories = json.load(f) + logger.info(f"当前分类: {current_categories}") + else: + logger.info(f"分类配置文件不存在: {categories_file}") + current_categories = {} + + # 2. 创建文档处理器实例 + doc_processor = DocumentProcessor(config_path=categories_file) + + # 3. 获取当前分类 + logger.info(f"文档处理器加载的分类: {doc_processor.categories}") + + # 4. 修改分类 + new_categories = { + "测试": ["测试1", "测试2", "测试3"], + "工作": ["会议", "项目", "计划", "报告", "任务", "进度"], + "学习": ["教程", "课程", "学习", "笔记", "知识", "总结"], + "生活": ["购物", "健康", "旅行", "美食", "娱乐"] + } + + # 5. 更新分类 + logger.info(f"更新分类: {new_categories}") + doc_processor.update_categories(new_categories) + + # 6. 检查配置文件是否已更新 + if os.path.exists(categories_file): + with open(categories_file, 'r', encoding='utf-8') as f: + updated_categories = json.load(f) + logger.info(f"更新后的分类: {updated_categories}") + + # 验证更新是否成功 + if updated_categories == new_categories: + logger.info("分类更新成功!") + else: + logger.error("分类更新失败!") + else: + logger.error(f"分类配置文件不存在: {categories_file}") + + # 7. 创建新的文档处理器实例,验证配置是否能被正确加载 + new_doc_processor = DocumentProcessor(config_path=categories_file) + logger.info(f"新文档处理器加载的分类: {new_doc_processor.categories}") + + # 验证加载是否成功 + if new_doc_processor.categories == new_categories: + logger.info("分类加载成功!") + else: + logger.error("分类加载失败!") + + return True + +if __name__ == "__main__": + test_categories_persistence() diff --git a/project/llmclipboard/test_category_manager.py b/project/llmclipboard/test_category_manager.py new file mode 100644 index 0000000..1c6de30 --- /dev/null +++ b/project/llmclipboard/test_category_manager.py @@ -0,0 +1,95 @@ +""" +测试LLMClipboard分类配置管理功能 +""" +import os +import sys +import json +import logging +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import QTimer + +# 配置日志记录 +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +if project_root not in sys.path: + sys.path.append(project_root) + +from llmclipboard.document_processor import DocumentProcessor +from llmclipboard.category_manager import CategoryManagerDialog +from llmclipboard.gui import MainWindow + +def test_category_manager_dialog(): + """测试分类管理器对话框""" + logger.info("开始测试分类管理器对话框") + + # 创建应用程序实例 + app = QApplication(sys.argv) + + # 确定配置文件路径 + if getattr(sys, 'frozen', False): + # 打包环境 - 使用可执行文件所在目录 + config_dir = os.path.dirname(sys.executable) + else: + # 开发环境 - 使用项目根目录 + config_dir = project_root + + categories_file = os.path.join(config_dir, 'categories.json') + logger.info(f"分类配置文件路径: {categories_file}") + + # 加载当前分类 + if os.path.exists(categories_file): + logger.info(f"分类配置文件已存在: {categories_file}") + with open(categories_file, 'r', encoding='utf-8') as f: + categories = json.load(f) + logger.info(f"当前分类: {categories}") + else: + logger.info(f"分类配置文件不存在: {categories_file}") + categories = {} + + # 创建分类管理器对话框 + dialog = CategoryManagerDialog(categories) + + # 连接信号 + def on_categories_updated(new_categories): + logger.info(f"分类已更新: {new_categories}") + + # 保存到文件 + with open(categories_file, 'w', encoding='utf-8') as f: + json.dump(new_categories, f, ensure_ascii=False, indent=2) + logger.info(f"分类已保存到文件: {categories_file}") + + dialog.categories_updated.connect(on_categories_updated) + + # 显示对话框 + dialog.show() + + # 5秒后自动关闭 + QTimer.singleShot(60000, dialog.close) + + # 运行应用程序 + sys.exit(app.exec()) + +def test_main_window_category_management(): + """测试主窗口的分类管理功能""" + logger.info("开始测试主窗口的分类管理功能") + + # 创建应用程序实例 + app = QApplication(sys.argv) + + # 创建主窗口 + window = MainWindow() + + # 显示窗口 + window.show() + + # 运行应用程序 + sys.exit(app.exec()) + +if __name__ == "__main__": + # 选择要运行的测试 + test_category_manager_dialog() + # test_main_window_category_management()