CursorTokenLogin/main.py

1196 lines
43 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import json
import shutil
import uuid
import random
import string
import os
import tempfile
import psutil
import subprocess
import sqlite3
import requests
from contextlib import contextmanager
from pathlib import Path
from typing import Optional
__VERSION__ = "0.0.3"
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QMessageBox,
QTextEdit,
QPushButton,
QVBoxLayout,
QHBoxLayout,
QWidget,
QLineEdit,
QLabel,
QGroupBox,
QDialog,
QScrollArea,
QSplashScreen,
)
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QMetaObject, Q_ARG, Slot
from PySide6.QtUiTools import QUiLoader
from PySide6.QtGui import QFont, QPixmap, QColor, QPainter, QPalette, QIcon
def get_resource_path(relative_path):
"""获取资源文件的绝对路径支持打包后的应用onefile 内嵌或 exe 旁便携文件)。"""
rel = Path(relative_path)
if getattr(sys, "frozen", False):
candidates = []
if hasattr(sys, "_MEIPASS"):
candidates.append(Path(sys._MEIPASS) / rel)
# onedir / 便携layout 与 exe 同目录
candidates.append(Path(sys.executable).resolve().parent / rel)
for p in candidates:
try:
if p.is_file():
return str(p.resolve())
except OSError:
continue
# 用于报错信息:优先 MEIPASS
primary = (
Path(sys._MEIPASS) / rel
if hasattr(sys, "_MEIPASS")
else Path(sys.executable).resolve().parent / rel
)
return str(primary.resolve())
return str((Path(__file__).parent / rel).resolve())
def get_app_icon() -> QIcon:
"""窗口标题栏 / 任务栏图标(使用根目录 logo.ico打包后需将 logo.ico 一并打入资源)。"""
p = Path(get_resource_path("logo.ico"))
if p.is_file():
return QIcon(str(p))
return QIcon()
def is_windows_pe_executable(path: Path) -> bool:
"""粗略校验是否为 Windows PE避免把 HTML/JSON 当成 exe 替换)。"""
try:
if path.stat().st_size < 64 * 1024:
return False
with open(path, "rb") as f:
return f.read(2) == b"MZ"
except OSError:
return False
def get_default_cursor_path():
if sys.platform == "win32":
paths = [
Path(os.environ.get("LOCALAPPDATA", ""))
/ "Programs"
/ "cursor"
/ "Cursor.exe",
Path(os.environ.get("PROGRAMFILES", "")) / "Cursor" / "Cursor.exe",
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Cursor" / "Cursor.exe",
]
for path in paths:
if path.exists():
return str(path)
elif sys.platform == "darwin":
return "/Applications/Cursor.app"
else:
return "/usr/bin/cursor"
return ""
def is_cursor_running():
"""检测Cursor是否正在运行"""
cursor_exe_names = ['cursor.exe', 'cursor']
for proc in psutil.process_iter(["name"]):
try:
name = proc.info["name"]
if name and name.lower() in cursor_exe_names:
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return False
def kill_cursor():
"""强制关闭Cursor进程"""
killed = False
cursor_exe_names = ['cursor.exe', 'cursor']
for proc in psutil.process_iter(["name", "pid"]):
try:
name = proc.info["name"]
if name and name.lower() in cursor_exe_names:
proc.kill()
killed = True
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return killed
def generate_random_email():
length = random.randint(6, 8)
username = "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
return f"{username}@cursor.com"
def generate_machine_id():
return str(uuid.uuid4())
def get_cursor_config_path():
home = Path.home()
if sys.platform == "win32":
base = home / "AppData" / "Roaming" / "Cursor" / "User" / "globalStorage"
elif sys.platform == "darwin":
base = (
home
/ "Library"
/ "Application Support"
/ "Cursor"
/ "User"
/ "globalStorage"
)
else:
base = home / ".config" / "Cursor" / "User" / "globalStorage"
return base
def update_vsdb_token(config_dir, new_token, new_email, log_callback):
"""更新 Cursor 数据库中的token和email"""
db_path = config_dir / "state.vscdb"
if not db_path.exists():
log_callback("⚠️ 未找到 Cursor库 文件,跳过")
return False
try:
log_callback("📖 连接 Cursor 数据库...")
# 备份数据库
db_backup = config_dir / "state.vscdb.backup"
if db_backup.exists():
db_backup.unlink()
shutil.copy2(db_path, db_backup)
# log_callback(f"📁 数据库已备份到: {db_backup}")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 更新 token 和 email
updates = [
("cursorAuth/accessToken", new_token),
("cursorAuth/refreshToken", new_token),
("cursorAuth/cachedEmail", new_email),
]
for key, value in updates:
cursor.execute(
"SELECT value FROM ItemTable WHERE key = ?",
(key,)
)
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE ItemTable SET value = ? WHERE key = ?",
(value, key)
)
# log_callback(f"✓ 更新了 {key}")
else:
cursor.execute(
"INSERT INTO ItemTable (key, value) VALUES (?, ?)",
(key, value)
)
# log_callback(f"✓ 插入了 {key}")
conn.commit()
conn.close()
log_callback("✅ Cursor库 更新成功")
return True
except Exception as e:
log_callback(f"❌ Cursor库 更新失败: {str(e)}")
return False
@contextmanager
def request_with_proxy_fallback(url, **kwargs):
"""优先走系统代理;代理不可用时自动回退直连。以 contextmanager 统一管理连接生命周期。"""
try:
with requests.get(url, **kwargs) as response:
yield response
return
except requests.exceptions.ProxyError:
with requests.Session() as session:
session.trust_env = False
with session.get(url, **kwargs) as response:
yield response
def check_for_updates():
"""检查软件更新,返回 (data, error) 元组"""
try:
url = "https://api.yunzer.cn/api/softwareupgrade/check?code=cursortokenlogin"
with request_with_proxy_fallback(url, timeout=10) as response:
response.raise_for_status()
data = response.json()
if data.get("code") == 200:
return (data.get("data", {}), None)
else:
return (None, f"服务器返回错误: {data.get('msg', '未知错误')}")
except requests.exceptions.Timeout:
return (None, "连接超时超过10秒请检查网络连接")
except requests.exceptions.ConnectionError as e:
return (None, f"网络连接失败: {str(e)}")
except requests.exceptions.RequestException as e:
return (None, f"请求失败: {str(e)}")
except Exception as e:
return (None, f"检查更新失败: {str(e)}")
def compare_versions(current, latest):
"""比较版本号"""
try:
current_parts = list(map(int, current.split('.')))
latest_parts = list(map(int, latest.split('.')))
for i in range(max(len(current_parts), len(latest_parts))):
current_part = current_parts[i] if i < len(current_parts) else 0
latest_part = latest_parts[i] if i < len(latest_parts) else 0
if latest_part > current_part:
return 1 # 需要更新
elif latest_part < current_part:
return -1 # 当前版本更新
return 0 # 版本相同
except Exception:
return 0
def normalize_download_url(raw):
if raw is None:
return ""
s = str(raw).strip()
if not s or s.lower() in ("null", "none", "undefined"):
return ""
return s
def write_windows_updater_batch(old_exe: Path, new_exe: Path) -> Path:
"""生成批处理:通过 %1 %2 传入路径,避免把长路径写入 bat 产生编码/转义问题。"""
bat = old_exe.parent / "_cursortokenlogin_update.bat"
lines = [
"@echo off",
"setlocal EnableExtensions",
'set "OLD=%~1"',
'set "NEW=%~2"',
'if not defined OLD goto :eof',
'if not defined NEW goto :eof',
'if not exist "%NEW%" exit /b 1',
"ping 127.0.0.1 -n 5 >nul",
":wait_del",
'del /F /Q "%OLD%" 2>nul',
'if exist "%OLD%" (ping 127.0.0.1 -n 2 >nul & goto wait_del)',
'move /Y "%NEW%" "%OLD%"',
"if errorlevel 1 exit /b 1",
# 不自动启动 exe解压/runtime 尚未就绪时易触发 Python DLL 加载失败,由用户稍后手动打开
'del /F /Q "%~f0"',
]
bat.write_text("\r\n".join(lines), encoding="utf-8")
return bat
def launch_detach_no_window(args, cwd=None):
if sys.platform == "win32":
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
subprocess.Popen(
args,
cwd=cwd,
creationflags=flags,
close_fds=True,
)
else:
subprocess.Popen(args, cwd=cwd, close_fds=True, start_new_session=True)
class DownloadUpdateThread(QThread):
log_signal = Signal(str)
progress_signal = Signal(int, int)
finished_signal = Signal(bool, str)
def __init__(self, download_url: str):
super().__init__()
self.download_url = download_url
def run(self):
new_path = None
try:
self.log_signal.emit("📡 正在请求安装包...")
with request_with_proxy_fallback(
self.download_url, stream=True, timeout=60, allow_redirects=True
) as r:
r.raise_for_status()
total = int(r.headers.get("Content-Length") or 0)
chunk_size = 256 * 1024
downloaded = 0
last_pct = -1
if getattr(sys, "frozen", False):
exe_path = Path(sys.executable).resolve()
suffix = exe_path.suffix or ".exe"
new_path = exe_path.parent / f"{exe_path.stem}_update_pending{suffix}"
else:
fd, tmp = tempfile.mkstemp(suffix=".exe")
os.close(fd)
new_path = Path(tmp)
# self.log_signal.emit(f"💾 下载保存到: {new_path}")
if total > 0:
self.progress_signal.emit(0, total)
next_unknown_log = 512 * 1024
with open(new_path, "wb") as f:
for chunk in r.iter_content(chunk_size):
if not chunk:
continue
f.write(chunk)
downloaded += len(chunk)
if total > 0:
pct = min(100, downloaded * 100 // total)
if pct >= last_pct + 5 or pct == 100:
last_pct = pct if pct == 100 else (pct // 5) * 5
self.progress_signal.emit(downloaded, total)
elif downloaded >= next_unknown_log:
self.progress_signal.emit(downloaded, 0)
next_unknown_log += 512 * 1024
self.log_signal.emit("✅ 下载完成")
if not is_windows_pe_executable(new_path):
try:
new_path.unlink()
except OSError:
pass
raise ValueError(
"下载的文件不是有效的 Windows 安装包(缺少 PE 头或过小),"
"可能为网页/错误信息,已取消替换。请确认服务端提供的是本软件完整打包的 .exe。"
)
if not getattr(sys, "frozen", False):
self.log_signal.emit(
" 当前为脚本/开发模式,不会自动替换已安装程序;请手动用下载文件替换。"
)
self.finished_signal.emit(True, "dev_mode")
return
if sys.platform != "win32":
self.log_signal.emit(
" 当前为非 Windows 打包环境,请手动将新文件替换为应用程序。"
)
self.finished_signal.emit(True, "no_auto_replace")
return
exe_path = Path(sys.executable).resolve()
self.log_signal.emit("🔧 正在准备替换:退出后将删除旧程序并启动新版本...")
bat = write_windows_updater_batch(exe_path, new_path)
launch_detach_no_window(
["cmd", "/c", str(bat), str(exe_path), str(new_path.resolve())],
cwd=str(exe_path.parent),
)
self.finished_signal.emit(True, "restarting")
except Exception as e:
if new_path and new_path.exists():
try:
new_path.unlink()
except OSError:
pass
self.log_signal.emit(f"❌ 下载或更新失败: {e}")
self.finished_signal.emit(False, str(e))
class ImageClickLabel(QLabel):
"""可点击的图片标签,点击放大显示"""
clicked = Signal(str)
def __init__(self, image_path, parent=None):
super().__init__(parent)
self.image_path = image_path
self.original_pixmap = QPixmap(image_path)
self.setScaledContents(True)
self.setCursor(Qt.PointingHandCursor)
self.setStyleSheet("QLabel { border: 1px solid #ccc; border-radius: 5px; }")
self.update_display()
def update_display(self):
"""更新图片显示(等比缩放)"""
if not self.original_pixmap.isNull():
scaled_pixmap = self.original_pixmap.scaled(
350, 450,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.setPixmap(scaled_pixmap)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clicked.emit(self.image_path)
super().mousePressEvent(event)
class DonateDialog(QDialog):
"""捐赠对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("❤️ 捐赠支持")
self.setFixedSize(800, 600)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# 标题
title = QLabel("您的捐赠将用于维护和改进本软件。\n\n感谢各位,为爱发电,软主需要大家的支持与关注!\n\n有问题请联系QQ:1066960883")
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
title.setFont(title_font)
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
layout.addSpacing(20)
# 图片区域
images_layout = QHBoxLayout()
# 微信
wx_layout = QVBoxLayout()
wx_label = QLabel("微信支付")
wx_label.setAlignment(Qt.AlignCenter)
wx_layout.addWidget(wx_label)
wx_path = get_resource_path(os.path.join("assets", "images", "donate", "wx.jpg"))
self.wx_image = ImageClickLabel(wx_path)
self.wx_image.clicked.connect(self.show_full_image)
wx_layout.addWidget(self.wx_image)
images_layout.addLayout(wx_layout)
images_layout.addSpacing(20)
# 支付宝
zfb_layout = QVBoxLayout()
zfb_label = QLabel("支付宝")
zfb_label.setAlignment(Qt.AlignCenter)
zfb_layout.addWidget(zfb_label)
zfb_path = get_resource_path(os.path.join("assets", "images", "donate", "zfb.jpg"))
self.zfb_image = ImageClickLabel(zfb_path)
self.zfb_image.clicked.connect(self.show_full_image)
zfb_layout.addWidget(self.zfb_image)
images_layout.addLayout(zfb_layout)
layout.addLayout(images_layout)
layout.addSpacing(20)
# 关闭按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
close_btn = QPushButton("关闭")
close_btn.clicked.connect(self.accept)
close_btn.setMinimumWidth(100)
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
def show_full_image(self, image_path):
"""显示全屏图片"""
dialog = QDialog(self)
dialog.setWindowTitle("图片预览")
dialog.setMinimumSize(600, 700)
layout = QVBoxLayout(dialog)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
label = QLabel()
pixmap = QPixmap(image_path)
label.setPixmap(pixmap)
label.setAlignment(Qt.AlignCenter)
scroll.setWidget(label)
layout.addWidget(scroll)
close_btn = QPushButton("关闭")
close_btn.clicked.connect(dialog.accept)
close_btn.setMinimumWidth(100)
btn_layout = QHBoxLayout()
btn_layout.addStretch()
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
dialog.exec()
class ChangeTokenThread(QThread):
log_signal = Signal(str)
finished_signal = Signal(bool, str)
def __init__(self, new_token, new_email):
super().__init__()
self.new_token = new_token
self.new_email = new_email
def run(self):
try:
config_dir = get_cursor_config_path()
if not config_dir.exists():
self.finished_signal.emit(False, "未找到Cursor配置目录")
return
# 创建备份(不显示日志)
backup_dir = config_dir / "backup"
backup_dir.mkdir(exist_ok=True)
timestamp = random.randint(100000, 999999)
backup_subdir = backup_dir / f"backup_{timestamp}"
backup_subdir.mkdir(exist_ok=True)
# 查找 storage.json
storage_file = config_dir / "storage.json"
if not storage_file.exists():
self.finished_signal.emit(False, "未找到 storage.json 文件")
return
# 检查并修复只读属性
if not os.access(storage_file, os.W_OK):
self.log_signal.emit("🔓 检测到文件只读,正在解除只读属性...")
import stat
storage_file.chmod(storage_file.stat().st_mode | stat.S_IWRITE)
# 读取原文件
self.log_signal.emit("📖 读取配置文件...")
with open(storage_file, "r", encoding="utf-8") as f:
content = f.read()
data = json.loads(content) if content.strip() else {}
# 显示原邮箱
if "cursorAuth" in data and "cachedEmail" in data.get("cursorAuth", {}):
old_email = data["cursorAuth"]["cachedEmail"]
self.log_signal.emit(f"📧 原邮箱: {old_email}")
# 备份原文件(不显示日志)
shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
# 修改 cursorAuth
self.log_signal.emit("🔑 替换 cursorAuth...")
if "cursorAuth" not in data:
data["cursorAuth"] = {}
data["cursorAuth"]["accessToken"] = self.new_token
data["cursorAuth"]["refreshToken"] = self.new_token
data["cursorAuth"]["cachedEmail"] = self.new_email
# 修改 cursorAccount
self.log_signal.emit("🔑 替换 cursorAccount...")
if "cursorAccount" not in data:
data["cursorAccount"] = {}
data["cursorAccount"]["token"] = self.new_token
data["cursorAccount"]["email"] = self.new_email
# 刷新机器ID
self.log_signal.emit("🔧 刷新机器ID...")
new_machine_id = generate_machine_id()
data["telemetryMacMachineId"] = new_machine_id
data["telemetryDevDeviceId"] = new_machine_id
data["workspaceIdentifier"] = new_machine_id
self.log_signal.emit(f"📧 新邮箱: {self.new_email}")
# 保存文件
self.log_signal.emit("💾 保存配置文件...")
with open(storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# 更新 Cursor 数据库
self.log_signal.emit("📦 更新 Cursor 数据库...")
update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
self.log_signal.emit("✅ 换号完成!")
self.finished_signal.emit(True, str(backup_subdir))
except Exception as e:
self.log_signal.emit(f"❌ 错误: {str(e)}")
self.finished_signal.emit(False, str(e))
class CheckUpdateThread(QThread):
"""检查更新线程"""
update_available = Signal(dict)
no_update = Signal()
error = Signal(str)
def run(self):
try:
update_data, error = check_for_updates()
if error:
self.error.emit(error)
return
if update_data:
latest_version = update_data.get("latestVersion", "")
if compare_versions(__VERSION__, latest_version) == 1:
self.update_available.emit(update_data)
else:
self.no_update.emit()
else:
self.no_update.emit()
except Exception as e:
self.error.emit(f"检查更新异常: {str(e)}")
def _create_startup_splash(app: QApplication) -> QSplashScreen:
"""启动画面背景与文字颜色跟随当前 Qt/系统主题QPalette"""
pal = app.palette()
bg = pal.color(QPalette.ColorRole.Window)
title_c = pal.color(QPalette.ColorRole.WindowText)
sub_c = pal.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText)
if not sub_c.isValid():
sub_c = pal.color(QPalette.ColorRole.Mid)
w, h = 440, 300
pix = QPixmap(w, h)
pix.fill(bg)
painter = QPainter(pix)
painter.setPen(title_c)
title_font = QFont()
title_font.setPointSize(14)
title_font.setBold(True)
painter.setFont(title_font)
painter.drawText(0, 100, w, 40, Qt.AlignHCenter, "CursorTokenLogin")
painter.setPen(sub_c)
sub_font = QFont()
sub_font.setPointSize(9)
painter.setFont(sub_font)
painter.drawText(0, 140, w, 30, Qt.AlignHCenter, "正在准备运行环境…")
painter.end()
splash = QSplashScreen(pix, Qt.WindowStaysOnTopHint)
splash.setWindowFlag(Qt.FramelessWindowHint, True)
splash.setPalette(pal)
return splash
class MainWindow(QMainWindow):
def __init__(self, splash: Optional[QSplashScreen] = None):
super().__init__()
self._splash = splash
self._splash_phase = 0
def splash_pulse(phase: str):
if not self._splash:
return
self._splash_phase += 1
dots = "." * (self._splash_phase % 4)
pal = QApplication.palette()
msg_c = pal.color(QPalette.ColorRole.PlaceholderText)
if not msg_c.isValid():
msg_c = pal.color(QPalette.ColorRole.WindowText)
self._splash.showMessage(
f"{phase}{dots}",
Qt.AlignBottom | Qt.AlignHCenter,
msg_c,
)
QApplication.processEvents()
self._splash_pulse = splash_pulse
self._splash_pulse("正在加载")
# 获取UI文件路径
ui_path = get_resource_path(os.path.join("layout", "main.ui"))
# 详细的调试信息
debug_info = f"UI文件路径: {ui_path}\n"
debug_info += f"路径存在: {Path(ui_path).exists()}\n"
# 检查基础路径
if hasattr(sys, '_MEIPASS'):
debug_info += f"打包路径(MEIPASS): {sys._MEIPASS}\n"
debug_info += f"脚本路径: {os.path.dirname(os.path.abspath(__file__))}\n"
debug_info += f"当前工作目录: {os.getcwd()}\n"
print(debug_info)
if not Path(ui_path).exists():
if self._splash:
self._splash.close()
# 用正斜杠展示路径,避免在部分环境下反斜杠被误解析
error_msg = f"找不到UI文件: {Path(ui_path).as_posix()}\n\n详细信息:\n{debug_info}"
QMessageBox.critical(None, "错误", error_msg)
sys.exit(1)
self._splash_pulse("正在加载界面")
# 加载UI文件 - 不设置parent让loader返回完整的窗口
try:
loader = QUiLoader()
self.ui = loader.load(ui_path)
if self.ui is None:
if self._splash:
self._splash.close()
QMessageBox.critical(None, "错误", "UI文件加载失败")
sys.exit(1)
self._splash_pulse("正在装配窗口")
# 将UI的所有属性和方法复制到当前窗口
# 先保存当前窗口的状态栏
status_bar = self.statusBar()
# 使用UI的central widget
self.setCentralWidget(self.ui.centralwidget)
# 复制窗口属性
self.setWindowTitle(self.ui.windowTitle())
self.resize(self.ui.size())
# 清理原来的ui对象避免混淆
del self.ui.centralwidget
except Exception as e:
if self._splash:
self._splash.close()
QMessageBox.critical(None, "错误", f"加载UI文件时出错: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
self._splash_pulse("正在初始化")
self.cursor_path = get_default_cursor_path()
self.backup_path = ""
self._update_download_thread = None
# 重新查找组件 - 从self查找
self.txtToken = self.findChild(QTextEdit, "txtToken")
self.txtLog = self.findChild(QTextEdit, "txtLog")
self.txtCursorPath = self.findChild(QLineEdit, "txtCursorPath")
self.btnChange = self.findChild(QPushButton, "btnChange")
self.btnBrowseCursor = self.findChild(QPushButton, "btnBrowseCursor")
self.btnClearLog = self.findChild(QPushButton, "btnClearLog")
self.btnDonate = self.findChild(QPushButton, "btnDonate")
self.btnCheckUpdate = self.findChild(QPushButton, "btnCheckUpdate")
self.btnEmergencyRepair = self.findChild(QPushButton, "btnEmergencyRepair")
self.btnQueryUsage = self.findChild(QPushButton, "btnQueryUsage")
# 调试信息
print(f"txtToken: {self.txtToken}")
print(f"txtLog: {self.txtLog}")
print(f"txtCursorPath: {self.txtCursorPath}")
print(f"btnChange: {self.btnChange}")
print(f"btnBrowseCursor: {self.btnBrowseCursor}")
print(f"btnClearLog: {self.btnClearLog}")
print(f"btnDonate: {self.btnDonate}")
print(f"btnCheckUpdate: {self.btnCheckUpdate}")
# 设置默认Cursor路径
if self.txtCursorPath:
self.txtCursorPath.setText(get_default_cursor_path())
# 信号连接
if self.btnChange:
self.btnChange.clicked.connect(self.on_change_clicked)
if self.btnBrowseCursor:
self.btnBrowseCursor.clicked.connect(self.on_browse_cursor)
if self.btnClearLog:
self.btnClearLog.clicked.connect(self.on_clear_log)
if self.btnDonate:
self.btnDonate.clicked.connect(self.on_donate_clicked)
if self.btnCheckUpdate:
self.btnCheckUpdate.clicked.connect(self.on_check_update_clicked)
if self.btnEmergencyRepair:
self.btnEmergencyRepair.clicked.connect(self.on_emergency_repair_clicked)
if self.btnQueryUsage:
self.btnQueryUsage.clicked.connect(self.on_query_usage_clicked)
# 设置版本号显示在状态栏右侧
self.statusBar().addPermanentWidget(QLabel(f"Version: {__VERSION__}"))
self.log("🚀 程序启动成功")
self.log("📋 请先粘贴Token然后点击换号")
if self._splash:
self._splash.finish(self)
# 启动检查更新
self.check_update_thread = CheckUpdateThread()
self.check_update_thread.update_available.connect(self.on_update_available)
self.check_update_thread.no_update.connect(self.on_no_update)
self.check_update_thread.error.connect(self.on_update_error)
self.check_update_thread.start()
def _restore_check_update_btn(self):
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(True)
self.btnCheckUpdate.setText("🔄 检查更新")
def _on_update_download_progress(self, downloaded: int, total: int):
if total > 0:
pct = min(100, downloaded * 100 // total)
self.log(f"⬇️ 下载进度: {pct}% ({downloaded // 1024} KB / {total // 1024} KB)")
else:
self.log(f"⬇️ 已下载: {downloaded // 1024} KB服务器未提供总大小")
def _on_update_download_finished(self, success: bool, detail: str):
self._restore_check_update_btn()
if success and detail == "restarting":
QMessageBox.information(
self,
"更新完成",
"新版本已替换完成。\n\n"
"本程序将自动退出。退出后请手动运行新版本"
)
QTimer.singleShot(200, QApplication.instance().quit)
elif not success:
QMessageBox.warning(self, "更新失败", detail or "下载或安装失败,请稍后重试。")
def _begin_update_workflow_after_confirm(self, update_data: dict):
"""用户确认升级后:日志展示流程 → 校验 downloadUrl → 下载(含进度)→ 替换程序。"""
# self.log("──────── 软件更新流程 ────────")
# self.log("① 校验接口返回的 downloadUrl 是否有效…")
url = normalize_download_url(update_data.get("downloadUrl"))
if not url:
err = "当前软件没有更新包,请联系管理员。"
self.log(f"{err}")
QMessageBox.warning(self, "无法更新", err)
self._restore_check_update_btn()
return
self.log(f"开始下载安装包…")
# self.log(f" 地址: {url}")
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(False)
self.btnCheckUpdate.setText("更新中…")
self._update_download_thread = DownloadUpdateThread(url)
self._update_download_thread.log_signal.connect(self.log)
self._update_download_thread.progress_signal.connect(self._on_update_download_progress)
self._update_download_thread.finished_signal.connect(self._on_update_download_finished)
self._update_download_thread.start()
def on_update_available(self, update_data):
"""有新版本可用"""
latest_version = update_data.get("latestVersion", "")
download_url = normalize_download_url(update_data.get("downloadUrl"))
force_update = update_data.get("forceUpdate", False)
release_notes = update_data.get("releaseNotes", "")
self.log(f"🔔 发现新版本 v{latest_version}")
message = f"发现新版本 v{latest_version}\n\n"
if release_notes:
message += f"更新内容:\n{release_notes}\n\n"
if force_update:
message += "⚠️ 这是强制更新,请立即升级!"
else:
message += "是否现在升级?"
msg_box = QMessageBox(self)
msg_box.setWindowTitle("发现新版本")
msg_box.setText(message)
msg_box.setIcon(QMessageBox.Information)
# Windows 上点标题栏 × 会触发「默认按钮」;默认设为「取消」/ 无默认,
# 避免 × 被当成「立即升级」或「确定」。
btn_update = None
btn_cancel = None
if download_url:
btn_update = msg_box.addButton("立即升级", QMessageBox.ActionRole)
btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
msg_box.setDefaultButton(btn_cancel)
else:
msg_box.setStandardButtons(QMessageBox.Ok)
msg_box.setDefaultButton(QMessageBox.NoButton)
ret = msg_box.exec()
if download_url:
if ret == QMessageBox.Rejected or msg_box.clickedButton() != btn_update:
self._restore_check_update_btn()
return
else:
# 无下载地址时仅「确定」会走校验流程;× 为 Rejected不等于 Ok
if ret != QMessageBox.Ok:
self._restore_check_update_btn()
return
self._begin_update_workflow_after_confirm(update_data)
def on_no_update(self):
"""没有新版本(启动时自动检查)"""
self.log(f"✅ 当前已是最新版本 v{__VERSION__}")
def on_update_error(self, error_msg):
"""检查更新失败(启动时自动检查)"""
print(f"检查更新失败: {error_msg}")
self.log(f"❌ 检查更新失败: {error_msg}")
# 恢复按钮状态
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(True)
self.btnCheckUpdate.setText("🔄 检查更新")
def on_check_update_clicked(self):
"""手动检查更新按钮点击"""
self.log("🔄 正在检查更新...")
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(False)
self.btnCheckUpdate.setText("检查中...")
self.check_update_thread = CheckUpdateThread()
self.check_update_thread.update_available.connect(self.on_update_available)
self.check_update_thread.no_update.connect(self.on_no_update_clicked)
self.check_update_thread.error.connect(self.on_update_error_clicked)
self.check_update_thread.start()
def on_no_update_clicked(self):
"""手动检查时没有新版本"""
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(True)
self.btnCheckUpdate.setText("🔄 检查更新")
self.log(f"✅ 当前已是最新版本 v{__VERSION__}")
def on_update_error_clicked(self, error_msg):
"""手动检查更新失败"""
if self.btnCheckUpdate:
self.btnCheckUpdate.setEnabled(True)
self.btnCheckUpdate.setText("🔄 检查更新")
self.log(f"❌ 检查更新失败: {error_msg}")
def log(self, message):
if self.txtLog:
self.txtLog.append(message)
self.statusBar().showMessage(message)
def on_clear_log(self):
if self.txtLog:
self.txtLog.clear()
def on_browse_cursor(self):
"""浏览Cursor路径"""
from PySide6.QtWidgets import QFileDialog
if sys.platform == "win32":
file_path, _ = QFileDialog.getOpenFileName(
self, "选择Cursor.exe", "", "可执行文件 (*.exe)"
)
elif sys.platform == "darwin":
file_path = QFileDialog.getExistingDirectory(self, "选择Cursor.app")
else:
file_path, _ = QFileDialog.getOpenFileName(self, "选择Cursor")
if file_path:
if self.txtCursorPath:
self.txtCursorPath.setText(file_path)
self.cursor_path = file_path
def on_change_clicked(self):
token = self.txtToken.toPlainText().strip() if self.txtToken else ""
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
if not token:
QMessageBox.warning(self, "警告", "请先粘贴Token")
return
if len(token) < 20:
QMessageBox.warning(self, "警告", "Token格式可能不正确")
return
if not cursor_path or not Path(cursor_path).exists():
QMessageBox.warning(self, "警告", "请设置正确的Cursor路径")
return
# 检测Cursor是否正在运行
if is_cursor_running():
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Cursor正在运行")
msg_box.setText("Cursor正在运行中\n请先保存代码并手动关闭Cursor。")
msg_box.setIcon(QMessageBox.Warning)
btn_close = msg_box.addButton("💀 强制关闭", QMessageBox.ActionRole)
btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
msg_box.setDefaultButton(btn_cancel)
msg_box.exec_()
if msg_box.clickedButton() == btn_close:
self.log("💀 正在强制关闭Cursor...")
if kill_cursor():
self.log("✅ Cursor已关闭")
else:
self.log("⚠️ 未找到运行中的Cursor进程")
QMessageBox.warning(self, "警告", "未找到运行中的Cursor进程")
return
else:
return
# 自动生成新邮箱
new_email = generate_random_email()
self.log(f"📧 生成新邮箱: {new_email}")
# 确认对话框
reply = QMessageBox.question(
self,
"确认",
f"确定要换号吗?\n新邮箱: {new_email}\n原账号数据将备份。",
QMessageBox.Yes | QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
# 禁用按钮
if self.btnChange:
self.btnChange.setEnabled(False)
self.btnChange.setText("🔄 处理中...")
# 启动后台线程
self.thread = ChangeTokenThread(token, new_email)
self.thread.log_signal.connect(self.log)
self.thread.finished_signal.connect(self.on_change_finished)
self.thread.start()
def on_donate_clicked(self):
"""打开捐赠对话框"""
dialog = DonateDialog(self)
dialog.exec()
def on_emergency_repair_clicked(self):
"""应急检修:下载工具到桌面"""
import urllib.request
import threading
url = "http://7colud.yunzer.cn/software/db%20browser%20for%20sqlite.zip"
desktop = Path.home() / "Desktop"
save_path = desktop / "db browser for sqlite.zip"
def download():
try:
self.log("🔧 正在下载检修工具...")
urllib.request.urlretrieve(url, str(save_path))
self.log(f"✅ 下载完成,已保存到桌面:{save_path.name}")
except Exception as e:
self.log(f"❌ 下载失败: {e}")
threading.Thread(target=download, daemon=True).start()
def on_query_usage_clicked(self):
"""查询当前token的额度使用情况"""
import threading
token = self.txtToken.toPlainText().strip() if self.txtToken else ""
if not token:
QMessageBox.warning(self, "提示", "请先在输入框中填入Token")
return
def query():
try:
clean_token = token.strip().replace('"', '').replace("SessionToken=", "")
resp = requests.get(
"https://cursor.com/api/usage",
headers={
"Cookie": f"SessionToken={clean_token}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
"Accept": "application/json",
},
timeout=10,
)
if resp.status_code == 401:
msg = "查询失败Token 已失效 (401 Unauthorized)。请检查账号是否已退出或被封。"
else:
resp.raise_for_status()
data = resp.json()
lines = []
premium = data.get("premiumUsage", {})
if premium:
used = premium.get("numRequestsTotal", 0)
limit = premium.get("maxRequestUsage", "无限制")
lines.append(f"高级模型 (GPT-4/Claude): 已用 {used} / {limit}")
for model_key, model_data in data.items():
if isinstance(model_data, dict) and "numRequestsTotal" in model_data:
used = model_data.get("numRequestsTotal", 0)
limit = model_data.get("maxRequestUsage")
limit_str = str(limit) if limit is not None else "无限制"
lines.append(f"{model_key}: 已用 {used} / {limit_str}")
msg = "\n".join(lines) if lines else "暂无额度数据,请检查账号状态。"
QMetaObject.invokeMethod(
self, "_show_usage_result",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, msg)
)
except Exception as e:
QMetaObject.invokeMethod(
self, "_show_usage_result",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"网络请求错误:{str(e)}")
)
threading.Thread(target=query, daemon=True).start()
@Slot(str)
def _show_usage_result(self, msg):
QMessageBox.information(self, "额度查询结果", msg)
if self.btnChange:
self.btnChange.setEnabled(True)
self.btnChange.setText("🚀 开始换号")
if success:
self.backup_path = message
QMessageBox.information(self, "成功", "换号完成!\n即将打开Cursor...")
self.log("🚀 正在打开Cursor...")
try:
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
if sys.platform == "win32":
os.startfile(cursor_path)
elif sys.platform == "darwin":
subprocess.Popen(["open", cursor_path])
else:
subprocess.Popen([cursor_path])
self.log("✅ Cursor已启动")
except Exception as e:
self.log(f"❌ 打开Cursor失败: {str(e)}")
QMessageBox.warning(self, "警告", f"打开Cursor失败: {str(e)}")
else:
QMessageBox.critical(self, "失败", message)
def main():
app = QApplication(sys.argv)
app_icon = get_app_icon()
if not app_icon.isNull():
app.setWindowIcon(app_icon)
splash = _create_startup_splash(app)
if not app_icon.isNull():
splash.setWindowIcon(app_icon)
splash.show()
app.processEvents()
window = MainWindow(splash=splash)
if not app_icon.isNull():
window.setWindowIcon(app_icon)
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()