first commit
This commit is contained in:
parent
e30c74007b
commit
84cc672823
189
.github/workflows/build.yml
vendored
Normal file
189
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
name: Build Multi-Platform Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # 当推送形如 v1.0.0 的 tag 时触发构建
|
||||
workflow_dispatch: # 支持手动触发构建
|
||||
|
||||
jobs:
|
||||
# 1. 编译 Windows 端 (.exe)
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build EXE
|
||||
run: |
|
||||
python -m PyInstaller build.spec --clean
|
||||
|
||||
- name: Upload Windows Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-Windows
|
||||
path: dist/CursorTokenLogin.exe
|
||||
|
||||
# 2. 编译 macOS 端 (.app)
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build App Bundle
|
||||
run: |
|
||||
python -m PyInstaller build.spec --clean
|
||||
|
||||
- name: Zip macOS App
|
||||
run: |
|
||||
# macOS app 实质上是文件夹,发布前需要打包成 zip
|
||||
cd dist
|
||||
zip -r CursorTokenLogin-macOS.zip CursorTokenLogin.app
|
||||
|
||||
- name: Upload macOS Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-macOS
|
||||
path: dist/CursorTokenLogin-macOS.zip
|
||||
|
||||
# 3. 编译 Linux 端并打包为 .deb 和 .rpm
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
# 安装打包 deb 和 rpm 所需的系统工具
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dpkg rpm alien
|
||||
|
||||
- name: Build Linux Binary
|
||||
run: |
|
||||
python -m PyInstaller build.spec --clean
|
||||
|
||||
- name: Pack DEB Package
|
||||
run: |
|
||||
# 创建 deb 包目录结构
|
||||
mkdir -p deb-package/DEBIAN
|
||||
mkdir -p deb-package/usr/bin
|
||||
mkdir -p deb-package/usr/share/applications
|
||||
mkdir -p deb-package/usr/share/pixmaps
|
||||
|
||||
# 拷贝编译好的二进制文件
|
||||
cp dist/CursorTokenLogin deb-package/usr/bin/cursortokenlogin
|
||||
chmod +x deb-package/usr/bin/cursortokenlogin
|
||||
|
||||
# 拷贝图标
|
||||
cp logo.png deb-package/usr/share/pixmaps/cursortokenlogin.png
|
||||
|
||||
# 创建控制文件
|
||||
cat <<EOF > deb-package/DEBIAN/control
|
||||
Package: cursortokenlogin
|
||||
Version: 1.0.0
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: Yunzer
|
||||
Description: Cursor Token Login Helper
|
||||
EOF
|
||||
|
||||
# 创建桌面启动快捷方式
|
||||
cat <<EOF > deb-package/usr/share/applications/cursortokenlogin.desktop
|
||||
[Desktop Entry]
|
||||
Name=CursorTokenLogin
|
||||
Comment=Cursor Token Login Helper
|
||||
Exec=/usr/bin/cursortokenlogin
|
||||
Icon=cursortokenlogin
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Development;
|
||||
EOF
|
||||
|
||||
# 构建 deb
|
||||
dpkg-deb --build deb-package cursortokenlogin.deb
|
||||
|
||||
- name: Pack RPM Package (Convert from DEB using alien)
|
||||
run: |
|
||||
# 使用 alien 工具将 deb 快速转换为 rpm,避免编写复杂的 spec 构建脚本
|
||||
sudo alien --to-rpm --scripts cursortokenlogin.deb
|
||||
# 重命名生成的 rpm 文件方便识别
|
||||
mv *.rpm cursortokenlogin.rpm
|
||||
|
||||
- name: Upload Linux Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-Linux
|
||||
path: |
|
||||
cursortokenlogin.deb
|
||||
cursortokenlogin.rpm
|
||||
|
||||
# 4. 自动创建 GitHub Release 并上传 4 个包
|
||||
create-release:
|
||||
needs: [build-windows, build-macos, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') # 仅在推送 tag 时执行 Release 发布
|
||||
steps:
|
||||
- name: Download Windows Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-Windows
|
||||
path: ./release-files
|
||||
|
||||
- name: Download macOS Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-macOS
|
||||
path: ./release-files
|
||||
|
||||
- name: Download Linux Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: CursorTokenLogin-Linux
|
||||
path: ./release-files
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
release-files/CursorTokenLogin.exe
|
||||
release-files/CursorTokenLogin-macOS.zip
|
||||
release-files/cursortokenlogin.deb
|
||||
release-files/cursortokenlogin.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
77
README.md
77
README.md
@ -1,13 +1,74 @@
|
||||
python -m PyInstaller -w -F main.py
|
||||
# 多平台编译与打包指南
|
||||
|
||||
<br />
|
||||
在 Windows 系统下,由于 PyInstaller 的工作机制是直接打包当前平台的 Python 解析器和系统动态库,**它不支持交叉编译**(即在 Windows 本机无法编译出 macOS 的 `.app` 苹果程序,也无法直接打包 Linux 的 `.deb` 或 `.rpm` 安装包)。
|
||||
|
||||
# 编译成exe
|
||||
为了解决这个问题,并在一台电脑上实现 4 端程序(Windows `.exe`、macOS `.app`、Debian/Ubuntu `.deb`、RedHat/Fedora `.rpm`)的统一打包,我们已经在项目中配置了 **GitHub Actions 自动化流水线**。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 推荐方案:使用 GitHub Actions 自动打包 4 端程序
|
||||
|
||||
我们已经在项目中创建了自动化打包脚本:[.github/workflows/build.yml](file:///.github/workflows/build.yml)。
|
||||
|
||||
您只需将代码托管在 GitHub 仓库中,GitHub 就会为您调用免费的 Windows、macOS 和 Linux 虚拟服务器,一键自动构建出所有安装包!
|
||||
|
||||
### 操作步骤:
|
||||
1. **将代码推送到您的 GitHub 仓库**。
|
||||
2. 当需要发布新版本时,在本地打上版本 tag 并推送:
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
3. GitHub Actions 会自动触发构建任务,并行启动三个环境:
|
||||
- 💻 **Windows**:编译出 `CursorTokenLogin.exe`
|
||||
- 🍏 **macOS**:编译出 `CursorTokenLogin-macOS.zip` (内含 `.app` 程序)
|
||||
- 🐧 **Linux**:编译出 Linux 二进制,并分别打包为 `cursortokenlogin.deb` 和 `cursortokenlogin.rpm`
|
||||
4. 构建完成后,GitHub 会自动创建一个 Release 版本,并将这 4 个编译好的安装包上传到 Release 中,您可以直接下载分发!
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 本地手动构建说明(Windows/Linux/Mac)
|
||||
|
||||
如果您不想使用 GitHub 托管,也可以在本地不同的系统环境中分别运行打包:
|
||||
|
||||
### 1. 编译 Windows 程序 (`.exe`)
|
||||
在 Windows 命令行中运行:
|
||||
```bash
|
||||
python -m PyInstaller build.spec --clean
|
||||
```
|
||||
|
||||
### 2. 编译 macOS 程序 (`.app`)
|
||||
在 macOS 终端中运行:
|
||||
```bash
|
||||
python -m PyInstaller build.spec --clean
|
||||
```
|
||||
*(编译生成 `.app` 文件目录,拷贝分发前建议压缩为 `.zip`)*
|
||||
|
||||
### 3. 编译 Linux 安装包 (`.deb` / `.rpm`)
|
||||
在 Ubuntu / Debian 终端下,可以使用以下命令快速打包:
|
||||
```bash
|
||||
# 1. 编译 Linux 原生可执行文件
|
||||
python -m PyInstaller build.spec --clean
|
||||
|
||||
<br />
|
||||
# 2. 组织 DEB 包目录结构并打包
|
||||
mkdir -p deb-package/DEBIAN
|
||||
mkdir -p deb-package/usr/bin
|
||||
cp dist/CursorTokenLogin deb-package/usr/bin/cursortokenlogin
|
||||
chmod +x deb-package/usr/bin/cursortokenlogin
|
||||
|
||||
# 关于 Win + Mac 同时构建(后续)
|
||||
- PyInstaller 不能在 Windows 本机直接产出 macOS 程序。
|
||||
- 当前命令在 Windows 只能生成 `.exe`,在 macOS 才能生成 `.app`。
|
||||
- 后续可用 GitHub Actions 做双平台构建:本地打 Windows,云端 `macos-latest` 打 macOS。
|
||||
# 创建 control 配置文件
|
||||
cat <<EOF > deb-package/DEBIAN/control
|
||||
Package: cursortokenlogin
|
||||
Version: 1.0.0
|
||||
Architecture: amd64
|
||||
Maintainer: Yunzer
|
||||
Description: Cursor Token Login Helper
|
||||
EOF
|
||||
|
||||
# 生成 deb 安装包
|
||||
dpkg-deb --build deb-package cursortokenlogin.deb
|
||||
|
||||
# 3. 使用 alien 将 deb 包转换为 rpm 包
|
||||
sudo apt-get install alien
|
||||
sudo alien --to-rpm --scripts cursortokenlogin.deb
|
||||
```
|
||||
|
||||
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
58
_inspect_state_vscdb.py
Normal file
58
_inspect_state_vscdb.py
Normal file
@ -0,0 +1,58 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
p = Path("state.vscdb")
|
||||
print("db_exists=", p.exists(), p.resolve())
|
||||
|
||||
conn = sqlite3.connect(p)
|
||||
cur = conn.cursor()
|
||||
|
||||
tables = cur.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||||
).fetchall()
|
||||
print("tables=", tables)
|
||||
|
||||
try:
|
||||
schema = cur.execute("PRAGMA table_info(ItemTable)").fetchall()
|
||||
print("ItemTable schema=", schema)
|
||||
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT key, value
|
||||
FROM ItemTable
|
||||
WHERE lower(key) LIKE '%auth%'
|
||||
OR lower(key) LIKE '%token%'
|
||||
OR lower(key) LIKE '%cursoraccount%'
|
||||
OR lower(key) LIKE '%cursor%'
|
||||
ORDER BY key
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
print("matched_rows=", len(rows))
|
||||
for k, v in rows:
|
||||
s = "" if v is None else str(v)
|
||||
masked = (s[:18] + "..." + s[-12:]) if len(s) > 40 else s
|
||||
print(f"{k} len={len(s)} value={masked}")
|
||||
|
||||
print("\ncursorDiskKV schema=", cur.execute("PRAGMA table_info(cursorDiskKV)").fetchall())
|
||||
disk_rows = cur.execute(
|
||||
"""
|
||||
SELECT key, value
|
||||
FROM cursorDiskKV
|
||||
WHERE lower(key) LIKE '%auth%'
|
||||
OR lower(key) LIKE '%token%'
|
||||
OR lower(key) LIKE '%cursor%'
|
||||
OR lower(key) LIKE '%account%'
|
||||
ORDER BY key
|
||||
"""
|
||||
).fetchall()
|
||||
print("cursorDiskKV matched_rows=", len(disk_rows))
|
||||
for k, v in disk_rows:
|
||||
if isinstance(v, bytes):
|
||||
s = v.decode("utf-8", "ignore")
|
||||
else:
|
||||
s = "" if v is None else str(v)
|
||||
masked = (s[:18] + "..." + s[-12:]) if len(s) > 40 else s
|
||||
print(f"{k} len={len(s)} value={masked}")
|
||||
finally:
|
||||
conn.close()
|
||||
@ -66,3 +66,12 @@ exe = EXE(
|
||||
# 必须用绝对路径;资源管理器若仍显示旧图标多半是 Windows 图标缓存,可改 exe 文件名或清 IconCache
|
||||
icon=_LOGO_ICO_ABS,
|
||||
)
|
||||
|
||||
import sys
|
||||
if sys.platform == 'darwin':
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='CursorTokenLogin.app',
|
||||
icon='logo.png',
|
||||
bundle_identifier='com.yunzer.cursortokenlogin',
|
||||
)
|
||||
|
||||
@ -623,6 +623,11 @@
|
||||
<string>关于软件</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>无感检测</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -826,6 +831,69 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pageHelpSilentDetect">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_helpSilentDetect">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupHelpSilentDetect">
|
||||
<property name="title">
|
||||
<string>无感检测</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_helpSilentDetectContent">
|
||||
<item>
|
||||
<widget class="QLabel" name="lblHelpSilentDetectDesc">
|
||||
<property name="text">
|
||||
<string>无感检测:不关闭 Cursor 程序,直接写入 Token 和自动生成的随机邮箱地址,方便快速检测 Token 是否可用。</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblSilentToken">
|
||||
<property name="text">
|
||||
<string>请输入新 Token (兼容纯 token / Cookie 串):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="txtSilentToken">
|
||||
<property name="placeholderText">
|
||||
<string>在此粘贴 Token,格式支持 Workos... 或原始 token</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnSilentChange">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>42</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>无感换号</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_helpSilentDetect">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
582
main.py
582
main.py
@ -265,7 +265,7 @@ def get_cursor_config_path():
|
||||
|
||||
|
||||
def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
||||
"""更新 Cursor 数据库中的token和email"""
|
||||
"""更新 Cursor 数据库中的 token / email / auth 缓存字段。"""
|
||||
db_path = config_dir / "state.vscdb"
|
||||
if not db_path.exists():
|
||||
log_callback("⚠️ 未找到 Cursor库 文件,跳过")
|
||||
@ -281,14 +281,27 @@ def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
||||
shutil.copy2(db_path, db_backup)
|
||||
# log_callback(f"📁 数据库已备份到: {db_backup}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
# 使用 timeout=5 避免瞬间锁导致失败
|
||||
conn = sqlite3.connect(db_path, timeout=5.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 更新 token 和 email
|
||||
# 强制合并并清除 WAL 缓存(切换为 DELETE 模式,这样会自动合并并删除 -wal 和 -shm 文件)
|
||||
try:
|
||||
cursor.execute("PRAGMA journal_mode=delete")
|
||||
except Exception as wal_err:
|
||||
log_callback(f"⚠️ 无法合并 WAL 缓存 (可能是 Cursor 进程未完全关闭): {wal_err}")
|
||||
|
||||
# 当前 Cursor 版本主要从 ItemTable 的 cursorAuth/* 读取登录态。
|
||||
# 不要只写 accessToken/refreshToken;补齐已登录库里常见的认证缓存字段,
|
||||
# 避免客户端启动时因为缓存状态不完整回落到 Log in。
|
||||
onboarding_date = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||||
updates = [
|
||||
("cursorAuth/accessToken", new_token),
|
||||
("cursorAuth/refreshToken", new_token),
|
||||
("cursorAuth/cachedEmail", new_email),
|
||||
("cursorAuth/cachedSignUpType", "Auth_0"),
|
||||
("cursorAuth/onboardingDate", onboarding_date),
|
||||
("cursorAuth/stripeMembershipType", "pro"),
|
||||
]
|
||||
|
||||
for key, value in updates:
|
||||
@ -312,8 +325,16 @@ def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
||||
# log_callback(f"✓ 插入了 {key}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 恢复 WAL 模式以便 Cursor 正常工作
|
||||
try:
|
||||
cursor.execute("PRAGMA journal_mode=wal")
|
||||
conn.commit()
|
||||
except Exception as wal_err:
|
||||
log_callback(f"⚠️ 无法还原为 WAL 模式: {wal_err}")
|
||||
|
||||
conn.close()
|
||||
log_callback("✅ Cursor库 更新成功")
|
||||
log_callback(f"✅ Cursor库 更新成功,已同步 {len(updates)} 个认证字段")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -687,10 +708,16 @@ class ChangeTokenThread(QThread):
|
||||
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"]
|
||||
# 显示原邮箱。access token 本身通常不包含 email claim,
|
||||
# 因此优先保留 Cursor 原缓存邮箱,避免写入随机邮箱导致登录缓存状态不一致。
|
||||
old_email = (
|
||||
data.get("cursorAuth", {}).get("cachedEmail")
|
||||
or data.get("cursorAccount", {}).get("email")
|
||||
or ""
|
||||
)
|
||||
if old_email:
|
||||
self.log_signal.emit(f"📧 原邮箱: {old_email}")
|
||||
self.new_email = str(old_email).strip()
|
||||
|
||||
# 备份原文件(不显示日志)
|
||||
shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
|
||||
@ -702,6 +729,8 @@ class ChangeTokenThread(QThread):
|
||||
data["cursorAuth"]["accessToken"] = self.new_token
|
||||
data["cursorAuth"]["refreshToken"] = self.new_token
|
||||
data["cursorAuth"]["cachedEmail"] = self.new_email
|
||||
data["cursorAuth"]["cachedSignUpType"] = data["cursorAuth"].get("cachedSignUpType", "Auth_0")
|
||||
data["cursorAuth"]["onboardingDate"] = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||||
data["cursorAuth"]["plan"] = "pro"
|
||||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||||
data["cursorAuth"]["membershipType"] = "pro"
|
||||
@ -733,7 +762,11 @@ class ChangeTokenThread(QThread):
|
||||
|
||||
# 更新 Cursor 数据库
|
||||
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
||||
update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
|
||||
db_ok = update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
|
||||
if not db_ok:
|
||||
self.log_signal.emit("❌ 更新 Cursor 数据库失败,换号未完成!")
|
||||
self.finished_signal.emit(False, "更新 Cursor 数据库失败,可能是 Cursor 进程未完全关闭,请在任务管理器中结束所有 Cursor 进程,或尝试以管理员身份运行本软件。")
|
||||
return
|
||||
|
||||
self.log_signal.emit("✅ 换号完成!")
|
||||
self.finished_signal.emit(True, str(backup_subdir))
|
||||
@ -743,6 +776,312 @@ class ChangeTokenThread(QThread):
|
||||
self.finished_signal.emit(False, str(e))
|
||||
|
||||
|
||||
class OneClickRenewalThread(QThread):
|
||||
log_signal = Signal(str)
|
||||
finished_signal = Signal(bool, str, str) # (success, message/backup_path, new_email)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.log_signal.emit("🔄 正在从服务端获取账号...")
|
||||
url = "https://api.yunzer.cn/api/getcard?type=local&module=cursor&data_type=tk"
|
||||
|
||||
response_data = None
|
||||
try:
|
||||
response = requests.get(url, timeout=15)
|
||||
response_data = response.text.strip()
|
||||
except requests.exceptions.ProxyError:
|
||||
with requests.Session() as session:
|
||||
session.trust_env = False
|
||||
response = session.get(url, timeout=15)
|
||||
response_data = response.text.strip()
|
||||
|
||||
if not response_data or not response_data.startswith("ey"):
|
||||
try:
|
||||
err_json = json.loads(response_data)
|
||||
msg = err_json.get("msg") or err_json.get("message") or response_data
|
||||
except Exception:
|
||||
msg = response_data
|
||||
self.finished_signal.emit(False, f"获取账号 Token 失败: {msg[:200]}", "")
|
||||
return
|
||||
|
||||
self.log_signal.emit("✅ 成功提取 Token!正在执行本地更换...")
|
||||
new_token = response_data
|
||||
new_email = generate_random_email()
|
||||
|
||||
config_dir = get_cursor_config_path()
|
||||
if not config_dir.exists():
|
||||
self.finished_signal.emit(False, "未找到Cursor配置目录", "")
|
||||
return
|
||||
|
||||
backup_dir = config_dir / "backup"
|
||||
backup_dir.mkdir(exist_ok=True)
|
||||
timestamp = random.randint(100000, 999999)
|
||||
backup_subdir = backup_dir / f"backup_{timestamp}"
|
||||
backup_subdir.mkdir(exist_ok=True)
|
||||
|
||||
storage_file = config_dir / "storage.json"
|
||||
if not storage_file.exists():
|
||||
self.finished_signal.emit(False, "未找到 storage.json 文件", "")
|
||||
return
|
||||
|
||||
if not os.access(storage_file, os.W_OK):
|
||||
self.log_signal.emit("🔓 检测到文件只读,正在解除只读属性...")
|
||||
import stat
|
||||
storage_file.chmod(storage_file.stat().st_mode | stat.S_IWRITE)
|
||||
|
||||
self.log_signal.emit("📖 读取配置文件...")
|
||||
with open(storage_file, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
data = json.loads(content) if content.strip() else {}
|
||||
|
||||
old_email = (
|
||||
data.get("cursorAuth", {}).get("cachedEmail")
|
||||
or data.get("cursorAccount", {}).get("email")
|
||||
or ""
|
||||
)
|
||||
if old_email:
|
||||
self.log_signal.emit(f"📧 原邮箱: {old_email}")
|
||||
new_email = str(old_email).strip()
|
||||
|
||||
shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
|
||||
|
||||
self.log_signal.emit("🔑 替换 cursorAuth...")
|
||||
if "cursorAuth" not in data:
|
||||
data["cursorAuth"] = {}
|
||||
data["cursorAuth"]["accessToken"] = new_token
|
||||
data["cursorAuth"]["refreshToken"] = new_token
|
||||
data["cursorAuth"]["cachedEmail"] = new_email
|
||||
data["cursorAuth"]["cachedSignUpType"] = data["cursorAuth"].get("cachedSignUpType", "Auth_0")
|
||||
data["cursorAuth"]["onboardingDate"] = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
|
||||
data["cursorAuth"]["plan"] = "pro"
|
||||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||||
data["cursorAuth"]["membershipType"] = "pro"
|
||||
|
||||
self.log_signal.emit("🔑 替换 cursorAccount...")
|
||||
if "cursorAccount" not in data:
|
||||
data["cursorAccount"] = {}
|
||||
data["cursorAccount"]["token"] = new_token
|
||||
data["cursorAccount"]["email"] = new_email
|
||||
data["cursorAccount"]["plan"] = "pro"
|
||||
|
||||
self.log_signal.emit("🔧 刷新机器ID...")
|
||||
new_machine_id = generate_machine_id()
|
||||
data["telemetryMacMachineId"] = new_machine_id
|
||||
data["telemetryDevDeviceId"] = new_machine_id
|
||||
data["workspaceIdentifier"] = new_machine_id
|
||||
data["membershipType"] = "pro"
|
||||
|
||||
self.log_signal.emit(f"📧 新邮箱: {new_email}")
|
||||
|
||||
self.log_signal.emit("💾 保存配置文件...")
|
||||
with open(storage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
||||
db_ok = update_vsdb_token(config_dir, new_token, new_email, self.log_signal.emit)
|
||||
if not db_ok:
|
||||
self.log_signal.emit("❌ 更新 Cursor 数据库失败,换号未完成!")
|
||||
self.finished_signal.emit(False, "更新 Cursor 数据库失败,可能是 Cursor 进程未完全关闭,请在任务管理器中结束所有 Cursor 进程,或尝试以管理员身份运行本软件。", "")
|
||||
return
|
||||
|
||||
self.log_signal.emit("✅ 一键续杯换号完成!")
|
||||
self.finished_signal.emit(True, str(backup_subdir), new_email)
|
||||
|
||||
except Exception as e:
|
||||
self.log_signal.emit(f"❌ 续杯换号失败: {str(e)}")
|
||||
self.finished_signal.emit(False, str(e), "")
|
||||
|
||||
|
||||
def get_device_info() -> str:
|
||||
"""获取 CPU/RAM/磁盘等设备信息"""
|
||||
try:
|
||||
cpu_name = _get_cpu_id()
|
||||
|
||||
virtual_mem = psutil.virtual_memory()
|
||||
total_ram_gb = round(virtual_mem.total / (1024 ** 3), 2)
|
||||
|
||||
try:
|
||||
disk_path = "C:\\" if sys.platform == "win32" else "/"
|
||||
disk_usage = psutil.disk_usage(disk_path)
|
||||
total_disk_gb = round(disk_usage.total / (1024 ** 3), 2)
|
||||
free_disk_gb = round(disk_usage.free / (1024 ** 3), 2)
|
||||
disk_str = f"Disk: {total_disk_gb} GB (Free: {free_disk_gb} GB)"
|
||||
except Exception:
|
||||
disk_str = "Disk: 获取失败"
|
||||
|
||||
cpu_count = psutil.cpu_count(logical=False)
|
||||
cpu_logical = psutil.cpu_count(logical=True)
|
||||
|
||||
info = f"CPU: {cpu_name} ({cpu_count}核/{cpu_logical}线程) | RAM: {total_ram_gb} GB | {disk_str}"
|
||||
return info
|
||||
except Exception as e:
|
||||
return f"获取设备信息失败: {str(e)}"
|
||||
|
||||
|
||||
def read_current_cursor_email_helper() -> str:
|
||||
"""从 Cursor 的 storage.json 读取当前 email。"""
|
||||
try:
|
||||
config_dir = get_cursor_config_path()
|
||||
storage_file = config_dir / "storage.json"
|
||||
if not storage_file.exists():
|
||||
return ""
|
||||
with open(storage_file, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
data = json.loads(content) if content.strip() else {}
|
||||
email = (
|
||||
data.get("cursorAuth", {}).get("cachedEmail")
|
||||
or data.get("cursorAccount", {}).get("email")
|
||||
or ""
|
||||
)
|
||||
return str(email).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def report_equipment_data(bind_account=None):
|
||||
"""上报设备数据,返回 (data, error) 元组"""
|
||||
try:
|
||||
url = "https://api.yunzer.cn/api/cursor/equipment/report"
|
||||
machine_code = get_renewal_device_id()
|
||||
device_info = get_device_info()
|
||||
system = "Windows" if sys.platform == "win32" else ("macOS" if sys.platform == "darwin" else "Linux")
|
||||
version = __VERSION__
|
||||
|
||||
if not bind_account:
|
||||
bind_account = read_current_cursor_email_helper()
|
||||
|
||||
payload = {
|
||||
"machineCode": machine_code,
|
||||
"deviceInfo": device_info,
|
||||
"system": system,
|
||||
"version": version,
|
||||
"bindAccount": bind_account,
|
||||
"remark": "登录器上报"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response_data = None
|
||||
# 优先走代理 fallback
|
||||
try:
|
||||
with requests.post(url, json=payload, headers=headers, timeout=10) as response:
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
except requests.exceptions.ProxyError:
|
||||
with requests.Session() as session:
|
||||
session.trust_env = False
|
||||
with session.post(url, json=payload, headers=headers, timeout=10) as response:
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
|
||||
if response_data and response_data.get("code") == 200:
|
||||
return (response_data.get("data", {}), None)
|
||||
elif response_data:
|
||||
return (None, response_data.get("msg", "未知错误"))
|
||||
else:
|
||||
return (None, "服务器未返回有效数据")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return (None, "连接超时,请检查网络")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
return (None, f"网络连接失败: {str(e)}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (None, f"请求失败: {str(e)}")
|
||||
except Exception as e:
|
||||
return (None, f"数据上报失败: {str(e)}")
|
||||
|
||||
|
||||
class ReportEquipmentThread(QThread):
|
||||
"""上报设备信息线程"""
|
||||
finished_signal = Signal(bool, dict, str)
|
||||
|
||||
def __init__(self, bind_account=None):
|
||||
super().__init__()
|
||||
self.bind_account = bind_account
|
||||
|
||||
def run(self):
|
||||
data, error = report_equipment_data(self.bind_account)
|
||||
if error:
|
||||
self.finished_signal.emit(False, {}, error)
|
||||
else:
|
||||
self.finished_signal.emit(True, data, "")
|
||||
|
||||
|
||||
def activate_by_code(activation_code, bind_account=None):
|
||||
"""
|
||||
使用激活码激活设备,返回 (data, error) 元组
|
||||
"""
|
||||
try:
|
||||
url = "https://api.yunzer.cn/api/cursor/equipment/activateByCode"
|
||||
machine_code = get_renewal_device_id()
|
||||
device_info = get_device_info()
|
||||
system = "Windows" if sys.platform == "win32" else ("macOS" if sys.platform == "darwin" else "Linux")
|
||||
version = __VERSION__
|
||||
|
||||
if not bind_account:
|
||||
bind_account = read_current_cursor_email_helper()
|
||||
|
||||
payload = {
|
||||
"activationCode": activation_code,
|
||||
"machineCode": machine_code,
|
||||
"deviceInfo": device_info,
|
||||
"system": system,
|
||||
"version": version,
|
||||
"bindAccount": bind_account,
|
||||
"remark": "登录器激活"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response_data = None
|
||||
# 优先走代理 fallback
|
||||
try:
|
||||
with requests.post(url, json=payload, headers=headers, timeout=10) as response:
|
||||
response_data = response.json()
|
||||
except requests.exceptions.ProxyError:
|
||||
with requests.Session() as session:
|
||||
session.trust_env = False
|
||||
with session.post(url, json=payload, headers=headers, timeout=10) as response:
|
||||
response_data = response.json()
|
||||
|
||||
if response_data and response_data.get("code") == 200:
|
||||
return (response_data.get("data", {}), None)
|
||||
elif response_data:
|
||||
return (None, response_data.get("msg", "未知错误"))
|
||||
else:
|
||||
return (None, "服务器未返回有效数据")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return (None, "连接超时,请检查网络")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
return (None, f"网络连接失败: {str(e)}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (None, f"请求失败: {str(e)}")
|
||||
except Exception as e:
|
||||
return (None, f"激活失败: {str(e)}")
|
||||
|
||||
|
||||
class ActivateCodeThread(QThread):
|
||||
"""激活码激活线程"""
|
||||
finished_signal = Signal(bool, dict, str)
|
||||
|
||||
def __init__(self, activation_code, bind_account=None):
|
||||
super().__init__()
|
||||
self.activation_code = activation_code
|
||||
self.bind_account = bind_account
|
||||
|
||||
def run(self):
|
||||
data, error = activate_by_code(self.activation_code, self.bind_account)
|
||||
if error:
|
||||
self.finished_signal.emit(False, {}, error)
|
||||
else:
|
||||
self.finished_signal.emit(True, data, "")
|
||||
|
||||
|
||||
class CheckUpdateThread(QThread):
|
||||
"""检查更新线程"""
|
||||
update_available = Signal(dict)
|
||||
@ -889,6 +1228,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.cursor_path = get_default_cursor_path()
|
||||
self.backup_path = ""
|
||||
self.member_status = 0
|
||||
self._update_download_thread = None
|
||||
self._emergency_dialog = None
|
||||
self._usage_guide_dialog = None
|
||||
@ -994,7 +1334,7 @@ class MainWindow(QMainWindow):
|
||||
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)
|
||||
self.btnRefreshMemberStatus.clicked.connect(lambda: self.on_refresh_member_status_clicked(show_popup=True))
|
||||
if self.btnOneClickRenewal:
|
||||
self.btnOneClickRenewal.clicked.connect(self.on_one_click_renewal_clicked)
|
||||
if self.actionExit:
|
||||
@ -1030,6 +1370,9 @@ class MainWindow(QMainWindow):
|
||||
self.check_update_thread.error.connect(self.on_update_error)
|
||||
self.check_update_thread.start()
|
||||
|
||||
# 启动设备上报并同步状态
|
||||
self.on_refresh_member_status_clicked(show_popup=False)
|
||||
|
||||
def _restore_check_update_btn(self):
|
||||
if self.btnCheckUpdate:
|
||||
self.btnCheckUpdate.setEnabled(True)
|
||||
@ -1394,10 +1737,64 @@ class MainWindow(QMainWindow):
|
||||
if not activation_code:
|
||||
QMessageBox.warning(self, "提示", "请输入激活码。")
|
||||
return
|
||||
self.log(f"🔑 正在激活一键续杯,设备号:{device_id}")
|
||||
self.log("ℹ️ 激活码已提交,请根据服务端接口补充实际激活逻辑。")
|
||||
QMessageBox.information(self, "提示", "激活码已提交,当前版本尚未配置服务端激活接口。")
|
||||
self.on_refresh_member_status_clicked()
|
||||
|
||||
self.log(f"🔑 正在激活,设备号:{device_id},激活码:{activation_code}")
|
||||
|
||||
if self.btnActivateRenewal:
|
||||
self.btnActivateRenewal.setEnabled(False)
|
||||
self.btnActivateRenewal.setText("🔄 激活中...")
|
||||
|
||||
self._activate_thread = ActivateCodeThread(activation_code)
|
||||
self._activate_thread.finished_signal.connect(self.on_activate_finished)
|
||||
self._activate_thread.start()
|
||||
|
||||
def on_activate_finished(self, success, data, error_msg):
|
||||
if self.btnActivateRenewal:
|
||||
self.btnActivateRenewal.setEnabled(True)
|
||||
self.btnActivateRenewal.setText("🔑 提交激活码")
|
||||
|
||||
if success:
|
||||
self.log("✅ 激活成功!已成功同步有效期及状态")
|
||||
|
||||
status = data.get("status", 1)
|
||||
self.member_status = status
|
||||
status_text = "已激活" if status == 1 else "已过期"
|
||||
|
||||
if self.lblMemberStatus:
|
||||
self.lblMemberStatus.setText(f"当前状态:{status_text}")
|
||||
if self.lblMemberLevel:
|
||||
self.lblMemberLevel.setText(status_text)
|
||||
|
||||
act_time = data.get("activatedAt") or data.get("activationAt") or "-"
|
||||
exp_time = data.get("expireTime") or data.get("expiredAt") or "-"
|
||||
|
||||
def format_time_str(t_str):
|
||||
if not t_str or t_str == "-":
|
||||
return "-"
|
||||
t_str = str(t_str).replace("T", " ")
|
||||
if "+" in t_str:
|
||||
t_str = t_str.split("+")[0]
|
||||
if "Z" in t_str:
|
||||
t_str = t_str.replace("Z", "")
|
||||
if "." in t_str:
|
||||
t_str = t_str.split(".")[0]
|
||||
return t_str.strip()
|
||||
|
||||
if self.lblActivatedAt:
|
||||
self.lblActivatedAt.setText(format_time_str(act_time))
|
||||
if self.lblExpiredAt:
|
||||
self.lblExpiredAt.setText(format_time_str(exp_time))
|
||||
|
||||
duration = data.get("durationDays", 0)
|
||||
self.log(f"🎉 激活成功!增加天数:{duration} 天,到期时间:{format_time_str(exp_time)}")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"激活成功",
|
||||
f"激活码校验通过!\n绑定机器码成功,已续期 {duration} 天。\n到期时间:{format_time_str(exp_time)}"
|
||||
)
|
||||
else:
|
||||
self.log(f"❌ 激活失败: {error_msg}")
|
||||
QMessageBox.critical(self, "激活失败", f"无法激活设备:{error_msg}")
|
||||
|
||||
def _reset_member_status_display(self):
|
||||
"""重置会员状态展示。"""
|
||||
@ -1412,33 +1809,124 @@ class MainWindow(QMainWindow):
|
||||
if self.lblExpiredAt:
|
||||
self.lblExpiredAt.setText("-")
|
||||
|
||||
def on_refresh_member_status_clicked(self):
|
||||
def start_equipment_report(self, bind_account=None, show_popup=False):
|
||||
"""开始上报设备信息"""
|
||||
self._report_thread = ReportEquipmentThread(bind_account)
|
||||
self._report_thread.finished_signal.connect(
|
||||
lambda success, data, error_msg: self.on_equipment_report_finished(success, data, error_msg, show_popup)
|
||||
)
|
||||
self._report_thread.start()
|
||||
|
||||
def on_equipment_report_finished(self, success, data, error_msg, show_popup=False):
|
||||
if success:
|
||||
# self.log("✅ 设备数据已成功上报到平台")
|
||||
status = data.get("status", 0)
|
||||
self.member_status = status
|
||||
status_text = "未激活"
|
||||
if status == 1:
|
||||
status_text = "已激活"
|
||||
elif status == 2:
|
||||
status_text = "已过期"
|
||||
elif status == 3:
|
||||
status_text = "已禁用"
|
||||
|
||||
if self.lblMemberStatus:
|
||||
self.lblMemberStatus.setText(f"当前状态:{status_text}")
|
||||
if self.lblMemberLevel:
|
||||
self.lblMemberLevel.setText(status_text)
|
||||
|
||||
act_time = data.get("activationTime") or data.get("activation_time") or "-"
|
||||
exp_time = data.get("expireTime") or data.get("expire_time") or "-"
|
||||
|
||||
def format_time_str(t_str):
|
||||
if not t_str or t_str == "-":
|
||||
return "-"
|
||||
t_str = str(t_str).replace("T", " ")
|
||||
if "+" in t_str:
|
||||
t_str = t_str.split("+")[0]
|
||||
if "Z" in t_str:
|
||||
t_str = t_str.replace("Z", "")
|
||||
if "." in t_str:
|
||||
t_str = t_str.split(".")[0]
|
||||
return t_str.strip()
|
||||
|
||||
if self.lblActivatedAt:
|
||||
self.lblActivatedAt.setText(format_time_str(act_time))
|
||||
if self.lblExpiredAt:
|
||||
self.lblExpiredAt.setText(format_time_str(exp_time))
|
||||
|
||||
owner_name = data.get("ownerUserName") or data.get("owner_user_name")
|
||||
if owner_name:
|
||||
if self.lblAccountType:
|
||||
self.lblAccountType.setText(f"绑定用户:{owner_name}")
|
||||
else:
|
||||
if self.lblAccountType:
|
||||
self.lblAccountType.setText("普通设备")
|
||||
|
||||
if show_popup:
|
||||
QMessageBox.information(self, "刷新成功", f"会员状态已同步!当前状态:{status_text}")
|
||||
else:
|
||||
self.log(f"⚠️ 设备数据同步失败: {error_msg}")
|
||||
if show_popup:
|
||||
QMessageBox.warning(self, "刷新失败", f"无法同步设备状态:{error_msg}")
|
||||
|
||||
def on_refresh_member_status_clicked(self, show_popup=False):
|
||||
"""刷新会员状态展示。"""
|
||||
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||||
if not device_id:
|
||||
if show_popup:
|
||||
QMessageBox.warning(self, "提示", "设备号为空,无法刷新会员状态。")
|
||||
return
|
||||
|
||||
if self.lblMemberStatus:
|
||||
self.lblMemberStatus.setText("当前状态:待接入服务端")
|
||||
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.lblMemberStatus.setText("当前状态:正在同步平台...")
|
||||
|
||||
self.log("🔄 已刷新会员状态展示,当前版本尚未配置服务端查询接口。")
|
||||
self.log("🔄 正在从平台同步设备与会员状态...")
|
||||
self.start_equipment_report(show_popup=show_popup)
|
||||
|
||||
def on_one_click_renewal_clicked(self):
|
||||
"""执行一键续杯。"""
|
||||
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||||
if not device_id:
|
||||
QMessageBox.warning(self, "提示", "设备号为空,无法执行一键续杯。")
|
||||
# 1. 检测是否激活
|
||||
is_active = False
|
||||
if hasattr(self, 'member_status') and self.member_status == 1:
|
||||
is_active = True
|
||||
elif self.lblMemberStatus and "已激活" in self.lblMemberStatus.text():
|
||||
is_active = True
|
||||
|
||||
if not is_active:
|
||||
QMessageBox.warning(self, "提示", "本设备未激活,请输入激活码激活。")
|
||||
self.log("⚠️ 续杯失败:本设备未激活,请输入激活码激活。")
|
||||
return
|
||||
|
||||
# 2. 检测 Cursor 是否正在运行
|
||||
if is_cursor_running():
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setWindowTitle("Cursor正在运行")
|
||||
msg_box.setText("Cursor正在运行中!\n请先保存代码并手动关闭Cursor。")
|
||||
msg_box.setIcon(QMessageBox.Warning)
|
||||
btn_close = msg_box.addButton("💀 强制关闭", QMessageBox.ActionRole)
|
||||
btn_cancel = msg_box.addButton("取消", QMessageBox.RejectRole)
|
||||
msg_box.setDefaultButton(btn_cancel)
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() == btn_close:
|
||||
self.log("💀 正在强制关闭Cursor...")
|
||||
if kill_cursor():
|
||||
self.log("✅ Cursor已关闭")
|
||||
else:
|
||||
self.log("⚠️ 未找到运行中的Cursor进程")
|
||||
QMessageBox.warning(self, "警告", "未找到运行中的Cursor进程")
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
# 3. 检查 Cursor 路径
|
||||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||
if not cursor_path or not Path(cursor_path).exists():
|
||||
QMessageBox.warning(self, "警告", "请设置正确的Cursor路径!")
|
||||
return
|
||||
|
||||
# 4. 再次确认续杯
|
||||
confirm = QMessageBox.question(
|
||||
self,
|
||||
"确认一键续杯",
|
||||
@ -1452,10 +1940,35 @@ class MainWindow(QMainWindow):
|
||||
self.log("ℹ️ 已取消一键续杯。")
|
||||
return
|
||||
|
||||
self.log(f"☕ 正在执行一键续杯,设备号:{device_id}")
|
||||
self.log("ℹ️ 一键续杯请求已提交,请根据服务端接口补充实际续杯逻辑。")
|
||||
QMessageBox.information(self, "提示", "一键续杯请求已提交,当前版本尚未配置服务端续杯接口。")
|
||||
self.on_refresh_member_status_clicked()
|
||||
# 5. 禁用按钮并启动续杯换号后台线程
|
||||
if self.btnOneClickRenewal:
|
||||
self.btnOneClickRenewal.setEnabled(False)
|
||||
self.btnOneClickRenewal.setText("🔄 续杯中...")
|
||||
|
||||
self._renewal_thread = OneClickRenewalThread()
|
||||
self._renewal_thread.log_signal.connect(self.log)
|
||||
self._renewal_thread.finished_signal.connect(self.on_one_click_renewal_finished)
|
||||
self._renewal_thread.start()
|
||||
|
||||
@Slot(bool, str, str)
|
||||
def on_one_click_renewal_finished(self, success, message, new_email):
|
||||
if self.btnOneClickRenewal:
|
||||
self.btnOneClickRenewal.setEnabled(True)
|
||||
self.btnOneClickRenewal.setText("一键续杯")
|
||||
|
||||
if success:
|
||||
self.backup_path = message
|
||||
self.log("✅ 一键续杯完成")
|
||||
|
||||
# 上报新换号绑定的设备信息(使用生成的新 email)
|
||||
self.start_equipment_report(bind_account=new_email)
|
||||
|
||||
self._show_change_success_countdown_dialog(4)
|
||||
self.log("🚀 正在打开Cursor...")
|
||||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||
self._launch_cursor(cursor_path)
|
||||
else:
|
||||
QMessageBox.critical(self, "失败", message)
|
||||
|
||||
def _extract_session_token(self, raw_value: str) -> str:
|
||||
"""从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。"""
|
||||
@ -1513,7 +2026,8 @@ class MainWindow(QMainWindow):
|
||||
dialog.exec()
|
||||
|
||||
def on_change_clicked(self):
|
||||
token = self.txtToken.toPlainText().strip() if self.txtToken else ""
|
||||
raw_token = self.txtToken.toPlainText().strip() if self.txtToken else ""
|
||||
token = self._extract_session_token(raw_token)
|
||||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||
|
||||
if not token:
|
||||
@ -1943,6 +2457,10 @@ class MainWindow(QMainWindow):
|
||||
if success:
|
||||
self.backup_path = message
|
||||
self.log("✅ 换号完成")
|
||||
|
||||
# 上报新换号绑定的设备信息(使用生成的新 email)
|
||||
self.start_equipment_report(bind_account=self.thread.new_email)
|
||||
|
||||
self._show_change_success_countdown_dialog(4)
|
||||
self.log("🚀 正在打开Cursor...")
|
||||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||
|
||||
28
main.spec
28
main.spec
@ -1,29 +1,11 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
try:
|
||||
_SPEC_ROOT = os.path.dirname(os.path.abspath(SPEC))
|
||||
except NameError:
|
||||
_SPEC_ROOT = os.getcwd()
|
||||
_LOGO_ICO = os.path.normpath(os.path.join(_SPEC_ROOT, "logo.ico"))
|
||||
if not os.path.isfile(_LOGO_ICO):
|
||||
raise FileNotFoundError(
|
||||
"找不到图标文件: %s\n请将 logo.ico 放在与 main.spec 同一目录后重新打包。" % _LOGO_ICO
|
||||
)
|
||||
_LOGO_ICO_ABS = os.path.abspath(_LOGO_ICO)
|
||||
|
||||
# 须与 build.spec 一致,否则 onefile 内无 layout/main.ui,会出现「找不到 UI 文件」
|
||||
_datas = [
|
||||
(os.path.join(os.getcwd(), 'layout'), 'layout'),
|
||||
(os.path.join(os.getcwd(), 'assets'), 'assets'),
|
||||
(_LOGO_ICO_ABS, "."),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[os.getcwd()],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=_datas,
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
@ -44,14 +26,14 @@ exe = EXE(
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=r'%LOCALAPPDATA%\CursorTokenLogin\runtime',
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=_LOGO_ICO_ABS,
|
||||
icon=['logo.ico'],
|
||||
)
|
||||
|
||||
BIN
state.vscdb
Normal file
BIN
state.vscdb
Normal file
Binary file not shown.
BIN
state1.vscdb
Normal file
BIN
state1.vscdb
Normal file
Binary file not shown.
BIN
state_test.vscdb
Normal file
BIN
state_test.vscdb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user