diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b77fe42 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +""" +听泉助手 - 脚本包 +""" + +__version__ = "4.0.0.1" \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e6f0d69 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/common_utils.cpython-312.pyc b/__pycache__/common_utils.cpython-312.pyc new file mode 100644 index 0000000..cc2d101 Binary files /dev/null and b/__pycache__/common_utils.cpython-312.pyc differ diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..c9724ab Binary files /dev/null and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/cursor_token_refresher.cpython-312.pyc b/__pycache__/cursor_token_refresher.cpython-312.pyc new file mode 100644 index 0000000..0aa17b5 Binary files /dev/null and b/__pycache__/cursor_token_refresher.cpython-312.pyc differ diff --git a/__pycache__/exit_cursor.cpython-312.pyc b/__pycache__/exit_cursor.cpython-312.pyc new file mode 100644 index 0000000..40b417e Binary files /dev/null and b/__pycache__/exit_cursor.cpython-312.pyc differ diff --git a/__pycache__/logger.cpython-312.pyc b/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000..358c232 Binary files /dev/null and b/__pycache__/logger.cpython-312.pyc differ diff --git a/__pycache__/machine_resetter.cpython-312.pyc b/__pycache__/machine_resetter.cpython-312.pyc new file mode 100644 index 0000000..ee26c7d Binary files /dev/null and b/__pycache__/machine_resetter.cpython-312.pyc differ diff --git a/__pycache__/update_disabler.cpython-312.pyc b/__pycache__/update_disabler.cpython-312.pyc new file mode 100644 index 0000000..c95f316 Binary files /dev/null and b/__pycache__/update_disabler.cpython-312.pyc differ diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..7a635e8 --- /dev/null +++ b/build.bat @@ -0,0 +1,113 @@ +@echo off +chcp 65001 +setlocal EnableDelayedExpansion +echo 开始正式版本打包... + +:: 设置工作目录为脚本所在目录 +cd /d "%~dp0" + +:: 激活虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 激活虚拟环境... + call venv\Scripts\activate.bat +) else ( + echo 警告: 未找到虚拟环境,使用系统 Python +) + +:: 确保安装了必要的包 +echo 检查依赖包... +python -c "import PyInstaller" >nul 2>&1 +if errorlevel 1 ( + echo 正在安装 PyInstaller... + python -m pip install pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +:: 如果存在requirements.txt,安装依赖 +if exist "requirements.txt" ( + echo 安装项目依赖... + pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +:: 设置基础变量 +set VERSION_FILE=version.txt +set SPEC_FILE=tingquan_assistant.spec + +:: 读取版本号 +set /p VERSION=<%VERSION_FILE% +echo 当前版本: !VERSION! + +:: 提取主版本号和次版本号 (4.0.0.1 -> 4.0) +for /f "tokens=1,2 delims=." %%a in ("!VERSION!") do ( + set MAJOR_VERSION=%%a.%%b +) +echo 主版本目录: !MAJOR_VERSION! + +:: 创建版本目录 +set VERSION_DIR=dist\!MAJOR_VERSION! +if not exist "!VERSION_DIR!" ( + mkdir "!VERSION_DIR!" + echo 创建目录: !VERSION_DIR! +) + +:: 清理 Python 缓存文件 +echo 清理Python缓存文件... +for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" +del /s /q *.pyc >nul 2>&1 +del /s /q *.pyo >nul 2>&1 + +:: 清理旧的打包文件 +echo 清理旧文件... +taskkill /F /IM "听泉助手*.exe" >nul 2>&1 +timeout /t 2 /nobreak >nul +if exist "build" rd /s /q "build" +if exist "dist" rd /s /q "dist" + +:: 使用优化选项进行打包 +echo 开始打包... +python -m PyInstaller !SPEC_FILE! --noconfirm --clean + +:: 检查打包结果并移动文件 +set BUILD_DIR=dist\听泉助手v!VERSION! +set TARGET_FILE=!VERSION_DIR!\听泉助手v!VERSION!.exe + +echo 检查构建目录: !BUILD_DIR! +if exist "!BUILD_DIR!" ( + echo 正式版本打包成功! + + :: 创建目标目录(如果不存在) + if not exist "!VERSION_DIR!" mkdir "!VERSION_DIR!" + + :: 移动整个目录到版本目录 + echo 移动文件到: !VERSION_DIR! + xcopy "!BUILD_DIR!\*" "!VERSION_DIR!" /E /I /Y + + :: 显示文件大小 + for %%I in ("!TARGET_FILE!") do ( + echo 文件大小: %%~zI 字节 + ) + + echo. + echo 正式版本构建完成! + echo 版本号: v!VERSION! + echo 文件位置: !TARGET_FILE! +) else ( + echo 错误: 打包失败,构建目录不存在 + echo 预期构建目录: !BUILD_DIR! + dir /b dist + exit /b 1 +) + +:: 清理临时文件 +echo 清理临时文件... +if exist "build" rd /s /q "build" +if exist "dist" rd /s /q "dist" + +:: 退出虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 退出虚拟环境... + deactivate +) + +echo. +echo 按任意键退出... +pause >nul \ No newline at end of file diff --git a/common_utils.py b/common_utils.py new file mode 100644 index 0000000..d365180 --- /dev/null +++ b/common_utils.py @@ -0,0 +1,420 @@ +"""通用工具函数库""" + +import os +import sys +import hashlib +import subprocess +import platform +import json +import requests +import urllib3 +import uuid +import winreg +import sqlite3 +from typing import List, Tuple, Optional, Dict +from datetime import datetime + +from logger import logger +from config import config + +# 获取日志记录器 +_logger = logger.get_logger("CommonUtils") + +# 全局用户状态 +_global_user_state = { + "is_activated": False, + "expire_time": None, + "days_left": 0, + "total_days": 0, + "device_info": {}, + "status": "inactive" +} + +# 设备信息缓存 +_device_info_cache = None + +def _get_cached_device_info() -> Dict: + """ + 获取设备信息(使用缓存) + 只在首次调用时获取系统信息,后续使用缓存 + + Returns: + Dict: 设备基础信息 + """ + global _device_info_cache + + if _device_info_cache is None: + try: + # 基础系统信息 + device_info = { + "os": platform.system(), + "device_name": platform.node(), + "ip": "未知", + "location": "未知" + } + + # 获取IP和位置信息 + try: + # 使用ip-api.com的免费API获取IP和位置信息 + response = requests.get('http://ip-api.com/json/?lang=zh-CN', timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('status') == 'success': + device_info.update({ + "ip": data.get('query', '未知'), + "location": f"{data.get('country', '')} {data.get('regionName', '')} {data.get('city', '')}" + }) + except Exception as e: + _logger.warning(f"获取IP和位置信息失败: {str(e)}") + + _device_info_cache = device_info + _logger.debug("已初始化设备信息缓存") + + except Exception as e: + _logger.error(f"获取设备信息失败: {str(e)}") + _device_info_cache = { + "os": "Windows", + "device_name": "未知", + "ip": "未知", + "location": "未知" + } + + return _device_info_cache.copy() + +def update_user_state(state_data: Dict) -> None: + """ + 更新全局用户状态 + Args: + state_data: 新的状态数据 + """ + global _global_user_state + + # 保留现有的设备信息 + current_device_info = _global_user_state.get("device_info", {}) + + # 更新状态数据 + _global_user_state.update(state_data) + + # 合并设备信息 + if "device_info" in state_data: + new_device_info = state_data["device_info"] + # 只更新可能变动的信息(IP和地区) + current_device_info.update({ + "ip": new_device_info.get("ip", current_device_info.get("ip", "未知")), + "location": new_device_info.get("location", current_device_info.get("location", "未知")) + }) + _global_user_state["device_info"] = current_device_info + + _logger.info(f"用户状态已更新: {state_data}") + +def get_user_state() -> Dict: + """ + 获取当前用户状态 + Returns: + Dict: 用户状态信息 + """ + return _global_user_state.copy() + +def check_user_state() -> bool: + """ + 检查用户是否处于可操作状态 + Returns: + bool: 是否可以操作 + """ + return _global_user_state.get("is_activated", False) + +def refresh_user_state(machine_id: str) -> Tuple[bool, str, Dict]: + """ + 检查设备激活状态 + + 通过设备ID向服务器请求验证设备的激活状态、剩余时间等信息。 + 服务器会进行以下检查: + 1. 设备ID是否合法 + 2. 是否已激活 + 3. 激活是否过期 + 4. 剩余使用时间 + + Args: + machine_id: 设备唯一标识 + + Returns: + Tuple[bool, str, Dict]: + - bool: 是否验证通过 + - str: 状态消息 + - Dict: 状态数据 + """ + try: + # 禁用SSL警告 + urllib3.disable_warnings() + + # 准备请求数据 + data = {"machine_id": machine_id} + + # 发送状态检查请求 + response = requests.post( + config.status_url, + json=data, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False + ) + + # 解析响应 + result = response.json() + + if result.get("code") in [1, 200]: + api_data = result.get("data", {}) + + # 获取缓存的设备信息 + device_info = _get_cached_device_info() + # 只更新可能变动的信息 + device_info.update({ + "ip": api_data.get("ip", device_info["ip"]), + "location": api_data.get("location", device_info["location"]) + }) + + # 更新状态数据 + state_data = { + "is_activated": api_data.get("status") == "active", + "status": api_data.get("status", "inactive"), + "expire_time": api_data.get("expire_time", ""), + "days_left": api_data.get("days_left", 0), + "total_days": api_data.get("total_days", 0), + "device_info": device_info + } + + # 更新全局状态 + update_user_state(state_data) + + # 根据状态返回对应消息 + if state_data["is_activated"]: + msg = f"设备已激活,剩余{state_data['days_left']}天" + else: + if api_data.get("status") == "expired": + msg = "设备授权已过期" + else: + msg = "设备未激活" + + return True, msg, state_data + + else: + error_msg = result.get("msg", "未知错误") + _logger.error(f"设备状态检查失败: {error_msg}") + return False, f"状态检查失败: {error_msg}", {} + + except Exception as e: + error_msg = str(e) + _logger.error(f"设备状态检查异常: {error_msg}") + return False, f"状态检查异常: {error_msg}", {} + +def get_hardware_id() -> str: + """获取硬件唯一标识 + 方案1: CPU ID + 主板序列号 + BIOS序列号 + 方案2: 系统盘序列号 + Windows安装时间 + 方案3: 计算机名(最后的备选方案) + + Returns: + str: 硬件ID的MD5哈希值 + + Raises: + RuntimeError: 当无法获取任何可用的硬件信息时 + """ + 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() + _logger.info("使用硬件信息生成ID成功") + return hardware_id + + except Exception as e: + _logger.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() + _logger.info("使用系统信息生成ID成功") + return hardware_id + + except Exception as e: + _logger.warning(f"方案2失败: {str(e)}") + + # 方案3: 使用计算机名(最后的备选方案) + computer_name = platform.node() + if computer_name: + hardware_id = hashlib.md5(computer_name.encode()).hexdigest() + _logger.info("使用计算机名生成ID成功") + return hardware_id + + raise ValueError("无法获取任何可用信息来生成硬件ID") + + except Exception as e: + error_msg = f"生成硬件ID失败: {str(e)}" + _logger.error(error_msg) + raise RuntimeError(error_msg) + +def verify_hardware_id(stored_id: str) -> bool: + """ + 验证当前硬件ID是否与存储的ID匹配 + + Args: + stored_id: 存储的硬件ID + + Returns: + bool: 如果匹配返回True,否则返回False + """ + try: + current_id = get_hardware_id() + return current_id == stored_id + except Exception as e: + _logger.error(f"验证硬件ID失败: {str(e)}") + return False + +def get_system_info() -> dict: + """ + 获取系统信息 + + Returns: + dict: 包含系统信息的字典 + """ + info = { + "os": platform.system(), + "os_version": platform.version(), + "machine": platform.machine(), + "processor": platform.processor(), + "node": platform.node() + } + + if sys.platform == "win32": + try: + # 获取更多Windows特定信息 + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + # Windows产品ID + windows_info = subprocess.check_output('wmic os get SerialNumber', startupinfo=startupinfo).decode() + info["windows_id"] = windows_info.split('\n')[1].strip() + + except Exception as e: + _logger.warning(f"获取Windows特定信息失败: {str(e)}") + + return info + +def activate_device(machine_id: str, activation_code: str) -> Tuple[bool, str, Optional[Dict]]: + """ + 激活设备 + + Args: + machine_id: 设备ID + activation_code: 激活码 + + Returns: + Tuple[bool, str, Optional[Dict]]: + - bool: 是否成功 + - str: 消息 + - Optional[Dict]: 激活数据 + """ + try: + # 禁用SSL警告 + urllib3.disable_warnings() + + # 准备请求数据 + data = { + "machine_id": machine_id, + "code": activation_code + } + + # 发送激活请求 + response = requests.post( + config.activate_url, + json=data, + headers={ + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Accept": "*/*" + }, + timeout=30, + verify=False + ) + + # 解析响应 + result = response.json() + + if result.get("code") == 200: + api_data = result.get("data", {}) + + # 获取缓存的设备信息 + device_info = _get_cached_device_info() + + # 构造状态数据 + state_data = { + "is_activated": True, + "status": "active", + "expire_time": api_data.get("expire_time", ""), + "days_left": api_data.get("days_left", 0), + "total_days": api_data.get("total_days", 0), + "device_info": device_info + } + + # 更新全局状态 + update_user_state(state_data) + + return True, f"激活成功,剩余{state_data['days_left']}天", state_data + + elif result.get("code") == 400: + error_msg = result.get("msg", "激活码无效或已被使用") + _logger.warning(f"激活失败: {error_msg}") + return False, error_msg, None + + else: + error_msg = result.get("msg", "未知错误") + _logger.error(f"激活失败: {error_msg}") + return False, f"激活失败: {error_msg}", None + + except requests.exceptions.RequestException as e: + error_msg = f"网络连接失败: {str(e)}" + _logger.error(error_msg) + return False, error_msg, None + + except Exception as e: + error_msg = f"激活失败: {str(e)}" + _logger.error(error_msg) + return False, error_msg, None \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..be1eb61 --- /dev/null +++ b/config.py @@ -0,0 +1,54 @@ +from typing import Dict + +class Config: + """配置管理类""" + + def __init__(self): + """初始化配置""" + # API基础URL + self.base_url = "https://cursorapi.nosqli.com" + + # API端点 + self.api_endpoints = { + "get_unused": f"{self.base_url}/admin/api.account/getUnused", + "activate": f"{self.base_url}/admin/api.member/activate", + "status": f"{self.base_url}/admin/api.member/status", + "heartbeat": f"{self.base_url}/admin/api.account/heartbeat" + } + + # API请求配置 + self.request_config = { + "timeout": 30, + "verify": False, + "headers": { + "Content-Type": "application/json" + } + } + + @property + def get_unused_url(self) -> str: + """获取未使用账号的API地址""" + return self.api_endpoints["get_unused"] + + @property + def activate_url(self) -> str: + """获取激活账号的API地址""" + return self.api_endpoints["activate"] + + @property + def status_url(self) -> str: + """获取状态检查的API地址""" + return self.api_endpoints["status"] + + @property + def heartbeat_url(self) -> str: + """获取心跳检测的API地址""" + return self.api_endpoints["heartbeat"] + + @property + def request_kwargs(self) -> Dict: + """获取请求配置""" + return self.request_config.copy() + +# 创建全局配置实例 +config = Config() \ No newline at end of file diff --git a/cursor_token_refresher.py b/cursor_token_refresher.py new file mode 100644 index 0000000..cbbe4e1 --- /dev/null +++ b/cursor_token_refresher.py @@ -0,0 +1,145 @@ +import os +import sys +import sqlite3 +import requests +import urllib3 +from typing import Dict, Tuple, Optional +from datetime import datetime +from config import config +from logger import logger + +class CursorTokenRefresher: + """Cursor Token 刷新器 - 简化版""" + + def __init__(self): + """初始化刷新器""" + # 设置数据库路径 + if sys.platform == "win32": + appdata = os.getenv("APPDATA") + if appdata is None: + raise EnvironmentError("APPDATA 环境变量未设置") + self.db_path = os.path.join(appdata, "Cursor", "User", "globalStorage", "state.vscdb") + else: + raise NotImplementedError(f"暂不支持的操作系统: {sys.platform}") + + # 获取日志记录器 + self.logger = logger.get_logger("TokenRefresh") + + def update_auth(self, email: Optional[str] = None, + access_token: Optional[str] = None, + refresh_token: Optional[str] = None) -> bool: + """ + 更新SQLite数据库中的认证信息 + :param email: 可选,新的邮箱地址 + :param access_token: 可选,新的访问令牌 + :param refresh_token: 可选,新的刷新令牌 + :return: 是否成功更新 + """ + 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: + self.logger.warning("没有提供任何要更新的值") + return False + + conn = None + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + for key, value in updates: + # 如果没有更新任何行,说明key不存在,执行插入 + # 检查 key 是否存在 + check_query = "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)) + self.logger.info(f"已插入新记录: {key.split('/')[-1]}") + else: + update_query = "UPDATE itemTable SET value = ? WHERE key = ?" + cursor.execute(update_query, (value, key)) + if cursor.rowcount > 0: + self.logger.info(f"成功更新 {key.split('/')[-1]}") + else: + self.logger.warning(f"未找到 {key.split('/')[-1]} 或值未变化") + + conn.commit() + return True + + except sqlite3.Error as e: + self.logger.error(f"数据库错误: {str(e)}") + return False + except Exception as e: + self.logger.error(f"发生错误: {str(e)}") + return False + finally: + if conn: + conn.close() + + def refresh_token(self, machine_id: str) -> Tuple[bool, str, Optional[Dict]]: + """ + 刷新token(从API获取新账号并更新到数据库) + :param machine_id: 机器ID + :return: (是否成功, 消息, 可选的账号信息) + """ + try: + # 禁用SSL警告 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # 发送请求 + response = requests.post( + config.get_unused_url, + json={"machine_id": machine_id}, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False + ) + + # 解析响应 + 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") + + if not all([email, access_token, refresh_token]): + return False, "获取账号信息不完整", None + + # 更新SQLite数据库 + if self.update_auth(email=email, + access_token=access_token, + refresh_token=refresh_token): + account_info = { + "email": email, + "expire_time": expire_time, + "days_left": days_left + } + return True, "认证信息更新成功", account_info + else: + return False, "更新认证信息失败", None + + elif response_data.get("code") == 404: + return False, "没有可用的未使用账号", None + else: + error_msg = response_data.get("msg", "未知错误") + return False, f"获取账号失败: {error_msg}", None + + except Exception as e: + error_msg = str(e) + self.logger.error(f"刷新token失败: {error_msg}") + return False, f"刷新token失败: {error_msg}", None \ No newline at end of file diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140.dll new file mode 100644 index 0000000..4fec76d Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140_1.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140_1.dll new file mode 100644 index 0000000..a4cae47 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/MSVCP140_1.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Core.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Core.dll new file mode 100644 index 0000000..40e8de1 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Core.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Gui.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Gui.dll new file mode 100644 index 0000000..bf38dda Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Gui.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Widgets.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Widgets.dll new file mode 100644 index 0000000..80ae4e3 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/Qt5Widgets.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/VCRUNTIME140_1.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/VCRUNTIME140_1.dll new file mode 100644 index 0000000..fc7bad4 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/bin/VCRUNTIME140_1.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/platforms/qwindows.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/platforms/qwindows.dll new file mode 100644 index 0000000..e9c319d Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/platforms/qwindows.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/styles/qwindowsvistastyle.dll b/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/styles/qwindowsvistastyle.dll new file mode 100644 index 0000000..c97acd6 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/Qt5/plugins/styles/qwindowsvistastyle.dll differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/QtCore.pyd b/dist/tingquan_assistant/_internal/PyQt5/QtCore.pyd new file mode 100644 index 0000000..5c9ec64 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/QtCore.pyd differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/QtGui.pyd b/dist/tingquan_assistant/_internal/PyQt5/QtGui.pyd new file mode 100644 index 0000000..9f67406 Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/QtGui.pyd differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/QtWidgets.pyd b/dist/tingquan_assistant/_internal/PyQt5/QtWidgets.pyd new file mode 100644 index 0000000..2d1c49a Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/QtWidgets.pyd differ diff --git a/dist/tingquan_assistant/_internal/PyQt5/sip.cp312-win_amd64.pyd b/dist/tingquan_assistant/_internal/PyQt5/sip.cp312-win_amd64.pyd new file mode 100644 index 0000000..12a269c Binary files /dev/null and b/dist/tingquan_assistant/_internal/PyQt5/sip.cp312-win_amd64.pyd differ diff --git a/dist/tingquan_assistant/_internal/VCRUNTIME140.dll b/dist/tingquan_assistant/_internal/VCRUNTIME140.dll new file mode 100644 index 0000000..53f1537 Binary files /dev/null and b/dist/tingquan_assistant/_internal/VCRUNTIME140.dll differ diff --git a/dist/tingquan_assistant/_internal/_brotli.cp312-win_amd64.pyd b/dist/tingquan_assistant/_internal/_brotli.cp312-win_amd64.pyd new file mode 100644 index 0000000..4f47048 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_brotli.cp312-win_amd64.pyd differ diff --git a/dist/tingquan_assistant/_internal/_bz2.pyd b/dist/tingquan_assistant/_internal/_bz2.pyd new file mode 100644 index 0000000..112c1fd Binary files /dev/null and b/dist/tingquan_assistant/_internal/_bz2.pyd differ diff --git a/dist/tingquan_assistant/_internal/_ctypes.pyd b/dist/tingquan_assistant/_internal/_ctypes.pyd new file mode 100644 index 0000000..d9280a6 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_ctypes.pyd differ diff --git a/dist/tingquan_assistant/_internal/_hashlib.pyd b/dist/tingquan_assistant/_internal/_hashlib.pyd new file mode 100644 index 0000000..23647b1 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_hashlib.pyd differ diff --git a/dist/tingquan_assistant/_internal/_lzma.pyd b/dist/tingquan_assistant/_internal/_lzma.pyd new file mode 100644 index 0000000..6b215da Binary files /dev/null and b/dist/tingquan_assistant/_internal/_lzma.pyd differ diff --git a/dist/tingquan_assistant/_internal/_queue.pyd b/dist/tingquan_assistant/_internal/_queue.pyd new file mode 100644 index 0000000..13b1d42 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_queue.pyd differ diff --git a/dist/tingquan_assistant/_internal/_socket.pyd b/dist/tingquan_assistant/_internal/_socket.pyd new file mode 100644 index 0000000..55fa155 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_socket.pyd differ diff --git a/dist/tingquan_assistant/_internal/_sqlite3.pyd b/dist/tingquan_assistant/_internal/_sqlite3.pyd new file mode 100644 index 0000000..f02e00e Binary files /dev/null and b/dist/tingquan_assistant/_internal/_sqlite3.pyd differ diff --git a/dist/tingquan_assistant/_internal/_ssl.pyd b/dist/tingquan_assistant/_internal/_ssl.pyd new file mode 100644 index 0000000..3dffc4c Binary files /dev/null and b/dist/tingquan_assistant/_internal/_ssl.pyd differ diff --git a/dist/tingquan_assistant/_internal/_uuid.pyd b/dist/tingquan_assistant/_internal/_uuid.pyd new file mode 100644 index 0000000..08dbf4c Binary files /dev/null and b/dist/tingquan_assistant/_internal/_uuid.pyd differ diff --git a/dist/tingquan_assistant/_internal/_wmi.pyd b/dist/tingquan_assistant/_internal/_wmi.pyd new file mode 100644 index 0000000..4cbca44 Binary files /dev/null and b/dist/tingquan_assistant/_internal/_wmi.pyd differ diff --git a/dist/tingquan_assistant/_internal/charset_normalizer/md.cp312-win_amd64.pyd b/dist/tingquan_assistant/_internal/charset_normalizer/md.cp312-win_amd64.pyd new file mode 100644 index 0000000..e7fad76 Binary files /dev/null and b/dist/tingquan_assistant/_internal/charset_normalizer/md.cp312-win_amd64.pyd differ diff --git a/dist/tingquan_assistant/_internal/charset_normalizer/md__mypyc.cp312-win_amd64.pyd b/dist/tingquan_assistant/_internal/charset_normalizer/md__mypyc.cp312-win_amd64.pyd new file mode 100644 index 0000000..35673fa Binary files /dev/null and b/dist/tingquan_assistant/_internal/charset_normalizer/md__mypyc.cp312-win_amd64.pyd differ diff --git a/dist/tingquan_assistant/_internal/libcrypto-3.dll b/dist/tingquan_assistant/_internal/libcrypto-3.dll new file mode 100644 index 0000000..778f4ed Binary files /dev/null and b/dist/tingquan_assistant/_internal/libcrypto-3.dll differ diff --git a/dist/tingquan_assistant/_internal/libffi-8.dll b/dist/tingquan_assistant/_internal/libffi-8.dll new file mode 100644 index 0000000..8ebbbe8 Binary files /dev/null and b/dist/tingquan_assistant/_internal/libffi-8.dll differ diff --git a/dist/tingquan_assistant/_internal/libssl-3.dll b/dist/tingquan_assistant/_internal/libssl-3.dll new file mode 100644 index 0000000..569658b Binary files /dev/null and b/dist/tingquan_assistant/_internal/libssl-3.dll differ diff --git a/dist/tingquan_assistant/_internal/psutil/_psutil_windows.pyd b/dist/tingquan_assistant/_internal/psutil/_psutil_windows.pyd new file mode 100644 index 0000000..3967f12 Binary files /dev/null and b/dist/tingquan_assistant/_internal/psutil/_psutil_windows.pyd differ diff --git a/dist/tingquan_assistant/_internal/python3.dll b/dist/tingquan_assistant/_internal/python3.dll new file mode 100644 index 0000000..b7c992b Binary files /dev/null and b/dist/tingquan_assistant/_internal/python3.dll differ diff --git a/dist/tingquan_assistant/_internal/python312.dll b/dist/tingquan_assistant/_internal/python312.dll new file mode 100644 index 0000000..49f7362 Binary files /dev/null and b/dist/tingquan_assistant/_internal/python312.dll differ diff --git a/dist/tingquan_assistant/_internal/select.pyd b/dist/tingquan_assistant/_internal/select.pyd new file mode 100644 index 0000000..3853f76 Binary files /dev/null and b/dist/tingquan_assistant/_internal/select.pyd differ diff --git a/dist/tingquan_assistant/_internal/sqlite3.dll b/dist/tingquan_assistant/_internal/sqlite3.dll new file mode 100644 index 0000000..bd4128b Binary files /dev/null and b/dist/tingquan_assistant/_internal/sqlite3.dll differ diff --git a/dist/tingquan_assistant/_internal/unicodedata.pyd b/dist/tingquan_assistant/_internal/unicodedata.pyd new file mode 100644 index 0000000..ca2ae95 Binary files /dev/null and b/dist/tingquan_assistant/_internal/unicodedata.pyd differ diff --git a/dist/tingquan_assistant/tingquan_assistant.exe b/dist/tingquan_assistant/tingquan_assistant.exe new file mode 100644 index 0000000..6194031 Binary files /dev/null and b/dist/tingquan_assistant/tingquan_assistant.exe differ diff --git a/exit_cursor.py b/exit_cursor.py new file mode 100644 index 0000000..7a7620a --- /dev/null +++ b/exit_cursor.py @@ -0,0 +1,68 @@ +import psutil +import logging +import time + +def ExitCursor(timeout=5): + """ + 温和地关闭 Cursor 进程 + Args: + timeout (int): 等待进程自然终止的超时时间(秒) + Returns: + bool: 是否成功关闭所有进程 + """ + try: + logging.info("开始退出Cursor...") + cursor_processes = [] + + # 收集所有 Cursor 进程 + for proc in psutil.process_iter(['pid', 'name']): + try: + if proc.info['name'].lower() in ['cursor.exe', 'cursor']: + cursor_processes.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if not cursor_processes: + logging.info("未发现运行中的 Cursor 进程") + return True + + # 温和地请求进程终止 + for proc in cursor_processes: + try: + if proc.is_running(): + proc.terminate() # 发送终止信号 + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + # 等待进程自然终止 + start_time = time.time() + while time.time() - start_time < timeout: + still_running = [] + for proc in cursor_processes: + try: + if proc.is_running(): + still_running.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if not still_running: + logging.info("所有 Cursor 进程已正常关闭") + return True + + # 等待一小段时间再检查 + time.sleep(0.5) + + # 如果超时后仍有进程在运行 + if still_running: + process_list = ", ".join([str(p.pid) for p in still_running]) + logging.warning(f"以下进程未能在规定时间内关闭: {process_list}") + return False + + return True + + except Exception as e: + logging.error(f"关闭 Cursor 进程时发生错误: {str(e)}") + return False + +if __name__ == "__main__": + ExitCursor() \ No newline at end of file diff --git a/file_version_info.txt b/file_version_info.txt new file mode 100644 index 0000000..b4ef9c5 --- /dev/null +++ b/file_version_info.txt @@ -0,0 +1,43 @@ +# UTF-8 +# +# For more details about fixed file info 'ffi' see: +# http://msdn.microsoft.com/en-us/library/ms646997.aspx +VSVersionInfo( + ffi=FixedFileInfo( + # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) + # Set not needed items to zero 0. + filevers=(4, 0, 0, 1), + prodvers=(4, 0, 0, 1), + # Contains a bitmask that specifies the valid bits 'flags'r + mask=0x3f, + # Contains a bitmask that specifies the Boolean attributes of the file. + flags=0x0, + # The operating system for which this file was designed. + # 0x4 - NT and there is no need to change it. + OS=0x40004, + # The general type of file. + # 0x1 - the file is an application. + fileType=0x1, + # The function of the file. + # 0x0 - the function is not defined for this fileType + subtype=0x0, + # Creation date and time stamp. + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'080404b0', + [StringStruct(u'CompanyName', u'听泉助手'), + StringStruct(u'FileDescription', u'听泉助手 - Cursor激活管理工具'), + StringStruct(u'FileVersion', u'4.0.0.1'), + StringStruct(u'InternalName', u'听泉助手'), + StringStruct(u'LegalCopyright', u'Copyright (C) 2024'), + StringStruct(u'OriginalFilename', u'听泉助手.exe'), + StringStruct(u'ProductName', u'听泉助手'), + StringStruct(u'ProductVersion', u'4.0.0.1')]) + ]), + VarFileInfo([VarStruct(u'Translation', [2052, 1200])]) + ] +) \ No newline at end of file diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..672700e --- /dev/null +++ b/gui/__init__.py @@ -0,0 +1 @@ +"""GUI 包""" \ No newline at end of file diff --git a/gui/__pycache__/__init__.cpython-312.pyc b/gui/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7a6ad34 Binary files /dev/null and b/gui/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/components/__init__.py b/gui/components/__init__.py new file mode 100644 index 0000000..64051bd --- /dev/null +++ b/gui/components/__init__.py @@ -0,0 +1 @@ +"""GUI 组件包""" \ No newline at end of file diff --git a/gui/components/__pycache__/__init__.cpython-312.pyc b/gui/components/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a23e50f Binary files /dev/null and b/gui/components/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/components/__pycache__/widgets.cpython-312.pyc b/gui/components/__pycache__/widgets.cpython-312.pyc new file mode 100644 index 0000000..81f0fa1 Binary files /dev/null and b/gui/components/__pycache__/widgets.cpython-312.pyc differ diff --git a/gui/components/__pycache__/workers.cpython-312.pyc b/gui/components/__pycache__/workers.cpython-312.pyc new file mode 100644 index 0000000..c87c79b Binary files /dev/null and b/gui/components/__pycache__/workers.cpython-312.pyc differ diff --git a/gui/components/widgets.py b/gui/components/widgets.py new file mode 100644 index 0000000..2a74984 --- /dev/null +++ b/gui/components/widgets.py @@ -0,0 +1,634 @@ +from typing import Dict +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QTextEdit, QLabel, QFrame, QLineEdit, QMessageBox, QApplication, QDialog, QStyle, QProgressBar +) +from PyQt5.QtCore import ( + Qt, QTimer, QPropertyAnimation, QEasingCurve, + QPoint, QRect, QSize, pyqtProperty +) +from PyQt5.QtGui import QPainter, QColor, QPen + +class ActivationStatusWidget(QFrame): + """激活状态显示组件""" + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # 状态标题 + title_label = QLabel("激活状态", self) + title_label.setStyleSheet("font-size: 16px; font-weight: bold;") + layout.addWidget(title_label) + + # 状态信息 + self.status_label = QLabel(self) + self.status_label.setStyleSheet("font-size: 14px;") + layout.addWidget(self.status_label) + + self.setStyleSheet(""" + QFrame { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 10px; + } + """) + + def update_status(self, status: Dict): + """更新状态显示""" + if status["is_activated"]: + status_text = f"已激活 | 到期时间: {status['expire_time']} | 剩余天数: {status['days_left']}天" + self.status_label.setStyleSheet("color: #28a745;") + else: + status_text = "未激活" + self.status_label.setStyleSheet("color: #dc3545;") + self.status_label.setText(status_text) + +class ActivationWidget(QFrame): + """激活码输入组件 + + 职责: + 1. 提供激活码输入界面 + 2. 处理输入框和激活按钮的基础交互 + 3. 将激活请求传递给父窗口处理 + 4. 处理激活相关的提示信息 + + 属性: + code_input (QLineEdit): 激活码输入框 + activate_btn (QPushButton): 激活按钮 + + 信号流: + activate_btn.clicked -> activate() -> parent.handle_activation() + """ + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + # 连接回车键到激活功能 + self.code_input.returnPressed.connect(self.activate) + + def show_activation_required(self): + """显示需要激活的提示""" + msg = QDialog(self) + msg.setWindowTitle("提示") + msg.setFixedWidth(360) + msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + # 创建布局 + layout = QVBoxLayout() + layout.setSpacing(12) + layout.setContentsMargins(20, 15, 20, 15) + + # 创建顶部警告框 + header_frame = QFrame() + header_frame.setStyleSheet(""" + QFrame { + background-color: #fff3cd; + border: 1px solid #ffeeba; + border-radius: 4px; + } + QLabel { + background: transparent; + border: none; + } + """) + header_layout = QHBoxLayout(header_frame) + header_layout.setContentsMargins(12, 10, 12, 10) + header_layout.setSpacing(10) + + icon_label = QLabel() + icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(20, 20)) + header_layout.addWidget(icon_label) + + text_label = QLabel("请输入激活码") + text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #856404; background: transparent;") + header_layout.addWidget(text_label) + header_layout.addStretch() + + layout.addWidget(header_frame) + + # 添加提示文本 + info_text = QLabel( + "获取会员激活码,请通过以下方式:\n\n" + "官方自助网站:cursorpro.com.cn\n" + "微信客服号:bshkcigar\n" + "商店铺:xxx\n\n" + "诚挚祝愿,欢迎加盟合作!" + ) + info_text.setStyleSheet(""" + QLabel { + font-size: 13px; + color: #333333; + margin: 5px 0; + } + """) + layout.addWidget(info_text) + + # 添加按钮区域 + btn_layout = QHBoxLayout() + btn_layout.setSpacing(8) + + # 复制网站按钮 + copy_web_btn = QPushButton("复制网址") + copy_web_btn.setCursor(Qt.PointingHandCursor) + copy_web_btn.clicked.connect(lambda: self._copy_to_clipboard("cursorpro.com.cn", "已复制官网地址")) + copy_web_btn.setStyleSheet(""" + QPushButton { + background-color: #0d6efd; + color: white; + border: none; + padding: 6px 16px; + border-radius: 3px; + font-size: 13px; + } + QPushButton:hover { + background-color: #0b5ed7; + } + """) + btn_layout.addWidget(copy_web_btn) + + # 复制微信按钮 + copy_wx_btn = QPushButton("复制微信") + copy_wx_btn.setCursor(Qt.PointingHandCursor) + copy_wx_btn.clicked.connect(lambda: self._copy_to_clipboard("bshkcigar", "已复制微信号")) + copy_wx_btn.setStyleSheet(""" + QPushButton { + background-color: #198754; + color: white; + border: none; + padding: 6px 16px; + border-radius: 3px; + font-size: 13px; + } + QPushButton:hover { + background-color: #157347; + } + """) + btn_layout.addWidget(copy_wx_btn) + + # 确定按钮 + ok_btn = QPushButton("确定") + ok_btn.setCursor(Qt.PointingHandCursor) + ok_btn.clicked.connect(msg.accept) + ok_btn.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 6px 16px; + border-radius: 3px; + font-size: 13px; + } + QPushButton:hover { + background-color: #5c636a; + } + """) + btn_layout.addWidget(ok_btn) + + layout.addLayout(btn_layout) + + # 设置对话框样式和布局 + msg.setStyleSheet(""" + QDialog { + background-color: white; + } + """) + msg.setLayout(layout) + + # 显示对话框 + msg.exec_() + + def _copy_to_clipboard(self, text: str, status_msg: str): + """复制文本到剪贴板并更新状态栏""" + QApplication.clipboard().setText(text) + main_window = self.window() + if main_window and hasattr(main_window, 'status_bar'): + main_window.status_bar.set_status(status_msg) + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 10) # 移除边距 + + # 标题 + title = QLabel("激活(盖加)会员,多个激活码可叠加整体时长") + title.setStyleSheet("color: #28a745; font-size: 14px; font-weight: bold;") # 绿色标题 + layout.addWidget(title) + + # 激活码输入区域 + input_layout = QHBoxLayout() + self.code_input = QLineEdit() + self.code_input.setPlaceholderText("请输入激活码") + self.code_input.setMinimumWidth(300) + self.code_input.setStyleSheet(""" + QLineEdit { + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + color: #495057; + } + """) + input_layout.addWidget(self.code_input) + + # 激活按钮 + self.activate_btn = QPushButton("激活") + self.activate_btn.clicked.connect(self.activate) + self.activate_btn.setStyleSheet(""" + QPushButton { + background-color: #007bff; + color: white; + border: none; + padding: 8px 22px; + border-radius: 4px; + font-size: 13px; + } + QPushButton:hover { + background-color: #0056b3; + } + """) + input_layout.addWidget(self.activate_btn) + + layout.addLayout(input_layout) + + self.setStyleSheet(""" + QFrame { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 10px; + } + """) + + def activate(self): + """处理激活""" + try: + code = self.code_input.text().strip() + + # 获取主窗口实例 + main_window = self.window() + + if not main_window: + print("错误:找不到主窗口") + return + + if not hasattr(main_window, 'handle_activation'): + print("错误:主窗口没有 handle_activation 方法") + return + + if not code: + print("警告:激活码为空") + self.show_activation_required() + return + + print(f"正在处理激活码:{code}") + main_window.handle_activation(code) + + except Exception as e: + print(f"激活处理发生错误:{str(e)}") + + def clear_input(self): + """清空输入框""" + self.code_input.clear() + +class LogWidget(QTextEdit): + """日志显示组件""" + def __init__(self, parent=None): + super().__init__(parent) + self.setReadOnly(True) + self.setStyleSheet(""" + QTextEdit { + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px; + font-family: 'Consolas', 'Microsoft YaHei', monospace; + font-size: 12px; + } + """) + + def append_log(self, text: str): + """添加日志并滚动到底部""" + self.append(text) + self.verticalScrollBar().setValue( + self.verticalScrollBar().maximum() + ) + +class StatusBar(QFrame): + """状态栏组件""" + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """初始化UI""" + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 5, 10, 5) + + self.status_label = QLabel("就绪") + self.status_label.setStyleSheet("font-size: 14px; color: #333;") + layout.addWidget(self.status_label) + + def set_status(self, text: str): + """设置状态文本""" + self.status_label.setText(text) + +class ActionButton(QPushButton): + """操作按钮基类""" + def __init__(self, text: str, color: str, parent=None): + super().__init__(text, parent) + self.setStyleSheet(f""" + QPushButton {{ + background-color: {color}; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + min-height: 36px; + }} + QPushButton:hover {{ + background-color: {self._get_hover_color(color)}; + }} + QPushButton:disabled {{ + background-color: #ccc; + }} + """) + + def _get_hover_color(self, color: str) -> str: + """获取悬停颜色""" + # 简单的颜色加深处理 + if color == "#007bff": # 蓝色 + return "#0056b3" + elif color == "#28a745": # 绿色 + return "#218838" + return color + +class MemberStatusWidget(QFrame): + """会员状态显示组件 + + 职责: + 1. 显示会员状态信息 + 2. 显示设备信息 + 3. 根据不同状态显示不同样式 + + 属性: + status_text (QTextEdit): 状态信息显示区域 + + 显示信息: + - 会员状态(正常/未激活) + - 到期时间 + - 剩余天数 + - 设备信息(系统、设备名、IP、地区) + """ + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 10) + + # 创建一个容器 Frame + container = QFrame(self) + container.setObjectName("memberStatusContainer") + container_layout = QVBoxLayout(container) + container_layout.setSpacing(5) # 减小组件间距 + container_layout.setContentsMargins(0,0,0,0) # 减小内边距 + container.setFixedHeight(220) # 根据需要调整这个值 + + # 状态信息 + self.status_text = QTextEdit() + self.status_text.setReadOnly(True) + + self.status_text.setStyleSheet(""" + QTextEdit { + background-color: #f8fafc; + border: none; + border-radius: 6px; + padding: 20px; + color: #4a5568; + font-size: 15px; + line-height: 1.5; + } + /* 滚动条整体样式 */ + QScrollBar:vertical { + border: none; + background: #f8fafc; + width: 6px; + margin: 0px; + } + /* 滚动条滑块 */ + QScrollBar::handle:vertical { + background: #e2e8f0; + border-radius: 3px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background: #cbd5e1; + } + /* 滚动条上下按钮 */ + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + border: none; + background: none; + height: 0px; + } + /* 滚动条背景 */ + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; + } + """) + container_layout.addWidget(self.status_text) + + # 设置容器样式 + container.setStyleSheet(""" + #memberStatusContainer { + background-color: white; + border: 1px solid #e2e8f0; + border-radius: 6px; + } + #titleContainer { + background: transparent; + } + """) + + layout.addWidget(container) + + def update_status(self, status: Dict): + """更新状态显示""" + try: + # 获取状态信息 + is_activated = status.get("is_activated", False) + expire_time = status.get("expire_time", "未知") + days_left = status.get("days_left", 0) + device_info = status.get("device_info", {}) + + # 构建状态文本 + status_text = [] + + # 会员状态 + status_color = "#10B981" if is_activated else "#EF4444" # 绿色或红色 + status_text.append(f'会员状态:{("正常" if is_activated else "未激活")}') + + # 到期时间和剩余天数 + if is_activated: + status_text.append(f"到期时间:{expire_time}") + # 根据剩余天数显示不同颜色 + days_color = "#10B981" if days_left > 30 else "#F59E0B" if days_left > 7 else "#EF4444" + status_text.append(f'剩余天数:{days_left}天') + + # 设备信息 + status_text.append("\n设备信息:") + status_text.append(f"系统:{device_info.get('os', 'Windows')}") + status_text.append(f"设备名:{device_info.get('device_name', '未知')}") + status_text.append(f"IP地址:{device_info.get('ip', '未知')}") + status_text.append(f"地区:{device_info.get('location', '未知')}") + + # 更新文本 + self.status_text.setHtml("
".join(status_text)) + + except Exception as e: + # 如果发生异常,显示错误信息 + error_text = f'状态更新失败: {str(e)}' + self.status_text.setHtml(error_text) + +class LoadingSpinner(QWidget): + """自定义加载动画组件""" + def __init__(self, parent=None, size=40, line_width=3): + super().__init__(parent) + self.setFixedSize(size, size) + self.angle = 0 + self.line_width = line_width + self._size = size + self._speed = 30 # 每次更新转动的角度 + + # 设置动画 + self.animation = QPropertyAnimation(self, b"rotation") + self.animation.setDuration(800) # 缩短动画时间到800ms使其更流畅 + self.animation.setStartValue(0) + self.animation.setEndValue(360) + self.animation.setLoopCount(-1) # 无限循环 + + # 使用 InOutQuad 缓动曲线让动画更自然 + curve = QEasingCurve(QEasingCurve.InOutQuad) + self.animation.setEasingCurve(curve) + + # 创建定时器用于额外的动画效果 + self.timer = QTimer(self) + self.timer.timeout.connect(self.rotate) + self.timer.setInterval(50) # 20fps的刷新率 + + def paintEvent(self, event): + """绘制旋转的圆弧""" + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + # 计算圆的位置 + rect = QRect(self.line_width, self.line_width, + self._size - 2*self.line_width, + self._size - 2*self.line_width) + + # 设置画笔 + pen = QPen() + pen.setWidth(self.line_width) + pen.setCapStyle(Qt.RoundCap) + + # 绘制渐变圆弧 + segments = 8 + for i in range(segments): + # 计算每个段的透明度 + alpha = int(255 * (1 - i / segments)) + # 使用更鲜艳的蓝色 + pen.setColor(QColor(0, 122, 255, alpha)) + painter.setPen(pen) + + # 计算每个段的起始角度和跨度 + start_angle = (self.angle - i * (360 // segments)) % 360 + span = 30 # 每个段的跨度 + + # 转换角度为16分之一度(Qt的角度单位) + start_angle_16 = start_angle * 16 + span_16 = span * 16 + + painter.drawArc(rect, -start_angle_16, -span_16) + + def rotate(self): + """更新旋转角度""" + self.angle = (self.angle + self._speed) % 360 + self.update() + + @pyqtProperty(int) + def rotation(self): + return self.angle + + @rotation.setter + def rotation(self, angle): + self.angle = angle + self.update() + + def start(self): + """开始动画""" + self.animation.start() + self.timer.start() + + def stop(self): + """停止动画""" + self.animation.stop() + self.timer.stop() + +class LoadingDialog(QDialog): + """加载对话框组件""" + def __init__(self, parent=None, message="请稍候..."): + super().__init__(parent) + self.setWindowTitle("处理中") + self.setFixedSize(300, 120) + self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) + + layout = QVBoxLayout() + layout.setSpacing(15) + + # 消息标签 + self.message_label = QLabel(message) + self.message_label.setAlignment(Qt.AlignCenter) + self.message_label.setStyleSheet(""" + color: #0d6efd; + font-size: 14px; + font-weight: bold; + padding: 10px; + """) + layout.addWidget(self.message_label) + + # 加载动画 + self.spinner = LoadingSpinner(self, size=48, line_width=4) # 增大尺寸 + spinner_layout = QHBoxLayout() + spinner_layout.addStretch() + spinner_layout.addWidget(self.spinner) + spinner_layout.addStretch() + layout.addLayout(spinner_layout) + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + QDialog { + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #dee2e6; + } + """) + + def showEvent(self, event): + """显示时启动动画""" + super().showEvent(event) + self.spinner.start() + + def hideEvent(self, event): + """隐藏时停止动画""" + self.spinner.stop() + super().hideEvent(event) + + def set_message(self, message: str): + """更新消息文本""" + self.message_label.setText(message) diff --git a/gui/components/workers.py b/gui/components/workers.py new file mode 100644 index 0000000..49fb0b1 --- /dev/null +++ b/gui/components/workers.py @@ -0,0 +1,100 @@ +from services.cursor_service import CursorService +from common_utils import get_hardware_id +from exit_cursor import ExitCursor +from PyQt5.QtCore import QThread, pyqtSignal + +class BaseWorker(QThread): + """基础工作线程类""" + progress = pyqtSignal(dict) # 进度信号 + finished = pyqtSignal(tuple) # 完成信号 + error = pyqtSignal(str) # 错误信号 + + def __init__(self): + super().__init__() + self._is_running = True + + def stop(self): + """停止线程""" + self._is_running = False + self.wait() + +class RefreshTokenWorker(BaseWorker): + """刷新 Cursor Token 工作线程""" + def run(self): + try: + service = CursorService() + machine_id = get_hardware_id() + + # 更新进度 - 退出 Cursor + self.progress.emit({ + "status": "exiting", + "message": "正在关闭 Cursor 进程..." + }) + + # 退出 Cursor + if not ExitCursor(): + raise Exception("无法完全关闭 Cursor 进程,请手动关闭后重试") + + # 更新进度 - 开始刷新 + self.progress.emit({ + "status": "refreshing", + "message": "正在刷新 Cursor 授权..." + }) + + # 调用刷新服务 + success, msg, account_info = service.refresh_account(machine_id) + + if success and account_info: + # 更新进度 - 开始重置机器码 + self.progress.emit({ + "status": "resetting", + "message": "正在重置机器码..." + }) + + # 调用重置机器码 + reset_success, reset_msg, reset_data = service.reset_machine() + + if reset_success: + # 更新进度 + self.progress.emit({ + "status": "success", + "message": f"授权刷新成功: {account_info.get('email')}" + }) + + result_msg = ( + f"授权刷新成功\n" + f"账号: {account_info.get('email')}\n" + f"到期时间: {account_info.get('expire_time')}\n" + f"剩余天数: {account_info.get('days_left')}天\n\n" + f"机器码重置成功\n" + f"请重新启动 Cursor 编辑器" + ) + else: + result_msg = f"授权刷新成功,但机器码重置失败: {reset_msg}" + else: + result_msg = f"授权刷新失败: {msg}" + + self.finished.emit(('refresh', (success, result_msg))) + + except Exception as e: + self.error.emit(str(e)) + +class ResetWorker(BaseWorker): + """重置机器码工作线程""" + def run(self): + try: + service = CursorService() + success, msg, data = service.reset_machine() + self.finished.emit(('reset', (success, msg, data))) + except Exception as e: + self.error.emit(str(e)) + +class DisableWorker(BaseWorker): + """禁用更新工作线程""" + def run(self): + try: + service = CursorService() + success, msg = service.disable_update() + self.finished.emit(('disable', (success, msg))) + except Exception as e: + self.error.emit(str(e)) \ No newline at end of file diff --git a/gui/windows/__init__.py b/gui/windows/__init__.py new file mode 100644 index 0000000..65076f6 --- /dev/null +++ b/gui/windows/__init__.py @@ -0,0 +1 @@ +"""GUI 窗口包""" \ No newline at end of file diff --git a/gui/windows/__pycache__/__init__.cpython-312.pyc b/gui/windows/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1699b34 Binary files /dev/null and b/gui/windows/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/windows/__pycache__/main_window.cpython-312.pyc b/gui/windows/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000..65121f1 Binary files /dev/null and b/gui/windows/__pycache__/main_window.cpython-312.pyc differ diff --git a/gui/windows/main_window.py b/gui/windows/main_window.py new file mode 100644 index 0000000..f6ba775 --- /dev/null +++ b/gui/windows/main_window.py @@ -0,0 +1,667 @@ +import json +from typing import Dict +from PyQt5.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QMessageBox, QLabel, QLineEdit, QPushButton, + QFrame, QTextEdit, QDesktopWidget, QSystemTrayIcon, + QMenu, QAction +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QFont, QIcon, QDesktopServices +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QApplication +from pathlib import Path + +from services.cursor_service import CursorService +from gui.components.widgets import ( + LogWidget, StatusBar, ActionButton, + ActivationWidget, MemberStatusWidget, + ActivationStatusWidget, LoadingDialog +) +from gui.components.workers import ResetWorker, DisableWorker, RefreshTokenWorker +from common_utils import get_hardware_id +from utils.version_manager import VersionManager + +class DeviceIdWidget(QFrame): + """设备识别码显示组件""" + def __init__(self, device_id: str, parent=None): + super().__init__(parent) + self.setup_ui(device_id) + + def setup_ui(self, device_id: str): + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) # 移除边距 + + # 标签 + label = QLabel("设备识别码(勿动):") + label.setStyleSheet("color: #dc3545; font-weight: bold;") # 红色警示 + layout.addWidget(label) + + # 显示设备ID的文本框 + self.id_input = QLineEdit(device_id) + self.id_input.setReadOnly(True) + layout.addWidget(self.id_input) + + # 复制按钮 + copy_btn = QPushButton("复制ID") + copy_btn.clicked.connect(self.copy_device_id) + layout.addWidget(copy_btn) + + self.setStyleSheet(""" + QLineEdit { + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 5px; + color: #495057; + } + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 5px 10px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #5a6268; + } + """) + + def copy_device_id(self): + """复制设备ID到剪贴板""" + self.id_input.selectAll() + self.id_input.copy() + QMessageBox.information(self, "成功", "设备ID已复制到剪贴板") + +class InstructionsWidget(QFrame): + """使用步骤说明组件""" + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 10) # 移除边距 + + # 标题 + title = QLabel("使用步骤说明:") + title.setStyleSheet("color: #17a2b8; font-size: 14px; font-weight: bold;") # 青色标题 + layout.addWidget(title) + + # 步骤说明 + steps = [ + ("第一步", "输入激活码点击【激活】按钮完成激活"), + ("第二步", "点击【刷新Cursor编辑器授权】,一般情况下刷新即可正常使用"), + ("如果无法对话", "点击【实现Cursor0.45.x限制】,然后重新刷新授权"), + ("建议操作", "点击【禁用Cursor版本更新】,保持软件稳定运行") + ] + + for step_title, step_content in steps: + step_label = QLabel(f"{step_title}:{step_content}") + step_label.setWordWrap(True) + step_label.setStyleSheet(""" + QLabel { + color: #495057; + margin: 3px 0; + } + """) + layout.addWidget(step_label) + + # 给标题部分添加颜色 + text = step_label.text() + step_label.setText(f'{step_title}:{step_content}') + +class MainWindow(QMainWindow): + """主窗口""" + def __init__(self): + super().__init__() + self.cursor_service = CursorService() + self.version_manager = VersionManager() + self.current_worker = None + self.loading_dialog = None + + # 初始化托盘图标 + self.setup_tray() + + self.setup_ui() + + # 移除直接自检 + # self.auto_check() + + def setup_tray(self): + """初始化托盘图标""" + # 创建托盘图标 + self.tray_icon = QSystemTrayIcon(self) + + # 设置图标 + icon_path = Path(__file__).parent.parent.parent / "two.ico" + if icon_path.exists(): + self.tray_icon.setIcon(QIcon(str(icon_path))) + + # 创建托盘菜单 + self.tray_menu = QMenu() + + # 显示主窗口动作 + show_action = QAction("显示主窗口", self) + show_action.triggered.connect(self.show_main_window) + self.tray_menu.addAction(show_action) + + # 刷新授权动作 + refresh_action = QAction("刷新 Cursor 编辑器授权", self) + refresh_action.triggered.connect(lambda: self.start_task('refresh')) + self.tray_menu.addAction(refresh_action) + + # 退出动作 + quit_action = QAction("退出", self) + quit_action.triggered.connect(self.quit_application) + self.tray_menu.addAction(quit_action) + + # 设置托盘菜单 + self.tray_icon.setContextMenu(self.tray_menu) + + # 托盘图标双击事件 + self.tray_icon.activated.connect(self.tray_icon_activated) + + # 显示托盘图标 + self.tray_icon.show() + + def show_main_window(self): + """显示主窗口""" + self.show() + self.activateWindow() + + def quit_application(self): + """退出应用程序""" + # 停止所有任务 + if self.current_worker: + self.current_worker.stop() + # 移除托盘图标 + self.tray_icon.setVisible(False) + # 退出应用 + QApplication.quit() + + def tray_icon_activated(self, reason): + """托盘图标激活事件""" + if reason == QSystemTrayIcon.DoubleClick: + # 双击显示主窗口 + self.show_main_window() + + def showEvent(self, event): + """窗口显示事件""" + super().showEvent(event) + # 在窗口显示后执行自检 + QTimer.singleShot(500, self.auto_check) # 延迟500ms执行,确保界面完全显示 + + def show_loading(self, message: str): + """显示加载对话框""" + if not self.loading_dialog: + self.loading_dialog = LoadingDialog(self) + self.loading_dialog.set_message(message) + self.loading_dialog.show() + QApplication.processEvents() # 确保对话框立即显示 + + def hide_loading(self): + """隐藏加载对话框""" + if self.loading_dialog: + self.loading_dialog.hide() + + def auto_check(self): + """启动时自检""" + try: + # 1. 检查更新 + has_update, msg, update_info = self.version_manager.check_update() + + if has_update and update_info: + reply = QMessageBox.question( + self, + "发现新版本", + f"发现新版本: {update_info['version']}\n\n" + f"更新内容:\n{update_info['message']}\n\n" + "是否立即下载更新?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + QDesktopServices.openUrl(QUrl(update_info['download_url'])) + + # 2. 检查激活状态 + self.update_member_status() + + # 3. 恢复状态栏 + self.status_bar.set_status("就绪") + + except Exception as e: + self.status_bar.set_status("自检过程出现错误") + self.logger.error(f"自检失败: {str(e)}") + + def setup_ui(self): + """初始化UI""" + self.setWindowTitle("听泉助手 v4.0.0.1") + self.setMinimumSize(600, 500) + + # 设置窗口图标 + icon_path = Path(__file__).parent.parent.parent / "two.ico" + if icon_path.exists(): + self.setWindowIcon(QIcon(str(icon_path))) + + # 设置窗口背景为白色 + self.setStyleSheet(""" + QMainWindow { + background-color: white; + } + QWidget { + background-color: white; + } + """) + + # 创建中心部件和布局 + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + layout.setSpacing(15) + layout.setContentsMargins(15, 15, 15, 15) + + # 设备识别码区域 + device_id = get_hardware_id() # 使用common_utils中的函数 + self.device_id_widget = DeviceIdWidget(device_id) + layout.addWidget(self.device_id_widget) + + # 会员状态区域 + self.member_status = MemberStatusWidget() + layout.addWidget(self.member_status) + + # 激活区域 + self.activation_widget = ActivationWidget(self) # 使用 self 作为父组件 + layout.addWidget(self.activation_widget) + + # 使用说明区域 + self.instructions = InstructionsWidget() + layout.addWidget(self.instructions) + + # 功能按钮区域 + button_layout = QVBoxLayout() + button_layout.setSpacing(10) + + # 刷新授权按钮 - 蓝色 + self.refresh_button = QPushButton("刷新 Cursor 编辑器授权") + self.refresh_button.clicked.connect(lambda: self.start_task('refresh')) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #007bff; + color: white; + border: none; + padding: 12px; + border-radius: 4px; + font-size: 14px; + } + QPushButton:hover { + background-color: #0056b3; + } + QPushButton:disabled { + background-color: #ccc; + } + """) + button_layout.addWidget(self.refresh_button) + + # 实现限制按钮 - 绿色 + self.limit_button = QPushButton("实现 Cursor 0.45.x 限制") + self.limit_button.clicked.connect(lambda: self.start_task('limit')) + self.limit_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 12px; + border-radius: 4px; + font-size: 14px; + } + QPushButton:hover { + background-color: #218838; + } + QPushButton:disabled { + background-color: #ccc; + } + """) + button_layout.addWidget(self.limit_button) + + # 禁用更新按钮 - 红色 + self.disable_button = QPushButton("禁用 Cursor 版本更新") + self.disable_button.clicked.connect(lambda: self.start_task('disable')) + self.disable_button.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border: none; + padding: 12px; + border-radius: 4px; + font-size: 14px; + } + QPushButton:hover { + background-color: #c82333; + } + QPushButton:disabled { + background-color: #ccc; + } + """) + button_layout.addWidget(self.disable_button) + + layout.addLayout(button_layout) + + # 检查更新按钮 - 灰色 + self.check_update_button = QPushButton("检查更新") + self.check_update_button.clicked.connect(self.check_update) + self.check_update_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px; + border-radius: 4px; + margin-top: 5px; + font-size: 13px; + min-width: 100px; + } + QPushButton:hover { + background-color: #5a6268; + } + QPushButton:disabled { + background-color: #cccccc; + } + """) + layout.addWidget(self.check_update_button) + + # 状态栏 + self.status_bar = StatusBar() + layout.addWidget(self.status_bar) + + # 初始化状态 + self.update_member_status() + + # 检查是否有未完成的更新 + self.check_pending_update() + + def update_member_status(self): + """更新会员状态""" + status = self.cursor_service.check_activation_status() + self.member_status.update_status(status) + + # 根据激活状态更新按钮可用性 + buttons_enabled = status["is_activated"] + self.refresh_button.setEnabled(buttons_enabled) + self.limit_button.setEnabled(buttons_enabled) + self.disable_button.setEnabled(buttons_enabled) + + def handle_activation(self, code: str): + """处理激活码激活""" + try: + print(f"MainWindow 收到激活请求,激活码:{code}") + + # 更新状态栏 + print("更新状态栏:正在激活...") + self.status_bar.set_status("正在激活...") + + # 禁用激活按钮 + print("禁用激活按钮") + self.activation_widget.activate_btn.setEnabled(False) + + # 调用激活服务 + print("调用激活服务...") + success, msg = self.cursor_service.activate_with_code(code) + print(f"激活结果:success={success}, msg={msg}") + + if success: + QMessageBox.information(self, "成功", msg) + self.update_member_status() + else: + QMessageBox.warning(self, "失败", msg) + + # 清空输入框 + self.activation_widget.clear_input() + + except Exception as e: + print(f"激活过程发生错误:{str(e)}") + QMessageBox.critical(self, "错误", f"激活过程发生错误:{str(e)}") + finally: + # 恢复状态栏 + print("恢复状态栏和按钮状态") + self.status_bar.set_status("就绪") + # 恢复激活按钮 + self.activation_widget.activate_btn.setEnabled(True) + + def start_task(self, task_type: str): + """启动任务""" + # 检查激活状态 + if not self.cursor_service.check_activation_status()["is_activated"]: + self.activation_widget.show_activation_required() + return + + # 停止当前任务(如果有) + if self.current_worker: + self.current_worker.stop() + + # 禁用所有按钮 + self.refresh_button.setEnabled(False) + self.limit_button.setEnabled(False) + self.disable_button.setEnabled(False) + + # 创建并启动工作线程 + if task_type == 'refresh': + self.status_bar.set_status("正在刷新授权...") + worker = RefreshTokenWorker() + worker.progress.connect(self.update_progress) + worker.finished.connect(self.handle_result) + worker.error.connect(self.handle_error) + worker.start() + self.current_worker = worker + elif task_type == 'limit': + self.status_bar.set_status("正在设置版本限制...") + worker = ResetWorker() + worker.progress.connect(self.update_progress) + worker.finished.connect(self.handle_result) + worker.error.connect(self.handle_error) + worker.start() + self.current_worker = worker + elif task_type == 'disable': + self.status_bar.set_status("正在禁用更新...") + worker = DisableWorker() + worker.progress.connect(self.update_progress) + worker.finished.connect(self.handle_result) + worker.error.connect(self.handle_error) + worker.start() + self.current_worker = worker + + def update_progress(self, info: dict): + """更新进度信息""" + self.status_bar.set_status(info.get('message', '处理中...')) + + def handle_result(self, result: tuple): + """处理任务结果""" + task_type, data = result + success, msg = data if isinstance(data, tuple) else (False, str(data)) + + if success: + QMessageBox.information(self, "成功", msg) + else: + QMessageBox.warning(self, "失败", msg) + + # 重新启用按钮 + self.update_member_status() + self.status_bar.set_status("就绪") + + # 清理工作线程 + if self.current_worker: + self.current_worker.stop() + self.current_worker = None + + def handle_error(self, error_msg: str): + """处理错误""" + self.status_bar.set_status("发生错误") + QMessageBox.critical(self, "错误", f"操作失败:{error_msg}") + + # 重新启用按钮 + self.update_member_status() + + # 清理工作线程 + if self.current_worker: + self.current_worker.stop() + self.current_worker = None + + def check_update(self): + """手动检查更新""" + try: + # 禁用更新按钮 + self.check_update_button.setEnabled(False) + self.status_bar.set_status("正在检查更新...") + + # 检查更新 + has_update, msg, update_info = self.version_manager.check_update() + + if has_update and update_info: + # 创建自定义消息框 + msg_box = QMessageBox(self) + msg_box.setWindowTitle("发现新版本") + msg_box.setIcon(QMessageBox.Information) + + # 设置文本 + text = ( + f"

发现新版本: {update_info['version']}

" + f"

更新内容:

" + f"

{update_info['message']}

" + ) + msg_box.setText(text) + + # 设置按钮 + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + yes_btn = msg_box.button(QMessageBox.Yes) + no_btn = msg_box.button(QMessageBox.No) + yes_btn.setText("立即下载") + no_btn.setText("暂不更新") + + # 设置样式 + msg_box.setStyleSheet(""" + QMessageBox { + background-color: white; + } + QPushButton { + min-width: 85px; + min-height: 24px; + padding: 4px 15px; + border-radius: 3px; + background-color: #007bff; + color: white; + border: none; + } + QPushButton:hover { + background-color: #0056b3; + } + QPushButton[text='暂不更新'] { + background-color: #6c757d; + } + QPushButton[text='暂不更新']:hover { + background-color: #5a6268; + } + """) + + # 设置最小宽度 + msg_box.setMinimumWidth(400) + + # 显示对话框 + if msg_box.exec_() == QMessageBox.Yes: + QDesktopServices.openUrl(QUrl(update_info['download_url'])) + else: + # 创建自定义消息框 + msg_box = QMessageBox(self) + msg_box.setWindowTitle("检查更新") + msg_box.setIcon(QMessageBox.Information) + msg_box.setText(f"

{msg}

") + msg_box.setStyleSheet(""" + QMessageBox { + background-color: white; + } + QPushButton { + min-width: 85px; + min-height: 24px; + padding: 4px 15px; + border-radius: 3px; + background-color: #007bff; + color: white; + border: none; + } + QPushButton:hover { + background-color: #0056b3; + } + """) + msg_box.setMinimumWidth(300) + msg_box.exec_() + + except Exception as e: + error_box = QMessageBox(self) + error_box.setWindowTitle("错误") + error_box.setIcon(QMessageBox.Warning) + error_box.setText(f"

检查更新失败: {str(e)}

") + error_box.setStyleSheet(""" + QMessageBox { + background-color: white; + } + QPushButton { + min-width: 85px; + min-height: 24px; + padding: 4px 15px; + border-radius: 3px; + background-color: #dc3545; + color: white; + border: none; + } + QPushButton:hover { + background-color: #c82333; + } + """) + error_box.setMinimumWidth(300) + error_box.exec_() + + finally: + # 恢复按钮状态和状态栏 + self.check_update_button.setEnabled(True) + self.status_bar.set_status("就绪") + + def check_pending_update(self): + """检查是否有未完成的更新""" + try: + update_info = self.version_manager.get_last_update_info() + if update_info: + reply = QMessageBox.question( + self, + "未完成的更新", + f"检测到未完成的更新: {update_info['version']}\n\n" + f"更新内容:\n{update_info['message']}\n\n" + "是否现在更新?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + # 打开下载链接 + QDesktopServices.openUrl(QUrl(update_info['download_url'])) + else: + # 清除更新信息 + self.version_manager.clear_update_info() + except Exception as e: + self.logger.error(f"检查未完成更新失败: {str(e)}") + + def closeEvent(self, event): + """窗口关闭事件""" + if self.tray_icon.isVisible(): + # 最小化到托盘 + QMessageBox.information( + self, + "提示", + "程序将继续在后台运行,双击托盘图标可以重新打开主窗口。" + ) + self.hide() + event.ignore() + else: + # 停止所有正在运行的任务 + if self.current_worker: + self.current_worker.stop() + event.accept() \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..5ca895a --- /dev/null +++ b/logger.py @@ -0,0 +1,67 @@ +import os +import logging +from datetime import datetime +from typing import Optional + +class Logger: + """统一的日志管理类""" + + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + # 创建日志目录 + self.log_dir = os.path.join(os.getenv('APPDATA'), 'Cursor', 'logs') + os.makedirs(self.log_dir, exist_ok=True) + + # 配置根日志记录器 + self.logger = logging.getLogger('CursorTools') + self.logger.setLevel(logging.INFO) + + # 日志格式 + self.formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # 添加处理器 + self._add_handlers() + + self._initialized = True + + def _add_handlers(self): + """添加日志处理器""" + if not self.logger.handlers: + # 文件处理器 + log_file = os.path.join( + self.log_dir, + f'cursor_tools_{datetime.now().strftime("%Y%m%d")}.log' + ) + fh = logging.FileHandler(log_file, encoding='utf-8') + fh.setLevel(logging.INFO) + fh.setFormatter(self.formatter) + self.logger.addHandler(fh) + + # 控制台处理器 + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + ch.setFormatter(self.formatter) + self.logger.addHandler(ch) + + def get_logger(self, name: Optional[str] = None) -> logging.Logger: + """ + 获取日志记录器 + :param name: 日志记录器名称 + :return: 日志记录器实例 + """ + if name: + return self.logger.getChild(name) + return self.logger + +# 创建全局日志管理器实例 +logger = Logger() \ No newline at end of file diff --git a/machine_resetter.py b/machine_resetter.py new file mode 100644 index 0000000..9c5734b --- /dev/null +++ b/machine_resetter.py @@ -0,0 +1,197 @@ +import os +import sys +import sqlite3 +import requests +import urllib3 +import uuid +import winreg +import ctypes +import shutil +import json +from datetime import datetime +from typing import Dict, Tuple, Any, Optional +from logger import logger +from exit_cursor import ExitCursor + +class MachineResetter: + """机器码重置核心类""" + + def __init__(self, storage_file: str = None, backup_dir: str = None): + """ + 初始化机器码重置器 + :param storage_file: 可选,storage.json文件路径 + :param backup_dir: 可选,备份目录路径 + """ + # 设置基础路径 + appdata = os.getenv('APPDATA') + if not appdata: + raise EnvironmentError("APPDATA 环境变量未设置") + + self.storage_file = storage_file or os.path.join(appdata, 'Cursor', 'User', 'globalStorage', 'storage.json') + self.backup_dir = backup_dir or os.path.join(os.path.dirname(self.storage_file), 'backups') + + # 确保备份目录存在 + os.makedirs(self.backup_dir, exist_ok=True) + + # 获取日志记录器 + self.logger = logger.get_logger("MachineReset") + + # 进度回调 + self._callback = None + + def set_progress_callback(self, callback) -> None: + """设置进度回调函数""" + self._callback = callback + + def _update_progress(self, status: str, message: str) -> None: + """更新进度信息""" + if self._callback: + self._callback({"status": status, "message": message}) + + @staticmethod + def is_admin() -> bool: + """检查是否具有管理员权限""" + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except: + return False + + def _get_storage_content(self) -> Dict[str, Any]: + """读取storage.json的内容""" + try: + with open(self.storage_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + raise Exception(f"读取配置文件失败: {str(e)}") + + def _save_storage_content(self, content: Dict[str, Any]) -> None: + """保存内容到storage.json""" + try: + with open(self.storage_file, 'w', encoding='utf-8') as f: + json.dump(content, f, indent=2, ensure_ascii=False) + except Exception as e: + raise Exception(f"保存配置文件失败: {str(e)}") + + def backup_file(self) -> str: + """ + 备份配置文件 + :return: 备份文件路径 + """ + if not os.path.exists(self.storage_file): + raise FileNotFoundError(f"配置文件不存在:{self.storage_file}") + + backup_name = f"storage.json.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + backup_path = os.path.join(self.backup_dir, backup_name) + shutil.copy2(self.storage_file, backup_path) + self.logger.info(f"配置已备份到: {backup_path}") + return backup_path + + def generate_new_ids(self) -> Dict[str, str]: + """ + 生成新的机器码系列 + :return: 包含新ID的字典 + """ + # 生成 auth0|user_ 前缀的十六进制 + prefix = "auth0|user_".encode('utf-8').hex() + # 生成32字节(64个十六进制字符)的随机数 + random_part = uuid.uuid4().hex + uuid.uuid4().hex + + return { + "machineId": f"{prefix}{random_part}", + "macMachineId": str(uuid.uuid4()), + "devDeviceId": str(uuid.uuid4()), + "sqmId": "{" + str(uuid.uuid4()).upper() + "}" + } + + def update_config(self, new_ids: Dict[str, str]) -> None: + """ + 更新配置文件 + :param new_ids: 新的ID字典 + """ + config_content = self._get_storage_content() + + # 更新配置 + config_content['telemetry.machineId'] = new_ids['machineId'] + config_content['telemetry.macMachineId'] = new_ids['macMachineId'] + config_content['telemetry.devDeviceId'] = new_ids['devDeviceId'] + config_content['telemetry.sqmId'] = new_ids['sqmId'] + + # 保存更新后的配置 + self._save_storage_content(config_content) + self.logger.info("配置文件更新成功") + + def update_machine_guid(self) -> str: + """ + 更新注册表中的MachineGuid + :return: 新的GUID + """ + reg_path = r"SOFTWARE\Microsoft\Cryptography" + try: + # 打开注册表键 + reg_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, + winreg.KEY_WRITE | winreg.KEY_READ) + + # 获取当前GUID用于备份 + current_guid, _ = winreg.QueryValueEx(reg_key, "MachineGuid") + self.logger.info(f"当前MachineGuid: {current_guid}") + + # 生成新GUID + new_guid = str(uuid.uuid4()) + + # 更新注册表 + winreg.SetValueEx(reg_key, "MachineGuid", 0, winreg.REG_SZ, new_guid) + + # 验证更改 + updated_guid, _ = winreg.QueryValueEx(reg_key, "MachineGuid") + if updated_guid != new_guid: + raise Exception("注册表验证失败,值未成功更新") + + winreg.CloseKey(reg_key) + self.logger.info(f"新MachineGuid: {new_guid}") + return new_guid + + except Exception as e: + raise Exception(f"更新MachineGuid失败: {str(e)}") + + def run(self) -> Tuple[Dict[str, str], str]: + """ + 执行重置操作 + :return: (新ID字典, 新GUID) + """ + try: + # 检查管理员权限 + self._update_progress("checking", "检查管理员权限...") + if not self.is_admin(): + raise PermissionError("必须以管理员权限运行该程序") + + # 先退出 Cursor 进程 + self._update_progress("exiting", "正在关闭 Cursor 进程...") + if not ExitCursor(): + raise Exception("无法完全关闭 Cursor 进程,请手动关闭后重试") + + # 备份配置文件 + self._update_progress("backup", "备份配置文件...") + backup_path = self.backup_file() + self.logger.info(f"配置已备份到: {backup_path}") + + # 生成新机器码 + self._update_progress("generating", "生成新的机器码...") + new_ids = self.generate_new_ids() + + # 更新配置文件 + self._update_progress("updating", "更新配置文件...") + self.update_config(new_ids) + + # 更新注册表 + self._update_progress("registry", "更新注册表...") + new_guid = self.update_machine_guid() + + self._update_progress("complete", "重置完成") + self.logger.info("机器码重置成功") + + return new_ids, new_guid + + except Exception as e: + self.logger.error(f"重置失败: {str(e)}") + self._update_progress("error", f"操作失败: {str(e)}") + raise \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..2397053 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ +"""服务层包""" \ No newline at end of file diff --git a/services/__pycache__/__init__.cpython-312.pyc b/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9967b78 Binary files /dev/null and b/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/services/__pycache__/cursor_service.cpython-312.pyc b/services/__pycache__/cursor_service.cpython-312.pyc new file mode 100644 index 0000000..f42a2d8 Binary files /dev/null and b/services/__pycache__/cursor_service.cpython-312.pyc differ diff --git a/services/cursor_service.py b/services/cursor_service.py new file mode 100644 index 0000000..cb6a196 --- /dev/null +++ b/services/cursor_service.py @@ -0,0 +1,237 @@ +"""Cursor 服务管理类""" + +import os +import json +import hashlib +from datetime import datetime, timedelta +from typing import Dict, Optional, Tuple +import urllib3 +import requests + +from cursor_token_refresher import CursorTokenRefresher +from machine_resetter import MachineResetter +from update_disabler import UpdateDisabler +from logger import logger +from common_utils import ( + get_hardware_id, + get_user_state, + check_user_state, + refresh_user_state, + update_user_state, + activate_device +) + +class CursorService: + """Cursor 服务管理类""" + _instance = None + _initialized = False + _last_check_time = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + # 基础组件初始化 + self.token_refresher = CursorTokenRefresher() + self.machine_resetter = MachineResetter() + self.update_disabler = UpdateDisabler() + self.logger = logger.get_logger("CursorService") + + # 配置文件路径 + self.config_dir = os.path.join(os.getenv('APPDATA'), 'TingquanAssistant') + self.config_file = os.path.join(self.config_dir, 'config.json') + + # 确保配置目录存在 + os.makedirs(self.config_dir, exist_ok=True) + + # 初始化配置 + self._init_config() + + # 首次初始化用户状态 + self._lazy_init_user_state() + + self._initialized = True + + def _init_config(self): + """初始化配置文件""" + # 默认配置 + self.default_config = { + "version": "4.0.0.1", + "last_check_update": None, + "auto_check_update": True, + "theme": "light" + } + + # 读取或创建配置文件 + if not os.path.exists(self.config_file): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.default_config, f, indent=2, ensure_ascii=False) + self.config = self.default_config + else: + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + self.config = json.load(f) + except Exception as e: + self.logger.error(f"读取配置文件失败: {e}") + self.config = self.default_config + + def _lazy_init_user_state(self): + """ + 懒加载方式初始化用户状态 + 只在首次使用时初始化,避免频繁刷新 + """ + try: + if not self._last_check_time: + machine_id = get_hardware_id() + success, msg, state_data = refresh_user_state(machine_id) + if success: + self._last_check_time = datetime.now() + self.logger.info("用户状态初始化成功") + else: + self.logger.warning(f"用户状态初始化失败: {msg}") + except Exception as e: + self.logger.error(f"初始化用户状态异常: {e}") + + def _should_refresh_state(self) -> bool: + """ + 检查是否需要刷新状态 + 每5分钟检测一次设备状态 + + Returns: + bool: 是否需要刷新状态 + """ + try: + # 如果没有上次检查时间,需要刷新 + if not self._last_check_time: + return True + + # 计算距离上次检查的时间 + now = datetime.now() + time_diff = now - self._last_check_time + + # 如果超过5分钟,需要刷新 + if time_diff.total_seconds() >= 300: # 5分钟 = 300秒 + self.logger.debug("距离上次状态检查已超过5分钟") + return True + + return False + + except Exception as e: + self.logger.error(f"检查状态刷新时间异常: {e}") + # 发生异常时返回False,避免频繁刷新 + return False + + def check_activation_status(self) -> Dict: + """ + 检查当前激活状态 + 如果需要刷新则自动刷新状态 + + Returns: + Dict: 状态信息字典 + """ + try: + # 检查是否需要刷新状态 + if self._should_refresh_state(): + machine_id = get_hardware_id() + success, msg, state_data = refresh_user_state(machine_id) + if success: + self._last_check_time = datetime.now() + self.logger.info("设备状态检查完成") + else: + self.logger.warning(f"设备状态检查失败: {msg}") + except Exception as e: + self.logger.error(f"设备状态检查异常: {e}") + + # 无论是否刷新成功,都返回当前状态 + return get_user_state() + + def activate_with_code(self, activation_code: str) -> Tuple[bool, str]: + """ + 使用激活码激活 + + Args: + activation_code: 激活码 + + Returns: + Tuple[bool, str]: (是否成功, 消息) + """ + try: + self.logger.info(f"开始处理激活码: {activation_code}") + + # 获取机器ID + machine_id = get_hardware_id() + if not machine_id: + self.logger.error("获取机器ID失败") + return False, "获取机器ID失败" + + self.logger.info(f"获取到机器ID: {machine_id}") + + # 调用激活接口 + self.logger.info("正在调用激活接口...") + success, msg, data = activate_device(machine_id, activation_code) + + if success: + self.logger.info(f"激活成功: {msg}") + else: + self.logger.warning(f"激活失败: {msg}") + + return success, msg + + except Exception as e: + error_msg = f"激活失败: {str(e)}" + self.logger.error(error_msg) + return False, error_msg + + def refresh_account(self, machine_id: str) -> Tuple[bool, str, Optional[Dict]]: + """刷新账号""" + # 检查是否已激活 + if not check_user_state(): + return False, "请先激活软件", None + return self.token_refresher.refresh_token(machine_id) + + def reset_machine(self) -> Tuple[bool, str, Dict]: + """重置机器码""" + # 检查是否已激活 + if not check_user_state(): + return False, "请先激活软件", {} + + try: + new_ids, new_guid = self.machine_resetter.run() + return True, "重置成功", { + "new_ids": new_ids, + "new_guid": new_guid + } + except Exception as e: + return False, f"重置失败: {str(e)}", {} + + def disable_update(self) -> Tuple[bool, str]: + """禁用更新""" + # 检查是否已激活 + if not check_user_state(): + return False, "请先激活软件" + + success = self.update_disabler.disable() + return success, "禁用成功" if success else "禁用失败" + + def save_config(self, new_config: Dict) -> bool: + """ + 保存配置 + :param new_config: 新的配置信息 + :return: 是否成功 + """ + try: + self.config.update(new_config) + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.config, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + self.logger.error(f"保存配置失败: {e}") + return False + + def get_config(self) -> Dict: + """获取当前配置""" + return self.config.copy() + diff --git a/tingquan_assistant.py b/tingquan_assistant.py new file mode 100644 index 0000000..7e74185 --- /dev/null +++ b/tingquan_assistant.py @@ -0,0 +1,91 @@ +""" +听泉助手 - Cursor 激活管理工具 + +项目结构说明: +--------------- +├── services/ # 服务层 - 处理核心业务逻辑 +│ ├── cursor_service.py # Cursor服务管理类 - 处理激活、配置等核心功能 +│ └── __init__.py +│ +├── gui/ # GUI层 - 处理界面相关逻辑 +│ ├── components/ # 可复用的GUI组件 +│ │ ├── widgets.py # 基础UI组件(按钮、状态栏等) +│ │ ├── workers.py # 后台工作线程 +│ │ └── __init__.py +│ ├── windows/ # 窗口类 +│ │ ├── main_window.py # 主窗口实现 +│ │ └── __init__.py +│ └── __init__.py +│ +├── config.py # 配置管理 +├── logger.py # 日志管理 +├── machine_resetter.py # 机器码重置 +├── update_disabler.py # 更新禁用 +├── cursor_token_refresher.py # Token刷新 +└── tingquan_assistant.py # 程序入口文件 + +设计规范: +--------------- +1. 分层设计 + - services层: 处理核心业务逻辑,与界面解耦 + - gui层: 只处理界面相关逻辑,通过services层调用业务功能 + - 工具类: 独立的功能模块(如日志、配置等) + +2. 代码规范 + - 使用类型注解 + - 函数必须有文档字符串 + - 遵循PEP 8命名规范 + - 异常必须合理处理和记录日志 + +3. 界面设计 + - 使用PyQt5构建GUI + - 所有耗时操作必须在后台线程中执行 + - 界面组件需实现合理的状态管理 + +4. 配置管理 + - 用户配置存储在 %APPDATA%/TingquanAssistant/ + - 激活信息使用JSON格式存储 + - 配置文件需要权限控制 + +5. 日志规范 + - 所有关键操作必须记录日志 + - 日志按日期分文件存储 + - 包含足够的错误诊断信息 + +使用说明: +--------------- +1. 运行入口: python tingquan_assistant.py +2. 开发新功能流程: + - 在services层添加业务逻辑 + - 在gui/components添加必要的界面组件 + - 在gui/windows中整合界面 + - 更新配置和日志相关代码 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, current_dir) + +from PyQt5.QtWidgets import QApplication +from gui.windows.main_window import MainWindow + +def main(): + """程序入口""" + # 创建应用 + app = QApplication(sys.argv) + + # 设置应用样式 + app.setStyle('Fusion') + + # 创建主窗口 + window = MainWindow() + window.show() + + # 运行应用 + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tingquan_assistant.spec b/tingquan_assistant.spec new file mode 100644 index 0000000..ec06125 --- /dev/null +++ b/tingquan_assistant.spec @@ -0,0 +1,120 @@ +# -*- mode: python ; coding: utf-8 -*- +import os +import sys +from PyInstaller.utils.hooks import collect_data_files, collect_submodules + +# 读取版本号 +with open('version.txt', 'r') as f: + VERSION = f.read().strip() + +# 收集所有Python文件 +python_files = [] +for root, dirs, files in os.walk('.'): + if '__pycache__' in root or '.git' in root or 'venv' in root: + continue + for file in files: + if file.endswith('.py') and not file.startswith('test_'): + rel_path = os.path.relpath(os.path.join(root, file)) + python_files.append(rel_path) + +# 收集数据文件 +datas = [ + ('version.txt', '.'), + ('two.ico', '.'), + ('file_version_info.txt', '.'), + ('utils', 'utils'), + ('services', 'services'), + ('gui', 'gui'), +] + +# 添加其他资源文件 +if os.path.exists('requirements.txt'): + datas.append(('requirements.txt', '.')) + +a = Analysis( + ['tingquan_assistant.py'] + python_files, + pathex=[], + binaries=[], + datas=datas, + hiddenimports=[ + 'PyQt5', + 'PyQt5.QtCore', + 'PyQt5.QtGui', + 'PyQt5.QtWidgets', + 'requests', + 'json', + 'logging', + 'win32api', + 'win32con', + 'win32gui', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[ + '_tkinter', + 'tkinter', + 'PIL.ImageTk', + 'PIL.ImageWin', + 'numpy', + 'pandas', + 'matplotlib', + 'scipy', + 'PyQt5.QtWebEngineWidgets', + 'PyQt5.QtWebEngine', + ], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False, +) + +# 过滤掉不需要的二进制文件 +def remove_from_list(source, patterns): + for file in source[:]: + for pattern in patterns: + if pattern in file[0]: + source.remove(file) + break + +remove_from_list(a.binaries, [ + 'Qt5WebEngine', + 'Qt5WebEngineCore', + 'Qt5WebEngineWidgets', + 'Qt5Designer', + 'Qt5Qml', +]) + +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name=f'听泉助手v{VERSION}', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['two.ico'], + version='file_version_info.txt', +) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name=f'听泉助手v{VERSION}', +) diff --git a/two.ico b/two.ico new file mode 100644 index 0000000..fcd403e Binary files /dev/null and b/two.ico differ diff --git a/update_disabler.py b/update_disabler.py new file mode 100644 index 0000000..dbc83f1 --- /dev/null +++ b/update_disabler.py @@ -0,0 +1,151 @@ +import os +import sys +import sqlite3 +import requests +import urllib3 +import shutil +import subprocess +import stat +import getpass +import time +from datetime import datetime +from typing import Optional, Dict, Tuple +from logger import logger + +class UpdateDisabler: + """更新禁用模块""" + + def __init__(self, updater_path: str = None): + """ + 初始化更新禁用器 + :param updater_path: 可选,updater目录路径 + """ + # 设置基础路径 + localappdata = os.getenv('LOCALAPPDATA') + if not localappdata: + raise EnvironmentError("LOCALAPPDATA 环境变量未设置") + + self.updater_path = updater_path or os.path.join(localappdata, 'cursor-updater') + self._callback = None + self.logger = logger.get_logger("UpdateDisable") + + def set_progress_callback(self, callback) -> None: + """设置进度回调函数""" + self._callback = callback + + def _update_progress(self, status: str, message: str) -> None: + """更新进度信息""" + if self._callback: + self._callback({"status": status, "message": message}) + + def _ensure_parent_dir_exists(self): + """确保父目录存在""" + parent_dir = os.path.dirname(self.updater_path) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir, exist_ok=True) + + def disable(self) -> bool: + """ + 禁用自动更新 + :return: 是否成功 + """ + try: + self._update_progress("start", "开始禁用自动更新...") + + # 确保父目录存在 + self._ensure_parent_dir_exists() + + # 检查并删除现有文件或目录 + self._update_progress("checking", "检查现有文件...") + try: + if os.path.exists(self.updater_path): + self._update_progress("removing", "删除现有文件...") + if os.path.isfile(self.updater_path): + os.remove(self.updater_path) + else: + shutil.rmtree(self.updater_path, ignore_errors=True) + self.logger.info(f"已删除: {self.updater_path}") + else: + self.logger.info("目标路径不存在,将创建新文件") + except Exception as e: + self.logger.warning(f"删除操作异常: {str(e)}") + + # 等待文件系统操作完成 + time.sleep(1) + + # 创建阻止更新的文件 + self._update_progress("creating", "创建阻止文件...") + try: + # 再次确保路径不存在 + if os.path.exists(self.updater_path): + os.remove(self.updater_path) + + # 创建新文件 + with open(self.updater_path, 'w') as f: + f.write("") + self.logger.info("已创建阻止文件") + + # 验证文件是否成功创建 + if not os.path.isfile(self.updater_path): + raise Exception("文件创建验证失败") + + except Exception as e: + raise Exception(f"创建文件失败: {str(e)}") + + # 设置文件只读属性 + self._update_progress("setting", "设置文件属性...") + try: + os.chmod(self.updater_path, stat.S_IREAD) + except Exception as e: + self.logger.warning(f"设置只读属性失败: {str(e)}") + + # 设置文件权限 + self._update_progress("permission", "设置文件权限...") + try: + username = getpass.getuser() + result = subprocess.run( + ['icacls', self.updater_path, '/inheritance:r', '/grant:r', f"{username}:(R)"], + capture_output=True, + text=True + ) + + if result.returncode != 0: + self.logger.warning(f"设置权限返回值非0: {result.stderr}") + except Exception as e: + self.logger.warning(f"设置权限失败: {str(e)}") + + # 最终验证 + if not os.path.exists(self.updater_path): + raise Exception("最终验证失败:文件不存在") + + if os.path.isdir(self.updater_path): + raise Exception("最终验证失败:创建了目录而不是文件") + + self._update_progress("complete", "禁用自动更新完成") + self.logger.info("成功禁用自动更新") + return True + + except Exception as e: + self.logger.error(f"禁用自动更新失败: {str(e)}") + self._update_progress("error", f"操作失败: {str(e)}") + self.show_manual_guide() + return False + + def show_manual_guide(self) -> str: + """ + 显示手动操作指南 + :return: 指南内容 + """ + guide = ( + "自动禁用更新失败,请按以下步骤手动操作:\n" + "1. 以管理员身份打开 PowerShell\n" + "2. 执行以下命令:\n" + f" Remove-Item -Path \"{self.updater_path}\" -Force -Recurse -ErrorAction SilentlyContinue\n" + f" New-Item -Path \"{self.updater_path}\" -ItemType File -Force\n" + f" Set-ItemProperty -Path \"{self.updater_path}\" -Name IsReadOnly -Value $true\n" + f" icacls \"{self.updater_path}\" /inheritance:r /grant:r \"{getpass.getuser()}:(R)\"" + ) + + self.logger.warning("需要手动禁用更新") + self.logger.info(guide) + return guide \ No newline at end of file diff --git a/utils/__pycache__/version_manager.cpython-312.pyc b/utils/__pycache__/version_manager.cpython-312.pyc new file mode 100644 index 0000000..761dfe1 Binary files /dev/null and b/utils/__pycache__/version_manager.cpython-312.pyc differ diff --git a/utils/version_manager.py b/utils/version_manager.py new file mode 100644 index 0000000..3b0b29a --- /dev/null +++ b/utils/version_manager.py @@ -0,0 +1,227 @@ +"""版本管理模块 + +职责: +1. 检查软件更新 +2. 管理版本信息 +3. 处理更新下载 +4. 维护更新配置 + +设计原则: +1. 单一职责 +2. 配置与逻辑分离 +3. 异常处理标准化 +4. 日志完整记录 +""" + +import os +import sys +import json +import requests +import urllib3 +from pathlib import Path +from typing import Dict, Tuple, Optional +from logger import logger +from config import config + +class VersionConfig: + """版本配置类""" + + def __init__(self): + """初始化版本配置""" + self.update_url = f"{config.base_url}/admin/api.version/check" + self.config_dir = os.path.join(os.getenv('APPDATA'), 'TingquanAssistant') + self.config_file = os.path.join(self.config_dir, 'version.json') + + # 确保配置目录存在 + os.makedirs(self.config_dir, exist_ok=True) + + # 获取版本号 + self.current_version = self._get_version() + + def _get_version(self) -> str: + """ + 从version.txt文件获取当前版本号 + 如果是打包后的程序,从打包目录读取 + 如果是开发环境,从根目录读取 + """ + try: + # 确定version.txt的路径 + if getattr(sys, 'frozen', False): + # 如果是打包后的程序 + base_path = Path(sys._MEIPASS) + else: + # 如果是开发环境,获取根目录路径 + current_file = Path(__file__) + base_path = current_file.parent.parent # utils -> root + + version_file = base_path / "version.txt" + + # 读取版本号 + if version_file.exists(): + with open(version_file, "r", encoding="utf-8") as f: + version = f.read().strip() + logger.get_logger().info(f"当前版本: {version}") + return version + else: + logger.get_logger().error(f"版本文件不存在: {version_file}") + return "0.0.0" + + except Exception as e: + logger.get_logger().error(f"读取版本号失败: {str(e)}") + return "0.0.0" + +class VersionManager: + """版本管理器""" + + def __init__(self): + """初始化版本管理器""" + self.config = VersionConfig() + + # 禁用SSL警告 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def _load_config(self) -> Dict: + """加载版本配置""" + try: + if os.path.exists(self.config.config_file): + with open(self.config.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + return {} + except Exception as e: + logger.get_logger().error(f"加载版本配置失败: {str(e)}") + return {} + + def _save_config(self, config_data: Dict) -> bool: + """保存版本配置""" + try: + with open(self.config.config_file, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logger.get_logger().error(f"保存版本配置失败: {str(e)}") + return False + + def check_update(self) -> Tuple[bool, str, Optional[Dict]]: + """ + 检查更新 + + Returns: + Tuple[bool, str, Optional[Dict]]: + - bool: 是否有更新 + - str: 消息 + - Optional[Dict]: 更新信息 + """ + try: + log = logger.get_logger() + log.info(f"开始检查更新,当前版本: {self.config.current_version}") + + # 准备请求数据 + data = { + "version": self.config.current_version, + "platform": "windows" + } + + log.info(f"请求URL: {self.config.update_url}") + log.info(f"请求数据: {json.dumps(data, ensure_ascii=False)}") + log.info(f"请求头: {json.dumps(config.request_config['headers'], ensure_ascii=False)}") + + # 发送更新检查请求 + response = requests.post( + self.config.update_url, + json=data, + headers=config.request_config["headers"], + timeout=config.request_config["timeout"], + verify=config.request_config["verify"] + ) + + log.info(f"响应状态码: {response.status_code}") + log.info(f"响应内容: {response.text}") + + # 解析响应 + result = response.json() + + # 检查响应码(0或1都是正常响应) + if result.get("code") in [0, 1]: + update_info = result.get("data", {}) + + if update_info.get("has_update"): + # 有更新可用 + version_info = update_info.get("version_info", {}) + new_version = version_info.get("version") + update_msg = version_info.get("update_msg", "") + download_url = version_info.get("download_url", "") + is_force = update_info.get("is_force", 0) + + log.info(f"发现新版本: {new_version}") + log.info(f"更新信息: {json.dumps(version_info, ensure_ascii=False)}") + + update_data = { + "version": new_version, + "message": update_msg, + "download_url": download_url, + "is_force": bool(is_force), + "check_time": update_info.get("check_time") + } + + # 更新配置 + config_data = self._load_config() + config_data["last_check_update"] = update_data + self._save_config(config_data) + + return True, f"发现新版本: {new_version}", update_data + else: + log.info("当前已是最新版本") + return False, "当前已是最新版本", None + + else: + error_msg = result.get("msg", "检查更新失败") + log.error(f"检查更新失败: {error_msg}") + log.error(f"错误响应: {json.dumps(result, ensure_ascii=False)}") + return False, f"检查更新失败: {error_msg}", None + + except requests.exceptions.RequestException as e: + error_msg = f"网络连接失败: {str(e)}" + logger.get_logger().error(error_msg) + return False, error_msg, None + + except json.JSONDecodeError as e: + error_msg = f"解析响应失败: {str(e)}" + logger.get_logger().error(error_msg) + logger.get_logger().error(f"无法解析的响应内容: {response.text}") + return False, error_msg, None + + except Exception as e: + error_msg = f"检查更新异常: {str(e)}" + logger.get_logger().error(error_msg) + return False, error_msg, None + + def get_last_update_info(self) -> Optional[Dict]: + """获取上次检查的更新信息""" + try: + config_data = self._load_config() + update_info = config_data.get("last_check_update") + + if update_info: + logger.get_logger().info(f"获取到未完成的更新信息: {update_info['version']}") + + return update_info + + except Exception as e: + logger.get_logger().error(f"获取更新信息失败: {str(e)}") + return None + + def clear_update_info(self) -> bool: + """清除更新信息""" + try: + config_data = self._load_config() + if "last_check_update" in config_data: + del config_data["last_check_update"] + success = self._save_config(config_data) + if success: + logger.get_logger().info("更新信息已清除") + return success + return True + + except Exception as e: + logger.get_logger().error(f"清除更新信息失败: {str(e)}") + return False \ No newline at end of file diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..9592bf1 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +4.0.0.1 \ No newline at end of file