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