批量更新

This commit is contained in:
扫地僧 2026-04-27 17:24:17 +08:00
parent 655d40760d
commit dbd28458b8
6 changed files with 2716 additions and 88 deletions

View File

@ -4,3 +4,10 @@ python -m PyInstaller -w -F main.py
# 编译成exe
python -m PyInstaller build.spec --clean
<br />
# 关于 Win + Mac 同时构建(后续)
- PyInstaller 不能在 Windows 本机直接产出 macOS 程序。
- 当前命令在 Windows 只能生成 `.exe`,在 macOS 才能生成 `.app`
- 后续可用 GitHub Actions 做双平台构建:本地打 Windows云端 `macos-latest` 打 macOS。

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

View File

@ -55,6 +55,38 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnAutoCursorPath">
<property name="minimumSize">
<size>
<width>88</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>自动查找</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnOpenCursor">
<property name="minimumSize">
<size>
<width>110</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>110</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>打开Cursor</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -110,6 +142,12 @@
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
@ -128,21 +166,40 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="btnQueryUsage">
<widget class="QPushButton" name="btnOnlineShop">
<property name="minimumSize">
<size>
<width>90</width>
<width>110</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>90</width>
<width>110</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>📊 查询额度(暂时无用)</string>
<string>在线商城</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnAiRelay">
<property name="minimumSize">
<size>
<width>110</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>110</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>AI中转站</string>
</property>
</widget>
</item>
@ -166,7 +223,17 @@
<item>
<widget class="QPushButton" name="btnEmergencyRepair">
<property name="text">
<string>🔧 应急检修,用户勿点</string>
<string>🔧 应急检修</string>
</property>
<property name="minimumHeight">
<number>30</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnUsageGuide">
<property name="text">
<string>📖 使用说明</string>
</property>
<property name="minimumHeight">
<number>30</number>

708
main.py
View File

@ -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)

2012
main_backup.py Normal file

File diff suppressed because it is too large Load Diff