3699 lines
149 KiB
Python
3699 lines
149 KiB
Python
import sys
|
||
import json
|
||
import base64
|
||
import hashlib
|
||
import shutil
|
||
import uuid
|
||
import random
|
||
import string
|
||
import re
|
||
import os
|
||
import webbrowser
|
||
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.8"
|
||
|
||
from PySide6.QtWidgets import (
|
||
QApplication,
|
||
QMainWindow,
|
||
QMessageBox,
|
||
QTextEdit,
|
||
QInputDialog,
|
||
QPushButton,
|
||
QVBoxLayout,
|
||
QHBoxLayout,
|
||
QWidget,
|
||
QLineEdit,
|
||
QLabel,
|
||
QGroupBox,
|
||
QDialog,
|
||
QScrollArea,
|
||
QSplashScreen,
|
||
QStyle,
|
||
QListWidget,
|
||
QStackedWidget,
|
||
QGridLayout,
|
||
QSpacerItem,
|
||
QSizePolicy,
|
||
QSplitter,
|
||
)
|
||
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, QAction
|
||
|
||
|
||
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 is_windsurf_running():
|
||
"""检测Windsurf是否正在运行"""
|
||
windsurf_exe_names = ['windsurf.exe', 'windsurf']
|
||
for proc in psutil.process_iter(["name"]):
|
||
try:
|
||
name = proc.info["name"]
|
||
if name and name.lower() in windsurf_exe_names:
|
||
return True
|
||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||
continue
|
||
return False
|
||
|
||
|
||
def kill_windsurf():
|
||
"""强制关闭Windsurf进程"""
|
||
killed = False
|
||
windsurf_exe_names = ['windsurf.exe', 'windsurf']
|
||
for proc in psutil.process_iter(["name", "pid"]):
|
||
try:
|
||
name = proc.info["name"]
|
||
if name and name.lower() in windsurf_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 _run_command_text(args):
|
||
"""执行系统命令并返回文本输出,失败时返回空字符串。"""
|
||
try:
|
||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) if sys.platform == "win32" else 0
|
||
return subprocess.check_output(
|
||
args,
|
||
stderr=subprocess.DEVNULL,
|
||
stdin=subprocess.DEVNULL,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="ignore",
|
||
creationflags=creationflags,
|
||
).strip()
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
def _get_wmic_value(alias: str, field: str) -> str:
|
||
"""通过 WMIC 获取硬件字段值,兼容空值/表头输出。"""
|
||
output = _run_command_text(["wmic", alias, "get", field, "/value"])
|
||
for line in output.splitlines():
|
||
line = line.strip()
|
||
prefix = f"{field}="
|
||
if line.startswith(prefix):
|
||
value = line[len(prefix):].strip()
|
||
if value and value.lower() not in ("none", "null", "to be filled by o.e.m."):
|
||
return value
|
||
|
||
output = _run_command_text(["wmic", alias, "get", field])
|
||
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
||
for line in lines[1:]:
|
||
if line and line.lower() not in ("none", "null", "to be filled by o.e.m."):
|
||
return line
|
||
return ""
|
||
|
||
|
||
def _get_windows_cim_value(class_name: str, field: str) -> str:
|
||
"""WMIC 不可用时,通过 PowerShell CIM 获取硬件字段值。"""
|
||
command = (
|
||
f"(Get-CimInstance {class_name} | "
|
||
f"Select-Object -First 1 -ExpandProperty {field})"
|
||
)
|
||
value = _run_command_text([
|
||
"powershell",
|
||
"-NoProfile",
|
||
"-ExecutionPolicy",
|
||
"Bypass",
|
||
"-Command",
|
||
command,
|
||
]).strip()
|
||
if value and value.lower() not in ("none", "null", "to be filled by o.e.m."):
|
||
return value
|
||
return ""
|
||
|
||
|
||
def _get_cpu_id() -> str:
|
||
"""获取 CPU 标识。"""
|
||
if sys.platform == "win32":
|
||
return (
|
||
_get_wmic_value("cpu", "ProcessorId")
|
||
or _get_windows_cim_value("Win32_Processor", "ProcessorId")
|
||
or os.environ.get("PROCESSOR_IDENTIFIER", "").strip()
|
||
)
|
||
return os.environ.get("PROCESSOR_IDENTIFIER", "").strip() or os.uname().machine
|
||
|
||
|
||
def _get_baseboard_id() -> str:
|
||
"""获取主板标识。"""
|
||
if sys.platform == "win32":
|
||
serial = (
|
||
_get_wmic_value("baseboard", "SerialNumber")
|
||
or _get_windows_cim_value("Win32_BaseBoard", "SerialNumber")
|
||
)
|
||
product = (
|
||
_get_wmic_value("baseboard", "Product")
|
||
or _get_windows_cim_value("Win32_BaseBoard", "Product")
|
||
)
|
||
manufacturer = (
|
||
_get_wmic_value("baseboard", "Manufacturer")
|
||
or _get_windows_cim_value("Win32_BaseBoard", "Manufacturer")
|
||
)
|
||
return "|".join(part for part in (manufacturer, product, serial) if part)
|
||
return socket.gethostname()
|
||
|
||
|
||
def get_renewal_device_id():
|
||
"""生成用于一键续杯的本机设备号:CPU + MAC + 主板。"""
|
||
try:
|
||
cpu_id = _get_cpu_id()
|
||
mac = f"{uuid.getnode():012x}"
|
||
baseboard_id = _get_baseboard_id()
|
||
raw = f"cpu={cpu_id}|mac={mac}|baseboard={baseboard_id}"
|
||
digest = hashlib.sha256(raw.encode("utf-8")).digest()
|
||
return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
|
||
except Exception:
|
||
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 / auth 缓存字段。"""
|
||
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}")
|
||
|
||
# 使用 timeout=5 避免瞬间锁导致失败
|
||
conn = sqlite3.connect(db_path, timeout=5.0)
|
||
cursor = conn.cursor()
|
||
|
||
# 强制合并并清除 WAL 缓存(切换为 DELETE 模式,这样会自动合并并删除 -wal 和 -shm 文件)
|
||
try:
|
||
cursor.execute("PRAGMA journal_mode=delete")
|
||
except Exception as wal_err:
|
||
log_callback(f"⚠️ 无法合并 WAL 缓存 (可能是 Cursor 进程未完全关闭): {wal_err}")
|
||
|
||
# 当前 Cursor 版本主要从 ItemTable 的 cursorAuth/* 读取登录态。
|
||
# 不要只写 accessToken/refreshToken;补齐已登录库里常见的认证缓存字段,
|
||
# 避免客户端启动时因为缓存状态不完整回落到 Log in。
|
||
onboarding_date = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||
updates = [
|
||
("cursorAuth/accessToken", new_token),
|
||
("cursorAuth/refreshToken", new_token),
|
||
("cursorAuth/cachedEmail", new_email),
|
||
("cursorAuth/cachedSignUpType", "Auth_0"),
|
||
("cursorAuth/onboardingDate", onboarding_date),
|
||
("cursorAuth/stripeMembershipType", "pro"),
|
||
]
|
||
|
||
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()
|
||
|
||
# 恢复 WAL 模式以便 Cursor 正常工作
|
||
try:
|
||
cursor.execute("PRAGMA journal_mode=wal")
|
||
conn.commit()
|
||
except Exception as wal_err:
|
||
log_callback(f"⚠️ 无法还原为 WAL 模式: {wal_err}")
|
||
|
||
conn.close()
|
||
log_callback(f"✅ Cursor库 更新成功,已同步 {len(updates)} 个认证字段")
|
||
return True
|
||
|
||
except Exception as e:
|
||
log_callback(f"❌ Cursor库 更新失败: {str(e)}")
|
||
return False
|
||
|
||
|
||
@contextmanager
|
||
def request_with_proxy_fallback(url, **kwargs):
|
||
"""优先走系统代理;代理不可用时自动回退直连。以 contextmanager 统一管理连接生命周期。"""
|
||
try:
|
||
with requests.get(url, **kwargs) as response:
|
||
yield response
|
||
return
|
||
except requests.exceptions.ProxyError:
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
with session.get(url, **kwargs) as response:
|
||
yield response
|
||
|
||
|
||
def check_for_updates():
|
||
"""检查软件更新,返回 (data, error) 元组"""
|
||
try:
|
||
url = "https://api.yunzer.cn/api/softwareupgrade/check?code=cursortokenlogin"
|
||
with request_with_proxy_fallback(url, timeout=10) as response:
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
if data.get("code") == 200:
|
||
return (data.get("data", {}), None)
|
||
else:
|
||
return (None, f"服务器返回错误: {data.get('msg', '未知错误')}")
|
||
except requests.exceptions.Timeout:
|
||
return (None, "连接超时(超过10秒),请检查网络连接")
|
||
except requests.exceptions.ConnectionError as e:
|
||
return (None, f"网络连接失败: {str(e)}")
|
||
except requests.exceptions.RequestException as e:
|
||
return (None, f"请求失败: {str(e)}")
|
||
except Exception as e:
|
||
return (None, f"检查更新失败: {str(e)}")
|
||
|
||
|
||
def compare_versions(current, latest):
|
||
"""比较版本号"""
|
||
try:
|
||
current_parts = list(map(int, current.split('.')))
|
||
latest_parts = list(map(int, latest.split('.')))
|
||
|
||
for i in range(max(len(current_parts), len(latest_parts))):
|
||
current_part = current_parts[i] if i < len(current_parts) else 0
|
||
latest_part = latest_parts[i] if i < len(latest_parts) else 0
|
||
|
||
if latest_part > current_part:
|
||
return 1 # 需要更新
|
||
elif latest_part < current_part:
|
||
return -1 # 当前版本更新
|
||
|
||
return 0 # 版本相同
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def normalize_download_url(raw):
|
||
if raw is None:
|
||
return ""
|
||
s = str(raw).strip()
|
||
if not s or s.lower() in ("null", "none", "undefined"):
|
||
return ""
|
||
return s
|
||
|
||
|
||
def write_windows_updater_batch(old_exe: Path, new_exe: Path) -> Path:
|
||
"""生成批处理:通过 %1 %2 传入路径,避免把长路径写入 bat 产生编码/转义问题。"""
|
||
bat = old_exe.parent / "_cursortokenlogin_update.bat"
|
||
lines = [
|
||
"@echo off",
|
||
"setlocal EnableExtensions",
|
||
'set "OLD=%~1"',
|
||
'set "NEW=%~2"',
|
||
'if not defined OLD goto :eof',
|
||
'if not defined NEW goto :eof',
|
||
'if not exist "%NEW%" exit /b 1',
|
||
"ping 127.0.0.1 -n 5 >nul",
|
||
":wait_del",
|
||
'del /F /Q "%OLD%" 2>nul',
|
||
'if exist "%OLD%" (ping 127.0.0.1 -n 2 >nul & goto wait_del)',
|
||
'move /Y "%NEW%" "%OLD%"',
|
||
"if errorlevel 1 exit /b 1",
|
||
# 不自动启动 exe:解压/runtime 尚未就绪时易触发 Python DLL 加载失败,由用户稍后手动打开
|
||
'del /F /Q "%~f0"',
|
||
]
|
||
bat.write_text("\r\n".join(lines), encoding="utf-8")
|
||
return bat
|
||
|
||
|
||
def launch_detach_no_window(args, cwd=None):
|
||
if sys.platform == "win32":
|
||
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||
subprocess.Popen(
|
||
args,
|
||
cwd=cwd,
|
||
creationflags=flags,
|
||
close_fds=True,
|
||
)
|
||
else:
|
||
subprocess.Popen(args, cwd=cwd, close_fds=True, start_new_session=True)
|
||
|
||
|
||
class DownloadUpdateThread(QThread):
|
||
log_signal = Signal(str)
|
||
progress_signal = Signal(int, int)
|
||
finished_signal = Signal(bool, str)
|
||
|
||
def __init__(self, download_url: str):
|
||
super().__init__()
|
||
self.download_url = download_url
|
||
|
||
def run(self):
|
||
new_path = None
|
||
try:
|
||
self.log_signal.emit("📡 正在请求安装包...")
|
||
with request_with_proxy_fallback(
|
||
self.download_url, stream=True, timeout=60, allow_redirects=True
|
||
) as r:
|
||
r.raise_for_status()
|
||
total = int(r.headers.get("Content-Length") or 0)
|
||
chunk_size = 256 * 1024
|
||
downloaded = 0
|
||
last_pct = -1
|
||
|
||
if getattr(sys, "frozen", False):
|
||
exe_path = Path(sys.executable).resolve()
|
||
suffix = exe_path.suffix or ".exe"
|
||
new_path = exe_path.parent / f"{exe_path.stem}_update_pending{suffix}"
|
||
else:
|
||
fd, tmp = tempfile.mkstemp(suffix=".exe")
|
||
os.close(fd)
|
||
new_path = Path(tmp)
|
||
|
||
# self.log_signal.emit(f"💾 下载保存到: {new_path}")
|
||
|
||
if total > 0:
|
||
self.progress_signal.emit(0, total)
|
||
|
||
next_unknown_log = 512 * 1024
|
||
with open(new_path, "wb") as f:
|
||
for chunk in r.iter_content(chunk_size):
|
||
if not chunk:
|
||
continue
|
||
f.write(chunk)
|
||
downloaded += len(chunk)
|
||
if total > 0:
|
||
pct = min(100, downloaded * 100 // total)
|
||
if pct >= last_pct + 5 or pct == 100:
|
||
last_pct = pct if pct == 100 else (pct // 5) * 5
|
||
self.progress_signal.emit(downloaded, total)
|
||
elif downloaded >= next_unknown_log:
|
||
self.progress_signal.emit(downloaded, 0)
|
||
next_unknown_log += 512 * 1024
|
||
|
||
self.log_signal.emit("✅ 下载完成")
|
||
|
||
if not is_windows_pe_executable(new_path):
|
||
try:
|
||
new_path.unlink()
|
||
except OSError:
|
||
pass
|
||
raise ValueError(
|
||
"下载的文件不是有效的 Windows 安装包(缺少 PE 头或过小),"
|
||
"可能为网页/错误信息,已取消替换。请确认服务端提供的是本软件完整打包的 .exe。"
|
||
)
|
||
|
||
if not getattr(sys, "frozen", False):
|
||
self.log_signal.emit(
|
||
"ℹ️ 当前为脚本/开发模式,不会自动替换已安装程序;请手动用下载文件替换。"
|
||
)
|
||
self.finished_signal.emit(True, "dev_mode")
|
||
return
|
||
|
||
if sys.platform != "win32":
|
||
self.log_signal.emit(
|
||
"ℹ️ 当前为非 Windows 打包环境,请手动将新文件替换为应用程序。"
|
||
)
|
||
self.finished_signal.emit(True, "no_auto_replace")
|
||
return
|
||
|
||
exe_path = Path(sys.executable).resolve()
|
||
self.log_signal.emit("🔧 正在准备替换:退出后将删除旧程序并启动新版本...")
|
||
bat = write_windows_updater_batch(exe_path, new_path)
|
||
launch_detach_no_window(
|
||
["cmd", "/c", str(bat), str(exe_path), str(new_path.resolve())],
|
||
cwd=str(exe_path.parent),
|
||
)
|
||
self.finished_signal.emit(True, "restarting")
|
||
except Exception as e:
|
||
if new_path and new_path.exists():
|
||
try:
|
||
new_path.unlink()
|
||
except OSError:
|
||
pass
|
||
self.log_signal.emit(f"❌ 下载或更新失败: {e}")
|
||
self.finished_signal.emit(False, str(e))
|
||
|
||
|
||
class ImageClickLabel(QLabel):
|
||
"""可点击的图片标签,点击放大显示"""
|
||
clicked = Signal(str)
|
||
|
||
def __init__(self, image_path, parent=None, max_w=350, max_h=450):
|
||
super().__init__(parent)
|
||
self.image_path = image_path
|
||
self.max_w = max_w
|
||
self.max_h = max_h
|
||
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(
|
||
self.max_w, self.max_h,
|
||
Qt.KeepAspectRatio,
|
||
Qt.SmoothTransformation
|
||
)
|
||
self.setPixmap(scaled_pixmap)
|
||
self.setMaximumSize(self.max_w, self.max_h)
|
||
|
||
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):
|
||
"""显示全屏图片"""
|
||
def launch():
|
||
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()
|
||
QTimer.singleShot(0, launch)
|
||
|
||
|
||
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 {}
|
||
|
||
# 显示原邮箱。access token 本身通常不包含 email claim,
|
||
# 因此优先保留 Cursor 原缓存邮箱,避免写入随机邮箱导致登录缓存状态不一致。
|
||
old_email = (
|
||
data.get("cursorAuth", {}).get("cachedEmail")
|
||
or data.get("cursorAccount", {}).get("email")
|
||
or ""
|
||
)
|
||
if old_email:
|
||
self.log_signal.emit(f"📧 原邮箱: {old_email}")
|
||
self.new_email = str(old_email).strip()
|
||
|
||
# 备份原文件(不显示日志)
|
||
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"]["cachedSignUpType"] = data["cursorAuth"].get("cachedSignUpType", "Auth_0")
|
||
data["cursorAuth"]["onboardingDate"] = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||
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 数据库...")
|
||
db_ok = update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
|
||
if not db_ok:
|
||
self.log_signal.emit("❌ 更新 Cursor 数据库失败,换号未完成!")
|
||
self.finished_signal.emit(False, "更新 Cursor 数据库失败,可能是 Cursor 进程未完全关闭,请在任务管理器中结束所有 Cursor 进程,或尝试以管理员身份运行本软件。")
|
||
return
|
||
|
||
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 OneClickRenewalThread(QThread):
|
||
log_signal = Signal(str)
|
||
finished_signal = Signal(bool, str, str) # (success, message/backup_path, new_email)
|
||
|
||
def run(self):
|
||
try:
|
||
self.log_signal.emit("🔄 正在从服务端获取账号...")
|
||
url = "https://api.yunzer.cn/api/getcard?type=local&module=cursor&data_type=tk"
|
||
|
||
response_data = None
|
||
try:
|
||
response = requests.get(url, timeout=15)
|
||
response_data = response.text.strip()
|
||
except requests.exceptions.ProxyError:
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
response = session.get(url, timeout=15)
|
||
response_data = response.text.strip()
|
||
|
||
if not response_data or not response_data.startswith("ey"):
|
||
try:
|
||
err_json = json.loads(response_data)
|
||
msg = err_json.get("msg") or err_json.get("message") or response_data
|
||
except Exception:
|
||
msg = response_data
|
||
self.finished_signal.emit(False, f"获取账号 Token 失败: {msg[:200]}", "")
|
||
return
|
||
|
||
self.log_signal.emit("✅ 成功提取 Token!正在执行本地更换...")
|
||
new_token = response_data
|
||
new_email = generate_random_email()
|
||
|
||
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_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 {}
|
||
|
||
old_email = (
|
||
data.get("cursorAuth", {}).get("cachedEmail")
|
||
or data.get("cursorAccount", {}).get("email")
|
||
or ""
|
||
)
|
||
if old_email:
|
||
self.log_signal.emit(f"📧 原邮箱: {old_email}")
|
||
new_email = str(old_email).strip()
|
||
|
||
shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
|
||
|
||
self.log_signal.emit("🔑 替换 cursorAuth...")
|
||
if "cursorAuth" not in data:
|
||
data["cursorAuth"] = {}
|
||
data["cursorAuth"]["accessToken"] = new_token
|
||
data["cursorAuth"]["refreshToken"] = new_token
|
||
data["cursorAuth"]["cachedEmail"] = new_email
|
||
data["cursorAuth"]["cachedSignUpType"] = data["cursorAuth"].get("cachedSignUpType", "Auth_0")
|
||
data["cursorAuth"]["onboardingDate"] = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||
data["cursorAuth"]["plan"] = "pro"
|
||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||
data["cursorAuth"]["membershipType"] = "pro"
|
||
|
||
self.log_signal.emit("🔑 替换 cursorAccount...")
|
||
if "cursorAccount" not in data:
|
||
data["cursorAccount"] = {}
|
||
data["cursorAccount"]["token"] = new_token
|
||
data["cursorAccount"]["email"] = new_email
|
||
data["cursorAccount"]["plan"] = "pro"
|
||
|
||
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
|
||
data["membershipType"] = "pro"
|
||
|
||
self.log_signal.emit(f"📧 新邮箱: {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)
|
||
|
||
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
||
db_ok = update_vsdb_token(config_dir, new_token, new_email, self.log_signal.emit)
|
||
if not db_ok:
|
||
self.log_signal.emit("❌ 更新 Cursor 数据库失败,换号未完成!")
|
||
self.finished_signal.emit(False, "更新 Cursor 数据库失败,可能是 Cursor 进程未完全关闭,请在任务管理器中结束所有 Cursor 进程,或尝试以管理员身份运行本软件。", "")
|
||
return
|
||
|
||
self.log_signal.emit("✅ 一键续杯换号完成!")
|
||
self.finished_signal.emit(True, str(backup_subdir), new_email)
|
||
|
||
except Exception as e:
|
||
self.log_signal.emit(f"❌ 续杯换号失败: {str(e)}")
|
||
self.finished_signal.emit(False, str(e), "")
|
||
|
||
|
||
class MarkTokenUsabilityThread(QThread):
|
||
"""标记服务端号池 Token 可用/不可用线程。"""
|
||
finished_signal = Signal(bool, dict, str)
|
||
|
||
def __init__(self, token_id, available: bool):
|
||
super().__init__()
|
||
self.token_id = str(token_id).strip()
|
||
self.available = available
|
||
|
||
def run(self):
|
||
endpoint = "available" if self.available else "unavailable"
|
||
url = f"https://api.yunzer.cn/api/cursor/token/{endpoint}?id={self.token_id}"
|
||
try:
|
||
response_data = None
|
||
try:
|
||
with requests.post(url, timeout=15) as response:
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
except requests.exceptions.ProxyError:
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
with session.post(url, timeout=15) as response:
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
|
||
if not response_data:
|
||
self.finished_signal.emit(False, {}, "服务器未返回有效数据")
|
||
return
|
||
|
||
if response_data.get("code") != 200:
|
||
msg = response_data.get("msg") or response_data.get("message") or "标记失败"
|
||
self.finished_signal.emit(False, {}, str(msg))
|
||
return
|
||
|
||
data = response_data.get("data") or {}
|
||
self.finished_signal.emit(True, data, "")
|
||
except requests.exceptions.Timeout:
|
||
self.finished_signal.emit(False, {}, "连接超时,请检查网络")
|
||
except requests.exceptions.ConnectionError as e:
|
||
self.finished_signal.emit(False, {}, f"网络连接失败: {str(e)}")
|
||
except requests.exceptions.RequestException as e:
|
||
self.finished_signal.emit(False, {}, f"请求失败: {str(e)}")
|
||
except Exception as e:
|
||
self.finished_signal.emit(False, {}, f"发生异常: {str(e)}")
|
||
|
||
|
||
class SilentDetectThread(QThread):
|
||
log_signal = Signal(str)
|
||
finished_signal = Signal(bool, str, int) # (success, message/token, next_id)
|
||
|
||
def __init__(self, target_id):
|
||
super().__init__()
|
||
self.target_id = target_id
|
||
|
||
def run(self):
|
||
try:
|
||
self.log_signal.emit(f"🔄 正在从服务端获取 ID={self.target_id} 的 Token...")
|
||
url = f"https://api.yunzer.cn/api/cursor/token/peek?id={self.target_id}&data_type=tk"
|
||
|
||
response_data = None
|
||
try:
|
||
response = requests.get(url, timeout=15)
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
except Exception:
|
||
# 备用连接(直连,不走系统代理)
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
response = session.get(url, timeout=15)
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
|
||
if not response_data or response_data.get("code") != 200:
|
||
msg = response_data.get("msg") or "获取失败"
|
||
self.finished_signal.emit(False, f"获取 Token 失败: {msg}", 0)
|
||
return
|
||
|
||
data_obj = response_data.get("data") or {}
|
||
new_token = data_obj.get("token")
|
||
self.new_token = new_token
|
||
next_id = data_obj.get("next_id") or (int(self.target_id) + 1)
|
||
|
||
if not new_token:
|
||
self.finished_signal.emit(False, "服务端返回的 Token 为空!", 0)
|
||
return
|
||
|
||
self.log_signal.emit("✅ 成功提取 Token!正在执行本地更换...")
|
||
new_email = generate_random_email()
|
||
|
||
config_dir = get_cursor_config_path()
|
||
if not config_dir.exists():
|
||
self.finished_signal.emit(False, "未找到Cursor配置目录", 0)
|
||
return
|
||
|
||
# 读取原 storage.json 并更新
|
||
storage_file = config_dir / "storage.json"
|
||
if not storage_file.exists():
|
||
self.finished_signal.emit(False, "未找到 storage.json 文件", 0)
|
||
return
|
||
|
||
if not os.access(storage_file, os.W_OK):
|
||
import stat
|
||
storage_file.chmod(storage_file.stat().st_mode | stat.S_IWRITE)
|
||
|
||
with open(storage_file, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
data = json.loads(content) if content.strip() else {}
|
||
|
||
old_email = (
|
||
data.get("cursorAuth", {}).get("cachedEmail")
|
||
or data.get("cursorAccount", {}).get("email")
|
||
or ""
|
||
)
|
||
if old_email:
|
||
new_email = str(old_email).strip()
|
||
|
||
# 修改 storage.json 的数据
|
||
if "cursorAuth" not in data:
|
||
data["cursorAuth"] = {}
|
||
data["cursorAuth"]["accessToken"] = new_token
|
||
data["cursorAuth"]["refreshToken"] = new_token
|
||
data["cursorAuth"]["cachedEmail"] = new_email
|
||
data["cursorAuth"]["cachedSignUpType"] = data["cursorAuth"].get("cachedSignUpType", "Auth_0")
|
||
data["cursorAuth"]["onboardingDate"] = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||
data["cursorAuth"]["plan"] = "pro"
|
||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||
data["cursorAuth"]["membershipType"] = "pro"
|
||
|
||
if "cursorAccount" not in data:
|
||
data["cursorAccount"] = {}
|
||
data["cursorAccount"]["token"] = new_token
|
||
data["cursorAccount"]["email"] = new_email
|
||
data["cursorAccount"]["plan"] = "pro"
|
||
|
||
# 刷新机器 ID
|
||
new_machine_id = generate_machine_id()
|
||
data["telemetryMacMachineId"] = new_machine_id
|
||
data["telemetryDevDeviceId"] = new_machine_id
|
||
data["workspaceIdentifier"] = new_machine_id
|
||
data["membershipType"] = "pro"
|
||
|
||
with open(storage_file, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
|
||
# 更新 Cursor 数据库 state.vscdb
|
||
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
||
db_ok = update_vsdb_token(config_dir, new_token, new_email, self.log_signal.emit)
|
||
if not db_ok:
|
||
self.finished_signal.emit(False, "更新 Cursor 数据库失败,可能是 Cursor 进程未完全关闭,请在任务管理器中结束所有 Cursor 进程,或尝试以管理员身份运行本软件。", 0)
|
||
return
|
||
|
||
self.log_signal.emit(f"✅ 无感换号完成!下一条 ID 推荐为: {next_id}")
|
||
self.finished_signal.emit(True, "检测换号成功!", next_id)
|
||
|
||
except Exception as e:
|
||
self.finished_signal.emit(False, f"发生异常: {str(e)}", 0)
|
||
|
||
|
||
def get_windsurf_user_path() -> Path:
|
||
home = Path.home()
|
||
if sys.platform == "win32":
|
||
return home / "AppData" / "Roaming" / "Windsurf" / "User"
|
||
elif sys.platform == "darwin":
|
||
return home / "Library" / "Application Support" / "Windsurf" / "User"
|
||
else:
|
||
return home / ".config" / "Windsurf" / "User"
|
||
|
||
|
||
def reset_windsurf_machine_id(log_callback) -> bool:
|
||
"""重置 Windsurf 的机器码"""
|
||
try:
|
||
config_dir = get_windsurf_user_path()
|
||
global_storage_dir = config_dir / "globalStorage"
|
||
if not global_storage_dir.exists():
|
||
log_callback("⚠️ 未找到 Windsurf 配置目录,请确认是否已安装并运行过 Windsurf。")
|
||
return False
|
||
|
||
# 1. 检查 Windsurf 是否在运行
|
||
windsurf_exe_names = ['windsurf.exe', 'windsurf']
|
||
for proc in psutil.process_iter(["name"]):
|
||
try:
|
||
name = proc.info["name"]
|
||
if name and name.lower() in windsurf_exe_names:
|
||
log_callback("⚠️ 检测到 Windsurf 正在运行,请先关闭 Windsurf 软件!")
|
||
return False
|
||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||
continue
|
||
|
||
# 2. 备份并更新 storage.json
|
||
storage_file = global_storage_dir / "storage.json"
|
||
if storage_file.exists():
|
||
log_callback("📖 读取 storage.json...")
|
||
backup_file = global_storage_dir / "storage.json.backup"
|
||
if backup_file.exists():
|
||
backup_file.unlink()
|
||
shutil.copy2(storage_file, backup_file)
|
||
|
||
with open(storage_file, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
data = json.loads(content) if content.strip() else {}
|
||
|
||
new_machine_id = hashlib.sha256(os.urandom(32)).hexdigest()
|
||
new_dev_device_id = str(uuid.uuid4())
|
||
new_sqm_id = "{" + str(uuid.uuid4()).upper() + "}"
|
||
|
||
data["telemetry.machineId"] = new_machine_id
|
||
data["telemetry.devDeviceId"] = new_dev_device_id
|
||
data["telemetry.sqmId"] = new_sqm_id
|
||
|
||
log_callback("🔧 已生成新的 storage.json Telemetry ID")
|
||
|
||
with open(storage_file, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
else:
|
||
log_callback("⚠️ 未找到 storage.json")
|
||
|
||
# 3. 备份并更新 state.vscdb
|
||
db_path = global_storage_dir / "state.vscdb"
|
||
if db_path.exists():
|
||
log_callback("📖 连接 Windsurf 数据库...")
|
||
db_backup = global_storage_dir / "state.vscdb.backup"
|
||
if db_backup.exists():
|
||
db_backup.unlink()
|
||
shutil.copy2(db_path, db_backup)
|
||
|
||
conn = sqlite3.connect(db_path)
|
||
cursor = conn.cursor()
|
||
|
||
new_service_machine_id = str(uuid.uuid4())
|
||
|
||
cursor.execute(
|
||
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)",
|
||
(new_service_machine_id,)
|
||
)
|
||
|
||
# 清除 codeium 登录会话
|
||
cursor.execute(
|
||
"DELETE FROM ItemTable WHERE key LIKE 'secret://%codeium.windsurf%'"
|
||
)
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
log_callback("🔧 已更新 state.vscdb 数据库机器码,并清除了旧的 Codeium 会话")
|
||
else:
|
||
log_callback("⚠️ 未找到 state.vscdb 数据库")
|
||
|
||
log_callback("✅ Windsurf 机器码重置成功!")
|
||
return True
|
||
except Exception as e:
|
||
log_callback(f"❌ Windsurf 重置失败: {str(e)}")
|
||
return False
|
||
|
||
|
||
def clear_windsurf_cache(log_callback) -> bool:
|
||
"""清除 Windsurf 缓存(删除 %APPDATA%/Windsurf 文件夹)"""
|
||
try:
|
||
path = Path.home() / "AppData" / "Roaming" / "Windsurf"
|
||
if not path.exists():
|
||
log_callback("⚠️ 未找到 Windsurf 数据文件夹,无需清除。")
|
||
return True
|
||
|
||
if is_windsurf_running():
|
||
log_callback("⚠️ 检测到 Windsurf 正在运行,请先关闭 Windsurf 软件!")
|
||
return False
|
||
|
||
log_callback(f"🧹 正在删除 Windsurf 数据文件夹: {path}")
|
||
|
||
def remove_readonly(func, p, excinfo):
|
||
import stat
|
||
os.chmod(p, stat.S_IWRITE)
|
||
func(p)
|
||
|
||
import stat
|
||
shutil.rmtree(path, onerror=remove_readonly)
|
||
|
||
log_callback("✅ Windsurf 数据文件夹已成功清除!")
|
||
return True
|
||
except Exception as e:
|
||
log_callback(f"❌ 清除 Windsurf 数据失败: {str(e)}")
|
||
return False
|
||
|
||
|
||
def get_device_info() -> str:
|
||
"""获取 CPU/RAM/磁盘等设备信息"""
|
||
try:
|
||
cpu_name = _get_cpu_id()
|
||
|
||
virtual_mem = psutil.virtual_memory()
|
||
total_ram_gb = round(virtual_mem.total / (1024 ** 3), 2)
|
||
|
||
try:
|
||
disk_path = "C:\\" if sys.platform == "win32" else "/"
|
||
disk_usage = psutil.disk_usage(disk_path)
|
||
total_disk_gb = round(disk_usage.total / (1024 ** 3), 2)
|
||
free_disk_gb = round(disk_usage.free / (1024 ** 3), 2)
|
||
disk_str = f"Disk: {total_disk_gb} GB (Free: {free_disk_gb} GB)"
|
||
except Exception:
|
||
disk_str = "Disk: 获取失败"
|
||
|
||
cpu_count = psutil.cpu_count(logical=False)
|
||
cpu_logical = psutil.cpu_count(logical=True)
|
||
|
||
info = f"CPU: {cpu_name} ({cpu_count}核/{cpu_logical}线程) | RAM: {total_ram_gb} GB | {disk_str}"
|
||
return info
|
||
except Exception as e:
|
||
return f"获取设备信息失败: {str(e)}"
|
||
|
||
|
||
def read_current_cursor_email_helper() -> str:
|
||
"""从 Cursor 的 storage.json 读取当前 email。"""
|
||
try:
|
||
config_dir = get_cursor_config_path()
|
||
storage_file = config_dir / "storage.json"
|
||
if not storage_file.exists():
|
||
return ""
|
||
with open(storage_file, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
data = json.loads(content) if content.strip() else {}
|
||
email = (
|
||
data.get("cursorAuth", {}).get("cachedEmail")
|
||
or data.get("cursorAccount", {}).get("email")
|
||
or ""
|
||
)
|
||
return str(email).strip()
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
def report_equipment_data(bind_account=None):
|
||
"""上报设备数据,返回 (data, error) 元组"""
|
||
try:
|
||
url = "https://api.yunzer.cn/api/cursor/equipment/report"
|
||
machine_code = get_renewal_device_id()
|
||
device_info = get_device_info()
|
||
system = "Windows" if sys.platform == "win32" else ("macOS" if sys.platform == "darwin" else "Linux")
|
||
version = __VERSION__
|
||
|
||
if not bind_account:
|
||
bind_account = read_current_cursor_email_helper()
|
||
|
||
payload = {
|
||
"machineCode": machine_code,
|
||
"deviceInfo": device_info,
|
||
"system": system,
|
||
"version": version,
|
||
"bindAccount": bind_account,
|
||
"remark": "登录器上报"
|
||
}
|
||
|
||
headers = {
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
response_data = None
|
||
# 优先走代理 fallback
|
||
try:
|
||
with requests.post(url, json=payload, headers=headers, timeout=10) as response:
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
except requests.exceptions.ProxyError:
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
with session.post(url, json=payload, headers=headers, timeout=10) as response:
|
||
response.raise_for_status()
|
||
response_data = response.json()
|
||
|
||
if response_data and response_data.get("code") == 200:
|
||
return (response_data.get("data", {}), None)
|
||
elif response_data:
|
||
return (None, response_data.get("msg", "未知错误"))
|
||
else:
|
||
return (None, "服务器未返回有效数据")
|
||
|
||
except requests.exceptions.Timeout:
|
||
return (None, "连接超时,请检查网络")
|
||
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)}")
|
||
|
||
|
||
class ReportEquipmentThread(QThread):
|
||
"""上报设备信息线程"""
|
||
finished_signal = Signal(bool, dict, str)
|
||
|
||
def __init__(self, bind_account=None):
|
||
super().__init__()
|
||
self.bind_account = bind_account
|
||
|
||
def run(self):
|
||
data, error = report_equipment_data(self.bind_account)
|
||
if error:
|
||
self.finished_signal.emit(False, {}, error)
|
||
else:
|
||
self.finished_signal.emit(True, data, "")
|
||
|
||
|
||
def activate_by_code(activation_code, bind_account=None):
|
||
"""
|
||
使用激活码激活设备,返回 (data, error) 元组
|
||
"""
|
||
try:
|
||
url = "https://api.yunzer.cn/api/cursor/equipment/activateByCode"
|
||
machine_code = get_renewal_device_id()
|
||
device_info = get_device_info()
|
||
system = "Windows" if sys.platform == "win32" else ("macOS" if sys.platform == "darwin" else "Linux")
|
||
version = __VERSION__
|
||
|
||
if not bind_account:
|
||
bind_account = read_current_cursor_email_helper()
|
||
|
||
payload = {
|
||
"activationCode": activation_code,
|
||
"machineCode": machine_code,
|
||
"deviceInfo": device_info,
|
||
"system": system,
|
||
"version": version,
|
||
"bindAccount": bind_account,
|
||
"remark": "登录器激活"
|
||
}
|
||
|
||
headers = {
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
response_data = None
|
||
# 优先走代理 fallback
|
||
try:
|
||
with requests.post(url, json=payload, headers=headers, timeout=10) as response:
|
||
response_data = response.json()
|
||
except requests.exceptions.ProxyError:
|
||
with requests.Session() as session:
|
||
session.trust_env = False
|
||
with session.post(url, json=payload, headers=headers, timeout=10) as response:
|
||
response_data = response.json()
|
||
|
||
if response_data and response_data.get("code") == 200:
|
||
return (response_data.get("data", {}), None)
|
||
elif response_data:
|
||
return (None, response_data.get("msg", "未知错误"))
|
||
else:
|
||
return (None, "服务器未返回有效数据")
|
||
|
||
except requests.exceptions.Timeout:
|
||
return (None, "连接超时,请检查网络")
|
||
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)}")
|
||
|
||
|
||
class ActivateCodeThread(QThread):
|
||
"""激活码激活线程"""
|
||
finished_signal = Signal(bool, dict, str)
|
||
|
||
def __init__(self, activation_code, bind_account=None):
|
||
super().__init__()
|
||
self.activation_code = activation_code
|
||
self.bind_account = bind_account
|
||
|
||
def run(self):
|
||
data, error = activate_by_code(self.activation_code, self.bind_account)
|
||
if error:
|
||
self.finished_signal.emit(False, {}, error)
|
||
else:
|
||
self.finished_signal.emit(True, data, "")
|
||
|
||
|
||
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", "main1.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.member_status = 0
|
||
self._update_download_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.txtDeviceId = self.findChild(QLineEdit, "txtDeviceId")
|
||
self.txtActivationCode = self.findChild(QLineEdit, "txtActivationCode")
|
||
self.btnChange = self.findChild(QPushButton, "btnChange")
|
||
self.btnOpenCursor = self.findChild(QPushButton, "btnOpenCursor")
|
||
self.btnOnlineShop = self.findChild(QPushButton, "btnOnlineShop")
|
||
self.btnBrowseCursor = self.findChild(QPushButton, "btnBrowseCursor")
|
||
self.btnAutoCursorPath = self.findChild(QPushButton, "btnAutoCursorPath")
|
||
self.btnAiRelay = self.findChild(QPushButton, "btnAiRelay")
|
||
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.btnAbout = self.findChild(QPushButton, "btnAbout")
|
||
self.btnCopyDeviceId = self.findChild(QPushButton, "btnCopyDeviceId")
|
||
self.btnActivateRenewal = self.findChild(QPushButton, "btnActivateRenewal")
|
||
self.btnBuyActivationCode = self.findChild(QPushButton, "btnBuyActivationCode")
|
||
self.btnRefreshMemberStatus = self.findChild(QPushButton, "btnRefreshMemberStatus")
|
||
self.btnOneClickRenewal = self.findChild(QPushButton, "btnOneClickRenewal")
|
||
self.lblMemberStatus = self.findChild(QLabel, "lblMemberStatus")
|
||
self.lblMemberLevel = self.findChild(QLabel, "lblMemberLevel")
|
||
self.lblAccountType = self.findChild(QLabel, "lblAccountType")
|
||
self.lblActivatedAt = self.findChild(QLabel, "lblActivatedAt")
|
||
self.lblExpiredAt = self.findChild(QLabel, "lblExpiredAt")
|
||
self.actionExit = self.findChild(QAction, "actionExit")
|
||
self.actionEmergencyRepair = self.findChild(QAction, "actionEmergencyRepair")
|
||
self.actionUsageGuide = self.findChild(QAction, "actionUsageGuide")
|
||
self.actionDonate = self.findChild(QAction, "actionDonate")
|
||
self.actionAbout = self.findChild(QAction, "actionAbout")
|
||
|
||
# 无感检测相关组件
|
||
self.txtSilentToken = self.findChild(QLineEdit, "txtSilentToken")
|
||
self.btnSilentIdMinus = QPushButton("-")
|
||
self.btnSilentIdPlus = QPushButton("+")
|
||
self.btnSilentChange = self.findChild(QPushButton, "btnSilentChange")
|
||
self.btnSilentUnavailable = self.findChild(QPushButton, "btnSilentUnavailable")
|
||
self.btnSilentCopyToken = self.findChild(QPushButton, "btnSilentCopyToken")
|
||
|
||
# 动态添加“可用”按钮,并应用红绿蓝配色样式
|
||
self.btnSilentAvailable = QPushButton("可用")
|
||
self.btnSilentAvailable.setMinimumHeight(42)
|
||
|
||
# 样式定义:无感换号-蓝色,不可用-红色,可用-绿色
|
||
if self.btnSilentChange:
|
||
self.btnSilentChange.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #1e88e5;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #1565c0;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #0d47a1;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #cccccc;
|
||
color: #666666;
|
||
}
|
||
""")
|
||
|
||
if self.btnSilentUnavailable:
|
||
self.btnSilentUnavailable.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #e53935;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #c62828;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #b71c1c;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #cccccc;
|
||
color: #666666;
|
||
}
|
||
""")
|
||
|
||
self.btnSilentAvailable.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #43a047;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #2e7d32;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #1b5e20;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #cccccc;
|
||
color: #666666;
|
||
}
|
||
""")
|
||
|
||
# 插入到布局中,放置在“不可用”按钮的旁边(即“不可用”后面,“复制 Token”前面)
|
||
layout = self.findChild(QHBoxLayout, "horizontalLayout_silentButtons")
|
||
if layout:
|
||
layout.insertWidget(2, self.btnSilentAvailable)
|
||
self.lblSilentToken = self.findChild(QLabel, "lblSilentToken")
|
||
self.lblCurrentDetectId = self.findChild(QLabel, "lblCurrentDetectId")
|
||
self.lblCurrentDetectToken = self.findChild(QTextEdit, "lblCurrentDetectToken")
|
||
self.groupHelpSilentDetect = self.findChild(QGroupBox, "groupHelpSilentDetect")
|
||
self.lblHelpSilentDetectDesc = self.findChild(QLabel, "lblHelpSilentDetectDesc")
|
||
|
||
# 调整无感检测界面的提示词,使其适配以 ID 检测换号的新功能
|
||
if self.lblSilentToken:
|
||
self.lblSilentToken.setText("请输入要检测的号池 ID(如 11):")
|
||
if self.txtSilentToken:
|
||
self.txtSilentToken.setPlaceholderText("在此输入数字 ID,例如:11")
|
||
self.txtSilentToken.setMaximumWidth(150)
|
||
for btn in (self.btnSilentIdMinus, self.btnSilentIdPlus):
|
||
btn.setFixedSize(28, 28)
|
||
btn.setStyleSheet("""
|
||
QPushButton {
|
||
border: 1px solid palette(mid);
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
background: palette(button);
|
||
color: palette(button-text);
|
||
}
|
||
QPushButton:hover {
|
||
background: palette(alternate-base);
|
||
}
|
||
QPushButton:pressed {
|
||
background: palette(midlight);
|
||
}
|
||
""")
|
||
silent_token_layout = self.txtSilentToken.parentWidget().layout() if self.txtSilentToken.parentWidget() else None
|
||
if silent_token_layout and hasattr(silent_token_layout, "indexOf") and hasattr(silent_token_layout, "insertWidget"):
|
||
token_input_index = silent_token_layout.indexOf(self.txtSilentToken)
|
||
if token_input_index >= 0:
|
||
silent_token_row = QWidget(self.txtSilentToken.parentWidget())
|
||
silent_token_row_layout = QHBoxLayout(silent_token_row)
|
||
silent_token_row_layout.setContentsMargins(0, 0, 0, 0)
|
||
silent_token_row_layout.setSpacing(4)
|
||
silent_token_layout.removeWidget(self.txtSilentToken)
|
||
silent_token_row_layout.addWidget(self.txtSilentToken)
|
||
silent_token_row_layout.addWidget(self.btnSilentIdMinus)
|
||
silent_token_row_layout.addWidget(self.btnSilentIdPlus)
|
||
silent_token_row_layout.addStretch()
|
||
silent_token_layout.insertWidget(token_input_index, silent_token_row)
|
||
if self.btnSilentChange:
|
||
self.btnSilentChange.setText("无感换号")
|
||
if self.btnSilentUnavailable:
|
||
self.btnSilentUnavailable.setText("不可用")
|
||
if self.btnSilentCopyToken:
|
||
self.btnSilentCopyToken.setText("复制 Token")
|
||
if self.lblCurrentDetectId:
|
||
self.lblCurrentDetectId.setText("当前检测 ID:-")
|
||
if self.lblCurrentDetectToken:
|
||
self.lblCurrentDetectToken.setText("")
|
||
if self.groupHelpSilentDetect:
|
||
self.groupHelpSilentDetect.setTitle("帮助-无感检测(ID 换号版)")
|
||
if self.lblHelpSilentDetectDesc:
|
||
self.lblHelpSilentDetectDesc.setText("无感检测:输入服务器号池的记录 ID,点击检测将自动从服务器拉取对应 Token,为您执行本地换号,并自动打开 Cursor,方便快速测试。")
|
||
|
||
# 初始化当前检测状态变量
|
||
self.current_detect_id = ""
|
||
self.current_detect_token = ""
|
||
|
||
# 无感检测按钮已移入应急检修的工具面板弹窗中
|
||
|
||
# 从左侧帮助导航列表中移除“无感检测”项与“应急检修”项,并在帮助页面进行偏置连接
|
||
self.helpTabList = self.findChild(QListWidget, "helpTabList")
|
||
help_tab_list = self.helpTabList
|
||
self.helpContentStack = self.findChild(QStackedWidget, "helpContentStack")
|
||
if help_tab_list:
|
||
help_tab_list.setMinimumWidth(100)
|
||
help_tab_list.setMaximumWidth(130)
|
||
if help_tab_list.count() > 4:
|
||
help_tab_list.takeItem(4) # 移除“无感检测”
|
||
if help_tab_list.count() > 0:
|
||
help_tab_list.takeItem(0) # 移除“应急检修”
|
||
|
||
if self.helpContentStack:
|
||
try:
|
||
help_tab_list.currentRowChanged.disconnect()
|
||
except Exception:
|
||
pass
|
||
help_tab_list.currentRowChanged.connect(lambda row: self.helpContentStack.setCurrentIndex(row + 1))
|
||
help_tab_list.setCurrentRow(0)
|
||
self.helpContentStack.setCurrentIndex(1)
|
||
|
||
# 直接在“帮助 -> 捐赠支持”页面中嵌入捐赠内容,不再使用弹窗
|
||
group_help_donate = self.findChild(QGroupBox, "groupHelpDonate")
|
||
if group_help_donate:
|
||
lbl_help_donate_desc = self.findChild(QLabel, "lblHelpDonateDesc")
|
||
if lbl_help_donate_desc:
|
||
lbl_help_donate_desc.hide()
|
||
if self.btnDonate:
|
||
self.btnDonate.hide()
|
||
|
||
layout_donate = group_help_donate.layout()
|
||
if not layout_donate:
|
||
layout_donate = QVBoxLayout(group_help_donate)
|
||
|
||
title_donate = QLabel("您的捐赠将用于维护和改进本软件。\n\n感谢各位,为爱发电,作者需要大家的支持与关注!\n\n有问题请联系QQ:1066960883", group_help_donate)
|
||
title_donate_font = QFont()
|
||
title_donate_font.setPointSize(11)
|
||
title_donate_font.setBold(True)
|
||
title_donate.setFont(title_donate_font)
|
||
title_donate.setAlignment(Qt.AlignCenter)
|
||
layout_donate.addWidget(title_donate)
|
||
|
||
layout_donate.addSpacing(15)
|
||
|
||
images_layout = QHBoxLayout()
|
||
|
||
wx_layout = QVBoxLayout()
|
||
wx_label = QLabel("微信支付", group_help_donate)
|
||
wx_label.setAlignment(Qt.AlignCenter)
|
||
wx_label.setStyleSheet("font-weight: bold;")
|
||
wx_layout.addWidget(wx_label)
|
||
|
||
wx_path = get_resource_path(os.path.join("assets", "images", "donate", "wx.jpg"))
|
||
self.wx_image_embedded = ImageClickLabel(wx_path, parent=group_help_donate, max_w=180, max_h=230)
|
||
self.wx_image_embedded.clicked.connect(self.show_full_image)
|
||
wx_layout.addWidget(self.wx_image_embedded, 0, Qt.AlignCenter)
|
||
images_layout.addLayout(wx_layout)
|
||
|
||
images_layout.addSpacing(20)
|
||
|
||
zfb_layout = QVBoxLayout()
|
||
zfb_label = QLabel("支付宝", group_help_donate)
|
||
zfb_label.setAlignment(Qt.AlignCenter)
|
||
zfb_label.setStyleSheet("font-weight: bold;")
|
||
zfb_layout.addWidget(zfb_label)
|
||
|
||
zfb_path = get_resource_path(os.path.join("assets", "images", "donate", "zfb.jpg"))
|
||
self.zfb_image_embedded = ImageClickLabel(zfb_path, parent=group_help_donate, max_w=180, max_h=230)
|
||
self.zfb_image_embedded.clicked.connect(self.show_full_image)
|
||
zfb_layout.addWidget(self.zfb_image_embedded, 0, Qt.AlignCenter)
|
||
images_layout.addLayout(zfb_layout)
|
||
|
||
layout_donate.addLayout(images_layout)
|
||
layout_donate.addStretch()
|
||
|
||
# 直接在“帮助 -> 关于软件”页面中嵌入关于内容,不再使用弹窗
|
||
group_help_about = self.findChild(QGroupBox, "groupHelpAbout")
|
||
if group_help_about:
|
||
lbl_help_about_desc = self.findChild(QLabel, "lblHelpAboutDesc")
|
||
if lbl_help_about_desc:
|
||
lbl_help_about_desc.hide()
|
||
if self.btnAbout:
|
||
self.btnAbout.hide()
|
||
|
||
layout_about = group_help_about.layout()
|
||
if not layout_about:
|
||
layout_about = QVBoxLayout(group_help_about)
|
||
|
||
about_label = QLabel(group_help_about)
|
||
about_label.setText(
|
||
f"<h2>牛马Cursor登录器</h2>"
|
||
f"<p><b>当前版本:</b>{__VERSION__}</p>"
|
||
f"<p><b>用途简介:</b>用于 Cursor 账号登录、配置管理和常用工具操作。</p>"
|
||
f"<p><b>技术交流群:</b>720797421 (QQ群)</p>"
|
||
)
|
||
about_label.setStyleSheet("color: palette(text); font-size: 11pt; line-height: 1.6;")
|
||
about_label.setOpenExternalLinks(True)
|
||
layout_about.addWidget(about_label)
|
||
layout_about.addStretch()
|
||
|
||
# 动态重组左侧菜单与页面层级,支持二级菜单
|
||
self.tabList = self.findChild(QListWidget, "tabList")
|
||
if self.tabList:
|
||
self.tabList.setMinimumWidth(100)
|
||
self.tabList.setMaximumWidth(130)
|
||
self.contentStack = self.findChild(QStackedWidget, "contentStack")
|
||
|
||
if self.tabList and self.contentStack:
|
||
# 1. 提取并暂存原有 UI 设计中的主页面控件
|
||
page_login = self.findChild(QWidget, "pageLogin")
|
||
page_renewal = self.findChild(QWidget, "pageRenewal")
|
||
page_cursor_config = self.findChild(QWidget, "pageCursorConfig")
|
||
page_help = self.findChild(QWidget, "pageHelp")
|
||
|
||
# 2. 从主 contentStack 中移除这些原有页面
|
||
if page_login:
|
||
self.contentStack.removeWidget(page_login)
|
||
if page_renewal:
|
||
self.contentStack.removeWidget(page_renewal)
|
||
if page_cursor_config:
|
||
self.contentStack.removeWidget(page_cursor_config)
|
||
if page_help:
|
||
self.contentStack.removeWidget(page_help)
|
||
|
||
# 样式定义:使二级侧边栏样式一致
|
||
sidebar_style = """
|
||
QListWidget {
|
||
border: 1px solid palette(mid);
|
||
border-radius: 4px;
|
||
background: palette(base);
|
||
color: palette(text);
|
||
outline: none;
|
||
}
|
||
QListWidget::item {
|
||
min-height: 34px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid palette(midlight);
|
||
}
|
||
QListWidget::item:selected {
|
||
color: palette(highlighted-text);
|
||
background: palette(highlight);
|
||
}
|
||
QListWidget::item:hover:!selected {
|
||
background: palette(alternate-base);
|
||
}
|
||
"""
|
||
|
||
# 3. 创建全新的 "Cursor" 归集主页面,采用双栏二级子菜单布局
|
||
self.pageCursor = QWidget()
|
||
layout_cursor = QHBoxLayout(self.pageCursor)
|
||
layout_cursor.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# Cursor 左侧二级菜单
|
||
self.cursorTabList = QListWidget(self.pageCursor)
|
||
self.cursorTabList.setMinimumWidth(100)
|
||
self.cursorTabList.setMaximumWidth(130)
|
||
self.cursorTabList.setStyleSheet(sidebar_style)
|
||
self.cursorTabList.addItem("Token切换")
|
||
self.cursorTabList.addItem("一键续杯")
|
||
self.cursorTabList.addItem("Cursor配置")
|
||
self.cursorTabList.addItem("应急检修")
|
||
self.cursorTabList.setCurrentRow(0)
|
||
layout_cursor.addWidget(self.cursorTabList)
|
||
|
||
# Cursor 右侧内容堆栈
|
||
self.cursorContentStack = QStackedWidget(self.pageCursor)
|
||
layout_cursor.addWidget(self.cursorContentStack)
|
||
|
||
if page_login:
|
||
self.cursorContentStack.addWidget(page_login)
|
||
if page_renewal:
|
||
self.cursorContentStack.addWidget(page_renewal)
|
||
if page_cursor_config:
|
||
self.cursorContentStack.addWidget(page_cursor_config)
|
||
|
||
# 创建“应急检修”子页面,并将按钮直接显示在右侧
|
||
self.pageCursorEmergency = QWidget()
|
||
layout_cursor_emergency = QVBoxLayout(self.pageCursorEmergency)
|
||
layout_cursor_emergency.setContentsMargins(10, 10, 10, 10)
|
||
|
||
group_emergency = QGroupBox("应急检修", self.pageCursorEmergency)
|
||
group_emergency_layout = QVBoxLayout(group_emergency)
|
||
|
||
desc_label = QLabel("应急检修:提供 Cursor 故障排查、缓存清理、Token 提取等应急工具。", group_emergency)
|
||
desc_label.setStyleSheet("color: palette(text); font-size: 10pt; margin-bottom: 10px;")
|
||
group_emergency_layout.addWidget(desc_label)
|
||
|
||
grid_layout_emergency = QGridLayout()
|
||
grid_layout_emergency.setSpacing(10)
|
||
|
||
self.btnDownloadDB = QPushButton("下载DB Browser", group_emergency)
|
||
self.btnDownloadDB.setMinimumHeight(45)
|
||
self.btnDownloadDB.setMinimumWidth(180)
|
||
self.btnDownloadDB.setMaximumWidth(260)
|
||
|
||
self.btnClearCursorCache = QPushButton("清除Cursor缓存", group_emergency)
|
||
self.btnClearCursorCache.setMinimumHeight(45)
|
||
self.btnClearCursorCache.setMinimumWidth(180)
|
||
self.btnClearCursorCache.setMaximumWidth(260)
|
||
|
||
self.btnExtractToken = QPushButton("Token提取", group_emergency)
|
||
self.btnExtractToken.setMinimumHeight(45)
|
||
self.btnExtractToken.setMinimumWidth(180)
|
||
self.btnExtractToken.setMaximumWidth(260)
|
||
|
||
self.btnSilentDetect = QPushButton("无感检测", group_emergency)
|
||
self.btnSilentDetect.setMinimumHeight(45)
|
||
self.btnSilentDetect.setMinimumWidth(180)
|
||
self.btnSilentDetect.setMaximumWidth(260)
|
||
|
||
# 应用与 self.btnEmergencyRepair 一致的样式
|
||
for btn in (self.btnDownloadDB, self.btnClearCursorCache, self.btnExtractToken, self.btnSilentDetect):
|
||
if self.btnEmergencyRepair:
|
||
btn.setStyleSheet(self.btnEmergencyRepair.styleSheet())
|
||
btn.setFont(self.btnEmergencyRepair.font())
|
||
else:
|
||
btn.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #1e88e5;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
font-size: 10pt;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #1565c0;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #0d47a1;
|
||
}
|
||
""")
|
||
|
||
grid_layout_emergency.addWidget(self.btnDownloadDB, 0, 0)
|
||
grid_layout_emergency.addWidget(self.btnClearCursorCache, 0, 1)
|
||
grid_layout_emergency.addWidget(self.btnExtractToken, 1, 0)
|
||
grid_layout_emergency.addWidget(self.btnSilentDetect, 1, 1)
|
||
grid_layout_emergency.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, 2)
|
||
|
||
group_emergency_layout.addLayout(grid_layout_emergency)
|
||
group_emergency_layout.addStretch()
|
||
|
||
layout_cursor_emergency.addWidget(group_emergency)
|
||
layout_cursor_emergency.addStretch()
|
||
|
||
self.cursorContentStack.addWidget(self.pageCursorEmergency)
|
||
|
||
# 连接应急检修的按钮信号
|
||
self.btnDownloadDB.clicked.connect(self.download_db_tool)
|
||
self.btnClearCursorCache.clicked.connect(self.clear_cursor_cache)
|
||
self.btnExtractToken.clicked.connect(self.on_token_extract_clicked)
|
||
self.btnSilentDetect.clicked.connect(self.on_silent_detect_menu_clicked)
|
||
|
||
self.cursorTabList.currentRowChanged.connect(self.cursorContentStack.setCurrentIndex)
|
||
|
||
# 4. 创建全新的 "Windsurf" 归集主页面,双栏二级子菜单布局
|
||
self.pageWindsurf = QWidget()
|
||
layout_windsurf = QHBoxLayout(self.pageWindsurf)
|
||
layout_windsurf.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# Windsurf 左侧二级菜单
|
||
self.windsurfTabList = QListWidget(self.pageWindsurf)
|
||
self.windsurfTabList.setMinimumWidth(100)
|
||
self.windsurfTabList.setMaximumWidth(130)
|
||
self.windsurfTabList.setStyleSheet(sidebar_style)
|
||
self.windsurfTabList.addItem("Winsurf配置")
|
||
self.windsurfTabList.setCurrentRow(0)
|
||
layout_windsurf.addWidget(self.windsurfTabList)
|
||
|
||
# Windsurf 右侧内容堆栈
|
||
self.windsurfContentStack = QStackedWidget(self.pageWindsurf)
|
||
layout_windsurf.addWidget(self.windsurfContentStack)
|
||
|
||
self.pageWindsurfConfig = QWidget()
|
||
layout_windsurf_config = QVBoxLayout(self.pageWindsurfConfig)
|
||
layout_windsurf_config.setContentsMargins(10, 10, 10, 10)
|
||
|
||
group_windsurf = QGroupBox("Winsurf配置", self.pageWindsurfConfig)
|
||
group_layout = QVBoxLayout(group_windsurf)
|
||
|
||
grid_layout = QGridLayout()
|
||
grid_layout.setSpacing(10)
|
||
|
||
self.btnResetWindsurf = QPushButton("重置机器码", group_windsurf)
|
||
self.btnResetWindsurf.setMinimumHeight(45)
|
||
self.btnClearWindsurf = QPushButton("清除缓存", group_windsurf)
|
||
self.btnClearWindsurf.setMinimumHeight(45)
|
||
|
||
for btn in (self.btnResetWindsurf, self.btnClearWindsurf):
|
||
if self.btnEmergencyRepair:
|
||
btn.setStyleSheet(self.btnEmergencyRepair.styleSheet())
|
||
btn.setFont(self.btnEmergencyRepair.font())
|
||
else:
|
||
btn.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #1e88e5;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
font-size: 10pt;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #1565c0;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #0d47a1;
|
||
}
|
||
""")
|
||
|
||
grid_layout.addWidget(self.btnResetWindsurf, 0, 0)
|
||
grid_layout.addWidget(self.btnClearWindsurf, 0, 1)
|
||
grid_layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, 2)
|
||
|
||
group_layout.addLayout(grid_layout)
|
||
group_layout.addStretch()
|
||
|
||
layout_windsurf_config.addWidget(group_windsurf)
|
||
layout_windsurf_config.addStretch()
|
||
|
||
self.windsurfContentStack.addWidget(self.pageWindsurfConfig)
|
||
self.windsurfContentStack.setCurrentIndex(0)
|
||
self.windsurfTabList.currentRowChanged.connect(self.windsurfContentStack.setCurrentIndex)
|
||
|
||
self.btnResetWindsurf.clicked.connect(self.on_reset_windsurf_clicked)
|
||
self.btnClearWindsurf.clicked.connect(self.on_clear_windsurf_clicked)
|
||
|
||
# 5. 创建全新 "Kiro" 主页面,双栏布局占位
|
||
self.pageKiro = QWidget()
|
||
layout_kiro = QHBoxLayout(self.pageKiro)
|
||
layout_kiro.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self.kiroTabList = QListWidget(self.pageKiro)
|
||
self.kiroTabList.setMinimumWidth(100)
|
||
self.kiroTabList.setMaximumWidth(130)
|
||
self.kiroTabList.setStyleSheet(sidebar_style)
|
||
self.kiroTabList.addItem("Kiro配置")
|
||
self.kiroTabList.setCurrentRow(0)
|
||
layout_kiro.addWidget(self.kiroTabList)
|
||
|
||
self.kiroContentStack = QStackedWidget(self.pageKiro)
|
||
layout_kiro.addWidget(self.kiroContentStack)
|
||
|
||
self.pageKiroConfig = QWidget()
|
||
layout_kiro_config = QVBoxLayout(self.pageKiroConfig)
|
||
layout_kiro_config.setContentsMargins(10, 10, 10, 10)
|
||
|
||
group_kiro = QGroupBox("Kiro配置", self.pageKiroConfig)
|
||
group_kiro_layout = QVBoxLayout(group_kiro)
|
||
|
||
kiro_label = QLabel("Kiro 应急检修工具正在开发中,敬请期待...", group_kiro)
|
||
kiro_label.setStyleSheet("color: palette(text); font-size: 10pt;")
|
||
group_kiro_layout.addWidget(kiro_label)
|
||
group_kiro_layout.addStretch()
|
||
|
||
layout_kiro_config.addWidget(group_kiro)
|
||
layout_kiro_config.addStretch()
|
||
|
||
self.kiroContentStack.addWidget(self.pageKiroConfig)
|
||
self.kiroContentStack.setCurrentIndex(0)
|
||
self.kiroTabList.currentRowChanged.connect(self.kiroContentStack.setCurrentIndex)
|
||
|
||
# 6. 清空主内容堆栈并按新排序添加页面:Cursor、Windsurf、Kiro、帮助
|
||
while self.contentStack.count() > 0:
|
||
self.contentStack.removeWidget(self.contentStack.widget(0))
|
||
|
||
self.contentStack.addWidget(self.pageCursor) # index 0
|
||
self.contentStack.addWidget(self.pageWindsurf) # index 1
|
||
self.contentStack.addWidget(self.pageKiro) # index 2
|
||
if page_help:
|
||
self.contentStack.addWidget(page_help) # index 3
|
||
|
||
# 7. 重建左侧主 tabList 导航项并定位到第一项
|
||
self.tabList.clear()
|
||
self.tabList.addItem("Cursor")
|
||
self.tabList.addItem("Windsurf")
|
||
self.tabList.addItem("Kiro")
|
||
self.tabList.addItem("帮助")
|
||
self.tabList.setCurrentRow(0)
|
||
|
||
# 8. 获取并配置拖拽分割条,使主页面和日志区域大小可交互可拖动,并增强把手的视觉提示
|
||
self.splitterMainLog = self.findChild(QSplitter, "splitterMainLog")
|
||
if self.splitterMainLog:
|
||
self.splitterMainLog.setStyleSheet("""
|
||
QSplitter::handle:vertical {
|
||
background-color: palette(mid);
|
||
height: 5px;
|
||
}
|
||
QSplitter::handle:vertical:hover {
|
||
background-color: palette(highlight);
|
||
}
|
||
""")
|
||
# 设置初始比例:主区域 430,日志面板 170
|
||
self.splitterMainLog.setSizes([430, 170])
|
||
|
||
self.splitterBody = self.findChild(QSplitter, "splitterBody")
|
||
if self.splitterBody:
|
||
self.splitterBody.setStyleSheet("""
|
||
QSplitter::handle:horizontal {
|
||
background-color: palette(mid);
|
||
width: 5px;
|
||
}
|
||
QSplitter::handle:horizontal:hover {
|
||
background-color: palette(highlight);
|
||
}
|
||
""")
|
||
|
||
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"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.txtDeviceId:
|
||
self.txtDeviceId.setText(get_renewal_device_id())
|
||
self._reset_member_status_display()
|
||
|
||
# 为在线商城按钮设置图标(使用 Qt 内置图标,避免依赖外部资源)
|
||
if self.btnOnlineShop or self.btnBuyActivationCode:
|
||
shop_icon = QIcon.fromTheme("shopping-cart")
|
||
if shop_icon.isNull():
|
||
shop_icon = QIcon.fromTheme("emblem-sales")
|
||
if shop_icon.isNull():
|
||
shop_icon = self.style().standardIcon(QStyle.SP_DialogOpenButton)
|
||
if self.btnOnlineShop:
|
||
self.btnOnlineShop.setIcon(shop_icon)
|
||
if self.btnBuyActivationCode:
|
||
self.btnBuyActivationCode.setIcon(shop_icon)
|
||
|
||
# 信号连接
|
||
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.btnOnlineShop:
|
||
self.btnOnlineShop.clicked.connect(self.on_open_online_shop_clicked)
|
||
if self.btnAiRelay:
|
||
self.btnAiRelay.clicked.connect(self.on_open_ai_relay_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)
|
||
if self.btnAbout:
|
||
self.btnAbout.clicked.connect(self.on_about_clicked)
|
||
if self.btnCopyDeviceId:
|
||
self.btnCopyDeviceId.clicked.connect(self.on_copy_device_id_clicked)
|
||
if self.btnActivateRenewal:
|
||
self.btnActivateRenewal.clicked.connect(self.on_activate_renewal_clicked)
|
||
if self.btnBuyActivationCode:
|
||
self.btnBuyActivationCode.clicked.connect(self.on_open_online_shop_clicked)
|
||
if self.btnRefreshMemberStatus:
|
||
self.btnRefreshMemberStatus.clicked.connect(lambda: self.on_refresh_member_status_clicked(show_popup=True))
|
||
if self.btnOneClickRenewal:
|
||
self.btnOneClickRenewal.clicked.connect(self.on_one_click_renewal_clicked)
|
||
if self.btnSilentIdMinus:
|
||
self.btnSilentIdMinus.clicked.connect(lambda: self._adjust_silent_id(-1))
|
||
if self.btnSilentIdPlus:
|
||
self.btnSilentIdPlus.clicked.connect(lambda: self._adjust_silent_id(1))
|
||
if self.btnSilentChange:
|
||
self.btnSilentChange.clicked.connect(self.on_silent_detect_clicked)
|
||
if self.btnSilentUnavailable:
|
||
self.btnSilentUnavailable.clicked.connect(self.on_silent_unavailable_clicked)
|
||
if self.btnSilentAvailable:
|
||
self.btnSilentAvailable.clicked.connect(self.on_silent_available_clicked)
|
||
if self.btnSilentCopyToken:
|
||
self.btnSilentCopyToken.clicked.connect(self.on_silent_copy_token_clicked)
|
||
if self.actionExit:
|
||
self.actionExit.triggered.connect(self.close)
|
||
if self.actionEmergencyRepair:
|
||
self.actionEmergencyRepair.triggered.connect(self.on_emergency_repair_clicked)
|
||
if self.actionUsageGuide:
|
||
self.actionUsageGuide.triggered.connect(self.on_usage_guide_clicked)
|
||
if self.actionDonate:
|
||
self.actionDonate.triggered.connect(self.on_donate_clicked)
|
||
if self.actionAbout:
|
||
self.actionAbout.triggered.connect(self.on_about_clicked)
|
||
# 设置状态栏:左侧显示QQ群,右侧显示版本号
|
||
qq_icon_label = QLabel()
|
||
qq_icon = QIcon.fromTheme("im-qq")
|
||
if qq_icon.isNull():
|
||
qq_icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation)
|
||
qq_icon_label.setPixmap(qq_icon.pixmap(16, 16))
|
||
self.statusBar().addWidget(qq_icon_label)
|
||
self.statusBar().addWidget(QLabel("QQ群:720797421"))
|
||
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()
|
||
|
||
# 启动设备上报并同步状态
|
||
self.on_refresh_member_status_clicked(show_popup=False)
|
||
|
||
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 on_open_online_shop_clicked(self):
|
||
"""打开在线商城页面。"""
|
||
shop_url = "https://shop.yunzer.cn/"
|
||
try:
|
||
opened = webbrowser.open(shop_url)
|
||
if opened:
|
||
self.log(f"🌐 已打开在线商城: {shop_url}")
|
||
else:
|
||
self.log(f"⚠️ 未能自动打开浏览器,请手动访问: {shop_url}")
|
||
QMessageBox.warning(self, "提示", f"无法自动打开浏览器,请手动访问:\n{shop_url}")
|
||
except Exception as e:
|
||
self.log(f"❌ 打开在线商城失败: {str(e)}")
|
||
QMessageBox.warning(self, "警告", f"打开在线商城失败: {str(e)}")
|
||
|
||
def on_open_ai_relay_clicked(self):
|
||
"""打开 AI 中转站页面。"""
|
||
relay_url = "https://api.yunzer.com.cn/"
|
||
try:
|
||
opened = webbrowser.open(relay_url)
|
||
if opened:
|
||
self.log(f"🌐 已打开AI中转站: {relay_url}")
|
||
else:
|
||
self.log(f"⚠️ 未能自动打开浏览器,请手动访问: {relay_url}")
|
||
QMessageBox.warning(self, "提示", f"无法自动打开浏览器,请手动访问:\n{relay_url}")
|
||
except Exception as e:
|
||
self.log(f"❌ 打开AI中转站失败: {str(e)}")
|
||
QMessageBox.warning(self, "警告", f"打开AI中转站失败: {str(e)}")
|
||
|
||
def on_copy_device_id_clicked(self):
|
||
"""复制一键续杯设备号。"""
|
||
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||
if not device_id:
|
||
QMessageBox.warning(self, "提示", "设备号为空,无法复制。")
|
||
return
|
||
QApplication.clipboard().setText(device_id)
|
||
self.log("✅ 设备号已复制到剪贴板")
|
||
QMessageBox.information(self, "成功", "设备号已复制到剪贴板。")
|
||
|
||
def on_activate_renewal_clicked(self):
|
||
"""提交一键续杯激活码。"""
|
||
activation_code = self.txtActivationCode.text().strip() if self.txtActivationCode else ""
|
||
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||
if not activation_code:
|
||
QMessageBox.warning(self, "提示", "请输入激活码。")
|
||
return
|
||
|
||
self.log(f"🔑 正在激活,设备号:{device_id},激活码:{activation_code}")
|
||
|
||
if self.btnActivateRenewal:
|
||
self.btnActivateRenewal.setEnabled(False)
|
||
self.btnActivateRenewal.setText("🔄 激活中...")
|
||
|
||
self._activate_thread = ActivateCodeThread(activation_code)
|
||
self._activate_thread.finished_signal.connect(self.on_activate_finished)
|
||
self._activate_thread.start()
|
||
|
||
def on_activate_finished(self, success, data, error_msg):
|
||
if self.btnActivateRenewal:
|
||
self.btnActivateRenewal.setEnabled(True)
|
||
self.btnActivateRenewal.setText("🔑 提交激活码")
|
||
|
||
if success:
|
||
self.log("✅ 激活成功!已成功同步有效期及状态")
|
||
|
||
status = data.get("status", 1)
|
||
self.member_status = status
|
||
status_text = "已激活" if status == 1 else "已过期"
|
||
|
||
if self.lblMemberStatus:
|
||
self.lblMemberStatus.setText(f"当前状态:{status_text}")
|
||
if self.lblMemberLevel:
|
||
self.lblMemberLevel.setText(status_text)
|
||
|
||
act_time = data.get("activatedAt") or data.get("activationAt") or "-"
|
||
exp_time = data.get("expireTime") or data.get("expiredAt") or "-"
|
||
|
||
def format_time_str(t_str):
|
||
if not t_str or t_str == "-":
|
||
return "-"
|
||
t_str = str(t_str).replace("T", " ")
|
||
if "+" in t_str:
|
||
t_str = t_str.split("+")[0]
|
||
if "Z" in t_str:
|
||
t_str = t_str.replace("Z", "")
|
||
if "." in t_str:
|
||
t_str = t_str.split(".")[0]
|
||
return t_str.strip()
|
||
|
||
if self.lblActivatedAt:
|
||
self.lblActivatedAt.setText(format_time_str(act_time))
|
||
if self.lblExpiredAt:
|
||
self.lblExpiredAt.setText(format_time_str(exp_time))
|
||
|
||
duration = data.get("durationDays", 0)
|
||
self.log(f"🎉 激活成功!增加天数:{duration} 天,到期时间:{format_time_str(exp_time)}")
|
||
QMessageBox.information(
|
||
self,
|
||
"激活成功",
|
||
f"激活码校验通过!\n绑定机器码成功,已续期 {duration} 天。\n到期时间:{format_time_str(exp_time)}"
|
||
)
|
||
else:
|
||
self.log(f"❌ 激活失败: {error_msg}")
|
||
QMessageBox.critical(self, "激活失败", f"无法激活设备:{error_msg}")
|
||
|
||
def _reset_member_status_display(self):
|
||
"""重置会员状态展示。"""
|
||
if self.lblMemberStatus:
|
||
self.lblMemberStatus.setText("当前状态:未刷新")
|
||
if self.lblMemberLevel:
|
||
self.lblMemberLevel.setText("-")
|
||
if self.lblAccountType:
|
||
self.lblAccountType.setText("-")
|
||
if self.lblActivatedAt:
|
||
self.lblActivatedAt.setText("-")
|
||
if self.lblExpiredAt:
|
||
self.lblExpiredAt.setText("-")
|
||
|
||
def start_equipment_report(self, bind_account=None, show_popup=False):
|
||
"""开始上报设备信息"""
|
||
self._report_thread = ReportEquipmentThread(bind_account)
|
||
self._report_thread.finished_signal.connect(
|
||
lambda success, data, error_msg: self.on_equipment_report_finished(success, data, error_msg, show_popup)
|
||
)
|
||
self._report_thread.start()
|
||
|
||
def on_equipment_report_finished(self, success, data, error_msg, show_popup=False):
|
||
if success:
|
||
# self.log("✅ 设备数据已成功上报到平台")
|
||
status = data.get("status", 0)
|
||
self.member_status = status
|
||
status_text = "未激活"
|
||
if status == 1:
|
||
status_text = "已激活"
|
||
elif status == 2:
|
||
status_text = "已过期"
|
||
elif status == 3:
|
||
status_text = "已禁用"
|
||
|
||
if self.lblMemberStatus:
|
||
self.lblMemberStatus.setText(f"当前状态:{status_text}")
|
||
if self.lblMemberLevel:
|
||
self.lblMemberLevel.setText(status_text)
|
||
|
||
act_time = data.get("activationTime") or data.get("activation_time") or "-"
|
||
exp_time = data.get("expireTime") or data.get("expire_time") or "-"
|
||
|
||
def format_time_str(t_str):
|
||
if not t_str or t_str == "-":
|
||
return "-"
|
||
t_str = str(t_str).replace("T", " ")
|
||
if "+" in t_str:
|
||
t_str = t_str.split("+")[0]
|
||
if "Z" in t_str:
|
||
t_str = t_str.replace("Z", "")
|
||
if "." in t_str:
|
||
t_str = t_str.split(".")[0]
|
||
return t_str.strip()
|
||
|
||
if self.lblActivatedAt:
|
||
self.lblActivatedAt.setText(format_time_str(act_time))
|
||
if self.lblExpiredAt:
|
||
self.lblExpiredAt.setText(format_time_str(exp_time))
|
||
|
||
owner_name = data.get("ownerUserName") or data.get("owner_user_name")
|
||
if owner_name:
|
||
if self.lblAccountType:
|
||
self.lblAccountType.setText(f"绑定用户:{owner_name}")
|
||
else:
|
||
if self.lblAccountType:
|
||
self.lblAccountType.setText("普通设备")
|
||
|
||
if show_popup:
|
||
QMessageBox.information(self, "刷新成功", f"会员状态已同步!当前状态:{status_text}")
|
||
else:
|
||
self.log(f"⚠️ 设备数据同步失败: {error_msg}")
|
||
if show_popup:
|
||
QMessageBox.warning(self, "刷新失败", f"无法同步设备状态:{error_msg}")
|
||
|
||
def on_refresh_member_status_clicked(self, show_popup=False):
|
||
"""刷新会员状态展示。"""
|
||
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||
if not device_id:
|
||
if show_popup:
|
||
QMessageBox.warning(self, "提示", "设备号为空,无法刷新会员状态。")
|
||
return
|
||
|
||
if self.lblMemberStatus:
|
||
self.lblMemberStatus.setText("当前状态:正在同步平台...")
|
||
|
||
self.log("🔄 正在从平台同步设备与会员状态...")
|
||
self.start_equipment_report(show_popup=show_popup)
|
||
|
||
def on_one_click_renewal_clicked(self):
|
||
"""执行一键续杯。"""
|
||
# 1. 检测是否激活
|
||
is_active = False
|
||
if hasattr(self, 'member_status') and self.member_status == 1:
|
||
is_active = True
|
||
elif self.lblMemberStatus and "已激活" in self.lblMemberStatus.text():
|
||
is_active = True
|
||
|
||
if not is_active:
|
||
QMessageBox.warning(self, "提示", "本设备未激活,请输入激活码激活。")
|
||
self.log("⚠️ 续杯失败:本设备未激活,请输入激活码激活。")
|
||
return
|
||
|
||
# 2. 检测 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
|
||
|
||
# 3. 检查 Cursor 路径
|
||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||
if not cursor_path or not Path(cursor_path).exists():
|
||
QMessageBox.warning(self, "警告", "请设置正确的Cursor路径!")
|
||
return
|
||
|
||
# 4. 再次确认续杯
|
||
confirm = QMessageBox.question(
|
||
self,
|
||
"确认一键续杯",
|
||
"请确认当前额度已经用完后再续杯。\n\n"
|
||
"如发现大量未使用完就续杯情况,一律封号处理,请悉知!\n\n"
|
||
"确定要继续执行一键续杯吗?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No,
|
||
)
|
||
if confirm != QMessageBox.Yes:
|
||
self.log("ℹ️ 已取消一键续杯。")
|
||
return
|
||
|
||
# 5. 禁用按钮并启动续杯换号后台线程
|
||
if self.btnOneClickRenewal:
|
||
self.btnOneClickRenewal.setEnabled(False)
|
||
self.btnOneClickRenewal.setText("🔄 续杯中...")
|
||
|
||
self._renewal_thread = OneClickRenewalThread()
|
||
self._renewal_thread.log_signal.connect(self.log)
|
||
self._renewal_thread.finished_signal.connect(self.on_one_click_renewal_finished)
|
||
self._renewal_thread.start()
|
||
|
||
@Slot(bool, str, str)
|
||
def on_one_click_renewal_finished(self, success, message, new_email):
|
||
if self.btnOneClickRenewal:
|
||
self.btnOneClickRenewal.setEnabled(True)
|
||
self.btnOneClickRenewal.setText("一键续杯")
|
||
|
||
if success:
|
||
self.backup_path = message
|
||
self.log("✅ 一键续杯完成")
|
||
|
||
# 上报新换号绑定的设备信息(使用生成的新 email)
|
||
self.start_equipment_report(bind_account=new_email)
|
||
|
||
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 _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 _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):
|
||
raw_token = self.txtToken.toPlainText().strip() if self.txtToken else ""
|
||
token = self._extract_session_token(raw_token)
|
||
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):
|
||
"""捐赠支持:跳转到帮助 -> 捐赠支持。"""
|
||
if self.tabList and hasattr(self, 'helpTabList') and self.helpTabList:
|
||
self.tabList.setCurrentRow(3)
|
||
self.helpTabList.setCurrentRow(1)
|
||
|
||
def on_about_clicked(self):
|
||
"""关于软件:跳转到帮助 -> 关于软件。"""
|
||
if self.tabList and hasattr(self, 'helpTabList') and self.helpTabList:
|
||
self.tabList.setCurrentRow(3)
|
||
self.helpTabList.setCurrentRow(2)
|
||
|
||
def show_full_image(self, image_path):
|
||
"""显示全屏图片"""
|
||
def launch():
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("图片预览")
|
||
dialog.setMinimumSize(600, 700)
|
||
|
||
layout = QVBoxLayout(dialog)
|
||
|
||
scroll = QScrollArea(dialog)
|
||
scroll.setWidgetResizable(True)
|
||
|
||
label = QLabel(scroll)
|
||
pixmap = QPixmap(image_path)
|
||
label.setPixmap(pixmap)
|
||
label.setAlignment(Qt.AlignCenter)
|
||
|
||
scroll.setWidget(label)
|
||
layout.addWidget(scroll)
|
||
|
||
close_btn = QPushButton("关闭", dialog)
|
||
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()
|
||
QTimer.singleShot(0, launch)
|
||
|
||
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):
|
||
"""应急检修:切换到 Cursor 归集二级菜单下的“应急检修”子页面。"""
|
||
if self.tabList and self.cursorTabList:
|
||
self.tabList.setCurrentRow(0)
|
||
self.cursorTabList.setCurrentRow(3)
|
||
|
||
def on_silent_detect_menu_clicked(self):
|
||
"""无感检测密码验证,通过后打开无感检测窗口。"""
|
||
pwd, ok = QInputDialog.getText(
|
||
self,
|
||
"无感检测",
|
||
"请输入密码:",
|
||
QLineEdit.Password,
|
||
)
|
||
if not ok:
|
||
return
|
||
if pwd != "920103":
|
||
self.log("❌ 无感检测密码错误")
|
||
QMessageBox.warning(self, "提示", "密码错误,无法使用无感检测。")
|
||
return
|
||
|
||
self.on_silent_detect_popup_clicked()
|
||
|
||
def on_silent_detect_popup_clicked(self):
|
||
"""点击无感检测按钮,弹窗显示无感检测功能"""
|
||
if not self.groupHelpSilentDetect:
|
||
QMessageBox.warning(self, "提示", "未找到无感检测组件")
|
||
return
|
||
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("无感检测")
|
||
dialog.setMinimumSize(450, 250)
|
||
dialog.setModal(False)
|
||
dialog.setWindowModality(Qt.NonModal)
|
||
|
||
# 保存原父级和布局,以便关闭时归还
|
||
original_parent = self.groupHelpSilentDetect.parentWidget()
|
||
original_layout = None
|
||
if original_parent:
|
||
original_layout = original_parent.layout()
|
||
|
||
dialog_layout = QVBoxLayout(dialog)
|
||
dialog_layout.addWidget(self.groupHelpSilentDetect)
|
||
|
||
# 添加关闭按钮
|
||
btn_close = QPushButton("关闭", dialog)
|
||
btn_close.setMinimumHeight(35)
|
||
btn_close.clicked.connect(dialog.close)
|
||
dialog_layout.addWidget(btn_close)
|
||
|
||
# 在关闭时将组件还原回原布局的顶部(在 spacer 之前)
|
||
def restore_widget():
|
||
if original_layout:
|
||
original_layout.insertWidget(0, self.groupHelpSilentDetect)
|
||
elif original_parent:
|
||
self.groupHelpSilentDetect.setParent(original_parent)
|
||
|
||
dialog.finished.connect(restore_widget)
|
||
dialog.show()
|
||
|
||
def on_reset_windsurf_clicked(self):
|
||
"""重置 Windsurf 机器码。"""
|
||
# 检测 Windsurf 是否正在运行
|
||
if is_windsurf_running():
|
||
msg_box = QMessageBox(self)
|
||
msg_box.setWindowTitle("Windsurf正在运行")
|
||
msg_box.setText("Windsurf正在运行中!\n请先保存代码并手动关闭Windsurf。")
|
||
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("💀 正在强制关闭Windsurf...")
|
||
if kill_windsurf():
|
||
self.log("✅ Windsurf已关闭")
|
||
else:
|
||
self.log("⚠️ 未找到运行中的Windsurf进程")
|
||
QMessageBox.warning(self, "警告", "未找到运行中的Windsurf进程")
|
||
return
|
||
else:
|
||
return
|
||
|
||
# 弹窗询问确认
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"确认重置",
|
||
"确定要重置 Windsurf 的机器码吗?\n该操作会清除当前账户的登录状态,重置后您需要重新登录账户。",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
self.log("🔄 开始重置 Windsurf 机器码...")
|
||
|
||
# 禁用重置按钮
|
||
if self.btnResetWindsurf:
|
||
self.btnResetWindsurf.setEnabled(False)
|
||
self.btnResetWindsurf.setText("🔄 重置中...")
|
||
|
||
success = reset_windsurf_machine_id(self.log)
|
||
|
||
if success:
|
||
QMessageBox.information(
|
||
self,
|
||
"重置成功",
|
||
"Windsurf 机器码已成功重置!\n您现在可以重新打开 Windsurf 并登录新的免费账户了。"
|
||
)
|
||
else:
|
||
QMessageBox.critical(
|
||
self,
|
||
"重置失败",
|
||
"重置 Windsurf 机器码失败,请查看日志了解详细原因。\n可能原因:Windsurf 正在运行,请先手动关闭它。"
|
||
)
|
||
|
||
if self.btnResetWindsurf:
|
||
self.btnResetWindsurf.setEnabled(True)
|
||
self.btnResetWindsurf.setText("重置机器码")
|
||
|
||
def on_clear_windsurf_clicked(self):
|
||
"""清除 Windsurf 缓存(直接删除文件夹)"""
|
||
# 检测 Windsurf 是否正在运行
|
||
if is_windsurf_running():
|
||
msg_box = QMessageBox(self)
|
||
msg_box.setWindowTitle("Windsurf正在运行")
|
||
msg_box.setText("检测到 Windsurf 正在运行!\n清除缓存前需要先关闭 Windsurf。")
|
||
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("💀 正在强制关闭Windsurf...")
|
||
if kill_windsurf():
|
||
self.log("✅ Windsurf已关闭")
|
||
else:
|
||
self.log("⚠️ 未找到运行中的Windsurf进程")
|
||
QMessageBox.warning(self, "警告", "未找到运行中的Windsurf进程")
|
||
return
|
||
else:
|
||
return
|
||
|
||
# 再次弹窗确认
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"确认清除缓存",
|
||
"确定要直接删除 %APPDATA%\\Windsurf 数据文件夹吗?\n该操作会彻底清除所有配置、扩展与缓存数据,且不可恢复!",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
self.log("🔄 开始清除 Windsurf 缓存文件夹...")
|
||
if self.btnClearWindsurf:
|
||
self.btnClearWindsurf.setEnabled(False)
|
||
self.btnClearWindsurf.setText("🔄 清理中...")
|
||
|
||
success = clear_windsurf_cache(self.log)
|
||
|
||
if success:
|
||
QMessageBox.information(
|
||
self,
|
||
"清理成功",
|
||
"Windsurf 数据文件夹已成功删除清除!"
|
||
)
|
||
else:
|
||
QMessageBox.critical(
|
||
self,
|
||
"清理失败",
|
||
"清除 Windsurf 数据文件夹失败,请查看日志了解详细原因。"
|
||
)
|
||
|
||
if self.btnClearWindsurf:
|
||
self.btnClearWindsurf.setEnabled(True)
|
||
self.btnClearWindsurf.setText("清除缓存")
|
||
|
||
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):
|
||
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("✅ 换号完成")
|
||
|
||
# 上报新换号绑定的设备信息(使用生成的新 email)
|
||
self.start_equipment_report(bind_account=self.thread.new_email)
|
||
|
||
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)
|
||
|
||
@Slot(bool, str, int)
|
||
def on_silent_detect_finished(self, success, message, next_id):
|
||
if self.btnSilentChange:
|
||
self.btnSilentChange.setEnabled(True)
|
||
self.btnSilentChange.setText("无感换号")
|
||
|
||
if success:
|
||
# 记录当前成功检测到的 ID 和 Token,并更新上方显示
|
||
self.current_detect_id = getattr(self.detect_thread, "target_id", "")
|
||
self.current_detect_token = getattr(self.detect_thread, "new_token", "")
|
||
if self.lblCurrentDetectId:
|
||
self.lblCurrentDetectId.setText(f"当前检测 ID:{self.current_detect_id}")
|
||
if self.lblCurrentDetectToken:
|
||
self.lblCurrentDetectToken.setText(self.current_detect_token)
|
||
|
||
self.log(f"🚀 换号成功 (ID={self.current_detect_id})")
|
||
self.log(f"🔑 提取的 Token: {self.current_detect_token}")
|
||
self.log("🚀 正在为您启动 Cursor...")
|
||
self.on_open_cursor_clicked()
|
||
else:
|
||
QMessageBox.critical(self, "错误", message)
|
||
|
||
def _adjust_silent_id(self, delta: int):
|
||
"""调整无感检测输入框中的 ID。"""
|
||
if not self.txtSilentToken:
|
||
return
|
||
|
||
raw_id = self.txtSilentToken.text().strip()
|
||
if raw_id and not raw_id.isdigit():
|
||
QMessageBox.warning(self, "警告", "ID 必须为纯数字,无法自动增减!")
|
||
return
|
||
|
||
current_id = int(raw_id) if raw_id else 0
|
||
new_id = max(0, current_id + delta)
|
||
self.txtSilentToken.setText(str(new_id))
|
||
|
||
def on_silent_detect_clicked(self):
|
||
"""执行无感检测换号。"""
|
||
id_str = self.txtSilentToken.text().strip() if self.txtSilentToken else ""
|
||
if not id_str:
|
||
QMessageBox.warning(self, "警告", "请先输入要检测的 ID!")
|
||
return
|
||
if not id_str.isdigit():
|
||
QMessageBox.warning(self, "警告", "ID 必须为纯数字!")
|
||
return
|
||
|
||
if is_cursor_running():
|
||
self.log("💀 检测到 Cursor 正在运行,正在自动关闭后继续无感换号...")
|
||
if kill_cursor():
|
||
self.log("✅ Cursor已自动关闭")
|
||
else:
|
||
self.log("⚠️ 自动关闭 Cursor 失败,请手动关闭后重试")
|
||
QMessageBox.warning(self, "警告", "自动关闭 Cursor 失败,请手动关闭后重试。")
|
||
return
|
||
|
||
if self.btnSilentChange:
|
||
self.btnSilentChange.setEnabled(False)
|
||
self.btnSilentChange.setText("🔍 检测换号中...")
|
||
|
||
self.detect_thread = SilentDetectThread(id_str)
|
||
self.detect_thread.log_signal.connect(self.log)
|
||
self.detect_thread.finished_signal.connect(self.on_silent_detect_finished)
|
||
self.detect_thread.start()
|
||
|
||
def _format_token_mark_result(self, data: dict) -> str:
|
||
"""格式化服务端标记可用/不可用接口返回数据。"""
|
||
token_id = data.get("id", self.current_detect_id)
|
||
is_used = data.get("is_used", "-")
|
||
is_available = data.get("is_available", "-")
|
||
state_changed = data.get("state_changed", "-")
|
||
update_time = data.get("update_time", "-")
|
||
return (
|
||
f"ID: {token_id}\n"
|
||
f"is_used: {is_used}\n"
|
||
f"is_available: {is_available}\n"
|
||
f"state_changed: {state_changed}\n"
|
||
f"update_time: {update_time}"
|
||
)
|
||
|
||
def _start_mark_current_token_usability(self, available: bool):
|
||
"""调用服务端接口标记当前检测 ID 的 Token 可用/不可用。"""
|
||
if not getattr(self, "current_detect_id", None):
|
||
QMessageBox.warning(self, "警告", "请先成功换号/检测一个 ID!")
|
||
return
|
||
|
||
action_text = "可用" if available else "不可用"
|
||
reply = QMessageBox.question(
|
||
self,
|
||
f"反馈 Token {action_text}",
|
||
f"确定要反馈当前 ID: {self.current_detect_id} 对应的 Token 为{action_text}吗?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
self.log(f"🔄 正在调用服务端接口标记 ID={self.current_detect_id} 为{action_text}...")
|
||
|
||
if self.btnSilentAvailable:
|
||
self.btnSilentAvailable.setEnabled(False)
|
||
if self.btnSilentUnavailable:
|
||
self.btnSilentUnavailable.setEnabled(False)
|
||
|
||
self._mark_token_thread = MarkTokenUsabilityThread(self.current_detect_id, available)
|
||
self._mark_token_thread.finished_signal.connect(
|
||
lambda success, data, error_msg, available=available: self.on_mark_token_usability_finished(
|
||
success, data, error_msg, available
|
||
)
|
||
)
|
||
self._mark_token_thread.start()
|
||
|
||
@Slot(bool, dict, str, bool)
|
||
def on_mark_token_usability_finished(self, success, data, error_msg, available):
|
||
"""服务端标记 Token 可用/不可用完成。"""
|
||
if self.btnSilentAvailable:
|
||
self.btnSilentAvailable.setEnabled(True)
|
||
if self.btnSilentUnavailable:
|
||
self.btnSilentUnavailable.setEnabled(True)
|
||
|
||
action_text = "可用" if available else "不可用"
|
||
if success:
|
||
result_text = self._format_token_mark_result(data)
|
||
self.log(f"✅ 已标记 ID={data.get('id', self.current_detect_id)} 为{action_text}")
|
||
self.log(
|
||
"📡 服务端返回:"
|
||
f"id={data.get('id', self.current_detect_id)}, "
|
||
f"is_used={data.get('is_used', '-')}, "
|
||
f"is_available={data.get('is_available', '-')}, "
|
||
f"state_changed={data.get('state_changed', '-')}, "
|
||
f"update_time={data.get('update_time', '-')}"
|
||
)
|
||
QMessageBox.information(self, "提交成功", f"Token 已标记为{action_text}。\n\n{result_text}")
|
||
else:
|
||
self.log(f"❌ 标记 ID={self.current_detect_id} 为{action_text}失败: {error_msg}")
|
||
QMessageBox.critical(self, "提交失败", f"Token 标记为{action_text}失败:\n{error_msg}")
|
||
|
||
@Slot()
|
||
def on_silent_available_clicked(self):
|
||
"""用户反馈当前 Token 可用。"""
|
||
self._start_mark_current_token_usability(True)
|
||
|
||
@Slot()
|
||
def on_silent_unavailable_clicked(self):
|
||
"""用户反馈当前 Token 不可用。"""
|
||
self._start_mark_current_token_usability(False)
|
||
|
||
@Slot()
|
||
def on_silent_copy_token_clicked(self):
|
||
"""复制当前检测到的 Token 到剪贴板。"""
|
||
if not getattr(self, "current_detect_token", None):
|
||
QMessageBox.warning(self, "警告", "当前没有可复制的 Token,请先成功换号/检测一个 ID!")
|
||
return
|
||
|
||
clipboard = QApplication.clipboard()
|
||
clipboard.setText(self.current_detect_token)
|
||
self.log("📋 已复制当前检测到的 Token 到剪贴板。")
|
||
QMessageBox.information(self, "成功", "Token 已成功复制到剪贴板!")
|
||
|
||
|
||
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()
|