添加项目源代码

This commit is contained in:
huangzhenpc
2025-02-20 20:20:19 +08:00
parent 7c63d8a390
commit 268a32f78f
76 changed files with 3344 additions and 0 deletions

5
__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""
听泉助手 - 脚本包
"""
__version__ = "4.0.0.1"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

113
build.bat Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

68
exit_cursor.py Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
"""GUI 包"""

Binary file not shown.

View File

@@ -0,0 +1 @@
"""GUI 组件包"""

Binary file not shown.

Binary file not shown.

Binary file not shown.

634
gui/components/widgets.py Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
"""GUI 窗口包"""

Binary file not shown.

Binary file not shown.

667
gui/windows/main_window.py Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
"""服务层包"""

Binary file not shown.

Binary file not shown.

237
services/cursor_service.py Normal file
View 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
View 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
View 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}',
)

BIN
two.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

151
update_disabler.py Normal file
View 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

Binary file not shown.

227
utils/version_manager.py Normal file
View 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
View File

@@ -0,0 +1 @@
4.0.0.1