diff --git a/README.md b/README.md
index 76ea8c9..6c899dd 100644
--- a/README.md
+++ b/README.md
@@ -4,3 +4,10 @@ python -m PyInstaller -w -F main.py
# 编译成exe
python -m PyInstaller build.spec --clean
+
+
+
+# 关于 Win + Mac 同时构建(后续)
+- PyInstaller 不能在 Windows 本机直接产出 macOS 程序。
+- 当前命令在 Windows 只能生成 `.exe`,在 macOS 才能生成 `.app`。
+- 后续可用 GitHub Actions 做双平台构建:本地打 Windows,云端 `macos-latest` 打 macOS。
diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..47af507
Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ
diff --git a/assets/images/donate/info.png b/assets/images/donate/info.png
new file mode 100644
index 0000000..aa9adaf
Binary files /dev/null and b/assets/images/donate/info.png differ
diff --git a/layout/main.ui b/layout/main.ui
index d5960bc..134969d 100644
--- a/layout/main.ui
+++ b/layout/main.ui
@@ -55,6 +55,38 @@
+ -
+
+
+
+ 88
+ 0
+
+
+
+ 自动查找
+
+
+
+ -
+
+
+
+ 110
+ 0
+
+
+
+
+ 110
+ 16777215
+
+
+
+ 打开Cursor
+
+
+
@@ -110,6 +142,12 @@
50
+
+
+ 16777215
+ 50
+
+
12
@@ -128,21 +166,40 @@
-
-
+
- 90
+ 110
50
- 90
+ 110
50
- 📊 查询额度(暂时无用)
+ 在线商城
+
+
+
+ -
+
+
+
+ 110
+ 50
+
+
+
+
+ 110
+ 50
+
+
+
+ AI中转站
@@ -166,7 +223,17 @@
-
- 🔧 应急检修,用户勿点
+ 🔧 应急检修
+
+
+ 30
+
+
+
+ -
+
+
+ 📖 使用说明
30
diff --git a/main.py b/main.py
index 7f14c5b..151e385 100644
--- a/main.py
+++ b/main.py
@@ -1,26 +1,33 @@
import sys
import json
+import base64
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.3"
+__VERSION__ = "0.0.6"
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QMessageBox,
QTextEdit,
+ QInputDialog,
QPushButton,
QVBoxLayout,
QHBoxLayout,
@@ -31,6 +38,7 @@ from PySide6.QtWidgets import (
QDialog,
QScrollArea,
QSplashScreen,
+ QStyle,
)
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QMetaObject, Q_ARG, Slot
from PySide6.QtUiTools import QUiLoader
@@ -595,6 +603,9 @@ class ChangeTokenThread(QThread):
data["cursorAuth"]["accessToken"] = self.new_token
data["cursorAuth"]["refreshToken"] = self.new_token
data["cursorAuth"]["cachedEmail"] = self.new_email
+ data["cursorAuth"]["plan"] = "pro"
+ data["cursorAuth"]["stripeMembershipType"] = "pro"
+ data["cursorAuth"]["membershipType"] = "pro"
# 修改 cursorAccount
self.log_signal.emit("🔑 替换 cursorAccount...")
@@ -602,6 +613,7 @@ class ChangeTokenThread(QThread):
data["cursorAccount"] = {}
data["cursorAccount"]["token"] = self.new_token
data["cursorAccount"]["email"] = self.new_email
+ data["cursorAccount"]["plan"] = "pro"
# 刷新机器ID
self.log_signal.emit("🔧 刷新机器ID...")
@@ -609,6 +621,9 @@ class ChangeTokenThread(QThread):
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}")
@@ -776,18 +791,29 @@ class MainWindow(QMainWindow):
self.cursor_path = get_default_cursor_path()
self.backup_path = ""
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.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.btnQueryUsage = self.findChild(QPushButton, "btnQueryUsage")
+ self.btnUsageGuide = self.findChild(QPushButton, "btnUsageGuide")
+ self._load_cached_logs_to_ui()
# 调试信息
print(f"txtToken: {self.txtToken}")
@@ -798,16 +824,34 @@ class MainWindow(QMainWindow):
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())
-
+
+ # 为在线商城按钮设置图标(使用 Qt 内置图标,避免依赖外部资源)
+ if self.btnOnlineShop:
+ 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)
+ self.btnOnlineShop.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:
@@ -816,10 +860,16 @@ class MainWindow(QMainWindow):
self.btnCheckUpdate.clicked.connect(self.on_check_update_clicked)
if self.btnEmergencyRepair:
self.btnEmergencyRepair.clicked.connect(self.on_emergency_repair_clicked)
- if self.btnQueryUsage:
- self.btnQueryUsage.clicked.connect(self.on_query_usage_clicked)
-
- # 设置版本号显示在状态栏右侧
+ if self.btnUsageGuide:
+ self.btnUsageGuide.clicked.connect(self.on_usage_guide_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("🚀 程序启动成功")
@@ -976,13 +1026,99 @@ class MainWindow(QMainWindow):
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(message)
- self.statusBar().showMessage(message)
+ 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路径"""
@@ -1001,6 +1137,156 @@ class MainWindow(QMainWindow):
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 _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):
token = self.txtToken.toPlainText().strip() if self.txtToken else ""
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
@@ -1042,16 +1328,6 @@ class MainWindow(QMainWindow):
new_email = generate_random_email()
self.log(f"📧 生成新邮箱: {new_email}")
- # 确认对话框
- reply = QMessageBox.question(
- self,
- "确认",
- f"确定要换号吗?\n新邮箱: {new_email}\n原账号数据将备份。",
- QMessageBox.Yes | QMessageBox.No,
- )
- if reply != QMessageBox.Yes:
- return
-
# 禁用按钮
if self.btnChange:
self.btnChange.setEnabled(False)
@@ -1068,8 +1344,253 @@ class MainWindow(QMainWindow):
dialog = DonateDialog(self)
dialog.exec()
+ def on_usage_guide_clicked(self):
+ """打开使用说明图片(非模态,可与其他窗口并行)。"""
+ if self._usage_guide_dialog and self._usage_guide_dialog.isVisible():
+ self._usage_guide_dialog.raise_()
+ self._usage_guide_dialog.activateWindow()
+ return
+
+ image_path = get_resource_path(os.path.join("assets", "images", "donate", "info.png"))
+ if not Path(image_path).is_file():
+ QMessageBox.warning(self, "提示", f"未找到使用说明图片:{image_path}")
+ return
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("使用说明")
+ dialog.setMinimumSize(900, 700)
+ dialog.setModal(False)
+ dialog.setWindowModality(Qt.NonModal)
+
+ layout = QVBoxLayout(dialog)
+
+ tip = QLabel("可使用鼠标滚轮查看长图内容,可通过按钮缩放图片。")
+ tip.setAlignment(Qt.AlignCenter)
+ layout.addWidget(tip)
+
+ zoom_row = QHBoxLayout()
+ zoom_row.addStretch()
+ btn_zoom_out = QPushButton("缩小 -", dialog)
+ btn_zoom_reset = QPushButton("100%", dialog)
+ btn_zoom_in = QPushButton("放大 +", dialog)
+ zoom_label = QLabel("100%", dialog)
+ zoom_row.addWidget(btn_zoom_out)
+ zoom_row.addWidget(btn_zoom_reset)
+ zoom_row.addWidget(btn_zoom_in)
+ zoom_row.addWidget(zoom_label)
+ layout.addLayout(zoom_row)
+
+ scroll = QScrollArea(dialog)
+ scroll.setWidgetResizable(True)
+
+ label = QLabel(scroll)
+ original_pixmap = QPixmap(image_path)
+ zoom_state = {"scale": 1.0}
+
+ def apply_zoom():
+ if original_pixmap.isNull():
+ return
+ target_w = max(1, int(original_pixmap.width() * zoom_state["scale"]))
+ target_h = max(1, int(original_pixmap.height() * zoom_state["scale"]))
+ scaled = original_pixmap.scaled(
+ target_w,
+ target_h,
+ Qt.KeepAspectRatio,
+ Qt.SmoothTransformation,
+ )
+ label.setPixmap(scaled)
+ label.resize(scaled.size())
+ zoom_label.setText(f"{int(zoom_state['scale'] * 100)}%")
+
+ def zoom_in():
+ zoom_state["scale"] = min(3.0, zoom_state["scale"] * 1.2)
+ apply_zoom()
+
+ def zoom_out():
+ zoom_state["scale"] = max(0.2, zoom_state["scale"] / 1.2)
+ apply_zoom()
+
+ def zoom_reset():
+ zoom_state["scale"] = 1.0
+ apply_zoom()
+
+ btn_zoom_in.clicked.connect(zoom_in)
+ btn_zoom_out.clicked.connect(zoom_out)
+ btn_zoom_reset.clicked.connect(zoom_reset)
+
+ apply_zoom()
+ label.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
+
+ scroll.setWidget(label)
+ layout.addWidget(scroll, 1)
+
+ btn_close = QPushButton("关闭", dialog)
+ btn_close.clicked.connect(dialog.accept)
+ btn_row = QHBoxLayout()
+ btn_row.addStretch()
+ btn_row.addWidget(btn_close)
+ layout.addLayout(btn_row)
+
+ self._usage_guide_dialog = dialog
+ dialog.show()
+
def on_emergency_repair_clicked(self):
- """应急检修:下载工具到桌面"""
+ """应急检修:弹出工具面板。"""
+ if self._emergency_dialog and self._emergency_dialog.isVisible():
+ self._emergency_dialog.raise_()
+ self._emergency_dialog.activateWindow()
+ return
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("应急检修")
+ dialog.setMinimumSize(400, 300)
+ dialog.setModal(False)
+ dialog.setWindowModality(Qt.NonModal)
+
+ layout = QVBoxLayout(dialog)
+ layout.addWidget(QLabel("请选择要执行的操作:"))
+
+ actions_widget = QWidget(dialog)
+ actions_layout = QVBoxLayout(actions_widget)
+ actions_layout.setContentsMargins(0, 0, 0, 0)
+ actions_layout.setSpacing(10)
+ actions_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+
+ btn_download = QPushButton("下载DB Browser")
+ btn_clear_cache = QPushButton("清除Cursor缓存")
+ btn_extract_token = QPushButton("Token提取")
+ for btn in (
+ btn_download,
+ btn_clear_cache,
+ btn_extract_token,
+ ):
+ btn.setMinimumWidth(180)
+ btn.setMaximumWidth(260)
+ btn.setSizePolicy(btn.sizePolicy().horizontalPolicy(), btn.sizePolicy().verticalPolicy())
+ actions_layout.addWidget(btn, 0, Qt.AlignLeft)
+
+ layout.addWidget(actions_widget)
+ layout.addStretch()
+
+ btn_download.clicked.connect(self.download_db_tool)
+ btn_clear_cache.clicked.connect(self.clear_cursor_cache)
+ btn_extract_token.clicked.connect(self.on_token_extract_clicked)
+ self._emergency_dialog = dialog
+ dialog.show()
+
+ 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
@@ -1087,88 +1608,109 @@ class MainWindow(QMainWindow):
threading.Thread(target=download, daemon=True).start()
- def on_query_usage_clicked(self):
- """查询当前token的额度使用情况"""
- import threading
+ 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()
- token = self.txtToken.toPlainText().strip() if self.txtToken else ""
- if not token:
- QMessageBox.warning(self, "提示", "请先在输入框中填入Token")
+ 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
- def query():
- try:
- clean_token = token.strip().replace('"', '').replace("SessionToken=", "")
- resp = requests.get(
- "https://cursor.com/api/usage",
- headers={
- "Cookie": f"SessionToken={clean_token}",
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
- "Accept": "application/json",
- },
- timeout=10,
- )
- if resp.status_code == 401:
- msg = "查询失败:Token 已失效 (401 Unauthorized)。请检查账号是否已退出或被封。"
- else:
- resp.raise_for_status()
- data = resp.json()
+ 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
- lines = []
- premium = data.get("premiumUsage", {})
- if premium:
- used = premium.get("numRequestsTotal", 0)
- limit = premium.get("maxRequestUsage", "无限制")
- lines.append(f"高级模型 (GPT-4/Claude): 已用 {used} / {limit}")
+ if sys.platform != "win32":
+ QMessageBox.warning(
+ self,
+ "失败",
+ "权限不足,无法删除缓存目录。请手动使用管理员权限删除。",
+ )
+ return
- for model_key, model_data in data.items():
- if isinstance(model_data, dict) and "numRequestsTotal" in model_data:
- used = model_data.get("numRequestsTotal", 0)
- limit = model_data.get("maxRequestUsage")
- limit_str = str(limit) if limit is not None else "无限制"
- lines.append(f"{model_key}: 已用 {used} / {limit_str}")
+ # 提权删除:弹 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
- msg = "\n".join(lines) if lines else "暂无额度数据,请检查账号状态。"
-
- QMetaObject.invokeMethod(
- self, "_show_usage_result",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, msg)
- )
- except Exception as e:
- QMetaObject.invokeMethod(
- self, "_show_usage_result",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"网络请求错误:{str(e)}")
- )
-
- threading.Thread(target=query, daemon=True).start()
+ 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
- QMessageBox.information(self, "成功", "换号完成!\n即将打开Cursor...")
+ self.log("✅ 换号完成")
+ self._show_change_success_countdown_dialog(4)
self.log("🚀 正在打开Cursor...")
- try:
- cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
- if sys.platform == "win32":
- os.startfile(cursor_path)
- elif sys.platform == "darwin":
- subprocess.Popen(["open", cursor_path])
- else:
- subprocess.Popen([cursor_path])
- self.log("✅ Cursor已启动")
- except Exception as e:
- self.log(f"❌ 打开Cursor失败: {str(e)}")
- QMessageBox.warning(self, "警告", f"打开Cursor失败: {str(e)}")
+ cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
+ self._launch_cursor(cursor_path)
else:
QMessageBox.critical(self, "失败", message)
diff --git a/main_backup.py b/main_backup.py
new file mode 100644
index 0000000..e9e65c4
--- /dev/null
+++ b/main_backup.py
@@ -0,0 +1,2012 @@
+import sys
+import json
+import base64
+import shutil
+import uuid
+import random
+import string
+import re
+import os
+import tempfile
+import ctypes
+import socket
+import psutil
+import subprocess
+import sqlite3
+import requests
+from contextlib import contextmanager
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
+
+__VERSION__ = "0.0.5"
+
+from PySide6.QtWidgets import (
+ QApplication,
+ QMainWindow,
+ QMessageBox,
+ QTextEdit,
+ QInputDialog,
+ QPushButton,
+ QVBoxLayout,
+ QHBoxLayout,
+ QWidget,
+ QLineEdit,
+ QLabel,
+ QGroupBox,
+ QDialog,
+ QScrollArea,
+ QSplashScreen,
+)
+from PySide6.QtCore import QThread, Signal, Qt, QTimer, QMetaObject, Q_ARG, Slot
+from PySide6.QtUiTools import QUiLoader
+from PySide6.QtGui import QFont, QPixmap, QColor, QPainter, QPalette, QIcon
+
+
+def get_resource_path(relative_path):
+ """获取资源文件的绝对路径,支持打包后的应用(onefile 内嵌或 exe 旁便携文件)。"""
+ rel = Path(relative_path)
+ if getattr(sys, "frozen", False):
+ candidates = []
+ if hasattr(sys, "_MEIPASS"):
+ candidates.append(Path(sys._MEIPASS) / rel)
+ # onedir / 便携:layout 与 exe 同目录
+ candidates.append(Path(sys.executable).resolve().parent / rel)
+ for p in candidates:
+ try:
+ if p.is_file():
+ return str(p.resolve())
+ except OSError:
+ continue
+ # 用于报错信息:优先 MEIPASS
+ primary = (
+ Path(sys._MEIPASS) / rel
+ if hasattr(sys, "_MEIPASS")
+ else Path(sys.executable).resolve().parent / rel
+ )
+ return str(primary.resolve())
+ return str((Path(__file__).parent / rel).resolve())
+
+
+def get_app_icon() -> QIcon:
+ """窗口标题栏 / 任务栏图标(使用根目录 logo.ico;打包后需将 logo.ico 一并打入资源)。"""
+ p = Path(get_resource_path("logo.ico"))
+ if p.is_file():
+ return QIcon(str(p))
+ return QIcon()
+
+
+def is_windows_pe_executable(path: Path) -> bool:
+ """粗略校验是否为 Windows PE(避免把 HTML/JSON 当成 exe 替换)。"""
+ try:
+ if path.stat().st_size < 64 * 1024:
+ return False
+ with open(path, "rb") as f:
+ return f.read(2) == b"MZ"
+ except OSError:
+ return False
+
+
+def get_default_cursor_path():
+ if sys.platform == "win32":
+ paths = [
+ Path(os.environ.get("LOCALAPPDATA", ""))
+ / "Programs"
+ / "cursor"
+ / "Cursor.exe",
+ Path(os.environ.get("PROGRAMFILES", "")) / "Cursor" / "Cursor.exe",
+ Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Cursor" / "Cursor.exe",
+ ]
+ for path in paths:
+ if path.exists():
+ return str(path)
+ elif sys.platform == "darwin":
+ return "/Applications/Cursor.app"
+ else:
+ return "/usr/bin/cursor"
+ return ""
+
+
+def is_cursor_running():
+ """检测Cursor是否正在运行"""
+ cursor_exe_names = ['cursor.exe', 'cursor']
+ for proc in psutil.process_iter(["name"]):
+ try:
+ name = proc.info["name"]
+ if name and name.lower() in cursor_exe_names:
+ return True
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+ return False
+
+
+def kill_cursor():
+ """强制关闭Cursor进程"""
+ killed = False
+ cursor_exe_names = ['cursor.exe', 'cursor']
+ for proc in psutil.process_iter(["name", "pid"]):
+ try:
+ name = proc.info["name"]
+ if name and name.lower() in cursor_exe_names:
+ proc.kill()
+ killed = True
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+ return killed
+
+
+def generate_random_email():
+ length = random.randint(6, 8)
+ username = "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
+ return f"{username}@cursor.com"
+
+
+def generate_machine_id():
+ return str(uuid.uuid4())
+
+
+def get_cursor_config_path():
+ home = Path.home()
+ if sys.platform == "win32":
+ base = home / "AppData" / "Roaming" / "Cursor" / "User" / "globalStorage"
+ elif sys.platform == "darwin":
+ base = (
+ home
+ / "Library"
+ / "Application Support"
+ / "Cursor"
+ / "User"
+ / "globalStorage"
+ )
+ else:
+ base = home / ".config" / "Cursor" / "User" / "globalStorage"
+ return base
+
+
+def update_vsdb_token(config_dir, new_token, new_email, log_callback):
+ """更新 Cursor 数据库中的token和email"""
+ db_path = config_dir / "state.vscdb"
+ if not db_path.exists():
+ log_callback("⚠️ 未找到 Cursor库 文件,跳过")
+ return False
+
+ try:
+ log_callback("📖 连接 Cursor 数据库...")
+
+ # 备份数据库
+ db_backup = config_dir / "state.vscdb.backup"
+ if db_backup.exists():
+ db_backup.unlink()
+ shutil.copy2(db_path, db_backup)
+ # log_callback(f"📁 数据库已备份到: {db_backup}")
+
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+
+ # 更新 token 和 email
+ updates = [
+ ("cursorAuth/accessToken", new_token),
+ ("cursorAuth/refreshToken", new_token),
+ ("cursorAuth/cachedEmail", new_email),
+ ]
+
+ for key, value in updates:
+ cursor.execute(
+ "SELECT value FROM ItemTable WHERE key = ?",
+ (key,)
+ )
+ result = cursor.fetchone()
+
+ if result:
+ cursor.execute(
+ "UPDATE ItemTable SET value = ? WHERE key = ?",
+ (value, key)
+ )
+ # log_callback(f"✓ 更新了 {key}")
+ else:
+ cursor.execute(
+ "INSERT INTO ItemTable (key, value) VALUES (?, ?)",
+ (key, value)
+ )
+ # log_callback(f"✓ 插入了 {key}")
+
+ conn.commit()
+ conn.close()
+ log_callback("✅ Cursor库 更新成功")
+ return True
+
+ except Exception as e:
+ log_callback(f"❌ Cursor库 更新失败: {str(e)}")
+ return False
+
+
+def test_session_token_detailed(token: str, log_callback):
+ """ session token """
+ log_callback(" session token...")
+ headers = {
+ "Cookie": f"SessionToken={token}",
+ "Accept": "application/json",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
+ "Content-Type": "application/json",
+ }
+
+ # API
+ test_endpoints = [
+ ("https://cursor.com/api/usage", "Usage API"),
+ ("https://api2.cursor.sh/v1/models", "Models API"),
+ ("https://api2.cursor.sh/v1/chat/completions", "Chat API"),
+ ]
+
+ for endpoint, name in test_endpoints:
+ try:
+ log_callback(f" {name}: {endpoint}")
+
+ if "chat/completions" in endpoint:
+ # API
+ test_data = {
+ "model": "default",
+ "messages": [{"role": "user", "content": "hi"}],
+ "max_tokens": 1
+ }
+ with request_with_proxy_fallback(
+ endpoint,
+ headers=headers,
+ json=test_data,
+ timeout=10,
+ allow_redirects=True,
+ ) as resp:
+ code = resp.status_code
+ log_callback(f" {name} HTTP {code}")
+ if code == 200:
+ log_callback(f" {name} ")
+ return True, f" session token {name}"
+ elif code == 401:
+ log_callback(f" {name} 401 - ")
+ continue
+ elif code == 403:
+ log_callback(f" {name} 403 - ")
+ continue
+ else:
+ log_callback(f" {name} HTTP {code}")
+ continue
+ else:
+ # GET
+ with request_with_proxy_fallback(
+ endpoint,
+ headers=headers,
+ timeout=10,
+ allow_redirects=True,
+ ) as resp:
+ code = resp.status_code
+ log_callback(f" {name} HTTP {code}")
+ if code == 200:
+ log_callback(f" {name} ")
+ return True, f" session token {name}"
+ elif code == 401:
+ log_callback(f" {name} 401 - ")
+ continue
+ elif code == 403:
+ log_callback(f" {name} 403 - ")
+ continue
+ else:
+ log_callback(f" {name} HTTP {code}")
+ continue
+
+ except Exception as e:
+ log_callback(f" {name} : {e}")
+ continue
+
+ return False, " session token "
+
+
+def test_session_token_simple(token: str, log_callback):
+ """session token hi"""
+ log_callback(" session token...")
+ headers = {
+ "Cookie": f"SessionToken={token}",
+ "Accept": "application/json",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
+ "Content-Type": "application/json",
+ }
+
+ try:
+ # API
+ test_data = {"message": "hi"}
+ with request_with_proxy_fallback(
+ "https://api2.cursor.sh/v1/chat/completions",
+ headers=headers,
+ json=test_data,
+ timeout=10,
+ allow_redirects=True,
+ ) as resp:
+ code = resp.status_code
+ if code == 200:
+ log_callback(" session token ")
+ return True, " session token "
+ elif code == 401:
+ return False, " session token 401"
+ elif code == 403:
+ return False, " session token 403"
+ else:
+ return False, f" session token HTTP {code}"
+ except Exception as e:
+ return False, f" session token : {e}"
+
+
+def detect_account_status(token: str, log_callback):
+ """账号状态检测:本地解析 JWT 过期时间 + 远端状态探测。"""
+ jwt_exp = None
+ try:
+ parts = token.split(".")
+ if len(parts) >= 2:
+ payload = parts[1]
+ payload += "=" * (-len(payload) % 4)
+ decoded = json.loads(base64.urlsafe_b64decode(payload.encode("utf-8")).decode("utf-8"))
+ if isinstance(decoded, dict):
+ jwt_exp = decoded.get("exp")
+ except Exception:
+ jwt_exp = None
+
+ if jwt_exp:
+ exp_dt = datetime.fromtimestamp(int(jwt_exp))
+ log_callback(f"🕒 Token过期时间(本地解析): {exp_dt.strftime('%Y-%m-%d %H:%M:%S')}")
+ if datetime.now().timestamp() >= float(jwt_exp):
+ return False, "❌ 账号状态:Token 已过期。"
+
+ log_callback("🔎 正在检测账号状态接口: https://cursor.com/api/usage")
+ headers = {
+ "Cookie": f"SessionToken={token}",
+ "Accept": "application/json",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0",
+ }
+ try:
+ with request_with_proxy_fallback(
+ "https://cursor.com/api/usage",
+ headers=headers,
+ timeout=12,
+ allow_redirects=True,
+ ) as resp:
+ code = resp.status_code
+ if code == 200:
+ return True, "✅ 账号状态:有效,可与服务器通信。"
+ if code == 401:
+ return False, "❌ 账号状态:无效或已失效(401)。"
+ if code == 403:
+ return False, "⚠️ 账号状态:受限(403)。"
+ if code == 429:
+ return False, "⚠️ 账号状态:请求过频(429),请稍后重试。"
+ return False, f"⚠️ 账号状态:接口返回 HTTP {code}。"
+ except Exception as e:
+ return False, f"⚠️ 账号状态检测失败:网络或环境异常({e})"
+
+
+@contextmanager
+def request_with_proxy_fallback(url, **kwargs):
+ """优先走系统代理;代理不可用时自动回退直连。以 contextmanager 统一管理连接生命周期。"""
+ try:
+ with requests.get(url, **kwargs) as response:
+ yield response
+ return
+ except requests.exceptions.ProxyError:
+ with requests.Session() as session:
+ session.trust_env = False
+ with session.get(url, **kwargs) as response:
+ yield response
+
+
+def check_for_updates():
+ """检查软件更新,返回 (data, error) 元组"""
+ try:
+ url = "https://api.yunzer.cn/api/softwareupgrade/check?code=cursortokenlogin"
+ with request_with_proxy_fallback(url, timeout=10) as response:
+ response.raise_for_status()
+ data = response.json()
+
+ if data.get("code") == 200:
+ return (data.get("data", {}), None)
+ else:
+ return (None, f"服务器返回错误: {data.get('msg', '未知错误')}")
+ except requests.exceptions.Timeout:
+ return (None, "连接超时(超过10秒),请检查网络连接")
+ except requests.exceptions.ConnectionError as e:
+ return (None, f"网络连接失败: {str(e)}")
+ except requests.exceptions.RequestException as e:
+ return (None, f"请求失败: {str(e)}")
+ except Exception as e:
+ return (None, f"检查更新失败: {str(e)}")
+
+
+def compare_versions(current, latest):
+ """比较版本号"""
+ try:
+ current_parts = list(map(int, current.split('.')))
+ latest_parts = list(map(int, latest.split('.')))
+
+ for i in range(max(len(current_parts), len(latest_parts))):
+ current_part = current_parts[i] if i < len(current_parts) else 0
+ latest_part = latest_parts[i] if i < len(latest_parts) else 0
+
+ if latest_part > current_part:
+ return 1 # 需要更新
+ elif latest_part < current_part:
+ return -1 # 当前版本更新
+
+ return 0 # 版本相同
+ except Exception:
+ return 0
+
+
+def normalize_download_url(raw):
+ if raw is None:
+ return ""
+ s = str(raw).strip()
+ if not s or s.lower() in ("null", "none", "undefined"):
+ return ""
+ return s
+
+
+def write_windows_updater_batch(old_exe: Path, new_exe: Path) -> Path:
+ """生成批处理:通过 %1 %2 传入路径,避免把长路径写入 bat 产生编码/转义问题。"""
+ bat = old_exe.parent / "_cursortokenlogin_update.bat"
+ lines = [
+ "@echo off",
+ "setlocal EnableExtensions",
+ 'set "OLD=%~1"',
+ 'set "NEW=%~2"',
+ 'if not defined OLD goto :eof',
+ 'if not defined NEW goto :eof',
+ 'if not exist "%NEW%" exit /b 1',
+ "ping 127.0.0.1 -n 5 >nul",
+ ":wait_del",
+ 'del /F /Q "%OLD%" 2>nul',
+ 'if exist "%OLD%" (ping 127.0.0.1 -n 2 >nul & goto wait_del)',
+ 'move /Y "%NEW%" "%OLD%"',
+ "if errorlevel 1 exit /b 1",
+ # 不自动启动 exe:解压/runtime 尚未就绪时易触发 Python DLL 加载失败,由用户稍后手动打开
+ 'del /F /Q "%~f0"',
+ ]
+ bat.write_text("\r\n".join(lines), encoding="utf-8")
+ return bat
+
+
+def launch_detach_no_window(args, cwd=None):
+ if sys.platform == "win32":
+ flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
+ subprocess.Popen(
+ args,
+ cwd=cwd,
+ creationflags=flags,
+ close_fds=True,
+ )
+ else:
+ subprocess.Popen(args, cwd=cwd, close_fds=True, start_new_session=True)
+
+
+class DownloadUpdateThread(QThread):
+ log_signal = Signal(str)
+ progress_signal = Signal(int, int)
+ finished_signal = Signal(bool, str)
+
+ def __init__(self, download_url: str):
+ super().__init__()
+ self.download_url = download_url
+
+ def run(self):
+ new_path = None
+ try:
+ self.log_signal.emit("📡 正在请求安装包...")
+ with request_with_proxy_fallback(
+ self.download_url, stream=True, timeout=60, allow_redirects=True
+ ) as r:
+ r.raise_for_status()
+ total = int(r.headers.get("Content-Length") or 0)
+ chunk_size = 256 * 1024
+ downloaded = 0
+ last_pct = -1
+
+ if getattr(sys, "frozen", False):
+ exe_path = Path(sys.executable).resolve()
+ suffix = exe_path.suffix or ".exe"
+ new_path = exe_path.parent / f"{exe_path.stem}_update_pending{suffix}"
+ else:
+ fd, tmp = tempfile.mkstemp(suffix=".exe")
+ os.close(fd)
+ new_path = Path(tmp)
+
+ # self.log_signal.emit(f"💾 下载保存到: {new_path}")
+
+ if total > 0:
+ self.progress_signal.emit(0, total)
+
+ next_unknown_log = 512 * 1024
+ with open(new_path, "wb") as f:
+ for chunk in r.iter_content(chunk_size):
+ if not chunk:
+ continue
+ f.write(chunk)
+ downloaded += len(chunk)
+ if total > 0:
+ pct = min(100, downloaded * 100 // total)
+ if pct >= last_pct + 5 or pct == 100:
+ last_pct = pct if pct == 100 else (pct // 5) * 5
+ self.progress_signal.emit(downloaded, total)
+ elif downloaded >= next_unknown_log:
+ self.progress_signal.emit(downloaded, 0)
+ next_unknown_log += 512 * 1024
+
+ self.log_signal.emit("✅ 下载完成")
+
+ if not is_windows_pe_executable(new_path):
+ try:
+ new_path.unlink()
+ except OSError:
+ pass
+ raise ValueError(
+ "下载的文件不是有效的 Windows 安装包(缺少 PE 头或过小),"
+ "可能为网页/错误信息,已取消替换。请确认服务端提供的是本软件完整打包的 .exe。"
+ )
+
+ if not getattr(sys, "frozen", False):
+ self.log_signal.emit(
+ "ℹ️ 当前为脚本/开发模式,不会自动替换已安装程序;请手动用下载文件替换。"
+ )
+ self.finished_signal.emit(True, "dev_mode")
+ return
+
+ if sys.platform != "win32":
+ self.log_signal.emit(
+ "ℹ️ 当前为非 Windows 打包环境,请手动将新文件替换为应用程序。"
+ )
+ self.finished_signal.emit(True, "no_auto_replace")
+ return
+
+ exe_path = Path(sys.executable).resolve()
+ self.log_signal.emit("🔧 正在准备替换:退出后将删除旧程序并启动新版本...")
+ bat = write_windows_updater_batch(exe_path, new_path)
+ launch_detach_no_window(
+ ["cmd", "/c", str(bat), str(exe_path), str(new_path.resolve())],
+ cwd=str(exe_path.parent),
+ )
+ self.finished_signal.emit(True, "restarting")
+ except Exception as e:
+ if new_path and new_path.exists():
+ try:
+ new_path.unlink()
+ except OSError:
+ pass
+ self.log_signal.emit(f"❌ 下载或更新失败: {e}")
+ self.finished_signal.emit(False, str(e))
+
+
+class ImageClickLabel(QLabel):
+ """可点击的图片标签,点击放大显示"""
+ clicked = Signal(str)
+
+ def __init__(self, image_path, parent=None):
+ super().__init__(parent)
+ self.image_path = image_path
+ self.original_pixmap = QPixmap(image_path)
+ self.setScaledContents(True)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setStyleSheet("QLabel { border: 1px solid #ccc; border-radius: 5px; }")
+ self.update_display()
+
+ def update_display(self):
+ """更新图片显示(等比缩放)"""
+ if not self.original_pixmap.isNull():
+ scaled_pixmap = self.original_pixmap.scaled(
+ 350, 450,
+ Qt.KeepAspectRatio,
+ Qt.SmoothTransformation
+ )
+ self.setPixmap(scaled_pixmap)
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ self.clicked.emit(self.image_path)
+ super().mousePressEvent(event)
+
+
+class DonateDialog(QDialog):
+ """捐赠对话框"""
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("❤️ 捐赠支持")
+ self.setFixedSize(800, 600)
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # 标题
+ title = QLabel("您的捐赠将用于维护和改进本软件。\n\n感谢各位,为爱发电,软主需要大家的支持与关注!\n\n有问题请联系QQ:1066960883")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title.setFont(title_font)
+ title.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title)
+
+ layout.addSpacing(20)
+
+ # 图片区域
+ images_layout = QHBoxLayout()
+
+ # 微信
+ wx_layout = QVBoxLayout()
+ wx_label = QLabel("微信支付")
+ wx_label.setAlignment(Qt.AlignCenter)
+ wx_layout.addWidget(wx_label)
+
+ wx_path = get_resource_path(os.path.join("assets", "images", "donate", "wx.jpg"))
+ self.wx_image = ImageClickLabel(wx_path)
+ self.wx_image.clicked.connect(self.show_full_image)
+ wx_layout.addWidget(self.wx_image)
+
+ images_layout.addLayout(wx_layout)
+
+ images_layout.addSpacing(20)
+
+ # 支付宝
+ zfb_layout = QVBoxLayout()
+ zfb_label = QLabel("支付宝")
+ zfb_label.setAlignment(Qt.AlignCenter)
+ zfb_layout.addWidget(zfb_label)
+
+ zfb_path = get_resource_path(os.path.join("assets", "images", "donate", "zfb.jpg"))
+ self.zfb_image = ImageClickLabel(zfb_path)
+ self.zfb_image.clicked.connect(self.show_full_image)
+ zfb_layout.addWidget(self.zfb_image)
+
+ images_layout.addLayout(zfb_layout)
+
+ layout.addLayout(images_layout)
+
+ layout.addSpacing(20)
+
+ # 关闭按钮
+ btn_layout = QHBoxLayout()
+ btn_layout.addStretch()
+ close_btn = QPushButton("关闭")
+ close_btn.clicked.connect(self.accept)
+ close_btn.setMinimumWidth(100)
+ btn_layout.addWidget(close_btn)
+ layout.addLayout(btn_layout)
+
+ def show_full_image(self, image_path):
+ """显示全屏图片"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle("图片预览")
+ dialog.setMinimumSize(600, 700)
+
+ layout = QVBoxLayout(dialog)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+
+ label = QLabel()
+ pixmap = QPixmap(image_path)
+ label.setPixmap(pixmap)
+ label.setAlignment(Qt.AlignCenter)
+
+ scroll.setWidget(label)
+ layout.addWidget(scroll)
+
+ close_btn = QPushButton("关闭")
+ close_btn.clicked.connect(dialog.accept)
+ close_btn.setMinimumWidth(100)
+
+ btn_layout = QHBoxLayout()
+ btn_layout.addStretch()
+ btn_layout.addWidget(close_btn)
+ layout.addLayout(btn_layout)
+
+ dialog.exec()
+
+
+class ChangeTokenThread(QThread):
+ log_signal = Signal(str)
+ finished_signal = Signal(bool, str)
+
+ def __init__(self, new_token, new_email):
+ super().__init__()
+ self.new_token = new_token
+ self.new_email = new_email
+
+ def run(self):
+ try:
+ config_dir = get_cursor_config_path()
+
+ if not config_dir.exists():
+ self.finished_signal.emit(False, "未找到Cursor配置目录")
+ return
+
+ # 创建备份(不显示日志)
+ backup_dir = config_dir / "backup"
+ backup_dir.mkdir(exist_ok=True)
+ timestamp = random.randint(100000, 999999)
+ backup_subdir = backup_dir / f"backup_{timestamp}"
+ backup_subdir.mkdir(exist_ok=True)
+
+ # 查找 storage.json
+ storage_file = config_dir / "storage.json"
+ if not storage_file.exists():
+ self.finished_signal.emit(False, "未找到 storage.json 文件")
+ return
+
+ # 检查并修复只读属性
+ if not os.access(storage_file, os.W_OK):
+ self.log_signal.emit("🔓 检测到文件只读,正在解除只读属性...")
+ import stat
+ storage_file.chmod(storage_file.stat().st_mode | stat.S_IWRITE)
+
+ # 读取原文件
+ self.log_signal.emit("📖 读取配置文件...")
+ with open(storage_file, "r", encoding="utf-8") as f:
+ content = f.read()
+ data = json.loads(content) if content.strip() else {}
+
+ # 显示原邮箱
+ if "cursorAuth" in data and "cachedEmail" in data.get("cursorAuth", {}):
+ old_email = data["cursorAuth"]["cachedEmail"]
+ self.log_signal.emit(f"📧 原邮箱: {old_email}")
+
+ # 备份原文件(不显示日志)
+ shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
+
+ # 修改 cursorAuth
+ self.log_signal.emit("🔑 替换 cursorAuth...")
+ if "cursorAuth" not in data:
+ data["cursorAuth"] = {}
+ data["cursorAuth"]["accessToken"] = self.new_token
+ data["cursorAuth"]["refreshToken"] = self.new_token
+ data["cursorAuth"]["cachedEmail"] = self.new_email
+ data["cursorAuth"]["plan"] = "pro"
+ data["cursorAuth"]["stripeMembershipType"] = "pro"
+ data["cursorAuth"]["membershipType"] = "pro"
+
+ # 修改 cursorAccount
+ self.log_signal.emit("🔑 替换 cursorAccount...")
+ if "cursorAccount" not in data:
+ data["cursorAccount"] = {}
+ data["cursorAccount"]["token"] = self.new_token
+ data["cursorAccount"]["email"] = self.new_email
+ data["cursorAccount"]["plan"] = "pro"
+
+ # 刷新机器ID
+ self.log_signal.emit("🔧 刷新机器ID...")
+ new_machine_id = generate_machine_id()
+ data["telemetryMacMachineId"] = new_machine_id
+ data["telemetryDevDeviceId"] = new_machine_id
+ data["workspaceIdentifier"] = new_machine_id
+
+ # membershipType pro
+ data["membershipType"] = "pro"
+
+ self.log_signal.emit(f"📧 新邮箱: {self.new_email}")
+
+ # 保存文件
+ self.log_signal.emit("💾 保存配置文件...")
+ with open(storage_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+ # 更新 Cursor 数据库
+ self.log_signal.emit("📦 更新 Cursor 数据库...")
+ update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
+
+ self.log_signal.emit("✅ 换号完成!")
+ self.finished_signal.emit(True, str(backup_subdir))
+
+ except Exception as e:
+ self.log_signal.emit(f"❌ 错误: {str(e)}")
+ self.finished_signal.emit(False, str(e))
+
+
+class CheckUpdateThread(QThread):
+ """检查更新线程"""
+ update_available = Signal(dict)
+ no_update = Signal()
+ error = Signal(str)
+
+ def run(self):
+ try:
+ update_data, error = check_for_updates()
+
+ if error:
+ self.error.emit(error)
+ return
+
+ if update_data:
+ latest_version = update_data.get("latestVersion", "")
+ if compare_versions(__VERSION__, latest_version) == 1:
+ self.update_available.emit(update_data)
+ else:
+ self.no_update.emit()
+ else:
+ self.no_update.emit()
+ except Exception as e:
+ self.error.emit(f"检查更新异常: {str(e)}")
+
+
+class CheckTokenAvailableThread(QThread):
+ log_signal = Signal(str)
+ finished_signal = Signal(bool, str)
+
+ def __init__(self, token: str):
+ super().__init__()
+ self.token = token
+
+ def run(self):
+ # token
+ self.log_signal.emit(" JWT ...")
+ ok, message = detect_account_status(self.token, self.log_signal.emit)
+
+ if not ok:
+ # JWT 401
+ self.log_signal.emit(" API ...")
+ ok_detailed, message_detailed = test_session_token_detailed(self.token, self.log_signal.emit)
+ if ok_detailed:
+ self.finished_signal.emit(True, message_detailed)
+ else:
+ self.finished_signal.emit(False, f"JWT {message} | API : {message_detailed}")
+ return
+
+ self.finished_signal.emit(ok, message)
+ self.finished_signal.emit(ok, message)
+
+
+class CursorNetworkMonitorThread(QThread):
+ log_signal = Signal(str)
+
+ def __init__(self, interval_sec: float = 2.0):
+ super().__init__()
+ self.interval_sec = interval_sec
+ self._running = True
+ self._seen = set()
+
+ def stop(self):
+ self._running = False
+
+ def _try_resolve_host(self, ip: str) -> str:
+ try:
+ return socket.gethostbyaddr(ip)[0]
+ except Exception:
+ return ""
+
+ def run(self):
+ self.log_signal.emit("🌐 网络监控已启动(仅显示连接目标,不含加密内容)")
+ while self._running:
+ cursor_pids = set()
+ for proc in psutil.process_iter(["pid", "name"]):
+ try:
+ name = (proc.info.get("name") or "").lower()
+ if name in ("cursor.exe", "cursor"):
+ cursor_pids.add(proc.info["pid"])
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ if not cursor_pids:
+ self.msleep(int(self.interval_sec * 1000))
+ continue
+
+ try:
+ conns = psutil.net_connections(kind="tcp")
+ except Exception as e:
+ self.log_signal.emit(f"⚠️ 读取网络连接失败: {e}")
+ self.msleep(int(self.interval_sec * 1000))
+ continue
+
+ for c in conns:
+ if c.pid not in cursor_pids or not c.raddr:
+ continue
+ remote_ip = c.raddr.ip
+ remote_port = c.raddr.port
+ status = c.status
+ key = (c.pid, remote_ip, remote_port, status)
+ if key in self._seen:
+ continue
+ self._seen.add(key)
+ host = self._try_resolve_host(remote_ip)
+ if host:
+ self.log_signal.emit(
+ f"🔗 Cursor(pid={c.pid}) -> {remote_ip}:{remote_port} ({host}) [{status}]"
+ )
+ else:
+ self.log_signal.emit(
+ f"🔗 Cursor(pid={c.pid}) -> {remote_ip}:{remote_port} [{status}]"
+ )
+
+ self.msleep(int(self.interval_sec * 1000))
+
+
+def _create_startup_splash(app: QApplication) -> QSplashScreen:
+ """启动画面背景与文字颜色跟随当前 Qt/系统主题(QPalette)。"""
+ pal = app.palette()
+ bg = pal.color(QPalette.ColorRole.Window)
+ title_c = pal.color(QPalette.ColorRole.WindowText)
+ sub_c = pal.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText)
+ if not sub_c.isValid():
+ sub_c = pal.color(QPalette.ColorRole.Mid)
+
+ w, h = 440, 300
+ pix = QPixmap(w, h)
+ pix.fill(bg)
+ painter = QPainter(pix)
+ painter.setPen(title_c)
+ title_font = QFont()
+ title_font.setPointSize(14)
+ title_font.setBold(True)
+ painter.setFont(title_font)
+ painter.drawText(0, 100, w, 40, Qt.AlignHCenter, "CursorTokenLogin")
+ painter.setPen(sub_c)
+ sub_font = QFont()
+ sub_font.setPointSize(9)
+ painter.setFont(sub_font)
+ painter.drawText(0, 140, w, 30, Qt.AlignHCenter, "正在准备运行环境…")
+ painter.end()
+ splash = QSplashScreen(pix, Qt.WindowStaysOnTopHint)
+ splash.setWindowFlag(Qt.FramelessWindowHint, True)
+ splash.setPalette(pal)
+ return splash
+
+
+class MainWindow(QMainWindow):
+ def __init__(self, splash: Optional[QSplashScreen] = None):
+ super().__init__()
+ self._splash = splash
+ self._splash_phase = 0
+
+ def splash_pulse(phase: str):
+ if not self._splash:
+ return
+ self._splash_phase += 1
+ dots = "." * (self._splash_phase % 4)
+ pal = QApplication.palette()
+ msg_c = pal.color(QPalette.ColorRole.PlaceholderText)
+ if not msg_c.isValid():
+ msg_c = pal.color(QPalette.ColorRole.WindowText)
+ self._splash.showMessage(
+ f"{phase}{dots}",
+ Qt.AlignBottom | Qt.AlignHCenter,
+ msg_c,
+ )
+ QApplication.processEvents()
+
+ self._splash_pulse = splash_pulse
+ self._splash_pulse("正在加载")
+
+ # 获取UI文件路径
+ ui_path = get_resource_path(os.path.join("layout", "main.ui"))
+
+ # 详细的调试信息
+ debug_info = f"UI文件路径: {ui_path}\n"
+ debug_info += f"路径存在: {Path(ui_path).exists()}\n"
+
+ # 检查基础路径
+ if hasattr(sys, '_MEIPASS'):
+ debug_info += f"打包路径(MEIPASS): {sys._MEIPASS}\n"
+ debug_info += f"脚本路径: {os.path.dirname(os.path.abspath(__file__))}\n"
+ debug_info += f"当前工作目录: {os.getcwd()}\n"
+
+ print(debug_info)
+
+ if not Path(ui_path).exists():
+ if self._splash:
+ self._splash.close()
+ # 用正斜杠展示路径,避免在部分环境下反斜杠被误解析
+ error_msg = f"找不到UI文件: {Path(ui_path).as_posix()}\n\n详细信息:\n{debug_info}"
+ QMessageBox.critical(None, "错误", error_msg)
+ sys.exit(1)
+
+ self._splash_pulse("正在加载界面")
+
+ # 加载UI文件 - 不设置parent,让loader返回完整的窗口
+ try:
+ loader = QUiLoader()
+ self.ui = loader.load(ui_path)
+
+ if self.ui is None:
+ if self._splash:
+ self._splash.close()
+ QMessageBox.critical(None, "错误", "UI文件加载失败")
+ sys.exit(1)
+
+ self._splash_pulse("正在装配窗口")
+
+ # 将UI的所有属性和方法复制到当前窗口
+ # 先保存当前窗口的状态栏
+ status_bar = self.statusBar()
+
+ # 使用UI的central widget
+ self.setCentralWidget(self.ui.centralwidget)
+
+ # 复制窗口属性
+ self.setWindowTitle(self.ui.windowTitle())
+ self.resize(self.ui.size())
+
+ # 清理原来的ui对象,避免混淆
+ del self.ui.centralwidget
+
+ except Exception as e:
+ if self._splash:
+ self._splash.close()
+ QMessageBox.critical(None, "错误", f"加载UI文件时出错: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+ self._splash_pulse("正在初始化")
+
+ self.cursor_path = get_default_cursor_path()
+ self.backup_path = ""
+ self._update_download_thread = None
+ self._token_check_thread = None
+ self._network_monitor_thread = None
+ self._emergency_dialog = None
+ self._usage_guide_dialog = None
+ self._token_extract_dialog = None
+ self._token_display = None
+ self._current_extracted_token = ""
+ self.log_file_path = self._prepare_log_file_path()
+
+ # 重新查找组件 - 从self查找
+ self.txtToken = self.findChild(QTextEdit, "txtToken")
+ self.txtLog = self.findChild(QTextEdit, "txtLog")
+ self.txtCursorPath = self.findChild(QLineEdit, "txtCursorPath")
+ self.btnChange = self.findChild(QPushButton, "btnChange")
+ self.btnOpenCursor = self.findChild(QPushButton, "btnOpenCursor")
+ self.btnCheckTokenAvailable = self.findChild(QPushButton, "btnCheckTokenAvailable")
+ self.btnBrowseCursor = self.findChild(QPushButton, "btnBrowseCursor")
+ self.btnAutoCursorPath = None
+ self.btnClearLog = self.findChild(QPushButton, "btnClearLog")
+ self.btnDonate = self.findChild(QPushButton, "btnDonate")
+ self.btnCheckUpdate = self.findChild(QPushButton, "btnCheckUpdate")
+ self.btnEmergencyRepair = self.findChild(QPushButton, "btnEmergencyRepair")
+ self.btnUsageGuide = self.findChild(QPushButton, "btnUsageGuide")
+ self._load_cached_logs_to_ui()
+
+ # 调试信息
+ print(f"txtToken: {self.txtToken}")
+ print(f"txtLog: {self.txtLog}")
+ print(f"txtCursorPath: {self.txtCursorPath}")
+ print(f"btnChange: {self.btnChange}")
+ print(f"btnBrowseCursor: {self.btnBrowseCursor}")
+ print(f"btnCheckTokenAvailable: {self.btnCheckTokenAvailable}")
+ print(f"btnClearLog: {self.btnClearLog}")
+ print(f"btnDonate: {self.btnDonate}")
+ print(f"btnCheckUpdate: {self.btnCheckUpdate}")
+ print(f"btnUsageGuide: {self.btnUsageGuide}")
+
+ # 设置默认Cursor路径
+ if self.txtCursorPath:
+ self.txtCursorPath.setText(get_default_cursor_path())
+
+ # 在「浏览」按钮右侧动态增加「自动查找」按钮
+ if self.btnBrowseCursor and self.btnBrowseCursor.parentWidget():
+ parent = self.btnBrowseCursor.parentWidget()
+ layout = parent.layout()
+ if layout:
+ self.btnAutoCursorPath = QPushButton("自动查找", parent)
+ self.btnAutoCursorPath.setObjectName("btnAutoCursorPath")
+ self.btnAutoCursorPath.setMinimumWidth(88)
+ idx = layout.indexOf(self.btnBrowseCursor)
+ if idx >= 0:
+ layout.insertWidget(idx + 1, self.btnAutoCursorPath)
+ else:
+ layout.addWidget(self.btnAutoCursorPath)
+
+ # 信号连接
+ if self.btnChange:
+ self.btnChange.clicked.connect(self.on_change_clicked)
+ if self.btnOpenCursor:
+ self.btnOpenCursor.clicked.connect(self.on_open_cursor_clicked)
+ if self.btnCheckTokenAvailable:
+ self.btnCheckTokenAvailable.clicked.connect(self.on_check_token_available_clicked)
+ if self.btnBrowseCursor:
+ self.btnBrowseCursor.clicked.connect(self.on_browse_cursor)
+ if self.btnAutoCursorPath:
+ self.btnAutoCursorPath.clicked.connect(self.on_auto_config_cursor)
+ if self.btnClearLog:
+ self.btnClearLog.clicked.connect(self.on_clear_log)
+ if self.btnDonate:
+ self.btnDonate.clicked.connect(self.on_donate_clicked)
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.clicked.connect(self.on_check_update_clicked)
+ if self.btnEmergencyRepair:
+ self.btnEmergencyRepair.clicked.connect(self.on_emergency_repair_clicked)
+ if self.btnUsageGuide:
+ self.btnUsageGuide.clicked.connect(self.on_usage_guide_clicked)
+ # 设置版本号显示在状态栏右侧
+ self.statusBar().addPermanentWidget(QLabel(f"Version: {__VERSION__}"))
+
+ self.log("🚀 程序启动成功")
+ self.log("📋 请先粘贴Token,然后点击换号")
+
+ if self._splash:
+ self._splash.finish(self)
+
+ # 启动检查更新
+ self.check_update_thread = CheckUpdateThread()
+ self.check_update_thread.update_available.connect(self.on_update_available)
+ self.check_update_thread.no_update.connect(self.on_no_update)
+ self.check_update_thread.error.connect(self.on_update_error)
+ self.check_update_thread.start()
+
+ def _restore_check_update_btn(self):
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(True)
+ self.btnCheckUpdate.setText("🔄 检查更新")
+
+ def _on_update_download_progress(self, downloaded: int, total: int):
+ if total > 0:
+ pct = min(100, downloaded * 100 // total)
+ self.log(f"⬇️ 下载进度: {pct}% ({downloaded // 1024} KB / {total // 1024} KB)")
+ else:
+ self.log(f"⬇️ 已下载: {downloaded // 1024} KB(服务器未提供总大小)")
+
+ def _on_update_download_finished(self, success: bool, detail: str):
+ self._restore_check_update_btn()
+ if success and detail == "restarting":
+ QMessageBox.information(
+ self,
+ "更新完成",
+ "新版本已替换完成。\n\n"
+ "本程序将自动退出。退出后请手动运行新版本"
+ )
+ QTimer.singleShot(200, QApplication.instance().quit)
+ elif not success:
+ QMessageBox.warning(self, "更新失败", detail or "下载或安装失败,请稍后重试。")
+
+ def _begin_update_workflow_after_confirm(self, update_data: dict):
+ """用户确认升级后:日志展示流程 → 校验 downloadUrl → 下载(含进度)→ 替换程序。"""
+ # self.log("──────── 软件更新流程 ────────")
+ # self.log("① 校验接口返回的 downloadUrl 是否有效…")
+ url = normalize_download_url(update_data.get("downloadUrl"))
+ if not url:
+ err = "当前软件没有更新包,请联系管理员。"
+ self.log(f"❌ {err}")
+ QMessageBox.warning(self, "无法更新", err)
+ self._restore_check_update_btn()
+ return
+
+ self.log(f"开始下载安装包…")
+ # self.log(f" 地址: {url}")
+
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(False)
+ self.btnCheckUpdate.setText("更新中…")
+
+ self._update_download_thread = DownloadUpdateThread(url)
+ self._update_download_thread.log_signal.connect(self.log)
+ self._update_download_thread.progress_signal.connect(self._on_update_download_progress)
+ self._update_download_thread.finished_signal.connect(self._on_update_download_finished)
+ self._update_download_thread.start()
+
+ def on_update_available(self, update_data):
+ """有新版本可用"""
+ latest_version = update_data.get("latestVersion", "")
+ download_url = normalize_download_url(update_data.get("downloadUrl"))
+ force_update = update_data.get("forceUpdate", False)
+ release_notes = update_data.get("releaseNotes", "")
+
+ self.log(f"🔔 发现新版本 v{latest_version}!")
+
+ message = f"发现新版本 v{latest_version}\n\n"
+ if release_notes:
+ message += f"更新内容:\n{release_notes}\n\n"
+
+ if force_update:
+ message += "⚠️ 这是强制更新,请立即升级!"
+ else:
+ message += "是否现在升级?"
+
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle("发现新版本")
+ msg_box.setText(message)
+ msg_box.setIcon(QMessageBox.Information)
+
+ # Windows 上点标题栏 × 会触发「默认按钮」;默认设为「取消」/ 无默认,
+ # 避免 × 被当成「立即升级」或「确定」。
+ btn_update = None
+ btn_cancel = None
+ if download_url:
+ btn_update = msg_box.addButton("立即升级", QMessageBox.ActionRole)
+ btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
+ msg_box.setDefaultButton(btn_cancel)
+ else:
+ msg_box.setStandardButtons(QMessageBox.Ok)
+ msg_box.setDefaultButton(QMessageBox.NoButton)
+
+ ret = msg_box.exec()
+
+ if download_url:
+ if ret == QMessageBox.Rejected or msg_box.clickedButton() != btn_update:
+ self._restore_check_update_btn()
+ return
+ else:
+ # 无下载地址时仅「确定」会走校验流程;× 为 Rejected,不等于 Ok
+ if ret != QMessageBox.Ok:
+ self._restore_check_update_btn()
+ return
+
+ self._begin_update_workflow_after_confirm(update_data)
+
+ def on_no_update(self):
+ """没有新版本(启动时自动检查)"""
+ self.log(f"✅ 当前已是最新版本 v{__VERSION__}")
+
+ def on_update_error(self, error_msg):
+ """检查更新失败(启动时自动检查)"""
+ print(f"检查更新失败: {error_msg}")
+ self.log(f"❌ 检查更新失败: {error_msg}")
+
+ # 恢复按钮状态
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(True)
+ self.btnCheckUpdate.setText("🔄 检查更新")
+
+ def on_check_update_clicked(self):
+ """手动检查更新按钮点击"""
+ self.log("🔄 正在检查更新...")
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(False)
+ self.btnCheckUpdate.setText("检查中...")
+
+ self.check_update_thread = CheckUpdateThread()
+ self.check_update_thread.update_available.connect(self.on_update_available)
+ self.check_update_thread.no_update.connect(self.on_no_update_clicked)
+ self.check_update_thread.error.connect(self.on_update_error_clicked)
+ self.check_update_thread.start()
+
+ def on_no_update_clicked(self):
+ """手动检查时没有新版本"""
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(True)
+ self.btnCheckUpdate.setText("🔄 检查更新")
+ self.log(f"✅ 当前已是最新版本 v{__VERSION__}")
+
+ def on_update_error_clicked(self, error_msg):
+ """手动检查更新失败"""
+ if self.btnCheckUpdate:
+ self.btnCheckUpdate.setEnabled(True)
+ self.btnCheckUpdate.setText("🔄 检查更新")
+ self.log(f"❌ 检查更新失败: {error_msg}")
+
+ def log(self, message):
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ line = f"[{ts}] {message}"
+ if self.txtLog:
+ self.txtLog.append(line)
+ self._scroll_log_to_bottom()
+ if self.log_file_path:
+ try:
+ with open(self.log_file_path, "a", encoding="utf-8") as f:
+ f.write(line + "\n")
+ except OSError:
+ pass
+
+ def _scroll_log_to_bottom(self):
+ """让日志文本框始终滚动到最底部,跟踪最新日志。"""
+ if not self.txtLog:
+ return
+ scroll_bar = self.txtLog.verticalScrollBar()
+ if scroll_bar:
+ scroll_bar.setValue(scroll_bar.maximum())
+
+ def _prepare_log_file_path(self) -> Optional[Path]:
+ """准备当前日志文件路径;优先沿用当天最新日志,失败时返回 None。"""
+ try:
+ self.log_dir = Path.home() / ".cursortokenlogin" / "logs"
+ self.log_dir.mkdir(parents=True, exist_ok=True)
+ latest = self._get_latest_log_file_path()
+ today = datetime.now().strftime("%Y-%m-%d")
+ return latest or (self.log_dir / f"{today}-0001.log")
+ except OSError:
+ self.log_dir = None
+ return None
+
+ def on_clear_log(self):
+ if self.txtLog:
+ self.txtLog.clear()
+ next_log = self._get_next_log_file_path()
+ if next_log:
+ self.log_file_path = next_log
+ try:
+ # 立即创建新的空日志文件,确保重启后读取的是清空后的新会话文件。
+ self.log_file_path.touch(exist_ok=True)
+ except OSError:
+ pass
+
+ def _load_cached_logs_to_ui(self):
+ """启动时把最近日志文件内容回显到界面。"""
+ if not self.txtLog or not self.log_file_path or not self.log_file_path.exists():
+ return
+ try:
+ with open(self.log_file_path, "r", encoding="utf-8") as f:
+ content = f.read().strip()
+ if content:
+ self.txtLog.setPlainText(content)
+ self._scroll_log_to_bottom()
+ except OSError:
+ pass
+
+ def _get_latest_log_file_path(self) -> Optional[Path]:
+ if not getattr(self, "log_dir", None):
+ return None
+ today = datetime.now().strftime("%Y-%m-%d")
+ prefix = f"{today}-"
+ try:
+ files = sorted(self.log_dir.glob(f"{prefix}*.log"))
+ except OSError:
+ return None
+ if not files:
+ return None
+ numbered = []
+ for p in files:
+ stem = p.stem
+ if not stem.startswith(prefix):
+ continue
+ seq = stem[len(prefix):]
+ if seq.isdigit():
+ numbered.append((int(seq), p))
+ if not numbered:
+ return None
+ numbered.sort(key=lambda x: x[0])
+ return numbered[-1][1]
+
+ def _get_next_log_file_path(self) -> Optional[Path]:
+ if not getattr(self, "log_dir", None):
+ return None
+ today = datetime.now().strftime("%Y-%m-%d")
+ prefix = f"{today}-"
+ latest = self._get_latest_log_file_path()
+ if latest and latest.stem.startswith(prefix):
+ seq = latest.stem[len(prefix):]
+ next_no = int(seq) + 1 if seq.isdigit() else 1
+ else:
+ next_no = 1
+ return self.log_dir / f"{today}-{next_no:04d}.log"
+
+ def on_browse_cursor(self):
+ """浏览Cursor路径"""
+ from PySide6.QtWidgets import QFileDialog
+ if sys.platform == "win32":
+ file_path, _ = QFileDialog.getOpenFileName(
+ self, "选择Cursor.exe", "", "可执行文件 (*.exe)"
+ )
+ elif sys.platform == "darwin":
+ file_path = QFileDialog.getExistingDirectory(self, "选择Cursor.app")
+ else:
+ file_path, _ = QFileDialog.getOpenFileName(self, "选择Cursor")
+
+ if file_path:
+ if self.txtCursorPath:
+ self.txtCursorPath.setText(file_path)
+ self.cursor_path = file_path
+
+ def on_auto_config_cursor(self):
+ """自动查找并填充 Cursor 安装路径。"""
+ cursor_path = get_default_cursor_path()
+ if cursor_path and Path(cursor_path).exists():
+ if self.txtCursorPath:
+ self.txtCursorPath.setText(cursor_path)
+ self.cursor_path = cursor_path
+ self.log(f"✅ 已自动查找 Cursor 路径: {cursor_path}")
+ return
+ self.log("❌ 自动查找失败:未找到 Cursor 安装目录,请手动浏览选择。")
+ QMessageBox.warning(self, "提示", "未自动找到 Cursor 安装目录,请点击“浏览”手动选择。")
+
+ def _launch_cursor(self, cursor_path: str) -> bool:
+ """按当前平台启动 Cursor。"""
+ try:
+ if sys.platform == "win32":
+ os.startfile(cursor_path)
+ elif sys.platform == "darwin":
+ subprocess.Popen(["open", cursor_path])
+ else:
+ subprocess.Popen([cursor_path])
+ self.log("✅ Cursor已启动")
+ return True
+ except Exception as e:
+ self.log(f"❌ 打开Cursor失败: {str(e)}")
+ QMessageBox.warning(self, "警告", f"打开Cursor失败: {str(e)}")
+ return False
+
+ def _resolve_cursor_path_or_prompt(self) -> str:
+ """
+ 返回可用的 Cursor 路径;当未配置/无效时,提示用户手动配置或自动查找。
+ """
+ cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
+ if cursor_path and Path(cursor_path).exists():
+ return cursor_path
+
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle("Cursor路径未配置")
+ msg_box.setText("未检测到有效的 Cursor 路径,请先配置。")
+ msg_box.setIcon(QMessageBox.Warning)
+ btn_manual = msg_box.addButton("手动配置", QMessageBox.ActionRole)
+ btn_auto = msg_box.addButton("自动查找", QMessageBox.ActionRole)
+ btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
+ msg_box.setDefaultButton(btn_auto)
+ msg_box.exec()
+
+ clicked = msg_box.clickedButton()
+ if clicked == btn_manual:
+ self.on_browse_cursor()
+ elif clicked == btn_auto:
+ self.on_auto_config_cursor()
+ else:
+ return ""
+
+ cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
+ if cursor_path and Path(cursor_path).exists():
+ return cursor_path
+ return ""
+
+ def on_open_cursor_clicked(self):
+ """点击“打开Cursor”按钮:优先使用已配置路径,否则引导配置。"""
+ cursor_path = self._resolve_cursor_path_or_prompt()
+ if not cursor_path:
+ return
+ self.log("🚀 正在打开Cursor...")
+ self._launch_cursor(cursor_path)
+
+ def _extract_session_token(self, raw_value: str) -> str:
+ """从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。"""
+ s = (raw_value or "").strip().strip('"').strip("'")
+ if not s:
+ return ""
+ m = re.search(r"SessionToken\s*=\s*([^;\s]+)", s, flags=re.IGNORECASE)
+ if m:
+ return m.group(1).strip().strip('"').strip("'")
+ if ";" in s:
+ first = s.split(";", 1)[0].strip()
+ if "=" in first:
+ k, v = first.split("=", 1)
+ if k.strip().lower() in ("workoscursorsessiontoken", "sessiontoken"):
+ return v.strip().strip('"').strip("'")
+ return s.replace("SessionToken=", "").strip()
+
+ def on_check_token_available_clicked(self):
+ """检测当前输入 token 的账号状态(不发送聊天请求)。"""
+ raw_token = self.txtToken.toPlainText().strip() if self.txtToken else ""
+ token = self._extract_session_token(raw_token)
+ if not token:
+ QMessageBox.warning(self, "提示", "请先粘贴有效 Token。")
+ return
+
+ self.log("🧪 开始账号状态检测...")
+ if self.btnCheckTokenAvailable:
+ self.btnCheckTokenAvailable.setEnabled(False)
+ self.btnCheckTokenAvailable.setText("检测中...")
+
+ self._token_check_thread = CheckTokenAvailableThread(token)
+ self._token_check_thread.log_signal.connect(self.log)
+ self._token_check_thread.finished_signal.connect(self.on_check_token_available_finished)
+ self._token_check_thread.start()
+
+ @Slot(bool, str)
+ def on_check_token_available_finished(self, ok: bool, message: str):
+ if self.btnCheckTokenAvailable:
+ self.btnCheckTokenAvailable.setEnabled(True)
+ self.btnCheckTokenAvailable.setText("状态检测")
+ self.log(message)
+ if ok:
+ QMessageBox.information(self, "检测结果", message)
+ else:
+ QMessageBox.warning(self, "检测结果", message)
+
+ def _show_change_success_countdown_dialog(self, seconds: int = 4):
+ """换号成功后显示倒计时提示框,倒计时结束自动关闭。"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle("提示")
+ dialog.setModal(True)
+ dialog.setFixedSize(420, 160)
+
+ layout = QVBoxLayout(dialog)
+ tip_label = QLabel("换号成功咯!请及时去确认收货哦!万分感谢!", dialog)
+ tip_label.setWordWrap(True)
+ tip_label.setAlignment(Qt.AlignCenter)
+ layout.addStretch()
+ layout.addWidget(tip_label)
+
+ countdown_label = QLabel("", dialog)
+ countdown_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(countdown_label)
+ layout.addStretch()
+
+ remain = {"value": max(1, int(seconds))}
+
+ def refresh_text():
+ countdown_label.setText(f"{remain['value']} 秒后自动关闭")
+
+ refresh_text()
+ timer = QTimer(dialog)
+
+ def on_timeout():
+ remain["value"] -= 1
+ if remain["value"] <= 0:
+ timer.stop()
+ dialog.accept()
+ return
+ refresh_text()
+
+ timer.timeout.connect(on_timeout)
+ timer.start(1000)
+ dialog.exec()
+
+ def on_change_clicked(self):
+ token = self.txtToken.toPlainText().strip() if self.txtToken else ""
+ cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
+
+ if not token:
+ QMessageBox.warning(self, "警告", "请先粘贴Token!")
+ return
+ if len(token) < 20:
+ QMessageBox.warning(self, "警告", "Token格式可能不正确!")
+ return
+
+ if not cursor_path or not Path(cursor_path).exists():
+ QMessageBox.warning(self, "警告", "请设置正确的Cursor路径!")
+ return
+
+ # 检测Cursor是否正在运行
+ if is_cursor_running():
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle("Cursor正在运行")
+ msg_box.setText("Cursor正在运行中!\n请先保存代码并手动关闭Cursor。")
+ msg_box.setIcon(QMessageBox.Warning)
+ btn_close = msg_box.addButton("💀 强制关闭", QMessageBox.ActionRole)
+ btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
+ msg_box.setDefaultButton(btn_cancel)
+ msg_box.exec_()
+
+ if msg_box.clickedButton() == btn_close:
+ self.log("💀 正在强制关闭Cursor...")
+ if kill_cursor():
+ self.log("✅ Cursor已关闭")
+ else:
+ self.log("⚠️ 未找到运行中的Cursor进程")
+ QMessageBox.warning(self, "警告", "未找到运行中的Cursor进程")
+ return
+ else:
+ return
+
+ # 自动生成新邮箱
+ new_email = generate_random_email()
+ self.log(f"📧 生成新邮箱: {new_email}")
+
+ # 禁用按钮
+ if self.btnChange:
+ self.btnChange.setEnabled(False)
+ self.btnChange.setText("🔄 处理中...")
+
+ # 启动后台线程
+ self.thread = ChangeTokenThread(token, new_email)
+ self.thread.log_signal.connect(self.log)
+ self.thread.finished_signal.connect(self.on_change_finished)
+ self.thread.start()
+
+ def on_donate_clicked(self):
+ """打开捐赠对话框"""
+ dialog = DonateDialog(self)
+ dialog.exec()
+
+ def on_usage_guide_clicked(self):
+ """打开使用说明图片(非模态,可与其他窗口并行)。"""
+ if self._usage_guide_dialog and self._usage_guide_dialog.isVisible():
+ self._usage_guide_dialog.raise_()
+ self._usage_guide_dialog.activateWindow()
+ return
+
+ image_path = get_resource_path(os.path.join("assets", "images", "donate", "info.png"))
+ if not Path(image_path).is_file():
+ QMessageBox.warning(self, "提示", f"未找到使用说明图片:{image_path}")
+ return
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("使用说明")
+ dialog.setMinimumSize(900, 700)
+ dialog.setModal(False)
+ dialog.setWindowModality(Qt.NonModal)
+
+ layout = QVBoxLayout(dialog)
+
+ tip = QLabel("可使用鼠标滚轮查看长图内容,可通过按钮缩放图片。")
+ tip.setAlignment(Qt.AlignCenter)
+ layout.addWidget(tip)
+
+ zoom_row = QHBoxLayout()
+ zoom_row.addStretch()
+ btn_zoom_out = QPushButton("缩小 -", dialog)
+ btn_zoom_reset = QPushButton("100%", dialog)
+ btn_zoom_in = QPushButton("放大 +", dialog)
+ zoom_label = QLabel("100%", dialog)
+ zoom_row.addWidget(btn_zoom_out)
+ zoom_row.addWidget(btn_zoom_reset)
+ zoom_row.addWidget(btn_zoom_in)
+ zoom_row.addWidget(zoom_label)
+ layout.addLayout(zoom_row)
+
+ scroll = QScrollArea(dialog)
+ scroll.setWidgetResizable(True)
+
+ label = QLabel(scroll)
+ original_pixmap = QPixmap(image_path)
+ zoom_state = {"scale": 1.0}
+
+ def apply_zoom():
+ if original_pixmap.isNull():
+ return
+ target_w = max(1, int(original_pixmap.width() * zoom_state["scale"]))
+ target_h = max(1, int(original_pixmap.height() * zoom_state["scale"]))
+ scaled = original_pixmap.scaled(
+ target_w,
+ target_h,
+ Qt.KeepAspectRatio,
+ Qt.SmoothTransformation,
+ )
+ label.setPixmap(scaled)
+ label.resize(scaled.size())
+ zoom_label.setText(f"{int(zoom_state['scale'] * 100)}%")
+
+ def zoom_in():
+ zoom_state["scale"] = min(3.0, zoom_state["scale"] * 1.2)
+ apply_zoom()
+
+ def zoom_out():
+ zoom_state["scale"] = max(0.2, zoom_state["scale"] / 1.2)
+ apply_zoom()
+
+ def zoom_reset():
+ zoom_state["scale"] = 1.0
+ apply_zoom()
+
+ btn_zoom_in.clicked.connect(zoom_in)
+ btn_zoom_out.clicked.connect(zoom_out)
+ btn_zoom_reset.clicked.connect(zoom_reset)
+
+ apply_zoom()
+ label.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
+
+ scroll.setWidget(label)
+ layout.addWidget(scroll, 1)
+
+ btn_close = QPushButton("关闭", dialog)
+ btn_close.clicked.connect(dialog.accept)
+ btn_row = QHBoxLayout()
+ btn_row.addStretch()
+ btn_row.addWidget(btn_close)
+ layout.addLayout(btn_row)
+
+ self._usage_guide_dialog = dialog
+ dialog.show()
+
+ def on_emergency_repair_clicked(self):
+ """应急检修:弹出工具面板。"""
+ if self._emergency_dialog and self._emergency_dialog.isVisible():
+ self._emergency_dialog.raise_()
+ self._emergency_dialog.activateWindow()
+ return
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("应急检修")
+ dialog.setMinimumSize(400, 300)
+ dialog.setModal(False)
+ dialog.setWindowModality(Qt.NonModal)
+
+ layout = QVBoxLayout(dialog)
+ layout.addWidget(QLabel("请选择要执行的操作:"))
+
+ actions_widget = QWidget(dialog)
+ actions_layout = QVBoxLayout(actions_widget)
+ actions_layout.setContentsMargins(0, 0, 0, 0)
+ actions_layout.setSpacing(10)
+ actions_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+
+ btn_download = QPushButton("下载DB Browser")
+ btn_clear_cache = QPushButton("清除Cursor缓存")
+ btn_extract_token = QPushButton("Token提取")
+ btn_network_monitor = QPushButton("网络监控(临时)")
+ btn_stop_monitor = QPushButton("停止网络监控")
+ for btn in (
+ btn_download,
+ btn_clear_cache,
+ btn_extract_token,
+ btn_network_monitor,
+ btn_stop_monitor,
+ ):
+ btn.setMinimumWidth(180)
+ btn.setMaximumWidth(260)
+ btn.setSizePolicy(btn.sizePolicy().horizontalPolicy(), btn.sizePolicy().verticalPolicy())
+ actions_layout.addWidget(btn, 0, Qt.AlignLeft)
+
+ layout.addWidget(actions_widget)
+ layout.addStretch()
+
+ btn_download.clicked.connect(self.download_db_tool)
+ btn_clear_cache.clicked.connect(self.clear_cursor_cache)
+ btn_extract_token.clicked.connect(self.on_token_extract_clicked)
+ btn_network_monitor.clicked.connect(self.start_cursor_network_monitor)
+ btn_stop_monitor.clicked.connect(self.stop_cursor_network_monitor)
+ self._emergency_dialog = dialog
+ dialog.show()
+
+ def start_cursor_network_monitor(self):
+ if self._network_monitor_thread and self._network_monitor_thread.isRunning():
+ self.log("ℹ️ 网络监控已在运行。")
+ return
+ self._network_monitor_thread = CursorNetworkMonitorThread(interval_sec=2.0)
+ self._network_monitor_thread.log_signal.connect(self.log)
+ self._network_monitor_thread.start()
+ self.log("🟢 已启动临时网络监控。")
+
+ def stop_cursor_network_monitor(self, silent: bool = False):
+ if not self._network_monitor_thread or not self._network_monitor_thread.isRunning():
+ if not silent:
+ self.log("ℹ️ 当前没有运行中的网络监控。")
+ return
+ self._network_monitor_thread.stop()
+ self._network_monitor_thread.wait(1500)
+ if not silent:
+ self.log("🛑 已停止临时网络监控。")
+
+ def on_token_extract_clicked(self):
+ """密码通过后打开 Token 提取窗口。"""
+ pwd, ok = QInputDialog.getText(
+ self,
+ "Token提取",
+ "请输入密码:",
+ QLineEdit.Password,
+ )
+ if not ok:
+ return
+ if pwd != "920103":
+ self.log("❌ Token提取密码错误")
+ QMessageBox.warning(self, "提示", "密码错误,无法使用 Token 提取。")
+ return
+
+ if self._token_extract_dialog and self._token_extract_dialog.isVisible():
+ self._token_extract_dialog.raise_()
+ self._token_extract_dialog.activateWindow()
+ return
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("Token提取")
+ dialog.setMinimumSize(700, 420)
+ dialog.setModal(False)
+ dialog.setWindowModality(Qt.NonModal)
+
+ layout = QVBoxLayout(dialog)
+ layout.addWidget(QLabel("Token:"))
+
+ token_display = QTextEdit(dialog)
+ token_display.setReadOnly(True)
+ token_display.setPlaceholderText("点击“读取Token”后将在此显示。")
+ layout.addWidget(token_display, 1)
+
+ btn_row = QHBoxLayout()
+ btn_read = QPushButton("读取Token", dialog)
+ btn_save = QPushButton("另存桌面", dialog)
+ btn_close = QPushButton("关闭", dialog)
+ btn_row.addWidget(btn_read)
+ btn_row.addWidget(btn_save)
+ btn_row.addStretch()
+ btn_row.addWidget(btn_close)
+ layout.addLayout(btn_row)
+
+ self._token_extract_dialog = dialog
+ self._token_display = token_display
+
+ btn_read.clicked.connect(self.read_cursor_token_for_dialog)
+ btn_save.clicked.connect(self.save_extracted_token_to_desktop)
+ btn_close.clicked.connect(dialog.close)
+ dialog.show()
+
+ def _read_current_cursor_token(self) -> str:
+ """从 Cursor 的 storage.json 读取当前 token。"""
+ config_dir = get_cursor_config_path()
+ storage_file = config_dir / "storage.json"
+ if not storage_file.exists():
+ return ""
+ try:
+ with open(storage_file, "r", encoding="utf-8") as f:
+ content = f.read()
+ data = json.loads(content) if content.strip() else {}
+ except Exception:
+ return ""
+
+ token = (
+ data.get("cursorAuth", {}).get("accessToken")
+ or data.get("cursorAuth", {}).get("refreshToken")
+ or data.get("cursorAccount", {}).get("token")
+ or ""
+ )
+ return str(token).strip()
+
+ def read_cursor_token_for_dialog(self):
+ token = self._read_current_cursor_token()
+ if not token:
+ self._current_extracted_token = ""
+ if self._token_display:
+ self._token_display.setPlainText("")
+ self.log("⚠️ 未读取到 Token")
+ QMessageBox.warning(self, "提示", "未读取到 Token,请确认 Cursor 配置是否存在。")
+ return
+
+ self._current_extracted_token = token
+ if self._token_display:
+ self._token_display.setPlainText(token)
+ self.log("✅ 已读取当前 Token")
+
+ def save_extracted_token_to_desktop(self):
+ token = self._current_extracted_token
+ if not token:
+ token = self._read_current_cursor_token()
+ if token:
+ self._current_extracted_token = token
+ if self._token_display:
+ self._token_display.setPlainText(token)
+ if not token:
+ QMessageBox.warning(self, "提示", "没有可保存的 Token,请先点击“读取Token”。")
+ return
+
+ desktop = Path.home() / "Desktop"
+ save_path = desktop / "token.txt"
+ try:
+ with open(save_path, "w", encoding="utf-8") as f:
+ f.write(token)
+ self.log(f"✅ Token 已保存到桌面: {save_path.name}")
+ QMessageBox.information(self, "成功", "Token 已覆盖保存到桌面 token.txt")
+ except OSError as e:
+ self.log(f"❌ Token 保存失败: {e}")
+ QMessageBox.warning(self, "失败", f"保存失败:{e}")
+
+ def download_db_tool(self):
+ """下载 DB Browser 到桌面。"""
+ import urllib.request
+ import threading
+
+ url = "http://7colud.yunzer.cn/software/db%20browser%20for%20sqlite.zip"
+ desktop = Path.home() / "Desktop"
+ save_path = desktop / "db browser for sqlite.zip"
+
+ def download():
+ try:
+ self.log("🔧 正在下载检修工具...")
+ urllib.request.urlretrieve(url, str(save_path))
+ self.log(f"✅ 下载完成,已保存到桌面:{save_path.name}")
+ except Exception as e:
+ self.log(f"❌ 下载失败: {e}")
+
+ threading.Thread(target=download, daemon=True).start()
+
+ def clear_cursor_cache(self):
+ """清除 %APPDATA%\\Cursor 目录。"""
+ if is_cursor_running():
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle("Cursor正在运行")
+ msg_box.setText("检测到 Cursor 正在运行。\n请先关闭 Cursor 后再清除缓存。")
+ msg_box.setIcon(QMessageBox.Warning)
+ btn_force_close = msg_box.addButton("💀 强制关闭", QMessageBox.ActionRole)
+ btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
+ msg_box.setDefaultButton(btn_cancel)
+ msg_box.exec()
+
+ if msg_box.clickedButton() == btn_force_close:
+ self.log("💀 正在强制关闭Cursor...")
+ if kill_cursor():
+ self.log("✅ Cursor已关闭")
+ else:
+ self.log("⚠️ 未找到运行中的Cursor进程")
+ QMessageBox.warning(self, "警告", "未找到运行中的Cursor进程")
+ return
+ else:
+ self.log("ℹ️ 已取消清除缓存。")
+ return
+
+ cursor_dir = Path(os.environ.get("APPDATA", "")) / "Cursor"
+ if not cursor_dir.exists():
+ self.log("ℹ️ 未找到 Cursor 缓存目录,无需清理。")
+ QMessageBox.information(self, "提示", "未找到 Cursor 缓存目录,无需清理。")
+ return
+
+ try:
+ shutil.rmtree(cursor_dir)
+ self.log("✅ Cursor 缓存清除成功")
+ QMessageBox.information(self, "完成", "Cursor 缓存已清除成功。")
+ return
+ except PermissionError:
+ self.log("⚠️ 权限不足,尝试请求管理员权限删除缓存...")
+ except Exception as e:
+ self.log(f"❌ 删除缓存失败: {e}")
+ QMessageBox.warning(self, "失败", f"删除缓存失败:{e}")
+ return
+
+ if sys.platform != "win32":
+ QMessageBox.warning(
+ self,
+ "失败",
+ "权限不足,无法删除缓存目录。请手动使用管理员权限删除。",
+ )
+ return
+
+ # 提权删除:弹 UAC 运行 PowerShell 删除 APPDATA 下的 Cursor 目录。
+ ps_cmd = (
+ "Remove-Item -LiteralPath \"$env:APPDATA\\Cursor\" -Recurse -Force "
+ "-ErrorAction Stop"
+ )
+ rc = ctypes.windll.shell32.ShellExecuteW(
+ None,
+ "runas",
+ "powershell.exe",
+ f"-NoProfile -ExecutionPolicy Bypass -Command \"{ps_cmd}\"",
+ None,
+ 1,
+ )
+ if rc <= 32:
+ self.log("❌ 提权删除未启动,请手动以管理员身份操作。")
+ QMessageBox.warning(
+ self,
+ "权限不足",
+ "无法自动提权。\n\n"
+ "请手动执行:\n"
+ "1. 关闭 Cursor\n"
+ "2. 右键 PowerShell 选择“以管理员身份运行”\n"
+ "3. 执行命令:Remove-Item -LiteralPath \"$env:APPDATA\\Cursor\" -Recurse -Force",
+ )
+ return
+
+ self.log("ℹ️ 已发起管理员删除请求,请在 UAC 窗口中确认。")
+ QMessageBox.information(
+ self,
+ "已请求管理员权限",
+ "已发起管理员权限删除请求。\n请在弹出的 UAC 窗口中确认后完成清理。",
+ )
+
+ @Slot(str)
+ def _show_usage_result(self, msg):
+ QMessageBox.information(self, "额度查询结果", msg)
+
+ def closeEvent(self, event):
+ self.stop_cursor_network_monitor(silent=True)
+ super().closeEvent(event)
+
+ @Slot(bool, str)
+ def on_change_finished(self, success, message):
+ if self.btnChange:
+ self.btnChange.setEnabled(True)
+ self.btnChange.setText("🚀 开始换号")
+
+ if success:
+ self.backup_path = message
+ self.log("✅ 换号完成")
+ self._show_change_success_countdown_dialog(4)
+ self.log("🚀 正在打开Cursor...")
+ cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
+ self._launch_cursor(cursor_path)
+ else:
+ QMessageBox.critical(self, "失败", message)
+
+
+def main():
+ app = QApplication(sys.argv)
+ app_icon = get_app_icon()
+ if not app_icon.isNull():
+ app.setWindowIcon(app_icon)
+
+ splash = _create_startup_splash(app)
+ if not app_icon.isNull():
+ splash.setWindowIcon(app_icon)
+ splash.show()
+ app.processEvents()
+
+ window = MainWindow(splash=splash)
+ if not app_icon.isNull():
+ window.setWindowIcon(app_icon)
+ window.show()
+ sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file