1118 lines
40 KiB
Python
1118 lines
40 KiB
Python
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 pathlib import Path
|
||
from typing import Optional
|
||
|
||
__VERSION__ = "0.0.2"
|
||
|
||
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
|
||
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
|
||
|
||
|
||
def check_for_updates():
|
||
"""检查软件更新,返回 (data, error) 元组"""
|
||
try:
|
||
url = "https://api.yunzer.cn/api/softwareupgrade/check?code=cursortokenlogin"
|
||
response = requests.get(url, timeout=10)
|
||
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 requests.get(
|
||
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")
|
||
|
||
# 调试信息
|
||
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)
|
||
|
||
# 设置版本号显示在状态栏右侧
|
||
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_change_finished(self, success, message):
|
||
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()
|