10 Commits

Author SHA1 Message Date
huangzhenpc
b11452aea8 feat: 发布 v3.5.0 版本 2025-02-14 16:15:04 +08:00
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
huangzhenpc
e3b5820d8b 界面优化: 简化UI设计和布局 1. 界面布局优化 - 移除多余的框架,使用分隔线代替 - 统一输入框和文本框样式 - 优化整体边距和间距 2. 视觉效果改进 - 添加渐变背景 - 保留重要信息的视觉强调 - 简化界面层次结构 3. 其他改进 - 优化使用说明的显示效果 - 更新版本截图 2025-02-13 11:06:36 +08:00
huangzhenpc
4ee3ac066e 功能优化: 系统托盘与测试版本构建 1. 系统托盘功能增强 - 添加托盘右键菜单刷新重置功能 - 优化托盘图标显示和管理 - 改进最小化到托盘的处理逻辑 2. 测试版本构建系统 - 新增 testbuild.bat 测试版本构建脚本 - 实现测试版本号自动递增 - 配置测试版本专用输出目录 3. 其他优化 - 优化 SSL 警告全局处理 - 改进加载对话框显示逻辑 - 完善版本号管理机制 2025-02-13 10:44:58 +08:00
huangzhenpc
30aab5f9b2 优化: 1. 修复SSL警告问题 2. 优化加载对话框显示逻辑 3. 添加API测试工具 2025-02-13 10:13:22 +08:00
huangzhenpc
207c3c604e v3.3.9版本更新: 1.优化UI界面和按钮样式 2.改进使用说明文字,增加颜色标注 3.优化加载对话框样式 4.添加请求节流机制,防止重复提交 5.完善错误处理和提示信息 6.优化会员状态检查逻辑 7.改进禁用更新功能的实现 2025-02-12 21:15:58 +08:00
20 changed files with 3878 additions and 916 deletions

4
.gitignore vendored
View File

@@ -17,4 +17,6 @@ venv/
env/
# Local development settings
.env
.env
testversion.txt

File diff suppressed because it is too large Load Diff

BIN
banbenjietu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

115
build.bat
View File

@@ -1,12 +1,119 @@
@echo off
chcp 65001
echo 开始打包流程...
setlocal EnableDelayedExpansion
echo 开始正式版本打包...
:: 设置工作目录为脚本所在目录
cd /d "%~dp0"
:: 激活虚拟环境
if exist "venv\Scripts\activate.bat" (
echo 激活虚拟环境...
call venv\Scripts\activate.bat
) else (
echo 警告: 未找到虚拟环境,使用系统 Python
)
:: 确保安装了必要的包
echo 检查依赖包...
pip install -r requirements.txt
:: 更新版本号
python update_version.py
:: 使用新的spec文件进行打包
pyinstaller --noconfirm build_nezha.spec
:: 读取版本号
set /p VERSION=<version.txt
echo 当前版本: !VERSION!
echo 打包完成!
:: 提取主版本号和次版本号 (3.4.7 -> 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!
)
:: 清理 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 清理旧文件...
if exist "build" rd /s /q "build"
if exist "*.spec" del /f /q "*.spec"
:: 使用优化选项进行打包
echo 开始打包...
pyinstaller ^
--noconfirm ^
--clean ^
--onefile ^
--noconsole ^
--icon=icon/two.ico ^
--name "听泉cursor助手!VERSION!" ^
--add-data "icon;icon" ^
--add-data "version.txt;." ^
--add-data "testversion.txt;." ^
--add-data "requirements.txt;." ^
--exclude-module _tkinter ^
--exclude-module tkinter ^
--exclude-module PIL.ImageTk ^
--exclude-module PIL.ImageWin ^
--exclude-module numpy ^
--exclude-module pandas ^
--exclude-module matplotlib ^
--exclude "__pycache__" ^
--exclude "*.pyc" ^
--exclude "*.pyo" ^
--exclude "*.pyd" ^
main.py
:: 检查打包结果并移动文件
set TEMP_FILE=dist\听泉cursor助手!VERSION!.exe
set TARGET_FILE=!VERSION_DIR!\听泉cursor助手v!VERSION!.exe
echo 检查文件: !TEMP_FILE!
if exist "!TEMP_FILE!" (
echo 正式版本打包成功!
:: 移动到版本目录
echo 移动文件到: !TARGET_FILE!
move "!TEMP_FILE!" "!TARGET_FILE!"
:: 显示文件大小
for %%I in ("!TARGET_FILE!") do (
echo 文件大小: %%~zI 字节
)
echo.
echo 正式版本构建完成!
echo 版本号: v!VERSION!
echo 文件位置: !TARGET_FILE!
) else (
echo 错误: 打包失败,文件不存在
echo 预期文件路径: !TEMP_FILE!
dir /b dist
exit /b 1
)
:: 清理临时文件
echo 清理临时文件...
if exist "build" rd /s /q "build"
if exist "dist\听泉cursor助手!VERSION!.exe" del /f /q "dist\听泉cursor助手!VERSION!.exe"
if exist "*.spec" del /f /q "*.spec"
:: 退出虚拟环境
if exist "venv\Scripts\activate.bat" (
echo 退出虚拟环境...
deactivate
)
endlocal
pause

View File

@@ -1,51 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import os
def get_version():
with open('version.txt', 'r', encoding='utf-8') as f:
version = f.read().strip()
return version
version = get_version()
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('icon', 'icon'), ('version.txt', '.')],
hiddenimports=[
'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
'requests', 'urllib3', 'certifi', # 网络请求相关
'json', 'uuid', 'hashlib', 'logging' # 基础功能相关
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['_tkinter', 'tkinter', 'Tkinter'], # 排除 tkinter 相关模块
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name=f'听泉cursor助手{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=['icon\\two.ico'],
)

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

145
gui/main_window_new.py Normal file
View File

@@ -0,0 +1,145 @@
import sys
from pathlib import Path
import logging
import os
from PIL import Image
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFrame, QTextEdit,
QMessageBox, QApplication, QSystemTrayIcon, QMenu,
QDialog, QProgressBar, QStyle)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap
import time
import requests
from urllib.parse import quote
import subprocess
sys.path.append(str(Path(__file__).parent.parent))
from utils.config import Config
from utils.version_manager import VersionManager
from account_switcher import AccountSwitcher
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.config = Config()
self.switcher = AccountSwitcher()
self.version_manager = VersionManager()
# 添加激活状态缓存
self._activation_status = None # 缓存的激活状态
self._status_timer = None # 状态更新定时器
# 添加心跳定时器
self._heartbeat_timer = QTimer()
self._heartbeat_timer.timeout.connect(self.send_heartbeat)
self._heartbeat_timer.start(5 * 60 * 1000) # 每5分钟发送一次心跳
# 添加请求锁,防止重复提交
self._is_requesting = False
self._last_request_time = 0
self._request_cooldown = 2 # 请求冷却时间(秒)
version = get_version()
cursor_version = self.switcher.get_cursor_version()
self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})")
self.setMinimumSize(600, 680) # 增加最小宽度到600
# 设置窗口图标
icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icon", "two.ico")
if os.path.exists(icon_path):
self.window_icon = QIcon(icon_path)
if not self.window_icon.isNull():
self.setWindowIcon(self.window_icon)
logging.info(f"成功设置窗口图标: {icon_path}")
else:
logging.warning("图标文件加载失败")
# 创建系统托盘图标
self.create_tray_icon()
# 创建主窗口部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 设置主窗口样式
central_widget.setStyleSheet("""
QWidget {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #f8f9fa,
stop:0.5 #ffffff,
stop:1 #f8f9fa);
}
QFrame {
border: none;
background: transparent;
}
""")
# 创建主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(12) # 减小区域间距
main_layout.setContentsMargins(20, 15, 20, 15) # 调整外边距
# 启动时检查一次状态
QTimer.singleShot(0, self.check_status)
# 启动时自动检查更新
QTimer.singleShot(1000, self.check_for_updates)
def send_heartbeat(self):
"""发送心跳请求"""
if not self._check_request_throttle():
return
def heartbeat_func():
return self.switcher.send_heartbeat()
# 创建工作线程
self.heartbeat_worker = ApiWorker(heartbeat_func)
self.heartbeat_worker.finished.connect(self.on_heartbeat_complete)
self.heartbeat_worker.start()
def on_heartbeat_complete(self, result):
"""心跳完成回调"""
success, message = result
self._request_complete()
if success:
logging.info(f"心跳发送成功: {message}")
# 更新状态显示
self.check_status()
else:
logging.error(f"心跳发送失败: {message}")
def closeEvent(self, event):
"""窗口关闭事件"""
try:
if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
event.ignore()
self.hide()
# 确保托盘图标显示
self.tray_icon.show()
self.tray_icon.showMessage(
"听泉Cursor助手",
"程序已最小化到系统托盘",
QSystemTrayIcon.Information,
2000
)
else:
# 如果托盘图标不可用,则正常退出
if self._status_timer:
self._status_timer.stop()
if hasattr(self, '_heartbeat_timer'):
self._heartbeat_timer.stop()
event.accept()
except Exception as e:
logging.error(f"处理关闭事件时发生错误: {str(e)}")
# 发生错误时,接受关闭事件
if self._status_timer:
self._status_timer.stop()
if hasattr(self, '_heartbeat_timer'):
self._heartbeat_timer.stop()
event.accept()

157
main.py
View File

@@ -5,11 +5,43 @@ import os
import atexit
import shutil
import tempfile
import urllib3
import ctypes
import win32event
import win32api
import winerror
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()
logging.getLogger('urllib3').setLevel(logging.ERROR)
def prevent_multiple_instances():
"""防止程序多开
Returns:
bool: 如果是第一个实例返回True否则返回False
"""
try:
# 创建一个唯一的互斥锁名称
mutex_name = "Global\\CursorHelper_SingleInstance_Lock"
# 尝试创建互斥锁
handle = win32event.CreateMutex(None, 1, mutex_name)
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
# 互斥锁已存在,说明程序已经在运行
logging.warning("程序已经在运行")
QMessageBox.warning(None, "警告", "程序已经在运行!\n请不要重复打开程序。")
return False
return True
except Exception as e:
logging.error(f"检查程序实例失败: {str(e)}")
return True # 如果检查失败,允许程序运行
def cleanup_temp():
"""清理临时文件"""
@@ -34,77 +66,120 @@ 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:
# 注册退出时的清理函数
# 0. 检查是否已有实例在运行
if not prevent_multiple_instances():
return 1
# 1. 首先检查管理员权限
if not is_admin():
if run_as_admin():
return 0
else:
QMessageBox.critical(None, "错误", "需要管理员权限运行此程序。\n请右键点击程序,选择'以管理员身份运行'")
return 1
# 2. 注册退出时的清理函数
atexit.register(cleanup_temp)
# 3. 设置日志
setup_logging()
# 检查Python版本
logging.info(f"Python版本: {sys.version}")
# 检查工作目录
logging.info(f"当前工作目录: {Path.cwd()}")
# 检查模块路径
logging.info("Python路径:")
for p in sys.path:
logging.info(f" - {p}")
logging.info("正在初始化主窗口...")
# 4. 创建QApplication实例
app = QApplication(sys.argv)
# 设置应用程序ID (在设置图标之前)
# 5. 检查系统托盘
if not QSystemTrayIcon.isSystemTrayAvailable():
logging.error("系统托盘不可用")
QMessageBox.critical(None, "错误", "系统托盘不可用,程序无法正常运行。")
return 1
# 6. 设置应用程序不会在最后一个窗口关闭时退出
app.setQuitOnLastWindowClosed(False)
# 7. 记录系统信息
logging.info(f"Python版本: {sys.version}")
logging.info(f"当前工作目录: {Path.cwd()}")
# 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()
sys.exit(app.exec_())
# 11. 运行应用程序
return app.exec_()
except Exception as e:
error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
# 使用 QMessageBox 显示错误
app = QApplication(sys.argv)
QMessageBox.critical(None, "错误", error_msg)
sys.exit(1)
return 1
if __name__ == "__main__":
main()
sys.exit(main())

View File

@@ -2,8 +2,10 @@
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
requests==2.31.0
setuptools>=68.0.0
altgraph>=0.17.4
pyinstaller==6.3.0
pillow==10.2.0 # For icon processing
setuptools==65.5.1 # Fix pkg_resources.extern issue
PyQt5==5.15.10 # GUI framework
pywin32==306 # Windows API support
pillow==10.2.0
PyQt5==5.15.10
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()

123
testbuild.bat Normal file
View File

@@ -0,0 +1,123 @@
@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 检查依赖包...
pip install -r requirements.txt
:: 读取版本号
set /p VERSION=<version.txt
echo 当前版本: !VERSION!
:: 提取主版本号和次版本号 (3.4.7 -> 3.4)
for /f "tokens=1,2 delims=." %%a in ("!VERSION!") do (
set MAJOR_VERSION=%%a.%%b
)
echo 主版本目录: !MAJOR_VERSION!
:: 读取测试版本号(如果存在)
if exist testversion.txt (
set /p TEST_VERSION=<testversion.txt
) else (
set TEST_VERSION=0
)
:: 增加测试版本号
set /a TEST_VERSION+=1
echo !TEST_VERSION!>testversion.txt
echo 测试版本号: !TEST_VERSION!
:: 组合完整版本号
set FULL_VERSION=!VERSION!.!TEST_VERSION!
echo 完整版本号: !FULL_VERSION!
:: 创建测试版本目录
set TEST_DIR=dist\test\!MAJOR_VERSION!
if not exist "!TEST_DIR!" (
mkdir "!TEST_DIR!"
echo 创建目录: !TEST_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 清理旧文件...
if exist "build" rd /s /q "build"
if exist "*.spec" del /f /q "*.spec"
:: 使用优化选项进行打包
echo 开始打包...
pyinstaller ^
--noconfirm ^
--clean ^
--onefile ^
--noconsole ^
--icon=icon/two.ico ^
--name "听泉cursor助手_test" ^
--add-data "icon;icon" ^
--add-data "version.txt;." ^
--add-data "testversion.txt;." ^
--add-data "requirements.txt;." ^
--exclude-module _tkinter ^
--exclude-module tkinter ^
--exclude-module PIL.ImageTk ^
--exclude-module PIL.ImageWin ^
--exclude-module numpy ^
--exclude-module pandas ^
--exclude-module matplotlib ^
--exclude "__pycache__" ^
--exclude "*.pyc" ^
--exclude "*.pyo" ^
--exclude "*.pyd" ^
main.py
:: 检查打包结果并移动文件
set TEMP_FILE=dist\听泉cursor助手_test.exe
set TARGET_FILE=!TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe
echo 检查文件: !TEMP_FILE!
if exist "!TEMP_FILE!" (
echo 测试打包成功!
:: 移动到版本目录
echo 移动文件到: !TARGET_FILE!
move "!TEMP_FILE!" "!TARGET_FILE!"
:: 显示文件大小
for %%I in ("!TARGET_FILE!") do (
echo 文件大小: %%~zI 字节
)
echo.
echo 测试版本构建完成!
echo 版本号: v!FULL_VERSION!
echo 文件位置: !TARGET_FILE!
) else (
echo 错误: 打包失败,文件不存在
)
:: 退出虚拟环境
if exist "venv\Scripts\activate.bat" (
echo 退出虚拟环境...
deactivate
)
endlocal
pause

View File

@@ -4,8 +4,16 @@ import logging
from pathlib import Path
class Config:
"""配置类"""
def __init__(self):
self.api_base_url = "https://cursorapi.nosqli.com/admin"
self.base_url = "https://cursorapi.nosqli.com"
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",
"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"
self.member_file = self.config_dir / "member.json"
@@ -64,4 +72,15 @@ class Config:
}
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
self.api_token = api_token
self.api_token = api_token
def get_api_url(self, endpoint_name: str) -> str:
"""获取API端点URL
Args:
endpoint_name: 端点名称
Returns:
str: 完整的API URL
"""
return self.api_endpoints.get(endpoint_name, "")

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)}"

311
utils/version_manager.py Normal file
View File

@@ -0,0 +1,311 @@
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:
# 首先尝试从打包后的路径读取
if getattr(sys, 'frozen', False):
# 如果是打包后的程序
base_path = Path(sys._MEIPASS)
else:
# 如果是开发环境
base_path = self.root_path
version_file = base_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.3.4
3.5.0

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
* }