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