4 Commits

10 changed files with 1019 additions and 532 deletions

2
.gitignore vendored
View File

@@ -18,3 +18,5 @@ env/
# Local development settings # Local development settings
.env .env
testversion.txt

View File

@@ -50,9 +50,41 @@ class AccountSwitcher:
self.package_json = self.app_path / "package.json" self.package_json = self.app_path / "package.json"
self.auth_manager = CursorAuthManager() self.auth_manager = CursorAuthManager()
self.config = Config() self.config = Config()
self.hardware_id = get_hardware_id() self.hardware_id = self.get_hardware_id() # 先获取硬件ID
self.registry = CursorRegistry() # 添加注册表操作工具类 self.registry = CursorRegistry() # 添加注册表操作工具类
logging.info(f"初始化硬件ID: {self.hardware_id}")
def get_hardware_id(self) -> str:
"""获取硬件唯一标识"""
try:
# 创建startupinfo对象来隐藏命令行窗口
startupinfo = None
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 获取CPU信息
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()
# 组合信息并生成哈希
combined = f"{cpu_id}:{board_id}:{bios_id}"
return hashlib.md5(combined.encode()).hexdigest()
except Exception as e:
logging.error(f"获取硬件ID失败: {str(e)}")
# 如果获取失败使用UUID作为备选方案
return str(uuid.uuid4())
def get_cursor_version(self) -> str: def get_cursor_version(self) -> str:
"""获取Cursor版本号""" """获取Cursor版本号"""
try: try:
@@ -71,199 +103,126 @@ class AccountSwitcher:
import platform import platform
import socket import socket
import requests import requests
import subprocess
# 获取操作系统信息 # 获取操作系统信息
os_info = f"{platform.system()} {platform.version()}" os_info = f"{platform.system()} {platform.release()}"
# 获取设备名称 # 获取设备名称
try:
# 在Windows上使用wmic获取更详细的计算机名称
if platform.system() == "Windows":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
computer_info = subprocess.check_output('wmic computersystem get model', startupinfo=startupinfo).decode()
device_name = computer_info.split('\n')[1].strip()
else:
device_name = platform.node()
except:
device_name = platform.node() device_name = platform.node()
# 获取地理位置(可选) # 获取IP地址
try: try:
ip_info = requests.get('https://ipapi.co/json/', timeout=5).json() ip_response = requests.get('https://api.ipify.org?format=json', timeout=5)
ip_address = ip_response.json()['ip']
except:
ip_address = "未知"
# 获取地理位置
try:
ip_info = requests.get(f'https://ipapi.co/{ip_address}/json/', timeout=5).json()
location = f"{ip_info.get('country_name', '')}-{ip_info.get('region', '')}-{ip_info.get('city', '')}" location = f"{ip_info.get('country_name', '')}-{ip_info.get('region', '')}-{ip_info.get('city', '')}"
except: except:
location = "" location = "未知"
return { return {
"os": os_info, "os": os_info,
"device_name": device_name, "device_name": device_name,
"ip": ip_address,
"location": location "location": location
} }
except Exception as e: except Exception as e:
logging.error(f"获取设备信息失败: {str(e)}") logging.error(f"获取设备信息失败: {str(e)}")
return { return {
"os": "Windows 10", "os": "Windows",
"device_name": "未知设备", "device_name": "未知设备",
"location": "" "ip": "未知",
"location": "未知"
} }
def check_activation_code(self, code: str) -> Tuple[bool, str, Optional[Dict]]: def check_activation_code(self, code: str) -> tuple:
"""验证激活码 """检查激活码
Args:
code: 激活码
Returns: Returns:
Tuple[bool, str, Optional[Dict]]: (是否成功, 提示消息, 账号信息) tuple: (成功标志, 消息, 账号信息)
""" """
try: try:
# 获取当前状态和历史记录
member_info = self.config.load_member_info()
activation_history = member_info.get("activation_records", []) if member_info else []
# 分割多个激活码
codes = [c.strip() for c in code.split(',')]
success_codes = []
failed_codes = []
activation_results = []
# 获取设备信息
device_info = self.get_device_info()
# 逐个验证激活码
for single_code in codes:
if not single_code:
continue
# 验证激活码
endpoint = "https://cursorapi.nosqli.com/admin/api.member/activate"
data = { data = {
"code": single_code,
"machine_id": self.hardware_id, "machine_id": self.hardware_id,
"os": device_info["os"], "code": code
"device_name": device_info["device_name"],
"location": device_info["location"]
} }
headers = { # 禁用SSL警告
"Content-Type": "application/json" import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 设置请求参数
request_kwargs = {
"json": data,
"headers": {"Content-Type": "application/json"},
"timeout": 5, # 减少超时时间从10秒到5秒
"verify": False # 禁用SSL验证
} }
# 尝试发送请求
try: try:
response = requests.post( response = requests.post(
endpoint, self.config.get_api_url("activate"),
json=data, **request_kwargs
headers=headers, )
timeout=10 except requests.exceptions.SSLError:
# 如果发生SSL错误创建自定义SSL上下文
import ssl
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# 使用自定义SSL上下文重试请求
session = requests.Session()
session.verify = False
response = session.post(
self.config.get_api_url("activate"),
**request_kwargs
) )
response_data = response.json() result = response.json()
if response_data.get("code") == 200: # 激活成功
result_data = response_data.get("data", {}) if result["code"] == 200:
logging.info(f"激活码 {single_code} 验证成功: {response_data.get('msg', '')}") api_data = result["data"]
activation_results.append(result_data) # 构造标准的返回数据结构
success_codes.append(single_code) account_info = {
elif response_data.get("code") == 400: "status": "active",
error_msg = response_data.get("msg", "参数错误") "expire_time": api_data.get("expire_time", ""),
if "已被使用" in error_msg or "已激活" in error_msg: "total_days": api_data.get("total_days", 0),
logging.warning(f"激活码 {single_code} 已被使用") "days_left": api_data.get("days_left", 0),
failed_codes.append(f"{single_code} (已被使用)") "device_info": self.get_device_info()
else:
logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
failed_codes.append(f"{single_code} ({error_msg})")
elif response_data.get("code") == 500:
error_msg = response_data.get("msg", "系统错误")
logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
failed_codes.append(f"{single_code} ({error_msg})")
else:
error_msg = response_data.get("msg", "未知错误")
logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
failed_codes.append(f"{single_code} ({error_msg})")
except requests.RequestException as e:
logging.error(f"激活码 {single_code} 请求失败: {str(e)}")
failed_codes.append(f"{single_code} (网络请求失败)")
except Exception as e:
logging.error(f"激活码 {single_code} 处理失败: {str(e)}")
failed_codes.append(f"{single_code} (处理失败)")
if not success_codes:
failed_msg = "\n".join(failed_codes)
return False, f"激活失败:\n{failed_msg}", None
try:
# 使用最后一次激活的结果作为最终状态
final_result = activation_results[-1]
# 合并历史记录
new_activation_records = final_result.get("activation_records", [])
if activation_history:
# 保留旧的激活记录,避免重复
existing_codes = {record.get("code") for record in activation_history}
for record in new_activation_records:
if record.get("code") not in existing_codes:
activation_history.append(record)
else:
activation_history = new_activation_records
# 保存会员信息,包含完整的历史记录
member_info = {
"hardware_id": final_result.get("machine_id", self.hardware_id),
"expire_time": final_result.get("expire_time", ""),
"days_left": final_result.get("days_left", 0),
"total_days": final_result.get("total_days", 0),
"status": final_result.get("status", "inactive"),
"device_info": final_result.get("device_info", device_info),
"activation_time": final_result.get("activation_time", ""),
"activation_records": activation_history # 使用合并后的历史记录
} }
self.config.save_member_info(member_info) return True, result["msg"], account_info
# 激活码无效或已被使用
# 生成结果消息 elif result["code"] == 400:
message = f"激活成功\n" logging.warning(f"激活码无效或已被使用: {result.get('msg', '未知错误')}")
return False, result.get("msg", "激活码无效或已被使用"), None
# 显示每个成功激活码的信息 # 其他错误情况
for i, result in enumerate(activation_results, 1): else:
message += f"\n{i}个激活码:\n" logging.error(f"激活失败: {result.get('msg', '未知错误')}")
message += f"- 新增天数: {result.get('added_days', 0)}\n" return False, result["msg"], None # 返回 None 而不是空的账号信息
activation_time = result.get('activation_time', '')
if activation_time:
try:
from datetime import datetime
dt = datetime.strptime(activation_time, "%Y-%m-%d %H:%M:%S")
activation_time = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
pass
message += f"- 激活时间: {activation_time}\n"
message += f"\n最终状态:"
message += f"\n- 总天数: {final_result.get('total_days', 0)}"
message += f"\n- 剩余天数: {final_result.get('days_left', 0)}"
# 格式化到期时间显示
expire_time = final_result.get('expire_time', '')
if expire_time:
try:
dt = datetime.strptime(expire_time, "%Y-%m-%d %H:%M:%S")
expire_time = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
pass
message += f"\n- 到期时间: {expire_time}"
# 显示完整的激活记录历史
message += "\n\n历史激活记录:"
for record in activation_history:
activation_time = record.get('activation_time', '')
if activation_time:
try:
dt = datetime.strptime(activation_time, "%Y-%m-%d %H:%M:%S")
activation_time = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
pass
message += f"\n- 激活码: {record.get('code', '')}"
message += f"\n 天数: {record.get('days', 0)}"
message += f"\n 时间: {activation_time}\n"
if failed_codes:
message += f"\n\n以下激活码验证失败:\n" + "\n".join(failed_codes)
return True, message, member_info
except Exception as e: except Exception as e:
logging.error(f"处理激活结果时出错: {str(e)}") logging.error(f"激活失败: {str(e)}")
return False, f"处理激活结果时出错: {str(e)}", None return False, f"激活失败: {str(e)}", None # 返回 None 而不是空的账号信息
except Exception as e:
logging.error(f"验证激活码时出错: {str(e)}")
return False, f"验证激活码时出错: {str(e)}", None
def reset_machine_id(self) -> bool: def reset_machine_id(self) -> bool:
"""重置机器码""" """重置机器码"""
@@ -356,92 +315,85 @@ class AccountSwitcher:
logging.error(f"激活过程出错: {str(e)}") logging.error(f"激活过程出错: {str(e)}")
return False, f"激活失败: {str(e)}" return False, f"激活失败: {str(e)}"
def get_member_status(self) -> Optional[Dict]: def get_member_status(self) -> dict:
"""获取会员状态 """获取会员状态
Returns: Returns:
Optional[Dict]: 会员状态信息 dict: 会员状态信息,包含:
- status: 状态(active/inactive/expired)
- expire_time: 到期时间
- total_days: 总天数
- days_left: 剩余天数
- device_info: 设备信息
""" """
try: try:
# 读取保存的会员信息
member_info = self.config.load_member_info()
# 构造状态检查请求
endpoint = "https://cursorapi.nosqli.com/admin/api.member/status"
data = { data = {
"machine_id": self.hardware_id "machine_id": self.hardware_id
} }
headers = {
"Content-Type": "application/json" api_url = self.config.get_api_url("status")
logging.info(f"正在检查会员状态...")
# 设置请求参数
request_kwargs = {
"json": data,
"headers": {"Content-Type": "application/json"},
"timeout": 2, # 减少超时时间到2秒
"verify": False
} }
# 使用session来复用连接
session = requests.Session()
session.verify = False
# 尝试发送请求
try: try:
response = requests.post(endpoint, json=data, headers=headers, timeout=10) response = session.post(api_url, **request_kwargs)
response_data = response.json() except requests.exceptions.Timeout:
# 超时后快速重试一次
logging.warning("首次请求超时,正在重试...")
response = session.post(api_url, **request_kwargs)
if response_data.get("code") == 200: result = response.json()
# 正常状态 logging.info(f"状态检查响应: {result}")
data = response_data.get("data", {})
status = data.get("status", "inactive")
# 构造会员信息 if result.get("code") in [1, 200]:
member_info = { api_data = result.get("data", {})
"hardware_id": data.get("machine_id", self.hardware_id), return {
"expire_time": data.get("expire_time", ""), "status": api_data.get("status", "inactive"),
"days_left": data.get("days_left", 0), # 使用days_left "expire_time": api_data.get("expire_time", ""),
"total_days": data.get("total_days", 0), # 使用total_days "total_days": api_data.get("total_days", 0),
"status": status, "days_left": api_data.get("days_left", 0),
"activation_records": data.get("activation_records", []) # 保存激活记录 "device_info": self.get_device_info()
}
else:
error_msg = result.get("msg", "未知错误")
logging.error(f"获取状态失败: {error_msg}")
return {
"status": "inactive",
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
} }
# 打印调试信息 except requests.exceptions.RequestException as e:
logging.info(f"API返回数据: {data}") logging.error(f"API请求失败: {str(e)}")
logging.info(f"处理后的会员信息: {member_info}")
self.config.save_member_info(member_info)
return member_info
elif response_data.get("code") == 401:
# 未激活或已过期
logging.warning("会员未激活或已过期")
return self._get_inactive_status()
elif response_data.get("code") == 400:
# 参数错误
error_msg = response_data.get("msg", "参数错误")
logging.error(f"获取会员状态失败: {error_msg}")
return self._get_inactive_status()
elif response_data.get("code") == 500:
# 系统错误
error_msg = response_data.get("msg", "系统错误")
logging.error(f"获取会员状态失败: {error_msg}")
return self._get_inactive_status()
else:
# 其他未知错误
error_msg = response_data.get("msg", "未知错误")
logging.error(f"获取会员状态失败: {error_msg}")
return self._get_inactive_status()
except requests.RequestException as e:
logging.error(f"请求会员状态失败: {str(e)}")
return self._get_inactive_status()
except Exception as e:
logging.error(f"获取会员状态出错: {str(e)}")
return self._get_inactive_status()
def _get_inactive_status(self) -> Dict:
"""获取未激活状态的默认信息"""
return { return {
"hardware_id": self.hardware_id,
"expire_time": "",
"days": 0,
"total_days": 0,
"status": "inactive", "status": "inactive",
"last_activation": {}, "expire_time": "",
"activation_records": [] "total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
}
except Exception as e:
logging.error(f"获取会员状态失败: {str(e)}")
return {
"status": "inactive",
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
} }
def restart_cursor(self) -> bool: def restart_cursor(self) -> bool:
@@ -509,17 +461,33 @@ class AccountSwitcher:
"Content-Type": "application/json" "Content-Type": "application/json"
} }
try:
# 添加SSL验证选项和超时设置
response = requests.post(
endpoint,
json=data,
headers=headers,
timeout=30, # 增加超时时间
verify=False, # 禁用SSL验证
)
# 禁用SSL警告 # 禁用SSL警告
requests.packages.urllib3.disable_warnings() import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 设置请求参数
request_kwargs = {
"json": data,
"headers": headers,
"timeout": 30, # 增加超时时间
"verify": False # 禁用SSL验证
}
try:
# 尝试发送请求
try:
response = requests.post(endpoint, **request_kwargs)
except requests.exceptions.SSLError:
# 如果发生SSL错误创建自定义SSL上下文
import ssl
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# 使用自定义SSL上下文重试请求
session = requests.Session()
session.verify = False
response = session.post(endpoint, **request_kwargs)
response_data = response.json() response_data = response.json()

BIN
banbenjietu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,5 +1,7 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
import os import os
import sys
from PyInstaller.utils.hooks import collect_all
def get_version(): def get_version():
with open('version.txt', 'r', encoding='utf-8') as f: with open('version.txt', 'r', encoding='utf-8') as f:
@@ -8,26 +10,43 @@ def get_version():
version = get_version() version = get_version()
# 收集所有需要的依赖
datas = [('icon', 'icon'), ('version.txt', '.')]
binaries = []
hiddenimports = [
'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
'PyQt5.sip', # PyQt5 必需
'PyQt5.QtNetwork', # 网络相关
'PIL', # Pillow 相关
'PIL._imaging', # Pillow 核心
'requests', 'urllib3', 'certifi', # 网络请求相关
'json', 'uuid', 'hashlib', 'logging', # 基础功能相关
'importlib', # 导入相关
'pkg_resources', # 包资源
]
# 主要的分析对象
a = Analysis( a = Analysis(
['main.py'], ['main.py'],
pathex=[], pathex=[],
binaries=[], binaries=binaries,
datas=[('icon', 'icon'), ('version.txt', '.')], datas=datas,
hiddenimports=[ hiddenimports=hiddenimports,
'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
'requests', 'urllib3', 'certifi', # 网络请求相关
'json', 'uuid', 'hashlib', 'logging' # 基础功能相关
],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
excludes=['_tkinter', 'tkinter', 'Tkinter'], # 排除 tkinter 相关模块 excludes=['_tkinter', 'tkinter', 'Tkinter'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
noarchive=False, noarchive=False,
optimize=0, module_collection_mode={'PyQt5': 'pyz+py'},
) )
# 创建PYZ
pyz = PYZ(a.pure) pyz = PYZ(a.pure)
# 创建EXE
exe = EXE( exe = EXE(
pyz, pyz,
a.scripts, a.scripts,

View File

@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QDialog, QProgressBar, QStyle) QDialog, QProgressBar, QStyle)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtGui import QIcon, QPixmap
import time
sys.path.append(str(Path(__file__).parent.parent)) sys.path.append(str(Path(__file__).parent.parent))
@@ -43,6 +44,7 @@ class LoadingDialog(QDialog):
self.progress_bar = QProgressBar() self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(False) self.progress_bar.setTextVisible(False)
self.progress_bar.setRange(0, 0) # 设置为循环模式 self.progress_bar.setRange(0, 0) # 设置为循环模式
self.progress_bar.setMinimumWidth(250) # 设置最小宽度
layout.addWidget(self.progress_bar) layout.addWidget(self.progress_bar)
self.setLayout(layout) self.setLayout(layout)
@@ -53,22 +55,64 @@ class LoadingDialog(QDialog):
background-color: #f8f9fa; background-color: #f8f9fa;
} }
QLabel { QLabel {
color: #333333; color: #0d6efd;
font-size: 14px; font-size: 14px;
font-weight: bold;
padding: 10px; padding: 10px;
} }
QProgressBar { QProgressBar {
border: 2px solid #e9ecef; border: 2px solid #e9ecef;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
min-height: 12px;
} }
QProgressBar::chunk { QProgressBar::chunk {
background-color: #0d6efd; background-color: #0d6efd;
width: 10px; width: 15px;
margin: 0.5px; margin: 0.5px;
} }
""") """)
# 添加定时器以自动更新进度条动画
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_progress)
self.progress_value = 0
def update_progress(self):
"""更新进度条动画"""
self.progress_value = (self.progress_value + 1) % 100
self.progress_bar.setValue(self.progress_value)
def showEvent(self, event):
"""显示时启动定时器"""
super().showEvent(event)
self.timer.start(50) # 每50毫秒更新一次
def hideEvent(self, event):
"""隐藏时停止定时器"""
self.timer.stop()
super().hideEvent(event)
def check_status(self):
"""检查会员状态从API获取"""
try:
# 只在首次检查时显示加载对话框,并设置更明确的提示
if self._activation_status is None:
self.show_loading_dialog("正在连接服务器,请稍候...")
# 创建工作线程
self.worker = ApiWorker(self.switcher.get_member_status)
self.worker.finished.connect(self.on_status_check_complete)
self.worker.start()
except Exception as e:
if self._activation_status is None:
self.hide_loading_dialog()
QMessageBox.critical(self, "错误", f"检查状态失败: {str(e)}")
self._activation_status = False
logging.error(f"检查状态时发生错误: {str(e)}")
return False
class ApiWorker(QThread): class ApiWorker(QThread):
"""API请求工作线程""" """API请求工作线程"""
finished = pyqtSignal(tuple) # 发送结果信号 finished = pyqtSignal(tuple) # 发送结果信号
@@ -96,6 +140,11 @@ class MainWindow(QMainWindow):
self._activation_status = None # 缓存的激活状态 self._activation_status = None # 缓存的激活状态
self._status_timer = None # 状态更新定时器 self._status_timer = None # 状态更新定时器
# 添加请求锁,防止重复提交
self._is_requesting = False
self._last_request_time = 0
self._request_cooldown = 2 # 请求冷却时间(秒)
version = get_version() version = get_version()
cursor_version = self.switcher.get_cursor_version() cursor_version = self.switcher.get_cursor_version()
self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})") self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})")
@@ -104,92 +153,161 @@ class MainWindow(QMainWindow):
# 设置窗口图标 # 设置窗口图标
icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icon", "two.ico") icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icon", "two.ico")
if os.path.exists(icon_path): if os.path.exists(icon_path):
window_icon = QIcon(icon_path) self.window_icon = QIcon(icon_path)
if not window_icon.isNull(): if not self.window_icon.isNull():
self.setWindowIcon(window_icon) self.setWindowIcon(self.window_icon)
logging.info(f"成功设置窗口图标: {icon_path}") logging.info(f"成功设置窗口图标: {icon_path}")
else:
logging.warning("图标文件加载失败")
# 创建系统托盘图标 # 创建系统托盘图标
self.tray_icon = QSystemTrayIcon(self) self.create_tray_icon()
self.tray_icon.setIcon(self.windowIcon())
self.tray_icon.setToolTip("听泉Cursor助手")
# 创建托盘菜单
tray_menu = QMenu()
show_action = tray_menu.addAction("显示主窗口")
show_action.triggered.connect(self.show)
quit_action = tray_menu.addAction("退出")
quit_action.triggered.connect(QApplication.instance().quit)
# 设置托盘菜单
self.tray_icon.setContextMenu(tray_menu)
# 连接托盘图标的信号
self.tray_icon.activated.connect(self.on_tray_icon_activated)
# 显示托盘图标
self.tray_icon.show()
# 创建主窗口部件 # 创建主窗口部件
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
# 设置主窗口样式
central_widget.setStyleSheet("""
QWidget {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #f8f9fa,
stop:0.5 #ffffff,
stop:1 #f8f9fa);
}
QLabel {
color: #495057;
}
QLineEdit {
background-color: #ffffff;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 5px;
color: #495057;
}
QTextEdit {
background-color: #ffffff;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 5px;
color: #495057;
}
""")
# 创建主布局 # 创建主布局
main_layout = QVBoxLayout(central_widget) main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(30, 30, 30, 30)
# 设备ID区域 # 设备ID区域
device_frame = QFrame() device_layout = QHBoxLayout()
device_layout = QHBoxLayout(device_frame)
device_layout.addWidget(QLabel("设备识别码(勿动):")) device_layout.addWidget(QLabel("设备识别码(勿动):"))
self.hardware_id_edit = QLineEdit(self.switcher.hardware_id) self.hardware_id_edit = QLineEdit(self.switcher.hardware_id)
self.hardware_id_edit.setReadOnly(True) self.hardware_id_edit.setReadOnly(True)
device_layout.addWidget(self.hardware_id_edit) device_layout.addWidget(self.hardware_id_edit)
copy_btn = QPushButton("复制ID") copy_btn = QPushButton("复制ID")
copy_btn.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
padding: 5px 15px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5a6268;
}
""")
copy_btn.clicked.connect(self.copy_device_id) copy_btn.clicked.connect(self.copy_device_id)
device_layout.addWidget(copy_btn) device_layout.addWidget(copy_btn)
main_layout.addWidget(device_frame) main_layout.addLayout(device_layout)
# 添加分隔线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setStyleSheet("background-color: #e9ecef;")
main_layout.addWidget(line)
# 会员状态区域 # 会员状态区域
status_frame = QFrame() status_label = QLabel("会员状态")
status_layout = QVBoxLayout(status_frame) status_label.setStyleSheet("font-weight: bold; margin-top: 10px;")
status_layout.addWidget(QLabel("会员状态")) main_layout.addWidget(status_label)
self.status_text = QTextEdit() self.status_text = QTextEdit()
self.status_text.setReadOnly(True) self.status_text.setReadOnly(True)
self.status_text.setMinimumHeight(100) self.status_text.setMinimumHeight(100)
status_layout.addWidget(self.status_text) main_layout.addWidget(self.status_text)
main_layout.addWidget(status_frame)
# 添加分隔线
line2 = QFrame()
line2.setFrameShape(QFrame.HLine)
line2.setStyleSheet("background-color: #e9ecef;")
main_layout.addWidget(line2)
# 激活区域 # 激活区域
activation_frame = QFrame() main_layout.addWidget(QLabel("激活(叠加)会员,多个激活码可叠加整体时长"))
activation_layout = QVBoxLayout(activation_frame)
# 使用说明
usage_label = QLabel()
usage_label.setText("使用说明:\n\n1. 输入激活码并点击激活\n2. 激活成功后点击\"刷新Cursor编辑器授权\"即可正常使用\n3. 如果刷新无效,请点击\"突破Cursor0.45.x限制\"\n4. 建议点击\"禁用Cursor版本更新\"保持长期稳定")
usage_label.setStyleSheet("""
QLabel {
color: #6c757d;
font-size: 13px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
""")
activation_layout.addWidget(usage_label)
# 激活码输入区域 # 激活码输入区域
activation_layout.addWidget(QLabel("激活(叠加)会员,多个激活码可叠加整体时长")) input_layout = QHBoxLayout()
input_frame = QFrame()
input_layout = QHBoxLayout(input_frame)
input_layout.addWidget(QLabel("激活码:")) input_layout.addWidget(QLabel("激活码:"))
self.activation_edit = QLineEdit() self.activation_edit = QLineEdit()
input_layout.addWidget(self.activation_edit) input_layout.addWidget(self.activation_edit)
activate_btn = QPushButton("激活") activate_btn = QPushButton("激活")
activate_btn.setStyleSheet("""
QPushButton {
background-color: #0d6efd;
color: white;
border: none;
padding: 8px 25px;
border-radius: 4px;
font-size: 13px;
min-width: 80px;
}
QPushButton:hover {
background-color: #0b5ed7;
}
QPushButton:pressed {
background-color: #0a58ca;
}
""")
activate_btn.clicked.connect(self.activate_account) activate_btn.clicked.connect(self.activate_account)
input_layout.addWidget(activate_btn) input_layout.addWidget(activate_btn)
activation_layout.addWidget(input_frame) main_layout.addLayout(input_layout)
main_layout.addWidget(activation_frame)
# 使用说明
usage_label = QLabel()
usage_text = (
"<div style='background-color: #f8f9fa; padding: 15px; border-radius: 8px; border: 2px solid #0d6efd;'>"
"<p style='margin-bottom: 15px; font-size: 15px;'><b style='color: #0d6efd;'>使用步骤说明:</b></p>"
"<p style='line-height: 2.0;'>"
"<span style='font-size: 14px; color: #dc3545;'><b>第一步:</b></span> "
"输入激活码点击<b style='color: #0d6efd;'>【激活】</b>按钮完成激活<br>"
"<span style='font-size: 14px; color: #198754;'><b>第二步:</b></span> "
"点击<b style='color: #0d6efd;'>【刷新Cursor编辑器授权】</b><span style='color: #198754;'>一般情况下刷新即可正常使用</span><br>"
"<span style='font-size: 14px; color: #fd7e14;'><b>如果无法对话:</b></span> "
"点击<b style='color: #198754;'>【突破Cursor0.45.x限制】</b>,然后重新刷新授权<br>"
"<span style='font-size: 14px; color: #6c757d;'><b>建议操作:</b></span> "
"点击<b style='color: #dc3545;'>【禁用Cursor版本更新】</b><span style='color: #6c757d;'>保持软件稳定运行</span>"
"</p>"
"</div>"
)
usage_label.setText(usage_text)
usage_label.setStyleSheet("""
QLabel {
color: #333333;
font-size: 13px;
padding: 0px;
margin: 10px 0;
}
""")
usage_label.setTextFormat(Qt.RichText)
usage_label.setWordWrap(True)
main_layout.addWidget(usage_label)
# 操作按钮区域 # 操作按钮区域
btn_frame = QFrame() btn_frame = QFrame()
@@ -203,8 +321,8 @@ class MainWindow(QMainWindow):
border: none; border: none;
padding: 15px; padding: 15px;
border-radius: 6px; border-radius: 6px;
font-size: 14px; font-size: 13px;
min-width: 200px; min-width: 300px;
margin: 5px; margin: 5px;
} }
QPushButton:hover { QPushButton:hover {
@@ -216,21 +334,21 @@ class MainWindow(QMainWindow):
""" """
# 刷新授权按钮 # 刷新授权按钮
refresh_btn = QPushButton("刷新Cursor编辑器授权") refresh_btn = QPushButton("刷新 Cursor 编辑器授权")
refresh_btn.setStyleSheet(button_style) refresh_btn.setStyleSheet(button_style)
refresh_btn.clicked.connect(self.refresh_cursor_auth) refresh_btn.clicked.connect(self.refresh_cursor_auth)
refresh_btn.setMinimumHeight(50) refresh_btn.setMinimumHeight(50)
btn_layout.addWidget(refresh_btn) btn_layout.addWidget(refresh_btn)
# 突破限制按钮 # 突破限制按钮
bypass_btn = QPushButton("突破Cursor0.45.x限制") bypass_btn = QPushButton("突破 Cursor 0.45.x 限制")
bypass_btn.setStyleSheet(button_style.replace("#0d6efd", "#198754").replace("#0b5ed7", "#157347").replace("#0a58ca", "#146c43")) bypass_btn.setStyleSheet(button_style.replace("#0d6efd", "#198754").replace("#0b5ed7", "#157347").replace("#0a58ca", "#146c43"))
bypass_btn.clicked.connect(self.dummy_function) bypass_btn.clicked.connect(self.bypass_cursor_limit)
bypass_btn.setMinimumHeight(50) bypass_btn.setMinimumHeight(50)
btn_layout.addWidget(bypass_btn) btn_layout.addWidget(bypass_btn)
# 禁用更新按钮 # 禁用更新按钮
disable_update_btn = QPushButton("禁用Cursor版本更新") disable_update_btn = QPushButton("禁用 Cursor 版本更新")
disable_update_btn.setStyleSheet(button_style.replace("#0d6efd", "#dc3545").replace("#0b5ed7", "#bb2d3b").replace("#0a58ca", "#b02a37")) disable_update_btn.setStyleSheet(button_style.replace("#0d6efd", "#dc3545").replace("#0b5ed7", "#bb2d3b").replace("#0a58ca", "#b02a37"))
disable_update_btn.clicked.connect(self.disable_cursor_update) disable_update_btn.clicked.connect(self.disable_cursor_update)
disable_update_btn.setMinimumHeight(50) disable_update_btn.setMinimumHeight(50)
@@ -245,17 +363,86 @@ class MainWindow(QMainWindow):
# 启动时检查一次状态 # 启动时检查一次状态
QTimer.singleShot(0, self.check_status) QTimer.singleShot(0, self.check_status)
def create_tray_icon(self):
"""创建系统托盘图标"""
try:
# 确保QSystemTrayIcon可用
if not QSystemTrayIcon.isSystemTrayAvailable():
logging.error("系统托盘不可用")
return
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(self.window_icon)
self.tray_icon.setToolTip("听泉Cursor助手")
# 创建托盘菜单
self.tray_menu = QMenu() # 保存为实例变量
# 添加刷新重置按钮
refresh_action = self.tray_menu.addAction("刷新重置")
refresh_action.triggered.connect(self.refresh_cursor_auth)
show_action = self.tray_menu.addAction("显示主窗口")
show_action.triggered.connect(self.show_and_activate)
# 添加分隔线
self.tray_menu.addSeparator()
quit_action = self.tray_menu.addAction("退出")
quit_action.triggered.connect(self.quit_application)
# 设置托盘菜单
self.tray_icon.setContextMenu(self.tray_menu)
# 连接托盘图标的信号
self.tray_icon.activated.connect(self.on_tray_icon_activated)
# 显示托盘图标
self.tray_icon.show()
logging.info("系统托盘图标创建成功")
except Exception as e:
logging.error(f"创建系统托盘图标失败: {str(e)}")
def show_and_activate(self):
"""显示并激活窗口"""
self.show()
self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
self.activateWindow()
self.raise_() # 确保窗口在最前面
def quit_application(self):
"""退出应用程序"""
try:
# 停止定时器
if self._status_timer:
self._status_timer.stop()
# 移除托盘图标
if hasattr(self, 'tray_icon'):
self.tray_icon.setVisible(False) # 先隐藏托盘图标
self.tray_icon.deleteLater() # 删除托盘图标
# 退出应用程序
QApplication.instance().quit()
except Exception as e:
logging.error(f"退出应用程序时发生错误: {str(e)}")
QApplication.instance().quit()
def on_tray_icon_activated(self, reason): def on_tray_icon_activated(self, reason):
"""处理托盘图标的点击事件""" """处理托盘图标的点击事件"""
if reason == QSystemTrayIcon.DoubleClick: if reason in (QSystemTrayIcon.DoubleClick, QSystemTrayIcon.Trigger):
self.show() self.show_and_activate()
self.activateWindow()
def closeEvent(self, event): def closeEvent(self, event):
"""重写关闭事件,最小化到托盘而不是退出""" """重写关闭事件,最小化到托盘而不是退出"""
try:
if hasattr(self, 'tray_icon') and self.tray_icon.isVisible(): if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
event.ignore() event.ignore()
self.hide() self.hide()
# 确保托盘图标显示
self.tray_icon.show()
self.tray_icon.showMessage( self.tray_icon.showMessage(
"听泉Cursor助手", "听泉Cursor助手",
"程序已最小化到系统托盘", "程序已最小化到系统托盘",
@@ -263,6 +450,16 @@ class MainWindow(QMainWindow):
2000 2000
) )
else: else:
# 如果托盘图标不可用,则正常退出
if self._status_timer:
self._status_timer.stop()
event.accept()
except Exception as e:
logging.error(f"处理关闭事件时发生错误: {str(e)}")
# 发生错误时,接受关闭事件
if self._status_timer:
self._status_timer.stop()
event.accept() event.accept()
if self._status_timer: if self._status_timer:
@@ -271,21 +468,46 @@ class MainWindow(QMainWindow):
def copy_device_id(self): def copy_device_id(self):
"""复制设备ID到剪贴板""" """复制设备ID到剪贴板"""
if not self.check_status():
return
QApplication.clipboard().setText(self.hardware_id_edit.text()) QApplication.clipboard().setText(self.hardware_id_edit.text())
QMessageBox.information(self, "提示", "设备ID已复制到剪贴板") QMessageBox.information(self, "提示", "设备ID已复制到剪贴板")
def show_loading_dialog(self, message="请稍候..."): def show_loading_dialog(self, message="请稍候..."):
"""显示加载对话框""" """显示加载对话框"""
if not hasattr(self, 'loading_dialog') or not self.loading_dialog:
self.loading_dialog = LoadingDialog(self, message) self.loading_dialog = LoadingDialog(self, message)
self.loading_dialog.setStyleSheet("""
QDialog {
background-color: #f8f9fa;
}
QLabel {
color: #0d6efd;
font-size: 14px;
font-weight: bold;
padding: 10px;
}
QProgressBar {
border: 2px solid #e9ecef;
border-radius: 5px;
text-align: center;
}
QProgressBar::chunk {
background-color: #0d6efd;
width: 10px;
margin: 0.5px;
}
""")
else:
self.loading_dialog.message_label.setText(message)
self.loading_dialog.show() self.loading_dialog.show()
QApplication.processEvents() # 确保对话框立即显示
def hide_loading_dialog(self): def hide_loading_dialog(self):
"""隐藏加载对话框""" """隐藏加载对话框"""
if hasattr(self, 'loading_dialog'): if hasattr(self, 'loading_dialog') and self.loading_dialog:
self.loading_dialog.hide() self.loading_dialog.hide()
self.loading_dialog.deleteLater() self.loading_dialog.deleteLater()
self.loading_dialog = None
QApplication.processEvents() # 确保对话框立即隐藏
def activate_account(self): def activate_account(self):
"""激活账号""" """激活账号"""
@@ -456,23 +678,21 @@ class MainWindow(QMainWindow):
if isinstance(data, tuple): if isinstance(data, tuple):
success, message, account_info = data success, message, account_info = data
if success: if success and account_info is not None:
# 更新会员信息显示 # 更新会员信息显示
self.update_status_display(account_info) self.update_status_display(account_info)
# 更新激活状态缓存 # 更新激活状态缓存
self._activation_status = True self._activation_status = account_info.get('status') == 'active'
# 更新状态定时器 # 更新状态定时器
days_left = account_info.get('days_left', 0) if self._activation_status:
seconds_left = days_left * 24 * 60 * 60
if self._status_timer: if self._status_timer:
self._status_timer.stop() self._status_timer.stop()
self._status_timer = QTimer(self) self._status_timer = QTimer(self)
self._status_timer.setSingleShot(True) self._status_timer.setSingleShot(False)
self._status_timer.timeout.connect(self.check_status) self._status_timer.timeout.connect(self.check_status)
update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次 self._status_timer.start(60 * 1000) # 每60秒检测一次
if update_interval > 0: logging.info("已设置每分钟检测会员状态")
self._status_timer.start(update_interval * 1000) # 转换为毫秒
msg = QMessageBox(self) msg = QMessageBox(self)
msg.setWindowTitle("激活成功") msg.setWindowTitle("激活成功")
@@ -549,21 +769,16 @@ class MainWindow(QMainWindow):
} }
""") """)
msg.exec_() msg.exec_()
# 所有失败情况都不更新状态显示
else: else:
QMessageBox.critical(self, "错误", f"激活失败: {data}") QMessageBox.critical(self, "错误", f"激活失败: {data}")
# 不更新状态显示
# 激活后检查一次状态
self.check_status()
def update_status_display(self, status_info: dict): def update_status_display(self, status_info: dict):
"""更新状态显示""" """更新状态显示"""
# 打印API返回的原始数据 # 打印API返回的原始数据
logging.info("=== API返回数据 ===") logging.info("=== API返回数据 ===")
logging.info(f"状态信息: {status_info}") logging.info(f"状态信息: {status_info}")
if 'activation_records' in status_info:
logging.info("激活记录:")
for record in status_info['activation_records']:
logging.info(f"- 记录: {record}")
# 更新状态文本 # 更新状态文本
status_map = { status_map = {
@@ -581,18 +796,10 @@ class MainWindow(QMainWindow):
f"剩余天数:{status_info.get('days_left', 0)}" f"剩余天数:{status_info.get('days_left', 0)}"
] ]
# 如果有激活记录,显示最近一次激活信息 # 添加设备信息
activation_records = status_info.get('activation_records', []) device_info = status_info.get('device_info', {})
if activation_records: if device_info:
latest_record = activation_records[-1] # 获取最新的激活记录
device_info = latest_record.get('device_info', {})
status_lines.extend([ status_lines.extend([
"",
"最近激活信息:",
f"激活码:{latest_record.get('code', '')}",
f"激活时间:{latest_record.get('activation_time', '')}",
f"增加天数:{latest_record.get('days', 0)}",
"", "",
"设备信息:", "设备信息:",
f"系统:{device_info.get('os', '')}", f"系统:{device_info.get('os', '')}",
@@ -607,8 +814,9 @@ class MainWindow(QMainWindow):
def check_status(self): def check_status(self):
"""检查会员状态从API获取""" """检查会员状态从API获取"""
try: try:
# 显示加载对话框 # 只在首次检查时显示加载对话框
self.show_loading_dialog("正在检查会员状态,请稍候...") if self._activation_status is None:
self.show_loading_dialog("正在连接服务器...")
# 创建工作线程 # 创建工作线程
self.worker = ApiWorker(self.switcher.get_member_status) self.worker = ApiWorker(self.switcher.get_member_status)
@@ -616,51 +824,67 @@ class MainWindow(QMainWindow):
self.worker.start() self.worker.start()
except Exception as e: except Exception as e:
if self._activation_status is None:
self.hide_loading_dialog() self.hide_loading_dialog()
QMessageBox.critical(self, "错误", f"检查状态失败: {str(e)}") QMessageBox.critical(self, "错误", f"检查状态失败: {str(e)}")
self._activation_status = False self._activation_status = False
logging.error(f"检查状态时发生错误: {str(e)}")
return False return False
def on_status_check_complete(self, result): def on_status_check_complete(self, result):
"""状态检查完成回调""" """状态检查完成回调"""
success, data = result success, data = result
# 只在首次检查时隐藏加载对话框
if self._activation_status is None:
self.hide_loading_dialog() self.hide_loading_dialog()
if success and data: if success and data:
self.update_status_display(data) # 更新激活状态和定时器
# 更新缓存的激活状态
self._activation_status = data.get('status') == 'active' self._activation_status = data.get('status') == 'active'
# 设置状态更新定时器
if self._activation_status: if self._activation_status:
# 获取剩余时间(秒) # 设置定时器
days_left = data.get('days_left', 0)
seconds_left = days_left * 24 * 60 * 60
# 创建定时器在到期前1分钟更新状态
if self._status_timer: if self._status_timer:
self._status_timer.stop() self._status_timer.stop()
self._status_timer = QTimer(self) self._status_timer = QTimer(self)
self._status_timer.setSingleShot(True) self._status_timer.setSingleShot(False)
self._status_timer.timeout.connect(self.check_status) self._status_timer.timeout.connect(self.check_status)
update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次 self._status_timer.start(60 * 1000) # 每60秒检测一次
if update_interval > 0: logging.info("已设置每分钟检测会员状态")
self._status_timer.start(update_interval * 1000) # 转换为毫秒
return self._activation_status
else: else:
if self._status_timer:
self._status_timer.stop()
# 更新显示
self.update_status_display(data)
else:
# 停止定时器
if self._status_timer:
self._status_timer.stop()
self._activation_status = False
# 更新为未激活状态 # 更新为未激活状态
device_info = self.switcher.get_device_info()
inactive_status = { inactive_status = {
"hardware_id": self.switcher.hardware_id,
"expire_time": "",
"days_left": 0,
"total_days": 0,
"status": "inactive", "status": "inactive",
"activation_records": [] "expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": device_info
} }
self.update_status_display(inactive_status) self.update_status_display(inactive_status)
self._activation_status = False logging.warning("会员状态检查失败或未激活")
return False
# 清除会员信息文件
try:
member_file = self.switcher.config.member_file
if member_file.exists():
member_file.unlink()
logging.info("已清除会员信息文件")
except Exception as e:
logging.error(f"清除会员信息文件时出错: {str(e)}")
return self._activation_status
def check_activation_status(self) -> bool: def check_activation_status(self) -> bool:
"""检查是否已激活(使用缓存) """检查是否已激活(使用缓存)
@@ -668,11 +892,18 @@ class MainWindow(QMainWindow):
Returns: Returns:
bool: 是否已激活 bool: 是否已激活
""" """
# 如果缓存的状态是None从API获取
if self._activation_status is None: if self._activation_status is None:
# 首次检查从API获取
return self.check_status() return self.check_status()
# 如果未激活,显示购买信息
if not self._activation_status: if not self._activation_status:
self.show_purchase_info()
return self._activation_status
def show_purchase_info(self):
"""显示购买信息对话框"""
# 创建自定义消息框 # 创建自定义消息框
msg = QDialog(self) msg = QDialog(self)
msg.setWindowTitle("会员未激活") msg.setWindowTitle("会员未激活")
@@ -778,13 +1009,14 @@ class MainWindow(QMainWindow):
msg.setLayout(layout) msg.setLayout(layout)
msg.exec_() msg.exec_()
return self._activation_status
def refresh_cursor_auth(self): def refresh_cursor_auth(self):
"""刷新Cursor授权""" """刷新Cursor授权"""
if not self.check_activation_status(): if not self.check_activation_status():
return return
if not self._check_request_throttle():
return
try: try:
# 显示加载对话框 # 显示加载对话框
self.show_loading_dialog("正在刷新授权,请稍候...") self.show_loading_dialog("正在刷新授权,请稍候...")
@@ -795,6 +1027,7 @@ class MainWindow(QMainWindow):
self.worker.start() self.worker.start()
except Exception as e: except Exception as e:
self._request_complete()
self.hide_loading_dialog() self.hide_loading_dialog()
self.show_custom_error("刷新授权失败", str(e)) self.show_custom_error("刷新授权失败", str(e))
@@ -802,6 +1035,7 @@ class MainWindow(QMainWindow):
"""刷新授权完成回调""" """刷新授权完成回调"""
success, data = result success, data = result
self.hide_loading_dialog() self.hide_loading_dialog()
self._request_complete()
if isinstance(data, tuple): if isinstance(data, tuple):
success, message = data success, message = data
@@ -817,27 +1051,178 @@ class MainWindow(QMainWindow):
if not self.check_activation_status(): if not self.check_activation_status():
return return
if not self._check_request_throttle():
return
try: try:
success, message = self.switcher.disable_cursor_update() # 显示加载对话框
if success: self.show_loading_dialog("正在禁用更新,请稍候...")
self.show_custom_message("成功", "禁用更新成功", message, QStyle.SP_DialogApplyButton, "#198754")
else: # 创建工作线程
self.show_custom_error("禁用更新失败", message) from utils.cursor_registry import CursorRegistry
registry = CursorRegistry()
def disable_func():
try:
# 1. 先关闭所有Cursor进程
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
time.sleep(2)
# 2. 处理updater文件
updater_path = Path(os.getenv('LOCALAPPDATA')) / "cursor-updater"
try:
# 如果是目录,则删除
if updater_path.is_dir():
import shutil
shutil.rmtree(str(updater_path))
logging.info("删除updater目录成功")
# 如果是文件,则删除
if updater_path.is_file():
updater_path.unlink()
logging.info("删除updater文件成功")
# 创建阻止文件
updater_path.touch()
logging.info("创建updater空文件成功")
# 设置文件权限
import subprocess
import stat
# 设置只读属性
os.chmod(str(updater_path), stat.S_IREAD)
logging.info("设置只读属性成功")
# 使用icacls设置权限只读
username = os.getenv('USERNAME')
cmd = f'icacls "{str(updater_path)}" /inheritance:r /grant:r "{username}:(R)"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
logging.error(f"设置文件权限失败: {result.stderr}")
return False, "设置文件权限失败"
logging.info("设置文件权限成功")
# 验证设置
if not os.path.exists(updater_path):
return False, "文件创建失败"
if os.access(str(updater_path), os.W_OK):
return False, "文件权限设置失败"
except Exception as e: except Exception as e:
logging.error(f"处理updater文件失败: {str(e)}")
return False, "处理updater文件失败"
# 3. 修改package.json配置
if not registry.fix_cursor_startup():
return False, "修改配置失败"
# 4. 重启Cursor
cursor_exe = registry.cursor_path / "Cursor.exe"
if cursor_exe.exists():
os.startfile(str(cursor_exe))
logging.info("Cursor重启成功")
return True, "Cursor更新已禁用程序已重启"
else:
return False, "未找到Cursor程序"
except Exception as e:
logging.error(f"禁用更新时发生错误: {str(e)}")
return False, str(e)
self.worker = ApiWorker(disable_func)
self.worker.finished.connect(lambda result: self.on_disable_update_complete(result))
self.worker.start()
except Exception as e:
self._request_complete()
self.hide_loading_dialog()
self.show_custom_error("禁用更新失败", str(e)) self.show_custom_error("禁用更新失败", str(e))
def dummy_function(self): def on_disable_update_complete(self, result):
"""突破版本限制""" """禁用更新完成回调"""
success, data = result
self.hide_loading_dialog()
self._request_complete()
if success:
self.show_custom_message(
"成功",
"禁用更新成功",
data,
QStyle.SP_DialogApplyButton,
"#198754"
)
else:
self.show_custom_error("禁用更新失败", str(data))
def bypass_cursor_limit(self):
"""突破Cursor版本限制"""
if not self.check_activation_status(): if not self.check_activation_status():
return return
if not self._check_request_throttle():
return
try:
# 显示加载对话框
self.show_loading_dialog("正在突破版本限制,请稍候...")
# 创建工作线程
from utils.cursor_registry import CursorRegistry
registry = CursorRegistry()
def reset_func():
try:
# 1. 先关闭所有Cursor进程
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
time.sleep(2)
# 2. 清理注册表
if not registry.clean_registry():
return False, "清理注册表失败"
# 3. 清理文件
if not registry.clean_cursor_files():
return False, "清理文件失败"
# 4. 重启Cursor
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
os.startfile(str(cursor_exe))
logging.info("Cursor重启成功")
return True, "突破限制成功"
else:
return False, "未找到Cursor程序"
except Exception as e:
logging.error(f"突破限制时发生错误: {str(e)}")
return False, str(e)
self.worker = ApiWorker(reset_func)
self.worker.finished.connect(self.on_bypass_complete)
self.worker.start()
except Exception as e:
self._request_complete()
self.hide_loading_dialog()
self.show_custom_error("突破限制失败", str(e))
def on_bypass_complete(self, result):
"""突破限制完成回调"""
success, data = result
self.hide_loading_dialog()
self._request_complete()
if success:
self.show_custom_message( self.show_custom_message(
"提示", "成功",
"能未实现", "突破限制成",
"此功能暂未实现,请等待后续更新", "Cursor版本限制已突破编辑器已重启",
QStyle.SP_MessageBoxInformation, QStyle.SP_DialogApplyButton,
"#0d6efd" "#198754"
) )
else:
self.show_custom_error("突破限制失败", str(data))
def show_custom_message(self, title, header, message, icon_type, color): def show_custom_message(self, title, header, message, icon_type, color):
"""显示自定义消息框""" """显示自定义消息框"""
@@ -915,3 +1300,28 @@ class MainWindow(QMainWindow):
QStyle.SP_MessageBoxCritical, QStyle.SP_MessageBoxCritical,
"#dc3545" "#dc3545"
) )
def _check_request_throttle(self) -> bool:
"""检查是否可以发送请求(防重复提交)
Returns:
bool: 是否可以发送请求
"""
current_time = time.time()
# 如果正在请求中返回False
if self._is_requesting:
return False
# 如果距离上次请求时间小于冷却时间返回False
if current_time - self._last_request_time < self._request_cooldown:
return False
# 更新状态和时间
self._is_requesting = True
self._last_request_time = current_time
return True
def _request_complete(self):
"""请求完成,重置状态"""
self._is_requesting = False

26
main.py
View File

@@ -5,12 +5,17 @@ import os
import atexit import atexit
import shutil import shutil
import tempfile import tempfile
import urllib3
from pathlib import Path from pathlib import Path
from PyQt5.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon, QMenu from PyQt5.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon, QMenu
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from gui.main_window import MainWindow from gui.main_window import MainWindow
# 禁用所有 SSL 相关警告
urllib3.disable_warnings()
logging.getLogger('urllib3').setLevel(logging.ERROR)
def cleanup_temp(): def cleanup_temp():
"""清理临时文件""" """清理临时文件"""
try: try:
@@ -52,6 +57,18 @@ def main():
# 注册退出时的清理函数 # 注册退出时的清理函数
atexit.register(cleanup_temp) atexit.register(cleanup_temp)
# 创建QApplication实例
app = QApplication(sys.argv)
# 检查系统托盘是否可用
if not QSystemTrayIcon.isSystemTrayAvailable():
logging.error("系统托盘不可用")
QMessageBox.critical(None, "错误", "系统托盘不可用,程序无法正常运行。")
return 1
# 设置应用程序不会在最后一个窗口关闭时退出
app.setQuitOnLastWindowClosed(False)
setup_logging() setup_logging()
# 检查Python版本 # 检查Python版本
@@ -66,7 +83,6 @@ def main():
logging.info(f" - {p}") logging.info(f" - {p}")
logging.info("正在初始化主窗口...") logging.info("正在初始化主窗口...")
app = QApplication(sys.argv)
# 设置应用程序ID (在设置图标之前) # 设置应用程序ID (在设置图标之前)
if sys.platform == "win32": if sys.platform == "win32":
@@ -96,15 +112,17 @@ def main():
logging.info("正在启动主窗口...") logging.info("正在启动主窗口...")
window.show() window.show()
sys.exit(app.exec_())
return app.exec_()
except Exception as e: except Exception as e:
error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}" error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg) logging.error(error_msg)
# 使用 QMessageBox 显示错误 # 使用 QMessageBox 显示错误
if QApplication.instance() is None:
app = QApplication(sys.argv) app = QApplication(sys.argv)
QMessageBox.critical(None, "错误", error_msg) QMessageBox.critical(None, "错误", error_msg)
sys.exit(1) return 1
if __name__ == "__main__": if __name__ == "__main__":
main() sys.exit(main())

View File

@@ -2,8 +2,9 @@
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
requests==2.31.0 requests==2.31.0
setuptools>=68.0.0
altgraph>=0.17.4
pyinstaller==6.3.0 pyinstaller==6.3.0
pillow==10.2.0 # For icon processing pillow==10.2.0
setuptools==65.5.1 # Fix pkg_resources.extern issue PyQt5==5.15.10
PyQt5==5.15.10 # GUI framework pywin32==306
pywin32==306 # Windows API support

51
testbuild.bat Normal file
View File

@@ -0,0 +1,51 @@
@echo off
chcp 65001 >nul
setlocal EnableDelayedExpansion
REM 激活虚拟环境
call venv\Scripts\activate.bat
REM 确保安装了必要的包
pip install -r requirements.txt
REM 读取当前版本号
set /p VERSION=<version.txt
echo 当前正式版本: %VERSION%
REM 读取测试版本号(如果存在)
if exist testversion.txt (
set /p TEST_VERSION=<testversion.txt
) else (
set TEST_VERSION=0
)
REM 增加测试版本号
set /a TEST_VERSION+=1
echo !TEST_VERSION!>testversion.txt
echo 测试版本号: !TEST_VERSION!
REM 组合完整版本号
set FULL_VERSION=%VERSION%.!TEST_VERSION!
echo 完整版本号: !FULL_VERSION!
REM 创建测试版本输出目录
if not exist "dist\test" mkdir "dist\test"
REM 清理旧文件
if exist "dist\听泉cursor助手%VERSION%.exe" del "dist\听泉cursor助手%VERSION%.exe"
if exist "build" rmdir /s /q "build"
REM 执行打包
venv\Scripts\python.exe -m PyInstaller build_nezha.spec --clean
REM 移动并重命名文件
move "dist\听泉cursor助手%VERSION%.exe" "dist\test\听泉cursor助手v!FULL_VERSION!.exe"
echo.
echo 测试版本构建完成!
echo 版本号: v!FULL_VERSION!
echo 文件位置: dist\test\听泉cursor助手v!FULL_VERSION!.exe
REM 退出虚拟环境
deactivate
pause

View File

@@ -4,8 +4,15 @@ import logging
from pathlib import Path from pathlib import Path
class Config: class Config:
"""配置类"""
def __init__(self): def __init__(self):
self.api_base_url = "https://cursorapi.nosqli.com/admin" self.base_url = "https://cursorapi.nosqli.com"
self.api_endpoints = {
"activate": f"{self.base_url}/admin/api.member/activate",
"status": f"{self.base_url}/admin/api.member/status",
"get_unused": f"{self.base_url}/admin/api.account/getUnused"
}
self.config_dir = Path(os.path.expanduser("~")) / ".cursor_switcher" self.config_dir = Path(os.path.expanduser("~")) / ".cursor_switcher"
self.config_file = self.config_dir / "config.json" self.config_file = self.config_dir / "config.json"
self.member_file = self.config_dir / "member.json" self.member_file = self.config_dir / "member.json"
@@ -65,3 +72,14 @@ class Config:
with open(self.config_file, "w", encoding="utf-8") as f: with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False) json.dump(config, f, indent=2, ensure_ascii=False)
self.api_token = api_token self.api_token = api_token
def get_api_url(self, endpoint_name: str) -> str:
"""获取API端点URL
Args:
endpoint_name: 端点名称
Returns:
str: 完整的API URL
"""
return self.api_endpoints.get(endpoint_name, "")

View File

@@ -1 +1 @@
3.3.4 3.4.0