API密钥加密安全性改进以及自动更新功能
This commit is contained in:
@@ -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*
|
||||||
@@ -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) 下载最新版本
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -260,4 +263,27 @@ Strictly adhere to the following format. Do not output anything else.
|
|||||||
if html_path:
|
if html_path:
|
||||||
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
|
||||||
53
app_ui.py
53
app_ui.py
@@ -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}")
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
cryptography
|
cryptography
|
||||||
openai
|
openai
|
||||||
markdown
|
markdown
|
||||||
|
packaging
|
||||||
Reference in New Issue
Block a user