添加项目源代码
This commit is contained in:
5
__init__.py
Normal file
5
__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
听泉助手 - 脚本包
|
||||
"""
|
||||
|
||||
__version__ = "4.0.0.1"
|
||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/common_utils.cpython-312.pyc
Normal file
BIN
__pycache__/common_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/cursor_token_refresher.cpython-312.pyc
Normal file
BIN
__pycache__/cursor_token_refresher.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/exit_cursor.cpython-312.pyc
Normal file
BIN
__pycache__/exit_cursor.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/logger.cpython-312.pyc
Normal file
BIN
__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/machine_resetter.cpython-312.pyc
Normal file
BIN
__pycache__/machine_resetter.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/update_disabler.cpython-312.pyc
Normal file
BIN
__pycache__/update_disabler.cpython-312.pyc
Normal file
Binary file not shown.
113
build.bat
Normal file
113
build.bat
Normal file
@@ -0,0 +1,113 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
setlocal EnableDelayedExpansion
|
||||
echo 开始正式版本打包...
|
||||
|
||||
:: 设置工作目录为脚本所在目录
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: 激活虚拟环境
|
||||
if exist "venv\Scripts\activate.bat" (
|
||||
echo 激活虚拟环境...
|
||||
call venv\Scripts\activate.bat
|
||||
) else (
|
||||
echo 警告: 未找到虚拟环境,使用系统 Python
|
||||
)
|
||||
|
||||
:: 确保安装了必要的包
|
||||
echo 检查依赖包...
|
||||
python -c "import PyInstaller" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 正在安装 PyInstaller...
|
||||
python -m pip install pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
)
|
||||
|
||||
:: 如果存在requirements.txt,安装依赖
|
||||
if exist "requirements.txt" (
|
||||
echo 安装项目依赖...
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
)
|
||||
|
||||
:: 设置基础变量
|
||||
set VERSION_FILE=version.txt
|
||||
set SPEC_FILE=tingquan_assistant.spec
|
||||
|
||||
:: 读取版本号
|
||||
set /p VERSION=<%VERSION_FILE%
|
||||
echo 当前版本: !VERSION!
|
||||
|
||||
:: 提取主版本号和次版本号 (4.0.0.1 -> 4.0)
|
||||
for /f "tokens=1,2 delims=." %%a in ("!VERSION!") do (
|
||||
set MAJOR_VERSION=%%a.%%b
|
||||
)
|
||||
echo 主版本目录: !MAJOR_VERSION!
|
||||
|
||||
:: 创建版本目录
|
||||
set VERSION_DIR=dist\!MAJOR_VERSION!
|
||||
if not exist "!VERSION_DIR!" (
|
||||
mkdir "!VERSION_DIR!"
|
||||
echo 创建目录: !VERSION_DIR!
|
||||
)
|
||||
|
||||
:: 清理 Python 缓存文件
|
||||
echo 清理Python缓存文件...
|
||||
for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d"
|
||||
del /s /q *.pyc >nul 2>&1
|
||||
del /s /q *.pyo >nul 2>&1
|
||||
|
||||
:: 清理旧的打包文件
|
||||
echo 清理旧文件...
|
||||
taskkill /F /IM "听泉助手*.exe" >nul 2>&1
|
||||
timeout /t 2 /nobreak >nul
|
||||
if exist "build" rd /s /q "build"
|
||||
if exist "dist" rd /s /q "dist"
|
||||
|
||||
:: 使用优化选项进行打包
|
||||
echo 开始打包...
|
||||
python -m PyInstaller !SPEC_FILE! --noconfirm --clean
|
||||
|
||||
:: 检查打包结果并移动文件
|
||||
set BUILD_DIR=dist\听泉助手v!VERSION!
|
||||
set TARGET_FILE=!VERSION_DIR!\听泉助手v!VERSION!.exe
|
||||
|
||||
echo 检查构建目录: !BUILD_DIR!
|
||||
if exist "!BUILD_DIR!" (
|
||||
echo 正式版本打包成功!
|
||||
|
||||
:: 创建目标目录(如果不存在)
|
||||
if not exist "!VERSION_DIR!" mkdir "!VERSION_DIR!"
|
||||
|
||||
:: 移动整个目录到版本目录
|
||||
echo 移动文件到: !VERSION_DIR!
|
||||
xcopy "!BUILD_DIR!\*" "!VERSION_DIR!" /E /I /Y
|
||||
|
||||
:: 显示文件大小
|
||||
for %%I in ("!TARGET_FILE!") do (
|
||||
echo 文件大小: %%~zI 字节
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 正式版本构建完成!
|
||||
echo 版本号: v!VERSION!
|
||||
echo 文件位置: !TARGET_FILE!
|
||||
) else (
|
||||
echo 错误: 打包失败,构建目录不存在
|
||||
echo 预期构建目录: !BUILD_DIR!
|
||||
dir /b dist
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 清理临时文件
|
||||
echo 清理临时文件...
|
||||
if exist "build" rd /s /q "build"
|
||||
if exist "dist" rd /s /q "dist"
|
||||
|
||||
:: 退出虚拟环境
|
||||
if exist "venv\Scripts\activate.bat" (
|
||||
echo 退出虚拟环境...
|
||||
deactivate
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 按任意键退出...
|
||||
pause >nul
|
||||
420
common_utils.py
Normal file
420
common_utils.py
Normal file
@@ -0,0 +1,420 @@
|
||||
"""通用工具函数库"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import subprocess
|
||||
import platform
|
||||
import json
|
||||
import requests
|
||||
import urllib3
|
||||
import uuid
|
||||
import winreg
|
||||
import sqlite3
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from logger import logger
|
||||
from config import config
|
||||
|
||||
# 获取日志记录器
|
||||
_logger = logger.get_logger("CommonUtils")
|
||||
|
||||
# 全局用户状态
|
||||
_global_user_state = {
|
||||
"is_activated": False,
|
||||
"expire_time": None,
|
||||
"days_left": 0,
|
||||
"total_days": 0,
|
||||
"device_info": {},
|
||||
"status": "inactive"
|
||||
}
|
||||
|
||||
# 设备信息缓存
|
||||
_device_info_cache = None
|
||||
|
||||
def _get_cached_device_info() -> Dict:
|
||||
"""
|
||||
获取设备信息(使用缓存)
|
||||
只在首次调用时获取系统信息,后续使用缓存
|
||||
|
||||
Returns:
|
||||
Dict: 设备基础信息
|
||||
"""
|
||||
global _device_info_cache
|
||||
|
||||
if _device_info_cache is None:
|
||||
try:
|
||||
# 基础系统信息
|
||||
device_info = {
|
||||
"os": platform.system(),
|
||||
"device_name": platform.node(),
|
||||
"ip": "未知",
|
||||
"location": "未知"
|
||||
}
|
||||
|
||||
# 获取IP和位置信息
|
||||
try:
|
||||
# 使用ip-api.com的免费API获取IP和位置信息
|
||||
response = requests.get('http://ip-api.com/json/?lang=zh-CN', timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get('status') == 'success':
|
||||
device_info.update({
|
||||
"ip": data.get('query', '未知'),
|
||||
"location": f"{data.get('country', '')} {data.get('regionName', '')} {data.get('city', '')}"
|
||||
})
|
||||
except Exception as e:
|
||||
_logger.warning(f"获取IP和位置信息失败: {str(e)}")
|
||||
|
||||
_device_info_cache = device_info
|
||||
_logger.debug("已初始化设备信息缓存")
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"获取设备信息失败: {str(e)}")
|
||||
_device_info_cache = {
|
||||
"os": "Windows",
|
||||
"device_name": "未知",
|
||||
"ip": "未知",
|
||||
"location": "未知"
|
||||
}
|
||||
|
||||
return _device_info_cache.copy()
|
||||
|
||||
def update_user_state(state_data: Dict) -> None:
|
||||
"""
|
||||
更新全局用户状态
|
||||
Args:
|
||||
state_data: 新的状态数据
|
||||
"""
|
||||
global _global_user_state
|
||||
|
||||
# 保留现有的设备信息
|
||||
current_device_info = _global_user_state.get("device_info", {})
|
||||
|
||||
# 更新状态数据
|
||||
_global_user_state.update(state_data)
|
||||
|
||||
# 合并设备信息
|
||||
if "device_info" in state_data:
|
||||
new_device_info = state_data["device_info"]
|
||||
# 只更新可能变动的信息(IP和地区)
|
||||
current_device_info.update({
|
||||
"ip": new_device_info.get("ip", current_device_info.get("ip", "未知")),
|
||||
"location": new_device_info.get("location", current_device_info.get("location", "未知"))
|
||||
})
|
||||
_global_user_state["device_info"] = current_device_info
|
||||
|
||||
_logger.info(f"用户状态已更新: {state_data}")
|
||||
|
||||
def get_user_state() -> Dict:
|
||||
"""
|
||||
获取当前用户状态
|
||||
Returns:
|
||||
Dict: 用户状态信息
|
||||
"""
|
||||
return _global_user_state.copy()
|
||||
|
||||
def check_user_state() -> bool:
|
||||
"""
|
||||
检查用户是否处于可操作状态
|
||||
Returns:
|
||||
bool: 是否可以操作
|
||||
"""
|
||||
return _global_user_state.get("is_activated", False)
|
||||
|
||||
def refresh_user_state(machine_id: str) -> Tuple[bool, str, Dict]:
|
||||
"""
|
||||
检查设备激活状态
|
||||
|
||||
通过设备ID向服务器请求验证设备的激活状态、剩余时间等信息。
|
||||
服务器会进行以下检查:
|
||||
1. 设备ID是否合法
|
||||
2. 是否已激活
|
||||
3. 激活是否过期
|
||||
4. 剩余使用时间
|
||||
|
||||
Args:
|
||||
machine_id: 设备唯一标识
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, Dict]:
|
||||
- bool: 是否验证通过
|
||||
- str: 状态消息
|
||||
- Dict: 状态数据
|
||||
"""
|
||||
try:
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings()
|
||||
|
||||
# 准备请求数据
|
||||
data = {"machine_id": machine_id}
|
||||
|
||||
# 发送状态检查请求
|
||||
response = requests.post(
|
||||
config.status_url,
|
||||
json=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") in [1, 200]:
|
||||
api_data = result.get("data", {})
|
||||
|
||||
# 获取缓存的设备信息
|
||||
device_info = _get_cached_device_info()
|
||||
# 只更新可能变动的信息
|
||||
device_info.update({
|
||||
"ip": api_data.get("ip", device_info["ip"]),
|
||||
"location": api_data.get("location", device_info["location"])
|
||||
})
|
||||
|
||||
# 更新状态数据
|
||||
state_data = {
|
||||
"is_activated": api_data.get("status") == "active",
|
||||
"status": api_data.get("status", "inactive"),
|
||||
"expire_time": api_data.get("expire_time", ""),
|
||||
"days_left": api_data.get("days_left", 0),
|
||||
"total_days": api_data.get("total_days", 0),
|
||||
"device_info": device_info
|
||||
}
|
||||
|
||||
# 更新全局状态
|
||||
update_user_state(state_data)
|
||||
|
||||
# 根据状态返回对应消息
|
||||
if state_data["is_activated"]:
|
||||
msg = f"设备已激活,剩余{state_data['days_left']}天"
|
||||
else:
|
||||
if api_data.get("status") == "expired":
|
||||
msg = "设备授权已过期"
|
||||
else:
|
||||
msg = "设备未激活"
|
||||
|
||||
return True, msg, state_data
|
||||
|
||||
else:
|
||||
error_msg = result.get("msg", "未知错误")
|
||||
_logger.error(f"设备状态检查失败: {error_msg}")
|
||||
return False, f"状态检查失败: {error_msg}", {}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
_logger.error(f"设备状态检查异常: {error_msg}")
|
||||
return False, f"状态检查异常: {error_msg}", {}
|
||||
|
||||
def get_hardware_id() -> str:
|
||||
"""获取硬件唯一标识
|
||||
方案1: CPU ID + 主板序列号 + BIOS序列号
|
||||
方案2: 系统盘序列号 + Windows安装时间
|
||||
方案3: 计算机名(最后的备选方案)
|
||||
|
||||
Returns:
|
||||
str: 硬件ID的MD5哈希值
|
||||
|
||||
Raises:
|
||||
RuntimeError: 当无法获取任何可用的硬件信息时
|
||||
"""
|
||||
try:
|
||||
# 创建startupinfo对象来隐藏命令行窗口
|
||||
startupinfo = None
|
||||
if sys.platform == "win32":
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
# 方案1: 尝试获取硬件信息
|
||||
try:
|
||||
# 获取CPU ID
|
||||
cpu_info = subprocess.check_output('wmic cpu get ProcessorId', startupinfo=startupinfo).decode()
|
||||
cpu_id = cpu_info.split('\n')[1].strip()
|
||||
|
||||
# 获取主板序列号
|
||||
board_info = subprocess.check_output('wmic baseboard get SerialNumber', startupinfo=startupinfo).decode()
|
||||
board_id = board_info.split('\n')[1].strip()
|
||||
|
||||
# 获取BIOS序列号
|
||||
bios_info = subprocess.check_output('wmic bios get SerialNumber', startupinfo=startupinfo).decode()
|
||||
bios_id = bios_info.split('\n')[1].strip()
|
||||
|
||||
# 如果所有信息都获取成功且有效
|
||||
if all([cpu_id, board_id, bios_id]) and not all(x in ['', '0', 'None', 'To be filled by O.E.M.'] for x in [cpu_id, board_id, bios_id]):
|
||||
combined = f"{cpu_id}:{board_id}:{bios_id}"
|
||||
hardware_id = hashlib.md5(combined.encode()).hexdigest()
|
||||
_logger.info("使用硬件信息生成ID成功")
|
||||
return hardware_id
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(f"方案1失败: {str(e)}")
|
||||
|
||||
# 方案2: 系统盘序列号 + Windows安装时间
|
||||
try:
|
||||
backup_info = []
|
||||
|
||||
# 获取系统盘序列号
|
||||
volume_info = subprocess.check_output('wmic logicaldisk where "DeviceID=\'C:\'" get VolumeSerialNumber', startupinfo=startupinfo).decode()
|
||||
volume_serial = volume_info.split('\n')[1].strip()
|
||||
if volume_serial and volume_serial not in ['', '0']:
|
||||
backup_info.append(("volume", volume_serial))
|
||||
|
||||
# 获取Windows安装时间
|
||||
os_info = subprocess.check_output('wmic os get InstallDate', startupinfo=startupinfo).decode()
|
||||
install_date = os_info.split('\n')[1].strip()
|
||||
if install_date:
|
||||
backup_info.append(("install", install_date))
|
||||
|
||||
if backup_info:
|
||||
combined = "|".join(f"{k}:{v}" for k, v in sorted(backup_info))
|
||||
hardware_id = hashlib.md5(combined.encode()).hexdigest()
|
||||
_logger.info("使用系统信息生成ID成功")
|
||||
return hardware_id
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(f"方案2失败: {str(e)}")
|
||||
|
||||
# 方案3: 使用计算机名(最后的备选方案)
|
||||
computer_name = platform.node()
|
||||
if computer_name:
|
||||
hardware_id = hashlib.md5(computer_name.encode()).hexdigest()
|
||||
_logger.info("使用计算机名生成ID成功")
|
||||
return hardware_id
|
||||
|
||||
raise ValueError("无法获取任何可用信息来生成硬件ID")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"生成硬件ID失败: {str(e)}"
|
||||
_logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
def verify_hardware_id(stored_id: str) -> bool:
|
||||
"""
|
||||
验证当前硬件ID是否与存储的ID匹配
|
||||
|
||||
Args:
|
||||
stored_id: 存储的硬件ID
|
||||
|
||||
Returns:
|
||||
bool: 如果匹配返回True,否则返回False
|
||||
"""
|
||||
try:
|
||||
current_id = get_hardware_id()
|
||||
return current_id == stored_id
|
||||
except Exception as e:
|
||||
_logger.error(f"验证硬件ID失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_system_info() -> dict:
|
||||
"""
|
||||
获取系统信息
|
||||
|
||||
Returns:
|
||||
dict: 包含系统信息的字典
|
||||
"""
|
||||
info = {
|
||||
"os": platform.system(),
|
||||
"os_version": platform.version(),
|
||||
"machine": platform.machine(),
|
||||
"processor": platform.processor(),
|
||||
"node": platform.node()
|
||||
}
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
# 获取更多Windows特定信息
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
# Windows产品ID
|
||||
windows_info = subprocess.check_output('wmic os get SerialNumber', startupinfo=startupinfo).decode()
|
||||
info["windows_id"] = windows_info.split('\n')[1].strip()
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(f"获取Windows特定信息失败: {str(e)}")
|
||||
|
||||
return info
|
||||
|
||||
def activate_device(machine_id: str, activation_code: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||
"""
|
||||
激活设备
|
||||
|
||||
Args:
|
||||
machine_id: 设备ID
|
||||
activation_code: 激活码
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, Optional[Dict]]:
|
||||
- bool: 是否成功
|
||||
- str: 消息
|
||||
- Optional[Dict]: 激活数据
|
||||
"""
|
||||
try:
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings()
|
||||
|
||||
# 准备请求数据
|
||||
data = {
|
||||
"machine_id": machine_id,
|
||||
"code": activation_code
|
||||
}
|
||||
|
||||
# 发送激活请求
|
||||
response = requests.post(
|
||||
config.activate_url,
|
||||
json=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept": "*/*"
|
||||
},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 200:
|
||||
api_data = result.get("data", {})
|
||||
|
||||
# 获取缓存的设备信息
|
||||
device_info = _get_cached_device_info()
|
||||
|
||||
# 构造状态数据
|
||||
state_data = {
|
||||
"is_activated": True,
|
||||
"status": "active",
|
||||
"expire_time": api_data.get("expire_time", ""),
|
||||
"days_left": api_data.get("days_left", 0),
|
||||
"total_days": api_data.get("total_days", 0),
|
||||
"device_info": device_info
|
||||
}
|
||||
|
||||
# 更新全局状态
|
||||
update_user_state(state_data)
|
||||
|
||||
return True, f"激活成功,剩余{state_data['days_left']}天", state_data
|
||||
|
||||
elif result.get("code") == 400:
|
||||
error_msg = result.get("msg", "激活码无效或已被使用")
|
||||
_logger.warning(f"激活失败: {error_msg}")
|
||||
return False, error_msg, None
|
||||
|
||||
else:
|
||||
error_msg = result.get("msg", "未知错误")
|
||||
_logger.error(f"激活失败: {error_msg}")
|
||||
return False, f"激活失败: {error_msg}", None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"网络连接失败: {str(e)}"
|
||||
_logger.error(error_msg)
|
||||
return False, error_msg, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"激活失败: {str(e)}"
|
||||
_logger.error(error_msg)
|
||||
return False, error_msg, None
|
||||
54
config.py
Normal file
54
config.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import Dict
|
||||
|
||||
class Config:
|
||||
"""配置管理类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化配置"""
|
||||
# API基础URL
|
||||
self.base_url = "https://cursorapi.nosqli.com"
|
||||
|
||||
# API端点
|
||||
self.api_endpoints = {
|
||||
"get_unused": f"{self.base_url}/admin/api.account/getUnused",
|
||||
"activate": f"{self.base_url}/admin/api.member/activate",
|
||||
"status": f"{self.base_url}/admin/api.member/status",
|
||||
"heartbeat": f"{self.base_url}/admin/api.account/heartbeat"
|
||||
}
|
||||
|
||||
# API请求配置
|
||||
self.request_config = {
|
||||
"timeout": 30,
|
||||
"verify": False,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def get_unused_url(self) -> str:
|
||||
"""获取未使用账号的API地址"""
|
||||
return self.api_endpoints["get_unused"]
|
||||
|
||||
@property
|
||||
def activate_url(self) -> str:
|
||||
"""获取激活账号的API地址"""
|
||||
return self.api_endpoints["activate"]
|
||||
|
||||
@property
|
||||
def status_url(self) -> str:
|
||||
"""获取状态检查的API地址"""
|
||||
return self.api_endpoints["status"]
|
||||
|
||||
@property
|
||||
def heartbeat_url(self) -> str:
|
||||
"""获取心跳检测的API地址"""
|
||||
return self.api_endpoints["heartbeat"]
|
||||
|
||||
@property
|
||||
def request_kwargs(self) -> Dict:
|
||||
"""获取请求配置"""
|
||||
return self.request_config.copy()
|
||||
|
||||
# 创建全局配置实例
|
||||
config = Config()
|
||||
145
cursor_token_refresher.py
Normal file
145
cursor_token_refresher.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import requests
|
||||
import urllib3
|
||||
from typing import Dict, Tuple, Optional
|
||||
from datetime import datetime
|
||||
from config import config
|
||||
from logger import logger
|
||||
|
||||
class CursorTokenRefresher:
|
||||
"""Cursor Token 刷新器 - 简化版"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化刷新器"""
|
||||
# 设置数据库路径
|
||||
if sys.platform == "win32":
|
||||
appdata = os.getenv("APPDATA")
|
||||
if appdata is None:
|
||||
raise EnvironmentError("APPDATA 环境变量未设置")
|
||||
self.db_path = os.path.join(appdata, "Cursor", "User", "globalStorage", "state.vscdb")
|
||||
else:
|
||||
raise NotImplementedError(f"暂不支持的操作系统: {sys.platform}")
|
||||
|
||||
# 获取日志记录器
|
||||
self.logger = logger.get_logger("TokenRefresh")
|
||||
|
||||
def update_auth(self, email: Optional[str] = None,
|
||||
access_token: Optional[str] = None,
|
||||
refresh_token: Optional[str] = None) -> bool:
|
||||
"""
|
||||
更新SQLite数据库中的认证信息
|
||||
:param email: 可选,新的邮箱地址
|
||||
:param access_token: 可选,新的访问令牌
|
||||
:param refresh_token: 可选,新的刷新令牌
|
||||
:return: 是否成功更新
|
||||
"""
|
||||
updates = []
|
||||
# 登录状态
|
||||
updates.append(("cursorAuth/cachedSignUpType", "Auth_0"))
|
||||
|
||||
if email is not None:
|
||||
updates.append(("cursorAuth/cachedEmail", email))
|
||||
if access_token is not None:
|
||||
updates.append(("cursorAuth/accessToken", access_token))
|
||||
if refresh_token is not None:
|
||||
updates.append(("cursorAuth/refreshToken", refresh_token))
|
||||
|
||||
if not updates:
|
||||
self.logger.warning("没有提供任何要更新的值")
|
||||
return False
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
for key, value in updates:
|
||||
# 如果没有更新任何行,说明key不存在,执行插入
|
||||
# 检查 key 是否存在
|
||||
check_query = "SELECT COUNT(*) FROM itemTable WHERE key = ?"
|
||||
cursor.execute(check_query, (key,))
|
||||
if cursor.fetchone()[0] == 0:
|
||||
insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)"
|
||||
cursor.execute(insert_query, (key, value))
|
||||
self.logger.info(f"已插入新记录: {key.split('/')[-1]}")
|
||||
else:
|
||||
update_query = "UPDATE itemTable SET value = ? WHERE key = ?"
|
||||
cursor.execute(update_query, (value, key))
|
||||
if cursor.rowcount > 0:
|
||||
self.logger.info(f"成功更新 {key.split('/')[-1]}")
|
||||
else:
|
||||
self.logger.warning(f"未找到 {key.split('/')[-1]} 或值未变化")
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
self.logger.error(f"数据库错误: {str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"发生错误: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def refresh_token(self, machine_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||
"""
|
||||
刷新token(从API获取新账号并更新到数据库)
|
||||
:param machine_id: 机器ID
|
||||
:return: (是否成功, 消息, 可选的账号信息)
|
||||
"""
|
||||
try:
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# 发送请求
|
||||
response = requests.post(
|
||||
config.get_unused_url,
|
||||
json={"machine_id": machine_id},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
|
||||
# 解析响应
|
||||
response_data = response.json()
|
||||
|
||||
if response_data.get("code") == 200:
|
||||
account_data = response_data.get("data", {})
|
||||
|
||||
# 获取账号信息
|
||||
email = account_data.get("email")
|
||||
access_token = account_data.get("access_token")
|
||||
refresh_token = account_data.get("refresh_token")
|
||||
expire_time = account_data.get("expire_time")
|
||||
days_left = account_data.get("days_left")
|
||||
|
||||
if not all([email, access_token, refresh_token]):
|
||||
return False, "获取账号信息不完整", None
|
||||
|
||||
# 更新SQLite数据库
|
||||
if self.update_auth(email=email,
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token):
|
||||
account_info = {
|
||||
"email": email,
|
||||
"expire_time": expire_time,
|
||||
"days_left": days_left
|
||||
}
|
||||
return True, "认证信息更新成功", account_info
|
||||
else:
|
||||
return False, "更新认证信息失败", None
|
||||
|
||||
elif response_data.get("code") == 404:
|
||||
return False, "没有可用的未使用账号", None
|
||||
else:
|
||||
error_msg = response_data.get("msg", "未知错误")
|
||||
return False, f"获取账号失败: {error_msg}", None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self.logger.error(f"刷新token失败: {error_msg}")
|
||||
return False, f"刷新token失败: {error_msg}", None
|
||||
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140_1.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140_1.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Core.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Core.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Gui.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Gui.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Widgets.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Widgets.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/VCRUNTIME140_1.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/VCRUNTIME140_1.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/platforms/qwindows.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/platforms/qwindows.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/styles/qwindowsvistastyle.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/styles/qwindowsvistastyle.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/QtCore.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/QtCore.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/QtGui.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/QtGui.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/QtWidgets.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/QtWidgets.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/PyQt5/sip.cp312-win_amd64.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/PyQt5/sip.cp312-win_amd64.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/VCRUNTIME140.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/VCRUNTIME140.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_brotli.cp312-win_amd64.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_brotli.cp312-win_amd64.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_bz2.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_bz2.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_ctypes.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_ctypes.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_hashlib.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_hashlib.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_lzma.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_lzma.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_queue.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_queue.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_socket.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_socket.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_sqlite3.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_sqlite3.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_ssl.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_ssl.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_uuid.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_uuid.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/_wmi.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/_wmi.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/charset_normalizer/md.cp312-win_amd64.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/charset_normalizer/md.cp312-win_amd64.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/charset_normalizer/md__mypyc.cp312-win_amd64.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/charset_normalizer/md__mypyc.cp312-win_amd64.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/libcrypto-3.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/libcrypto-3.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/libffi-8.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/libffi-8.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/libssl-3.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/libssl-3.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/psutil/_psutil_windows.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/psutil/_psutil_windows.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/python3.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/python3.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/python312.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/python312.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/select.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/select.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/sqlite3.dll
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/sqlite3.dll
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/_internal/unicodedata.pyd
vendored
Normal file
BIN
dist/tingquan_assistant/_internal/unicodedata.pyd
vendored
Normal file
Binary file not shown.
BIN
dist/tingquan_assistant/tingquan_assistant.exe
vendored
Normal file
BIN
dist/tingquan_assistant/tingquan_assistant.exe
vendored
Normal file
Binary file not shown.
68
exit_cursor.py
Normal file
68
exit_cursor.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import psutil
|
||||
import logging
|
||||
import time
|
||||
|
||||
def ExitCursor(timeout=5):
|
||||
"""
|
||||
温和地关闭 Cursor 进程
|
||||
Args:
|
||||
timeout (int): 等待进程自然终止的超时时间(秒)
|
||||
Returns:
|
||||
bool: 是否成功关闭所有进程
|
||||
"""
|
||||
try:
|
||||
logging.info("开始退出Cursor...")
|
||||
cursor_processes = []
|
||||
|
||||
# 收集所有 Cursor 进程
|
||||
for proc in psutil.process_iter(['pid', 'name']):
|
||||
try:
|
||||
if proc.info['name'].lower() in ['cursor.exe', 'cursor']:
|
||||
cursor_processes.append(proc)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
if not cursor_processes:
|
||||
logging.info("未发现运行中的 Cursor 进程")
|
||||
return True
|
||||
|
||||
# 温和地请求进程终止
|
||||
for proc in cursor_processes:
|
||||
try:
|
||||
if proc.is_running():
|
||||
proc.terminate() # 发送终止信号
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
# 等待进程自然终止
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
still_running = []
|
||||
for proc in cursor_processes:
|
||||
try:
|
||||
if proc.is_running():
|
||||
still_running.append(proc)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
if not still_running:
|
||||
logging.info("所有 Cursor 进程已正常关闭")
|
||||
return True
|
||||
|
||||
# 等待一小段时间再检查
|
||||
time.sleep(0.5)
|
||||
|
||||
# 如果超时后仍有进程在运行
|
||||
if still_running:
|
||||
process_list = ", ".join([str(p.pid) for p in still_running])
|
||||
logging.warning(f"以下进程未能在规定时间内关闭: {process_list}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"关闭 Cursor 进程时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
ExitCursor()
|
||||
43
file_version_info.txt
Normal file
43
file_version_info.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
# UTF-8
|
||||
#
|
||||
# For more details about fixed file info 'ffi' see:
|
||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
||||
# Set not needed items to zero 0.
|
||||
filevers=(4, 0, 0, 1),
|
||||
prodvers=(4, 0, 0, 1),
|
||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||
mask=0x3f,
|
||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||
flags=0x0,
|
||||
# The operating system for which this file was designed.
|
||||
# 0x4 - NT and there is no need to change it.
|
||||
OS=0x40004,
|
||||
# The general type of file.
|
||||
# 0x1 - the file is an application.
|
||||
fileType=0x1,
|
||||
# The function of the file.
|
||||
# 0x0 - the function is not defined for this fileType
|
||||
subtype=0x0,
|
||||
# Creation date and time stamp.
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'080404b0',
|
||||
[StringStruct(u'CompanyName', u'听泉助手'),
|
||||
StringStruct(u'FileDescription', u'听泉助手 - Cursor激活管理工具'),
|
||||
StringStruct(u'FileVersion', u'4.0.0.1'),
|
||||
StringStruct(u'InternalName', u'听泉助手'),
|
||||
StringStruct(u'LegalCopyright', u'Copyright (C) 2024'),
|
||||
StringStruct(u'OriginalFilename', u'听泉助手.exe'),
|
||||
StringStruct(u'ProductName', u'听泉助手'),
|
||||
StringStruct(u'ProductVersion', u'4.0.0.1')])
|
||||
]),
|
||||
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
|
||||
]
|
||||
)
|
||||
1
gui/__init__.py
Normal file
1
gui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""GUI 包"""
|
||||
BIN
gui/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
gui/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
1
gui/components/__init__.py
Normal file
1
gui/components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""GUI 组件包"""
|
||||
BIN
gui/components/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
gui/components/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
gui/components/__pycache__/widgets.cpython-312.pyc
Normal file
BIN
gui/components/__pycache__/widgets.cpython-312.pyc
Normal file
Binary file not shown.
BIN
gui/components/__pycache__/workers.cpython-312.pyc
Normal file
BIN
gui/components/__pycache__/workers.cpython-312.pyc
Normal file
Binary file not shown.
634
gui/components/widgets.py
Normal file
634
gui/components/widgets.py
Normal file
@@ -0,0 +1,634 @@
|
||||
from typing import Dict
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QTextEdit, QLabel, QFrame, QLineEdit, QMessageBox, QApplication, QDialog, QStyle, QProgressBar
|
||||
)
|
||||
from PyQt5.QtCore import (
|
||||
Qt, QTimer, QPropertyAnimation, QEasingCurve,
|
||||
QPoint, QRect, QSize, pyqtProperty
|
||||
)
|
||||
from PyQt5.QtGui import QPainter, QColor, QPen
|
||||
|
||||
class ActivationStatusWidget(QFrame):
|
||||
"""激活状态显示组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 状态标题
|
||||
title_label = QLabel("激活状态", self)
|
||||
title_label.setStyleSheet("font-size: 16px; font-weight: bold;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 状态信息
|
||||
self.status_label = QLabel(self)
|
||||
self.status_label.setStyleSheet("font-size: 14px;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
""")
|
||||
|
||||
def update_status(self, status: Dict):
|
||||
"""更新状态显示"""
|
||||
if status["is_activated"]:
|
||||
status_text = f"已激活 | 到期时间: {status['expire_time']} | 剩余天数: {status['days_left']}天"
|
||||
self.status_label.setStyleSheet("color: #28a745;")
|
||||
else:
|
||||
status_text = "未激活"
|
||||
self.status_label.setStyleSheet("color: #dc3545;")
|
||||
self.status_label.setText(status_text)
|
||||
|
||||
class ActivationWidget(QFrame):
|
||||
"""激活码输入组件
|
||||
|
||||
职责:
|
||||
1. 提供激活码输入界面
|
||||
2. 处理输入框和激活按钮的基础交互
|
||||
3. 将激活请求传递给父窗口处理
|
||||
4. 处理激活相关的提示信息
|
||||
|
||||
属性:
|
||||
code_input (QLineEdit): 激活码输入框
|
||||
activate_btn (QPushButton): 激活按钮
|
||||
|
||||
信号流:
|
||||
activate_btn.clicked -> activate() -> parent.handle_activation()
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
# 连接回车键到激活功能
|
||||
self.code_input.returnPressed.connect(self.activate)
|
||||
|
||||
def show_activation_required(self):
|
||||
"""显示需要激活的提示"""
|
||||
msg = QDialog(self)
|
||||
msg.setWindowTitle("提示")
|
||||
msg.setFixedWidth(360)
|
||||
msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(12)
|
||||
layout.setContentsMargins(20, 15, 20, 15)
|
||||
|
||||
# 创建顶部警告框
|
||||
header_frame = QFrame()
|
||||
header_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeeba;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QLabel {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
header_layout = QHBoxLayout(header_frame)
|
||||
header_layout.setContentsMargins(12, 10, 12, 10)
|
||||
header_layout.setSpacing(10)
|
||||
|
||||
icon_label = QLabel()
|
||||
icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(20, 20))
|
||||
header_layout.addWidget(icon_label)
|
||||
|
||||
text_label = QLabel("请输入激活码")
|
||||
text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #856404; background: transparent;")
|
||||
header_layout.addWidget(text_label)
|
||||
header_layout.addStretch()
|
||||
|
||||
layout.addWidget(header_frame)
|
||||
|
||||
# 添加提示文本
|
||||
info_text = QLabel(
|
||||
"获取会员激活码,请通过以下方式:\n\n"
|
||||
"官方自助网站:cursorpro.com.cn\n"
|
||||
"微信客服号:bshkcigar\n"
|
||||
"商店铺:xxx\n\n"
|
||||
"诚挚祝愿,欢迎加盟合作!"
|
||||
)
|
||||
info_text.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
margin: 5px 0;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(info_text)
|
||||
|
||||
# 添加按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(8)
|
||||
|
||||
# 复制网站按钮
|
||||
copy_web_btn = QPushButton("复制网址")
|
||||
copy_web_btn.setCursor(Qt.PointingHandCursor)
|
||||
copy_web_btn.clicked.connect(lambda: self._copy_to_clipboard("cursorpro.com.cn", "已复制官网地址"))
|
||||
copy_web_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(copy_web_btn)
|
||||
|
||||
# 复制微信按钮
|
||||
copy_wx_btn = QPushButton("复制微信")
|
||||
copy_wx_btn.setCursor(Qt.PointingHandCursor)
|
||||
copy_wx_btn.clicked.connect(lambda: self._copy_to_clipboard("bshkcigar", "已复制微信号"))
|
||||
copy_wx_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #198754;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #157347;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(copy_wx_btn)
|
||||
|
||||
# 确定按钮
|
||||
ok_btn = QPushButton("确定")
|
||||
ok_btn.setCursor(Qt.PointingHandCursor)
|
||||
ok_btn.clicked.connect(msg.accept)
|
||||
ok_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #5c636a;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(ok_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 设置对话框样式和布局
|
||||
msg.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
msg.setLayout(layout)
|
||||
|
||||
# 显示对话框
|
||||
msg.exec_()
|
||||
|
||||
def _copy_to_clipboard(self, text: str, status_msg: str):
|
||||
"""复制文本到剪贴板并更新状态栏"""
|
||||
QApplication.clipboard().setText(text)
|
||||
main_window = self.window()
|
||||
if main_window and hasattr(main_window, 'status_bar'):
|
||||
main_window.status_bar.set_status(status_msg)
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 10) # 移除边距
|
||||
|
||||
# 标题
|
||||
title = QLabel("激活(盖加)会员,多个激活码可叠加整体时长")
|
||||
title.setStyleSheet("color: #28a745; font-size: 14px; font-weight: bold;") # 绿色标题
|
||||
layout.addWidget(title)
|
||||
|
||||
# 激活码输入区域
|
||||
input_layout = QHBoxLayout()
|
||||
self.code_input = QLineEdit()
|
||||
self.code_input.setPlaceholderText("请输入激活码")
|
||||
self.code_input.setMinimumWidth(300)
|
||||
self.code_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
color: #495057;
|
||||
}
|
||||
""")
|
||||
input_layout.addWidget(self.code_input)
|
||||
|
||||
# 激活按钮
|
||||
self.activate_btn = QPushButton("激活")
|
||||
self.activate_btn.clicked.connect(self.activate)
|
||||
self.activate_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 22px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
""")
|
||||
input_layout.addWidget(self.activate_btn)
|
||||
|
||||
layout.addLayout(input_layout)
|
||||
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
""")
|
||||
|
||||
def activate(self):
|
||||
"""处理激活"""
|
||||
try:
|
||||
code = self.code_input.text().strip()
|
||||
|
||||
# 获取主窗口实例
|
||||
main_window = self.window()
|
||||
|
||||
if not main_window:
|
||||
print("错误:找不到主窗口")
|
||||
return
|
||||
|
||||
if not hasattr(main_window, 'handle_activation'):
|
||||
print("错误:主窗口没有 handle_activation 方法")
|
||||
return
|
||||
|
||||
if not code:
|
||||
print("警告:激活码为空")
|
||||
self.show_activation_required()
|
||||
return
|
||||
|
||||
print(f"正在处理激活码:{code}")
|
||||
main_window.handle_activation(code)
|
||||
|
||||
except Exception as e:
|
||||
print(f"激活处理发生错误:{str(e)}")
|
||||
|
||||
def clear_input(self):
|
||||
"""清空输入框"""
|
||||
self.code_input.clear()
|
||||
|
||||
class LogWidget(QTextEdit):
|
||||
"""日志显示组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setReadOnly(True)
|
||||
self.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-family: 'Consolas', 'Microsoft YaHei', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
def append_log(self, text: str):
|
||||
"""添加日志并滚动到底部"""
|
||||
self.append(text)
|
||||
self.verticalScrollBar().setValue(
|
||||
self.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
class StatusBar(QFrame):
|
||||
"""状态栏组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 5, 10, 5)
|
||||
|
||||
self.status_label = QLabel("就绪")
|
||||
self.status_label.setStyleSheet("font-size: 14px; color: #333;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
def set_status(self, text: str):
|
||||
"""设置状态文本"""
|
||||
self.status_label.setText(text)
|
||||
|
||||
class ActionButton(QPushButton):
|
||||
"""操作按钮基类"""
|
||||
def __init__(self, text: str, color: str, parent=None):
|
||||
super().__init__(text, parent)
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {color};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
min-height: 36px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: {self._get_hover_color(color)};
|
||||
}}
|
||||
QPushButton:disabled {{
|
||||
background-color: #ccc;
|
||||
}}
|
||||
""")
|
||||
|
||||
def _get_hover_color(self, color: str) -> str:
|
||||
"""获取悬停颜色"""
|
||||
# 简单的颜色加深处理
|
||||
if color == "#007bff": # 蓝色
|
||||
return "#0056b3"
|
||||
elif color == "#28a745": # 绿色
|
||||
return "#218838"
|
||||
return color
|
||||
|
||||
class MemberStatusWidget(QFrame):
|
||||
"""会员状态显示组件
|
||||
|
||||
职责:
|
||||
1. 显示会员状态信息
|
||||
2. 显示设备信息
|
||||
3. 根据不同状态显示不同样式
|
||||
|
||||
属性:
|
||||
status_text (QTextEdit): 状态信息显示区域
|
||||
|
||||
显示信息:
|
||||
- 会员状态(正常/未激活)
|
||||
- 到期时间
|
||||
- 剩余天数
|
||||
- 设备信息(系统、设备名、IP、地区)
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 10)
|
||||
|
||||
# 创建一个容器 Frame
|
||||
container = QFrame(self)
|
||||
container.setObjectName("memberStatusContainer")
|
||||
container_layout = QVBoxLayout(container)
|
||||
container_layout.setSpacing(5) # 减小组件间距
|
||||
container_layout.setContentsMargins(0,0,0,0) # 减小内边距
|
||||
container.setFixedHeight(220) # 根据需要调整这个值
|
||||
|
||||
# 状态信息
|
||||
self.status_text = QTextEdit()
|
||||
self.status_text.setReadOnly(True)
|
||||
|
||||
self.status_text.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: #f8fafc;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
color: #4a5568;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* 滚动条整体样式 */
|
||||
QScrollBar:vertical {
|
||||
border: none;
|
||||
background: #f8fafc;
|
||||
width: 6px;
|
||||
margin: 0px;
|
||||
}
|
||||
/* 滚动条滑块 */
|
||||
QScrollBar::handle:vertical {
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
min-height: 20px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
/* 滚动条上下按钮 */
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
border: none;
|
||||
background: none;
|
||||
height: 0px;
|
||||
}
|
||||
/* 滚动条背景 */
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
""")
|
||||
container_layout.addWidget(self.status_text)
|
||||
|
||||
# 设置容器样式
|
||||
container.setStyleSheet("""
|
||||
#memberStatusContainer {
|
||||
background-color: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
#titleContainer {
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
|
||||
layout.addWidget(container)
|
||||
|
||||
def update_status(self, status: Dict):
|
||||
"""更新状态显示"""
|
||||
try:
|
||||
# 获取状态信息
|
||||
is_activated = status.get("is_activated", False)
|
||||
expire_time = status.get("expire_time", "未知")
|
||||
days_left = status.get("days_left", 0)
|
||||
device_info = status.get("device_info", {})
|
||||
|
||||
# 构建状态文本
|
||||
status_text = []
|
||||
|
||||
# 会员状态
|
||||
status_color = "#10B981" if is_activated else "#EF4444" # 绿色或红色
|
||||
status_text.append(f'会员状态:<span style="color: {status_color};">{("正常" if is_activated else "未激活")}</span>')
|
||||
|
||||
# 到期时间和剩余天数
|
||||
if is_activated:
|
||||
status_text.append(f"到期时间:{expire_time}")
|
||||
# 根据剩余天数显示不同颜色
|
||||
days_color = "#10B981" if days_left > 30 else "#F59E0B" if days_left > 7 else "#EF4444"
|
||||
status_text.append(f'剩余天数:<span style="color: {days_color};">{days_left}天</span>')
|
||||
|
||||
# 设备信息
|
||||
status_text.append("\n设备信息:")
|
||||
status_text.append(f"系统:{device_info.get('os', 'Windows')}")
|
||||
status_text.append(f"设备名:{device_info.get('device_name', '未知')}")
|
||||
status_text.append(f"IP地址:{device_info.get('ip', '未知')}")
|
||||
status_text.append(f"地区:{device_info.get('location', '未知')}")
|
||||
|
||||
# 更新文本
|
||||
self.status_text.setHtml("<br>".join(status_text))
|
||||
|
||||
except Exception as e:
|
||||
# 如果发生异常,显示错误信息
|
||||
error_text = f'<span style="color: #EF4444;">状态更新失败: {str(e)}</span>'
|
||||
self.status_text.setHtml(error_text)
|
||||
|
||||
class LoadingSpinner(QWidget):
|
||||
"""自定义加载动画组件"""
|
||||
def __init__(self, parent=None, size=40, line_width=3):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(size, size)
|
||||
self.angle = 0
|
||||
self.line_width = line_width
|
||||
self._size = size
|
||||
self._speed = 30 # 每次更新转动的角度
|
||||
|
||||
# 设置动画
|
||||
self.animation = QPropertyAnimation(self, b"rotation")
|
||||
self.animation.setDuration(800) # 缩短动画时间到800ms使其更流畅
|
||||
self.animation.setStartValue(0)
|
||||
self.animation.setEndValue(360)
|
||||
self.animation.setLoopCount(-1) # 无限循环
|
||||
|
||||
# 使用 InOutQuad 缓动曲线让动画更自然
|
||||
curve = QEasingCurve(QEasingCurve.InOutQuad)
|
||||
self.animation.setEasingCurve(curve)
|
||||
|
||||
# 创建定时器用于额外的动画效果
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.rotate)
|
||||
self.timer.setInterval(50) # 20fps的刷新率
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""绘制旋转的圆弧"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# 计算圆的位置
|
||||
rect = QRect(self.line_width, self.line_width,
|
||||
self._size - 2*self.line_width,
|
||||
self._size - 2*self.line_width)
|
||||
|
||||
# 设置画笔
|
||||
pen = QPen()
|
||||
pen.setWidth(self.line_width)
|
||||
pen.setCapStyle(Qt.RoundCap)
|
||||
|
||||
# 绘制渐变圆弧
|
||||
segments = 8
|
||||
for i in range(segments):
|
||||
# 计算每个段的透明度
|
||||
alpha = int(255 * (1 - i / segments))
|
||||
# 使用更鲜艳的蓝色
|
||||
pen.setColor(QColor(0, 122, 255, alpha))
|
||||
painter.setPen(pen)
|
||||
|
||||
# 计算每个段的起始角度和跨度
|
||||
start_angle = (self.angle - i * (360 // segments)) % 360
|
||||
span = 30 # 每个段的跨度
|
||||
|
||||
# 转换角度为16分之一度(Qt的角度单位)
|
||||
start_angle_16 = start_angle * 16
|
||||
span_16 = span * 16
|
||||
|
||||
painter.drawArc(rect, -start_angle_16, -span_16)
|
||||
|
||||
def rotate(self):
|
||||
"""更新旋转角度"""
|
||||
self.angle = (self.angle + self._speed) % 360
|
||||
self.update()
|
||||
|
||||
@pyqtProperty(int)
|
||||
def rotation(self):
|
||||
return self.angle
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, angle):
|
||||
self.angle = angle
|
||||
self.update()
|
||||
|
||||
def start(self):
|
||||
"""开始动画"""
|
||||
self.animation.start()
|
||||
self.timer.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止动画"""
|
||||
self.animation.stop()
|
||||
self.timer.stop()
|
||||
|
||||
class LoadingDialog(QDialog):
|
||||
"""加载对话框组件"""
|
||||
def __init__(self, parent=None, message="请稍候..."):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("处理中")
|
||||
self.setFixedSize(300, 120)
|
||||
self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 消息标签
|
||||
self.message_label = QLabel(message)
|
||||
self.message_label.setAlignment(Qt.AlignCenter)
|
||||
self.message_label.setStyleSheet("""
|
||||
color: #0d6efd;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
""")
|
||||
layout.addWidget(self.message_label)
|
||||
|
||||
# 加载动画
|
||||
self.spinner = LoadingSpinner(self, size=48, line_width=4) # 增大尺寸
|
||||
spinner_layout = QHBoxLayout()
|
||||
spinner_layout.addStretch()
|
||||
spinner_layout.addWidget(self.spinner)
|
||||
spinner_layout.addStretch()
|
||||
layout.addLayout(spinner_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# 设置样式
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
""")
|
||||
|
||||
def showEvent(self, event):
|
||||
"""显示时启动动画"""
|
||||
super().showEvent(event)
|
||||
self.spinner.start()
|
||||
|
||||
def hideEvent(self, event):
|
||||
"""隐藏时停止动画"""
|
||||
self.spinner.stop()
|
||||
super().hideEvent(event)
|
||||
|
||||
def set_message(self, message: str):
|
||||
"""更新消息文本"""
|
||||
self.message_label.setText(message)
|
||||
100
gui/components/workers.py
Normal file
100
gui/components/workers.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from services.cursor_service import CursorService
|
||||
from common_utils import get_hardware_id
|
||||
from exit_cursor import ExitCursor
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
|
||||
class BaseWorker(QThread):
|
||||
"""基础工作线程类"""
|
||||
progress = pyqtSignal(dict) # 进度信号
|
||||
finished = pyqtSignal(tuple) # 完成信号
|
||||
error = pyqtSignal(str) # 错误信号
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._is_running = True
|
||||
|
||||
def stop(self):
|
||||
"""停止线程"""
|
||||
self._is_running = False
|
||||
self.wait()
|
||||
|
||||
class RefreshTokenWorker(BaseWorker):
|
||||
"""刷新 Cursor Token 工作线程"""
|
||||
def run(self):
|
||||
try:
|
||||
service = CursorService()
|
||||
machine_id = get_hardware_id()
|
||||
|
||||
# 更新进度 - 退出 Cursor
|
||||
self.progress.emit({
|
||||
"status": "exiting",
|
||||
"message": "正在关闭 Cursor 进程..."
|
||||
})
|
||||
|
||||
# 退出 Cursor
|
||||
if not ExitCursor():
|
||||
raise Exception("无法完全关闭 Cursor 进程,请手动关闭后重试")
|
||||
|
||||
# 更新进度 - 开始刷新
|
||||
self.progress.emit({
|
||||
"status": "refreshing",
|
||||
"message": "正在刷新 Cursor 授权..."
|
||||
})
|
||||
|
||||
# 调用刷新服务
|
||||
success, msg, account_info = service.refresh_account(machine_id)
|
||||
|
||||
if success and account_info:
|
||||
# 更新进度 - 开始重置机器码
|
||||
self.progress.emit({
|
||||
"status": "resetting",
|
||||
"message": "正在重置机器码..."
|
||||
})
|
||||
|
||||
# 调用重置机器码
|
||||
reset_success, reset_msg, reset_data = service.reset_machine()
|
||||
|
||||
if reset_success:
|
||||
# 更新进度
|
||||
self.progress.emit({
|
||||
"status": "success",
|
||||
"message": f"授权刷新成功: {account_info.get('email')}"
|
||||
})
|
||||
|
||||
result_msg = (
|
||||
f"授权刷新成功\n"
|
||||
f"账号: {account_info.get('email')}\n"
|
||||
f"到期时间: {account_info.get('expire_time')}\n"
|
||||
f"剩余天数: {account_info.get('days_left')}天\n\n"
|
||||
f"机器码重置成功\n"
|
||||
f"请重新启动 Cursor 编辑器"
|
||||
)
|
||||
else:
|
||||
result_msg = f"授权刷新成功,但机器码重置失败: {reset_msg}"
|
||||
else:
|
||||
result_msg = f"授权刷新失败: {msg}"
|
||||
|
||||
self.finished.emit(('refresh', (success, result_msg)))
|
||||
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class ResetWorker(BaseWorker):
|
||||
"""重置机器码工作线程"""
|
||||
def run(self):
|
||||
try:
|
||||
service = CursorService()
|
||||
success, msg, data = service.reset_machine()
|
||||
self.finished.emit(('reset', (success, msg, data)))
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class DisableWorker(BaseWorker):
|
||||
"""禁用更新工作线程"""
|
||||
def run(self):
|
||||
try:
|
||||
service = CursorService()
|
||||
success, msg = service.disable_update()
|
||||
self.finished.emit(('disable', (success, msg)))
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
1
gui/windows/__init__.py
Normal file
1
gui/windows/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""GUI 窗口包"""
|
||||
BIN
gui/windows/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
gui/windows/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
gui/windows/__pycache__/main_window.cpython-312.pyc
Normal file
BIN
gui/windows/__pycache__/main_window.cpython-312.pyc
Normal file
Binary file not shown.
667
gui/windows/main_window.py
Normal file
667
gui/windows/main_window.py
Normal file
@@ -0,0 +1,667 @@
|
||||
import json
|
||||
from typing import Dict
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QMessageBox, QLabel, QLineEdit, QPushButton,
|
||||
QFrame, QTextEdit, QDesktopWidget, QSystemTrayIcon,
|
||||
QMenu, QAction
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QFont, QIcon, QDesktopServices
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from pathlib import Path
|
||||
|
||||
from services.cursor_service import CursorService
|
||||
from gui.components.widgets import (
|
||||
LogWidget, StatusBar, ActionButton,
|
||||
ActivationWidget, MemberStatusWidget,
|
||||
ActivationStatusWidget, LoadingDialog
|
||||
)
|
||||
from gui.components.workers import ResetWorker, DisableWorker, RefreshTokenWorker
|
||||
from common_utils import get_hardware_id
|
||||
from utils.version_manager import VersionManager
|
||||
|
||||
class DeviceIdWidget(QFrame):
|
||||
"""设备识别码显示组件"""
|
||||
def __init__(self, device_id: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui(device_id)
|
||||
|
||||
def setup_ui(self, device_id: str):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0) # 移除边距
|
||||
|
||||
# 标签
|
||||
label = QLabel("设备识别码(勿动):")
|
||||
label.setStyleSheet("color: #dc3545; font-weight: bold;") # 红色警示
|
||||
layout.addWidget(label)
|
||||
|
||||
# 显示设备ID的文本框
|
||||
self.id_input = QLineEdit(device_id)
|
||||
self.id_input.setReadOnly(True)
|
||||
layout.addWidget(self.id_input)
|
||||
|
||||
# 复制按钮
|
||||
copy_btn = QPushButton("复制ID")
|
||||
copy_btn.clicked.connect(self.copy_device_id)
|
||||
layout.addWidget(copy_btn)
|
||||
|
||||
self.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
color: #495057;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
""")
|
||||
|
||||
def copy_device_id(self):
|
||||
"""复制设备ID到剪贴板"""
|
||||
self.id_input.selectAll()
|
||||
self.id_input.copy()
|
||||
QMessageBox.information(self, "成功", "设备ID已复制到剪贴板")
|
||||
|
||||
class InstructionsWidget(QFrame):
|
||||
"""使用步骤说明组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 10) # 移除边距
|
||||
|
||||
# 标题
|
||||
title = QLabel("使用步骤说明:")
|
||||
title.setStyleSheet("color: #17a2b8; font-size: 14px; font-weight: bold;") # 青色标题
|
||||
layout.addWidget(title)
|
||||
|
||||
# 步骤说明
|
||||
steps = [
|
||||
("第一步", "输入激活码点击【激活】按钮完成激活"),
|
||||
("第二步", "点击【刷新Cursor编辑器授权】,一般情况下刷新即可正常使用"),
|
||||
("如果无法对话", "点击【实现Cursor0.45.x限制】,然后重新刷新授权"),
|
||||
("建议操作", "点击【禁用Cursor版本更新】,保持软件稳定运行")
|
||||
]
|
||||
|
||||
for step_title, step_content in steps:
|
||||
step_label = QLabel(f"{step_title}:{step_content}")
|
||||
step_label.setWordWrap(True)
|
||||
step_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #495057;
|
||||
margin: 3px 0;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(step_label)
|
||||
|
||||
# 给标题部分添加颜色
|
||||
text = step_label.text()
|
||||
step_label.setText(f'<span style="color: #17a2b8;">{step_title}</span>:{step_content}')
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.cursor_service = CursorService()
|
||||
self.version_manager = VersionManager()
|
||||
self.current_worker = None
|
||||
self.loading_dialog = None
|
||||
|
||||
# 初始化托盘图标
|
||||
self.setup_tray()
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# 移除直接自检
|
||||
# self.auto_check()
|
||||
|
||||
def setup_tray(self):
|
||||
"""初始化托盘图标"""
|
||||
# 创建托盘图标
|
||||
self.tray_icon = QSystemTrayIcon(self)
|
||||
|
||||
# 设置图标
|
||||
icon_path = Path(__file__).parent.parent.parent / "two.ico"
|
||||
if icon_path.exists():
|
||||
self.tray_icon.setIcon(QIcon(str(icon_path)))
|
||||
|
||||
# 创建托盘菜单
|
||||
self.tray_menu = QMenu()
|
||||
|
||||
# 显示主窗口动作
|
||||
show_action = QAction("显示主窗口", self)
|
||||
show_action.triggered.connect(self.show_main_window)
|
||||
self.tray_menu.addAction(show_action)
|
||||
|
||||
# 刷新授权动作
|
||||
refresh_action = QAction("刷新 Cursor 编辑器授权", self)
|
||||
refresh_action.triggered.connect(lambda: self.start_task('refresh'))
|
||||
self.tray_menu.addAction(refresh_action)
|
||||
|
||||
# 退出动作
|
||||
quit_action = QAction("退出", self)
|
||||
quit_action.triggered.connect(self.quit_application)
|
||||
self.tray_menu.addAction(quit_action)
|
||||
|
||||
# 设置托盘菜单
|
||||
self.tray_icon.setContextMenu(self.tray_menu)
|
||||
|
||||
# 托盘图标双击事件
|
||||
self.tray_icon.activated.connect(self.tray_icon_activated)
|
||||
|
||||
# 显示托盘图标
|
||||
self.tray_icon.show()
|
||||
|
||||
def show_main_window(self):
|
||||
"""显示主窗口"""
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
def quit_application(self):
|
||||
"""退出应用程序"""
|
||||
# 停止所有任务
|
||||
if self.current_worker:
|
||||
self.current_worker.stop()
|
||||
# 移除托盘图标
|
||||
self.tray_icon.setVisible(False)
|
||||
# 退出应用
|
||||
QApplication.quit()
|
||||
|
||||
def tray_icon_activated(self, reason):
|
||||
"""托盘图标激活事件"""
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
# 双击显示主窗口
|
||||
self.show_main_window()
|
||||
|
||||
def showEvent(self, event):
|
||||
"""窗口显示事件"""
|
||||
super().showEvent(event)
|
||||
# 在窗口显示后执行自检
|
||||
QTimer.singleShot(500, self.auto_check) # 延迟500ms执行,确保界面完全显示
|
||||
|
||||
def show_loading(self, message: str):
|
||||
"""显示加载对话框"""
|
||||
if not self.loading_dialog:
|
||||
self.loading_dialog = LoadingDialog(self)
|
||||
self.loading_dialog.set_message(message)
|
||||
self.loading_dialog.show()
|
||||
QApplication.processEvents() # 确保对话框立即显示
|
||||
|
||||
def hide_loading(self):
|
||||
"""隐藏加载对话框"""
|
||||
if self.loading_dialog:
|
||||
self.loading_dialog.hide()
|
||||
|
||||
def auto_check(self):
|
||||
"""启动时自检"""
|
||||
try:
|
||||
# 1. 检查更新
|
||||
has_update, msg, update_info = self.version_manager.check_update()
|
||||
|
||||
if has_update and update_info:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"发现新版本",
|
||||
f"发现新版本: {update_info['version']}\n\n"
|
||||
f"更新内容:\n{update_info['message']}\n\n"
|
||||
"是否立即下载更新?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
QDesktopServices.openUrl(QUrl(update_info['download_url']))
|
||||
|
||||
# 2. 检查激活状态
|
||||
self.update_member_status()
|
||||
|
||||
# 3. 恢复状态栏
|
||||
self.status_bar.set_status("就绪")
|
||||
|
||||
except Exception as e:
|
||||
self.status_bar.set_status("自检过程出现错误")
|
||||
self.logger.error(f"自检失败: {str(e)}")
|
||||
|
||||
def setup_ui(self):
|
||||
"""初始化UI"""
|
||||
self.setWindowTitle("听泉助手 v4.0.0.1")
|
||||
self.setMinimumSize(600, 500)
|
||||
|
||||
# 设置窗口图标
|
||||
icon_path = Path(__file__).parent.parent.parent / "two.ico"
|
||||
if icon_path.exists():
|
||||
self.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
# 设置窗口背景为白色
|
||||
self.setStyleSheet("""
|
||||
QMainWindow {
|
||||
background-color: white;
|
||||
}
|
||||
QWidget {
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
# 创建中心部件和布局
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout(central_widget)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(15, 15, 15, 15)
|
||||
|
||||
# 设备识别码区域
|
||||
device_id = get_hardware_id() # 使用common_utils中的函数
|
||||
self.device_id_widget = DeviceIdWidget(device_id)
|
||||
layout.addWidget(self.device_id_widget)
|
||||
|
||||
# 会员状态区域
|
||||
self.member_status = MemberStatusWidget()
|
||||
layout.addWidget(self.member_status)
|
||||
|
||||
# 激活区域
|
||||
self.activation_widget = ActivationWidget(self) # 使用 self 作为父组件
|
||||
layout.addWidget(self.activation_widget)
|
||||
|
||||
# 使用说明区域
|
||||
self.instructions = InstructionsWidget()
|
||||
layout.addWidget(self.instructions)
|
||||
|
||||
# 功能按钮区域
|
||||
button_layout = QVBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
# 刷新授权按钮 - 蓝色
|
||||
self.refresh_button = QPushButton("刷新 Cursor 编辑器授权")
|
||||
self.refresh_button.clicked.connect(lambda: self.start_task('refresh'))
|
||||
self.refresh_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
""")
|
||||
button_layout.addWidget(self.refresh_button)
|
||||
|
||||
# 实现限制按钮 - 绿色
|
||||
self.limit_button = QPushButton("实现 Cursor 0.45.x 限制")
|
||||
self.limit_button.clicked.connect(lambda: self.start_task('limit'))
|
||||
self.limit_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
""")
|
||||
button_layout.addWidget(self.limit_button)
|
||||
|
||||
# 禁用更新按钮 - 红色
|
||||
self.disable_button = QPushButton("禁用 Cursor 版本更新")
|
||||
self.disable_button.clicked.connect(lambda: self.start_task('disable'))
|
||||
self.disable_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
""")
|
||||
button_layout.addWidget(self.disable_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 检查更新按钮 - 灰色
|
||||
self.check_update_button = QPushButton("检查更新")
|
||||
self.check_update_button.clicked.connect(self.check_update)
|
||||
self.check_update_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
min-width: 100px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.check_update_button)
|
||||
|
||||
# 状态栏
|
||||
self.status_bar = StatusBar()
|
||||
layout.addWidget(self.status_bar)
|
||||
|
||||
# 初始化状态
|
||||
self.update_member_status()
|
||||
|
||||
# 检查是否有未完成的更新
|
||||
self.check_pending_update()
|
||||
|
||||
def update_member_status(self):
|
||||
"""更新会员状态"""
|
||||
status = self.cursor_service.check_activation_status()
|
||||
self.member_status.update_status(status)
|
||||
|
||||
# 根据激活状态更新按钮可用性
|
||||
buttons_enabled = status["is_activated"]
|
||||
self.refresh_button.setEnabled(buttons_enabled)
|
||||
self.limit_button.setEnabled(buttons_enabled)
|
||||
self.disable_button.setEnabled(buttons_enabled)
|
||||
|
||||
def handle_activation(self, code: str):
|
||||
"""处理激活码激活"""
|
||||
try:
|
||||
print(f"MainWindow 收到激活请求,激活码:{code}")
|
||||
|
||||
# 更新状态栏
|
||||
print("更新状态栏:正在激活...")
|
||||
self.status_bar.set_status("正在激活...")
|
||||
|
||||
# 禁用激活按钮
|
||||
print("禁用激活按钮")
|
||||
self.activation_widget.activate_btn.setEnabled(False)
|
||||
|
||||
# 调用激活服务
|
||||
print("调用激活服务...")
|
||||
success, msg = self.cursor_service.activate_with_code(code)
|
||||
print(f"激活结果:success={success}, msg={msg}")
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "成功", msg)
|
||||
self.update_member_status()
|
||||
else:
|
||||
QMessageBox.warning(self, "失败", msg)
|
||||
|
||||
# 清空输入框
|
||||
self.activation_widget.clear_input()
|
||||
|
||||
except Exception as e:
|
||||
print(f"激活过程发生错误:{str(e)}")
|
||||
QMessageBox.critical(self, "错误", f"激活过程发生错误:{str(e)}")
|
||||
finally:
|
||||
# 恢复状态栏
|
||||
print("恢复状态栏和按钮状态")
|
||||
self.status_bar.set_status("就绪")
|
||||
# 恢复激活按钮
|
||||
self.activation_widget.activate_btn.setEnabled(True)
|
||||
|
||||
def start_task(self, task_type: str):
|
||||
"""启动任务"""
|
||||
# 检查激活状态
|
||||
if not self.cursor_service.check_activation_status()["is_activated"]:
|
||||
self.activation_widget.show_activation_required()
|
||||
return
|
||||
|
||||
# 停止当前任务(如果有)
|
||||
if self.current_worker:
|
||||
self.current_worker.stop()
|
||||
|
||||
# 禁用所有按钮
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.limit_button.setEnabled(False)
|
||||
self.disable_button.setEnabled(False)
|
||||
|
||||
# 创建并启动工作线程
|
||||
if task_type == 'refresh':
|
||||
self.status_bar.set_status("正在刷新授权...")
|
||||
worker = RefreshTokenWorker()
|
||||
worker.progress.connect(self.update_progress)
|
||||
worker.finished.connect(self.handle_result)
|
||||
worker.error.connect(self.handle_error)
|
||||
worker.start()
|
||||
self.current_worker = worker
|
||||
elif task_type == 'limit':
|
||||
self.status_bar.set_status("正在设置版本限制...")
|
||||
worker = ResetWorker()
|
||||
worker.progress.connect(self.update_progress)
|
||||
worker.finished.connect(self.handle_result)
|
||||
worker.error.connect(self.handle_error)
|
||||
worker.start()
|
||||
self.current_worker = worker
|
||||
elif task_type == 'disable':
|
||||
self.status_bar.set_status("正在禁用更新...")
|
||||
worker = DisableWorker()
|
||||
worker.progress.connect(self.update_progress)
|
||||
worker.finished.connect(self.handle_result)
|
||||
worker.error.connect(self.handle_error)
|
||||
worker.start()
|
||||
self.current_worker = worker
|
||||
|
||||
def update_progress(self, info: dict):
|
||||
"""更新进度信息"""
|
||||
self.status_bar.set_status(info.get('message', '处理中...'))
|
||||
|
||||
def handle_result(self, result: tuple):
|
||||
"""处理任务结果"""
|
||||
task_type, data = result
|
||||
success, msg = data if isinstance(data, tuple) else (False, str(data))
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "成功", msg)
|
||||
else:
|
||||
QMessageBox.warning(self, "失败", msg)
|
||||
|
||||
# 重新启用按钮
|
||||
self.update_member_status()
|
||||
self.status_bar.set_status("就绪")
|
||||
|
||||
# 清理工作线程
|
||||
if self.current_worker:
|
||||
self.current_worker.stop()
|
||||
self.current_worker = None
|
||||
|
||||
def handle_error(self, error_msg: str):
|
||||
"""处理错误"""
|
||||
self.status_bar.set_status("发生错误")
|
||||
QMessageBox.critical(self, "错误", f"操作失败:{error_msg}")
|
||||
|
||||
# 重新启用按钮
|
||||
self.update_member_status()
|
||||
|
||||
# 清理工作线程
|
||||
if self.current_worker:
|
||||
self.current_worker.stop()
|
||||
self.current_worker = None
|
||||
|
||||
def check_update(self):
|
||||
"""手动检查更新"""
|
||||
try:
|
||||
# 禁用更新按钮
|
||||
self.check_update_button.setEnabled(False)
|
||||
self.status_bar.set_status("正在检查更新...")
|
||||
|
||||
# 检查更新
|
||||
has_update, msg, update_info = self.version_manager.check_update()
|
||||
|
||||
if has_update and update_info:
|
||||
# 创建自定义消息框
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setWindowTitle("发现新版本")
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
|
||||
# 设置文本
|
||||
text = (
|
||||
f"<p style='font-size: 14px;'><b>发现新版本: {update_info['version']}</b></p>"
|
||||
f"<p style='font-size: 13px; margin: 10px 0;'><b>更新内容:</b></p>"
|
||||
f"<p style='font-size: 13px; white-space: pre-wrap;'>{update_info['message']}</p>"
|
||||
)
|
||||
msg_box.setText(text)
|
||||
|
||||
# 设置按钮
|
||||
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||
yes_btn = msg_box.button(QMessageBox.Yes)
|
||||
no_btn = msg_box.button(QMessageBox.No)
|
||||
yes_btn.setText("立即下载")
|
||||
no_btn.setText("暂不更新")
|
||||
|
||||
# 设置样式
|
||||
msg_box.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
QPushButton {
|
||||
min-width: 85px;
|
||||
min-height: 24px;
|
||||
padding: 4px 15px;
|
||||
border-radius: 3px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
QPushButton[text='暂不更新'] {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
QPushButton[text='暂不更新']:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
""")
|
||||
|
||||
# 设置最小宽度
|
||||
msg_box.setMinimumWidth(400)
|
||||
|
||||
# 显示对话框
|
||||
if msg_box.exec_() == QMessageBox.Yes:
|
||||
QDesktopServices.openUrl(QUrl(update_info['download_url']))
|
||||
else:
|
||||
# 创建自定义消息框
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setWindowTitle("检查更新")
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setText(f"<p style='font-size: 13px;'>{msg}</p>")
|
||||
msg_box.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
QPushButton {
|
||||
min-width: 85px;
|
||||
min-height: 24px;
|
||||
padding: 4px 15px;
|
||||
border-radius: 3px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
""")
|
||||
msg_box.setMinimumWidth(300)
|
||||
msg_box.exec_()
|
||||
|
||||
except Exception as e:
|
||||
error_box = QMessageBox(self)
|
||||
error_box.setWindowTitle("错误")
|
||||
error_box.setIcon(QMessageBox.Warning)
|
||||
error_box.setText(f"<p style='font-size: 13px;'>检查更新失败: {str(e)}</p>")
|
||||
error_box.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
QPushButton {
|
||||
min-width: 85px;
|
||||
min-height: 24px;
|
||||
padding: 4px 15px;
|
||||
border-radius: 3px;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
""")
|
||||
error_box.setMinimumWidth(300)
|
||||
error_box.exec_()
|
||||
|
||||
finally:
|
||||
# 恢复按钮状态和状态栏
|
||||
self.check_update_button.setEnabled(True)
|
||||
self.status_bar.set_status("就绪")
|
||||
|
||||
def check_pending_update(self):
|
||||
"""检查是否有未完成的更新"""
|
||||
try:
|
||||
update_info = self.version_manager.get_last_update_info()
|
||||
if update_info:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"未完成的更新",
|
||||
f"检测到未完成的更新: {update_info['version']}\n\n"
|
||||
f"更新内容:\n{update_info['message']}\n\n"
|
||||
"是否现在更新?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
# 打开下载链接
|
||||
QDesktopServices.openUrl(QUrl(update_info['download_url']))
|
||||
else:
|
||||
# 清除更新信息
|
||||
self.version_manager.clear_update_info()
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查未完成更新失败: {str(e)}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""窗口关闭事件"""
|
||||
if self.tray_icon.isVisible():
|
||||
# 最小化到托盘
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示",
|
||||
"程序将继续在后台运行,双击托盘图标可以重新打开主窗口。"
|
||||
)
|
||||
self.hide()
|
||||
event.ignore()
|
||||
else:
|
||||
# 停止所有正在运行的任务
|
||||
if self.current_worker:
|
||||
self.current_worker.stop()
|
||||
event.accept()
|
||||
67
logger.py
Normal file
67
logger.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class Logger:
|
||||
"""统一的日志管理类"""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._initialized:
|
||||
# 创建日志目录
|
||||
self.log_dir = os.path.join(os.getenv('APPDATA'), 'Cursor', 'logs')
|
||||
os.makedirs(self.log_dir, exist_ok=True)
|
||||
|
||||
# 配置根日志记录器
|
||||
self.logger = logging.getLogger('CursorTools')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
# 日志格式
|
||||
self.formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# 添加处理器
|
||||
self._add_handlers()
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def _add_handlers(self):
|
||||
"""添加日志处理器"""
|
||||
if not self.logger.handlers:
|
||||
# 文件处理器
|
||||
log_file = os.path.join(
|
||||
self.log_dir,
|
||||
f'cursor_tools_{datetime.now().strftime("%Y%m%d")}.log'
|
||||
)
|
||||
fh = logging.FileHandler(log_file, encoding='utf-8')
|
||||
fh.setLevel(logging.INFO)
|
||||
fh.setFormatter(self.formatter)
|
||||
self.logger.addHandler(fh)
|
||||
|
||||
# 控制台处理器
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
ch.setFormatter(self.formatter)
|
||||
self.logger.addHandler(ch)
|
||||
|
||||
def get_logger(self, name: Optional[str] = None) -> logging.Logger:
|
||||
"""
|
||||
获取日志记录器
|
||||
:param name: 日志记录器名称
|
||||
:return: 日志记录器实例
|
||||
"""
|
||||
if name:
|
||||
return self.logger.getChild(name)
|
||||
return self.logger
|
||||
|
||||
# 创建全局日志管理器实例
|
||||
logger = Logger()
|
||||
197
machine_resetter.py
Normal file
197
machine_resetter.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import requests
|
||||
import urllib3
|
||||
import uuid
|
||||
import winreg
|
||||
import ctypes
|
||||
import shutil
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple, Any, Optional
|
||||
from logger import logger
|
||||
from exit_cursor import ExitCursor
|
||||
|
||||
class MachineResetter:
|
||||
"""机器码重置核心类"""
|
||||
|
||||
def __init__(self, storage_file: str = None, backup_dir: str = None):
|
||||
"""
|
||||
初始化机器码重置器
|
||||
:param storage_file: 可选,storage.json文件路径
|
||||
:param backup_dir: 可选,备份目录路径
|
||||
"""
|
||||
# 设置基础路径
|
||||
appdata = os.getenv('APPDATA')
|
||||
if not appdata:
|
||||
raise EnvironmentError("APPDATA 环境变量未设置")
|
||||
|
||||
self.storage_file = storage_file or os.path.join(appdata, 'Cursor', 'User', 'globalStorage', 'storage.json')
|
||||
self.backup_dir = backup_dir or os.path.join(os.path.dirname(self.storage_file), 'backups')
|
||||
|
||||
# 确保备份目录存在
|
||||
os.makedirs(self.backup_dir, exist_ok=True)
|
||||
|
||||
# 获取日志记录器
|
||||
self.logger = logger.get_logger("MachineReset")
|
||||
|
||||
# 进度回调
|
||||
self._callback = None
|
||||
|
||||
def set_progress_callback(self, callback) -> None:
|
||||
"""设置进度回调函数"""
|
||||
self._callback = callback
|
||||
|
||||
def _update_progress(self, status: str, message: str) -> None:
|
||||
"""更新进度信息"""
|
||||
if self._callback:
|
||||
self._callback({"status": status, "message": message})
|
||||
|
||||
@staticmethod
|
||||
def is_admin() -> bool:
|
||||
"""检查是否具有管理员权限"""
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||
except:
|
||||
return False
|
||||
|
||||
def _get_storage_content(self) -> Dict[str, Any]:
|
||||
"""读取storage.json的内容"""
|
||||
try:
|
||||
with open(self.storage_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
raise Exception(f"读取配置文件失败: {str(e)}")
|
||||
|
||||
def _save_storage_content(self, content: Dict[str, Any]) -> None:
|
||||
"""保存内容到storage.json"""
|
||||
try:
|
||||
with open(self.storage_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(content, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
raise Exception(f"保存配置文件失败: {str(e)}")
|
||||
|
||||
def backup_file(self) -> str:
|
||||
"""
|
||||
备份配置文件
|
||||
:return: 备份文件路径
|
||||
"""
|
||||
if not os.path.exists(self.storage_file):
|
||||
raise FileNotFoundError(f"配置文件不存在:{self.storage_file}")
|
||||
|
||||
backup_name = f"storage.json.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
backup_path = os.path.join(self.backup_dir, backup_name)
|
||||
shutil.copy2(self.storage_file, backup_path)
|
||||
self.logger.info(f"配置已备份到: {backup_path}")
|
||||
return backup_path
|
||||
|
||||
def generate_new_ids(self) -> Dict[str, str]:
|
||||
"""
|
||||
生成新的机器码系列
|
||||
:return: 包含新ID的字典
|
||||
"""
|
||||
# 生成 auth0|user_ 前缀的十六进制
|
||||
prefix = "auth0|user_".encode('utf-8').hex()
|
||||
# 生成32字节(64个十六进制字符)的随机数
|
||||
random_part = uuid.uuid4().hex + uuid.uuid4().hex
|
||||
|
||||
return {
|
||||
"machineId": f"{prefix}{random_part}",
|
||||
"macMachineId": str(uuid.uuid4()),
|
||||
"devDeviceId": str(uuid.uuid4()),
|
||||
"sqmId": "{" + str(uuid.uuid4()).upper() + "}"
|
||||
}
|
||||
|
||||
def update_config(self, new_ids: Dict[str, str]) -> None:
|
||||
"""
|
||||
更新配置文件
|
||||
:param new_ids: 新的ID字典
|
||||
"""
|
||||
config_content = self._get_storage_content()
|
||||
|
||||
# 更新配置
|
||||
config_content['telemetry.machineId'] = new_ids['machineId']
|
||||
config_content['telemetry.macMachineId'] = new_ids['macMachineId']
|
||||
config_content['telemetry.devDeviceId'] = new_ids['devDeviceId']
|
||||
config_content['telemetry.sqmId'] = new_ids['sqmId']
|
||||
|
||||
# 保存更新后的配置
|
||||
self._save_storage_content(config_content)
|
||||
self.logger.info("配置文件更新成功")
|
||||
|
||||
def update_machine_guid(self) -> str:
|
||||
"""
|
||||
更新注册表中的MachineGuid
|
||||
:return: 新的GUID
|
||||
"""
|
||||
reg_path = r"SOFTWARE\Microsoft\Cryptography"
|
||||
try:
|
||||
# 打开注册表键
|
||||
reg_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0,
|
||||
winreg.KEY_WRITE | winreg.KEY_READ)
|
||||
|
||||
# 获取当前GUID用于备份
|
||||
current_guid, _ = winreg.QueryValueEx(reg_key, "MachineGuid")
|
||||
self.logger.info(f"当前MachineGuid: {current_guid}")
|
||||
|
||||
# 生成新GUID
|
||||
new_guid = str(uuid.uuid4())
|
||||
|
||||
# 更新注册表
|
||||
winreg.SetValueEx(reg_key, "MachineGuid", 0, winreg.REG_SZ, new_guid)
|
||||
|
||||
# 验证更改
|
||||
updated_guid, _ = winreg.QueryValueEx(reg_key, "MachineGuid")
|
||||
if updated_guid != new_guid:
|
||||
raise Exception("注册表验证失败,值未成功更新")
|
||||
|
||||
winreg.CloseKey(reg_key)
|
||||
self.logger.info(f"新MachineGuid: {new_guid}")
|
||||
return new_guid
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"更新MachineGuid失败: {str(e)}")
|
||||
|
||||
def run(self) -> Tuple[Dict[str, str], str]:
|
||||
"""
|
||||
执行重置操作
|
||||
:return: (新ID字典, 新GUID)
|
||||
"""
|
||||
try:
|
||||
# 检查管理员权限
|
||||
self._update_progress("checking", "检查管理员权限...")
|
||||
if not self.is_admin():
|
||||
raise PermissionError("必须以管理员权限运行该程序")
|
||||
|
||||
# 先退出 Cursor 进程
|
||||
self._update_progress("exiting", "正在关闭 Cursor 进程...")
|
||||
if not ExitCursor():
|
||||
raise Exception("无法完全关闭 Cursor 进程,请手动关闭后重试")
|
||||
|
||||
# 备份配置文件
|
||||
self._update_progress("backup", "备份配置文件...")
|
||||
backup_path = self.backup_file()
|
||||
self.logger.info(f"配置已备份到: {backup_path}")
|
||||
|
||||
# 生成新机器码
|
||||
self._update_progress("generating", "生成新的机器码...")
|
||||
new_ids = self.generate_new_ids()
|
||||
|
||||
# 更新配置文件
|
||||
self._update_progress("updating", "更新配置文件...")
|
||||
self.update_config(new_ids)
|
||||
|
||||
# 更新注册表
|
||||
self._update_progress("registry", "更新注册表...")
|
||||
new_guid = self.update_machine_guid()
|
||||
|
||||
self._update_progress("complete", "重置完成")
|
||||
self.logger.info("机器码重置成功")
|
||||
|
||||
return new_ids, new_guid
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"重置失败: {str(e)}")
|
||||
self._update_progress("error", f"操作失败: {str(e)}")
|
||||
raise
|
||||
1
services/__init__.py
Normal file
1
services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""服务层包"""
|
||||
BIN
services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/cursor_service.cpython-312.pyc
Normal file
BIN
services/__pycache__/cursor_service.cpython-312.pyc
Normal file
Binary file not shown.
237
services/cursor_service.py
Normal file
237
services/cursor_service.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""Cursor 服务管理类"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Tuple
|
||||
import urllib3
|
||||
import requests
|
||||
|
||||
from cursor_token_refresher import CursorTokenRefresher
|
||||
from machine_resetter import MachineResetter
|
||||
from update_disabler import UpdateDisabler
|
||||
from logger import logger
|
||||
from common_utils import (
|
||||
get_hardware_id,
|
||||
get_user_state,
|
||||
check_user_state,
|
||||
refresh_user_state,
|
||||
update_user_state,
|
||||
activate_device
|
||||
)
|
||||
|
||||
class CursorService:
|
||||
"""Cursor 服务管理类"""
|
||||
_instance = None
|
||||
_initialized = False
|
||||
_last_check_time = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._initialized:
|
||||
# 基础组件初始化
|
||||
self.token_refresher = CursorTokenRefresher()
|
||||
self.machine_resetter = MachineResetter()
|
||||
self.update_disabler = UpdateDisabler()
|
||||
self.logger = logger.get_logger("CursorService")
|
||||
|
||||
# 配置文件路径
|
||||
self.config_dir = os.path.join(os.getenv('APPDATA'), 'TingquanAssistant')
|
||||
self.config_file = os.path.join(self.config_dir, 'config.json')
|
||||
|
||||
# 确保配置目录存在
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
|
||||
# 初始化配置
|
||||
self._init_config()
|
||||
|
||||
# 首次初始化用户状态
|
||||
self._lazy_init_user_state()
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def _init_config(self):
|
||||
"""初始化配置文件"""
|
||||
# 默认配置
|
||||
self.default_config = {
|
||||
"version": "4.0.0.1",
|
||||
"last_check_update": None,
|
||||
"auto_check_update": True,
|
||||
"theme": "light"
|
||||
}
|
||||
|
||||
# 读取或创建配置文件
|
||||
if not os.path.exists(self.config_file):
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.default_config, f, indent=2, ensure_ascii=False)
|
||||
self.config = self.default_config
|
||||
else:
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
self.config = json.load(f)
|
||||
except Exception as e:
|
||||
self.logger.error(f"读取配置文件失败: {e}")
|
||||
self.config = self.default_config
|
||||
|
||||
def _lazy_init_user_state(self):
|
||||
"""
|
||||
懒加载方式初始化用户状态
|
||||
只在首次使用时初始化,避免频繁刷新
|
||||
"""
|
||||
try:
|
||||
if not self._last_check_time:
|
||||
machine_id = get_hardware_id()
|
||||
success, msg, state_data = refresh_user_state(machine_id)
|
||||
if success:
|
||||
self._last_check_time = datetime.now()
|
||||
self.logger.info("用户状态初始化成功")
|
||||
else:
|
||||
self.logger.warning(f"用户状态初始化失败: {msg}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化用户状态异常: {e}")
|
||||
|
||||
def _should_refresh_state(self) -> bool:
|
||||
"""
|
||||
检查是否需要刷新状态
|
||||
每5分钟检测一次设备状态
|
||||
|
||||
Returns:
|
||||
bool: 是否需要刷新状态
|
||||
"""
|
||||
try:
|
||||
# 如果没有上次检查时间,需要刷新
|
||||
if not self._last_check_time:
|
||||
return True
|
||||
|
||||
# 计算距离上次检查的时间
|
||||
now = datetime.now()
|
||||
time_diff = now - self._last_check_time
|
||||
|
||||
# 如果超过5分钟,需要刷新
|
||||
if time_diff.total_seconds() >= 300: # 5分钟 = 300秒
|
||||
self.logger.debug("距离上次状态检查已超过5分钟")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查状态刷新时间异常: {e}")
|
||||
# 发生异常时返回False,避免频繁刷新
|
||||
return False
|
||||
|
||||
def check_activation_status(self) -> Dict:
|
||||
"""
|
||||
检查当前激活状态
|
||||
如果需要刷新则自动刷新状态
|
||||
|
||||
Returns:
|
||||
Dict: 状态信息字典
|
||||
"""
|
||||
try:
|
||||
# 检查是否需要刷新状态
|
||||
if self._should_refresh_state():
|
||||
machine_id = get_hardware_id()
|
||||
success, msg, state_data = refresh_user_state(machine_id)
|
||||
if success:
|
||||
self._last_check_time = datetime.now()
|
||||
self.logger.info("设备状态检查完成")
|
||||
else:
|
||||
self.logger.warning(f"设备状态检查失败: {msg}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"设备状态检查异常: {e}")
|
||||
|
||||
# 无论是否刷新成功,都返回当前状态
|
||||
return get_user_state()
|
||||
|
||||
def activate_with_code(self, activation_code: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
使用激活码激活
|
||||
|
||||
Args:
|
||||
activation_code: 激活码
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否成功, 消息)
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"开始处理激活码: {activation_code}")
|
||||
|
||||
# 获取机器ID
|
||||
machine_id = get_hardware_id()
|
||||
if not machine_id:
|
||||
self.logger.error("获取机器ID失败")
|
||||
return False, "获取机器ID失败"
|
||||
|
||||
self.logger.info(f"获取到机器ID: {machine_id}")
|
||||
|
||||
# 调用激活接口
|
||||
self.logger.info("正在调用激活接口...")
|
||||
success, msg, data = activate_device(machine_id, activation_code)
|
||||
|
||||
if success:
|
||||
self.logger.info(f"激活成功: {msg}")
|
||||
else:
|
||||
self.logger.warning(f"激活失败: {msg}")
|
||||
|
||||
return success, msg
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"激活失败: {str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
def refresh_account(self, machine_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||
"""刷新账号"""
|
||||
# 检查是否已激活
|
||||
if not check_user_state():
|
||||
return False, "请先激活软件", None
|
||||
return self.token_refresher.refresh_token(machine_id)
|
||||
|
||||
def reset_machine(self) -> Tuple[bool, str, Dict]:
|
||||
"""重置机器码"""
|
||||
# 检查是否已激活
|
||||
if not check_user_state():
|
||||
return False, "请先激活软件", {}
|
||||
|
||||
try:
|
||||
new_ids, new_guid = self.machine_resetter.run()
|
||||
return True, "重置成功", {
|
||||
"new_ids": new_ids,
|
||||
"new_guid": new_guid
|
||||
}
|
||||
except Exception as e:
|
||||
return False, f"重置失败: {str(e)}", {}
|
||||
|
||||
def disable_update(self) -> Tuple[bool, str]:
|
||||
"""禁用更新"""
|
||||
# 检查是否已激活
|
||||
if not check_user_state():
|
||||
return False, "请先激活软件"
|
||||
|
||||
success = self.update_disabler.disable()
|
||||
return success, "禁用成功" if success else "禁用失败"
|
||||
|
||||
def save_config(self, new_config: Dict) -> bool:
|
||||
"""
|
||||
保存配置
|
||||
:param new_config: 新的配置信息
|
||||
:return: 是否成功
|
||||
"""
|
||||
try:
|
||||
self.config.update(new_config)
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存配置失败: {e}")
|
||||
return False
|
||||
|
||||
def get_config(self) -> Dict:
|
||||
"""获取当前配置"""
|
||||
return self.config.copy()
|
||||
|
||||
91
tingquan_assistant.py
Normal file
91
tingquan_assistant.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
听泉助手 - Cursor 激活管理工具
|
||||
|
||||
项目结构说明:
|
||||
---------------
|
||||
├── services/ # 服务层 - 处理核心业务逻辑
|
||||
│ ├── cursor_service.py # Cursor服务管理类 - 处理激活、配置等核心功能
|
||||
│ └── __init__.py
|
||||
│
|
||||
├── gui/ # GUI层 - 处理界面相关逻辑
|
||||
│ ├── components/ # 可复用的GUI组件
|
||||
│ │ ├── widgets.py # 基础UI组件(按钮、状态栏等)
|
||||
│ │ ├── workers.py # 后台工作线程
|
||||
│ │ └── __init__.py
|
||||
│ ├── windows/ # 窗口类
|
||||
│ │ ├── main_window.py # 主窗口实现
|
||||
│ │ └── __init__.py
|
||||
│ └── __init__.py
|
||||
│
|
||||
├── config.py # 配置管理
|
||||
├── logger.py # 日志管理
|
||||
├── machine_resetter.py # 机器码重置
|
||||
├── update_disabler.py # 更新禁用
|
||||
├── cursor_token_refresher.py # Token刷新
|
||||
└── tingquan_assistant.py # 程序入口文件
|
||||
|
||||
设计规范:
|
||||
---------------
|
||||
1. 分层设计
|
||||
- services层: 处理核心业务逻辑,与界面解耦
|
||||
- gui层: 只处理界面相关逻辑,通过services层调用业务功能
|
||||
- 工具类: 独立的功能模块(如日志、配置等)
|
||||
|
||||
2. 代码规范
|
||||
- 使用类型注解
|
||||
- 函数必须有文档字符串
|
||||
- 遵循PEP 8命名规范
|
||||
- 异常必须合理处理和记录日志
|
||||
|
||||
3. 界面设计
|
||||
- 使用PyQt5构建GUI
|
||||
- 所有耗时操作必须在后台线程中执行
|
||||
- 界面组件需实现合理的状态管理
|
||||
|
||||
4. 配置管理
|
||||
- 用户配置存储在 %APPDATA%/TingquanAssistant/
|
||||
- 激活信息使用JSON格式存储
|
||||
- 配置文件需要权限控制
|
||||
|
||||
5. 日志规范
|
||||
- 所有关键操作必须记录日志
|
||||
- 日志按日期分文件存储
|
||||
- 包含足够的错误诊断信息
|
||||
|
||||
使用说明:
|
||||
---------------
|
||||
1. 运行入口: python tingquan_assistant.py
|
||||
2. 开发新功能流程:
|
||||
- 在services层添加业务逻辑
|
||||
- 在gui/components添加必要的界面组件
|
||||
- 在gui/windows中整合界面
|
||||
- 更新配置和日志相关代码
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from gui.windows.main_window import MainWindow
|
||||
|
||||
def main():
|
||||
"""程序入口"""
|
||||
# 创建应用
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置应用样式
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# 创建主窗口
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
# 运行应用
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
120
tingquan_assistant.spec
Normal file
120
tingquan_assistant.spec
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||
|
||||
# 读取版本号
|
||||
with open('version.txt', 'r') as f:
|
||||
VERSION = f.read().strip()
|
||||
|
||||
# 收集所有Python文件
|
||||
python_files = []
|
||||
for root, dirs, files in os.walk('.'):
|
||||
if '__pycache__' in root or '.git' in root or 'venv' in root:
|
||||
continue
|
||||
for file in files:
|
||||
if file.endswith('.py') and not file.startswith('test_'):
|
||||
rel_path = os.path.relpath(os.path.join(root, file))
|
||||
python_files.append(rel_path)
|
||||
|
||||
# 收集数据文件
|
||||
datas = [
|
||||
('version.txt', '.'),
|
||||
('two.ico', '.'),
|
||||
('file_version_info.txt', '.'),
|
||||
('utils', 'utils'),
|
||||
('services', 'services'),
|
||||
('gui', 'gui'),
|
||||
]
|
||||
|
||||
# 添加其他资源文件
|
||||
if os.path.exists('requirements.txt'):
|
||||
datas.append(('requirements.txt', '.'))
|
||||
|
||||
a = Analysis(
|
||||
['tingquan_assistant.py'] + python_files,
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=[
|
||||
'PyQt5',
|
||||
'PyQt5.QtCore',
|
||||
'PyQt5.QtGui',
|
||||
'PyQt5.QtWidgets',
|
||||
'requests',
|
||||
'json',
|
||||
'logging',
|
||||
'win32api',
|
||||
'win32con',
|
||||
'win32gui',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[
|
||||
'_tkinter',
|
||||
'tkinter',
|
||||
'PIL.ImageTk',
|
||||
'PIL.ImageWin',
|
||||
'numpy',
|
||||
'pandas',
|
||||
'matplotlib',
|
||||
'scipy',
|
||||
'PyQt5.QtWebEngineWidgets',
|
||||
'PyQt5.QtWebEngine',
|
||||
],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=None,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
# 过滤掉不需要的二进制文件
|
||||
def remove_from_list(source, patterns):
|
||||
for file in source[:]:
|
||||
for pattern in patterns:
|
||||
if pattern in file[0]:
|
||||
source.remove(file)
|
||||
break
|
||||
|
||||
remove_from_list(a.binaries, [
|
||||
'Qt5WebEngine',
|
||||
'Qt5WebEngineCore',
|
||||
'Qt5WebEngineWidgets',
|
||||
'Qt5Designer',
|
||||
'Qt5Qml',
|
||||
])
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name=f'听泉助手v{VERSION}',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['two.ico'],
|
||||
version='file_version_info.txt',
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name=f'听泉助手v{VERSION}',
|
||||
)
|
||||
151
update_disabler.py
Normal file
151
update_disabler.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import requests
|
||||
import urllib3
|
||||
import shutil
|
||||
import subprocess
|
||||
import stat
|
||||
import getpass
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Tuple
|
||||
from logger import logger
|
||||
|
||||
class UpdateDisabler:
|
||||
"""更新禁用模块"""
|
||||
|
||||
def __init__(self, updater_path: str = None):
|
||||
"""
|
||||
初始化更新禁用器
|
||||
:param updater_path: 可选,updater目录路径
|
||||
"""
|
||||
# 设置基础路径
|
||||
localappdata = os.getenv('LOCALAPPDATA')
|
||||
if not localappdata:
|
||||
raise EnvironmentError("LOCALAPPDATA 环境变量未设置")
|
||||
|
||||
self.updater_path = updater_path or os.path.join(localappdata, 'cursor-updater')
|
||||
self._callback = None
|
||||
self.logger = logger.get_logger("UpdateDisable")
|
||||
|
||||
def set_progress_callback(self, callback) -> None:
|
||||
"""设置进度回调函数"""
|
||||
self._callback = callback
|
||||
|
||||
def _update_progress(self, status: str, message: str) -> None:
|
||||
"""更新进度信息"""
|
||||
if self._callback:
|
||||
self._callback({"status": status, "message": message})
|
||||
|
||||
def _ensure_parent_dir_exists(self):
|
||||
"""确保父目录存在"""
|
||||
parent_dir = os.path.dirname(self.updater_path)
|
||||
if not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
def disable(self) -> bool:
|
||||
"""
|
||||
禁用自动更新
|
||||
:return: 是否成功
|
||||
"""
|
||||
try:
|
||||
self._update_progress("start", "开始禁用自动更新...")
|
||||
|
||||
# 确保父目录存在
|
||||
self._ensure_parent_dir_exists()
|
||||
|
||||
# 检查并删除现有文件或目录
|
||||
self._update_progress("checking", "检查现有文件...")
|
||||
try:
|
||||
if os.path.exists(self.updater_path):
|
||||
self._update_progress("removing", "删除现有文件...")
|
||||
if os.path.isfile(self.updater_path):
|
||||
os.remove(self.updater_path)
|
||||
else:
|
||||
shutil.rmtree(self.updater_path, ignore_errors=True)
|
||||
self.logger.info(f"已删除: {self.updater_path}")
|
||||
else:
|
||||
self.logger.info("目标路径不存在,将创建新文件")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"删除操作异常: {str(e)}")
|
||||
|
||||
# 等待文件系统操作完成
|
||||
time.sleep(1)
|
||||
|
||||
# 创建阻止更新的文件
|
||||
self._update_progress("creating", "创建阻止文件...")
|
||||
try:
|
||||
# 再次确保路径不存在
|
||||
if os.path.exists(self.updater_path):
|
||||
os.remove(self.updater_path)
|
||||
|
||||
# 创建新文件
|
||||
with open(self.updater_path, 'w') as f:
|
||||
f.write("")
|
||||
self.logger.info("已创建阻止文件")
|
||||
|
||||
# 验证文件是否成功创建
|
||||
if not os.path.isfile(self.updater_path):
|
||||
raise Exception("文件创建验证失败")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"创建文件失败: {str(e)}")
|
||||
|
||||
# 设置文件只读属性
|
||||
self._update_progress("setting", "设置文件属性...")
|
||||
try:
|
||||
os.chmod(self.updater_path, stat.S_IREAD)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"设置只读属性失败: {str(e)}")
|
||||
|
||||
# 设置文件权限
|
||||
self._update_progress("permission", "设置文件权限...")
|
||||
try:
|
||||
username = getpass.getuser()
|
||||
result = subprocess.run(
|
||||
['icacls', self.updater_path, '/inheritance:r', '/grant:r', f"{username}:(R)"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
self.logger.warning(f"设置权限返回值非0: {result.stderr}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"设置权限失败: {str(e)}")
|
||||
|
||||
# 最终验证
|
||||
if not os.path.exists(self.updater_path):
|
||||
raise Exception("最终验证失败:文件不存在")
|
||||
|
||||
if os.path.isdir(self.updater_path):
|
||||
raise Exception("最终验证失败:创建了目录而不是文件")
|
||||
|
||||
self._update_progress("complete", "禁用自动更新完成")
|
||||
self.logger.info("成功禁用自动更新")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"禁用自动更新失败: {str(e)}")
|
||||
self._update_progress("error", f"操作失败: {str(e)}")
|
||||
self.show_manual_guide()
|
||||
return False
|
||||
|
||||
def show_manual_guide(self) -> str:
|
||||
"""
|
||||
显示手动操作指南
|
||||
:return: 指南内容
|
||||
"""
|
||||
guide = (
|
||||
"自动禁用更新失败,请按以下步骤手动操作:\n"
|
||||
"1. 以管理员身份打开 PowerShell\n"
|
||||
"2. 执行以下命令:\n"
|
||||
f" Remove-Item -Path \"{self.updater_path}\" -Force -Recurse -ErrorAction SilentlyContinue\n"
|
||||
f" New-Item -Path \"{self.updater_path}\" -ItemType File -Force\n"
|
||||
f" Set-ItemProperty -Path \"{self.updater_path}\" -Name IsReadOnly -Value $true\n"
|
||||
f" icacls \"{self.updater_path}\" /inheritance:r /grant:r \"{getpass.getuser()}:(R)\""
|
||||
)
|
||||
|
||||
self.logger.warning("需要手动禁用更新")
|
||||
self.logger.info(guide)
|
||||
return guide
|
||||
BIN
utils/__pycache__/version_manager.cpython-312.pyc
Normal file
BIN
utils/__pycache__/version_manager.cpython-312.pyc
Normal file
Binary file not shown.
227
utils/version_manager.py
Normal file
227
utils/version_manager.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""版本管理模块
|
||||
|
||||
职责:
|
||||
1. 检查软件更新
|
||||
2. 管理版本信息
|
||||
3. 处理更新下载
|
||||
4. 维护更新配置
|
||||
|
||||
设计原则:
|
||||
1. 单一职责
|
||||
2. 配置与逻辑分离
|
||||
3. 异常处理标准化
|
||||
4. 日志完整记录
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import urllib3
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, Optional
|
||||
from logger import logger
|
||||
from config import config
|
||||
|
||||
class VersionConfig:
|
||||
"""版本配置类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化版本配置"""
|
||||
self.update_url = f"{config.base_url}/admin/api.version/check"
|
||||
self.config_dir = os.path.join(os.getenv('APPDATA'), 'TingquanAssistant')
|
||||
self.config_file = os.path.join(self.config_dir, 'version.json')
|
||||
|
||||
# 确保配置目录存在
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
|
||||
# 获取版本号
|
||||
self.current_version = self._get_version()
|
||||
|
||||
def _get_version(self) -> str:
|
||||
"""
|
||||
从version.txt文件获取当前版本号
|
||||
如果是打包后的程序,从打包目录读取
|
||||
如果是开发环境,从根目录读取
|
||||
"""
|
||||
try:
|
||||
# 确定version.txt的路径
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 如果是打包后的程序
|
||||
base_path = Path(sys._MEIPASS)
|
||||
else:
|
||||
# 如果是开发环境,获取根目录路径
|
||||
current_file = Path(__file__)
|
||||
base_path = current_file.parent.parent # utils -> root
|
||||
|
||||
version_file = base_path / "version.txt"
|
||||
|
||||
# 读取版本号
|
||||
if version_file.exists():
|
||||
with open(version_file, "r", encoding="utf-8") as f:
|
||||
version = f.read().strip()
|
||||
logger.get_logger().info(f"当前版本: {version}")
|
||||
return version
|
||||
else:
|
||||
logger.get_logger().error(f"版本文件不存在: {version_file}")
|
||||
return "0.0.0"
|
||||
|
||||
except Exception as e:
|
||||
logger.get_logger().error(f"读取版本号失败: {str(e)}")
|
||||
return "0.0.0"
|
||||
|
||||
class VersionManager:
|
||||
"""版本管理器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化版本管理器"""
|
||||
self.config = VersionConfig()
|
||||
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def _load_config(self) -> Dict:
|
||||
"""加载版本配置"""
|
||||
try:
|
||||
if os.path.exists(self.config.config_file):
|
||||
with open(self.config.config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.get_logger().error(f"加载版本配置失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _save_config(self, config_data: Dict) -> bool:
|
||||
"""保存版本配置"""
|
||||
try:
|
||||
with open(self.config.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.get_logger().error(f"保存版本配置失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def check_update(self) -> Tuple[bool, str, Optional[Dict]]:
|
||||
"""
|
||||
检查更新
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, Optional[Dict]]:
|
||||
- bool: 是否有更新
|
||||
- str: 消息
|
||||
- Optional[Dict]: 更新信息
|
||||
"""
|
||||
try:
|
||||
log = logger.get_logger()
|
||||
log.info(f"开始检查更新,当前版本: {self.config.current_version}")
|
||||
|
||||
# 准备请求数据
|
||||
data = {
|
||||
"version": self.config.current_version,
|
||||
"platform": "windows"
|
||||
}
|
||||
|
||||
log.info(f"请求URL: {self.config.update_url}")
|
||||
log.info(f"请求数据: {json.dumps(data, ensure_ascii=False)}")
|
||||
log.info(f"请求头: {json.dumps(config.request_config['headers'], ensure_ascii=False)}")
|
||||
|
||||
# 发送更新检查请求
|
||||
response = requests.post(
|
||||
self.config.update_url,
|
||||
json=data,
|
||||
headers=config.request_config["headers"],
|
||||
timeout=config.request_config["timeout"],
|
||||
verify=config.request_config["verify"]
|
||||
)
|
||||
|
||||
log.info(f"响应状态码: {response.status_code}")
|
||||
log.info(f"响应内容: {response.text}")
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
|
||||
# 检查响应码(0或1都是正常响应)
|
||||
if result.get("code") in [0, 1]:
|
||||
update_info = result.get("data", {})
|
||||
|
||||
if update_info.get("has_update"):
|
||||
# 有更新可用
|
||||
version_info = update_info.get("version_info", {})
|
||||
new_version = version_info.get("version")
|
||||
update_msg = version_info.get("update_msg", "")
|
||||
download_url = version_info.get("download_url", "")
|
||||
is_force = update_info.get("is_force", 0)
|
||||
|
||||
log.info(f"发现新版本: {new_version}")
|
||||
log.info(f"更新信息: {json.dumps(version_info, ensure_ascii=False)}")
|
||||
|
||||
update_data = {
|
||||
"version": new_version,
|
||||
"message": update_msg,
|
||||
"download_url": download_url,
|
||||
"is_force": bool(is_force),
|
||||
"check_time": update_info.get("check_time")
|
||||
}
|
||||
|
||||
# 更新配置
|
||||
config_data = self._load_config()
|
||||
config_data["last_check_update"] = update_data
|
||||
self._save_config(config_data)
|
||||
|
||||
return True, f"发现新版本: {new_version}", update_data
|
||||
else:
|
||||
log.info("当前已是最新版本")
|
||||
return False, "当前已是最新版本", None
|
||||
|
||||
else:
|
||||
error_msg = result.get("msg", "检查更新失败")
|
||||
log.error(f"检查更新失败: {error_msg}")
|
||||
log.error(f"错误响应: {json.dumps(result, ensure_ascii=False)}")
|
||||
return False, f"检查更新失败: {error_msg}", None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"网络连接失败: {str(e)}"
|
||||
logger.get_logger().error(error_msg)
|
||||
return False, error_msg, None
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"解析响应失败: {str(e)}"
|
||||
logger.get_logger().error(error_msg)
|
||||
logger.get_logger().error(f"无法解析的响应内容: {response.text}")
|
||||
return False, error_msg, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"检查更新异常: {str(e)}"
|
||||
logger.get_logger().error(error_msg)
|
||||
return False, error_msg, None
|
||||
|
||||
def get_last_update_info(self) -> Optional[Dict]:
|
||||
"""获取上次检查的更新信息"""
|
||||
try:
|
||||
config_data = self._load_config()
|
||||
update_info = config_data.get("last_check_update")
|
||||
|
||||
if update_info:
|
||||
logger.get_logger().info(f"获取到未完成的更新信息: {update_info['version']}")
|
||||
|
||||
return update_info
|
||||
|
||||
except Exception as e:
|
||||
logger.get_logger().error(f"获取更新信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def clear_update_info(self) -> bool:
|
||||
"""清除更新信息"""
|
||||
try:
|
||||
config_data = self._load_config()
|
||||
if "last_check_update" in config_data:
|
||||
del config_data["last_check_update"]
|
||||
success = self._save_config(config_data)
|
||||
if success:
|
||||
logger.get_logger().info("更新信息已清除")
|
||||
return success
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.get_logger().error(f"清除更新信息失败: {str(e)}")
|
||||
return False
|
||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
||||
4.0.0.1
|
||||
Reference in New Issue
Block a user