v3.3.9版本更新: 1.优化UI界面和按钮样式 2.改进使用说明文字,增加颜色标注 3.优化加载对话框样式 4.添加请求节流机制,防止重复提交 5.完善错误处理和提示信息 6.优化会员状态检查逻辑 7.改进禁用更新功能的实现

This commit is contained in:
huangzhenpc
2025-02-12 21:15:58 +08:00
parent 4e11deb530
commit 207c3c604e
7 changed files with 681 additions and 467 deletions

View File

@@ -50,9 +50,41 @@ class AccountSwitcher:
self.package_json = self.app_path / "package.json"
self.auth_manager = CursorAuthManager()
self.config = Config()
self.hardware_id = get_hardware_id()
self.hardware_id = self.get_hardware_id() # 先获取硬件ID
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:
"""获取Cursor版本号"""
try:
@@ -71,199 +103,103 @@ class AccountSwitcher:
import platform
import socket
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()
# 获取地理位置(可选)
# 获取IP地址
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', '')}"
except:
location = ""
location = "未知"
return {
"os": os_info,
"device_name": device_name,
"ip": ip_address,
"location": location
}
except Exception as e:
logging.error(f"获取设备信息失败: {str(e)}")
return {
"os": "Windows 10",
"os": "Windows",
"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:
Tuple[bool, str, Optional[Dict]]: (是否成功, 提示消息, 账号信息)
tuple: (成功标志, 消息, 账号信息)
"""
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 = {
"code": single_code,
"machine_id": self.hardware_id,
"os": device_info["os"],
"device_name": device_info["device_name"],
"location": device_info["location"]
"code": code
}
headers = {
"Content-Type": "application/json"
}
try:
response = requests.post(
endpoint,
self.config.get_api_url("activate"),
json=data,
headers=headers,
headers={"Content-Type": "application/json"},
timeout=10
)
response_data = response.json()
if response_data.get("code") == 200:
result_data = response_data.get("data", {})
logging.info(f"激活码 {single_code} 验证成功: {response_data.get('msg', '')}")
activation_results.append(result_data)
success_codes.append(single_code)
elif response_data.get("code") == 400:
error_msg = response_data.get("msg", "参数错误")
if "已被使用" in error_msg or "已激活" in error_msg:
logging.warning(f"激活码 {single_code} 已被使用")
failed_codes.append(f"{single_code} (已被使用)")
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 # 使用合并后的历史记录
result = response.json()
if result["code"] == 200:
api_data = result["data"]
# 构造标准的返回数据结构
account_info = {
"status": "active",
"expire_time": api_data.get("expire_time", ""),
"total_days": api_data.get("total_days", 0),
"days_left": api_data.get("days_left", 0),
"device_info": self.get_device_info()
}
return True, result["msg"], account_info
return False, result["msg"], {
"status": "inactive",
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
}
self.config.save_member_info(member_info)
# 生成结果消息
message = f"激活成功\n"
# 显示每个成功激活码的信息
for i, result in enumerate(activation_results, 1):
message += f"\n{i}个激活码:\n"
message += f"- 新增天数: {result.get('added_days', 0)}\n"
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:
logging.error(f"处理激活结果时出错: {str(e)}")
return False, f"处理激活结果时出错: {str(e)}", None
except Exception as e:
logging.error(f"验证激活码时出错: {str(e)}")
return False, f"验证激活码时出错: {str(e)}", None
return False, f"激活失败: {str(e)}", {
"status": "inactive",
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
}
def reset_machine_id(self) -> bool:
"""重置机器码"""
@@ -356,92 +292,75 @@ class AccountSwitcher:
logging.error(f"激活过程出错: {str(e)}")
return False, f"激活失败: {str(e)}"
def get_member_status(self) -> Optional[Dict]:
def get_member_status(self) -> dict:
"""获取会员状态
Returns:
Optional[Dict]: 会员状态信息
dict: 会员状态信息,包含:
- status: 状态(active/inactive/expired)
- expire_time: 到期时间
- total_days: 总天数
- days_left: 剩余天数
- device_info: 设备信息
"""
try:
# 读取保存的会员信息
member_info = self.config.load_member_info()
# 构造状态检查请求
endpoint = "https://cursorapi.nosqli.com/admin/api.member/status"
data = {
"machine_id": self.hardware_id
}
headers = {
"Content-Type": "application/json"
}
try:
response = requests.post(endpoint, json=data, headers=headers, timeout=10)
response_data = response.json()
api_url = self.config.get_api_url("status")
logging.info(f"正在检查会员状态...")
logging.info(f"API URL: {api_url}")
logging.info(f"请求数据: {data}")
if response_data.get("code") == 200:
# 正常状态
data = response_data.get("data", {})
status = data.get("status", "inactive")
response = requests.post(
api_url,
json=data,
headers={"Content-Type": "application/json"},
timeout=10
)
# 构造会员信息
member_info = {
"hardware_id": data.get("machine_id", self.hardware_id),
"expire_time": data.get("expire_time", ""),
"days_left": data.get("days_left", 0), # 使用days_left
"total_days": data.get("total_days", 0), # 使用total_days
"status": status,
"activation_records": data.get("activation_records", []) # 保存激活记录
}
result = response.json()
logging.info(f"状态检查响应: {result}")
# 打印调试信息
logging.info(f"API返回数据: {data}")
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:
"""获取未激活状态的默认信息"""
if result.get("code") in [1, 200]: # 兼容两种响应码
api_data = result.get("data", {})
# 构造标准的返回数据结构
return {
"status": api_data.get("status", "inactive"),
"expire_time": api_data.get("expire_time", ""),
"total_days": api_data.get("total_days", 0),
"days_left": api_data.get("days_left", 0),
"device_info": self.get_device_info() # 添加设备信息
}
else:
error_msg = result.get("msg", "未知错误")
logging.error(f"获取状态失败: {error_msg}")
return {
"hardware_id": self.hardware_id,
"expire_time": "",
"days": 0,
"total_days": 0,
"status": "inactive",
"last_activation": {},
"activation_records": []
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": self.get_device_info()
}
except requests.exceptions.RequestException as e:
logging.error(f"API请求失败: {str(e)}")
return {
"status": "inactive",
"expire_time": "",
"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:

View File

@@ -1,5 +1,7 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
from PyInstaller.utils.hooks import collect_all
def get_version():
with open('version.txt', 'r', encoding='utf-8') as f:
@@ -8,26 +10,43 @@ def 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(
['main.py'],
pathex=[],
binaries=[],
datas=[('icon', 'icon'), ('version.txt', '.')],
hiddenimports=[
'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
'requests', 'urllib3', 'certifi', # 网络请求相关
'json', 'uuid', 'hashlib', 'logging' # 基础功能相关
],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['_tkinter', 'tkinter', 'Tkinter'], # 排除 tkinter 相关模块
excludes=['_tkinter', 'tkinter', 'Tkinter'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
noarchive=False,
optimize=0,
module_collection_mode={'PyQt5': 'pyz+py'},
)
# 创建PYZ
pyz = PYZ(a.pure)
# 创建EXE
exe = EXE(
pyz,
a.scripts,

View File

@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QDialog, QProgressBar, QStyle)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap
import time
sys.path.append(str(Path(__file__).parent.parent))
@@ -53,8 +54,9 @@ class LoadingDialog(QDialog):
background-color: #f8f9fa;
}
QLabel {
color: #333333;
color: #0d6efd;
font-size: 14px;
font-weight: bold;
padding: 10px;
}
QProgressBar {
@@ -96,6 +98,11 @@ class MainWindow(QMainWindow):
self._activation_status = None # 缓存的激活状态
self._status_timer = None # 状态更新定时器
# 添加请求锁,防止重复提交
self._is_requesting = False
self._last_request_time = 0
self._request_cooldown = 2 # 请求冷却时间(秒)
version = get_version()
cursor_version = self.switcher.get_cursor_version()
self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})")
@@ -108,6 +115,8 @@ class MainWindow(QMainWindow):
if not window_icon.isNull():
self.setWindowIcon(window_icon)
logging.info(f"成功设置窗口图标: {icon_path}")
else:
logging.warning("图标文件加载失败")
# 创建系统托盘图标
self.tray_icon = QSystemTrayIcon(self)
@@ -163,21 +172,6 @@ class MainWindow(QMainWindow):
activation_frame = QFrame()
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_frame = QFrame()
@@ -186,11 +180,54 @@ class MainWindow(QMainWindow):
self.activation_edit = QLineEdit()
input_layout.addWidget(self.activation_edit)
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)
input_layout.addWidget(activate_btn)
activation_layout.addWidget(input_frame)
main_layout.addWidget(activation_frame)
# 使用说明
usage_label = QLabel()
usage_text = (
"<p style='margin-bottom: 10px;'><b style='color: #0d6efd;'>使用步骤:</b></p>"
"<p style='line-height: 1.5;'>"
"1. <span style='color: #0d6efd;'>第一步:</span>输入激活码并点击<span style='color: #0d6efd;'>【激活】</span>按钮<br>"
"2. <span style='color: #198754;'>第二步:</span>激活成功后点击<span style='color: #198754;'>【刷新Cursor编辑器授权】</span>即可正常使用<br>"
"3. <span style='color: #dc3545;'>如果刷新无效:</span>请先点击<span style='color: #198754;'>【突破Cursor0.45.x限制】</span>,然后再点击刷新<br>"
"4. <span style='color: #6c757d;'>建议操作:</span>点击<span style='color: #dc3545;'>【禁用Cursor版本更新】</span>保持长期稳定"
"</p>"
)
usage_label.setText(usage_text)
usage_label.setStyleSheet("""
QLabel {
color: #333333;
font-size: 13px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
""")
usage_label.setTextFormat(Qt.RichText)
usage_label.setWordWrap(True)
activation_layout.addWidget(usage_label)
# 操作按钮区域
btn_frame = QFrame()
btn_layout = QVBoxLayout(btn_frame)
@@ -203,8 +240,8 @@ class MainWindow(QMainWindow):
border: none;
padding: 15px;
border-radius: 6px;
font-size: 14px;
min-width: 200px;
font-size: 13px;
min-width: 300px;
margin: 5px;
}
QPushButton:hover {
@@ -216,21 +253,21 @@ class MainWindow(QMainWindow):
"""
# 刷新授权按钮
refresh_btn = QPushButton("刷新Cursor编辑器授权")
refresh_btn = QPushButton("刷新 Cursor 编辑器授权")
refresh_btn.setStyleSheet(button_style)
refresh_btn.clicked.connect(self.refresh_cursor_auth)
refresh_btn.setMinimumHeight(50)
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.clicked.connect(self.dummy_function)
bypass_btn.clicked.connect(self.bypass_cursor_limit)
bypass_btn.setMinimumHeight(50)
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.clicked.connect(self.disable_cursor_update)
disable_update_btn.setMinimumHeight(50)
@@ -271,14 +308,33 @@ class MainWindow(QMainWindow):
def copy_device_id(self):
"""复制设备ID到剪贴板"""
if not self.check_status():
return
QApplication.clipboard().setText(self.hardware_id_edit.text())
QMessageBox.information(self, "提示", "设备ID已复制到剪贴板")
def show_loading_dialog(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;
}
""")
self.loading_dialog.show()
def hide_loading_dialog(self):
@@ -460,19 +516,17 @@ class MainWindow(QMainWindow):
# 更新会员信息显示
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)
seconds_left = days_left * 24 * 60 * 60
if self._activation_status:
if self._status_timer:
self._status_timer.stop()
self._status_timer = QTimer(self)
self._status_timer.setSingleShot(True)
self._status_timer.setSingleShot(False)
self._status_timer.timeout.connect(self.check_status)
update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次
if update_interval > 0:
self._status_timer.start(update_interval * 1000) # 转换为毫秒
self._status_timer.start(60 * 1000) # 每60秒检测一次
logging.info("已设置每分钟检测会员状态")
msg = QMessageBox(self)
msg.setWindowTitle("激活成功")
@@ -549,21 +603,26 @@ class MainWindow(QMainWindow):
}
""")
msg.exec_()
# 更新显示为未激活状态
self.update_status_display(account_info)
else:
QMessageBox.critical(self, "错误", f"激活失败: {data}")
# 激活后检查一次状态
self.check_status()
# 更新为未激活状态
device_info = self.switcher.get_device_info()
inactive_status = {
"status": "inactive",
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": device_info
}
self.update_status_display(inactive_status)
def update_status_display(self, status_info: dict):
"""更新状态显示"""
# 打印API返回的原始数据
logging.info("=== API返回数据 ===")
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 = {
@@ -581,18 +640,10 @@ class MainWindow(QMainWindow):
f"剩余天数:{status_info.get('days_left', 0)}"
]
# 如果有激活记录,显示最近一次激活信息
activation_records = status_info.get('activation_records', [])
if activation_records:
latest_record = activation_records[-1] # 获取最新的激活记录
device_info = latest_record.get('device_info', {})
# 添加设备信息
device_info = status_info.get('device_info', {})
if device_info:
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', '')}",
@@ -607,7 +658,8 @@ class MainWindow(QMainWindow):
def check_status(self):
"""检查会员状态从API获取"""
try:
# 显示加载对话框
# 只在首次检查时显示加载对话框
if self._activation_status is None:
self.show_loading_dialog("正在检查会员状态,请稍候...")
# 创建工作线程
@@ -616,51 +668,67 @@ class MainWindow(QMainWindow):
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
def on_status_check_complete(self, result):
"""状态检查完成回调"""
success, data = result
# 只在首次检查时隐藏加载对话框
if self._activation_status is None:
self.hide_loading_dialog()
if success and data:
self.update_status_display(data)
# 更新缓存的激活状态
# 更新激活状态和定时器
self._activation_status = data.get('status') == 'active'
# 设置状态更新定时器
if self._activation_status:
# 获取剩余时间(秒)
days_left = data.get('days_left', 0)
seconds_left = days_left * 24 * 60 * 60
# 创建定时器在到期前1分钟更新状态
# 设置定时器
if self._status_timer:
self._status_timer.stop()
self._status_timer = QTimer(self)
self._status_timer.setSingleShot(True)
self._status_timer.setSingleShot(False)
self._status_timer.timeout.connect(self.check_status)
update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次
if update_interval > 0:
self._status_timer.start(update_interval * 1000) # 转换为毫秒
return self._activation_status
self._status_timer.start(60 * 1000) # 每60秒检测一次
logging.info("已设置每分钟检测会员状态")
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 = {
"hardware_id": self.switcher.hardware_id,
"expire_time": "",
"days_left": 0,
"total_days": 0,
"status": "inactive",
"activation_records": []
"expire_time": "",
"total_days": 0,
"days_left": 0,
"device_info": device_info
}
self.update_status_display(inactive_status)
self._activation_status = False
return False
logging.warning("会员状态检查失败或未激活")
# 清除会员信息文件
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:
"""检查是否已激活(使用缓存)
@@ -668,11 +736,18 @@ class MainWindow(QMainWindow):
Returns:
bool: 是否已激活
"""
# 如果缓存的状态是None从API获取
if self._activation_status is None:
# 首次检查从API获取
return self.check_status()
# 如果未激活,显示购买信息
if not self._activation_status:
self.show_purchase_info()
return self._activation_status
def show_purchase_info(self):
"""显示购买信息对话框"""
# 创建自定义消息框
msg = QDialog(self)
msg.setWindowTitle("会员未激活")
@@ -778,13 +853,14 @@ class MainWindow(QMainWindow):
msg.setLayout(layout)
msg.exec_()
return self._activation_status
def refresh_cursor_auth(self):
"""刷新Cursor授权"""
if not self.check_activation_status():
return
if not self._check_request_throttle():
return
try:
# 显示加载对话框
self.show_loading_dialog("正在刷新授权,请稍候...")
@@ -795,6 +871,7 @@ class MainWindow(QMainWindow):
self.worker.start()
except Exception as e:
self._request_complete()
self.hide_loading_dialog()
self.show_custom_error("刷新授权失败", str(e))
@@ -802,6 +879,7 @@ class MainWindow(QMainWindow):
"""刷新授权完成回调"""
success, data = result
self.hide_loading_dialog()
self._request_complete()
if isinstance(data, tuple):
success, message = data
@@ -817,27 +895,178 @@ class MainWindow(QMainWindow):
if not self.check_activation_status():
return
if not self._check_request_throttle():
return
try:
success, message = self.switcher.disable_cursor_update()
if success:
self.show_custom_message("成功", "禁用更新成功", message, QStyle.SP_DialogApplyButton, "#198754")
else:
self.show_custom_error("禁用更新失败", message)
# 显示加载对话框
self.show_loading_dialog("正在禁用更新,请稍候...")
# 创建工作线程
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:
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))
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():
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(
"提示",
"能未实现",
"此功能暂未实现,请等待后续更新",
QStyle.SP_MessageBoxInformation,
"#0d6efd"
"成功",
"突破限制成",
"Cursor版本限制已突破编辑器已重启",
QStyle.SP_DialogApplyButton,
"#198754"
)
else:
self.show_custom_error("突破限制失败", str(data))
def show_custom_message(self, title, header, message, icon_type, color):
"""显示自定义消息框"""
@@ -915,3 +1144,28 @@ class MainWindow(QMainWindow):
QStyle.SP_MessageBoxCritical,
"#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

View File

@@ -52,6 +52,9 @@ def main():
# 注册退出时的清理函数
atexit.register(cleanup_temp)
# 创建QApplication实例
app = QApplication(sys.argv)
setup_logging()
# 检查Python版本
@@ -66,7 +69,6 @@ def main():
logging.info(f" - {p}")
logging.info("正在初始化主窗口...")
app = QApplication(sys.argv)
# 设置应用程序ID (在设置图标之前)
if sys.platform == "win32":
@@ -102,6 +104,7 @@ def main():
error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
# 使用 QMessageBox 显示错误
if QApplication.instance() is None:
app = QApplication(sys.argv)
QMessageBox.critical(None, "错误", error_msg)
sys.exit(1)

View File

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

View File

@@ -4,8 +4,15 @@ import logging
from pathlib import Path
class Config:
"""配置类"""
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_file = self.config_dir / "config.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:
json.dump(config, f, indent=2, ensure_ascii=False)
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.3.9