diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index 47af507..3d647d5 100644 Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ diff --git a/layout/main1.ui b/layout/main1.ui new file mode 100644 index 0000000..0c4e115 --- /dev/null +++ b/layout/main1.ui @@ -0,0 +1,991 @@ + + + MainWindow + + + + 0 + 0 + 900 + 650 + + + + 牛马Cursor登录器 + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Qt::Orientation::Vertical + + + false + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Horizontal + + + false + + + + + 150 + 0 + + + + + 220 + 16777215 + + + + QListWidget { + border: 1px solid palette(mid); + border-radius: 4px; + background: palette(base); + color: palette(text); + outline: none; + } + QListWidget::item { + min-height: 38px; + padding: 8px 12px; + border-bottom: 1px solid palette(midlight); + } + QListWidget::item:selected { + color: palette(highlighted-text); + background: palette(highlight); + } + QListWidget::item:hover:!selected { + background: palette(alternate-base); + } + + + 0 + + + + Token切换 + + + + + 一键续杯 + + + + + Cursor 配置 + + + + + 帮助 + + + + + + 0 + + + + + + + Token 输入 + + + + + + 请粘贴 Cursor Token 到这里... + + + + + + + + + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + + 12 + true + + + + QPushButton { + background-color: #0078D4; + color: white; + border-radius: 5px; + } + QPushButton:hover { + background-color: #106EBE; + } + QPushButton:pressed { + background-color: #005A9E; + } + + + 🚀 开始换号 + + + + + + + + 160 + 50 + + + + + 220 + 50 + + + + + 12 + true + + + + QPushButton { + background-color: #FF9800; + color: white; + border-radius: 5px; + } + QPushButton:hover { + background-color: #F57C00; + } + QPushButton:pressed { + background-color: #E65100; + } + + + 在线购买Token + + + + + + + + + + + + + 设备号 + + + + + + true + + + 设备号将在程序启动后自动生成... + + + + + + + + 88 + 0 + + + + 复制 + + + + + + + + + + 激活码 + + + + + + 激活码: + + + + + + + 请输入激活码... + + + + + + + + 88 + 0 + + + + 激活 + + + + + + + + 88 + 0 + + + + 在线购买 + + + + + + + + + + 会员状态 + + + + + + + + 当前状态:未刷新 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 88 + 0 + + + + 刷新 + + + + + + + + + + + 会员等级: + + + + + + + - + + + + + + + 账号类型: + + + + + + + - + + + + + + + 激活时间: + + + + + + + - + + + + + + + 到期时间: + + + + + + + - + + + + + + + + + + + + + + + 444 + 48 + + + + + 16777215 + 48 + + + + + 12 + true + + + + QPushButton { + background-color: #D83B01; + color: white; + border-radius: 5px; + } + QPushButton:hover { + background-color: #C23900; + } + QPushButton:pressed { + background-color: #A4262C; + } + + + 一键续杯 + + + + + + + + + color: #D83B01; font-weight: bold; + + + 请用完后再点击续杯,如发现大量未使用完就续杯情况,一律封号处理,请悉知! + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + Cursor 路径 + + + + + + 路径: + + + + + + + 请选择 Cursor 安装路径... + + + + + + + 浏览 + + + + + + + + 88 + 0 + + + + 自动查找 + + + + + + + + + + + 0 + 40 + + + + 打开 Cursor + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 140 + 0 + + + + + 190 + 16777215 + + + + QListWidget { + border: 1px solid palette(mid); + border-radius: 4px; + background: palette(base); + color: palette(text); + outline: none; + } + QListWidget::item { + min-height: 34px; + padding: 8px 12px; + border-bottom: 1px solid palette(midlight); + } + QListWidget::item:selected { + color: palette(highlighted-text); + background: palette(highlight); + } + QListWidget::item:hover:!selected { + background: palette(alternate-base); + } + + + 0 + + + + 应急检修 + + + + + 使用说明 + + + + + 捐赠支持 + + + + + 关于软件 + + + + + + + + 0 + + + + + + + 应急检修 + + + + + + 打开应急检修工具面板,可进行 DB Browser 下载、清除 Cursor 缓存、Token 提取等操作。 + + + true + + + + + + + + 0 + 42 + + + + 打开应急检修 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + 使用说明 + + + + + + 查看软件使用说明图片,支持滚轮查看和缩放。 + + + true + + + + + + + + 0 + 42 + + + + 打开使用说明 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + 捐赠支持 + + + + + + 打开捐赠支持窗口,查看微信和支付宝捐赠二维码。 + + + true + + + + + + + + 0 + 42 + + + + 打开捐赠支持 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + 关于软件 + + + + + + 查看当前软件版本、用途和交流群信息。 + + + true + + + + + + + + 0 + 42 + + + + 查看关于软件 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + + 0 + 140 + + + + 日志展示 + + + + + + true + + + 日志输出... + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 88 + 0 + + + + 检查更新 + + + + + + + + 88 + 0 + + + + 清空日志 + + + + + + + + + + + + + + + 0 + 0 + 900 + 33 + + + + + 设置 + + + + + + + + true + + + + + 退出 + + + + + 应急检修 + + + + + 使用说明 + + + + + 捐赠支持 + + + + + 关于软件 + + + + + + + tabList + currentRowChanged(int) + contentStack + setCurrentIndex(int) + + + 93 + 120 + + + 420 + 120 + + + + + helpTabList + currentRowChanged(int) + helpContentStack + setCurrentIndex(int) + + + 220 + 180 + + + 520 + 180 + + + + + \ No newline at end of file diff --git a/main.py b/main.py index 151e385..d759d5e 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import sys import json import base64 +import hashlib import shutil import uuid import random @@ -20,7 +21,7 @@ from datetime import datetime from pathlib import Path from typing import Optional -__VERSION__ = "0.0.6" +__VERSION__ = "0.0.7" from PySide6.QtWidgets import ( QApplication, @@ -42,7 +43,7 @@ from PySide6.QtWidgets import ( ) 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 +from PySide6.QtGui import QFont, QPixmap, QColor, QPainter, QPalette, QIcon, QAction def get_resource_path(relative_path): @@ -147,6 +148,104 @@ def generate_machine_id(): return str(uuid.uuid4()) +def _run_command_text(args): + """执行系统命令并返回文本输出,失败时返回空字符串。""" + try: + creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) if sys.platform == "win32" else 0 + return subprocess.check_output( + args, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + text=True, + encoding="utf-8", + errors="ignore", + creationflags=creationflags, + ).strip() + except Exception: + return "" + + +def _get_wmic_value(alias: str, field: str) -> str: + """通过 WMIC 获取硬件字段值,兼容空值/表头输出。""" + output = _run_command_text(["wmic", alias, "get", field, "/value"]) + for line in output.splitlines(): + line = line.strip() + prefix = f"{field}=" + if line.startswith(prefix): + value = line[len(prefix):].strip() + if value and value.lower() not in ("none", "null", "to be filled by o.e.m."): + return value + + output = _run_command_text(["wmic", alias, "get", field]) + lines = [line.strip() for line in output.splitlines() if line.strip()] + for line in lines[1:]: + if line and line.lower() not in ("none", "null", "to be filled by o.e.m."): + return line + return "" + + +def _get_windows_cim_value(class_name: str, field: str) -> str: + """WMIC 不可用时,通过 PowerShell CIM 获取硬件字段值。""" + command = ( + f"(Get-CimInstance {class_name} | " + f"Select-Object -First 1 -ExpandProperty {field})" + ) + value = _run_command_text([ + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + command, + ]).strip() + if value and value.lower() not in ("none", "null", "to be filled by o.e.m."): + return value + return "" + + +def _get_cpu_id() -> str: + """获取 CPU 标识。""" + if sys.platform == "win32": + return ( + _get_wmic_value("cpu", "ProcessorId") + or _get_windows_cim_value("Win32_Processor", "ProcessorId") + or os.environ.get("PROCESSOR_IDENTIFIER", "").strip() + ) + return os.environ.get("PROCESSOR_IDENTIFIER", "").strip() or os.uname().machine + + +def _get_baseboard_id() -> str: + """获取主板标识。""" + if sys.platform == "win32": + serial = ( + _get_wmic_value("baseboard", "SerialNumber") + or _get_windows_cim_value("Win32_BaseBoard", "SerialNumber") + ) + product = ( + _get_wmic_value("baseboard", "Product") + or _get_windows_cim_value("Win32_BaseBoard", "Product") + ) + manufacturer = ( + _get_wmic_value("baseboard", "Manufacturer") + or _get_windows_cim_value("Win32_BaseBoard", "Manufacturer") + ) + return "|".join(part for part in (manufacturer, product, serial) if part) + return socket.gethostname() + + +def get_renewal_device_id(): + """生成用于一键续杯的本机设备号:CPU + MAC + 主板。""" + try: + cpu_id = _get_cpu_id() + mac = f"{uuid.getnode():012x}" + baseboard_id = _get_baseboard_id() + raw = f"cpu={cpu_id}|mac={mac}|baseboard={baseboard_id}" + digest = hashlib.sha256(raw.encode("utf-8")).digest() + return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=") + except Exception: + return str(uuid.uuid4()) + + def get_cursor_config_path(): home = Path.home() if sys.platform == "win32": @@ -727,7 +826,7 @@ class MainWindow(QMainWindow): self._splash_pulse("正在加载") # 获取UI文件路径 - ui_path = get_resource_path(os.path.join("layout", "main.ui")) + ui_path = get_resource_path(os.path.join("layout", "main1.ui")) # 详细的调试信息 debug_info = f"UI文件路径: {ui_path}\n" @@ -802,6 +901,8 @@ class MainWindow(QMainWindow): self.txtToken = self.findChild(QTextEdit, "txtToken") self.txtLog = self.findChild(QTextEdit, "txtLog") self.txtCursorPath = self.findChild(QLineEdit, "txtCursorPath") + self.txtDeviceId = self.findChild(QLineEdit, "txtDeviceId") + self.txtActivationCode = self.findChild(QLineEdit, "txtActivationCode") self.btnChange = self.findChild(QPushButton, "btnChange") self.btnOpenCursor = self.findChild(QPushButton, "btnOpenCursor") self.btnOnlineShop = self.findChild(QPushButton, "btnOnlineShop") @@ -813,6 +914,22 @@ class MainWindow(QMainWindow): self.btnCheckUpdate = self.findChild(QPushButton, "btnCheckUpdate") self.btnEmergencyRepair = self.findChild(QPushButton, "btnEmergencyRepair") self.btnUsageGuide = self.findChild(QPushButton, "btnUsageGuide") + self.btnAbout = self.findChild(QPushButton, "btnAbout") + self.btnCopyDeviceId = self.findChild(QPushButton, "btnCopyDeviceId") + self.btnActivateRenewal = self.findChild(QPushButton, "btnActivateRenewal") + self.btnBuyActivationCode = self.findChild(QPushButton, "btnBuyActivationCode") + self.btnRefreshMemberStatus = self.findChild(QPushButton, "btnRefreshMemberStatus") + self.btnOneClickRenewal = self.findChild(QPushButton, "btnOneClickRenewal") + self.lblMemberStatus = self.findChild(QLabel, "lblMemberStatus") + self.lblMemberLevel = self.findChild(QLabel, "lblMemberLevel") + self.lblAccountType = self.findChild(QLabel, "lblAccountType") + self.lblActivatedAt = self.findChild(QLabel, "lblActivatedAt") + self.lblExpiredAt = self.findChild(QLabel, "lblExpiredAt") + self.actionExit = self.findChild(QAction, "actionExit") + self.actionEmergencyRepair = self.findChild(QAction, "actionEmergencyRepair") + self.actionUsageGuide = self.findChild(QAction, "actionUsageGuide") + self.actionDonate = self.findChild(QAction, "actionDonate") + self.actionAbout = self.findChild(QAction, "actionAbout") self._load_cached_logs_to_ui() # 调试信息 @@ -829,15 +946,21 @@ class MainWindow(QMainWindow): # 设置默认Cursor路径 if self.txtCursorPath: self.txtCursorPath.setText(get_default_cursor_path()) + if self.txtDeviceId: + self.txtDeviceId.setText(get_renewal_device_id()) + self._reset_member_status_display() # 为在线商城按钮设置图标(使用 Qt 内置图标,避免依赖外部资源) - if self.btnOnlineShop: + if self.btnOnlineShop or self.btnBuyActivationCode: shop_icon = QIcon.fromTheme("shopping-cart") if shop_icon.isNull(): shop_icon = QIcon.fromTheme("emblem-sales") if shop_icon.isNull(): shop_icon = self.style().standardIcon(QStyle.SP_DialogOpenButton) - self.btnOnlineShop.setIcon(shop_icon) + if self.btnOnlineShop: + self.btnOnlineShop.setIcon(shop_icon) + if self.btnBuyActivationCode: + self.btnBuyActivationCode.setIcon(shop_icon) # 信号连接 if self.btnChange: @@ -862,6 +985,28 @@ class MainWindow(QMainWindow): self.btnEmergencyRepair.clicked.connect(self.on_emergency_repair_clicked) if self.btnUsageGuide: self.btnUsageGuide.clicked.connect(self.on_usage_guide_clicked) + if self.btnAbout: + self.btnAbout.clicked.connect(self.on_about_clicked) + if self.btnCopyDeviceId: + self.btnCopyDeviceId.clicked.connect(self.on_copy_device_id_clicked) + if self.btnActivateRenewal: + self.btnActivateRenewal.clicked.connect(self.on_activate_renewal_clicked) + if self.btnBuyActivationCode: + self.btnBuyActivationCode.clicked.connect(self.on_open_online_shop_clicked) + if self.btnRefreshMemberStatus: + self.btnRefreshMemberStatus.clicked.connect(self.on_refresh_member_status_clicked) + if self.btnOneClickRenewal: + self.btnOneClickRenewal.clicked.connect(self.on_one_click_renewal_clicked) + if self.actionExit: + self.actionExit.triggered.connect(self.close) + if self.actionEmergencyRepair: + self.actionEmergencyRepair.triggered.connect(self.on_emergency_repair_clicked) + if self.actionUsageGuide: + self.actionUsageGuide.triggered.connect(self.on_usage_guide_clicked) + if self.actionDonate: + self.actionDonate.triggered.connect(self.on_donate_clicked) + if self.actionAbout: + self.actionAbout.triggered.connect(self.on_about_clicked) # 设置状态栏:左侧显示QQ群,右侧显示版本号 qq_icon_label = QLabel() qq_icon = QIcon.fromTheme("im-qq") @@ -1232,6 +1377,86 @@ class MainWindow(QMainWindow): self.log(f"❌ 打开AI中转站失败: {str(e)}") QMessageBox.warning(self, "警告", f"打开AI中转站失败: {str(e)}") + def on_copy_device_id_clicked(self): + """复制一键续杯设备号。""" + device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else "" + if not device_id: + QMessageBox.warning(self, "提示", "设备号为空,无法复制。") + return + QApplication.clipboard().setText(device_id) + self.log("✅ 设备号已复制到剪贴板") + QMessageBox.information(self, "成功", "设备号已复制到剪贴板。") + + def on_activate_renewal_clicked(self): + """提交一键续杯激活码。""" + activation_code = self.txtActivationCode.text().strip() if self.txtActivationCode else "" + device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else "" + if not activation_code: + QMessageBox.warning(self, "提示", "请输入激活码。") + return + self.log(f"🔑 正在激活一键续杯,设备号:{device_id}") + self.log("ℹ️ 激活码已提交,请根据服务端接口补充实际激活逻辑。") + QMessageBox.information(self, "提示", "激活码已提交,当前版本尚未配置服务端激活接口。") + self.on_refresh_member_status_clicked() + + def _reset_member_status_display(self): + """重置会员状态展示。""" + if self.lblMemberStatus: + self.lblMemberStatus.setText("当前状态:未刷新") + if self.lblMemberLevel: + self.lblMemberLevel.setText("-") + if self.lblAccountType: + self.lblAccountType.setText("-") + if self.lblActivatedAt: + self.lblActivatedAt.setText("-") + if self.lblExpiredAt: + self.lblExpiredAt.setText("-") + + def on_refresh_member_status_clicked(self): + """刷新会员状态展示。""" + device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else "" + if not device_id: + QMessageBox.warning(self, "提示", "设备号为空,无法刷新会员状态。") + return + + if self.lblMemberStatus: + self.lblMemberStatus.setText("当前状态:待接入服务端") + if self.lblMemberLevel: + self.lblMemberLevel.setText("未激活") + if self.lblAccountType: + self.lblAccountType.setText("普通账号") + if self.lblActivatedAt: + self.lblActivatedAt.setText("-") + if self.lblExpiredAt: + self.lblExpiredAt.setText("-") + + self.log("🔄 已刷新会员状态展示,当前版本尚未配置服务端查询接口。") + + def on_one_click_renewal_clicked(self): + """执行一键续杯。""" + device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else "" + if not device_id: + QMessageBox.warning(self, "提示", "设备号为空,无法执行一键续杯。") + return + + confirm = QMessageBox.question( + self, + "确认一键续杯", + "请确认当前额度已经用完后再续杯。\n\n" + "如发现大量未使用完就续杯情况,一律封号处理,请悉知!\n\n" + "确定要继续执行一键续杯吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + if confirm != QMessageBox.Yes: + self.log("ℹ️ 已取消一键续杯。") + return + + self.log(f"☕ 正在执行一键续杯,设备号:{device_id}") + self.log("ℹ️ 一键续杯请求已提交,请根据服务端接口补充实际续杯逻辑。") + QMessageBox.information(self, "提示", "一键续杯请求已提交,当前版本尚未配置服务端续杯接口。") + self.on_refresh_member_status_clicked() + def _extract_session_token(self, raw_value: str) -> str: """从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。""" s = (raw_value or "").strip().strip('"').strip("'") @@ -1344,6 +1569,17 @@ class MainWindow(QMainWindow): dialog = DonateDialog(self) dialog.exec() + def on_about_clicked(self): + """显示关于软件信息。""" + QMessageBox.about( + self, + "关于软件", + f"牛马Cursor登录器\n\n" + f"当前版本:{__VERSION__}\n\n" + "用于 Cursor 账号登录、配置管理和常用工具操作。\n\n" + "QQ群:720797421" + ) + def on_usage_guide_clicked(self): """打开使用说明图片(非模态,可与其他窗口并行)。""" if self._usage_guide_dialog and self._usage_guide_dialog.isVisible(): @@ -1735,4 +1971,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()