Compare commits
No commits in common. "1b54447ccbb7579db36b7d90c10a0cf86b30521c" and "e30c74007be39ca66080ff13386efd4e05cd7dba" have entirely different histories.
1b54447ccb
...
e30c74007b
189
.github/workflows/build.yml
vendored
189
.github/workflows/build.yml
vendored
@ -1,189 +0,0 @@
|
|||||||
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: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-13, 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 Pillow
|
|
||||||
|
|
||||||
- name: Set Architecture Name
|
|
||||||
run: |
|
|
||||||
if [ "${{ matrix.os }}" = "macos-13" ]; then
|
|
||||||
echo "ARCH_NAME=Intel" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "ARCH_NAME=AppleSilicon" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build App Bundle
|
|
||||||
run: |
|
|
||||||
python -m PyInstaller build.spec --clean
|
|
||||||
|
|
||||||
- name: Zip macOS App
|
|
||||||
run: |
|
|
||||||
cd dist
|
|
||||||
zip -r CursorTokenLogin-macOS-${{ env.ARCH_NAME }}.zip CursorTokenLogin.app
|
|
||||||
|
|
||||||
- name: Upload macOS Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: CursorTokenLogin-macOS-${{ env.ARCH_NAME }}
|
|
||||||
path: dist/CursorTokenLogin-macOS-${{ env.ARCH_NAME }}.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 并上传 5 个包
|
|
||||||
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 All Artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./release-files
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
release-files/CursorTokenLogin.exe
|
|
||||||
release-files/CursorTokenLogin-macOS-Intel.zip
|
|
||||||
release-files/CursorTokenLogin-macOS-AppleSilicon.zip
|
|
||||||
release-files/cursortokenlogin.deb
|
|
||||||
release-files/cursortokenlogin.rpm
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
88
README.md
88
README.md
@ -1,85 +1,13 @@
|
|||||||
# 多平台编译与打包指南
|
python -m PyInstaller -w -F main.py
|
||||||
|
|
||||||
在 Windows 系统下,由于 PyInstaller 的工作机制是直接打包当前平台的 Python 解析器和系统动态库,**它不支持交叉编译**(即在 Windows 本机无法编译出 macOS 的 `.app` 苹果程序,也无法直接打包 Linux 的 `.deb` 或 `.rpm` 安装包)。
|
<br />
|
||||||
|
|
||||||
为了解决这个问题,并在一台电脑上实现 4 端程序(Windows `.exe`、macOS `.app`、Debian/Ubuntu `.deb`、RedHat/Fedora `.rpm`)的统一打包,我们已经在项目中配置了 **GitHub Actions 自动化流水线**。
|
# 编译成exe
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 推荐方案:使用 GitHub Actions 自动打包 4 端程序
|
|
||||||
|
|
||||||
我们已经在项目中创建了自动化打包脚本:[.github/workflows/build.yml](file:///.github/workflows/build.yml)。
|
|
||||||
|
|
||||||
您只需将代码托管在 GitHub 仓库中,GitHub 就会为您调用免费的 Windows、macOS 和 Linux 虚拟服务器,一键自动构建出所有安装包!
|
|
||||||
|
|
||||||
### 操作步骤:
|
|
||||||
1. **首次配置并推送代码到 GitHub 仓库** (在本地项目终端运行):
|
|
||||||
```bash
|
|
||||||
# 关联远程 GitHub 仓库,并将其命名为 github (实现双仓库并存)
|
|
||||||
git remote add github https://github.com/hero920103/cursorlogin.git
|
|
||||||
|
|
||||||
# 将本地的 master 分支推送到 GitHub
|
|
||||||
git push -u github master
|
|
||||||
```
|
|
||||||
2. **推送 Tag 触发 4 端自动打包** (后续每次需要打包发布时,直接运行下面两行):
|
|
||||||
```bash
|
|
||||||
# 1. 本地打上版本 tag
|
|
||||||
git tag vx.x.x
|
|
||||||
|
|
||||||
# 2. 将 tag 单独推送到 github 触发云端自动构建
|
|
||||||
git push github vx.x.x
|
|
||||||
```
|
|
||||||
3. GitHub Actions 收到 Tag 后会自动触发构建,云端会启动多个编译任务,最终产出:
|
|
||||||
- 💻 **Windows**:编译出 `CursorTokenLogin.exe`
|
|
||||||
- 🍏 **macOS (M1/M2/M3 芯片)**:编译出 `CursorTokenLogin-macOS-AppleSilicon.zip` (解压即得 `.app`)
|
|
||||||
- 🍏 **macOS (Intel 芯片)**:编译出 `CursorTokenLogin-macOS-Intel.zip` (解压即得 `.app`)
|
|
||||||
- 🐧 **Linux**:编译出 Linux 运行文件,并自动打包为 `cursortokenlogin.deb` 和 `cursortokenlogin.rpm`
|
|
||||||
4. **下载成品**:构建完成后(耗时约 3~5 分钟),GitHub 会自动在您的仓库右侧 **Releases** 栏目中生成一个名为 `v1.0.0` 的发布页,进入即可直接下载编译好的 5 个安装包!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ 本地手动构建说明(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
|
python -m PyInstaller build.spec --clean
|
||||||
|
|
||||||
# 2. 组织 DEB 包目录结构并打包
|
<br />
|
||||||
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
|
|
||||||
|
|
||||||
# 创建 control 配置文件
|
# 关于 Win + Mac 同时构建(后续)
|
||||||
cat <<EOF > deb-package/DEBIAN/control
|
- PyInstaller 不能在 Windows 本机直接产出 macOS 程序。
|
||||||
Package: cursortokenlogin
|
- 当前命令在 Windows 只能生成 `.exe`,在 macOS 才能生成 `.app`。
|
||||||
Version: 1.0.0
|
- 后续可用 GitHub Actions 做双平台构建:本地打 Windows,云端 `macos-latest` 打 macOS。
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1,58 +0,0 @@
|
|||||||
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()
|
|
||||||
11
build.spec
11
build.spec
@ -13,7 +13,7 @@ if not os.path.isfile(_LOGO_ICO):
|
|||||||
"找不到图标文件: %s\n请将 logo.ico 放在与 build.spec 同一目录后重新打包。" % _LOGO_ICO
|
"找不到图标文件: %s\n请将 logo.ico 放在与 build.spec 同一目录后重新打包。" % _LOGO_ICO
|
||||||
)
|
)
|
||||||
_LOGO_ICO_ABS = os.path.abspath(_LOGO_ICO)
|
_LOGO_ICO_ABS = os.path.abspath(_LOGO_ICO)
|
||||||
print("[build.spec] EXE icon path:", _LOGO_ICO_ABS)
|
print("[build.spec] EXE 嵌入图标:", _LOGO_ICO_ABS)
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
@ -66,12 +66,3 @@ exe = EXE(
|
|||||||
# 必须用绝对路径;资源管理器若仍显示旧图标多半是 Windows 图标缓存,可改 exe 文件名或清 IconCache
|
# 必须用绝对路径;资源管理器若仍显示旧图标多半是 Windows 图标缓存,可改 exe 文件名或清 IconCache
|
||||||
icon=_LOGO_ICO_ABS,
|
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,11 +623,6 @@
|
|||||||
<string>关于软件</string>
|
<string>关于软件</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>无感检测</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -831,69 +826,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
584
main.py
584
main.py
@ -265,7 +265,7 @@ def get_cursor_config_path():
|
|||||||
|
|
||||||
|
|
||||||
def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
||||||
"""更新 Cursor 数据库中的 token / email / auth 缓存字段。"""
|
"""更新 Cursor 数据库中的token和email"""
|
||||||
db_path = config_dir / "state.vscdb"
|
db_path = config_dir / "state.vscdb"
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
log_callback("⚠️ 未找到 Cursor库 文件,跳过")
|
log_callback("⚠️ 未找到 Cursor库 文件,跳过")
|
||||||
@ -281,27 +281,14 @@ def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
|||||||
shutil.copy2(db_path, db_backup)
|
shutil.copy2(db_path, db_backup)
|
||||||
# log_callback(f"📁 数据库已备份到: {db_backup}")
|
# log_callback(f"📁 数据库已备份到: {db_backup}")
|
||||||
|
|
||||||
# 使用 timeout=5 避免瞬间锁导致失败
|
conn = sqlite3.connect(db_path)
|
||||||
conn = sqlite3.connect(db_path, timeout=5.0)
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 强制合并并清除 WAL 缓存(切换为 DELETE 模式,这样会自动合并并删除 -wal 和 -shm 文件)
|
# 更新 token 和 email
|
||||||
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 = [
|
updates = [
|
||||||
("cursorAuth/accessToken", new_token),
|
("cursorAuth/accessToken", new_token),
|
||||||
("cursorAuth/refreshToken", new_token),
|
("cursorAuth/refreshToken", new_token),
|
||||||
("cursorAuth/cachedEmail", new_email),
|
("cursorAuth/cachedEmail", new_email),
|
||||||
("cursorAuth/cachedSignUpType", "Auth_0"),
|
|
||||||
("cursorAuth/onboardingDate", onboarding_date),
|
|
||||||
("cursorAuth/stripeMembershipType", "pro"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, value in updates:
|
for key, value in updates:
|
||||||
@ -325,16 +312,8 @@ def update_vsdb_token(config_dir, new_token, new_email, log_callback):
|
|||||||
# log_callback(f"✓ 插入了 {key}")
|
# log_callback(f"✓ 插入了 {key}")
|
||||||
|
|
||||||
conn.commit()
|
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()
|
conn.close()
|
||||||
log_callback(f"✅ Cursor库 更新成功,已同步 {len(updates)} 个认证字段")
|
log_callback("✅ Cursor库 更新成功")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -708,16 +687,10 @@ class ChangeTokenThread(QThread):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
data = json.loads(content) if content.strip() else {}
|
data = json.loads(content) if content.strip() else {}
|
||||||
|
|
||||||
# 显示原邮箱。access token 本身通常不包含 email claim,
|
# 显示原邮箱
|
||||||
# 因此优先保留 Cursor 原缓存邮箱,避免写入随机邮箱导致登录缓存状态不一致。
|
if "cursorAuth" in data and "cachedEmail" in data.get("cursorAuth", {}):
|
||||||
old_email = (
|
old_email = data["cursorAuth"]["cachedEmail"]
|
||||||
data.get("cursorAuth", {}).get("cachedEmail")
|
|
||||||
or data.get("cursorAccount", {}).get("email")
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
if old_email:
|
|
||||||
self.log_signal.emit(f"📧 原邮箱: {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")
|
shutil.copy2(storage_file, backup_subdir / "storage.json.bak")
|
||||||
@ -729,8 +702,6 @@ class ChangeTokenThread(QThread):
|
|||||||
data["cursorAuth"]["accessToken"] = self.new_token
|
data["cursorAuth"]["accessToken"] = self.new_token
|
||||||
data["cursorAuth"]["refreshToken"] = self.new_token
|
data["cursorAuth"]["refreshToken"] = self.new_token
|
||||||
data["cursorAuth"]["cachedEmail"] = self.new_email
|
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"]["plan"] = "pro"
|
||||||
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
data["cursorAuth"]["stripeMembershipType"] = "pro"
|
||||||
data["cursorAuth"]["membershipType"] = "pro"
|
data["cursorAuth"]["membershipType"] = "pro"
|
||||||
@ -762,11 +733,7 @@ class ChangeTokenThread(QThread):
|
|||||||
|
|
||||||
# 更新 Cursor 数据库
|
# 更新 Cursor 数据库
|
||||||
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
self.log_signal.emit("📦 更新 Cursor 数据库...")
|
||||||
db_ok = update_vsdb_token(config_dir, self.new_token, self.new_email, self.log_signal.emit)
|
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.log_signal.emit("✅ 换号完成!")
|
||||||
self.finished_signal.emit(True, str(backup_subdir))
|
self.finished_signal.emit(True, str(backup_subdir))
|
||||||
@ -776,312 +743,6 @@ class ChangeTokenThread(QThread):
|
|||||||
self.finished_signal.emit(False, str(e))
|
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):
|
class CheckUpdateThread(QThread):
|
||||||
"""检查更新线程"""
|
"""检查更新线程"""
|
||||||
update_available = Signal(dict)
|
update_available = Signal(dict)
|
||||||
@ -1228,7 +889,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.cursor_path = get_default_cursor_path()
|
self.cursor_path = get_default_cursor_path()
|
||||||
self.backup_path = ""
|
self.backup_path = ""
|
||||||
self.member_status = 0
|
|
||||||
self._update_download_thread = None
|
self._update_download_thread = None
|
||||||
self._emergency_dialog = None
|
self._emergency_dialog = None
|
||||||
self._usage_guide_dialog = None
|
self._usage_guide_dialog = None
|
||||||
@ -1334,7 +994,7 @@ class MainWindow(QMainWindow):
|
|||||||
if self.btnBuyActivationCode:
|
if self.btnBuyActivationCode:
|
||||||
self.btnBuyActivationCode.clicked.connect(self.on_open_online_shop_clicked)
|
self.btnBuyActivationCode.clicked.connect(self.on_open_online_shop_clicked)
|
||||||
if self.btnRefreshMemberStatus:
|
if self.btnRefreshMemberStatus:
|
||||||
self.btnRefreshMemberStatus.clicked.connect(lambda: self.on_refresh_member_status_clicked(show_popup=True))
|
self.btnRefreshMemberStatus.clicked.connect(self.on_refresh_member_status_clicked)
|
||||||
if self.btnOneClickRenewal:
|
if self.btnOneClickRenewal:
|
||||||
self.btnOneClickRenewal.clicked.connect(self.on_one_click_renewal_clicked)
|
self.btnOneClickRenewal.clicked.connect(self.on_one_click_renewal_clicked)
|
||||||
if self.actionExit:
|
if self.actionExit:
|
||||||
@ -1370,9 +1030,6 @@ class MainWindow(QMainWindow):
|
|||||||
self.check_update_thread.error.connect(self.on_update_error)
|
self.check_update_thread.error.connect(self.on_update_error)
|
||||||
self.check_update_thread.start()
|
self.check_update_thread.start()
|
||||||
|
|
||||||
# 启动设备上报并同步状态
|
|
||||||
self.on_refresh_member_status_clicked(show_popup=False)
|
|
||||||
|
|
||||||
def _restore_check_update_btn(self):
|
def _restore_check_update_btn(self):
|
||||||
if self.btnCheckUpdate:
|
if self.btnCheckUpdate:
|
||||||
self.btnCheckUpdate.setEnabled(True)
|
self.btnCheckUpdate.setEnabled(True)
|
||||||
@ -1737,64 +1394,10 @@ class MainWindow(QMainWindow):
|
|||||||
if not activation_code:
|
if not activation_code:
|
||||||
QMessageBox.warning(self, "提示", "请输入激活码。")
|
QMessageBox.warning(self, "提示", "请输入激活码。")
|
||||||
return
|
return
|
||||||
|
self.log(f"🔑 正在激活一键续杯,设备号:{device_id}")
|
||||||
self.log(f"🔑 正在激活,设备号:{device_id},激活码:{activation_code}")
|
self.log("ℹ️ 激活码已提交,请根据服务端接口补充实际激活逻辑。")
|
||||||
|
QMessageBox.information(self, "提示", "激活码已提交,当前版本尚未配置服务端激活接口。")
|
||||||
if self.btnActivateRenewal:
|
self.on_refresh_member_status_clicked()
|
||||||
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):
|
def _reset_member_status_display(self):
|
||||||
"""重置会员状态展示。"""
|
"""重置会员状态展示。"""
|
||||||
@ -1809,124 +1412,33 @@ class MainWindow(QMainWindow):
|
|||||||
if self.lblExpiredAt:
|
if self.lblExpiredAt:
|
||||||
self.lblExpiredAt.setText("-")
|
self.lblExpiredAt.setText("-")
|
||||||
|
|
||||||
def start_equipment_report(self, bind_account=None, show_popup=False):
|
def on_refresh_member_status_clicked(self):
|
||||||
"""开始上报设备信息"""
|
|
||||||
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 ""
|
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||||||
if not device_id:
|
if not device_id:
|
||||||
if show_popup:
|
QMessageBox.warning(self, "提示", "设备号为空,无法刷新会员状态。")
|
||||||
QMessageBox.warning(self, "提示", "设备号为空,无法刷新会员状态。")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.lblMemberStatus:
|
if self.lblMemberStatus:
|
||||||
self.lblMemberStatus.setText("当前状态:正在同步平台...")
|
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("🔄 正在从平台同步设备与会员状态...")
|
self.log("🔄 已刷新会员状态展示,当前版本尚未配置服务端查询接口。")
|
||||||
self.start_equipment_report(show_popup=show_popup)
|
|
||||||
|
|
||||||
def on_one_click_renewal_clicked(self):
|
def on_one_click_renewal_clicked(self):
|
||||||
"""执行一键续杯。"""
|
"""执行一键续杯。"""
|
||||||
# 1. 检测是否激活
|
device_id = self.txtDeviceId.text().strip() if self.txtDeviceId else ""
|
||||||
is_active = False
|
if not device_id:
|
||||||
if hasattr(self, 'member_status') and self.member_status == 1:
|
QMessageBox.warning(self, "提示", "设备号为空,无法执行一键续杯。")
|
||||||
is_active = True
|
|
||||||
elif self.lblMemberStatus and "已激活" in self.lblMemberStatus.text():
|
|
||||||
is_active = True
|
|
||||||
|
|
||||||
if not is_active:
|
|
||||||
QMessageBox.warning(self, "提示", "本设备未激活,请输入激活码激活。")
|
|
||||||
self.log("⚠️ 续杯失败:本设备未激活,请输入激活码激活。")
|
|
||||||
return
|
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(
|
confirm = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"确认一键续杯",
|
"确认一键续杯",
|
||||||
@ -1940,35 +1452,10 @@ class MainWindow(QMainWindow):
|
|||||||
self.log("ℹ️ 已取消一键续杯。")
|
self.log("ℹ️ 已取消一键续杯。")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 5. 禁用按钮并启动续杯换号后台线程
|
self.log(f"☕ 正在执行一键续杯,设备号:{device_id}")
|
||||||
if self.btnOneClickRenewal:
|
self.log("ℹ️ 一键续杯请求已提交,请根据服务端接口补充实际续杯逻辑。")
|
||||||
self.btnOneClickRenewal.setEnabled(False)
|
QMessageBox.information(self, "提示", "一键续杯请求已提交,当前版本尚未配置服务端续杯接口。")
|
||||||
self.btnOneClickRenewal.setText("🔄 续杯中...")
|
self.on_refresh_member_status_clicked()
|
||||||
|
|
||||||
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:
|
def _extract_session_token(self, raw_value: str) -> str:
|
||||||
"""从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。"""
|
"""从输入文本中提取 SessionToken,兼容纯 token / Cookie 串。"""
|
||||||
@ -2026,8 +1513,7 @@ class MainWindow(QMainWindow):
|
|||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
def on_change_clicked(self):
|
def on_change_clicked(self):
|
||||||
raw_token = self.txtToken.toPlainText().strip() if self.txtToken else ""
|
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 ""
|
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
@ -2457,10 +1943,6 @@ class MainWindow(QMainWindow):
|
|||||||
if success:
|
if success:
|
||||||
self.backup_path = message
|
self.backup_path = message
|
||||||
self.log("✅ 换号完成")
|
self.log("✅ 换号完成")
|
||||||
|
|
||||||
# 上报新换号绑定的设备信息(使用生成的新 email)
|
|
||||||
self.start_equipment_report(bind_account=self.thread.new_email)
|
|
||||||
|
|
||||||
self._show_change_success_countdown_dialog(4)
|
self._show_change_success_countdown_dialog(4)
|
||||||
self.log("🚀 正在打开Cursor...")
|
self.log("🚀 正在打开Cursor...")
|
||||||
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
cursor_path = self.txtCursorPath.text().strip() if self.txtCursorPath else ""
|
||||||
|
|||||||
28
main.spec
28
main.spec
@ -1,11 +1,29 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- 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(
|
a = Analysis(
|
||||||
['main.py'],
|
['main.py'],
|
||||||
pathex=[],
|
pathex=[os.getcwd()],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=_datas,
|
||||||
hiddenimports=[],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
@ -26,14 +44,14 @@ exe = EXE(
|
|||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=True,
|
upx=False,
|
||||||
upx_exclude=[],
|
upx_exclude=[],
|
||||||
runtime_tmpdir=None,
|
runtime_tmpdir=r'%LOCALAPPDATA%\CursorTokenLogin\runtime',
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
argv_emulation=False,
|
argv_emulation=False,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=None,
|
entitlements_file=None,
|
||||||
icon=['logo.ico'],
|
icon=_LOGO_ICO_ABS,
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
state.vscdb
BIN
state.vscdb
Binary file not shown.
BIN
state1.vscdb
BIN
state1.vscdb
Binary file not shown.
BIN
state_test.vscdb
BIN
state_test.vscdb
Binary file not shown.
Loading…
Reference in New Issue
Block a user