From 207c3c604e5598c3e13026fd658b6055a4d1d30a Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Wed, 12 Feb 2025 21:15:58 +0800 Subject: [PATCH] =?UTF-8?q?v3.3.9=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0:=201?= =?UTF-8?q?.=E4=BC=98=E5=8C=96UI=E7=95=8C=E9=9D=A2=E5=92=8C=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=A0=B7=E5=BC=8F=202.=E6=94=B9=E8=BF=9B=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=AF=B4=E6=98=8E=E6=96=87=E5=AD=97=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=A2=9C=E8=89=B2=E6=A0=87=E6=B3=A8=203.=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8A=A0=E8=BD=BD=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=204.=E6=B7=BB=E5=8A=A0=E8=AF=B7=E6=B1=82=E8=8A=82?= =?UTF-8?q?=E6=B5=81=E6=9C=BA=E5=88=B6=EF=BC=8C=E9=98=B2=E6=AD=A2=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=8F=90=E4=BA=A4=205.=E5=AE=8C=E5=96=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E6=8F=90=E7=A4=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=206.=E4=BC=98=E5=8C=96=E4=BC=9A=E5=91=98=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=207.=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E7=A6=81=E7=94=A8=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account_switcher.py | 409 +++++++++++---------------- build_nezha.spec | 39 ++- gui/main_window.py | 660 ++++++++++++++++++++++++++++++-------------- main.py | 7 +- requirements.txt | 9 +- utils/config.py | 22 +- version.txt | 2 +- 7 files changed, 681 insertions(+), 467 deletions(-) diff --git a/account_switcher.py b/account_switcher.py index ed2784e..49b2f14 100644 --- a/account_switcher.py +++ b/account_switcher.py @@ -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()}" # 获取设备名称 - device_name = platform.node() - - # 获取地理位置(可选) try: - ip_info = requests.get('https://ipapi.co/json/', timeout=5).json() + # 在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_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 [] + data = { + "machine_id": self.hardware_id, + "code": code + } - # 分割多个激活码 - 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"] + response = requests.post( + self.config.get_api_url("activate"), + json=data, + headers={"Content-Type": "application/json"}, + timeout=10 + ) + + 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() } - - headers = { - "Content-Type": "application/json" - } - - try: - response = requests.post( - endpoint, - json=data, - headers=headers, - 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 # 使用合并后的历史记录 - } - 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 - + 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() + } + 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,93 +292,76 @@ 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" + + api_url = self.config.get_api_url("status") + logging.info(f"正在检查会员状态...") + logging.info(f"API URL: {api_url}") + logging.info(f"请求数据: {data}") + + response = requests.post( + api_url, + json=data, + headers={"Content-Type": "application/json"}, + timeout=10 + ) + + result = response.json() + logging.info(f"状态检查响应: {result}") + + 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 { + "status": "inactive", + "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() } - - try: - response = requests.post(endpoint, json=data, headers=headers, timeout=10) - response_data = response.json() - - if response_data.get("code") == 200: - # 正常状态 - data = response_data.get("data", {}) - status = data.get("status", "inactive") - - # 构造会员信息 - 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", []) # 保存激活记录 - } - - # 打印调试信息 - 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: - """获取未激活状态的默认信息""" - return { - "hardware_id": self.hardware_id, - "expire_time": "", - "days": 0, - "total_days": 0, - "status": "inactive", - "last_activation": {}, - "activation_records": [] - } + 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: """重启Cursor编辑器 diff --git a/build_nezha.spec b/build_nezha.spec index d7b6f66..02d02d6 100644 --- a/build_nezha.spec +++ b/build_nezha.spec @@ -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, diff --git a/gui/main_window.py b/gui/main_window.py index 52cefb6..737e7e6 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -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 = ( + "

使用步骤:

" + "

" + "1. 第一步:输入激活码并点击【激活】按钮
" + "2. 第二步:激活成功后点击【刷新Cursor编辑器授权】即可正常使用
" + "3. 如果刷新无效:请先点击【突破Cursor0.45.x限制】,然后再点击刷新
" + "4. 建议操作:点击【禁用Cursor版本更新】保持长期稳定" + "

" + ) + 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): @@ -286,7 +342,7 @@ class MainWindow(QMainWindow): if hasattr(self, 'loading_dialog'): self.loading_dialog.hide() self.loading_dialog.deleteLater() - + def activate_account(self): """激活账号""" code = self.activation_edit.text().strip() @@ -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._status_timer: - self._status_timer.stop() - self._status_timer = QTimer(self) - self._status_timer.setSingleShot(True) - 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) # 转换为毫秒 + if self._activation_status: + if self._status_timer: + self._status_timer.stop() + self._status_timer = QTimer(self) + self._status_timer.setSingleShot(False) + self._status_timer.timeout.connect(self.check_status) + 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}") + # 更新为未激活状态 + 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) - # 激活后检查一次状态 - self.check_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', '')}", @@ -603,12 +654,13 @@ class MainWindow(QMainWindow): # 更新状态文本 self.status_text.setPlainText("\n".join(status_lines)) - + def check_status(self): """检查会员状态(从API获取)""" try: - # 显示加载对话框 - self.show_loading_dialog("正在检查会员状态,请稍候...") + # 只在首次检查时显示加载对话框 + if self._activation_status is None: + self.show_loading_dialog("正在检查会员状态,请稍候...") # 创建工作线程 self.worker = ApiWorker(self.switcher.get_member_status) @@ -616,175 +668,199 @@ class MainWindow(QMainWindow): self.worker.start() except Exception as e: - self.hide_loading_dialog() + 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 - self.hide_loading_dialog() + + # 只在首次检查时隐藏加载对话框 + 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) # 转换为毫秒 + self._status_timer.start(60 * 1000) # 每60秒检测一次 + logging.info("已设置每分钟检测会员状态") + else: + if self._status_timer: + self._status_timer.stop() - return self._activation_status + # 更新显示 + 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: """检查是否已激活(使用缓存) Returns: bool: 是否已激活 """ + # 如果缓存的状态是None,从API获取 if self._activation_status is None: - # 首次检查,从API获取 return self.check_status() + # 如果未激活,显示购买信息 if not self._activation_status: - # 创建自定义消息框 - msg = QDialog(self) - msg.setWindowTitle("会员未激活") - msg.setFixedWidth(400) - msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint) - - # 创建布局 - layout = QVBoxLayout() - - # 添加图标和文本 - icon_label = QLabel() - icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(32, 32)) - icon_label.setAlignment(Qt.AlignCenter) - layout.addWidget(icon_label) - - text_label = QLabel("您还未激活会员或会员已过期") - text_label.setAlignment(Qt.AlignCenter) - text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #333333; padding: 10px;") - layout.addWidget(text_label) - - # 添加购买信息 - info_text = "获取会员激活码,请通过以下方式:\n\n" \ - "• 官方自助网站:cursor.nosqli.com\n" \ - "• 微信客服:behikcigar\n" \ - "• 闲鱼店铺:xxx\n\n" \ - "————————————————————\n" \ - "诚招代理商,欢迎加盟合作!" - - info_label = QLabel(info_text) - info_label.setAlignment(Qt.AlignLeft) - info_label.setStyleSheet(""" - QLabel { - color: #333333; - font-size: 14px; - padding: 15px; - background-color: #f8f9fa; - border-radius: 4px; - border: 1px solid #dee2e6; - margin: 10px; - } - """) - layout.addWidget(info_label) - - # 添加复制按钮区域 - btn_layout = QHBoxLayout() - - # 复制网站按钮 - copy_web_btn = QPushButton("复制网站") - copy_web_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "cursor.nosqli.com", "网站地址已复制到剪贴板")) - copy_web_btn.setStyleSheet(""" - QPushButton { - background-color: #0d6efd; - color: white; - border: none; - padding: 8px 15px; - border-radius: 4px; - font-size: 13px; - } - QPushButton:hover { - background-color: #0b5ed7; - } - """) - btn_layout.addWidget(copy_web_btn) - - # 复制微信按钮 - copy_wx_btn = QPushButton("复制微信") - copy_wx_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "behikcigar", "微信号已复制到剪贴板")) - copy_wx_btn.setStyleSheet(""" - QPushButton { - background-color: #198754; - color: white; - border: none; - padding: 8px 15px; - border-radius: 4px; - font-size: 13px; - } - QPushButton:hover { - background-color: #157347; - } - """) - btn_layout.addWidget(copy_wx_btn) - - # 确定按钮 - ok_btn = QPushButton("确定") - ok_btn.clicked.connect(msg.accept) - ok_btn.setStyleSheet(""" - QPushButton { - background-color: #0d6efd; - color: white; - border: none; - padding: 8px 25px; - border-radius: 4px; - font-size: 13px; - min-width: 100px; - } - QPushButton:hover { - background-color: #0b5ed7; - } - """) - btn_layout.addWidget(ok_btn) - - layout.addLayout(btn_layout) - msg.setLayout(layout) - msg.exec_() + self.show_purchase_info() return self._activation_status - + + def show_purchase_info(self): + """显示购买信息对话框""" + # 创建自定义消息框 + msg = QDialog(self) + msg.setWindowTitle("会员未激活") + msg.setFixedWidth(400) + msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + # 创建布局 + layout = QVBoxLayout() + + # 添加图标和文本 + icon_label = QLabel() + icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(32, 32)) + icon_label.setAlignment(Qt.AlignCenter) + layout.addWidget(icon_label) + + text_label = QLabel("您还未激活会员或会员已过期") + text_label.setAlignment(Qt.AlignCenter) + text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #333333; padding: 10px;") + layout.addWidget(text_label) + + # 添加购买信息 + info_text = "获取会员激活码,请通过以下方式:\n\n" \ + "• 官方自助网站:cursor.nosqli.com\n" \ + "• 微信客服:behikcigar\n" \ + "• 闲鱼店铺:xxx\n\n" \ + "————————————————————\n" \ + "诚招代理商,欢迎加盟合作!" + + info_label = QLabel(info_text) + info_label.setAlignment(Qt.AlignLeft) + info_label.setStyleSheet(""" + QLabel { + color: #333333; + font-size: 14px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 4px; + border: 1px solid #dee2e6; + margin: 10px; + } + """) + layout.addWidget(info_label) + + # 添加复制按钮区域 + btn_layout = QHBoxLayout() + + # 复制网站按钮 + copy_web_btn = QPushButton("复制网站") + copy_web_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "cursor.nosqli.com", "网站地址已复制到剪贴板")) + copy_web_btn.setStyleSheet(""" + QPushButton { + background-color: #0d6efd; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-size: 13px; + } + QPushButton:hover { + background-color: #0b5ed7; + } + """) + btn_layout.addWidget(copy_web_btn) + + # 复制微信按钮 + copy_wx_btn = QPushButton("复制微信") + copy_wx_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "behikcigar", "微信号已复制到剪贴板")) + copy_wx_btn.setStyleSheet(""" + QPushButton { + background-color: #198754; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-size: 13px; + } + QPushButton:hover { + background-color: #157347; + } + """) + btn_layout.addWidget(copy_wx_btn) + + # 确定按钮 + ok_btn = QPushButton("确定") + ok_btn.clicked.connect(msg.accept) + ok_btn.setStyleSheet(""" + QPushButton { + background-color: #0d6efd; + color: white; + border: none; + padding: 8px 25px; + border-radius: 4px; + font-size: 13px; + min-width: 100px; + } + QPushButton:hover { + background-color: #0b5ed7; + } + """) + btn_layout.addWidget(ok_btn) + + layout.addLayout(btn_layout) + msg.setLayout(layout) + msg.exec_() + 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 @@ -811,34 +889,185 @@ class MainWindow(QMainWindow): self.show_custom_error("刷新授权失败", message) else: self.show_custom_error("刷新授权失败", str(data)) - + def disable_cursor_update(self): """禁用Cursor更新""" 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 - self.show_custom_message( - "提示", - "功能未实现", - "此功能暂未实现,请等待后续更新。", - QStyle.SP_MessageBoxInformation, - "#0d6efd" - ) + 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( + "成功", + "突破限制成功", + "Cursor版本限制已突破,编辑器已重启。", + QStyle.SP_DialogApplyButton, + "#198754" + ) + else: + self.show_custom_error("突破限制失败", str(data)) + def show_custom_message(self, title, header, message, icon_type, color): """显示自定义消息框""" msg = QDialog(self) @@ -914,4 +1143,29 @@ class MainWindow(QMainWindow): message, QStyle.SP_MessageBoxCritical, "#dc3545" - ) \ No newline at end of file + ) + + 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 \ No newline at end of file diff --git a/main.py b/main.py index 6ce8a4c..57d20a8 100644 --- a/main.py +++ b/main.py @@ -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,7 +104,8 @@ def main(): error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}" logging.error(error_msg) # 使用 QMessageBox 显示错误 - app = QApplication(sys.argv) + if QApplication.instance() is None: + app = QApplication(sys.argv) QMessageBox.critical(None, "错误", error_msg) sys.exit(1) diff --git a/requirements.txt b/requirements.txt index ab393db..742fcca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +pillow==10.2.0 +PyQt5==5.15.10 +pywin32==306 \ No newline at end of file diff --git a/utils/config.py b/utils/config.py index f896cde..5e90f94 100644 --- a/utils/config.py +++ b/utils/config.py @@ -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" @@ -64,4 +71,15 @@ 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 \ No newline at end of file + 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, "") \ No newline at end of file diff --git a/version.txt b/version.txt index 2c6109e..aa00c92 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.3.4 \ No newline at end of file +3.3.9 \ No newline at end of file