diff --git a/README.md b/README.md index b15daed..6532db7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,69 @@ +# 听泉助手 + +一个用于管理Cursor编辑器授权的跨平台桌面应用。 + +## 功能特点 + +- 设备ID管理 +- 会员状态显示 +- 激活码管理 +- Cursor编辑器授权刷新 +- 版本限制实现 +- 更新控制 + +## 开发环境要求 + +- Python 3.9+ +- PyQt5 +- 其他依赖见 requirements.txt + +## 安装依赖 + +```bash +# 创建虚拟环境 +python -m venv venv + +# 激活虚拟环境 +# Windows: +venv\Scripts\activate +# Mac/Linux: +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +## 打包说明 + +### Windows版本 +```bash +# 运行打包脚本 +build_win.bat +``` + +### Mac版本 +```bash +# 添加执行权限 +chmod +x build_mac.command +# 运行打包脚本 +./build_mac.command +``` + +## 项目结构 + +``` +. +├── README.md +├── requirements.txt +├── build_mac.command # Mac打包脚本 +├── build_win.bat # Windows打包脚本 +├── gui/ +│ └── main_window.py # 主窗口界面 +├── logger.py # 日志模块 +├── update_cursor_token.py # 授权更新模块 +└── ... # 其他模块 +``` + # Cursor Pro 自动化工具使用说明 diff --git a/build_mac.command b/build_mac.command new file mode 100644 index 0000000..4794188 --- /dev/null +++ b/build_mac.command @@ -0,0 +1,61 @@ +#!/bin/bash + +# 获取脚本所在目录 +cd "$(dirname "$0")" + +# 检查虚拟环境 +if [ -f "venv/bin/activate" ]; then + echo "激活虚拟环境..." + source venv/bin/activate +else + echo "警告: 未找到虚拟环境,创建新的虚拟环境..." + python3 -m venv venv + source venv/bin/activate + + echo "安装依赖..." + pip3 install --upgrade pip + pip3 install pyinstaller + pip3 install PyQt5 + pip3 install requests + pip3 install urllib3 +fi + +# 确保依赖已安装 +echo "检查依赖..." +pip3 list | grep -E "pyinstaller|PyQt5|requests|urllib3" || { + echo "安装缺失的依赖..." + pip3 install pyinstaller PyQt5 requests urllib3 +} + +# 执行打包 +echo "开始打包..." +python3 -m PyInstaller \ + --name="听泉助手" \ + --windowed \ + --clean \ + --noconfirm \ + --add-data="logger.py:." \ + --add-data="update_cursor_token.py:." \ + --add-data="cursor_auth_manager.py:." \ + --add-data="reset_machine.py:." \ + --add-data="patch_cursor_get_machine_id.py:." \ + --add-data="exit_cursor.py:." \ + --add-data="go_cursor_help.py:." \ + --add-data="logo.py:." \ + --hidden-import=PyQt5 \ + --hidden-import=requests \ + --hidden-import=urllib3 \ + --target-architecture=universal2 \ + --codesign-identity=- \ + --osx-bundle-identifier=com.tingquan.helper \ + gui/main_window.py + +# 检查打包结果 +if [ -d "dist/听泉助手.app" ]; then + echo "打包成功!应用程序包已生成在 dist/听泉助手.app" +else + echo "打包失败,请检查错误信息" +fi + +# 退出虚拟环境 +deactivate \ No newline at end of file diff --git a/build_mac.py b/build_mac.py new file mode 100644 index 0000000..16d6586 --- /dev/null +++ b/build_mac.py @@ -0,0 +1,35 @@ +import PyInstaller.__main__ +import os +import sys + +# 获取当前脚本所在目录 +current_dir = os.path.dirname(os.path.abspath(__file__)) + +# 打包参数 +params = [ + 'gui/main_window.py', # 主程序入口 + '--name=听泉助手', # 应用名称 + '--onefile', # 打包成单个文件 + '--windowed', # 不显示控制台窗口 + '--clean', # 清理临时文件 + '--noconfirm', # 不确认覆盖 + f'--distpath={os.path.join(current_dir, "dist")}', # 输出目录 + f'--workpath={os.path.join(current_dir, "build")}', # 工作目录 + f'--specpath={current_dir}', # spec文件目录 + '--add-data=logger.py;.', # 添加额外文件 + '--add-data=update_cursor_token.py;.', + '--add-data=cursor_auth_manager.py;.', + '--add-data=reset_machine.py;.', + '--add-data=patch_cursor_get_machine_id.py;.', + '--add-data=exit_cursor.py;.', + '--add-data=go_cursor_help.py;.', + '--add-data=logo.py;.', + '--hidden-import=PyQt5', + '--hidden-import=requests', + '--hidden-import=urllib3', +] + +# 执行打包 +PyInstaller.__main__.run(params) + +print("打包完成!可执行文件已生成在dist目录下。") \ No newline at end of file diff --git a/build_mac_docker.bat b/build_mac_docker.bat new file mode 100644 index 0000000..53ff921 --- /dev/null +++ b/build_mac_docker.bat @@ -0,0 +1,17 @@ +@echo off +setlocal + +echo 构建Docker镜像... +docker build -t tingquan-mac-builder . + +echo 运行Docker容器进行打包... +docker run --rm -v "%cd%":/app tingquan-mac-builder + +echo 检查打包结果... +if exist "dist\听泉助手.app" ( + echo 打包成功!Mac应用程序包已生成在 dist\听泉助手.app +) else ( + echo 打包失败,请检查错误信息 +) + +endlocal \ No newline at end of file diff --git a/build_win.bat b/build_win.bat new file mode 100644 index 0000000..c6edb6f --- /dev/null +++ b/build_win.bat @@ -0,0 +1,54 @@ +@echo off +setlocal + +:: 获取脚本所在目录 +cd /d "%~dp0" + +:: 检查虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo Activating virtual environment... + call venv\Scripts\activate.bat +) else ( + echo Creating new virtual environment... + python -m venv venv + call venv\Scripts\activate.bat + + echo Installing dependencies... + python -m pip install --upgrade pip + pip install pyinstaller + pip install PyQt5 + pip install requests + pip install urllib3 +) + +:: 执行打包 +echo Starting build process... +python -m PyInstaller ^ + --name="TingquanHelper" ^ + --onefile ^ + --windowed ^ + --clean ^ + --noconfirm ^ + --add-data="logger.py;." ^ + --add-data="update_cursor_token.py;." ^ + --add-data="cursor_auth_manager.py;." ^ + --add-data="reset_machine.py;." ^ + --add-data="patch_cursor_get_machine_id.py;." ^ + --add-data="exit_cursor.py;." ^ + --add-data="go_cursor_help.py;." ^ + --add-data="logo.py;." ^ + --hidden-import=PyQt5 ^ + --hidden-import=requests ^ + --hidden-import=urllib3 ^ + gui/main_window.py + +:: 检查打包结果 +if exist "dist\TingquanHelper.exe" ( + echo Build successful! Executable created at dist\TingquanHelper.exe +) else ( + echo Build failed. Please check the error messages. +) + +:: 退出虚拟环境 +deactivate +endlocal \ No newline at end of file diff --git a/build_win.py b/build_win.py new file mode 100644 index 0000000..16d6586 --- /dev/null +++ b/build_win.py @@ -0,0 +1,35 @@ +import PyInstaller.__main__ +import os +import sys + +# 获取当前脚本所在目录 +current_dir = os.path.dirname(os.path.abspath(__file__)) + +# 打包参数 +params = [ + 'gui/main_window.py', # 主程序入口 + '--name=听泉助手', # 应用名称 + '--onefile', # 打包成单个文件 + '--windowed', # 不显示控制台窗口 + '--clean', # 清理临时文件 + '--noconfirm', # 不确认覆盖 + f'--distpath={os.path.join(current_dir, "dist")}', # 输出目录 + f'--workpath={os.path.join(current_dir, "build")}', # 工作目录 + f'--specpath={current_dir}', # spec文件目录 + '--add-data=logger.py;.', # 添加额外文件 + '--add-data=update_cursor_token.py;.', + '--add-data=cursor_auth_manager.py;.', + '--add-data=reset_machine.py;.', + '--add-data=patch_cursor_get_machine_id.py;.', + '--add-data=exit_cursor.py;.', + '--add-data=go_cursor_help.py;.', + '--add-data=logo.py;.', + '--hidden-import=PyQt5', + '--hidden-import=requests', + '--hidden-import=urllib3', +] + +# 执行打包 +PyInstaller.__main__.run(params) + +print("打包完成!可执行文件已生成在dist目录下。") \ No newline at end of file diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 0000000..e102a9c --- /dev/null +++ b/gui/main_window.py @@ -0,0 +1,405 @@ +import sys +import os + +# 添加父目录到系统路径 +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +sys.path.append(parent_dir) + +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QPushButton, QLabel, QLineEdit, QTextEdit, QMessageBox, + QHBoxLayout, QFrame) +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QFont, QIcon +from update_cursor_token import CursorTokenUpdater +from logger import logging + +class UpdateWorker(QThread): + """后台更新线程""" + finished = pyqtSignal(bool, str) + progress = pyqtSignal(str) + + def __init__(self, updater): + super().__init__() + self.updater = updater + + def run(self): + try: + success = self.updater.full_update_process() + if success: + self.finished.emit(True, "更新成功!") + else: + self.finished.emit(False, "更新失败,请查看日志") + except Exception as e: + self.finished.emit(False, f"发生错误: {str(e)}") + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + try: + logging.info("正在初始化主窗口...") + self.updater = CursorTokenUpdater() + self.init_ui() + logging.info("主窗口初始化完成") + except Exception as e: + logging.error(f"初始化主窗口时发生错误: {str(e)}") + QMessageBox.critical(self, "错误", f"初始化失败: {str(e)}") + + def init_ui(self): + # 设置窗口基本属性 + self.setWindowTitle('听泉助手') + self.setMinimumSize(600, 500) + self.setStyleSheet(""" + QMainWindow { + background-color: #f5f5f7; + } + QPushButton { + background-color: #0066cc; + color: white; + border: none; + border-radius: 6px; + padding: 10px; + font-size: 14px; + min-width: 120px; + } + QPushButton:hover { + background-color: #0052a3; + } + QPushButton:pressed { + background-color: #003d7a; + } + QLabel { + font-size: 14px; + color: #333333; + } + QTextEdit { + border: 1px solid #cccccc; + border-radius: 6px; + padding: 10px; + background-color: white; + font-family: Consolas, Monaco, monospace; + } + QLineEdit { + border: 1px solid #cccccc; + border-radius: 6px; + padding: 8px; + background-color: white; + } + QMessageBox { + background-color: #f5f5f7; + } + QMessageBox QPushButton { + min-width: 80px; + padding: 5px 15px; + } + """) + + try: + # 创建主窗口部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + layout.setSpacing(20) + layout.setContentsMargins(30, 30, 30, 30) + + # 设备ID显示区域 + device_frame = QFrame() + device_frame.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + padding: 10px; + } + """) + device_layout = QHBoxLayout(device_frame) + device_layout.setContentsMargins(10, 5, 10, 5) + + device_id_label = QLabel("设备标识:") + self.device_id_text = QLineEdit() + self.device_id_text.setReadOnly(True) + + try: + hardware_id = self.updater.hardware_id + logging.info(f"获取到硬件ID: {hardware_id}") + self.device_id_text.setText(hardware_id) + except Exception as e: + logging.error(f"获取硬件ID失败: {str(e)}") + self.device_id_text.setText("获取失败") + + copy_button = QPushButton("复制ID") + copy_button.setMaximumWidth(80) + copy_button.clicked.connect(self.copy_device_id) + + device_layout.addWidget(device_id_label) + device_layout.addWidget(self.device_id_text) + device_layout.addWidget(copy_button) + + layout.addWidget(device_frame) + + # 会员状态区域 + member_frame = QFrame() + member_frame.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + padding: 20px; + } + """) + member_layout = QVBoxLayout(member_frame) + member_layout.setContentsMargins(15, 10, 15, 10) + member_layout.setSpacing(8) + + # 会员信息显示区域 + self.member_info = QTextEdit() + self.member_info.setReadOnly(True) + self.member_info.setFixedHeight(100) # 增加高度 + self.member_info.setStyleSheet(""" + QTextEdit { + border: none; + background-color: transparent; + font-family: Consolas, Monaco, monospace; + font-size: 14px; + line-height: 1.6; + } + """) + + # 设置默认会员信息(简化版) + self.member_info.setText("会员状态: 正常\n到期时间: 2025-02-22 20:44:23") + + member_layout.addWidget(self.member_info) + layout.addWidget(member_frame) + + # 激活区域 + activate_frame = QFrame() + activate_frame.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + padding: 10px; + } + """) + activate_layout = QVBoxLayout(activate_frame) + activate_layout.setContentsMargins(10, 5, 10, 5) + activate_layout.setSpacing(5) + + # 激活标题和说明 + activate_title = QLabel("激活(益加)会员,多个激活码可益加整体时长") + activate_title.setStyleSheet(""" + QLabel { + color: #333333; + } + """) + + # 激活码输入区域 + input_layout = QHBoxLayout() + input_layout.setSpacing(10) + self.activate_input = QLineEdit() + self.activate_input.setPlaceholderText("请输入激活码") + + activate_button = QPushButton("激活") + activate_button.setMaximumWidth(80) + activate_button.clicked.connect(self.activate_license) + + input_layout.addWidget(self.activate_input) + input_layout.addWidget(activate_button) + + activate_layout.addWidget(activate_title) + activate_layout.addLayout(input_layout) + + layout.addWidget(activate_frame) + + # 功能按钮区域 + button_frame = QFrame() + button_frame.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + padding: 15px; + } + """) + button_layout = QVBoxLayout(button_frame) + button_layout.setSpacing(10) + + # 第一行按钮 + self.update_button = QPushButton("刷新Cursor编辑器授权") + self.update_button.clicked.connect(self.start_update) + + # 第二行按钮 + self.reset_button = QPushButton("实现Cursor0.45.+限制") + self.reset_button.clicked.connect(self.reset_machine) + + # 第三行按钮 + self.disable_update_button = QPushButton("禁用Cursor版本更新") + self.disable_update_button.clicked.connect(self.disable_cursor_update) + + # 设置按钮样式 + for button in [self.update_button, self.reset_button, self.disable_update_button]: + button.setStyleSheet(""" + QPushButton { + background-color: #0066cc; + color: white; + border: none; + border-radius: 4px; + padding: 12px; + font-size: 14px; + } + QPushButton:hover { + background-color: #0052a3; + } + QPushButton:pressed { + background-color: #003d7a; + } + """) + + button_layout.addWidget(self.update_button) + button_layout.addWidget(self.reset_button) + button_layout.addWidget(self.disable_update_button) + + layout.addWidget(button_frame) + + # 初始化工作线程 + self.worker = None + + logging.info("界面元素初始化完成") + + except Exception as e: + logging.error(f"初始化界面元素时发生错误: {str(e)}") + raise + + def copy_device_id(self): + """复制设备ID到剪贴板""" + try: + clipboard = QApplication.clipboard() + clipboard.setText(self.device_id_text.text()) + QMessageBox.information(self, "提示", "设备ID已复制到剪贴板") + logging.info("设备ID已复制到剪贴板") + except Exception as e: + logging.error(f"复制设备ID时发生错误: {str(e)}") + QMessageBox.warning(self, "错误", f"复制失败: {str(e)}") + + def append_log(self, text): + """添加日志到状态显示区域""" + try: + self.status_text.append(text) + self.status_text.verticalScrollBar().setValue( + self.status_text.verticalScrollBar().maximum() + ) + logging.info(f"状态更新: {text}") + except Exception as e: + logging.error(f"更新状态显示时发生错误: {str(e)}") + + def start_update(self): + """开始更新流程""" + try: + self.update_button.setEnabled(False) + self.reset_button.setEnabled(False) + self.status_text.clear() + self.append_log("开始更新流程...") + + self.worker = UpdateWorker(self.updater) + self.worker.finished.connect(self.update_finished) + self.worker.progress.connect(self.append_log) + self.worker.start() + + logging.info("更新进程已启动") + except Exception as e: + logging.error(f"启动更新进程时发生错误: {str(e)}") + self.update_button.setEnabled(True) + self.reset_button.setEnabled(True) + QMessageBox.warning(self, "错误", f"启动更新失败: {str(e)}") + + def update_finished(self, success, message): + """更新完成的回调""" + try: + self.update_button.setEnabled(True) + self.reset_button.setEnabled(True) + + if success: + QMessageBox.information(self, "成功", message) + else: + QMessageBox.warning(self, "失败", message) + + self.append_log(message) + logging.info(f"更新完成: {message}") + except Exception as e: + logging.error(f"处理更新完成回调时发生错误: {str(e)}") + + def reset_machine(self): + """重置机器码""" + try: + reply = QMessageBox.question( + self, + "确认", + "确定要重置机器码吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + logging.info("开始重置机器码") + success = self.updater.reset_machine_id() + if success: + QMessageBox.information(self, "成功", "机器码重置成功") + self.device_id_text.setText(self.updater.hardware_id) + logging.info("机器码重置成功") + else: + QMessageBox.warning(self, "失败", "机器码重置失败") + logging.error("机器码重置失败") + except Exception as e: + logging.error(f"重置机器码时发生错误: {str(e)}") + QMessageBox.warning(self, "错误", f"重置失败: {str(e)}") + + def activate_license(self): + """激活许可证""" + try: + activation_code = self.activate_input.text().strip() + if not activation_code: + QMessageBox.warning(self, "提示", "请输入激活码") + return + + # TODO: 实现激活逻辑 + logging.info(f"正在处理激活请求,激活码: {activation_code}") + QMessageBox.information(self, "提示", "激活功能即将实现") + + except Exception as e: + logging.error(f"激活过程中发生错误: {str(e)}") + QMessageBox.warning(self, "错误", f"激活失败: {str(e)}") + + def disable_cursor_update(self): + """禁用Cursor版本更新""" + try: + # TODO: 实现禁用更新逻辑 + logging.info("正在禁用Cursor版本更新...") + QMessageBox.information(self, "提示", "禁用更新功能即将实现") + except Exception as e: + logging.error(f"禁用更新时发生错误: {str(e)}") + QMessageBox.warning(self, "错误", f"禁用失败: {str(e)}") + +def main(): + try: + logging.info("程序启动...") + app = QApplication(sys.argv) + + # 设置应用程序图标 + if getattr(sys, 'frozen', False): + # 如果是打包后的应用 + application_path = sys._MEIPASS + logging.info(f"运行于打包环境: {application_path}") + else: + # 如果是开发环境 + application_path = os.path.dirname(os.path.abspath(__file__)) + logging.info(f"运行于开发环境: {application_path}") + + # 创建并显示主窗口 + window = MainWindow() + window.show() + logging.info("主窗口已显示") + + sys.exit(app.exec_()) + except Exception as e: + logging.error(f"程序运行时发生错误: {str(e)}") + QMessageBox.critical(None, "错误", f"程序启动失败: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b1577d7..5eb4d1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ DrissionPage==4.1.0.9 colorama==0.4.6 python-dotenv==1.0.0 -pyinstaller \ No newline at end of file +pyinstaller==6.3.0 +PyQt5==5.15.9 +requests==2.31.0 +urllib3==2.1.0 \ No newline at end of file diff --git a/update_cursor_token.py b/update_cursor_token.py new file mode 100644 index 0000000..b272f8f --- /dev/null +++ b/update_cursor_token.py @@ -0,0 +1,338 @@ +import os +import json +import platform +import requests +import urllib3 +import ssl +import sys +import subprocess +import hashlib +from cursor_auth_manager import CursorAuthManager +from logger import logging +from reset_machine import MachineIDResetter +import patch_cursor_get_machine_id +from exit_cursor import ExitCursor +import go_cursor_help +from logo import print_logo +from typing import Tuple, Dict, Optional + +class CursorTokenUpdater: + def __init__(self): + self.auth_manager = CursorAuthManager() + self._hardware_id = None # 延迟初始化硬件ID + + @property + def hardware_id(self) -> str: + """获取硬件ID(延迟初始化)""" + if self._hardware_id is None: + self._hardware_id = self._get_hardware_id() + return self._hardware_id + + def _get_hardware_id(self) -> str: + """获取硬件唯一标识 + 方案1: CPU ID + 主板序列号 + BIOS序列号 + 方案2: 系统盘序列号 + Windows安装时间 + 方案3: 计算机名(最后的备选方案) + """ + try: + # 创建startupinfo对象来隐藏命令行窗口 + startupinfo = None + if sys.platform == "win32": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + # 方案1: 尝试获取硬件信息 + try: + # 获取CPU ID + cpu_info = subprocess.check_output('wmic cpu get ProcessorId', startupinfo=startupinfo).decode() + cpu_id = cpu_info.split('\n')[1].strip() + + # 获取主板序列号 + board_info = subprocess.check_output('wmic baseboard get SerialNumber', startupinfo=startupinfo).decode() + board_id = board_info.split('\n')[1].strip() + + # 获取BIOS序列号 + bios_info = subprocess.check_output('wmic bios get SerialNumber', startupinfo=startupinfo).decode() + bios_id = bios_info.split('\n')[1].strip() + + # 如果所有信息都获取成功且有效 + if all([cpu_id, board_id, bios_id]) and not all(x in ['', '0', 'None', 'To be filled by O.E.M.'] for x in [cpu_id, board_id, bios_id]): + combined = f"{cpu_id}:{board_id}:{bios_id}" + hardware_id = hashlib.md5(combined.encode()).hexdigest() + logging.info("使用硬件信息生成ID成功") + return hardware_id + + except Exception as e: + logging.warning(f"方案1失败: {str(e)}") + + # 方案2: 系统盘序列号 + Windows安装时间 + try: + backup_info = [] + + # 获取系统盘序列号 + volume_info = subprocess.check_output('wmic logicaldisk where "DeviceID=\'C:\'" get VolumeSerialNumber', startupinfo=startupinfo).decode() + volume_serial = volume_info.split('\n')[1].strip() + if volume_serial and volume_serial not in ['', '0']: + backup_info.append(("volume", volume_serial)) + + # 获取Windows安装时间 + os_info = subprocess.check_output('wmic os get InstallDate', startupinfo=startupinfo).decode() + install_date = os_info.split('\n')[1].strip() + if install_date: + backup_info.append(("install", install_date)) + + if backup_info: + combined = "|".join(f"{k}:{v}" for k, v in sorted(backup_info)) + hardware_id = hashlib.md5(combined.encode()).hexdigest() + logging.info("使用系统信息生成ID成功") + return hardware_id + + except Exception as e: + logging.warning(f"方案2失败: {str(e)}") + + # 方案3: 使用计算机名(最后的备选方案) + computer_name = platform.node() + if computer_name: + hardware_id = hashlib.md5(computer_name.encode()).hexdigest() + logging.info("使用计算机名生成ID成功") + return hardware_id + + raise ValueError("无法获取任何可用信息来生成硬件ID") + + except Exception as e: + error_msg = f"生成硬件ID失败: {str(e)}" + logging.error(error_msg) + raise RuntimeError(error_msg) + + def get_unused_account(self) -> Tuple[bool, str, Optional[Dict]]: + """ + 从API获取未使用的账号 + + Returns: + Tuple[bool, str, Optional[Dict]]: + - 是否成功 + - 错误信息 + - 账号数据(如果成功) + """ + endpoint = "https://cursorapi.nosqli.com/admin/api.account/getUnused" + data = { + "machine_id": self.hardware_id + } + headers = { + "Content-Type": "application/json" + } + + # 禁用SSL警告 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + request_kwargs = { + "json": data, + "headers": headers, + "timeout": 30, + "verify": False + } + + try: + try: + response = requests.post(endpoint, **request_kwargs) + except requests.exceptions.SSLError: + # SSL错误时使用自定义SSL上下文 + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + session = requests.Session() + session.verify = False + response = session.post(endpoint, **request_kwargs) + + response_data = response.json() + + if response_data.get("code") == 200: + account_data = response_data.get("data", {}) + + # 获取账号信息 + email = account_data.get("email", "") + access_token = account_data.get("access_token", "") + refresh_token = account_data.get("refresh_token", "") + expire_time = account_data.get("expire_time", "") + days_left = account_data.get("days_left", 0) + + if not all([email, access_token, refresh_token]): + return False, "获取账号信息不完整", None + + account_info = { + "email": email, + "access_token": access_token, + "refresh_token": refresh_token, + "expire_time": expire_time, + "days_left": days_left + } + + logging.info(f"成功获取账号信息 - 邮箱: {email}, 剩余天数: {days_left}") + return True, "", account_info + else: + error_msg = response_data.get("msg", "未知错误") + return False, f"API返回错误: {error_msg}", None + + except Exception as e: + error_msg = f"获取账号时发生错误: {str(e)}" + logging.error(error_msg) + return False, error_msg, None + + def update_auth_info(self, email: str, access_token: str, refresh_token: str = None) -> bool: + """ + 更新Cursor的认证信息 + + Args: + email: 用户邮箱 + access_token: 访问令牌 + refresh_token: 刷新令牌(如果没有提供,将使用access_token) + + Returns: + bool: 更新是否成功 + """ + try: + # 如果没有提供refresh_token,使用access_token + if refresh_token is None: + refresh_token = access_token + + # 更新认证信息 + result = self.auth_manager.update_auth( + email=email, + access_token=access_token, + refresh_token=refresh_token + ) + + if result: + logging.info(f"认证信息更新成功 - 邮箱: {email}") + return True + else: + logging.error("认证信息更新失败") + return False + + except Exception as e: + logging.error(f"更新认证信息时发生错误: {str(e)}") + return False + + def reset_machine_id(self, greater_than_0_45: bool = True) -> bool: + """ + 重置机器码 + + Args: + greater_than_0_45: 是否大于0.45版本 + + Returns: + bool: 重置是否成功 + """ + try: + logging.info("开始重置机器码...") + resetter = MachineIDResetter() + result = resetter.reset(greater_than_0_45) + + if result: + logging.info("机器码重置成功") + # 重置后更新硬件ID缓存 + self._hardware_id = None + return True + else: + logging.error("机器码重置失败") + return False + + except Exception as e: + logging.error(f"重置机器码时发生错误: {str(e)}") + return False + + def patch_machine_id(self) -> bool: + """ + 修补机器码获取方法 + + Returns: + bool: 修补是否成功 + """ + try: + logging.info("开始修补机器码获取方法...") + patch_cursor_get_machine_id.patch() + logging.info("机器码获取方法修补完成") + return True + except Exception as e: + logging.error(f"修补机器码获取方法时发生错误: {str(e)}") + return False + + def exit_cursor(self) -> bool: + """ + 退出Cursor进程 + + Returns: + bool: 退出是否成功 + """ + try: + logging.info("正在退出Cursor进程...") + exit_handler = ExitCursor() + exit_handler.exit() + logging.info("Cursor进程已退出") + return True + except Exception as e: + logging.error(f"退出Cursor进程时发生错误: {str(e)}") + return False + + def full_update_process(self, email: str = None, access_token: str = None, refresh_token: str = None) -> bool: + """ + 执行完整的更新流程 + + Args: + email: 用户邮箱(可选,如果不提供则从API获取) + access_token: 访问令牌(可选,如果不提供则从API获取) + refresh_token: 刷新令牌(可选,如果不提供则从API获取) + + Returns: + bool: 更新流程是否全部成功 + """ + print_logo() + logging.info("=== 开始完整更新流程 ===") + + # 1. 退出Cursor进程 + if not self.exit_cursor(): + return False + + # 2. 修补机器码获取方法 + if not self.patch_machine_id(): + return False + + # 3. 重置机器码 + if not self.reset_machine_id(greater_than_0_45=True): + return False + + # 4. 如果没有提供认证信息,从API获取 + if not all([email, access_token]): + success, error_msg, account_info = self.get_unused_account() + if not success: + logging.error(f"无法获取账号信息: {error_msg}") + return False + email = account_info["email"] + access_token = account_info["access_token"] + refresh_token = account_info["refresh_token"] + + # 5. 更新认证信息 + if not self.update_auth_info(email, access_token, refresh_token): + return False + + logging.info("=== 所有操作已完成 ===") + return True + +def main(): + updater = CursorTokenUpdater() + + # 从环境变量获取认证信息(可选) + + + # 如果环境变量中有认证信息,使用环境变量中的信息 + # 否则,将从API获取新的账号信息 + success = updater.full_update_process( + + ) + + print("更新状态:", "成功" if success else "失败") + +if __name__ == "__main__": + main() \ No newline at end of file