From fdc56730bb31eedca287beb6bd8e064f8877a5c2 Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Wed, 12 Feb 2025 09:33:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=96=87=E4=BB=B6=E5=92=8C=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account_switcher.py | 183 ++++++++++++++++++++++++++++++++------- cursor_auth_manager.py | 134 ++++++++++++++++++++++++++++ gui/main_window.py | 26 ++---- main.py | 60 ++++++++----- requirements.txt | 4 +- utils/cursor_registry.py | 72 +++++++++++++++ 6 files changed, 409 insertions(+), 70 deletions(-) create mode 100644 cursor_auth_manager.py create mode 100644 utils/cursor_registry.py diff --git a/account_switcher.py b/account_switcher.py index e0520c2..d81453a 100644 --- a/account_switcher.py +++ b/account_switcher.py @@ -5,23 +5,34 @@ import logging import subprocess import uuid import hashlib +import sys +import time from typing import Optional, Dict, Tuple from pathlib import Path from utils.config import Config +from utils.cursor_registry import CursorRegistry +from cursor_auth_manager import CursorAuthManager def get_hardware_id() -> 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').decode() + 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').decode() + 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').decode() + bios_info = subprocess.check_output('wmic bios get SerialNumber', startupinfo=startupinfo).decode() bios_id = bios_info.split('\n')[1].strip() # 组合信息并生成哈希 @@ -32,35 +43,6 @@ def get_hardware_id() -> str: # 如果获取失败,使用UUID作为备选方案 return str(uuid.uuid4()) -class CursorAuthManager: - def __init__(self): - self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor" - self.app_path = self.cursor_path / "resources" / "app" - self.package_json = self.app_path / "package.json" - - def update_auth(self, email: str, access_token: str, refresh_token: str) -> bool: - """更新Cursor认证信息""" - try: - # 读取package.json - with open(self.package_json, "r", encoding="utf-8") as f: - data = json.load(f) - - # 更新认证信息 - data["email"] = email - data["accessToken"] = access_token - data["refreshToken"] = refresh_token - - # 保存更新后的文件 - with open(self.package_json, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - - logging.info(f"认证信息更新成功: {email}") - return True - - except Exception as e: - logging.error(f"更新认证信息失败: {str(e)}") - return False - class AccountSwitcher: def __init__(self): self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor" @@ -69,6 +51,7 @@ class AccountSwitcher: self.auth_manager = CursorAuthManager() self.config = Config() self.hardware_id = get_hardware_id() + self.registry = CursorRegistry() # 添加注册表操作工具类 def get_device_info(self) -> dict: """获取设备信息""" @@ -384,6 +367,142 @@ class AccountSwitcher: "activation_records": [] } + def restart_cursor(self) -> bool: + """重启Cursor编辑器 + + Returns: + bool: 是否成功重启 + """ + try: + logging.info("正在重启Cursor...") + if sys.platform == "win32": + # Windows系统 + # 关闭Cursor + os.system("taskkill /f /im Cursor.exe 2>nul") + time.sleep(2) + # 获取Cursor安装路径 + cursor_exe = self.cursor_path / "Cursor.exe" + if cursor_exe.exists(): + # 启动Cursor + os.startfile(str(cursor_exe)) + logging.info("Cursor重启成功") + return True + else: + logging.error(f"未找到Cursor程序: {cursor_exe}") + return False + elif sys.platform == "darwin": + # macOS系统 + os.system("killall Cursor 2>/dev/null") + time.sleep(2) + os.system("open -a Cursor") + logging.info("Cursor重启成功") + return True + elif sys.platform == "linux": + # Linux系统 + os.system("pkill -f cursor") + time.sleep(2) + os.system("cursor &") + logging.info("Cursor重启成功") + return True + else: + logging.error(f"不支持的操作系统: {sys.platform}") + return False + except Exception as e: + logging.error(f"重启Cursor时发生错误: {str(e)}") + return False + + def refresh_cursor_auth(self) -> Tuple[bool, str]: + """刷新Cursor授权 + + Returns: + Tuple[bool, str]: (是否成功, 提示消息) + """ + try: + # 获取未使用的账号 + endpoint = "https://cursorapi.nosqli.com/admin/api.account/getUnused" + data = { + "machine_id": self.hardware_id + } + headers = { + "Content-Type": "application/json" + } + + try: + # 添加SSL验证选项和超时设置 + response = requests.post( + endpoint, + json=data, + headers=headers, + timeout=30, # 增加超时时间 + verify=False, # 禁用SSL验证 + ) + # 禁用SSL警告 + requests.packages.urllib3.disable_warnings() + + 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, "获取账号信息不完整" + + # 更新Cursor认证信息 + if not self.auth_manager.update_auth(email, access_token, refresh_token): + return False, "更新Cursor认证信息失败" + + # 重置机器码 + if not self.reset_machine_id(): + return False, "重置机器码失败" + + # 刷新注册表 + if not self.registry.refresh_registry(): + logging.warning("注册表刷新失败,但不影响主要功能") + + # 重启Cursor + if not self.auth_manager.restart_cursor(): + return False, "重启Cursor失败" + # 重启Cursor + # if not self.restart_cursor(): + # logging.warning("Cursor重启失败,请手动重启") + # return True, f"授权刷新成功,请手动重启Cursor编辑器\n邮箱: {email}\n到期时间: {expire_time}\n剩余天数: {days_left}天" + + return True, f"授权刷新成功,Cursor编辑器已重启\n邮箱: {email}\n" + + elif response_data.get("code") == 404: + return False, "没有可用的未使用账号" + else: + error_msg = response_data.get("msg", "未知错误") + logging.error(f"获取未使用账号失败: {error_msg}") + return False, f"获取账号失败: {error_msg}" + + except requests.exceptions.SSLError as e: + logging.error(f"SSL验证失败: {str(e)}") + return False, "SSL验证失败,请检查网络设置" + except requests.exceptions.ConnectionError as e: + logging.error(f"网络连接错误: {str(e)}") + return False, "网络连接失败,请检查网络设置" + except requests.exceptions.Timeout as e: + logging.error(f"请求超时: {str(e)}") + return False, "请求超时,请稍后重试" + except requests.RequestException as e: + logging.error(f"请求失败: {str(e)}") + return False, f"网络请求失败: {str(e)}" + except Exception as e: + logging.error(f"未知错误: {str(e)}") + return False, f"发生未知错误: {str(e)}" + + except Exception as e: + logging.error(f"刷新授权过程出错: {str(e)}") + return False, f"刷新失败: {str(e)}" + def main(): """主函数""" try: diff --git a/cursor_auth_manager.py b/cursor_auth_manager.py new file mode 100644 index 0000000..15c5218 --- /dev/null +++ b/cursor_auth_manager.py @@ -0,0 +1,134 @@ +import os +import json +import sys +import time +import logging +import sqlite3 +from pathlib import Path + +class CursorAuthManager: + """Cursor认证信息管理器""" + + def __init__(self): + # 判断操作系统 + if sys.platform == "win32": # Windows + appdata = os.getenv("APPDATA") + if appdata is None: + raise EnvironmentError("APPDATA 环境变量未设置") + self.db_path = os.path.join( + appdata, "Cursor", "User", "globalStorage", "state.vscdb" + ) + elif sys.platform == "darwin": # macOS + self.db_path = os.path.abspath(os.path.expanduser( + "~/Library/Application Support/Cursor/User/globalStorage/state.vscdb" + )) + elif sys.platform == "linux" : # Linux 和其他类Unix系统 + self.db_path = os.path.abspath(os.path.expanduser( + "~/.config/Cursor/User/globalStorage/state.vscdb" + )) + else: + raise NotImplementedError(f"不支持的操作系统: {sys.platform}") + + self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor" + + def update_auth(self, email=None, access_token=None, refresh_token=None): + """ + 更新Cursor的认证信息 + :param email: 新的邮箱地址 + :param access_token: 新的访问令牌 + :param refresh_token: 新的刷新令牌 + :return: bool 是否成功更新 + """ + updates = [] + # 登录状态 + updates.append(("cursorAuth/cachedSignUpType", "Auth_0")) + + if email is not None: + updates.append(("cursorAuth/cachedEmail", email)) + if access_token is not None: + updates.append(("cursorAuth/accessToken", access_token)) + if refresh_token is not None: + updates.append(("cursorAuth/refreshToken", refresh_token)) + + if not updates: + logging.warning("没有提供任何要更新的值") + return False + + conn = None + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + for key, value in updates: + # 检查key是否存在 + check_query = f"SELECT COUNT(*) FROM itemTable WHERE key = ?" + cursor.execute(check_query, (key,)) + if cursor.fetchone()[0] == 0: + insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)" + cursor.execute(insert_query, (key, value)) + else: + update_query = "UPDATE itemTable SET value = ? WHERE key = ?" + cursor.execute(update_query, (value, key)) + + if cursor.rowcount > 0: + logging.info(f"成功更新 {key.split('/')[-1]}") + else: + logging.warning(f"未找到 {key.split('/')[-1]} 或值未变化") + + conn.commit() + logging.info(f"认证信息更新成功: {email}") + return True + + except sqlite3.Error as e: + logging.error(f"数据库错误: {str(e)}") + return False + except Exception as e: + logging.error(f"更新认证信息失败: {str(e)}") + return False + finally: + if conn: + conn.close() + + def restart_cursor(self) -> bool: + """重启Cursor编辑器 + + Returns: + bool: 是否成功重启 + """ + try: + logging.info("正在重启Cursor...") + if sys.platform == "win32": + # Windows系统 + # 关闭Cursor + os.system("taskkill /f /im Cursor.exe 2>nul") + time.sleep(2) + # 获取Cursor安装路径 + cursor_exe = self.cursor_path / "Cursor.exe" + if cursor_exe.exists(): + # 启动Cursor + os.startfile(str(cursor_exe)) + logging.info("Cursor重启成功") + return True + else: + logging.error(f"未找到Cursor程序: {cursor_exe}") + return False + elif sys.platform == "darwin": + # macOS系统 + os.system("killall Cursor 2>/dev/null") + time.sleep(2) + os.system("open -a Cursor") + logging.info("Cursor重启成功") + return True + elif sys.platform == "linux": + # Linux系统 + os.system("pkill -f cursor") + time.sleep(2) + os.system("cursor &") + logging.info("Cursor重启成功") + return True + else: + logging.error(f"不支持的操作系统: {sys.platform}") + return False + except Exception as e: + logging.error(f"重启Cursor时发生错误: {str(e)}") + return False \ No newline at end of file diff --git a/gui/main_window.py b/gui/main_window.py index fcf068d..823ed24 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -110,8 +110,8 @@ class MainWindow: btn_frame.pack(fill="x", pady=5) self.style.configure("Action.TButton", padding=8) - ttk.Button(btn_frame, text="刷新Cursor授权", command=self.reset_machine_id, style="Action.TButton").pack(fill="x", pady=2) - ttk.Button(btn_frame, text="实现Cursor0.45.x限制", command=self.dummy_function, style="Action.TButton").pack(fill="x", pady=2) + ttk.Button(btn_frame, text="刷新Cursor编辑器授权", command=self.reset_machine_id, style="Action.TButton").pack(fill="x", pady=2) + ttk.Button(btn_frame, text="突破Cursor0.45.x限制", command=self.dummy_function, style="Action.TButton").pack(fill="x", pady=2) ttk.Button(btn_frame, text="禁用Cursor版本更新", command=self.dummy_function, style="Action.TButton").pack(fill="x", pady=2) def copy_device_id(self): @@ -283,21 +283,13 @@ class MainWindow: messagebox.showinfo("提示", "此功能暂未实现") def reset_machine_id(self): - """重置机器码""" - # 先检查状态 - if not self.check_status(): - return - + """刷新Cursor编辑器授权""" try: - if self.switcher.reset_machine_id(): - messagebox.showinfo("成功", "机器码重置成功") - # 重置后检查一次状态 - self.check_status() + # 刷新授权 + success, message = self.switcher.refresh_cursor_auth() + if success: + messagebox.showinfo("成功", "Cursor编辑器授权刷新成功!\n" + message) else: - messagebox.showerror("错误", "机器码重置失败,请查看日志") - # 失败后也检查状态 - self.check_status() + messagebox.showerror("错误", message) except Exception as e: - messagebox.showerror("错误", f"重置失败: {str(e)}") - # 出错后也检查状态 - self.check_status() \ No newline at end of file + messagebox.showerror("错误", f"刷新失败: {str(e)}") \ No newline at end of file diff --git a/main.py b/main.py index 236612f..5c6befb 100644 --- a/main.py +++ b/main.py @@ -1,37 +1,57 @@ import logging +import sys +import traceback from pathlib import Path from gui.main_window import MainWindow def setup_logging(): """设置日志""" - log_dir = Path.home() / ".cursor_switcher" / "logs" - log_dir.mkdir(parents=True, exist_ok=True) - - log_file = log_dir / "switcher.log" - - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", - handlers=[ - logging.FileHandler(log_file, encoding="utf-8"), - logging.StreamHandler() - ] - ) - logging.info("日志系统初始化完成") + try: + log_dir = Path.home() / ".cursor_switcher" / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + + log_file = log_dir / "switcher.log" + + # 只输出到文件,不输出到控制台 + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler(log_file, encoding="utf-8"), + ] + ) + except Exception as e: + # 不打印错误信息,只记录到日志 + pass def main(): """主函数""" try: setup_logging() - logging.info("启动GUI界面...") + + # 检查Python版本 + logging.info(f"Python版本: {sys.version}") + + # 检查工作目录 + logging.info(f"当前工作目录: {Path.cwd()}") + + # 检查模块路径 + logging.info("Python路径:") + for p in sys.path: + logging.info(f" - {p}") + + logging.info("正在初始化主窗口...") window = MainWindow() + + logging.info("正在启动主窗口...") window.run() - except KeyboardInterrupt: - logging.info("程序被用户中断") + except Exception as e: - logging.error(f"程序运行出错: {str(e)}") - finally: - logging.info("程序退出") + error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}" + logging.error(error_msg) + # 使用tkinter的消息框显示错误 + from tkinter import messagebox + messagebox.showerror("错误", error_msg) if __name__ == "__main__": main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 34d8ed1..8b52759 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ requests==2.31.0 -pyinstaller==6.3.0 \ No newline at end of file +pyinstaller==6.3.0 +pillow==10.2.0 # 用于处理图标 +setuptools==65.5.1 # 解决pkg_resources.extern问题 \ No newline at end of file diff --git a/utils/cursor_registry.py b/utils/cursor_registry.py new file mode 100644 index 0000000..7966fec --- /dev/null +++ b/utils/cursor_registry.py @@ -0,0 +1,72 @@ +import os +import winreg +import logging +from pathlib import Path + +class CursorRegistry: + """Cursor注册表操作工具类""" + + def __init__(self): + self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor" + self.app_path = self.cursor_path / "resources" / "app" + + def refresh_registry(self) -> bool: + """刷新Cursor相关的注册表项 + + Returns: + bool: 是否成功 + """ + try: + # 获取Cursor安装路径 + cursor_exe = self.cursor_path / "Cursor.exe" + if not cursor_exe.exists(): + logging.error("未找到Cursor.exe") + return False + + # 刷新注册表项 + try: + # 打开HKEY_CURRENT_USER\Software\Classes\Directory\Background\shell\Cursor + key_path = r"Software\Classes\Directory\Background\shell\Cursor" + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE) as key: + winreg.SetValueEx(key, "Icon", 0, winreg.REG_SZ, str(cursor_exe)) + + # 打开command子键 + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path + r"\command", 0, winreg.KEY_WRITE) as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f'"{str(cursor_exe)}" "%V"') + + logging.info("注册表刷新成功") + return True + + except WindowsError as e: + logging.error(f"刷新注册表失败: {str(e)}") + return False + + except Exception as e: + logging.error(f"刷新注册表过程出错: {str(e)}") + return False + + def clean_registry(self) -> bool: + """清理Cursor相关的注册表项 + + Returns: + bool: 是否成功 + """ + try: + # 删除注册表项 + try: + key_path = r"Software\Classes\Directory\Background\shell\Cursor" + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, key_path + r"\command") + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, key_path) + logging.info("注册表清理成功") + return True + + except WindowsError as e: + if e.winerror == 2: # 找不到注册表项 + logging.info("注册表项不存在,无需清理") + return True + logging.error(f"清理注册表失败: {str(e)}") + return False + + except Exception as e: + logging.error(f"清理注册表过程出错: {str(e)}") + return False \ No newline at end of file