5 Commits

Author SHA1 Message Date
huangzhenpc
8b2fbef54a feat(v3.4.7): 重构重置功能和优化重启逻辑
1. 新增 CursorResetter 类,完整封装 cursor_win_id_modifier.ps1 的核心功能

2. 优化 AccountSwitcher 的重启逻辑,避免重复重启

3. 改进进程管理,移除 wmi 依赖,使用 tasklist 替代

4. 提升代码可维护性,后续只需更新 CursorResetter 即可适配脚本变更
2025-02-14 15:06:05 +08:00
huangzhenpc
10523de040 优化更新: 1. 修复版本显示和检查逻辑 2. 优化打包目录结构为两位版本号(3.4.4 -> 3.4) 2025-02-13 20:38:49 +08:00
huangzhenpc
dd0a307ff4 优化更新: 1. 隐藏命令行窗口弹出 2. 优化打包目录结构按版本号分类 3. 使用subprocess替代os.system 2025-02-13 18:19:06 +08:00
huangzhenpc
b5cbf0779b feat: 优化更新下载界面 v3.4.2 #patch - 1.优化下载进度对话框布局和样式 2.增大信息显示区域,提升可读性 3.美化进度条和状态显示 4.添加实时下载速度和剩余时间显示 5.更新版本截图 2025-02-13 16:27:25 +08:00
huangzhenpc
d81244c7e4 feat: 完善版本管理功能 1. 优化使用步骤说明的样式,调整行高和字体大小,提升可读性 2. 新增版本截图 3. 更新依赖包版本 4. 添加版本管理相关文件 2025-02-13 15:16:55 +08:00
17 changed files with 2765 additions and 495 deletions

View File

@@ -7,11 +7,53 @@ import uuid
import hashlib
import sys
import time
from typing import Optional, Dict, Tuple
import ctypes
from typing import Optional, Dict, Tuple, List
from pathlib import Path
from utils.config import Config
from utils.cursor_registry import CursorRegistry
from cursor_auth_manager import CursorAuthManager
from utils.cursor_resetter import CursorResetter # 添加导入
def is_admin() -> bool:
"""检查是否具有管理员权限
Returns:
bool: 是否具有管理员权限
"""
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except:
return False
def run_as_admin():
"""以管理员权限重新运行程序"""
try:
if not is_admin():
# 获取当前脚本的路径
script = sys.argv[0]
params = ' '.join(sys.argv[1:])
# 创建 startupinfo 对象来隐藏命令行窗口
startupinfo = None
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 以管理员权限重新运行
ctypes.windll.shell32.ShellExecuteW(
None,
"runas",
sys.executable,
f'"{script}" {params}',
None,
1
)
return True
except Exception as e:
logging.error(f"以管理员权限运行失败: {str(e)}")
return False
def get_hardware_id() -> str:
"""获取硬件唯一标识"""
@@ -45,6 +87,15 @@ def get_hardware_id() -> str:
class AccountSwitcher:
def __init__(self):
# 检查管理员权限
if not is_admin():
logging.warning("当前不是管理员权限运行")
if run_as_admin():
sys.exit(0)
else:
logging.error("无法获取管理员权限")
raise PermissionError("需要管理员权限才能运行此程序")
self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor"
self.app_path = self.cursor_path / "resources" / "app"
self.package_json = self.app_path / "package.json"
@@ -52,6 +103,9 @@ class AccountSwitcher:
self.config = Config()
self.hardware_id = self.get_hardware_id() # 先获取硬件ID
self.registry = CursorRegistry() # 添加注册表操作工具类
self.resetter = CursorResetter() # 添加重置工具类
self.max_retries = 5
self.wait_time = 1
logging.info(f"初始化硬件ID: {self.hardware_id}")
@@ -159,146 +213,333 @@ class AccountSwitcher:
Returns:
tuple: (成功标志, 消息, 账号信息)
"""
try:
data = {
"machine_id": self.hardware_id,
"code": code
}
# 禁用SSL警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 设置请求参数
request_kwargs = {
"json": data,
"headers": {"Content-Type": "application/json"},
"timeout": 5, # 减少超时时间从10秒到5秒
"verify": False # 禁用SSL验证
}
# 尝试发送请求
max_retries = 3 # 最大重试次数
retry_delay = 1 # 重试间隔(秒)
for attempt in range(max_retries):
try:
response = requests.post(
self.config.get_api_url("activate"),
**request_kwargs
)
except requests.exceptions.SSLError:
# 如果发生SSL错误创建自定义SSL上下文
import ssl
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
data = {
"machine_id": self.hardware_id,
"code": code
}
# 使用自定义SSL上下文重试请求
# 禁用SSL警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 设置请求参数
request_kwargs = {
"json": data,
"headers": {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept": "*/*",
"Connection": "keep-alive"
},
"timeout": 10, # 增加超时时间
"verify": False # 禁用SSL验证
}
# 创建session
session = requests.Session()
session.verify = False
response = session.post(
self.config.get_api_url("activate"),
**request_kwargs
# 设置重试策略
retry_strategy = urllib3.Retry(
total=3, # 总重试次数
backoff_factor=0.5, # 重试间隔
status_forcelist=[500, 502, 503, 504] # 需要重试的HTTP状态码
)
adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
try:
# 尝试发送请求
response = session.post(
self.config.get_api_url("activate"),
**request_kwargs
)
response.raise_for_status() # 检查HTTP状态码
result = response.json()
# 激活成功
if result["code"] == 200:
api_data = result["data"]
# 构造标准的返回数据结构
account_info = {
"status": "active",
"expire_time": api_data.get("expire_time", ""),
"total_days": api_data.get("total_days", 0),
"days_left": api_data.get("days_left", 0),
"device_info": self.get_device_info()
}
return True, result["msg"], account_info
# 激活码无效或已被使用
elif result["code"] == 400:
logging.warning(f"激活码无效或已被使用: {result.get('msg', '未知错误')}")
return False, result.get("msg", "激活码无效或已被使用"), None
# 其他错误情况
else:
error_msg = result.get("msg", "未知错误")
if attempt < max_retries - 1: # 如果还有重试机会
logging.warning(f"{attempt + 1}次尝试失败: {error_msg}, 准备重试...")
time.sleep(retry_delay)
continue
logging.error(f"激活失败: {error_msg}")
return False, error_msg, None
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1: # 如果还有重试机会
logging.warning(f"{attempt + 1}次网络请求失败: {str(e)}, 准备重试...")
time.sleep(retry_delay)
continue
error_msg = self._get_network_error_message(e)
logging.error(f"网络请求失败: {error_msg}")
return False, f"网络连接失败: {error_msg}", None
except Exception as e:
if attempt < max_retries - 1: # 如果还有重试机会
logging.warning(f"{attempt + 1}次请求发生错误: {str(e)}, 准备重试...")
time.sleep(retry_delay)
continue
logging.error(f"激活失败: {str(e)}")
return False, f"激活失败: {str(e)}", None
# 如果所有重试都失败了
return False, "多次尝试后激活失败,请检查网络连接或稍后重试", None
def _get_network_error_message(self, error: Exception) -> str:
"""获取网络错误的友好提示信息"""
if isinstance(error, requests.exceptions.SSLError):
return "SSL证书验证失败请检查系统时间是否正确"
elif isinstance(error, requests.exceptions.ConnectionError):
if "10054" in str(error):
return "连接被重置,可能是防火墙拦截,请检查防火墙设置"
elif "10061" in str(error):
return "无法连接到服务器,请检查网络连接"
return "网络连接错误,请检查网络设置"
elif isinstance(error, requests.exceptions.Timeout):
return "请求超时,请检查网络连接"
elif isinstance(error, requests.exceptions.RequestException):
return "网络请求失败,请稍后重试"
return str(error)
def get_process_details(self, process_name: str) -> List[Dict]:
"""获取进程详细信息
Args:
process_name: 进程名称
result = response.json()
# 激活成功
if result["code"] == 200:
api_data = result["data"]
# 构造标准的返回数据结构
account_info = {
"status": "active",
"expire_time": api_data.get("expire_time", ""),
"total_days": api_data.get("total_days", 0),
"days_left": api_data.get("days_left", 0),
"device_info": self.get_device_info()
}
return True, result["msg"], account_info
# 激活码无效或已被使用
elif result["code"] == 400:
logging.warning(f"激活码无效或已被使用: {result.get('msg', '未知错误')}")
return False, result.get("msg", "激活码无效或已被使用"), None
# 其他错误情况
else:
logging.error(f"激活失败: {result.get('msg', '未知错误')}")
return False, result["msg"], None # 返回 None 而不是空的账号信息
Returns:
List[Dict]: 进程详细信息列表
"""
try:
# 使用 tasklist 命令替代 wmi
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
output = subprocess.check_output(
f'tasklist /FI "IMAGENAME eq {process_name}" /FO CSV /NH',
startupinfo=startupinfo,
shell=True
).decode('gbk')
processes = []
if output.strip():
for line in output.strip().split('\n'):
if line.strip():
parts = line.strip('"').split('","')
if len(parts) >= 2:
processes.append({
'name': parts[0],
'pid': parts[1]
})
return processes
except Exception as e:
logging.error(f"激活失败: {str(e)}")
return False, f"激活失败: {str(e)}", None # 返回 None 而不是空的账号信息
logging.error(f"获取进程信息失败: {str(e)}")
return []
def close_cursor_process(self) -> bool:
"""关闭所有Cursor进程
Returns:
bool: 是否成功关闭所有进程
"""
try:
if sys.platform == "win32":
# 创建startupinfo对象来隐藏命令行窗口
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 获取进程详情
processes = self.get_process_details("Cursor.exe")
if processes:
logging.info(f"发现 {len(processes)} 个Cursor进程")
for p in processes:
logging.info(f"进程信息: PID={p['pid']}, 路径={p['name']}")
# 尝试关闭进程
subprocess.run(
"taskkill /f /im Cursor.exe >nul 2>&1",
startupinfo=startupinfo,
shell=True
)
# 等待进程关闭
retry_count = 0
while retry_count < self.max_retries:
if not self.get_process_details("Cursor.exe"):
logging.info("所有Cursor进程已关闭")
return True
retry_count += 1
if retry_count >= self.max_retries:
processes = self.get_process_details("Cursor.exe")
if processes:
logging.error(f"无法关闭以下进程:")
for p in processes:
logging.error(f"PID={p['pid']}, 路径={p['name']}")
return False
logging.warning(f"等待进程关闭, 尝试 {retry_count}/{self.max_retries}...")
time.sleep(self.wait_time)
return True
else:
# 其他系统的处理
if sys.platform == "darwin":
subprocess.run("killall Cursor 2>/dev/null", shell=True)
else:
subprocess.run("pkill -f cursor", shell=True)
time.sleep(2)
return True
except Exception as e:
logging.error(f"关闭进程失败: {str(e)}")
return False
def reset_machine_id(self) -> bool:
"""重置机器码"""
try:
# 1. 关闭所有Cursor进程
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
time.sleep(2)
# 2. 清理注册表(包括更新系统 MachineGuid
if not self.registry.clean_registry():
logging.warning("注册表清理失败")
# 3. 清理文件(包括备份 storage.json
if not self.registry.clean_cursor_files():
logging.warning("文件清理失败")
# 4. 修改 package.json 中的 machineId
if self.package_json.exists():
with open(self.package_json, "r", encoding="utf-8") as f:
data = json.load(f)
# 1. 关闭所有Cursor进程
if not self.close_cursor_process():
logging.error("无法关闭所有Cursor进程")
return False
if "machineId" in data:
del data["machineId"]
# 2. 使用新的重置工具类执行重置
success, message = self.resetter.reset_cursor(disable_update=True)
if not success:
logging.error(f"重置失败: {message}")
return False
with open(self.package_json, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# 5. 修改 storage.json 中的遥测 ID
storage_path = Path(os.getenv('APPDATA')) / "Cursor" / "User" / "globalStorage" / "storage.json"
if storage_path.exists():
with open(storage_path, "r", encoding="utf-8") as f:
storage_data = json.load(f)
# 只修改 machineId保持其他遥测 ID 不变
if "telemetry.machineId" in storage_data:
# 生成新的 machineId使用与 GitHub 脚本类似的格式)
new_machine_id = hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()
storage_data["telemetry.machineId"] = new_machine_id
with open(storage_path, "w", encoding="utf-8") as f:
json.dump(storage_data, f, indent=2)
# 6. 重启Cursor
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
os.startfile(str(cursor_exe))
logging.info("Cursor重启成功")
# 不在这里重启Cursor让调用者决定何时重启
logging.info("机器码重置完成")
return True
except Exception as e:
logging.error(f"重置机器码失败: {str(e)}")
return False
def bypass_version_limit(self) -> Tuple[bool, str]:
"""突破Cursor版本限制"""
def restart_cursor(self) -> bool:
"""重启Cursor编辑器
Returns:
bool: 是否成功重启
"""
try:
# 1. 先关闭所有Cursor进程
logging.info("正在重启Cursor...")
# 确保进程已关闭
if not self.close_cursor_process():
logging.error("无法关闭Cursor进程")
return False
# 等待进程完全关闭
time.sleep(2)
# 启动Cursor
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
time.sleep(2)
# 2. 重置机器码
if not self.reset_machine_id():
return False, "重置机器码失败"
# 3. 等待Cursor启动
time.sleep(3)
return True, "突破版本限制成功Cursor已重启"
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
try:
# 使用subprocess启动
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.Popen(
str(cursor_exe),
startupinfo=startupinfo,
creationflags=subprocess.CREATE_NEW_CONSOLE
)
# 等待进程启动
time.sleep(3)
# 验证进程是否启动
processes = self.get_process_details("Cursor.exe")
if processes:
logging.info("Cursor启动成功")
return True
else:
logging.error("Cursor进程未找到")
# 尝试使用 os.startfile 作为备选方案
try:
os.startfile(str(cursor_exe))
time.sleep(3)
logging.info("使用备选方案启动Cursor")
return True
except Exception as e:
logging.error(f"备选启动方案失败: {str(e)}")
return False
except Exception as e:
logging.error(f"启动Cursor失败: {str(e)}")
# 尝试使用 os.startfile 作为备选方案
try:
os.startfile(str(cursor_exe))
time.sleep(3)
logging.info("使用备选方案启动Cursor")
return True
except Exception as e:
logging.error(f"备选启动方案失败: {str(e)}")
return False
else:
logging.error(f"未找到Cursor程序: {cursor_exe}")
return False
elif sys.platform == "darwin":
try:
subprocess.run("open -a Cursor", shell=True, check=True)
logging.info("Cursor启动成功")
return True
except subprocess.CalledProcessError as e:
logging.error(f"启动Cursor失败: {str(e)}")
return False
elif sys.platform == "linux":
try:
subprocess.run("cursor &", shell=True, check=True)
logging.info("Cursor启动成功")
return True
except subprocess.CalledProcessError as e:
logging.error(f"启动Cursor失败: {str(e)}")
return False
return False
except Exception as e:
logging.error(f"突破版本限制失败: {str(e)}")
return False, f"突破失败: {str(e)}"
logging.error(f"重启Cursor失败: {str(e)}")
# 尝试使用 os.startfile 作为最后的备选方案
try:
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
os.startfile(str(cursor_exe))
time.sleep(3)
logging.info("使用最终备选方案启动Cursor")
return True
except:
pass
return False
def activate_and_switch(self, activation_code: str) -> Tuple[bool, str]:
"""激活并切换账号
@@ -396,50 +637,6 @@ class AccountSwitcher:
"device_info": self.get_device_info()
}
def restart_cursor(self) -> bool:
"""重启Cursor编辑器
Returns:
bool: 是否成功重启
"""
try:
logging.info("正在重启Cursor...")
if sys.platform == "win32":
# Windows系统
# 关闭Cursor
os.system("taskkill /f /im Cursor.exe 2>nul")
time.sleep(2)
# 获取Cursor安装路径
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
# 启动Cursor
os.startfile(str(cursor_exe))
logging.info("Cursor重启成功")
return True
else:
logging.error(f"未找到Cursor程序: {cursor_exe}")
return False
elif sys.platform == "darwin":
# macOS系统
os.system("killall Cursor 2>/dev/null")
time.sleep(2)
os.system("open -a Cursor")
logging.info("Cursor重启成功")
return True
elif sys.platform == "linux":
# Linux系统
os.system("pkill -f cursor")
time.sleep(2)
os.system("cursor &")
logging.info("Cursor重启成功")
return True
else:
logging.error(f"不支持的操作系统: {sys.platform}")
return False
except Exception as e:
logging.error(f"重启Cursor时发生错误: {str(e)}")
return False
def refresh_cursor_auth(self) -> Tuple[bool, str]:
"""刷新Cursor授权
@@ -447,12 +644,7 @@ class AccountSwitcher:
Tuple[bool, str]: (是否成功, 提示消息)
"""
try:
# 1. 先关闭所有Cursor进程
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
time.sleep(2)
# 2. 获取未使用的账号
# 1. 获取未使用的账号
endpoint = "https://cursorapi.nosqli.com/admin/api.account/getUnused"
data = {
"machine_id": self.hardware_id
@@ -469,8 +661,8 @@ class AccountSwitcher:
request_kwargs = {
"json": data,
"headers": headers,
"timeout": 30, # 增加超时时间
"verify": False # 禁用SSL验证
"timeout": 30,
"verify": False
}
try:
@@ -504,15 +696,55 @@ class AccountSwitcher:
if not all([email, access_token, refresh_token]):
return False, "获取账号信息不完整"
# 2. 先关闭Cursor进程
logging.info("正在关闭Cursor进程...")
if not self.close_cursor_process():
logging.error("无法关闭Cursor进程")
return False, "无法关闭Cursor进程请手动关闭后重试"
# 3. 更新Cursor认证信息
logging.info("正在更新认证信息...")
if not self.auth_manager.update_auth(email, access_token, refresh_token):
return False, "更新Cursor认证信息失败"
# 4. 重置机器码(包含了清理注册表、文件和重启操作)
# 4. 验证认证信息是否正确写入
logging.info("正在验证认证信息...")
if not self.auth_manager.verify_auth(email, access_token, refresh_token):
return False, "认证信息验证失败"
# 5. 保存email到package.json
try:
if self.package_json.exists():
with open(self.package_json, "r", encoding="utf-8") as f:
package_data = json.load(f)
package_data["email"] = email
with open(self.package_json, "w", encoding="utf-8", newline='\n') as f:
json.dump(package_data, f, indent=2)
logging.info(f"已保存email到package.json: {email}")
except Exception as e:
logging.warning(f"保存email到package.json失败: {str(e)}")
# 6. 重置机器码
logging.info("正在重置机器码...")
if not self.reset_machine_id():
return False, "重置机器码失败"
return True, f"授权刷新成功Cursor编辑器已重启\n邮箱: {email}\n"
# 7. 重启Cursor只在这里执行一次重启
logging.info("正在重启Cursor...")
retry_count = 0
max_retries = 3
while retry_count < max_retries:
if self.restart_cursor():
break
retry_count += 1
if retry_count < max_retries:
logging.warning(f"重启失败,正在重试 ({retry_count}/{max_retries})...")
time.sleep(2)
if retry_count >= max_retries:
return True, f"授权刷新成功但Cursor重启失败请手动启动Cursor\n邮箱: {email}\n到期时间: {expire_time}\n剩余天数: {days_left}"
return True, f"授权刷新成功Cursor已重启\n邮箱: {email}\n到期时间: {expire_time}\n剩余天数: {days_left}"
elif response_data.get("code") == 404:
return False, "没有可用的未使用账号"
@@ -546,7 +778,17 @@ class AccountSwitcher:
try:
# 1. 先关闭所有Cursor进程
if sys.platform == "win32":
os.system("taskkill /f /im Cursor.exe >nul 2>&1")
# 创建startupinfo对象来隐藏命令行窗口
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 关闭Cursor
subprocess.run(
"taskkill /f /im Cursor.exe >nul 2>&1",
startupinfo=startupinfo,
shell=True
)
time.sleep(2)
# 2. 删除updater目录并创建同名文件以阻止更新
@@ -584,9 +826,112 @@ class AccountSwitcher:
logging.error(f"禁用Cursor更新失败: {str(e)}")
return False, f"禁用更新失败: {str(e)}"
def send_heartbeat(self) -> Tuple[bool, str]:
"""
发送心跳请求
Returns:
Tuple[bool, str]: (是否成功, 消息)
"""
max_retries = 3 # 最大重试次数
retry_delay = 1 # 重试间隔(秒)
# 获取硬件ID
hardware_id = self.get_hardware_id()
# 禁用SSL警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 设置请求参数
params = {
"machine_id": hardware_id
}
request_kwargs = {
"params": params, # 使用URL参数而不是JSON body
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept": "*/*",
"Connection": "keep-alive"
},
"timeout": 10,
"verify": False
}
for attempt in range(max_retries):
try:
# 创建session
session = requests.Session()
session.verify = False
# 设置重试策略
retry_strategy = urllib3.Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]
)
adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 发送请求
response = session.get( # 改用GET请求
self.config.get_api_url("heartbeat"),
**request_kwargs
)
# 检查响应
if response.status_code == 200:
result = response.json()
if result.get("code") == 200: # 修改成功码为200
data = result.get("data", {})
expire_time = data.get("expire_time", "")
days_left = data.get("days_left", 0)
status = data.get("status", "")
return True, f"心跳发送成功 [到期时间: {expire_time}, 剩余天数: {days_left}, 状态: {status}]"
else:
error_msg = result.get("msg", "未知错误")
if attempt < max_retries - 1:
logging.warning(f"{attempt + 1}次心跳失败: {error_msg}, 准备重试...")
time.sleep(retry_delay)
continue
return False, f"心跳发送失败: {error_msg}"
else:
if attempt < max_retries - 1:
logging.warning(f"{attempt + 1}次心跳HTTP错误: {response.status_code}, 准备重试...")
time.sleep(retry_delay)
continue
return False, f"心跳请求失败: HTTP {response.status_code}"
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
logging.warning(f"{attempt + 1}次网络请求失败: {str(e)}, 准备重试...")
time.sleep(retry_delay)
continue
error_message = self._get_network_error_message(e)
return False, f"心跳发送失败: {error_message}"
except Exception as e:
if attempt < max_retries - 1:
logging.warning(f"{attempt + 1}次发生异常: {str(e)}, 准备重试...")
time.sleep(retry_delay)
continue
logging.error(f"心跳发送异常: {str(e)}")
return False, f"心跳发送异常: {str(e)}"
return False, "多次尝试后心跳发送失败"
def main():
"""主函数"""
try:
# 检查管理员权限
if not is_admin():
print("\n[错误] 请以管理员身份运行此程序")
print("请右键点击程序,选择'以管理员身份运行'")
if run_as_admin():
return
input("\n按回车键退出...")
return
switcher = AccountSwitcher()
print("\n=== Cursor账号切换工具 ===")
@@ -615,6 +960,9 @@ def main():
else:
print("\n机器码重置失败,请查看日志了解详细信息")
except PermissionError as e:
print(f"\n[错误] {str(e)}")
print("请右键点击程序,选择'以管理员身份运行'")
except Exception as e:
logging.error(f"程序执行出错: {str(e)}")
print("\n程序执行出错,请查看日志了解详细信息")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -5,8 +5,61 @@ echo 开始打包流程...
:: 更新版本号
python update_version.py
:: 读取版本号
set /p VERSION=<version.txt
echo 当前版本: %VERSION%
:: 提取主版本号和次版本号 (3.4.4 -> 3.4)
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%
)
:: 使用新的spec文件进行打包
pyinstaller --noconfirm build_nezha.spec
:: 检查源文件是否存在
echo 检查文件: dist\听泉cursor助手%VERSION%.exe
if not exist "dist\听泉cursor助手%VERSION%.exe" (
echo 错误: 打包后的文件不存在
echo 预期文件路径: dist\听泉cursor助手%VERSION%.exe
dir /b dist
pause
exit /b 1
)
:: 检查目标目录是否存在
echo 检查目标目录: %VERSION_DIR%
if not exist "%VERSION_DIR%" (
echo 错误: 目标目录不存在
pause
exit /b 1
)
:: 移动文件到版本目录
echo 移动文件:
echo 源文件: dist\听泉cursor助手%VERSION%.exe
echo 目标文件: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe
move "dist\听泉cursor助手%VERSION%.exe" "%VERSION_DIR%\听泉cursor助手v%VERSION%.exe"
if errorlevel 1 (
echo 移动文件失败,请检查:
echo 1. 源文件是否存在: dist\听泉cursor助手%VERSION%.exe
echo 2. 目标目录是否可写: %VERSION_DIR%
echo 3. 目标文件是否已存在: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe
dir /b dist
dir /b "%VERSION_DIR%"
pause
exit /b 1
)
echo.
echo 打包完成!
echo 文件保存在: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe
pause

View File

@@ -5,6 +5,9 @@ import time
import logging
import sqlite3
from pathlib import Path
import subprocess
from typing import Optional, Dict, Tuple
from datetime import datetime
class CursorAuthManager:
"""Cursor认证信息管理器"""
@@ -30,105 +33,344 @@ class CursorAuthManager:
raise NotImplementedError(f"不支持的操作系统: {sys.platform}")
self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor"
self.backup_dir = Path(os.getenv('APPDATA')) / "Cursor" / "User" / "globalStorage" / "backups"
self.max_retries = 5
self.wait_time = 1
def update_auth(self, email=None, access_token=None, refresh_token=None):
def backup_database(self) -> Optional[Path]:
"""备份数据库文件
Returns:
Optional[Path]: 备份文件路径,失败返回None
"""
更新Cursor的认证信息
:param email: 新的邮箱地址
:param access_token: 新的访问令牌
:param refresh_token: 新的刷新令牌
:return: bool 是否成功更新
"""
updates = []
# 登录状态
updates.append(("cursorAuth/cachedSignUpType", "Auth_0"))
if email is not None:
updates.append(("cursorAuth/cachedEmail", email))
if access_token is not None:
updates.append(("cursorAuth/accessToken", access_token))
if refresh_token is not None:
updates.append(("cursorAuth/refreshToken", refresh_token))
if not updates:
logging.warning("没有提供任何要更新的值")
return False
conn = None
try:
if not Path(self.db_path).exists():
logging.warning(f"数据库文件不存在: {self.db_path}")
return None
self.backup_dir.mkdir(parents=True, exist_ok=True)
backup_name = f"state.vscdb.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = self.backup_dir / backup_name
# 如果数据库正在使用,先关闭连接
try:
conn = sqlite3.connect(self.db_path)
conn.close()
except:
pass
import shutil
shutil.copy2(self.db_path, backup_path)
logging.info(f"已备份数据库: {backup_path}")
return backup_path
except Exception as e:
logging.error(f"备份数据库失败: {str(e)}")
return None
def get_auth_info(self) -> Optional[Dict]:
"""获取当前的认证信息
Returns:
Optional[Dict]: 认证信息字典,失败返回None
"""
try:
if not Path(self.db_path).exists():
return None
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for key, value in updates:
# 检查key是否存在
check_query = f"SELECT COUNT(*) FROM itemTable WHERE key = ?"
cursor.execute(check_query, (key,))
if cursor.fetchone()[0] == 0:
insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)"
cursor.execute(insert_query, (key, value))
else:
update_query = "UPDATE itemTable SET value = ? WHERE key = ?"
cursor.execute(update_query, (value, key))
if cursor.rowcount > 0:
logging.info(f"成功更新 {key.split('/')[-1]}")
else:
logging.warning(f"未找到 {key.split('/')[-1]} 或值未变化")
conn.commit()
logging.info(f"认证信息更新成功: {email}")
return True
except sqlite3.Error as e:
logging.error(f"数据库错误: {str(e)}")
return False
# 查询认证相关的键值
auth_keys = [
'authentication.currentToken',
'authentication.refreshToken',
'authentication.accessToken',
'authentication.email'
]
result = {}
for key in auth_keys:
cursor.execute('SELECT value FROM ItemTable WHERE key = ?', (key,))
row = cursor.fetchone()
if row:
try:
value = json.loads(row[0])
result[key] = value
except:
result[key] = row[0]
conn.close()
return result if result else None
except Exception as e:
logging.error(f"获取认证信息失败: {str(e)}")
return None
def update_auth(self, email: str = None, access_token: str = None, refresh_token: str = None) -> bool:
"""更新Cursor的认证信息
Args:
email: 新的邮箱地址
access_token: 新的访问令牌
refresh_token: 新的刷新令牌
Returns:
bool: 是否成功更新
"""
try:
# 备份数据库
if not self.backup_database():
logging.warning("数据库备份失败")
# 准备更新数据
updates = []
# 登录状态
updates.append(("cursorAuth/cachedSignUpType", "Auth_0"))
if email is not None:
updates.append(("cursorAuth/cachedEmail", email))
if access_token is not None:
updates.append(("cursorAuth/accessToken", access_token))
if refresh_token is not None:
updates.append(("cursorAuth/refreshToken", refresh_token))
if not updates:
logging.warning("没有提供任何要更新的值")
return False
# 确保数据库目录存在
db_dir = Path(self.db_path).parent
db_dir.mkdir(parents=True, exist_ok=True)
# 确保数据库表存在
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建表(如果不存在)
cursor.execute('''
CREATE TABLE IF NOT EXISTS ItemTable (
key TEXT PRIMARY KEY,
value TEXT
)
''')
try:
# 开始事务
cursor.execute('BEGIN TRANSACTION')
# 执行更新
for key, value in updates:
# 检查key是否存在
cursor.execute('SELECT COUNT(*) FROM ItemTable WHERE key = ?', (key,))
if cursor.fetchone()[0] == 0:
# 插入新值
cursor.execute(
'INSERT INTO ItemTable (key, value) VALUES (?, ?)',
(key, value)
)
else:
# 更新现有值
cursor.execute(
'UPDATE ItemTable SET value = ? WHERE key = ?',
(value, key)
)
if cursor.rowcount > 0:
logging.info(f"成功更新 {key.split('/')[-1]}")
else:
logging.warning(f"未找到 {key.split('/')[-1]} 或值未变化")
# 提交事务
cursor.execute('COMMIT')
logging.info(f"认证信息更新成功: {email}")
return True
except Exception as e:
# 如果出错,回滚事务
cursor.execute('ROLLBACK')
raise e
except Exception as e:
logging.error(f"更新认证信息失败: {str(e)}")
return False
finally:
if conn:
if 'conn' in locals():
conn.close()
def verify_auth(self, email: str, access_token: str, refresh_token: str) -> bool:
"""验证认证信息是否正确写入
Args:
email: 邮箱
access_token: 访问令牌
refresh_token: 新的刷新令牌
Returns:
bool: 是否正确写入
"""
try:
# 连接数据库
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 验证每个字段
expected = {
'cursorAuth/cachedEmail': email,
'cursorAuth/accessToken': access_token,
'cursorAuth/refreshToken': refresh_token,
'cursorAuth/cachedSignUpType': 'Auth_0'
}
for key, expected_value in expected.items():
cursor.execute('SELECT value FROM ItemTable WHERE key = ?', (key,))
row = cursor.fetchone()
if not row:
logging.error(f"缺少认证信息: {key}")
return False
actual_value = row[0]
if actual_value != expected_value:
logging.error(f"认证信息不匹配: {key}")
logging.error(f"预期: {expected_value}")
logging.error(f"实际: {actual_value}")
return False
conn.close()
return True
except Exception as e:
logging.error(f"验证认证信息失败: {str(e)}")
return False
finally:
if 'conn' in locals():
conn.close()
def clear_auth(self) -> bool:
"""清除认证信息
Returns:
bool: 是否成功
"""
try:
# 备份数据库
if not self.backup_database():
logging.warning("数据库备份失败")
# 清除数据库中的认证信息
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 要清除的键
auth_keys = [
'authentication.currentToken',
'authentication.refreshToken',
'authentication.accessToken',
'authentication.email'
]
# 执行删除
for key in auth_keys:
cursor.execute('DELETE FROM ItemTable WHERE key = ?', (key,))
conn.commit()
conn.close()
logging.info("已清除认证信息")
return True
except Exception as e:
logging.error(f"清除认证信息失败: {str(e)}")
return False
def close_cursor_process(self) -> bool:
"""关闭所有Cursor进程
Returns:
bool: 是否成功
"""
try:
if sys.platform == "win32":
# 创建startupinfo对象来隐藏命令行窗口
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 关闭进程
subprocess.run(
"taskkill /f /im Cursor.exe >nul 2>&1",
startupinfo=startupinfo,
shell=True
)
# 等待进程关闭
retry_count = 0
while retry_count < self.max_retries:
try:
subprocess.check_output(
"tasklist | findstr Cursor.exe",
startupinfo=startupinfo,
shell=True
)
retry_count += 1
if retry_count >= self.max_retries:
logging.error("无法关闭所有Cursor进程")
return False
time.sleep(self.wait_time)
except subprocess.CalledProcessError:
# 进程已关闭
break
return True
else:
# 其他系统的处理
if sys.platform == "darwin":
subprocess.run("killall Cursor 2>/dev/null", shell=True)
else:
subprocess.run("pkill -f cursor", shell=True)
time.sleep(2)
return True
except Exception as e:
logging.error(f"关闭进程失败: {str(e)}")
return False
def restart_cursor(self) -> bool:
"""重启Cursor编辑器
Returns:
bool: 是否成功重启
bool: 是否成功
"""
try:
logging.info("正在重启Cursor...")
# 确保进程已关闭
if not self.close_cursor_process():
return False
# 启动Cursor
if sys.platform == "win32":
# Windows系统
# 关闭Cursor
os.system("taskkill /f /im Cursor.exe 2>nul")
time.sleep(2)
# 获取Cursor安装路径
cursor_exe = self.cursor_path / "Cursor.exe"
if cursor_exe.exists():
# 启动Cursor
os.startfile(str(cursor_exe))
logging.info("Cursor启成功")
logging.info("Cursor启成功")
return True
else:
logging.error(f"未找到Cursor程序: {cursor_exe}")
return False
elif sys.platform == "darwin":
# macOS系统
os.system("killall Cursor 2>/dev/null")
time.sleep(2)
os.system("open -a Cursor")
logging.info("Cursor重启成功")
subprocess.run("open -a Cursor", shell=True)
logging.info("Cursor启动成功")
return True
elif sys.platform == "linux":
# Linux系统
os.system("pkill -f cursor")
time.sleep(2)
os.system("cursor &")
logging.info("Cursor重启成功")
subprocess.run("cursor &", shell=True)
logging.info("Cursor启动成功")
return True
else:
logging.error(f"不支持的操作系统: {sys.platform}")
return False
return False
except Exception as e:
logging.error(f"重启Cursor时发生错误: {str(e)}")
logging.error(f"重启Cursor失败: {str(e)}")
return False

0
cursor_win_id.ps1 Normal file
View File

File diff suppressed because it is too large Load Diff

114
main.py
View File

@@ -6,11 +6,13 @@ import atexit
import shutil
import tempfile
import urllib3
import ctypes
from pathlib import Path
from PyQt5.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon, QMenu
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
from gui.main_window import MainWindow
from account_switcher import AccountSwitcher
# 禁用所有 SSL 相关警告
urllib3.disable_warnings()
@@ -39,88 +41,114 @@ def setup_logging():
log_file = log_dir / "switcher.log"
# 输出到文件,不输出到控制台
# 同时输出到文件控制台
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(log_file, encoding="utf-8"),
logging.StreamHandler()
]
)
except Exception as e:
# 不打印错误信息,只记录到日志
pass
print(f"设置日志失败: {str(e)}")
def is_admin():
"""检查是否具有管理员权限"""
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except:
return False
def run_as_admin():
"""以管理员权限重新运行程序"""
try:
if not is_admin():
# 获取当前脚本的路径
script = sys.argv[0]
params = ' '.join(sys.argv[1:])
# 以管理员权限重新运行
ctypes.windll.shell32.ShellExecuteW(
None,
"runas",
sys.executable,
f'"{script}" {params}',
None,
1
)
return True
except Exception as e:
print(f"提升权限失败: {str(e)}")
return False
def print_banner():
"""打印程序横幅"""
print("""
====================================
Cursor 账号管理工具
====================================
""")
def main():
"""主函数"""
try:
# 注册退出时的清理函数
# 1. 首先检查管理员权限
if not is_admin():
if run_as_admin():
return 0
else:
QMessageBox.critical(None, "错误", "需要管理员权限运行此程序。\n请右键点击程序,选择'以管理员身份运行'")
return 1
# 2. 注册退出时的清理函数
atexit.register(cleanup_temp)
# 创建QApplication实例
# 3. 设置日志
setup_logging()
# 4. 创建QApplication实例
app = QApplication(sys.argv)
# 检查系统托盘是否可用
# 5. 检查系统托盘
if not QSystemTrayIcon.isSystemTrayAvailable():
logging.error("系统托盘不可用")
QMessageBox.critical(None, "错误", "系统托盘不可用,程序无法正常运行。")
return 1
# 设置应用程序不会在最后一个窗口关闭时退出
# 6. 设置应用程序不会在最后一个窗口关闭时退出
app.setQuitOnLastWindowClosed(False)
setup_logging()
# 检查Python版本
# 7. 记录系统信息
logging.info(f"Python版本: {sys.version}")
# 检查工作目录
logging.info(f"当前工作目录: {Path.cwd()}")
# 检查模块路径
logging.info("Python路径:")
for p in sys.path:
logging.info(f" - {p}")
logging.info("正在初始化主窗口...")
# 设置应用程序ID (在设置图标之前)
# 8. 设置应用程序ID
if sys.platform == "win32":
import ctypes
myappid = u'nezha.cursor.helper.v3'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
logging.info(f"设置应用程序ID: {myappid}")
# 设置应用程序图标
try:
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon", "two.ico")
if os.path.exists(icon_path):
app_icon = QIcon(icon_path)
if not app_icon.isNull():
app.setWindowIcon(app_icon)
logging.info(f"成功设置应用程序图标: {icon_path}")
else:
logging.error("图标文件加载失败")
else:
logging.error(f"图标文件不存在: {icon_path}")
except Exception as e:
logging.error(f"设置应用程序图标失败: {str(e)}")
# 9. 设置应用程序图标
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon", "two.ico")
if os.path.exists(icon_path):
app_icon = QIcon(icon_path)
if not app_icon.isNull():
app.setWindowIcon(app_icon)
logging.info(f"成功设置应用程序图标: {icon_path}")
# 10. 创建并显示主窗口
logging.info("正在初始化主窗口...")
window = MainWindow()
window.setWindowIcon(app.windowIcon()) # 确保窗口使用相同的图标
logging.info("正在启动主窗口...")
window.setWindowIcon(app.windowIcon())
window.show()
# 11. 运行应用程序
return app.exec_()
except Exception as e:
error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
# 使用 QMessageBox 显示错误
if QApplication.instance() is None:
app = QApplication(sys.argv)
QMessageBox.critical(None, "错误", error_msg)
return 1

View File

@@ -7,4 +7,5 @@ altgraph>=0.17.4
pyinstaller==6.3.0
pillow==10.2.0
PyQt5==5.15.10
pywin32==306
pywin32==306
packaging>=23.2

63
test_version_manager.py Normal file
View File

@@ -0,0 +1,63 @@
import logging
import sys
from pathlib import Path
from utils.version_manager import VersionManager
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('version_check.log')
]
)
def test_version_manager():
"""测试版本管理器功能"""
try:
vm = VersionManager()
logging.info(f"当前版本: {vm.current_version}")
logging.info(f"当前平台: {vm.platform}")
# 测试获取最新版本
logging.info("\n=== 测试获取最新版本 ===")
latest = vm.get_latest_version()
logging.info(f"最新版本信息: {latest}")
# 测试检查更新
logging.info("\n=== 测试检查更新 ===")
update_info = vm.check_update()
logging.info(f"更新检查结果: {update_info}")
# 测试是否需要更新
logging.info("\n=== 测试是否需要更新 ===")
has_update, is_force, version_info = vm.needs_update()
logging.info(f"是否有更新: {has_update}")
logging.info(f"是否强制更新: {is_force}")
logging.info(f"版本信息: {version_info}")
# 如果有更新,测试下载功能
if has_update and version_info:
logging.info("\n=== 测试下载更新 ===")
download_url = version_info.get('download_url')
if download_url:
save_path = Path.home() / "Downloads" / "CursorHelper" / "test_update.exe"
save_path.parent.mkdir(parents=True, exist_ok=True)
logging.info(f"下载地址: {download_url}")
logging.info(f"保存路径: {save_path}")
success = vm.download_update(download_url, str(save_path))
logging.info(f"下载结果: {'成功' if success else '失败'}")
if success:
logging.info(f"文件大小: {save_path.stat().st_size} 字节")
else:
logging.warning("未找到下载地址")
except Exception as e:
logging.error(f"测试过程中发生错误: {str(e)}", exc_info=True)
if __name__ == "__main__":
test_version_manager()

View File

@@ -12,6 +12,12 @@ REM 读取当前版本号
set /p VERSION=<version.txt
echo 当前正式版本: %VERSION%
REM 提取主版本号和次版本号 (3.4.4 -> 3.4)
for /f "tokens=1,2 delims=." %%a in ("%VERSION%") do (
set MAJOR_VERSION=%%a.%%b
)
echo 主版本目录: %MAJOR_VERSION%
REM 读取测试版本号(如果存在)
if exist testversion.txt (
set /p TEST_VERSION=<testversion.txt
@@ -29,7 +35,11 @@ set FULL_VERSION=%VERSION%.!TEST_VERSION!
echo 完整版本号: !FULL_VERSION!
REM 创建测试版本输出目录
if not exist "dist\test" mkdir "dist\test"
set TEST_DIR=dist\test\%MAJOR_VERSION%
if not exist "!TEST_DIR!" (
mkdir "!TEST_DIR!"
echo 创建目录: !TEST_DIR!
)
REM 清理旧文件
if exist "dist\听泉cursor助手%VERSION%.exe" del "dist\听泉cursor助手%VERSION%.exe"
@@ -38,13 +48,36 @@ if exist "build" rmdir /s /q "build"
REM 执行打包
venv\Scripts\python.exe -m PyInstaller build_nezha.spec --clean
REM 检查源文件是否存在
echo 检查文件: dist\听泉cursor助手%VERSION%.exe
if not exist "dist\听泉cursor助手%VERSION%.exe" (
echo 错误: 打包后的文件不存在
echo 预期文件路径: dist\听泉cursor助手%VERSION%.exe
dir /b dist
pause
exit /b 1
)
REM 移动并重命名文件
move "dist\听泉cursor助手%VERSION%.exe" "dist\test\听泉cursor助手v!FULL_VERSION!.exe"
echo 移动文件:
echo 源文件: dist\听泉cursor助手%VERSION%.exe
echo 目标文件: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe
move "dist\听泉cursor助手%VERSION%.exe" "!TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe"
if errorlevel 1 (
echo 移动文件失败,请检查:
echo 1. 源文件是否存在: dist\听泉cursor助手%VERSION%.exe
echo 2. 目标目录是否可写: !TEST_DIR!
echo 3. 目标文件是否已存在: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe
dir /b dist
dir /b "!TEST_DIR!"
pause
exit /b 1
)
echo.
echo 测试版本构建完成!
echo 版本号: v!FULL_VERSION!
echo 文件位置: dist\test\听泉cursor助手v!FULL_VERSION!.exe
echo 文件位置: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe
REM 退出虚拟环境
deactivate

View File

@@ -11,7 +11,8 @@ class Config:
self.api_endpoints = {
"activate": f"{self.base_url}/admin/api.member/activate",
"status": f"{self.base_url}/admin/api.member/status",
"get_unused": f"{self.base_url}/admin/api.account/getUnused"
"get_unused": f"{self.base_url}/admin/api.account/getUnused",
"heartbeat": f"{self.base_url}/admin/api.account/heartbeat"
}
self.config_dir = Path(os.path.expanduser("~")) / ".cursor_switcher"
self.config_file = self.config_dir / "config.json"

View File

@@ -7,6 +7,8 @@ import uuid
from datetime import datetime
import json
import hashlib
import ctypes
from typing import Optional
class CursorRegistry:
"""Cursor注册表操作工具类"""
@@ -14,61 +16,115 @@ class CursorRegistry:
def __init__(self):
self.cursor_path = Path(os.path.expanduser("~")) / "AppData" / "Local" / "Programs" / "Cursor"
self.app_path = self.cursor_path / "resources" / "app"
self.backup_dir = Path(os.getenv('APPDATA')) / "Cursor" / "User" / "globalStorage" / "backups"
def get_random_hex(self, length: int) -> str:
"""生成安全的随机十六进制字符串
Args:
length: 需要生成的字节长度
Returns:
str: 十六进制字符串
"""
import secrets
return secrets.token_hex(length)
def new_standard_machine_id(self) -> str:
"""生成标准格式的机器ID
Returns:
str: 标准格式的机器ID
"""
template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
def replace_char(match):
import random
r = random.randint(0, 15)
v = r if match == 'x' else (r & 0x3 | 0x8)
return hex(v)[2:]
return ''.join(replace_char(c) for c in template)
def is_admin(self) -> bool:
"""检查是否具有管理员权限
Returns:
bool: 是否具有管理员权限
"""
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except:
return False
def backup_file(self, source_path: Path, backup_name: Optional[str] = None) -> Optional[Path]:
"""备份文件
Args:
source_path: 源文件路径
backup_name: 备份文件名(可选)
Returns:
Optional[Path]: 备份文件路径,失败返回None
"""
try:
if not source_path.exists():
return None
self.backup_dir.mkdir(parents=True, exist_ok=True)
if backup_name is None:
backup_name = f"{source_path.name}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = self.backup_dir / backup_name
shutil.copy2(source_path, backup_path)
logging.info(f"已备份文件: {source_path} -> {backup_path}")
return backup_path
except Exception as e:
logging.error(f"备份文件失败 {source_path}: {str(e)}")
return None
def update_machine_guid(self) -> bool:
"""更新系统的 MachineGuid
Returns:
bool: 是否成功
"""
if not self.is_admin():
logging.error("需要管理员权限来修改 MachineGuid")
return False
try:
# 生成新的 GUID
new_guid = str(uuid.uuid4())
registry_path = r"SOFTWARE\Microsoft\Cryptography"
try:
# 使用管理员权限打开注册表项
key = None
try:
# 先尝试直接打开读取权限
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY)
# 读取原始值并备份
# 备份原始值
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY) as key:
original_guid = winreg.QueryValueEx(key, "MachineGuid")[0]
winreg.CloseKey(key)
# 备份原始 MachineGuid
backup_dir = Path(os.getenv('APPDATA')) / "Cursor" / "User" / "globalStorage" / "backups"
backup_dir.mkdir(parents=True, exist_ok=True)
backup_name = f"MachineGuid.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
with open(backup_dir / backup_name, 'w', encoding='utf-8') as f:
f.write(original_guid)
logging.info(f"备份 MachineGuid 到: {backup_name}")
# 重新打开写入权限
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY)
except WindowsError:
# 如果失败,尝试以管理员权限运行
import ctypes
if ctypes.windll.shell32.IsUserAnAdmin() == 0:
logging.warning("需要管理员权限来修改 MachineGuid")
return False
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY)
# 备份原始 GUID
backup_path = self.backup_dir / f"MachineGuid.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
self.backup_dir.mkdir(parents=True, exist_ok=True)
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(original_guid)
logging.info(f"已备份 MachineGuid: {backup_path}")
# 设置新的 GUID
winreg.SetValueEx(key, "MachineGuid", 0, winreg.REG_SZ, new_guid)
winreg.CloseKey(key)
logging.info(f"更新系统 MachineGuid 成功: {new_guid}")
# 更新 GUID
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY) as key:
winreg.SetValueEx(key, "MachineGuid", 0, winreg.REG_SZ, new_guid)
logging.info(f"已更新系统 MachineGuid: {new_guid}")
return True
except WindowsError as e:
logging.error(f"更新系统 MachineGuid 失败: {str(e)}")
logging.error(f"注册表操作失败: {str(e)}")
return False
except Exception as e:
logging.error(f"更新 MachineGuid 过程出错: {str(e)}")
logging.error(f"更新 MachineGuid 失败: {str(e)}")
return False
def clean_registry(self) -> bool:
@@ -107,97 +163,111 @@ class CursorRegistry:
return False
def clean_cursor_files(self) -> bool:
"""清理Cursor相关的文件和目录但保留重要的配置和历史记录"""
"""清理Cursor相关的文件和目录但保留重要的配置和历史记录
Returns:
bool: 是否成功
"""
try:
local_app_data = Path(os.getenv('LOCALAPPDATA'))
app_data = Path(os.getenv('APPDATA'))
storage_path = Path(os.getenv('APPDATA')) / "Cursor" / "User" / "globalStorage" / "storage.json"
global_storage_dir = storage_path.parent
# 需要备份的文件
storage_path = app_data / "Cursor" / "User" / "globalStorage" / "storage.json"
backup_dir = app_data / "Cursor" / "User" / "globalStorage" / "backups"
global_storage_dir = app_data / "Cursor" / "User" / "globalStorage"
# 如果存在 storage.json备份
# 备份 storage.json
if storage_path.exists():
if not self.backup_file(storage_path):
return False
# 备份其他重要文件
if global_storage_dir.exists():
for item in global_storage_dir.iterdir():
if item.name not in ["storage.json", "backups"]:
try:
backup_item_dir = self.backup_dir / f"other_files_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_item_dir.mkdir(exist_ok=True)
if item.is_file():
shutil.copy2(item, backup_item_dir / item.name)
elif item.is_dir():
shutil.copytree(item, backup_item_dir / item.name)
logging.info(f"已备份: {item}")
except Exception as e:
logging.error(f"备份失败 {item}: {str(e)}")
# 更新 storage.json
if storage_path.exists():
# 确保备份目录存在
backup_dir.mkdir(parents=True, exist_ok=True)
# 备份 storage.json
backup_name = f"storage.json.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
shutil.copy2(storage_path, backup_dir / backup_name)
logging.info(f"备份 storage.json 到: {backup_name}")
# 备份 global_storage 目录中的其他重要文件
if global_storage_dir.exists():
for item in global_storage_dir.iterdir():
if item.name != "storage.json" and item.name != "backups":
try:
backup_item_dir = backup_dir / f"other_files_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_item_dir.mkdir(exist_ok=True)
if item.is_file():
shutil.copy2(item, backup_item_dir / item.name)
logging.info(f"备份文件: {item.name}")
elif item.is_dir():
shutil.copytree(item, backup_item_dir / item.name)
logging.info(f"备份目录: {item.name}")
except Exception as e:
logging.error(f"备份 {item} 失败: {str(e)}")
# 读取当前内容
with open(storage_path, "r", encoding="utf-8") as f:
storage_data = json.load(f)
# 只修改 machineId保持其他配置不变
if "telemetry.machineId" in storage_data:
# 生成新的 machineId
new_machine_id = hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()
storage_data["telemetry.machineId"] = new_machine_id
logging.info(f"更新 machineId: {new_machine_id}")
# 保存修改后的内容
with open(storage_path, "w", encoding="utf-8") as f:
json.dump(storage_data, f, indent=2)
# 处理 updater 目录
updater_path = local_app_data / "cursor-updater"
try:
# 如果是目录,则删除
if updater_path.is_dir():
shutil.rmtree(str(updater_path))
logging.info("删除 updater 目录成功")
# 如果是文件,则删除
if updater_path.is_file():
updater_path.unlink()
logging.info("删除 updater 文件成功")
# 创建同名空文件来阻止更新
updater_path.touch()
logging.info("创建 updater 空文件成功")
except Exception as e:
logging.error(f"处理 updater 文件失败: {str(e)}")
# 只清理缓存相关的路径
paths_to_clean = [
local_app_data / "Cursor" / "Cache"
]
for path in paths_to_clean:
try:
if path.is_dir():
shutil.rmtree(str(path), ignore_errors=True)
logging.info(f"删除目录成功: {path}")
elif path.exists():
path.unlink()
logging.info(f"删除文件成功: {path}")
with open(storage_path, "r", encoding="utf-8") as f:
storage_data = json.load(f)
if "telemetry.machineId" in storage_data:
new_machine_id = self.get_random_hex(32)
storage_data["telemetry.machineId"] = new_machine_id
logging.info(f"已更新 machineId: {new_machine_id}")
# 使用 UTF-8 无 BOM 编码保存
with open(storage_path, "w", encoding="utf-8", newline='\n') as f:
json.dump(storage_data, f, indent=2)
except Exception as e:
logging.error(f"清理文件/目录失败: {path}, 错误: {str(e)}")
# 修复 Cursor 启动配置
self.fix_cursor_startup()
logging.error(f"更新 storage.json 失败: {str(e)}")
return False
# 处理更新程序
updater_path = Path(os.getenv('LOCALAPPDATA')) / "cursor-updater"
if updater_path.exists():
try:
if updater_path.is_dir():
shutil.rmtree(updater_path)
else:
updater_path.unlink()
except Exception as e:
logging.error(f"删除更新程序失败: {str(e)}")
return False
return True
except Exception as e:
logging.error(f"清理文件过程出错: {str(e)}")
logging.error(f"清理文件失败: {str(e)}")
return False
def disable_auto_update(self) -> bool:
"""禁用自动更新功能
Returns:
bool: 是否成功
"""
try:
updater_path = Path(os.getenv('LOCALAPPDATA')) / "cursor-updater"
# 删除现有目录/文件
if updater_path.exists():
if updater_path.is_dir():
shutil.rmtree(updater_path)
else:
updater_path.unlink()
# 创建空文件
updater_path.touch()
# 设置只读属性
import stat
updater_path.chmod(stat.S_IREAD)
# 设置文件权限(仅Windows)
if os.name == 'nt':
import subprocess
subprocess.run(
f'icacls "{updater_path}" /inheritance:r /grant:r "{os.getenv("USERNAME")}:(R)"',
shell=True,
check=True
)
logging.info("已禁用自动更新")
return True
except Exception as e:
logging.error(f"禁用自动更新失败: {str(e)}")
return False
def fix_cursor_startup(self) -> bool:

227
utils/cursor_resetter.py Normal file
View File

@@ -0,0 +1,227 @@
import os
import sys
import json
import logging
import subprocess
import uuid
from pathlib import Path
from datetime import datetime
from typing import Optional, Tuple, Dict
class CursorResetter:
"""Cursor重置工具类封装PowerShell脚本的核心功能"""
def __init__(self):
self.appdata = os.getenv('APPDATA')
self.localappdata = os.getenv('LOCALAPPDATA')
self.storage_file = Path(self.appdata) / "Cursor" / "User" / "globalStorage" / "storage.json"
self.backup_dir = Path(self.appdata) / "Cursor" / "User" / "globalStorage" / "backups"
self.cursor_path = Path(self.localappdata) / "Programs" / "cursor"
self.app_path = self.cursor_path / "resources" / "app"
self.package_json = self.app_path / "package.json"
def get_random_hex(self, length: int) -> str:
"""生成安全的随机十六进制字符串"""
import secrets
return secrets.token_hex(length)
def new_standard_machine_id(self) -> str:
"""生成标准格式的机器ID"""
template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
import random
def replace_char(match):
r = random.randint(0, 15)
v = r if match == 'x' else (r & 0x3 | 0x8)
return hex(v)[2:]
return ''.join(replace_char(c) for c in template)
def generate_ids(self) -> Dict[str, str]:
"""生成所有需要的ID"""
# 生成标准格式的ID
mac_machine_id = self.new_standard_machine_id()
uuid_str = str(uuid.uuid4())
# 生成带前缀的machineId
prefix = "auth0|user_"
prefix_hex = ''.join(hex(b)[2:].zfill(2) for b in prefix.encode())
random_part = self.get_random_hex(32)
machine_id = f"{prefix_hex}{random_part}"
# 生成大写的SQM ID
sqm_id = "{" + str(uuid.uuid4()).upper() + "}"
return {
"mac_machine_id": mac_machine_id,
"uuid": uuid_str,
"machine_id": machine_id,
"sqm_id": sqm_id
}
def backup_file(self, file_path: Path) -> Optional[Path]:
"""备份文件"""
try:
if not file_path.exists():
return None
self.backup_dir.mkdir(parents=True, exist_ok=True)
backup_name = f"{file_path.name}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = self.backup_dir / backup_name
import shutil
shutil.copy2(file_path, backup_path)
logging.info(f"已备份文件: {backup_path}")
return backup_path
except Exception as e:
logging.error(f"备份文件失败: {str(e)}")
return None
def update_machine_guid(self) -> bool:
"""更新系统MachineGuid"""
try:
import winreg
new_guid = str(uuid.uuid4())
registry_path = r"SOFTWARE\Microsoft\Cryptography"
# 备份原始值
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY) as key:
original_guid = winreg.QueryValueEx(key, "MachineGuid")[0]
# 备份到文件
self.backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = self.backup_dir / f"MachineGuid.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(original_guid)
# 更新GUID
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path, 0,
winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY) as key:
winreg.SetValueEx(key, "MachineGuid", 0, winreg.REG_SZ, new_guid)
logging.info(f"已更新系统MachineGuid: {new_guid}")
return True
except Exception as e:
logging.error(f"更新MachineGuid失败: {str(e)}")
return False
def update_storage_json(self) -> bool:
"""更新storage.json文件"""
try:
if not self.storage_file.exists():
logging.error(f"未找到配置文件: {self.storage_file}")
return False
# 备份文件
if not self.backup_file(self.storage_file):
logging.warning("配置文件备份失败")
# 生成新ID
ids = self.generate_ids()
# 读取并更新配置
with open(self.storage_file, "r", encoding="utf-8") as f:
config = json.load(f)
# 更新ID
config['telemetry.machineId'] = ids['machine_id']
config['telemetry.macMachineId'] = ids['mac_machine_id']
config['telemetry.devDeviceId'] = ids['uuid']
config['telemetry.sqmId'] = ids['sqm_id']
# 保存更新
with open(self.storage_file, "w", encoding="utf-8", newline='\n') as f:
json.dump(config, f, indent=2)
logging.info("已更新配置文件")
return True
except Exception as e:
logging.error(f"更新配置文件失败: {str(e)}")
return False
def disable_auto_update(self) -> bool:
"""禁用自动更新"""
try:
updater_path = Path(self.localappdata) / "cursor-updater"
# 删除现有文件/目录
if updater_path.exists():
if updater_path.is_dir():
import shutil
shutil.rmtree(updater_path)
else:
updater_path.unlink()
# 创建空文件并设置只读
updater_path.touch()
import stat
updater_path.chmod(stat.S_IREAD)
# 设置文件权限
if os.name == 'nt':
subprocess.run(
f'icacls "{updater_path}" /inheritance:r /grant:r "{os.getenv("USERNAME")}:(R)"',
shell=True,
check=True
)
logging.info("已禁用自动更新")
return True
except Exception as e:
logging.error(f"禁用自动更新失败: {str(e)}")
return False
def reset_cursor(self, disable_update: bool = True) -> Tuple[bool, str]:
"""重置Cursor
Args:
disable_update: 是否禁用自动更新
Returns:
Tuple[bool, str]: (是否成功, 消息)
"""
try:
# 1. 检查管理员权限
if os.name == 'nt':
import ctypes
if not ctypes.windll.shell32.IsUserAnAdmin():
return False, "需要管理员权限来执行重置操作"
# 2. 更新配置文件
if not self.update_storage_json():
return False, "更新配置文件失败"
# 3. 更新系统MachineGuid
if not self.update_machine_guid():
return False, "更新系统MachineGuid失败"
# 4. 禁用自动更新(如果需要)
if disable_update and not self.disable_auto_update():
logging.warning("禁用自动更新失败")
# 5. 修改package.json
if self.package_json.exists():
try:
with open(self.package_json, "r", encoding="utf-8") as f:
data = json.load(f)
if "machineId" in data:
del data["machineId"]
data["updateUrl"] = ""
data["disableUpdate"] = True
with open(self.package_json, "w", encoding="utf-8", newline='\n') as f:
json.dump(data, f, indent=2)
except Exception as e:
logging.warning(f"修改package.json失败: {str(e)}")
return True, "Cursor重置成功"
except Exception as e:
logging.error(f"重置过程出错: {str(e)}")
return False, f"重置失败: {str(e)}"

303
utils/version_manager.py Normal file
View File

@@ -0,0 +1,303 @@
import os
import sys
import requests
from packaging import version
from typing import Optional, Dict, Any
import json
import logging
from urllib.parse import quote, unquote
from pathlib import Path
class VersionManager:
"""版本管理器
错误码说明:
- 0: 成功
- 1: 一般性错误
- 401: 未授权或授权失败
- 404: 请求的资源不存在
- 500: 服务器内部错误
"""
def __init__(self):
self.base_url = "https://cursorapi.nosqli.com"
# 获取项目根目录路径
self.root_path = Path(__file__).parent.parent
self.current_version = self._get_current_version()
self.platform = "windows" if sys.platform.startswith("win") else "mac" if sys.platform.startswith("darwin") else "linux"
def _get_current_version(self) -> str:
"""获取当前版本号"""
try:
version_file = self.root_path / "version.txt"
if not version_file.exists():
logging.error(f"版本文件不存在: {version_file}")
return "0.0.0"
with open(version_file, "r", encoding="utf-8") as f:
version = f.read().strip()
logging.info(f"当前版本: {version}")
return version
except Exception as e:
logging.error(f"读取版本号失败: {str(e)}")
return "0.0.0"
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""处理API响应
Args:
response: API响应对象
Returns:
Dict[str, Any]: 处理后的响应数据
Raises:
Exception: API调用失败时抛出异常
"""
try:
data = response.json()
code = data.get("code")
msg = data.get("msg") or data.get("info", "未知错误") # 兼容 info 字段
if code == 0 or code == 1: # 兼容 code=1 的情况
# 处理空数据情况
if not data.get("data"):
logging.warning("API返回空数据")
return {
"code": 0,
"msg": msg,
"data": {
"has_update": False,
"is_force": False,
"version_info": None
}
}
return {
"code": 0, # 统一返回 code=0
"msg": msg,
"data": data.get("data")
}
elif code == 401:
raise Exception("未授权或授权失败")
elif code == 404:
raise Exception("请求的资源不存在")
elif code == 500:
raise Exception("服务器内部错误")
else:
raise Exception(msg)
except requests.exceptions.JSONDecodeError:
raise Exception("服务器响应格式错误")
def check_update(self) -> Dict[str, Any]:
"""检查是否有更新"""
try:
url = f"{self.base_url}/admin/api.version/check"
current_version = self.current_version.lstrip('v') # 移除可能存在的v前缀
params = {
"version": current_version,
"platform": self.platform
}
logging.info(f"正在请求: {url}")
logging.info(f"参数: {params}")
response = requests.get(
url,
params=params,
timeout=10
)
logging.info(f"状态码: {response.status_code}")
logging.info(f"响应头: {dict(response.headers)}")
logging.info(f"响应内容: {response.text}")
result = self._handle_response(response)
# 确保返回的数据包含版本信息
if result["code"] == 0 and result.get("data"):
data = result["data"]
if "version_info" in data:
version_info = data["version_info"]
# 确保版本号格式一致
if "version_no" in version_info:
version_info["version_no"] = version_info["version_no"].lstrip('v')
return result
except requests.exceptions.Timeout:
logging.error("检查更新超时")
return {"code": -1, "msg": "请求超时,请检查网络连接", "data": None}
except requests.exceptions.ConnectionError as e:
logging.error(f"检查更新连接失败: {str(e)}")
return {"code": -1, "msg": "连接服务器失败,请检查网络连接", "data": None}
except Exception as e:
logging.error(f"检查更新失败: {str(e)}")
return {"code": -1, "msg": str(e), "data": None}
def get_latest_version(self) -> Dict[str, Any]:
"""获取最新版本信息"""
try:
url = f"{self.base_url}/admin/api.version/latest"
params = {"platform": self.platform}
logging.info(f"正在请求: {url}")
logging.info(f"参数: {params}")
response = requests.get(
url,
params=params,
timeout=10
)
logging.info(f"状态码: {response.status_code}")
logging.info(f"响应头: {dict(response.headers)}")
logging.info(f"响应内容: {response.text}")
return self._handle_response(response)
except requests.exceptions.Timeout:
logging.error("获取最新版本超时")
return {"code": -1, "msg": "请求超时,请检查网络连接", "data": None}
except requests.exceptions.ConnectionError as e:
logging.error(f"获取最新版本连接失败: {str(e)}")
return {"code": -1, "msg": "连接服务器失败,请检查网络连接", "data": None}
except Exception as e:
logging.error(f"获取最新版本失败: {str(e)}")
return {"code": -1, "msg": str(e), "data": None}
def needs_update(self) -> tuple[bool, bool, Optional[Dict[str, Any]]]:
"""检查是否需要更新
Returns:
tuple: (是否有更新, 是否强制更新, 版本信息)
"""
try:
result = self.check_update()
if result["code"] == 0 and result["data"]:
data = result["data"]
version_info = data.get("version_info", {})
# 比较版本号(移除v前缀)
current = self.current_version.lstrip('v')
latest = version_info.get("version_no", "0.0.0").lstrip('v')
# 使用packaging.version进行版本比较
has_update = version.parse(latest) > version.parse(current)
return (
has_update,
bool(data.get("is_force")),
version_info
)
return False, False, None
except Exception as e:
logging.error(f"检查更新失败: {str(e)}")
return False, False, None
def download_update(self, download_url: str, save_path: str) -> tuple[bool, str]:
"""下载更新文件
Args:
download_url: 下载地址
save_path: 保存路径
Returns:
tuple[bool, str]: (是否下载成功, 错误信息)
"""
try:
if not download_url:
error_msg = "下载地址为空,请联系管理员"
logging.error(error_msg)
return False, error_msg
# 处理下载地址中的中文字符
url_parts = download_url.split('/')
# 只对最后一部分(文件名)进行编码
url_parts[-1] = quote(url_parts[-1])
encoded_url = '/'.join(url_parts)
logging.info(f"原始下载地址: {download_url}")
logging.info(f"编码后的地址: {encoded_url}")
# 设置请求头,模拟浏览器行为
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}
response = requests.get(
encoded_url,
stream=True,
headers=headers,
timeout=30 # 增加下载超时时间
)
# 检查响应状态
if response.status_code == 404:
error_msg = "下载地址无效,请联系管理员更新下载地址"
logging.error(error_msg)
return False, error_msg
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
if total_size == 0:
error_msg = "无法获取文件大小,下载地址可能无效,请联系管理员"
logging.error(error_msg)
return False, error_msg
block_size = 8192
downloaded_size = 0
logging.info(f"开始下载文件,总大小: {total_size} 字节")
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=block_size):
if chunk:
f.write(chunk)
downloaded_size += len(chunk)
# 打印下载进度
if total_size > 0:
progress = (downloaded_size / total_size) * 100
logging.info(f"下载进度: {progress:.2f}%")
# 验证文件大小
actual_size = os.path.getsize(save_path)
if actual_size != total_size:
error_msg = f"文件下载不完整: 预期{total_size}字节,实际{actual_size}字节,请重试或联系管理员"
logging.error(error_msg)
# 删除不完整文件
try:
os.remove(save_path)
logging.info(f"已删除不完整的下载文件: {save_path}")
except Exception as clean_e:
logging.error(f"清理不完整文件失败: {str(clean_e)}")
return False, error_msg
logging.info(f"文件下载完成: {save_path}")
return True, "下载成功"
except requests.exceptions.Timeout:
error_msg = "下载超时,请检查网络连接后重试"
logging.error(error_msg)
return False, error_msg
except requests.exceptions.ConnectionError as e:
error_msg = "下载连接失败,请检查网络连接后重试"
logging.error(f"{error_msg}: {str(e)}")
return False, error_msg
except requests.exceptions.HTTPError as e:
error_msg = f"下载地址无效或服务器错误,请联系管理员 (HTTP {e.response.status_code})"
logging.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"下载失败,请联系管理员: {str(e)}"
logging.error(error_msg)
# 如果下载失败,删除可能存在的不完整文件
try:
if os.path.exists(save_path):
os.remove(save_path)
logging.info(f"已删除不完整的下载文件: {save_path}")
except Exception as clean_e:
logging.error(f"清理不完整文件失败: {str(clean_e)}")
return False, error_msg

View File

@@ -1 +1 @@
3.4.0
3.4.7

127
version_check.log Normal file
View File

@@ -0,0 +1,127 @@
2025-02-13 13:30:48,255 - INFO - <20><>ǰ<EFBFBD>汾: 3.4.1
2025-02-13 13:30:48,255 - INFO - <20><>ǰƽ̨: windows
2025-02-13 13:30:48,255 - INFO -
=== <20><><EFBFBD>Ի<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>°汾 ===
2025-02-13 13:30:49,989 - INFO - <20><><EFBFBD>°汾<C2B0><E6B1BE>Ϣ: {'code': 0, 'info': '<27><><EFBFBD>ް汾<DEB0><E6B1BE>Ϣ', 'data': {}}
2025-02-13 13:30:49,989 - INFO -
=== <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
2025-02-13 13:30:51,712 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:30:51,713 - INFO - <20><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:30:51,713 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> ===
2025-02-13 13:30:53,394 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:30:53,395 - INFO - <20>Ƿ<EFBFBD><C7B7>и<EFBFBD><D0B8><EFBFBD>: False
2025-02-13 13:30:53,395 - INFO - <20>Ƿ<EFBFBD>ǿ<EFBFBD>Ƹ<EFBFBD><C6B8><EFBFBD>: False
2025-02-13 13:30:53,395 - INFO - <20><EFBFBD><E6B1BE>Ϣ: None
2025-02-13 13:49:13,952 - INFO - <20><>ǰ<EFBFBD>汾: 3.4.1
2025-02-13 13:49:13,952 - INFO - <20><>ǰƽ̨: windows
2025-02-13 13:49:13,952 - INFO -
=== <20><><EFBFBD>Ի<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>°汾 ===
2025-02-13 13:49:15,718 - ERROR - <20><>ȡ<EFBFBD><C8A1><EFBFBD>°汾ʧ<E6B1BE><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:49:15,720 - INFO - <20><><EFBFBD>°汾<C2B0><E6B1BE>Ϣ: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:49:15,720 - INFO -
=== <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
2025-02-13 13:49:17,452 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:49:17,454 - INFO - <20><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:49:17,454 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> ===
2025-02-13 13:49:19,277 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:49:19,278 - INFO - <20>Ƿ<EFBFBD><C7B7>и<EFBFBD><D0B8><EFBFBD>: False
2025-02-13 13:49:19,278 - INFO - <20>Ƿ<EFBFBD>ǿ<EFBFBD>Ƹ<EFBFBD><C6B8><EFBFBD>: False
2025-02-13 13:49:19,278 - INFO - <20><EFBFBD><E6B1BE>Ϣ: None
2025-02-13 13:53:02,577 - INFO - <20><>ǰ<EFBFBD>汾: 3.4.1
2025-02-13 13:53:02,577 - INFO - <20><>ǰƽ̨: windows
2025-02-13 13:53:02,577 - INFO -
=== <20><><EFBFBD>Ի<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>°汾 ===
2025-02-13 13:53:02,578 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/latest
2025-02-13 13:53:02,578 - INFO - <20><><EFBFBD><EFBFBD>: {'platform': 'windows'}
2025-02-13 13:53:04,292 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:04,292 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:02 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=c6053c5e6170796bf1c5dde92415b981; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:04,292 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><>ȡ<EFBFBD>ɹ<EFBFBD>","data":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}
2025-02-13 13:53:04,292 - ERROR - <20><>ȡ<EFBFBD><C8A1><EFBFBD>°汾ʧ<E6B1BE><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:04,294 - INFO - <20><><EFBFBD>°汾<C2B0><E6B1BE>Ϣ: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:53:04,294 - INFO -
=== <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
2025-02-13 13:53:04,294 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:53:04,294 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:53:06,028 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:06,028 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:04 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=cbb7943860ca50662d842719c53e7c73; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:06,028 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:53:06,028 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:06,029 - INFO - <20><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:53:06,029 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> ===
2025-02-13 13:53:06,029 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:53:06,029 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:53:07,770 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:07,770 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:05 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=c8004bac4b2d4c5054b69dca0311d6f7; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:07,770 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:53:07,771 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:07,774 - INFO - <20>Ƿ<EFBFBD><C7B7>и<EFBFBD><D0B8><EFBFBD>: False
2025-02-13 13:53:07,774 - INFO - <20>Ƿ<EFBFBD>ǿ<EFBFBD>Ƹ<EFBFBD><C6B8><EFBFBD>: False
2025-02-13 13:53:07,774 - INFO - <20><EFBFBD><E6B1BE>Ϣ: None
2025-02-13 13:53:33,800 - INFO - <20><>ǰ<EFBFBD>汾: 3.4.1
2025-02-13 13:53:33,801 - INFO - <20><>ǰƽ̨: windows
2025-02-13 13:53:33,801 - INFO -
=== <20><><EFBFBD>Ի<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>°汾 ===
2025-02-13 13:53:33,801 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/latest
2025-02-13 13:53:33,801 - INFO - <20><><EFBFBD><EFBFBD>: {'platform': 'windows'}
2025-02-13 13:53:35,509 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:35,510 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:33 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=16d07427624c6aaf6c89254d173fe273; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:35,510 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><>ȡ<EFBFBD>ɹ<EFBFBD>","data":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}
2025-02-13 13:53:35,510 - ERROR - <20><>ȡ<EFBFBD><C8A1><EFBFBD>°汾ʧ<E6B1BE><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:35,513 - INFO - <20><><EFBFBD>°汾<C2B0><E6B1BE>Ϣ: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:53:35,513 - INFO -
=== <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
2025-02-13 13:53:35,513 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:53:35,513 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:53:37,280 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:37,281 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:35 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=489a85b5766d7a30c4ba9dccda6f4967; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:37,281 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:53:37,281 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:37,283 - INFO - <20><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {'code': -1, 'msg': 'δ֪<CEB4><D6AA><EFBFBD><EFBFBD>', 'data': None}
2025-02-13 13:53:37,283 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> ===
2025-02-13 13:53:37,283 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:53:37,284 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:53:39,003 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:53:39,003 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:53:37 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=b3619976145458f7ffd03d0438958a73; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:53:39,004 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:53:39,004 - ERROR - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: δ֪<CEB4><D6AA><EFBFBD><EFBFBD>
2025-02-13 13:53:39,005 - INFO - <20>Ƿ<EFBFBD><C7B7>и<EFBFBD><D0B8><EFBFBD>: False
2025-02-13 13:53:39,005 - INFO - <20>Ƿ<EFBFBD>ǿ<EFBFBD>Ƹ<EFBFBD><C6B8><EFBFBD>: False
2025-02-13 13:53:39,005 - INFO - <20><EFBFBD><E6B1BE>Ϣ: None
2025-02-13 13:54:24,914 - INFO - <20><>ǰ<EFBFBD>汾: 3.4.1
2025-02-13 13:54:24,915 - INFO - <20><>ǰƽ̨: windows
2025-02-13 13:54:24,915 - INFO -
=== <20><><EFBFBD>Ի<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>°汾 ===
2025-02-13 13:54:24,915 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/latest
2025-02-13 13:54:24,915 - INFO - <20><><EFBFBD><EFBFBD>: {'platform': 'windows'}
2025-02-13 13:54:26,652 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:54:26,652 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:54:24 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=fc779e7ca81172e81a4d03cab86876a1; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:54:26,652 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><>ȡ<EFBFBD>ɹ<EFBFBD>","data":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}
2025-02-13 13:54:26,654 - INFO - <20><><EFBFBD>°汾<C2B0><E6B1BE>Ϣ: {'code': 0, 'msg': '<27><>ȡ<EFBFBD>ɹ<EFBFBD>', 'data': {'id': 1, 'version_no': '3.4.1.4', 'version_name': 'cursor<6F><72><EFBFBD><EFBFBD>', 'download_url': 'https://cursorapi.nosqli.com/upload/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe', 'is_force': 0, 'min_version': '', 'platform': 'all', 'status': 1, 'description': '', 'create_time': '2025-02-13 13:32:35', 'update_time': '2025-02-13 13:32:35'}}
2025-02-13 13:54:26,654 - INFO -
=== <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
2025-02-13 13:54:26,654 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:54:26,654 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:54:28,445 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:54:28,445 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:54:26 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=f8a3f46919c8aaa4d8f34d361ea3386a; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:54:28,445 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:54:28,447 - INFO - <20><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {'code': 0, 'msg': '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>', 'data': {'has_update': True, 'is_force': 0, 'version_info': {'id': 1, 'version_no': '3.4.1.4', 'version_name': 'cursor<6F><72><EFBFBD><EFBFBD>', 'download_url': 'https://cursorapi.nosqli.com/upload/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe', 'is_force': 0, 'min_version': '', 'platform': 'all', 'status': 1, 'description': '', 'create_time': '2025-02-13 13:32:35', 'update_time': '2025-02-13 13:32:35'}}}
2025-02-13 13:54:28,447 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> ===
2025-02-13 13:54:28,447 - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://cursorapi.nosqli.com/admin/api.version/check
2025-02-13 13:54:28,447 - INFO - <20><><EFBFBD><EFBFBD>: {'version': '3.4.1', 'platform': 'windows'}
2025-02-13 13:54:30,144 - INFO - ״̬<D7B4><CCAC>: 200
2025-02-13 13:54:30,145 - INFO - <20><>Ӧͷ: {'Server': 'nginx', 'Date': 'Thu, 13 Feb 2025 05:54:28 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'sameorigin', 'Set-Cookie': 'ssid=169a8bdefde9a16f0e9f3e32da4d8ba5; path=/; secure; HttpOnly, lang=zh-cn; path=/; secure; HttpOnly', 'Strict-Transport-Security': 'max-age=31536000', 'Alt-Svc': 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"', 'Content-Encoding': 'gzip'}
2025-02-13 13:54:30,145 - INFO - <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>: {"code":1,"info":"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>","data":{"has_update":true,"is_force":0,"version_info":{"id":1,"version_no":"3.4.1.4","version_name":"cursor<6F><72><EFBFBD><EFBFBD>","download_url":"https:\/\/cursorapi.nosqli.com\/upload\/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe","is_force":0,"min_version":"","platform":"all","status":1,"description":"","create_time":"2025-02-13 13:32:35","update_time":"2025-02-13 13:32:35"}}}
2025-02-13 13:54:30,146 - INFO - <20>Ƿ<EFBFBD><C7B7>и<EFBFBD><D0B8><EFBFBD>: True
2025-02-13 13:54:30,146 - INFO - <20>Ƿ<EFBFBD>ǿ<EFBFBD>Ƹ<EFBFBD><C6B8><EFBFBD>: False
2025-02-13 13:54:30,146 - INFO - <20><EFBFBD><E6B1BE>Ϣ: {'id': 1, 'version_no': '3.4.1.4', 'version_name': 'cursor<6F><72><EFBFBD><EFBFBD>', 'download_url': 'https://cursorapi.nosqli.com/upload/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe', 'is_force': 0, 'min_version': '', 'platform': 'all', 'status': 1, 'description': '', 'create_time': '2025-02-13 13:32:35', 'update_time': '2025-02-13 13:32:35'}
2025-02-13 13:54:30,146 - INFO -
=== <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD> ===
2025-02-13 13:54:30,148 - INFO - <20><><EFBFBD>ص<EFBFBD>ַ: https://cursorapi.nosqli.com/upload/<2F><>Ȫcursor<6F><72><EFBFBD><EFBFBD>v3.4.1.4.exe
2025-02-13 13:54:30,148 - INFO - <20><><EFBFBD><EFBFBD>·<EFBFBD><C2B7>: C:\Users\huangzhen\Downloads\CursorHelper\test_update.exe
2025-02-13 13:54:31,822 - ERROR - <20><><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>ʧ<EFBFBD><CAA7>: 404 Client Error: Not Found for url: https://cursorapi.nosqli.com/upload/%E5%90%AC%E6%B3%89cursor%E5%8A%A9%E6%89%8Bv3.4.1.4.exe
2025-02-13 13:54:31,823 - INFO - <20><><EFBFBD>ؽ<EFBFBD><D8BD><EFBFBD>: ʧ<><CAA7>

70
versioncheck.doc Normal file
View File

@@ -0,0 +1,70 @@
版本更新API文档
域名
base_url: https://cursorapi.nosqli.com
# 版本更新API文档
#
* 公共返回参数:
* - code: 错误码0表示成功非0表示失败
* - msg: 提示信息
* - data: 返回的数据,请求失败时可能为空
*
* 错误码说明:
* - 0: 成功
* - 1: 一般性错误(具体错误信息见msg)
* - 401: 未授权或授权失败
* - 404: 请求的资源不存在
* - 500: 服务器内部错误
*
* 版本号格式x.x.x (例如: 3.4.1)
* 平台类型:
* - all: 全平台
* - windows: Windows平台
* - mac: Mac平台
* - linux: Linux平台
* ====================================================
*
* 1. 获取最新版本 [GET] /admin/api.version/latest
* 请求参数:
* - platform: 平台类型(all|windows|mac|linux), 默认为all
* 返回数据:
* {
* "code": 0,
* "msg": "获取成功",
* "data": {
* "id": "1",
* "version_no": "3.4.1.4",
* "version_name": "听泉cursor助手",
* "download_url": "http://domain/upload/xxx.exe",
* "is_force": 1, // 是否强制更新(1是,0否)
* "min_version": "3.4.0.0", // 最低要求版本
* "platform": "all", // 平台类型
* "description": "版本描述", // 版本描述
* "status": 1, // 状态(1启用,0禁用)
* "create_time": "2024-03-20 10:00:00"
* }
* }
*
* 2. 检查版本更新 [GET] /admin/api.version/check
* 请求参数:
* - version: 当前版本号(必填)
* - platform: 平台类型(all|windows|mac|linux), 默认为all
* 返回数据:
* {
* "code": 0,
* "msg": "检查完成",
* "data": {
* "has_update": true, // 是否有更新
* "is_force": 1, // 是否强制更新
* "version_info": { // 新版本信息(has_update为true时返回)
* // 同上面的版本信息
* }
* }
* }
*
* 错误返回示例:
* {
* "code": 1,
* "msg": "请提供当前版本号",
* "data": null
* }