API密钥加密安全性改进以及自动更新功能

This commit is contained in:
Eric-Terminal
2025-09-18 02:03:16 +08:00
parent 7013346011
commit 5f637c191c
7 changed files with 120 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
# 工作流的名称 # 工作流的名称
name: Build Windows Executable name: Build Cross-Platform Executables
# Trivial change to force a cache refresh # Trivial change to force a cache refresh
# 触发条件: # 触发条件:
@@ -15,8 +15,10 @@ on:
jobs: jobs:
build: build:
# 指定运行环境为最新的Windows服务器 strategy:
runs-on: windows-latest matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
permissions: permissions:
contents: write contents: write
@@ -44,19 +46,18 @@ jobs:
# --onefile: 打包成单个.exe文件 # --onefile: 打包成单个.exe文件
# --name: 指定生成的可执行文件名 # --name: 指定生成的可执行文件名
- name: Build with PyInstaller - name: Build with PyInstaller
shell: cmd
run: pyinstaller --noconsole --onefile --name "AI-Essay-Corrector" main.py run: pyinstaller --noconsole --onefile --name "AI-Essay-Corrector" main.py
# 第5步将打包好的.exe上传,以便在工作流页面下载 (适合测试) # 第5步将打包好的上传以便在工作流页面下载 (适合测试)
- name: Upload artifact for testing - name: Upload artifact for testing
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: AI-Essay-Corrector-Windows-exe name: AI-Essay-Corrector-${{ matrix.os }}
path: dist/AI-Essay-Corrector.exe path: dist/AI-Essay-Corrector*
# 第6步 (专业级开源发布): 当你创建Tag时自动创建Release并附上.exe # 第6步 (专业级开源发布): 当你创建Tag时自动创建Release并附上.exe
- name: Create Release and Upload Asset - name: Create Release and Upload Asset
if: startsWith(github.ref, 'refs/tags/') # 仅在创建Tag时运行此步骤 if: startsWith(github.ref, 'refs/tags/') # 仅在创建Tag时运行此步骤
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist/AI-Essay-Corrector.exe files: dist/AI-Essay-Corrector*

View File

@@ -40,7 +40,7 @@
--- ---
## 📖 使用指南 ## 使用指南
### 快速开始 ### 快速开始
1. **下载程序**: 前往 [Releases页面](https://github.com/Eric-Terminal/Pro_llm_correct/releases) 下载最新版本 1. **下载程序**: 前往 [Releases页面](https://github.com/Eric-Terminal/Pro_llm_correct/releases) 下载最新版本

View File

@@ -1,5 +1,8 @@
import base64 import base64
from typing import Dict, Any, Tuple, Optional from typing import Dict, Any, Tuple, Optional
import urllib.request
import json
from packaging import version
from config_manager import ConfigManager from config_manager import ConfigManager
from markdown_renderer import create_markdown_renderer from markdown_renderer import create_markdown_renderer
import os import os
@@ -261,3 +264,26 @@ Strictly adhere to the following format. Do not output anything else.
self._log(f"已生成HTML报告: {os.path.basename(html_path)}") self._log(f"已生成HTML报告: {os.path.basename(html_path)}")
return final_report, vlm_usage, llm_usage, html_path 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

View File

@@ -6,12 +6,15 @@ import os
from typing import List from typing import List
import concurrent.futures import concurrent.futures
import logging import logging
import webbrowser
from config_manager import ConfigManager 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') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
CURRENT_VERSION = "3.2.0"
class AboutDialog(tk.Toplevel): class AboutDialog(tk.Toplevel):
"""“关于”对话框,展示应用信息,支持滚动查看。""" """“关于”对话框,展示应用信息,支持滚动查看。"""
def __init__(self, parent, config_manager: ConfigManager): def __init__(self, parent, config_manager: ConfigManager):
@@ -75,7 +78,7 @@ class AboutDialog(tk.Toplevel):
作者: Eric_Terminal 作者: Eric_Terminal
项目地址: https://github.com/Eric-Terminal/Pro_llm_correct 项目地址: https://github.com/Eric-Terminal/Pro_llm_correct
版本: 3.2 版本: {CURRENT_VERSION}
--- ---
历史Token使用统计: 历史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.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_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_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.sensitivity_factor = tk.StringVar(value=config_manager.get("SensitivityFactor", "1.5"))
self.max_workers = tk.StringVar(value=config_manager.get("MaxWorkers", "4")) self.max_workers = tk.StringVar(value=config_manager.get("MaxWorkers", "4"))
self.max_retries = tk.StringVar(value=config_manager.get("MaxRetries", "3")) self.max_retries = tk.StringVar(value=config_manager.get("MaxRetries", "3"))
self.retry_delay = tk.StringVar(value=config_manager.get("RetryDelay", "5")) self.retry_delay = tk.StringVar(value=config_manager.get("RetryDelay", "5"))
self.save_markdown = tk.BooleanVar(value=config_manager.get("SaveMarkdown", True)) self.save_markdown = tk.BooleanVar(value=config_manager.get("SaveMarkdown", True))
self.render_markdown = tk.BooleanVar(value=config_manager.get("RenderMarkdown", 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模板优先使用用户自定义模板否则使用默认模板 # 智能加载Prompt模板优先使用用户自定义模板否则使用默认模板
user_template = config_manager.get("LlmPromptTemplate") 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.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.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.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.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)) 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.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.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.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.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)) 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.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.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.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模板编辑区域 # LLM Prompt模板编辑区域
prompt_frame = ttk.LabelFrame(frame, text="LLM Prompt 模板 (可在此修改,请勿修改{}占位符内容导致程序参数无法正常传递,通常情况下修改总分即可)", padding="10") prompt_frame = ttk.LabelFrame(frame, text="LLM Prompt 模板 (可在此修改,请勿修改{}占位符内容导致程序参数无法正常传递,通常情况下修改总分即可)", padding="10")
@@ -208,6 +214,7 @@ class SettingsDialog(tk.Toplevel):
"RetryDelay": self.retry_delay.get(), "RetryDelay": self.retry_delay.get(),
"SaveMarkdown": self.save_markdown.get(), "SaveMarkdown": self.save_markdown.get(),
"RenderMarkdown": self.render_markdown.get(), "RenderMarkdown": self.render_markdown.get(),
"AutoUpdateCheck": self.auto_update_check.get(),
"LlmPromptTemplate": self.llm_prompt_text.get("1.0", "end-1c") "LlmPromptTemplate": self.llm_prompt_text.get("1.0", "end-1c")
} }
@@ -238,6 +245,7 @@ class MainApp:
self._setup_ui() self._setup_ui()
self._initialize_config() self._initialize_config()
self._check_for_updates_on_startup()
self.root.after(100, self._process_ui_queue) self.root.after(100, self._process_ui_queue)
def _setup_ui(self): def _setup_ui(self):
@@ -328,9 +336,14 @@ class MainApp:
is_ok, missing_item = self.config_manager.check_settings() is_ok, missing_item = self.config_manager.check_settings()
if is_ok: if is_ok:
return 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() self.root.quit()
return return
# 用户点击重试,继续循环
def _open_settings_dialog(self): def _open_settings_dialog(self):
"""打开设置对话框,并根据返回结果更新和保存配置。""" """打开设置对话框,并根据返回结果更新和保存配置。"""
@@ -401,6 +414,8 @@ class MainApp:
messagebox.showinfo("完成", "所有文件处理完成") messagebox.showinfo("完成", "所有文件处理完成")
self.progress_bar['value'] = 0 self.progress_bar['value'] = 0
self.is_file_selected = False self.is_file_selected = False
elif task == "update_found":
self._show_update_dialog(data)
except queue.Empty: except queue.Empty:
pass pass
finally: finally:
@@ -474,3 +489,29 @@ class MainApp:
# 无论成功或失败,都更新进度 # 无论成功或失败,都更新进度
self.ui_queue.put(("progress", 1)) 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}")

View File

@@ -1 +0,0 @@
{}

View File

@@ -2,6 +2,8 @@ import json
import os import os
import base64 import base64
import hashlib import hashlib
import platform
import subprocess
from typing import Dict, Optional, Tuple, Any from typing import Dict, Optional, Tuple, Any
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
@@ -24,9 +26,39 @@ class ConfigManager:
# 确保默认渲染设置存在 # 确保默认渲染设置存在
self._ensure_default_render_settings() 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): def _initialize_encryption(self):
"""使用预设密码和盐生成加密密钥并初始化Fernet加密/解密实例""" """使用预设密码和设备唯一的盐生成加密密钥并初始化Fernet。"""
kdf = hashlib.pbkdf2_hmac('sha256', self._ENCRYPTION_PASSWORD, self._SALT, 100000) 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) key = base64.urlsafe_b64encode(kdf)
self._fernet = Fernet(key) self._fernet = Fernet(key)

View File

@@ -1,3 +1,4 @@
cryptography cryptography
openai openai
markdown markdown
packaging