diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build.yml similarity index 80% rename from .github/workflows/build-windows.yml rename to .github/workflows/build.yml index 7feae2b..ae8e43f 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ # 工作流的名称 -name: Build Windows Executable +name: Build Cross-Platform Executables # Trivial change to force a cache refresh # 触发条件: @@ -15,8 +15,10 @@ on: jobs: build: - # 指定运行环境为最新的Windows服务器 - runs-on: windows-latest + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} permissions: contents: write @@ -44,19 +46,18 @@ jobs: # --onefile: 打包成单个.exe文件 # --name: 指定生成的可执行文件名 - name: Build with PyInstaller - shell: cmd run: pyinstaller --noconsole --onefile --name "AI-Essay-Corrector" main.py - # 第5步:将打包好的.exe上传,以便在工作流页面下载 (适合测试) + # 第5步:将打包好的上传,以便在工作流页面下载 (适合测试) - name: Upload artifact for testing uses: actions/upload-artifact@v4 with: - name: AI-Essay-Corrector-Windows-exe - path: dist/AI-Essay-Corrector.exe + name: AI-Essay-Corrector-${{ matrix.os }} + path: dist/AI-Essay-Corrector* # 第6步 (专业级开源发布): 当你创建Tag时,自动创建Release并附上.exe - name: Create Release and Upload Asset if: startsWith(github.ref, 'refs/tags/') # 仅在创建Tag时运行此步骤 uses: softprops/action-gh-release@v2 with: - files: dist/AI-Essay-Corrector.exe \ No newline at end of file + files: dist/AI-Essay-Corrector* \ No newline at end of file diff --git a/README.md b/README.md index 7b67820..d8b2bb6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ --- -## 📖 使用指南 +## 使用指南 ### 快速开始 1. **下载程序**: 前往 [Releases页面](https://github.com/Eric-Terminal/Pro_llm_correct/releases) 下载最新版本 diff --git a/api_services.py b/api_services.py index 5f06f62..119af06 100644 --- a/api_services.py +++ b/api_services.py @@ -1,5 +1,8 @@ import base64 from typing import Dict, Any, Tuple, Optional +import urllib.request +import json +from packaging import version from config_manager import ConfigManager from markdown_renderer import create_markdown_renderer import os @@ -260,4 +263,27 @@ Strictly adhere to the following format. Do not output anything else. if html_path: self._log(f"已生成HTML报告: {os.path.basename(html_path)}") - return final_report, vlm_usage, llm_usage, html_path \ No newline at end of file + return final_report, vlm_usage, llm_usage, html_path + + +def check_for_updates(current_version_str: str) -> Optional[str]: + """ + Checks for new releases on GitHub. + Returns the new version tag if an update is available, otherwise None. + """ + try: + url = "https://api.github.com/repos/Eric-Terminal/Pro_llm_correct/releases/latest" + # Add a user-agent to avoid being blocked + req = urllib.request.Request(url, headers={'User-Agent': 'Pro_llm_correct-Update-Checker'}) + with urllib.request.urlopen(req) as response: + if response.status == 200: + data = json.loads(response.read().decode()) + # 根据您的调试反馈,我们现在读取 'name' 字段来获取版本号 + latest_version_name = data.get("name", "v0.0.0") + + # Use packaging.version for robust comparison + if version.parse(latest_version_name) > version.parse(current_version_str): + return latest_version_name + except Exception as e: + logging.error(f"Failed to check for updates: {e}") + return None \ No newline at end of file diff --git a/app_ui.py b/app_ui.py index 18d73eb..712c3d1 100644 --- a/app_ui.py +++ b/app_ui.py @@ -6,12 +6,15 @@ import os from typing import List import concurrent.futures import logging +import webbrowser from config_manager import ConfigManager -from api_services import ApiService, DEFAULT_LLM_PROMPT_TEMPLATE +from api_services import ApiService, DEFAULT_LLM_PROMPT_TEMPLATE, check_for_updates # 配置日志记录器 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +CURRENT_VERSION = "3.2.0" + class AboutDialog(tk.Toplevel): """“关于”对话框,展示应用信息,支持滚动查看。""" def __init__(self, parent, config_manager: ConfigManager): @@ -75,7 +78,7 @@ class AboutDialog(tk.Toplevel): 作者: Eric_Terminal 项目地址: https://github.com/Eric-Terminal/Pro_llm_correct -版本: 3.2 +版本: {CURRENT_VERSION} --- 历史Token使用统计: @@ -116,13 +119,14 @@ class SettingsDialog(tk.Toplevel): self.vlm_model = tk.StringVar(value=config_manager.get("VlmModel", "Pro/THUDM/GLM-4.1V-9B-Thinking")) self.llm_url = tk.StringVar(value=config_manager.get("LlmUrl", "https://api.siliconflow.cn/v1")) self.llm_api_key = tk.StringVar(value=config_manager.get("LlmApiKey", "")) - self.llm_model = tk.StringVar(value=config_manager.get("LlmModel", "moonshotai/Kimi-K2-Instruct")) + self.llm_model = tk.StringVar(value=config_manager.get("LlmModel", "Qwen/Qwen3-235B-A22B-Instruct-2507")) self.sensitivity_factor = tk.StringVar(value=config_manager.get("SensitivityFactor", "1.5")) self.max_workers = tk.StringVar(value=config_manager.get("MaxWorkers", "4")) self.max_retries = tk.StringVar(value=config_manager.get("MaxRetries", "3")) self.retry_delay = tk.StringVar(value=config_manager.get("RetryDelay", "5")) self.save_markdown = tk.BooleanVar(value=config_manager.get("SaveMarkdown", True)) self.render_markdown = tk.BooleanVar(value=config_manager.get("RenderMarkdown", True)) + self.auto_update_check = tk.BooleanVar(value=config_manager.get("AutoUpdateCheck", True)) # 智能加载Prompt模板:优先使用用户自定义模板,否则使用默认模板 user_template = config_manager.get("LlmPromptTemplate") @@ -140,7 +144,7 @@ class SettingsDialog(tk.Toplevel): ttk.Label(vlm_frame, text="VLM URL:").grid(column=0, row=0, sticky=tk.W, pady=2) ttk.Entry(vlm_frame, textvariable=self.vlm_url, width=40).grid(column=1, row=0, sticky=(tk.W, tk.E)) ttk.Label(vlm_frame, text="VLM API Key:").grid(column=0, row=1, sticky=tk.W, pady=2) - ttk.Entry(vlm_frame, textvariable=self.vlm_api_key, width=40).grid(column=1, row=1, sticky=(tk.W, tk.E)) + ttk.Entry(vlm_frame, textvariable=self.vlm_api_key, width=40, show='*').grid(column=1, row=1, sticky=(tk.W, tk.E)) ttk.Label(vlm_frame, text="VLM 模型:").grid(column=0, row=2, sticky=tk.W, pady=2) ttk.Entry(vlm_frame, textvariable=self.vlm_model, width=40).grid(column=1, row=2, sticky=(tk.W, tk.E)) @@ -151,7 +155,7 @@ class SettingsDialog(tk.Toplevel): ttk.Label(llm_frame, text="LLM URL:").grid(column=0, row=0, sticky=tk.W, pady=2) ttk.Entry(llm_frame, textvariable=self.llm_url, width=40).grid(column=1, row=0, sticky=(tk.W, tk.E)) ttk.Label(llm_frame, text="LLM API Key:").grid(column=0, row=1, sticky=tk.W, pady=2) - ttk.Entry(llm_frame, textvariable=self.llm_api_key, width=40).grid(column=1, row=1, sticky=(tk.W, tk.E)) + ttk.Entry(llm_frame, textvariable=self.llm_api_key, width=40, show='*').grid(column=1, row=1, sticky=(tk.W, tk.E)) ttk.Label(llm_frame, text="LLM 模型:").grid(column=0, row=2, sticky=tk.W, pady=2) ttk.Entry(llm_frame, textvariable=self.llm_model, width=40).grid(column=1, row=2, sticky=(tk.W, tk.E)) @@ -171,6 +175,8 @@ class SettingsDialog(tk.Toplevel): ttk.Checkbutton(other_frame, variable=self.save_markdown).grid(column=1, row=4, sticky=tk.W) ttk.Label(other_frame, text="渲染HTML报告:").grid(column=0, row=5, sticky=tk.W, pady=2) ttk.Checkbutton(other_frame, variable=self.render_markdown).grid(column=1, row=5, sticky=tk.W) + ttk.Label(other_frame, text="启动时检查更新:").grid(column=0, row=6, sticky=tk.W, pady=2) + ttk.Checkbutton(other_frame, variable=self.auto_update_check).grid(column=1, row=6, sticky=tk.W) # LLM Prompt模板编辑区域 prompt_frame = ttk.LabelFrame(frame, text="LLM Prompt 模板 (可在此修改,请勿修改{}占位符内容导致程序参数无法正常传递,通常情况下修改总分即可)", padding="10") @@ -208,6 +214,7 @@ class SettingsDialog(tk.Toplevel): "RetryDelay": self.retry_delay.get(), "SaveMarkdown": self.save_markdown.get(), "RenderMarkdown": self.render_markdown.get(), + "AutoUpdateCheck": self.auto_update_check.get(), "LlmPromptTemplate": self.llm_prompt_text.get("1.0", "end-1c") } @@ -238,6 +245,7 @@ class MainApp: self._setup_ui() self._initialize_config() + self._check_for_updates_on_startup() self.root.after(100, self._process_ui_queue) def _setup_ui(self): @@ -328,9 +336,14 @@ class MainApp: is_ok, missing_item = self.config_manager.check_settings() if is_ok: return - if messagebox.askretrycancel("配置未完成", f"请配置: {missing_item}") == "cancel": + # 用户可以选择取消配置,此时直接退出程序 + result = messagebox.askretrycancel("配置未完成", + f"请配置: {missing_item}\n\n点击'重试'继续配置,点击'取消'退出程序") + if result is None or result == "cancel" or not result: + # 用户点击取消或关闭对话框,直接退出程序 self.root.quit() return + # 用户点击重试,继续循环 def _open_settings_dialog(self): """打开设置对话框,并根据返回结果更新和保存配置。""" @@ -401,6 +414,8 @@ class MainApp: messagebox.showinfo("完成", "所有文件处理完成") self.progress_bar['value'] = 0 self.is_file_selected = False + elif task == "update_found": + self._show_update_dialog(data) except queue.Empty: pass finally: @@ -474,3 +489,29 @@ class MainApp: # 无论成功或失败,都更新进度 self.ui_queue.put(("progress", 1)) + + def _check_for_updates_on_startup(self): + """如果启用了自动更新检查,则在后台线程中启动检查。""" + if self.config_manager.get("AutoUpdateCheck", True): + thread = threading.Thread(target=self._perform_update_check, daemon=True) + thread.start() + + def _perform_update_check(self): + """执行实际的更新检查并向UI队列发送结果。""" + logging.info("正在检查更新...") + new_version = check_for_updates(CURRENT_VERSION) + if new_version: + logging.info(f"发现新版本: {new_version}") + self.ui_queue.put(("update_found", new_version)) + else: + logging.info("当前已是最新版本。") + + def _show_update_dialog(self, new_version: str): + """显示更新可用对话框,并根据用户选择打开下载页面。""" + title = "发现新版本" + message = f"发现新版本 {new_version}!\n您当前的版本是 {CURRENT_VERSION}。\n\n是否前往下载页面?" + if messagebox.askyesno(title, message): + try: + webbrowser.open("https://github.com/Eric-Terminal/Pro_llm_correct/releases/latest") + except Exception as e: + messagebox.showerror("打开失败", f"无法打开浏览器:{e}") diff --git a/config.json b/config.json deleted file mode 100644 index 9e26dfe..0000000 --- a/config.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/config_manager.py b/config_manager.py index bd38e6d..db56cb5 100644 --- a/config_manager.py +++ b/config_manager.py @@ -2,6 +2,8 @@ import json import os import base64 import hashlib +import platform +import subprocess from typing import Dict, Optional, Tuple, Any from cryptography.fernet import Fernet, InvalidToken @@ -24,9 +26,39 @@ class ConfigManager: # 确保默认渲染设置存在 self._ensure_default_render_settings() + def _get_device_identifier(self) -> str: + """ + 获取设备的唯一标识符(如序列号),用于加密。 + 这使得配置文件在另一台机器上无法解密。 + """ + system = platform.system() + try: + if system == "Windows": + # 使用wmic获取BIOS序列号 + return subprocess.check_output("wmic bios get serialnumber", shell=True).decode().split('\n')[1].strip() + elif system == "Darwin": # macOS + # 使用ioreg获取平台序列号 + return subprocess.check_output("ioreg -l | grep IOPlatformSerialNumber", shell=True).decode().split('"')[-2] + elif system == "Linux": + # 尝试获取DMI产品UUID,如果失败则使用/etc/machine-id + try: + return subprocess.check_output("sudo dmidecode -s system-serial-number", shell=True).decode().strip() + except Exception: + with open("/etc/machine-id", "r") as f: + return f.read().strip() + except Exception as e: + print(f"无法获取设备ID: {e},将使用默认值。") + # 在无法获取硬件ID时提供一个固定的后备值 + return "default-device-id-for-encryption" + return "unknown-device-for-security" + def _initialize_encryption(self): - """使用预设的密码和盐生成加密密钥,并初始化Fernet加密/解密实例。""" - kdf = hashlib.pbkdf2_hmac('sha256', self._ENCRYPTION_PASSWORD, self._SALT, 100000) + """使用预设密码和设备唯一的盐生成加密密钥,并初始化Fernet。""" + device_id = self._get_device_identifier() + # 将设备ID与固定的盐结合,为每台设备创建唯一的盐 + device_specific_salt = self._SALT + device_id.encode('utf-8') + + kdf = hashlib.pbkdf2_hmac('sha256', self._ENCRYPTION_PASSWORD, device_specific_salt, 100000) key = base64.urlsafe_b64encode(kdf) self._fernet = Fernet(key) diff --git a/requirements.txt b/requirements.txt index 7c51eed..846df44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cryptography openai -markdown \ No newline at end of file +markdown +packaging \ No newline at end of file