2012 lines
74 KiB
Python
2012 lines
74 KiB
Python
import sys
|
||
import json
|
||
import base64
|
||
import shutil
|
||
import uuid
|
||
import random
|
||
import string
|
||
import re
|
||
import os
|
||
import tempfile
|
||
import ctypes
|
||
import socket
|
||
import psutil
|
||
import subprocess
|
||
import sqlite3
|
||
import requests
|
||
from contextlib import contextmanager
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
__VERSION__ = "0.0.5"
|
||
|
||
from PySide6.QtWidgets import (
|
||
QApplication,
|
||
QMainWindow,
|
||
QMessageBox,
|
||
QTextEdit,
|
||
QInputDialog,
|
||
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
|
||
|
||
|
||
def test_session_token_detailed(token: str, log_callback):
|
||
""" session token """
|
||
log_callback(" session token...")
|
||
headers = {
|
||
"Cookie": f"SessionToken={token}",
|
||
"Accept": "application/json",
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
# API
|
||
test_endpoints = [
|
||
("https://cursor.com/api/usage", "Usage API"),
|
||
("https://api2.cursor.sh/v1/models", "Models API"),
|
||
("https://api2.cursor.sh/v1/chat/completions", "Chat API"),
|
||
]
|
||
|
||
for endpoint, name in test_endpoints:
|
||
try:
|
||
log_callback(f" {name}: {endpoint}")
|
||
|
||
if "chat/completions" in endpoint:
|
||
# API
|
||
test_data = {
|
||
"model": "default",
|
||
"messages": [{"role": "user", "content": "hi"}],
|
||
"max_tokens": 1
|
||
}
|
||
with request_with_proxy_fallback(
|
||
endpoint,
|
||
headers=headers,
|
||
json=test_data,
|
||
timeout=10,
|
||
allow_redirects=True,
|
||
) as resp:
|
||
code = resp.status_code
|
||
log_callback(f" {name} HTTP {code}")
|
||
if code == 200:
|
||
log_callback(f" {name} ")
|
||
return True, f" session token {name}"
|
||
elif code == 401:
|
||
log_callback(f" {name} 401 - ")
|
||
continue
|
||
elif code == 403:
|
||
log_callback(f" {name} 403 - ")
|
||
continue
|
||
else:
|
||
log_callback(f" {name} HTTP {code}")
|
||
continue
|
||
else:
|
||
# GET
|
||
with request_with_proxy_fallback(
|
||
endpoint,
|
||
headers=headers,
|
||
timeout=10,
|
||
allow_redirects=True,
|
||
) as resp:
|
||
code = resp.status_code
|
||
log_callback(f" {name} HTTP {code}")
|
||
if code == 200:
|
||
log_callback(f" {name} ")
|
||
return True, f" session token {name}"
|
||
elif code == 401:
|
||
log_callback(f" {name} 401 - ")
|
||
continue
|
||
elif code == 403:
|
||
log_callback(f" {name} 403 - ")
|
||
continue
|
||
else:
|
||
log_callback(f" {name} HTTP {code}")
|
||
continue
|
||
|
||
except Exception as e:
|
||
log_callback(f" {name} : {e}")
|
||
continue
|
||
|
||
return False, " session token "
|
||
|
||
|
||
def test_session_token_simple(token: str, log_callback):
|
||
"""session token hi"""
|
||
log_callback(" session token...")
|
||
headers = {
|
||
"Cookie": f"SessionToken={token}",
|
||
"Accept": "application/json",
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
try:
|
||
# API
|
||
test_data = {"message": "hi"}
|
||
with request_with_proxy_fallback(
|
||
"https://api2.cursor.sh/v1/chat/completions",
|
||
headers=headers,
|
||
json=test_data,
|
||
timeout=10,
|
||
allow_redirects=True,
|
||
) as resp:
|
||
code = resp.status_code
|
||
if code == 200:
|
||
log_callback(" session token ")
|
||
return True, " session token "
|
||
elif code == 401:
|
||
return False, " session token 401"
|
||
elif code == 403:
|
||
return False, " session token 403"
|
||
else:
|
||
return False, f" session token HTTP {code}"
|
||
except Exception as e:
|
||
return False, f" session token : {e}"
|
||
|
||
|
||
def detect_account_status(token: str, log_callback):
|
||
"""账号状态检测:本地解析 JWT 过期时间 + 远端状态探测。"""
|
||
jwt_exp = None
|
||
try:
|
||
parts = token.split(".")
|
||
if len(parts) >= 2:
|
||
payload = parts[1]
|
||
payload += "=" * (-len(payload) % 4)
|
||
decoded = json.loads(base64.urlsafe_b64decode(payload.encode("utf-8")).decode("utf-8"))
|
||
if isinstance(decoded, dict):
|
||
jwt_exp = decoded.get("exp")
|
||
except Exception:
|
||
jwt_exp = None
|
||
|
||
if jwt_exp:
|
||
exp_dt = datetime.fromtimestamp(int(jwt_exp))
|
||
log_callback(f"🕒 Token过期时间(本地解析): {exp_dt.strftime('%Y-%m-%d %H:%M:%S')}")
|
||
if datetime.now().timestamp() >= float(jwt_exp):
|
||
return False, "❌ 账号状态:Token 已过期。"
|
||
|
||
log_callback("🔎 正在检测账号状态接口: https://cursor.com/api/usage")
|
||
headers = {
|
||
"Cookie": f"SessionToken={token}",
|
||
"Accept": "application/json",
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
|
||
}
|
||
try:
|
||
with request_with_proxy_fallback(
|
||
"https://cursor.com/api/usage",
|
||
headers=headers,
|
||
timeout=12,
|
||
allow_redirects=True,
|
||
) as resp:
|
||
code = resp.status_code
|
||
if code == 200:
|
||
return True, "✅ 账号状态:有效,可与服务器通信。"
|
||
if code == 401:
|
||
return False, "❌ 账号状态:无效或已失效(401)。"
|
||
if code == 403:
|
||
return False, "⚠️ 账号状态:受限(403)。"
|
||
if code == 429:
|
||
return False, "⚠️ 账号状态:请求过频(429),请稍后重试。"
|
||
return False, f"⚠️ 账号状态:接口返回 HTTP {code}。"
|
||
except Exception as e:
|
||
return False, f"⚠️ 账号状态检测失败:网络或环境异常({e})"
|
||
|
||
|
||
@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
|
||
data["cursorAuth"]["plan"] = "pro"
|
||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||
data["cursorAuth"]["membershipType"] = "pro"
|
||
|
||
# 修改 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
|
||
data["cursorAccount"]["plan"] = "pro"
|
||
|
||
# 刷新机器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
|
||
|
||
# membershipType pro
|
||
data["membershipType"] = "pro"
|
||
|
||
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)}")
|
||
|
||
|
||
class CheckTokenAvailableThread(QThread):
|
||
log_signal = Signal(str)
|
||
finished_signal = Signal(bool, str)
|
||
|
||
def __init__(self, token: str):
|
||
super().__init__()
|
||
self.token = token
|
||
|
||
def run(self):
|
||
# token
|
||
self.log_signal.emit(" JWT ...")
|
||
ok, message = detect_account_status(self.token, self.log_signal.emit)
|
||
|
||
if not ok:
|
||
# JWT 401
|
||
self.log_signal.emit(" API ...")
|
||
ok_detailed, message_detailed = test_session_token_detailed(self.token, self.log_signal.emit)
|
||
if ok_detailed:
|
||
self.finished_signal.emit(True, message_detailed)
|
||
else:
|
||
self.finished_signal.emit(False, f"JWT {message} | API : {message_detailed}")
|
||
return
|
||
|
||
self.finished_signal.emit(ok, message)
|
||
self.finished_signal.emit(ok, message)
|
||
|
||
|
||
class CursorNetworkMonitorThread(QThread):
|
||
log_signal = Signal(str)
|
||
|
||
def __init__(self, interval_sec: float = 2.0):
|
||
super().__init__()
|
||
self.interval_sec = interval_sec
|
||
self._running = True
|
||
self._seen = set()
|
||
|
||
def stop(self):
|
||
self._running = False
|
||
|
||
def _try_resolve_host(self, ip: str) -> str:
|
||
try:
|
||
return socket.gethostbyaddr(ip)[0]
|
||
except Exception:
|
||
return ""
|
||
|
||
def run(self):
|
||
self.log_signal.emit("🌐 网络监控已启动(仅显示连接目标,不含加密内容)")
|
||
while self._running:
|
||
cursor_pids = set()
|
||
for proc in psutil.process_iter(["pid", "name"]):
|
||
try:
|
||
name = (proc.info.get("name") or "").lower()
|
||
if name in ("cursor.exe", "cursor"):
|
||
cursor_pids.add(proc.info["pid"])
|
||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||
continue
|
||
|
||
if not cursor_pids:
|
||
self.msleep(int(self.interval_sec * 1000))
|
||
continue
|
||
|
||
try:
|
||
conns = psutil.net_connections(kind="tcp")
|
||
except Exception as e:
|
||
self.log_signal.emit(f"⚠️ 读取网络连接失败: {e}")
|
||
self.msleep(int(self.interval_sec * 1000))
|
||
continue
|
||
|
||
for c in conns:
|
||
if c.pid not in cursor_pids or not c.raddr:
|
||
continue
|
||
remote_ip = c.raddr.ip
|
||
remote_port = c.raddr.port
|
||
status = c.status
|
||
key = (c.pid, remote_ip, remote_port, status)
|
||
if key in self._seen:
|
||
continue
|
||
self._seen.add(key)
|
||
host = self._try_resolve_host(remote_ip)
|
||
if host:
|
||
self.log_signal.emit(
|
||
f"🔗 Cursor(pid={c.pid}) -> {remote_ip}:{remote_port} ({host}) [{status}]"
|
||
)
|
||
else:
|
||
self.log_signal.emit(
|
||
f"🔗 Cursor(pid={c.pid}) -> {remote_ip}:{remote_port} [{status}]"
|
||
)
|
||
|
||
self.msleep(int(self.interval_sec * 1000))
|
||
|
||
|
||
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._token_check_thread = None
|
||
self._network_monitor_thread = None
|
||
self._emergency_dialog = None
|
||
self._usage_guide_dialog = None
|
||
self._token_extract_dialog = None
|
||
self._token_display = None
|
||
self._current_extracted_token = ""
|
||
self.log_file_path = self._prepare_log_file_path()
|
||
|
||
# 重新查找组件 - 从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.btnOpenCursor = self.findChild(QPushButton, "btnOpenCursor")
|
||
self.btnCheckTokenAvailable = self.findChild(QPushButton, "btnCheckTokenAvailable")
|
||
self.btnBrowseCursor = self.findChild(QPushButton, "btnBrowseCursor")
|
||
self.btnAutoCursorPath = None
|
||
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.btnUsageGuide = self.findChild(QPushButton, "btnUsageGuide")
|
||
self._load_cached_logs_to_ui()
|
||
|
||
# 调试信息
|
||
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"btnCheckTokenAvailable: {self.btnCheckTokenAvailable}")
|
||
print(f"btnClearLog: {self.btnClearLog}")
|
||
print(f"btnDonate: {self.btnDonate}")
|
||
print(f"btnCheckUpdate: {self.btnCheckUpdate}")
|
||
print(f"btnUsageGuide: {self.btnUsageGuide}")
|
||
|
||
# 设置默认Cursor路径
|
||
if self.txtCursorPath:
|
||
self.txtCursorPath.setText(get_default_cursor_path())
|
||
|
||
# 在「浏览」按钮右侧动态增加「自动查找」按钮
|
||
if self.btnBrowseCursor and self.btnBrowseCursor.parentWidget():
|
||
parent = self.btnBrowseCursor.parentWidget()
|
||
layout = parent.layout()
|
||
if layout:
|
||
self.btnAutoCursorPath = QPushButton("自动查找", parent)
|
||
self.btnAutoCursorPath.setObjectName("btnAutoCursorPath")
|
||
self.btnAutoCursorPath.setMinimumWidth(88)
|
||
idx = layout.indexOf(self.btnBrowseCursor)
|
||
if idx >= 0:
|
||
layout.insertWidget(idx + 1, self.btnAutoCursorPath)
|
||
else:
|
||
layout.addWidget(self.btnAutoCursorPath)
|
||
|
||
# 信号连接
|
||
if self.btnChange:
|
||
self.btnChange.clicked.connect(self.on_change_clicked)
|
||
if self.btnOpenCursor:
|
||
self.btnOpenCursor.clicked.connect(self.on_open_cursor_clicked)
|
||
if self.btnCheckTokenAvailable:
|
||
self.btnCheckTokenAvailable.clicked.connect(self.on_check_token_available_clicked)
|
||
if self.btnBrowseCursor:
|
||
self.btnBrowseCursor.clicked.connect(self.on_browse_cursor)
|
||
if self.btnAutoCursorPath:
|
||
self.btnAutoCursorPath.clicked.connect(self.on_auto_config_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.btnUsageGuide:
|
||
self.btnUsageGuide.clicked.connect(self.on_usage_guide_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):
|
||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
line = f"[{ts}] {message}"
|
||
if self.txtLog:
|
||
self.txtLog.append(line)
|
||
self._scroll_log_to_bottom()
|
||
if self.log_file_path:
|
||
try:
|
||
with open(self.log_file_path, "a", encoding="utf-8") as f:
|
||
f.write(line + "\n")
|
||
except OSError:
|
||
pass
|
||
|
||
def _scroll_log_to_bottom(self):
|
||
"""让日志文本框始终滚动到最底部,跟踪最新日志。"""
|
||
if not self.txtLog:
|
||
return
|
||
scroll_bar = self.txtLog.verticalScrollBar()
|
||
if scroll_bar:
|
||
scroll_bar.setValue(scroll_bar.maximum())
|
||
|
||
def _prepare_log_file_path(self) -> Optional[Path]:
|
||
"""准备当前日志文件路径;优先沿用当天最新日志,失败时返回 None。"""
|
||
try:
|
||
self.log_dir = Path.home() / ".cursortokenlogin" / "logs"
|
||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||
latest = self._get_latest_log_file_path()
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
return latest or (self.log_dir / f"{today}-0001.log")
|
||
except OSError:
|
||
self.log_dir = None
|
||
return None
|
||
|
||
def on_clear_log(self):
|
||
if self.txtLog:
|
||
self.txtLog.clear()
|
||
next_log = self._get_next_log_file_path()
|
||
if next_log:
|
||
self.log_file_path = next_log
|
||
try:
|
||
# 立即创建新的空日志文件,确保重启后读取的是清空后的新会话文件。
|
||
self.log_file_path.touch(exist_ok=True)
|
||
except OSError:
|
||
pass
|
||
|
||
def _load_cached_logs_to_ui(self):
|
||
"""启动时把最近日志文件内容回显到界面。"""
|
||
if not self.txtLog or not self.log_file_path or not self.log_file_path.exists():
|
||
return
|
||
try:
|
||
with open(self.log_file_path, "r", encoding="utf-8") as f:
|
||
content = f.read().strip()
|
||
if content:
|
||
self.txtLog.setPlainText(content)
|
||
self._scroll_log_to_bottom()
|
||
except OSError:
|
||
pass
|
||
|
||
def _get_latest_log_file_path(self) -> Optional[Path]:
|
||
if not getattr(self, "log_dir", None):
|
||
return None
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
prefix = f"{today}-"
|
||
try:
|
||
files = sorted(self.log_dir.glob(f"{prefix}*.log"))
|
||
except OSError:
|
||
return None
|
||
if not files:
|
||
return None
|
||
numbered = []
|
||
for p in files:
|
||
stem = p.stem
|
||
if not stem.startswith(prefix):
|
||
continue
|
||
seq = stem[len(prefix):]
|
||
if seq.isdigit():
|
||
numbered.append((int(seq), p))
|
||
if not numbered:
|
||
return None
|
||
numbered.sort(key=lambda x: x[0])
|
||
return numbered[-1][1]
|
||
|
||
def _get_next_log_file_path(self) -> Optional[Path]:
|
||
if not getattr(self, "log_dir", None):
|
||
return None
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
prefix = f"{today}-"
|
||
latest = self._get_latest_log_file_path()
|
||
if latest and latest.stem.startswith(prefix):
|
||
seq = latest.stem[len(prefix):]
|
||
next_no = int(seq) + 1 if seq.isdigit() else 1
|
||
else:
|
||
next_no = 1
|
||
return self.log_dir / f"{today}-{next_no:04d}.log"
|
||
|
||
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_auto_config_cursor(self):
|
||
"""自动查找并填充 Cursor 安装路径。"""
|
||
cursor_path = get_default_cursor_path()
|
||
if cursor_path and Path(cursor_path).exists():
|
||
if self.txtCursorPath:
|
||
self.txtCursorPath.setText(cursor_path)
|
||
self.cursor_path = cursor_path
|
||
self.log(f"✅ 已自动查找 Cursor 路径: {cursor_path}")
|
||
return
|
||
self.log("❌ 自动查找失败:未找到 Cursor 安装目录,请手动浏览选择。")
|
||
QMessageBox.warning(self, "提示", "未自动找到 Cursor 安装目录,请点击“浏览”手动选择。")
|
||
|
||
def _launch_cursor(self, cursor_path: str) -> bool:
|
||
"""按当前平台启动 Cursor。"""
|
||
try:
|
||
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已启动")
|
||
return True
|
||
except Exception as e:
|
||
self.log(f"❌ 打开Cursor失败: {str(e)}")
|
||
QMessageBox.warning(self, "警告", f"打开Cursor失败: {str(e)}")
|
||
return False
|
||
|
||
def _resolve_cursor_path_or_prompt(self) -> str:
|
||
"""
|
||
返回可用的 Cursor 路径;当未配置/无效时,提示用户手动配置或自动查找。
|
||
"""
|
||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||
if cursor_path and Path(cursor_path).exists():
|
||
return cursor_path
|
||
|
||
msg_box = QMessageBox(self)
|
||
msg_box.setWindowTitle("Cursor路径未配置")
|
||
msg_box.setText("未检测到有效的 Cursor 路径,请先配置。")
|
||
msg_box.setIcon(QMessageBox.Warning)
|
||
btn_manual = msg_box.addButton("手动配置", QMessageBox.ActionRole)
|
||
btn_auto = msg_box.addButton("自动查找", QMessageBox.ActionRole)
|
||
btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
|
||
msg_box.setDefaultButton(btn_auto)
|
||
msg_box.exec()
|
||
|
||
clicked = msg_box.clickedButton()
|
||
if clicked == btn_manual:
|
||
self.on_browse_cursor()
|
||
elif clicked == btn_auto:
|
||
self.on_auto_config_cursor()
|
||
else:
|
||
return ""
|
||
|
||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||
if cursor_path and Path(cursor_path).exists():
|
||
return cursor_path
|
||
return ""
|
||
|
||
def on_open_cursor_clicked(self):
|
||
"""点击“打开Cursor”按钮:优先使用已配置路径,否则引导配置。"""
|
||
cursor_path = self._resolve_cursor_path_or_prompt()
|
||
if not cursor_path:
|
||
return
|
||
self.log("🚀 正在打开Cursor...")
|
||
self._launch_cursor(cursor_path)
|
||
|
||
def _extract_session_token(self, raw_value: str) -> str:
|
||
"""从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。"""
|
||
s = (raw_value or "").strip().strip('"').strip("'")
|
||
if not s:
|
||
return ""
|
||
m = re.search(r"SessionToken\s*=\s*([^;\s]+)", s, flags=re.IGNORECASE)
|
||
if m:
|
||
return m.group(1).strip().strip('"').strip("'")
|
||
if ";" in s:
|
||
first = s.split(";", 1)[0].strip()
|
||
if "=" in first:
|
||
k, v = first.split("=", 1)
|
||
if k.strip().lower() in ("workoscursorsessiontoken", "sessiontoken"):
|
||
return v.strip().strip('"').strip("'")
|
||
return s.replace("SessionToken=", "").strip()
|
||
|
||
def on_check_token_available_clicked(self):
|
||
"""检测当前输入 token 的账号状态(不发送聊天请求)。"""
|
||
raw_token = self.txtToken.toPlainText().strip() if self.txtToken else ""
|
||
token = self._extract_session_token(raw_token)
|
||
if not token:
|
||
QMessageBox.warning(self, "提示", "请先粘贴有效 Token。")
|
||
return
|
||
|
||
self.log("🧪 开始账号状态检测...")
|
||
if self.btnCheckTokenAvailable:
|
||
self.btnCheckTokenAvailable.setEnabled(False)
|
||
self.btnCheckTokenAvailable.setText("检测中...")
|
||
|
||
self._token_check_thread = CheckTokenAvailableThread(token)
|
||
self._token_check_thread.log_signal.connect(self.log)
|
||
self._token_check_thread.finished_signal.connect(self.on_check_token_available_finished)
|
||
self._token_check_thread.start()
|
||
|
||
@Slot(bool, str)
|
||
def on_check_token_available_finished(self, ok: bool, message: str):
|
||
if self.btnCheckTokenAvailable:
|
||
self.btnCheckTokenAvailable.setEnabled(True)
|
||
self.btnCheckTokenAvailable.setText("状态检测")
|
||
self.log(message)
|
||
if ok:
|
||
QMessageBox.information(self, "检测结果", message)
|
||
else:
|
||
QMessageBox.warning(self, "检测结果", message)
|
||
|
||
def _show_change_success_countdown_dialog(self, seconds: int = 4):
|
||
"""换号成功后显示倒计时提示框,倒计时结束自动关闭。"""
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("提示")
|
||
dialog.setModal(True)
|
||
dialog.setFixedSize(420, 160)
|
||
|
||
layout = QVBoxLayout(dialog)
|
||
tip_label = QLabel("换号成功咯!请及时去确认收货哦!万分感谢!", dialog)
|
||
tip_label.setWordWrap(True)
|
||
tip_label.setAlignment(Qt.AlignCenter)
|
||
layout.addStretch()
|
||
layout.addWidget(tip_label)
|
||
|
||
countdown_label = QLabel("", dialog)
|
||
countdown_label.setAlignment(Qt.AlignCenter)
|
||
layout.addWidget(countdown_label)
|
||
layout.addStretch()
|
||
|
||
remain = {"value": max(1, int(seconds))}
|
||
|
||
def refresh_text():
|
||
countdown_label.setText(f"{remain['value']} 秒后自动关闭")
|
||
|
||
refresh_text()
|
||
timer = QTimer(dialog)
|
||
|
||
def on_timeout():
|
||
remain["value"] -= 1
|
||
if remain["value"] <= 0:
|
||
timer.stop()
|
||
dialog.accept()
|
||
return
|
||
refresh_text()
|
||
|
||
timer.timeout.connect(on_timeout)
|
||
timer.start(1000)
|
||
dialog.exec()
|
||
|
||
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}")
|
||
|
||
# 禁用按钮
|
||
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_usage_guide_clicked(self):
|
||
"""打开使用说明图片(非模态,可与其他窗口并行)。"""
|
||
if self._usage_guide_dialog and self._usage_guide_dialog.isVisible():
|
||
self._usage_guide_dialog.raise_()
|
||
self._usage_guide_dialog.activateWindow()
|
||
return
|
||
|
||
image_path = get_resource_path(os.path.join("assets", "images", "donate", "info.png"))
|
||
if not Path(image_path).is_file():
|
||
QMessageBox.warning(self, "提示", f"未找到使用说明图片:{image_path}")
|
||
return
|
||
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("使用说明")
|
||
dialog.setMinimumSize(900, 700)
|
||
dialog.setModal(False)
|
||
dialog.setWindowModality(Qt.NonModal)
|
||
|
||
layout = QVBoxLayout(dialog)
|
||
|
||
tip = QLabel("可使用鼠标滚轮查看长图内容,可通过按钮缩放图片。")
|
||
tip.setAlignment(Qt.AlignCenter)
|
||
layout.addWidget(tip)
|
||
|
||
zoom_row = QHBoxLayout()
|
||
zoom_row.addStretch()
|
||
btn_zoom_out = QPushButton("缩小 -", dialog)
|
||
btn_zoom_reset = QPushButton("100%", dialog)
|
||
btn_zoom_in = QPushButton("放大 +", dialog)
|
||
zoom_label = QLabel("100%", dialog)
|
||
zoom_row.addWidget(btn_zoom_out)
|
||
zoom_row.addWidget(btn_zoom_reset)
|
||
zoom_row.addWidget(btn_zoom_in)
|
||
zoom_row.addWidget(zoom_label)
|
||
layout.addLayout(zoom_row)
|
||
|
||
scroll = QScrollArea(dialog)
|
||
scroll.setWidgetResizable(True)
|
||
|
||
label = QLabel(scroll)
|
||
original_pixmap = QPixmap(image_path)
|
||
zoom_state = {"scale": 1.0}
|
||
|
||
def apply_zoom():
|
||
if original_pixmap.isNull():
|
||
return
|
||
target_w = max(1, int(original_pixmap.width() * zoom_state["scale"]))
|
||
target_h = max(1, int(original_pixmap.height() * zoom_state["scale"]))
|
||
scaled = original_pixmap.scaled(
|
||
target_w,
|
||
target_h,
|
||
Qt.KeepAspectRatio,
|
||
Qt.SmoothTransformation,
|
||
)
|
||
label.setPixmap(scaled)
|
||
label.resize(scaled.size())
|
||
zoom_label.setText(f"{int(zoom_state['scale'] * 100)}%")
|
||
|
||
def zoom_in():
|
||
zoom_state["scale"] = min(3.0, zoom_state["scale"] * 1.2)
|
||
apply_zoom()
|
||
|
||
def zoom_out():
|
||
zoom_state["scale"] = max(0.2, zoom_state["scale"] / 1.2)
|
||
apply_zoom()
|
||
|
||
def zoom_reset():
|
||
zoom_state["scale"] = 1.0
|
||
apply_zoom()
|
||
|
||
btn_zoom_in.clicked.connect(zoom_in)
|
||
btn_zoom_out.clicked.connect(zoom_out)
|
||
btn_zoom_reset.clicked.connect(zoom_reset)
|
||
|
||
apply_zoom()
|
||
label.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||
|
||
scroll.setWidget(label)
|
||
layout.addWidget(scroll, 1)
|
||
|
||
btn_close = QPushButton("关闭", dialog)
|
||
btn_close.clicked.connect(dialog.accept)
|
||
btn_row = QHBoxLayout()
|
||
btn_row.addStretch()
|
||
btn_row.addWidget(btn_close)
|
||
layout.addLayout(btn_row)
|
||
|
||
self._usage_guide_dialog = dialog
|
||
dialog.show()
|
||
|
||
def on_emergency_repair_clicked(self):
|
||
"""应急检修:弹出工具面板。"""
|
||
if self._emergency_dialog and self._emergency_dialog.isVisible():
|
||
self._emergency_dialog.raise_()
|
||
self._emergency_dialog.activateWindow()
|
||
return
|
||
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("应急检修")
|
||
dialog.setMinimumSize(400, 300)
|
||
dialog.setModal(False)
|
||
dialog.setWindowModality(Qt.NonModal)
|
||
|
||
layout = QVBoxLayout(dialog)
|
||
layout.addWidget(QLabel("请选择要执行的操作:"))
|
||
|
||
actions_widget = QWidget(dialog)
|
||
actions_layout = QVBoxLayout(actions_widget)
|
||
actions_layout.setContentsMargins(0, 0, 0, 0)
|
||
actions_layout.setSpacing(10)
|
||
actions_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||
|
||
btn_download = QPushButton("下载DB Browser")
|
||
btn_clear_cache = QPushButton("清除Cursor缓存")
|
||
btn_extract_token = QPushButton("Token提取")
|
||
btn_network_monitor = QPushButton("网络监控(临时)")
|
||
btn_stop_monitor = QPushButton("停止网络监控")
|
||
for btn in (
|
||
btn_download,
|
||
btn_clear_cache,
|
||
btn_extract_token,
|
||
btn_network_monitor,
|
||
btn_stop_monitor,
|
||
):
|
||
btn.setMinimumWidth(180)
|
||
btn.setMaximumWidth(260)
|
||
btn.setSizePolicy(btn.sizePolicy().horizontalPolicy(), btn.sizePolicy().verticalPolicy())
|
||
actions_layout.addWidget(btn, 0, Qt.AlignLeft)
|
||
|
||
layout.addWidget(actions_widget)
|
||
layout.addStretch()
|
||
|
||
btn_download.clicked.connect(self.download_db_tool)
|
||
btn_clear_cache.clicked.connect(self.clear_cursor_cache)
|
||
btn_extract_token.clicked.connect(self.on_token_extract_clicked)
|
||
btn_network_monitor.clicked.connect(self.start_cursor_network_monitor)
|
||
btn_stop_monitor.clicked.connect(self.stop_cursor_network_monitor)
|
||
self._emergency_dialog = dialog
|
||
dialog.show()
|
||
|
||
def start_cursor_network_monitor(self):
|
||
if self._network_monitor_thread and self._network_monitor_thread.isRunning():
|
||
self.log("ℹ️ 网络监控已在运行。")
|
||
return
|
||
self._network_monitor_thread = CursorNetworkMonitorThread(interval_sec=2.0)
|
||
self._network_monitor_thread.log_signal.connect(self.log)
|
||
self._network_monitor_thread.start()
|
||
self.log("🟢 已启动临时网络监控。")
|
||
|
||
def stop_cursor_network_monitor(self, silent: bool = False):
|
||
if not self._network_monitor_thread or not self._network_monitor_thread.isRunning():
|
||
if not silent:
|
||
self.log("ℹ️ 当前没有运行中的网络监控。")
|
||
return
|
||
self._network_monitor_thread.stop()
|
||
self._network_monitor_thread.wait(1500)
|
||
if not silent:
|
||
self.log("🛑 已停止临时网络监控。")
|
||
|
||
def on_token_extract_clicked(self):
|
||
"""密码通过后打开 Token 提取窗口。"""
|
||
pwd, ok = QInputDialog.getText(
|
||
self,
|
||
"Token提取",
|
||
"请输入密码:",
|
||
QLineEdit.Password,
|
||
)
|
||
if not ok:
|
||
return
|
||
if pwd != "920103":
|
||
self.log("❌ Token提取密码错误")
|
||
QMessageBox.warning(self, "提示", "密码错误,无法使用 Token 提取。")
|
||
return
|
||
|
||
if self._token_extract_dialog and self._token_extract_dialog.isVisible():
|
||
self._token_extract_dialog.raise_()
|
||
self._token_extract_dialog.activateWindow()
|
||
return
|
||
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("Token提取")
|
||
dialog.setMinimumSize(700, 420)
|
||
dialog.setModal(False)
|
||
dialog.setWindowModality(Qt.NonModal)
|
||
|
||
layout = QVBoxLayout(dialog)
|
||
layout.addWidget(QLabel("Token:"))
|
||
|
||
token_display = QTextEdit(dialog)
|
||
token_display.setReadOnly(True)
|
||
token_display.setPlaceholderText("点击“读取Token”后将在此显示。")
|
||
layout.addWidget(token_display, 1)
|
||
|
||
btn_row = QHBoxLayout()
|
||
btn_read = QPushButton("读取Token", dialog)
|
||
btn_save = QPushButton("另存桌面", dialog)
|
||
btn_close = QPushButton("关闭", dialog)
|
||
btn_row.addWidget(btn_read)
|
||
btn_row.addWidget(btn_save)
|
||
btn_row.addStretch()
|
||
btn_row.addWidget(btn_close)
|
||
layout.addLayout(btn_row)
|
||
|
||
self._token_extract_dialog = dialog
|
||
self._token_display = token_display
|
||
|
||
btn_read.clicked.connect(self.read_cursor_token_for_dialog)
|
||
btn_save.clicked.connect(self.save_extracted_token_to_desktop)
|
||
btn_close.clicked.connect(dialog.close)
|
||
dialog.show()
|
||
|
||
def _read_current_cursor_token(self) -> str:
|
||
"""从 Cursor 的 storage.json 读取当前 token。"""
|
||
config_dir = get_cursor_config_path()
|
||
storage_file = config_dir / "storage.json"
|
||
if not storage_file.exists():
|
||
return ""
|
||
try:
|
||
with open(storage_file, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
data = json.loads(content) if content.strip() else {}
|
||
except Exception:
|
||
return ""
|
||
|
||
token = (
|
||
data.get("cursorAuth", {}).get("accessToken")
|
||
or data.get("cursorAuth", {}).get("refreshToken")
|
||
or data.get("cursorAccount", {}).get("token")
|
||
or ""
|
||
)
|
||
return str(token).strip()
|
||
|
||
def read_cursor_token_for_dialog(self):
|
||
token = self._read_current_cursor_token()
|
||
if not token:
|
||
self._current_extracted_token = ""
|
||
if self._token_display:
|
||
self._token_display.setPlainText("")
|
||
self.log("⚠️ 未读取到 Token")
|
||
QMessageBox.warning(self, "提示", "未读取到 Token,请确认 Cursor 配置是否存在。")
|
||
return
|
||
|
||
self._current_extracted_token = token
|
||
if self._token_display:
|
||
self._token_display.setPlainText(token)
|
||
self.log("✅ 已读取当前 Token")
|
||
|
||
def save_extracted_token_to_desktop(self):
|
||
token = self._current_extracted_token
|
||
if not token:
|
||
token = self._read_current_cursor_token()
|
||
if token:
|
||
self._current_extracted_token = token
|
||
if self._token_display:
|
||
self._token_display.setPlainText(token)
|
||
if not token:
|
||
QMessageBox.warning(self, "提示", "没有可保存的 Token,请先点击“读取Token”。")
|
||
return
|
||
|
||
desktop = Path.home() / "Desktop"
|
||
save_path = desktop / "token.txt"
|
||
try:
|
||
with open(save_path, "w", encoding="utf-8") as f:
|
||
f.write(token)
|
||
self.log(f"✅ Token 已保存到桌面: {save_path.name}")
|
||
QMessageBox.information(self, "成功", "Token 已覆盖保存到桌面 token.txt")
|
||
except OSError as e:
|
||
self.log(f"❌ Token 保存失败: {e}")
|
||
QMessageBox.warning(self, "失败", f"保存失败:{e}")
|
||
|
||
def download_db_tool(self):
|
||
"""下载 DB Browser 到桌面。"""
|
||
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 clear_cursor_cache(self):
|
||
"""清除 %APPDATA%\\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_force_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_force_close:
|
||
self.log("💀 正在强制关闭Cursor...")
|
||
if kill_cursor():
|
||
self.log("✅ Cursor已关闭")
|
||
else:
|
||
self.log("⚠️ 未找到运行中的Cursor进程")
|
||
QMessageBox.warning(self, "警告", "未找到运行中的Cursor进程")
|
||
return
|
||
else:
|
||
self.log("ℹ️ 已取消清除缓存。")
|
||
return
|
||
|
||
cursor_dir = Path(os.environ.get("APPDATA", "")) / "Cursor"
|
||
if not cursor_dir.exists():
|
||
self.log("ℹ️ 未找到 Cursor 缓存目录,无需清理。")
|
||
QMessageBox.information(self, "提示", "未找到 Cursor 缓存目录,无需清理。")
|
||
return
|
||
|
||
try:
|
||
shutil.rmtree(cursor_dir)
|
||
self.log("✅ Cursor 缓存清除成功")
|
||
QMessageBox.information(self, "完成", "Cursor 缓存已清除成功。")
|
||
return
|
||
except PermissionError:
|
||
self.log("⚠️ 权限不足,尝试请求管理员权限删除缓存...")
|
||
except Exception as e:
|
||
self.log(f"❌ 删除缓存失败: {e}")
|
||
QMessageBox.warning(self, "失败", f"删除缓存失败:{e}")
|
||
return
|
||
|
||
if sys.platform != "win32":
|
||
QMessageBox.warning(
|
||
self,
|
||
"失败",
|
||
"权限不足,无法删除缓存目录。请手动使用管理员权限删除。",
|
||
)
|
||
return
|
||
|
||
# 提权删除:弹 UAC 运行 PowerShell 删除 APPDATA 下的 Cursor 目录。
|
||
ps_cmd = (
|
||
"Remove-Item -LiteralPath \"$env:APPDATA\\Cursor\" -Recurse -Force "
|
||
"-ErrorAction Stop"
|
||
)
|
||
rc = ctypes.windll.shell32.ShellExecuteW(
|
||
None,
|
||
"runas",
|
||
"powershell.exe",
|
||
f"-NoProfile -ExecutionPolicy Bypass -Command \"{ps_cmd}\"",
|
||
None,
|
||
1,
|
||
)
|
||
if rc <= 32:
|
||
self.log("❌ 提权删除未启动,请手动以管理员身份操作。")
|
||
QMessageBox.warning(
|
||
self,
|
||
"权限不足",
|
||
"无法自动提权。\n\n"
|
||
"请手动执行:\n"
|
||
"1. 关闭 Cursor\n"
|
||
"2. 右键 PowerShell 选择“以管理员身份运行”\n"
|
||
"3. 执行命令:Remove-Item -LiteralPath \"$env:APPDATA\\Cursor\" -Recurse -Force",
|
||
)
|
||
return
|
||
|
||
self.log("ℹ️ 已发起管理员删除请求,请在 UAC 窗口中确认。")
|
||
QMessageBox.information(
|
||
self,
|
||
"已请求管理员权限",
|
||
"已发起管理员权限删除请求。\n请在弹出的 UAC 窗口中确认后完成清理。",
|
||
)
|
||
|
||
@Slot(str)
|
||
def _show_usage_result(self, msg):
|
||
QMessageBox.information(self, "额度查询结果", msg)
|
||
|
||
def closeEvent(self, event):
|
||
self.stop_cursor_network_monitor(silent=True)
|
||
super().closeEvent(event)
|
||
|
||
@Slot(bool, str)
|
||
def on_change_finished(self, success, message):
|
||
if self.btnChange:
|
||
self.btnChange.setEnabled(True)
|
||
self.btnChange.setText("🚀 开始换号")
|
||
|
||
if success:
|
||
self.backup_path = message
|
||
self.log("✅ 换号完成")
|
||
self._show_change_success_countdown_dialog(4)
|
||
self.log("🚀 正在打开Cursor...")
|
||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||
self._launch_cursor(cursor_path)
|
||
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() |