diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2f39938 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# 你的CF路由填写的域名 +DOMAIN=nosqli.com +# 邮件服务地址 +# 注册临时邮件服务 https://tempmail.plus +TEMP_MAIL=ademyyk +# 设置的PIN码 +TEMP_MAIL_EPIN=xxxxxx +# 使用的后缀 +TEMP_MAIL_EXT=@mailto.plus +BROWSER_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36 +MAIL_SERVER=https://tempmail.plus + +# 代理 +BROWSER_PROXY='http://127.0.0.1:2080' + +# 无头模式 默认开启 +BROWSER_HEADLESS='True' + +# API配置 +API_BASE_URL=http://api.example.com +API_TOKEN=your_api_token_here diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..08e4bf0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,149 @@ +name: Build Executables + +on: + push: + tags: + - 'v*' # 添加标签触发条件,匹配 v1.0.0 这样的标签 + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build EXE + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-Windows + path: dist/CursorPro.exe + + build-macos-arm64: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build MacOS ARM executable + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload MacOS ARM artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-MacOS-ARM64 + path: dist/CursorPro + + build-linux: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build Linux executable + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-Linux + path: dist/CursorPro + + build-macos-intel: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + arch -x86_64 pip3 install --upgrade pip + arch -x86_64 pip3 install pyinstaller + arch -x86_64 pip3 install -r requirements.txt + + - name: Build MacOS Intel executable + env: + TARGET_ARCH: 'x86_64' + run: | + arch -x86_64 python3 -m PyInstaller CursorKeepAlive.spec + + - name: Upload MacOS Intel artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-MacOS-Intel + path: dist/CursorPro + + create-release: + needs: [build-windows, build-macos-arm64, build-linux, build-macos-intel] + runs-on: ubuntu-22.04 + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create release archives + run: | + cd artifacts + zip -r CursorPro-Windows.zip CursorPro-Windows/ + zip -r CursorPro-MacOS-ARM64.zip CursorPro-MacOS-ARM64/ + zip -r CursorPro-Linux.zip CursorPro-Linux/ + zip -r CursorPro-MacOS-Intel.zip CursorPro-MacOS-Intel/ + + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/CursorPro-Windows.zip + artifacts/CursorPro-MacOS-ARM64.zip + artifacts/CursorPro-Linux.zip + artifacts/CursorPro-MacOS-Intel.zip + + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/.github/workflows/remove-old-artifacts.yml b/.github/workflows/remove-old-artifacts.yml new file mode 100644 index 0000000..e67b9d8 --- /dev/null +++ b/.github/workflows/remove-old-artifacts.yml @@ -0,0 +1,24 @@ +name: Remove old artifacts + +on: + schedule: + # Every day at 1am + - cron: '0 1 * * *' + # 手动 + workflow_dispatch: + + +jobs: + remove-old-artifacts: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Remove old artifacts + uses: c-hive/gha-remove-artifacts@v1 + with: + GITHUB_TOKEN: ${{ secrets.TOKEN }} + age: '5 days' # ' ', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js + # Optional inputs + # skip-tags: true + # skip-recent: 5 \ No newline at end of file diff --git a/.github/workflows/temp-build.yml b/.github/workflows/temp-build.yml new file mode 100644 index 0000000..e8fcbed --- /dev/null +++ b/.github/workflows/temp-build.yml @@ -0,0 +1,115 @@ +name: temp Build Executables + +on: + workflow_dispatch: # 手动触发工作流 + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build EXE + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-Windows + path: dist/CursorPro.exe + + build-macos-arm64: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build MacOS ARM executable + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload MacOS ARM artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-MacOS-ARM64 + path: dist/CursorPro + + build-linux: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build Linux executable + run: | + pyinstaller CursorKeepAlive.spec + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-Linux + path: dist/CursorPro + + build-macos-intel: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + arch -x86_64 pip3 install --upgrade pip + arch -x86_64 pip3 install pyinstaller + arch -x86_64 pip3 install -r requirements.txt + + - name: Build MacOS Intel executable + env: + TARGET_ARCH: 'x86_64' + run: | + arch -x86_64 python3 -m PyInstaller CursorKeepAlive.spec + + - name: Upload MacOS Intel artifact + uses: actions/upload-artifact@v4 + with: + name: CursorPro-MacOS-Intel + path: dist/CursorPro \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b0590d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# PyInstaller +build/ +dist/ +*.spec +!CursorKeepAlive.mac.spec +!CursorKeepAlive.win.spec + +# Python +__pycache__/ +*.py[cod] +*$py.class + +# Logs +*.log + +# IDE +.vscode/ +.idea/ + +# Mac +.DS_Store + +venv/ + +node_modules/ + +.env + +screenshots/ \ No newline at end of file diff --git a/README.EN.md b/README.EN.md new file mode 100644 index 0000000..10f7046 --- /dev/null +++ b/README.EN.md @@ -0,0 +1,41 @@ +# Cursor Pro Automation Tool User Guide + +README also available in: [中文](./README.md) + +## Online Documentation +[cursor-auto-free-doc.vercel.app](https://cursor-auto-free-doc.vercel.app) + +## Note +Recently, some users have sold this software on platforms like Xianyu. Please avoid such practices—there's no need to earn money this way. + +## Sponsor for More Updates +![image](./screen/afdian-[未认证]阿臻.jpg) + +## License +This project is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/). +This means you may: +- **Share** — Copy and redistribute the material in any medium or format. +But you must comply with the following conditions: +- **Non-commercial** — You may not use the material for commercial purposes. + +## Features +Automated account registration and token refreshing to free your hands. + +## Important Notes +1. **Ensure you have Chrome installed. If not, [download here](https://www.google.com/intl/en_pk/chrome/).** +2. **You must log into your account, regardless of its validity. Logged-in is mandatory.** +3. **A stable internet connection is required, preferably via an overseas node. Do not enable global proxy.** + +## Configuration Instructions +Please refer to our [online documentation](https://cursor-auto-free-doc.vercel.app) for detailed configuration instructions. + +## Download +[https://github.com/chengazhen/cursor-auto-free/releases](https://github.com/chengazhen/cursor-auto-free/releases) + +## Update Log +- **2025-01-09**: Added logs and auto-build feature. +- **2025-01-10**: Switched to Cloudflare domain email. +- **2025-01-11**: Added headless mode and proxy configuration through .env file. +- **2025-01-20**: Added IMAP to replace tempmail.plus. + +Inspired by [gpt-cursor-auto](https://github.com/hmhm2022/gpt-cursor-auto); optimized verification and email auto-registration logic; solved the issue of not being able to receive email verification codes. diff --git a/account_manager.py b/account_manager.py new file mode 100644 index 0000000..9bb118b --- /dev/null +++ b/account_manager.py @@ -0,0 +1,128 @@ +import json +import os +import time +from datetime import datetime +import requests +from typing import Dict, Optional +from config import Config +from logger import logging + +class AccountManager: + def __init__(self, api_base_url: str = None): + self.config = Config() + self.api_base_url = api_base_url or os.getenv("API_BASE_URL", "http://api.example.com") + self.accounts_file = "accounts.json" + self._ensure_accounts_file() + + def _ensure_accounts_file(self): + """确保accounts.json文件存在""" + if not os.path.exists(self.accounts_file): + with open(self.accounts_file, "w", encoding="utf-8") as f: + json.dump({"accounts": []}, f, ensure_ascii=False, indent=2) + + def save_account(self, account_info: Dict) -> bool: + """ + 保存账号信息到本地JSON文件 + + Args: + account_info: 包含账号信息的字典 + Returns: + bool: 保存是否成功 + """ + try: + # 添加时间戳 + account_info["created_at"] = datetime.now().isoformat() + + # 读取现有数据 + with open(self.accounts_file, "r", encoding="utf-8") as f: + data = json.load(f) + + # 添加新账号 + data["accounts"].append(account_info) + + # 保存回文件 + with open(self.accounts_file, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + logging.info(f"账号信息已保存到本地: {account_info['email']}") + return True + + except Exception as e: + logging.error(f"保存账号信息到本地失败: {str(e)}") + return False + + def sync_to_server(self, account_info: Dict) -> bool: + """ + 同步账号信息到服务器 + + Args: + account_info: 包含账号信息的字典 + Returns: + bool: 同步是否成功 + """ + try: + # 构建API请求 + endpoint = f"{self.api_base_url}/api/accounts" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {os.getenv('API_TOKEN', '')}" + } + + # 发送POST请求 + response = requests.post( + endpoint, + json=account_info, + headers=headers, + timeout=10 + ) + + if response.status_code == 200: + logging.info(f"账号信息已同步到服务器: {account_info['email']}") + return True + else: + logging.error(f"同步到服务器失败: {response.status_code} - {response.text}") + return False + + except Exception as e: + logging.error(f"同步账号信息到服务器失败: {str(e)}") + return False + + def process_account(self, account_info: Dict) -> bool: + """ + 处理账号信息:保存到本地并同步到服务器 + + Args: + account_info: 包含账号信息的字典 + Returns: + bool: 处理是否成功 + """ + # 保存到本地 + local_save_success = self.save_account(account_info) + + # 同步到服务器 + server_sync_success = self.sync_to_server(account_info) + + return local_save_success and server_sync_success + + def get_account_info(self, email: str) -> Optional[Dict]: + """ + 获取指定邮箱的账号信息 + + Args: + email: 邮箱地址 + Returns: + Optional[Dict]: 账号信息或None + """ + try: + with open(self.accounts_file, "r", encoding="utf-8") as f: + data = json.load(f) + + for account in data["accounts"]: + if account["email"] == email: + return account + + return None + + except Exception as e: + logging.error(f"获取账号信息失败: {str(e)}") + return None \ No newline at end of file diff --git a/accounts.json b/accounts.json new file mode 100644 index 0000000..3519150 --- /dev/null +++ b/accounts.json @@ -0,0 +1,40 @@ +{ + "accounts": [ + { + "email": "ooqxgnua245213@nosqli.com", + "password": "!lzWkDxZpgs@", + "first_name": "Vhztdm", + "last_name": "Jfzpkv", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSEdOTTI3NENEUTVLWU4wWVBaUlowIiwidGltZSI6IjE3MzkyNDUyNzAiLCJyYW5kb21uZXNzIjoiYTllZGIxOWMtOTRlYi00YWNlIiwiZXhwIjo0MzMxMjQ1MjcwLCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.SwUHxP_n9zp-GVUTNi2M3t7GbD5jsAJ0YqRZjkqQyUc", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSEdOTTI3NENEUTVLWU4wWVBaUlowIiwidGltZSI6IjE3MzkyNDUyNzAiLCJyYW5kb21uZXNzIjoiYTllZGIxOWMtOTRlYi00YWNlIiwiZXhwIjo0MzMxMjQ1MjcwLCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.SwUHxP_n9zp-GVUTNi2M3t7GbD5jsAJ0YqRZjkqQyUc", + "machine_id": "", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + "registration_time": "2025-02-11T11:41:41.973383", + "created_at": "2025-02-11T11:41:41.973383" + }, + { + "email": "sdqqdjga245322@nosqli.com", + "password": "!lzWkDxZpgs@", + "first_name": "Pibfln", + "last_name": "Wpofth", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSE0zR0cxUDVXQVg3UU1HV1A4OTQwIiwidGltZSI6IjE3MzkyNDUzODQiLCJyYW5kb21uZXNzIjoiNjc2MDU0NTMtNTM2NC00Mjc3IiwiZXhwIjo0MzMxMjQ1Mzg0LCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.zbUv135w7aYz1EReHi0n_I0DvaBFHcrXIwDieFTMX_c", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSE0zR0cxUDVXQVg3UU1HV1A4OTQwIiwidGltZSI6IjE3MzkyNDUzODQiLCJyYW5kb21uZXNzIjoiNjc2MDU0NTMtNTM2NC00Mjc3IiwiZXhwIjo0MzMxMjQ1Mzg0LCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.zbUv135w7aYz1EReHi0n_I0DvaBFHcrXIwDieFTMX_c", + "machine_id": "", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + "registration_time": "2025-02-11T11:43:50.996557", + "created_at": "2025-02-11T11:43:50.996557" + }, + { + "email": "sflujanc245451@nosqli.com", + "password": "!lzWkDxZpgs@", + "first_name": "Zohrnj", + "last_name": "Tioakn", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSFFZV1dSTkVHM0ZEUFJXU1haRTc2IiwidGltZSI6IjE3MzkyNDU1MTEiLCJyYW5kb21uZXNzIjoiZTgzYWUyNDgtZDliOC00YjgyIiwiZXhwIjo0MzMxMjQ1NTExLCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.kqbGsapRB1goNEtwiiUuj_3DkzWSXB4GSDFee00UTuE", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxSktTSFFZV1dSTkVHM0ZEUFJXU1haRTc2IiwidGltZSI6IjE3MzkyNDU1MTEiLCJyYW5kb21uZXNzIjoiZTgzYWUyNDgtZDliOC00YjgyIiwiZXhwIjo0MzMxMjQ1NTExLCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20ifQ.kqbGsapRB1goNEtwiiUuj_3DkzWSXB4GSDFee00UTuE", + "machine_id": "", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + "registration_time": "2025-02-11T11:45:42.926280", + "created_at": "2025-02-11T11:45:42.926280" + } + ] +} \ No newline at end of file diff --git a/browser_utils.py b/browser_utils.py new file mode 100644 index 0000000..77e333a --- /dev/null +++ b/browser_utils.py @@ -0,0 +1,69 @@ +from DrissionPage import ChromiumOptions, Chromium +import sys +import os +import logging +from dotenv import load_dotenv + +load_dotenv() + + +class BrowserManager: + def __init__(self): + self.browser = None + + def init_browser(self, user_agent=None): + """初始化浏览器""" + co = self._get_browser_options(user_agent) + self.browser = Chromium(co) + return self.browser + + def _get_browser_options(self, user_agent=None): + """获取浏览器配置""" + co = ChromiumOptions() + try: + extension_path = self._get_extension_path() + co.add_extension(extension_path) + except FileNotFoundError as e: + logging.warning(f"警告: {e}") + + co.set_pref("credentials_enable_service", False) + co.set_argument("--hide-crash-restore-bubble") + proxy = os.getenv("BROWSER_PROXY") + if proxy: + co.set_proxy(proxy) + + co.auto_port() + if user_agent: + co.set_user_agent(user_agent) + + co.headless( + os.getenv("BROWSER_HEADLESS", "True").lower() == "true" + ) # 生产环境使用无头模式 + + # Mac 系统特殊处理 + if sys.platform == "darwin": + co.set_argument("--no-sandbox") + co.set_argument("--disable-gpu") + + return co + + def _get_extension_path(self): + """获取插件路径""" + root_dir = os.getcwd() + extension_path = os.path.join(root_dir, "turnstilePatch") + + if hasattr(sys, "_MEIPASS"): + extension_path = os.path.join(sys._MEIPASS, "turnstilePatch") + + if not os.path.exists(extension_path): + raise FileNotFoundError(f"插件不存在: {extension_path}") + + return extension_path + + def quit(self): + """关闭浏览器""" + if self.browser: + try: + self.browser.quit() + except: + pass diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..3249dca --- /dev/null +++ b/build.bat @@ -0,0 +1,32 @@ +@echo off +set PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage +echo Building Cursor Keep Alive... + +:: Check if virtual environment exists +if not exist "venv" ( + python -m venv venv + if errorlevel 1 ( + echo Failed to create virtual environment! + exit /b 1 + ) +) + +:: Activate virtual environment and wait for activation to complete +call venv\Scripts\activate.bat +timeout /t 2 /nobreak > nul + +:: Install dependencies +echo Installing dependencies... +python -m pip install --upgrade pip +pip install -r requirements.txt + +:: Run build script +echo Starting build process... +python build.py + +:: Deactivate virtual environment +deactivate + +:: Keep window open +echo Build completed! +pause \ No newline at end of file diff --git a/build.mac.command b/build.mac.command new file mode 100644 index 0000000..6fd4bb4 --- /dev/null +++ b/build.mac.command @@ -0,0 +1,33 @@ +#!/bin/bash +export PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage + +# Get script directory +cd "$(dirname "$0")" + +echo "Creating virtual environment..." + +# Check if virtual environment exists +if [ ! -d "venv" ]; then + python3 -m venv venv + if [ $? -ne 0 ]; then + echo "Failed to create virtual environment!" + exit 1 + fi +fi + +# Activate virtual environment +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +python -m pip install --upgrade pip +pip install -r requirements.txt + +# Run build script +echo "Starting build process..." +python build.py + +# Keep window open +echo "Build completed!" +echo "Press any key to exit..." +read -n 1 \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..2bc5e4f --- /dev/null +++ b/build.py @@ -0,0 +1,179 @@ +import warnings +import os +import platform +import subprocess +import time +import threading + +# Ignore specific SyntaxWarning +warnings.filterwarnings("ignore", category=SyntaxWarning, module="DrissionPage") + +CURSOR_LOGO = """ + ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ + ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ + ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ + ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ + ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ +""" + + +class LoadingAnimation: + def __init__(self): + self.is_running = False + self.animation_thread = None + + def start(self, message="Building"): + self.is_running = True + self.animation_thread = threading.Thread(target=self._animate, args=(message,)) + self.animation_thread.start() + + def stop(self): + self.is_running = False + if self.animation_thread: + self.animation_thread.join() + print("\r" + " " * 70 + "\r", end="", flush=True) # Clear the line + + def _animate(self, message): + animation = "|/-\\" + idx = 0 + while self.is_running: + print(f"\r{message} {animation[idx % len(animation)]}", end="", flush=True) + idx += 1 + time.sleep(0.1) + + +def print_logo(): + print("\033[96m" + CURSOR_LOGO + "\033[0m") + print("\033[93m" + "Building Cursor Keep Alive...".center(56) + "\033[0m\n") + + +def progress_bar(progress, total, prefix="", length=50): + filled = int(length * progress // total) + bar = "█" * filled + "░" * (length - filled) + percent = f"{100 * progress / total:.1f}" + print(f"\r{prefix} |{bar}| {percent}% Complete", end="", flush=True) + if progress == total: + print() + + +def simulate_progress(message, duration=1.0, steps=20): + print(f"\033[94m{message}\033[0m") + for i in range(steps + 1): + time.sleep(duration / steps) + progress_bar(i, steps, prefix="Progress:", length=40) + + +def filter_output(output): + """ImportantMessage""" + if not output: + return "" + important_lines = [] + for line in output.split("\n"): + # Only keep lines containing specific keywords + if any( + keyword in line.lower() + for keyword in ["error:", "failed:", "completed", "directory:"] + ): + important_lines.append(line) + return "\n".join(important_lines) + + +def build(): + # Clear screen + os.system("cls" if platform.system().lower() == "windows" else "clear") + + # Print logo + print_logo() + + system = platform.system().lower() + spec_file = os.path.join("CursorKeepAlive.spec") + + # if system not in ["darwin", "windows"]: + # print(f"\033[91mUnsupported operating system: {system}\033[0m") + # return + + output_dir = f"dist/{system if system != 'darwin' else 'mac'}" + + # Create output directory + os.makedirs(output_dir, exist_ok=True) + simulate_progress("Creating output directory...", 0.5) + + # Run PyInstaller with loading animation + pyinstaller_command = [ + "pyinstaller", + spec_file, + "--distpath", + output_dir, + "--workpath", + f"build/{system}", + "--noconfirm", + ] + + loading = LoadingAnimation() + try: + simulate_progress("Running PyInstaller...", 2.0) + loading.start("Building in progress") + result = subprocess.run( + pyinstaller_command, check=True, capture_output=True, text=True + ) + loading.stop() + + if result.stderr: + filtered_errors = [ + line + for line in result.stderr.split("\n") + if any( + keyword in line.lower() + for keyword in ["error:", "failed:", "completed", "directory:"] + ) + ] + if filtered_errors: + print("\033[93mBuild Warnings/Errors:\033[0m") + print("\n".join(filtered_errors)) + + except subprocess.CalledProcessError as e: + loading.stop() + print(f"\033[91mBuild failed with error code {e.returncode}\033[0m") + if e.stderr: + print("\033[91mError Details:\033[0m") + print(e.stderr) + return + except FileNotFoundError: + loading.stop() + print( + "\033[91mError: Please ensure PyInstaller is installed (pip install pyinstaller)\033[0m" + ) + return + except KeyboardInterrupt: + loading.stop() + print("\n\033[91mBuild cancelled by user\033[0m") + return + finally: + loading.stop() + + # Copy config file + if os.path.exists("config.ini.example"): + simulate_progress("Copying configuration file...", 0.5) + if system == "windows": + subprocess.run( + ["copy", "config.ini.example", f"{output_dir}\\config.ini"], shell=True + ) + else: + subprocess.run(["cp", "config.ini.example", f"{output_dir}/config.ini"]) + + # Copy .env.example file + if os.path.exists(".env.example"): + simulate_progress("Copying environment file...", 0.5) + if system == "windows": + subprocess.run(["copy", ".env.example", f"{output_dir}\\.env"], shell=True) + else: + subprocess.run(["cp", ".env.example", f"{output_dir}/.env"]) + + print( + f"\n\033[92mBuild completed successfully! Output directory: {output_dir}\033[0m" + ) + + +if __name__ == "__main__": + build() diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..a9e477e --- /dev/null +++ b/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash +export PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage + +echo "Creating virtual environment..." + +# Check if virtual environment exists +if [ ! -d "venv" ]; then + python3 -m venv venv + if [ $? -ne 0 ]; then + echo "Failed to create virtual environment!" + exit 1 + fi +fi + +# Activate virtual environment +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +python -m pip install --upgrade pip +pip install -r requirements.txt + +# Run build script +echo "Starting build process..." +python build.py + +# Complete +echo "Build completed!" \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..69944ac --- /dev/null +++ b/config.py @@ -0,0 +1,146 @@ +from dotenv import load_dotenv +import os +import sys +from logger import logging + + +class Config: + def __init__(self): + # 获取应用程序的根目录路径 + if getattr(sys, "frozen", False): + # 如果是打包后的可执行文件 + application_path = os.path.dirname(sys.executable) + else: + # 如果是开发环境 + application_path = os.path.dirname(os.path.abspath(__file__)) + + # 指定 .env 文件的路径 + dotenv_path = os.path.join(application_path, ".env") + + if not os.path.exists(dotenv_path): + raise FileNotFoundError(f"文件 {dotenv_path} 不存在") + + # 加载 .env 文件 + load_dotenv(dotenv_path) + + self.imap = False + self.temp_mail = os.getenv("TEMP_MAIL", "").strip().split("@")[0] + self.temp_mail_epin = os.getenv("TEMP_MAIL_EPIN", "").strip() + self.temp_mail_ext = os.getenv("TEMP_MAIL_EXT", "").strip() + self.domain = os.getenv("DOMAIN", "").strip() + + # 如果临时邮箱为null则加载IMAP + if self.temp_mail == "null": + self.imap = True + self.imap_server = os.getenv("IMAP_SERVER", "").strip() + self.imap_port = os.getenv("IMAP_PORT", "").strip() + self.imap_user = os.getenv("IMAP_USER", "").strip() + self.imap_pass = os.getenv("IMAP_PASS", "").strip() + self.imap_dir = os.getenv("IMAP_DIR", "inbox").strip() + + self.check_config() + + def get_temp_mail(self): + + return self.temp_mail + + def get_temp_mail_epin(self): + + return self.temp_mail_epin + + def get_temp_mail_ext(self): + + return self.temp_mail_ext + + def get_imap(self): + if not self.imap: + return False + return { + "imap_server": self.imap_server, + "imap_port": self.imap_port, + "imap_user": self.imap_user, + "imap_pass": self.imap_pass, + "imap_dir": self.imap_dir, + } + + def get_domain(self): + return self.domain + + def check_config(self): + """检查配置项是否有效 + + 检查规则: + 1. 如果使用 tempmail.plus,需要配置 TEMP_MAIL 和 DOMAIN + 2. 如果使用 IMAP,需要配置 IMAP_SERVER、IMAP_PORT、IMAP_USER、IMAP_PASS + 3. IMAP_DIR 是可选的 + """ + # 基础配置检查 + required_configs = { + "domain": "域名", + } + + # 检查基础配置 + for key, name in required_configs.items(): + if not self.check_is_valid(getattr(self, key)): + raise ValueError(f"{name}未配置,请在 .env 文件中设置 {key.upper()}") + + # 检查邮箱配置 + if self.temp_mail != "null": + # tempmail.plus 模式 + if not self.check_is_valid(self.temp_mail): + raise ValueError("临时邮箱未配置,请在 .env 文件中设置 TEMP_MAIL") + else: + # IMAP 模式 + imap_configs = { + "imap_server": "IMAP服务器", + "imap_port": "IMAP端口", + "imap_user": "IMAP用户名", + "imap_pass": "IMAP密码", + } + + for key, name in imap_configs.items(): + value = getattr(self, key) + if value == "null" or not self.check_is_valid(value): + raise ValueError( + f"{name}未配置,请在 .env 文件中设置 {key.upper()}" + ) + + # IMAP_DIR 是可选的,如果设置了就检查其有效性 + if self.imap_dir != "null" and not self.check_is_valid(self.imap_dir): + raise ValueError( + "IMAP收件箱目录配置无效,请在 .env 文件中正确设置 IMAP_DIR" + ) + + def check_is_valid(self, value): + """检查配置项是否有效 + + Args: + value: 配置项的值 + + Returns: + bool: 配置项是否有效 + """ + return isinstance(value, str) and len(str(value).strip()) > 0 + + def print_config(self): + if self.imap: + logging.info(f"\033[32mIMAP服务器: {self.imap_server}\033[0m") + logging.info(f"\033[32mIMAP端口: {self.imap_port}\033[0m") + logging.info(f"\033[32mIMAP用户名: {self.imap_user}\033[0m") + logging.info(f"\033[32mIMAP密码: {'*' * len(self.imap_pass)}\033[0m") + logging.info(f"\033[32mIMAP收件箱目录: {self.imap_dir}\033[0m") + if self.temp_mail != "null": + logging.info( + f"\033[32m临时邮箱: {self.temp_mail}{self.temp_mail_ext}\033[0m" + ) + logging.info(f"\033[32m域名: {self.domain}\033[0m") + + +# 使用示例 +if __name__ == "__main__": + try: + config = Config() + print("环境变量加载成功!") + config.print_config() + except ValueError as e: + print(f"错误: {e}") diff --git a/cursor_auth_manager.py b/cursor_auth_manager.py new file mode 100644 index 0000000..3ad5579 --- /dev/null +++ b/cursor_auth_manager.py @@ -0,0 +1,86 @@ +import sqlite3 +import os +import sys + + +class CursorAuthManager: + """Cursor认证信息管理器""" + + def __init__(self): + # 判断操作系统 + if sys.platform == "win32": # Windows + appdata = os.getenv("APPDATA") + if appdata is None: + raise EnvironmentError("APPDATA 环境变量未设置") + self.db_path = os.path.join( + appdata, "Cursor", "User", "globalStorage", "state.vscdb" + ) + elif sys.platform == "darwin": # macOS + self.db_path = os.path.abspath(os.path.expanduser( + "~/Library/Application Support/Cursor/User/globalStorage/state.vscdb" + )) + elif sys.platform == "linux" : # Linux 和其他类Unix系统 + self.db_path = os.path.abspath(os.path.expanduser( + "~/.config/Cursor/User/globalStorage/state.vscdb" + )) + else: + raise NotImplementedError(f"不支持的操作系统: {sys.platform}") + + def update_auth(self, email=None, access_token=None, refresh_token=None): + """ + 更新Cursor的认证信息 + :param email: 新的邮箱地址 + :param access_token: 新的访问令牌 + :param refresh_token: 新的刷新令牌 + :return: bool 是否成功更新 + """ + updates = [] + # 登录状态 + updates.append(("cursorAuth/cachedSignUpType", "Auth_0")) + + if email is not None: + updates.append(("cursorAuth/cachedEmail", email)) + if access_token is not None: + updates.append(("cursorAuth/accessToken", access_token)) + if refresh_token is not None: + updates.append(("cursorAuth/refreshToken", refresh_token)) + + if not updates: + print("没有提供任何要更新的值") + return False + + conn = None + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + for key, value in updates: + + # 如果没有更新任何行,说明key不存在,执行插入 + # 检查 accessToken 是否存在 + check_query = f"SELECT COUNT(*) FROM itemTable WHERE key = ?" + cursor.execute(check_query, (key,)) + if cursor.fetchone()[0] == 0: + insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)" + cursor.execute(insert_query, (key, value)) + else: + update_query = "UPDATE itemTable SET value = ? WHERE key = ?" + cursor.execute(update_query, (value, key)) + + if cursor.rowcount > 0: + print(f"成功更新 {key.split('/')[-1]}") + else: + print(f"未找到 {key.split('/')[-1]} 或值未变化") + + conn.commit() + return True + + except sqlite3.Error as e: + print("数据库错误:", str(e)) + return False + except Exception as e: + print("发生错误:", str(e)) + return False + finally: + if conn: + conn.close() diff --git a/cursor_pro_keep_alive.py b/cursor_pro_keep_alive.py new file mode 100644 index 0000000..663cf06 --- /dev/null +++ b/cursor_pro_keep_alive.py @@ -0,0 +1,596 @@ +import os +import platform +import json +import sys +from colorama import Fore, Style +from enum import Enum +from typing import Optional + +from exit_cursor import ExitCursor +import patch_cursor_get_machine_id +from reset_machine import MachineIDResetter +from account_manager import AccountManager + +os.environ["PYTHONVERBOSE"] = "0" +os.environ["PYINSTALLER_VERBOSE"] = "0" + +import time +import random +from cursor_auth_manager import CursorAuthManager +import os +from logger import logging +from browser_utils import BrowserManager +from get_email_code import EmailVerificationHandler +from logo import print_logo +from config import Config +from datetime import datetime + +# 定义全局变量 +LOGIN_URL = "https://authenticator.cursor.sh" +SIGN_UP_URL = "https://authenticator.cursor.sh/sign-up" +SETTINGS_URL = "https://www.cursor.com/settings" +MAIL_URL = "https://tempmail.plus" + +# 定义 EMOJI 字典 +EMOJI = {"ERROR": "❌", "WARNING": "⚠️", "INFO": "ℹ️"} + + +class VerificationStatus(Enum): + """验证状态枚举""" + + PASSWORD_PAGE = "@name=password" + CAPTCHA_PAGE = "@data-index=0" + ACCOUNT_SETTINGS = "Account Settings" + + +class TurnstileError(Exception): + """Turnstile 验证相关异常""" + + pass + + +def save_screenshot(tab, stage: str, timestamp: bool = True) -> None: + """ + 保存页面截图 + + Args: + tab: 浏览器标签页对象 + stage: 截图阶段标识 + timestamp: 是否添加时间戳 + """ + try: + # 创建 screenshots 目录 + screenshot_dir = "screenshots" + if not os.path.exists(screenshot_dir): + os.makedirs(screenshot_dir) + + # 生成文件名 + if timestamp: + filename = f"turnstile_{stage}_{int(time.time())}.png" + else: + filename = f"turnstile_{stage}.png" + + filepath = os.path.join(screenshot_dir, filename) + + # 保存截图 + tab.get_screenshot(filepath) + logging.debug(f"截图已保存: {filepath}") + except Exception as e: + logging.warning(f"截图保存失败: {str(e)}") + + +def check_verification_success(tab) -> Optional[VerificationStatus]: + """ + 检查验证是否成功 + + Returns: + VerificationStatus: 验证成功时返回对应状态,失败返回 None + """ + for status in VerificationStatus: + if tab.ele(status.value): + logging.info(f"验证成功 - 已到达{status.name}页面") + return status + return None + + +def handle_turnstile(tab, max_retries: int = 2, retry_interval: tuple = (1, 2)) -> bool: + """ + 处理 Turnstile 验证 + + Args: + tab: 浏览器标签页对象 + max_retries: 最大重试次数 + retry_interval: 重试间隔时间范围(最小值, 最大值) + + Returns: + bool: 验证是否成功 + + Raises: + TurnstileError: 验证过程中出现异常 + """ + logging.info("正在检测 Turnstile 验证...") + save_screenshot(tab, "start") + + retry_count = 0 + + try: + while retry_count < max_retries: + retry_count += 1 + logging.debug(f"第 {retry_count} 次尝试验证") + + try: + # 定位验证框元素 + challenge_check = ( + tab.ele("@id=cf-turnstile", timeout=2) + .child() + .shadow_root.ele("tag:iframe") + .ele("tag:body") + .sr("tag:input") + ) + + if challenge_check: + logging.info("检测到 Turnstile 验证框,开始处理...") + # 随机延时后点击验证 + time.sleep(random.uniform(1, 3)) + challenge_check.click() + time.sleep(2) + + # 保存验证后的截图 + save_screenshot(tab, "clicked") + + # 检查验证结果 + if check_verification_success(tab): + logging.info("Turnstile 验证通过") + save_screenshot(tab, "success") + return True + + except Exception as e: + logging.debug(f"当前尝试未成功: {str(e)}") + + # 检查是否已经验证成功 + if check_verification_success(tab): + return True + + # 随机延时后继续下一次尝试 + time.sleep(random.uniform(*retry_interval)) + + # 超出最大重试次数 + logging.error(f"验证失败 - 已达到最大重试次数 {max_retries}") + save_screenshot(tab, "failed") + return False + + except Exception as e: + error_msg = f"Turnstile 验证过程发生异常: {str(e)}" + logging.error(error_msg) + save_screenshot(tab, "error") + raise TurnstileError(error_msg) + + +def get_cursor_session_token(tab, max_attempts=3, retry_interval=2): + """ + 获取Cursor会话token,带有重试机制 + :param tab: 浏览器标签页 + :param max_attempts: 最大尝试次数 + :param retry_interval: 重试间隔(秒) + :return: session token 或 None + """ + logging.info("开始获取cookie") + attempts = 0 + + while attempts < max_attempts: + try: + cookies = tab.cookies() + for cookie in cookies: + if cookie.get("name") == "WorkosCursorSessionToken": + return cookie["value"].split("%3A%3A")[1] + + attempts += 1 + if attempts < max_attempts: + logging.warning( + f"第 {attempts} 次尝试未获取到CursorSessionToken,{retry_interval}秒后重试..." + ) + time.sleep(retry_interval) + else: + logging.error( + f"已达到最大尝试次数({max_attempts}),获取CursorSessionToken失败" + ) + + except Exception as e: + logging.error(f"获取cookie失败: {str(e)}") + attempts += 1 + if attempts < max_attempts: + logging.info(f"将在 {retry_interval} 秒后重试...") + time.sleep(retry_interval) + + return None + + +def update_cursor_auth(email=None, access_token=None, refresh_token=None): + """ + 更新Cursor的认证信息的便捷函数 + """ + auth_manager = CursorAuthManager() + return auth_manager.update_auth(email, access_token, refresh_token) + + +def sign_up_account(browser, tab): + """ + 注册账号流程 + + Args: + browser: 浏览器实例 + tab: 浏览器标签页 + Returns: + bool: 注册是否成功 + """ + global account, password, first_name, last_name, email_handler + + if not all([account, password, first_name, last_name, email_handler]): + logging.error("账号信息未完整初始化") + return False + + logging.info("=== 开始注册账号流程 ===") + logging.info(f"正在访问注册页面: {SIGN_UP_URL}") + tab.get(SIGN_UP_URL) + + try: + if tab.ele("@name=first_name"): + logging.info("正在填写个人信息...") + logging.info(f"账号信息: {first_name} {last_name} ({account})") + + tab.actions.click("@name=first_name").input(first_name) + logging.info(f"已输入名字: {first_name}") + time.sleep(random.uniform(1, 3)) + + tab.actions.click("@name=last_name").input(last_name) + logging.info(f"已输入姓氏: {last_name}") + time.sleep(random.uniform(1, 3)) + + tab.actions.click("@name=email").input(account) + logging.info(f"已输入邮箱: {account}") + time.sleep(random.uniform(1, 3)) + + logging.info("提交个人信息...") + tab.actions.click("@type=submit") + + except Exception as e: + logging.error(f"注册页面访问失败: {str(e)}") + return False + + handle_turnstile(tab) + + try: + if tab.ele("@name=password"): + logging.info("正在设置密码...") + tab.ele("@name=password").input(password) + time.sleep(random.uniform(1, 3)) + + logging.info("提交密码...") + tab.ele("@type=submit").click() + logging.info("密码设置完成,等待系统响应...") + + except Exception as e: + logging.error(f"密码设置失败: {str(e)}") + return False + + if tab.ele("This email is not available."): + logging.error("注册失败:邮箱已被使用") + return False + + handle_turnstile(tab) + + while True: + try: + if tab.ele("Account Settings"): + logging.info("注册成功 - 已进入账户设置页面") + break + if tab.ele("@data-index=0"): + logging.info("正在获取邮箱验证码...") + code = email_handler.get_verification_code() + if not code: + logging.error("获取验证码失败") + return False + + logging.info(f"成功获取验证码: {code}") + logging.info("正在输入验证码...") + i = 0 + for digit in code: + tab.ele(f"@data-index={i}").input(digit) + time.sleep(random.uniform(0.1, 0.3)) + i += 1 + logging.info("验证码输入完成") + break + except Exception as e: + logging.error(f"验证码处理过程出错: {str(e)}") + + handle_turnstile(tab) + wait_time = random.randint(3, 6) + for i in range(wait_time): + logging.info(f"等待系统处理中... 剩余 {wait_time-i} 秒") + time.sleep(1) + + logging.info("正在获取账户信息...") + tab.get(SETTINGS_URL) + try: + usage_selector = ( + "css:div.col-span-2 > div > div > div > div > " + "div:nth-child(1) > div.flex.items-center.justify-between.gap-2 > " + "span.font-mono.text-sm\\/\\[0\\.875rem\\]" + ) + usage_ele = tab.ele(usage_selector) + if usage_ele: + usage_info = usage_ele.text + total_usage = usage_info.split("/")[-1].strip() + logging.info(f"账户可用额度上限: {total_usage}") + except Exception as e: + logging.error(f"获取账户额度信息失败: {str(e)}") + + logging.info("\n=== 注册完成 ===") + account_info = f"Cursor 账号信息:\n邮箱: {account}\n密码: {password}" + logging.info(account_info) + time.sleep(5) + return True + + +class EmailGenerator: + def __init__( + self, + password="".join( + random.choices( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*", + k=12, + ) + ), + ): + configInstance = Config() + configInstance.print_config() + self.domain = configInstance.get_domain() + self.default_password = password + self.default_first_name = self.generate_random_name() + self.default_last_name = self.generate_random_name() + + def generate_random_name(self, length=6): + """生成随机用户名""" + first_letter = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + rest_letters = "".join( + random.choices("abcdefghijklmnopqrstuvwxyz", k=length - 1) + ) + return first_letter + rest_letters + + def generate_email(self, length=8): + """生成随机邮箱地址""" + random_str = "".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=length)) + timestamp = str(int(time.time()))[-6:] # 使用时间戳后6位 + return f"{random_str}{timestamp}@{self.domain}" + + def get_account_info(self): + """获取完整的账号信息""" + return { + "email": self.generate_email(), + "password": self.default_password, + "first_name": self.default_first_name, + "last_name": self.default_last_name, + } + + +def get_user_agent(): + """获取user_agent""" + try: + # 使用JavaScript获取user agent + browser_manager = BrowserManager() + browser = browser_manager.init_browser() + user_agent = browser.latest_tab.run_js("return navigator.userAgent") + browser_manager.quit() + return user_agent + except Exception as e: + logging.error(f"获取user agent失败: {str(e)}") + return None + + +def check_cursor_version(): + """检查cursor版本""" + pkg_path, main_path = patch_cursor_get_machine_id.get_cursor_paths() + with open(pkg_path, "r", encoding="utf-8") as f: + version = json.load(f)["version"] + return patch_cursor_get_machine_id.version_check(version, min_version="0.45.0") + + +def reset_machine_id(greater_than_0_45): + if greater_than_0_45: + # 提示请手动执行脚本 https://github.com/chengazhen/cursor-auto-free/blob/main/patch_cursor_get_machine_id.py + patch_cursor_get_machine_id.patch_cursor_get_machine_id() + else: + MachineIDResetter().reset_machine_ids() + + +def get_machine_id(): + """获取机器ID""" + try: + pkg_path, main_path = patch_cursor_get_machine_id.get_cursor_paths() + with open(pkg_path, "r", encoding="utf-8") as f: + pkg_data = json.load(f) + return pkg_data.get("machineId", "") + except Exception as e: + logging.error(f"获取机器ID失败: {str(e)}") + return "" + + +def run_registration_process(account_manager: AccountManager) -> bool: + """ + 执行单次注册流程 + + Args: + account_manager: AccountManager实例 + Returns: + bool: 注册是否成功 + """ + global account, password, first_name, last_name, email_handler, browser_manager + browser_manager = None + try: + logging.info("正在初始化浏览器...") + + # 获取user_agent + user_agent = get_user_agent() + if not user_agent: + logging.error("获取user agent失败,使用默认值") + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + + # 剔除user_agent中的"HeadlessChrome" + user_agent = user_agent.replace("HeadlessChrome", "Chrome") + + browser_manager = BrowserManager() + browser = browser_manager.init_browser(user_agent) + + # 获取并打印浏览器的user-agent + user_agent = browser.latest_tab.run_js("return navigator.userAgent") + + logging.info("正在初始化邮箱验证模块...") + email_handler = EmailVerificationHandler() + + logging.info("\n=== 配置信息 ===") + + logging.info("正在生成随机账号信息...") + email_generator = EmailGenerator() + # 生成新的账号信息并赋值给全局变量 + account = email_generator.generate_email() + password = email_generator.default_password + first_name = email_generator.default_first_name + last_name = email_generator.default_last_name + + logging.info(f"生成的邮箱账号: {account}") + logging.info(f"账号信息: 名字={first_name}, 姓氏={last_name}") + + tab = browser.latest_tab + tab.run_js("try { turnstile.reset() } catch(e) { }") + + logging.info("\n=== 开始注册流程 ===") + logging.info(f"正在访问登录页面: {LOGIN_URL}") + tab.get(LOGIN_URL) + + if sign_up_account(browser, tab): + logging.info("正在获取会话令牌...") + token = get_cursor_session_token(tab) + if token: + logging.info("更新认证信息...") + + # 获取机器ID + machine_id = get_machine_id() + + # 创建账号信息字典 + account_info = { + "email": account, + "password": password, + "first_name": first_name, + "last_name": last_name, + "access_token": token, + "refresh_token": token, + "machine_id": machine_id, + "user_agent": user_agent, + "registration_time": datetime.now().isoformat() + } + + # 保存并同步账号信息 + account_manager.process_account(account_info) + + # 更新Cursor认证 + # update_cursor_auth( + # email=account, access_token=token, refresh_token=token + # ) + + logging.info("重置机器码...") + reset_machine_id(greater_than_0_45) + logging.info("单次注册流程完成") + return True + else: + logging.error("获取会话令牌失败,注册流程未完成") + return False + return False + + except Exception as e: + logging.error(f"注册过程出现错误: {str(e)}") + import traceback + logging.error(traceback.format_exc()) + return False + + finally: + if browser_manager: + browser_manager.quit() + + +if __name__ == "__main__": + print_logo() + greater_than_0_45 = check_cursor_version() + account_manager = AccountManager() + + # 初始化全局变量 + account = None + password = None + first_name = None + last_name = None + email_handler = None + browser_manager = None + + try: + logging.info("\n=== 初始化程序 ===") + # ExitCursor() + + # 提示用户选择操作模式 + print("\n请选择操作模式:") + print("1. 仅重置机器码") + print("2. 单次完整注册流程") + print("3. 批量注册流程") + + while True: + try: + choice = int(input("请输入选项 (1, 2 或 3): ").strip()) + if choice in [1, 2, 3]: + break + else: + print("无效的选项,请重新输入") + except ValueError: + print("请输入有效的数字") + + if choice == 1: + # 仅执行重置机器码 + reset_machine_id(greater_than_0_45) + logging.info("机器码重置完成") + + elif choice == 2: + # 单次注册流程 + run_registration_process(account_manager) + + elif choice == 3: + # 批量注册流程 + while True: + try: + count = int(input("请输入要注册的账号数量: ").strip()) + if count > 0: + break + else: + print("请输入大于0的数字") + except ValueError: + print("请输入有效的数字") + + success_count = 0 + for i in range(count): + logging.info(f"\n=== 开始第 {i+1}/{count} 次注册 ===") + if run_registration_process(account_manager): + success_count += 1 + logging.info(f"成功完成第 {i+1} 次注册") + else: + logging.error(f"第 {i+1} 次注册失败") + + # 在每次注册之间添加随机延时 + if i < count - 1: # 如果不是最后一次注册 + delay = random.uniform(5, 15) + logging.info(f"等待 {delay:.1f} 秒后进行下一次注册...") + time.sleep(delay) + + logging.info(f"\n批量注册完成! 成功: {success_count}, 失败: {count-success_count}") + + except Exception as e: + logging.error(f"程序执行出现错误: {str(e)}") + import traceback + logging.error(traceback.format_exc()) + finally: + input("\n程序执行完毕,按回车键退出...") \ No newline at end of file diff --git a/exit_cursor.py b/exit_cursor.py new file mode 100644 index 0000000..cd5f576 --- /dev/null +++ b/exit_cursor.py @@ -0,0 +1,68 @@ +import psutil +from logger import logging +import time + +def ExitCursor(timeout=5): + """ + 温和地关闭 Cursor 进程 + + Args: + timeout (int): 等待进程自然终止的超时时间(秒) + Returns: + bool: 是否成功关闭所有进程 + """ + try: + logging.info("开始退出Cursor...") + cursor_processes = [] + # 收集所有 Cursor 进程 + for proc in psutil.process_iter(['pid', 'name']): + try: + if proc.info['name'].lower() in ['cursor.exe', 'cursor']: + cursor_processes.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if not cursor_processes: + logging.info("未发现运行中的 Cursor 进程") + return True + + # 温和地请求进程终止 + for proc in cursor_processes: + try: + if proc.is_running(): + proc.terminate() # 发送终止信号 + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + # 等待进程自然终止 + start_time = time.time() + while time.time() - start_time < timeout: + still_running = [] + for proc in cursor_processes: + try: + if proc.is_running(): + still_running.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if not still_running: + logging.info("所有 Cursor 进程已正常关闭") + return True + + # 等待一小段时间再检查 + time.sleep(0.5) + + # 如果超时后仍有进程在运行 + if still_running: + process_list = ", ".join([str(p.pid) for p in still_running]) + logging.warning(f"以下进程未能在规定时间内关闭: {process_list}") + return False + + return True + + except Exception as e: + logging.error(f"关闭 Cursor 进程时发生错误: {str(e)}") + return False + +if __name__ == "__main__": + ExitCursor() diff --git a/get_email_code.py b/get_email_code.py new file mode 100644 index 0000000..f0b5c3b --- /dev/null +++ b/get_email_code.py @@ -0,0 +1,172 @@ +import logging +import time +import re +from config import Config +import requests +import email +import imaplib + + +class EmailVerificationHandler: + def __init__(self): + self.imap = Config().get_imap() + self.username = Config().get_temp_mail() + self.epin = Config().get_temp_mail_epin() + self.session = requests.Session() + self.emailExtension = Config().get_temp_mail_ext() + + def get_verification_code(self): + code = None + + try: + print("正在处理...") + + if self.imap is False: + # 等待并获取最新邮件 + code, first_id = self._get_latest_mail_code() + # 清理邮件 + self._cleanup_mail(first_id) + else: + code = self._get_mail_code_by_imap() + + except Exception as e: + print(f"获取验证码失败: {str(e)}") + + return code + + # 使用imap获取邮件 + def _get_mail_code_by_imap(self, retry = 0): + if retry > 0: + time.sleep(3) + if retry >= 20: + raise Exception("获取验证码超时") + try: + # 连接到IMAP服务器 + mail = imaplib.IMAP4_SSL(self.imap['imap_server'], self.imap['imap_port']) + mail.login(self.imap['imap_user'], self.imap['imap_pass']) + mail.select(self.imap['imap_dir']) + + status, messages = mail.search(None, 'FROM', '"no-reply@cursor.sh"') + if status != 'OK': + return None + + mail_ids = messages[0].split() + if not mail_ids: + # 没有获取到,就在获取一次 + return self._get_mail_code_by_imap(retry=retry + 1) + + latest_mail_id = mail_ids[-1] + + # 获取邮件内容 + status, msg_data = mail.fetch(latest_mail_id, '(RFC822)') + if status != 'OK': + return None + + raw_email = msg_data[0][1] + email_message = email.message_from_bytes(raw_email) + + # 提取邮件正文 + body = self._extract_imap_body(email_message) + if body: + # 使用正则表达式查找6位数字验证码 + code_match = re.search(r"\b\d{6}\b", body) + if code_match: + code = code_match.group() + # 删除邮件 + mail.store(latest_mail_id, '+FLAGS', '\\Deleted') + mail.expunge() + mail.logout() + # print(f"找到的验证码: {code}") + return code + # print("未找到验证码") + mail.logout() + return None + except Exception as e: + print(f"发生错误: {e}") + return None + + def _extract_imap_body(self, email_message): + # 提取邮件正文 + if email_message.is_multipart(): + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + if content_type == "text/plain" and "attachment" not in content_disposition: + charset = part.get_content_charset() or 'utf-8' + try: + body = part.get_payload(decode=True).decode(charset, errors='ignore') + return body + except Exception as e: + logging.error(f"解码邮件正文失败: {e}") + else: + content_type = email_message.get_content_type() + if content_type == "text/plain": + charset = email_message.get_content_charset() or 'utf-8' + try: + body = email_message.get_payload(decode=True).decode(charset, errors='ignore') + return body + except Exception as e: + logging.error(f"解码邮件正文失败: {e}") + return "" + + # 手动输入验证码 + def _get_latest_mail_code(self): + # 获取邮件列表 + mail_list_url = f"https://tempmail.plus/api/mails?email={self.username}{self.emailExtension}&limit=20&epin={self.epin}" + mail_list_response = self.session.get(mail_list_url) + mail_list_data = mail_list_response.json() + time.sleep(0.5) + if not mail_list_data.get("result"): + return None, None + + # 获取最新邮件的ID + first_id = mail_list_data.get("first_id") + if not first_id: + return None, None + + # 获取具体邮件内容 + mail_detail_url = f"https://tempmail.plus/api/mails/{first_id}?email={self.username}{self.emailExtension}&epin={self.epin}" + mail_detail_response = self.session.get(mail_detail_url) + mail_detail_data = mail_detail_response.json() + time.sleep(0.5) + if not mail_detail_data.get("result"): + return None, None + + # 从邮件文本中提取6位数字验证码 + mail_text = mail_detail_data.get("text", "") + # 修改正则表达式,确保 6 位数字不紧跟在字母或域名相关符号后面 + code_match = re.search(r"(? logging.Logger: + """配置并返回logger实例""" + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +logger = setup_logging() + + +def get_cursor_paths() -> Tuple[str, str]: + """ + 根据不同操作系统获取 Cursor 相关路径 + + Returns: + Tuple[str, str]: (package.json路径, main.js路径)的元组 + + Raises: + OSError: 当找不到有效路径或系统不支持时抛出 + """ + system = platform.system() + + paths_map = { + "Darwin": { + "base": "/Applications/Cursor.app/Contents/Resources/app", + "package": "package.json", + "main": "out/main.js", + }, + "Windows": { + "base": os.path.join( + os.getenv("LOCALAPPDATA", ""), "Programs", "Cursor", "resources", "app" + ), + "package": "package.json", + "main": "out/main.js", + }, + "Linux": { + "bases": ["/opt/Cursor/resources/app", "/usr/share/cursor/resources/app"], + "package": "package.json", + "main": "out/main.js", + }, + } + + if system not in paths_map: + raise OSError(f"不支持的操作系统: {system}") + + if system == "Linux": + for base in paths_map["Linux"]["bases"]: + pkg_path = os.path.join(base, paths_map["Linux"]["package"]) + if os.path.exists(pkg_path): + return (pkg_path, os.path.join(base, paths_map["Linux"]["main"])) + raise OSError("在 Linux 系统上未找到 Cursor 安装路径") + + base_path = paths_map[system]["base"] + return ( + os.path.join(base_path, paths_map[system]["package"]), + os.path.join(base_path, paths_map[system]["main"]), + ) + + +def check_system_requirements(pkg_path: str, main_path: str) -> bool: + """ + 检查系统要求 + + Args: + pkg_path: package.json 文件路径 + main_path: main.js 文件路径 + + Returns: + bool: 检查是否通过 + """ + for file_path in [pkg_path, main_path]: + if not os.path.isfile(file_path): + logger.error(f"文件不存在: {file_path}") + return False + + if not os.access(file_path, os.W_OK): + logger.error(f"没有文件写入权限: {file_path}") + return False + + return True + + +def version_check(version: str, min_version: str = "", max_version: str = "") -> bool: + """ + 版本号检查 + + Args: + version: 当前版本号 + min_version: 最小版本号要求 + max_version: 最大版本号要求 + + Returns: + bool: 版本号是否符合要求 + """ + version_pattern = r"^\d+\.\d+\.\d+$" + try: + if not re.match(version_pattern, version): + logger.error(f"无效的版本号格式: {version}") + return False + + def parse_version(ver: str) -> Tuple[int, ...]: + return tuple(map(int, ver.split("."))) + + current = parse_version(version) + + if min_version and current < parse_version(min_version): + logger.error(f"版本号 {version} 小于最小要求 {min_version}") + return False + + if max_version and current > parse_version(max_version): + logger.error(f"版本号 {version} 大于最大要求 {max_version}") + return False + + return True + + except Exception as e: + logger.error(f"版本检查失败: {str(e)}") + return False + + +def modify_main_js(main_path: str) -> bool: + """ + 修改 main.js 文件 + + Args: + main_path: main.js 文件路径 + + Returns: + bool: 修改是否成功 + """ + try: + # 获取原始文件的权限和所有者信息 + original_stat = os.stat(main_path) + original_mode = original_stat.st_mode + original_uid = original_stat.st_uid + original_gid = original_stat.st_gid + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp_file: + with open(main_path, "r", encoding="utf-8") as main_file: + content = main_file.read() + + # 执行替换 + patterns = { + r"async getMachineId\(\)\{return [^??]+\?\?([^}]+)\}": r"async getMachineId(){return \1}", + r"async getMacMachineId\(\)\{return [^??]+\?\?([^}]+)\}": r"async getMacMachineId(){return \1}", + } + + for pattern, replacement in patterns.items(): + content = re.sub(pattern, replacement, content) + + tmp_file.write(content) + tmp_path = tmp_file.name + + # 使用 shutil.copy2 保留文件权限 + shutil.copy2(main_path, main_path + ".old") + shutil.move(tmp_path, main_path) + + # 恢复原始文件的权限和所有者 + os.chmod(main_path, original_mode) + if os.name != "nt": # 在非Windows系统上设置所有者 + os.chown(main_path, original_uid, original_gid) + + logger.info("文件修改成功") + return True + + except Exception as e: + logger.error(f"修改文件时发生错误: {str(e)}") + if "tmp_path" in locals(): + os.unlink(tmp_path) + return False + + +def backup_files(pkg_path: str, main_path: str) -> bool: + """ + 备份原始文件 + + Args: + pkg_path: package.json 文件路径(未使用) + main_path: main.js 文件路径 + + Returns: + bool: 备份是否成功 + """ + try: + # 只备份 main.js + if os.path.exists(main_path): + backup_main = f"{main_path}.bak" + shutil.copy2(main_path, backup_main) + logger.info(f"已备份 main.js: {backup_main}") + + return True + except Exception as e: + logger.error(f"备份文件失败: {str(e)}") + return False + + +def restore_backup_files(pkg_path: str, main_path: str) -> bool: + """ + 恢复备份文件 + + Args: + pkg_path: package.json 文件路径(未使用) + main_path: main.js 文件路径 + + Returns: + bool: 恢复是否成功 + """ + try: + # 只恢复 main.js + backup_main = f"{main_path}.bak" + if os.path.exists(backup_main): + shutil.copy2(backup_main, main_path) + logger.info(f"已恢复 main.js") + return True + + logger.error("未找到备份文件") + return False + except Exception as e: + logger.error(f"恢复备份失败: {str(e)}") + return False + + +def patch_cursor_get_machine_id(restore_mode=False) -> None: + """ + 主函数 + + Args: + restore_mode: 是否为恢复模式 + """ + logger.info("开始执行脚本...") + + try: + # 获取路径 + pkg_path, main_path = get_cursor_paths() + + # 检查系统要求 + if not check_system_requirements(pkg_path, main_path): + sys.exit(1) + + if restore_mode: + # 恢复备份 + if restore_backup_files(pkg_path, main_path): + logger.info("备份恢复完成") + else: + logger.error("备份恢复失败") + return + + # 获取版本号 + try: + with open(pkg_path, "r", encoding="utf-8") as f: + version = json.load(f)["version"] + logger.info(f"当前 Cursor 版本: {version}") + except Exception as e: + logger.error(f"无法读取版本号: {str(e)}") + sys.exit(1) + + # 检查版本 + if not version_check(version, min_version="0.45.0"): + logger.error("版本不符合要求(需 >= 0.45.x)") + sys.exit(1) + + logger.info("版本检查通过,准备修改文件") + + # 备份文件 + if not backup_files(pkg_path, main_path): + logger.error("文件备份失败,终止操作") + sys.exit(1) + + # 修改文件 + if not modify_main_js(main_path): + sys.exit(1) + + logger.info("脚本执行完成") + + except Exception as e: + logger.error(f"执行过程中发生错误: {str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + patch_cursor_get_machine_id() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a81fc6b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +DrissionPage==4.1.0.9 +colorama==0.4.6 +python-dotenv==1.0.0 +pyinstaller +playwright==1.41.1 +requests==2.31.0 \ No newline at end of file diff --git a/reset_machine.py b/reset_machine.py new file mode 100644 index 0000000..ddb9039 --- /dev/null +++ b/reset_machine.py @@ -0,0 +1,134 @@ +import os +import sys +import json +import uuid +import hashlib +import shutil +from colorama import Fore, Style, init + +# 初始化colorama +init() + +# 定义emoji和颜色常量 +EMOJI = { + "FILE": "📄", + "BACKUP": "💾", + "SUCCESS": "✅", + "ERROR": "❌", + "INFO": "ℹ️", + "RESET": "🔄", +} + + +class MachineIDResetter: + def __init__(self): + # 判断操作系统 + if sys.platform == "win32": # Windows + appdata = os.getenv("APPDATA") + if appdata is None: + raise EnvironmentError("APPDATA 环境变量未设置") + self.db_path = os.path.join( + appdata, "Cursor", "User", "globalStorage", "storage.json" + ) + elif sys.platform == "darwin": # macOS + self.db_path = os.path.abspath( + os.path.expanduser( + "~/Library/Application Support/Cursor/User/globalStorage/storage.json" + ) + ) + elif sys.platform == "linux": # Linux 和其他类Unix系统 + self.db_path = os.path.abspath( + os.path.expanduser("~/.config/Cursor/User/globalStorage/storage.json") + ) + else: + raise NotImplementedError(f"不支持的操作系统: {sys.platform}") + + def generate_new_ids(self): + """生成新的机器ID""" + # 生成新的UUID + dev_device_id = str(uuid.uuid4()) + + # 生成新的machineId (64个字符的十六进制) + machine_id = hashlib.sha256(os.urandom(32)).hexdigest() + + # 生成新的macMachineId (128个字符的十六进制) + mac_machine_id = hashlib.sha512(os.urandom(64)).hexdigest() + + # 生成新的sqmId + sqm_id = "{" + str(uuid.uuid4()).upper() + "}" + + return { + "telemetry.devDeviceId": dev_device_id, + "telemetry.macMachineId": mac_machine_id, + "telemetry.machineId": machine_id, + "telemetry.sqmId": sqm_id, + } + + def reset_machine_ids(self): + """重置机器ID并备份原文件""" + try: + print(f"{Fore.CYAN}{EMOJI['INFO']} 正在检查配置文件...{Style.RESET_ALL}") + + # 检查文件是否存在 + if not os.path.exists(self.db_path): + print( + f"{Fore.RED}{EMOJI['ERROR']} 配置文件不存在: {self.db_path}{Style.RESET_ALL}" + ) + return False + + # 检查文件权限 + if not os.access(self.db_path, os.R_OK | os.W_OK): + print( + f"{Fore.RED}{EMOJI['ERROR']} 无法读写配置文件,请检查文件权限!{Style.RESET_ALL}" + ) + print( + f"{Fore.RED}{EMOJI['ERROR']} 如果你使用过 go-cursor-help 来修改 ID; 请修改文件只读权限 {self.db_path} {Style.RESET_ALL}" + ) + return False + + # 读取现有配置 + print(f"{Fore.CYAN}{EMOJI['FILE']} 读取当前配置...{Style.RESET_ALL}") + with open(self.db_path, "r", encoding="utf-8") as f: + config = json.load(f) + + # 生成新的ID + print(f"{Fore.CYAN}{EMOJI['RESET']} 生成新的机器标识...{Style.RESET_ALL}") + new_ids = self.generate_new_ids() + + # 更新配置 + config.update(new_ids) + + # 保存新配置 + print(f"{Fore.CYAN}{EMOJI['FILE']} 保存新配置...{Style.RESET_ALL}") + with open(self.db_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=4) + + print(f"{Fore.GREEN}{EMOJI['SUCCESS']} 机器标识重置成功!{Style.RESET_ALL}") + print(f"\n{Fore.CYAN}新的机器标识:{Style.RESET_ALL}") + for key, value in new_ids.items(): + print(f"{EMOJI['INFO']} {key}: {Fore.GREEN}{value}{Style.RESET_ALL}") + + return True + + except PermissionError as e: + print(f"{Fore.RED}{EMOJI['ERROR']} 权限错误: {str(e)}{Style.RESET_ALL}") + print( + f"{Fore.YELLOW}{EMOJI['INFO']} 请尝试以管理员身份运行此程序{Style.RESET_ALL}" + ) + return False + except Exception as e: + print(f"{Fore.RED}{EMOJI['ERROR']} 重置过程出错: {str(e)}{Style.RESET_ALL}") + + return False + + +if __name__ == "__main__": + print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}") + print(f"{Fore.CYAN}{EMOJI['RESET']} Cursor 机器标识重置工具{Style.RESET_ALL}") + print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}") + + resetter = MachineIDResetter() + resetter.reset_machine_ids() + + print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}") + input(f"{EMOJI['INFO']} 按回车键退出...") diff --git a/screen/28613e3f3f23a935b66a7ba31ff4e3f.jpg b/screen/28613e3f3f23a935b66a7ba31ff4e3f.jpg new file mode 100644 index 0000000..2039cb3 Binary files /dev/null and b/screen/28613e3f3f23a935b66a7ba31ff4e3f.jpg differ diff --git a/screen/afdian-[未认证]阿臻.jpg b/screen/afdian-[未认证]阿臻.jpg new file mode 100644 index 0000000..d76b743 Binary files /dev/null and b/screen/afdian-[未认证]阿臻.jpg differ diff --git a/screen/c29ea438-ee74-4ba1-bbf6-25e622cdfad5.png b/screen/c29ea438-ee74-4ba1-bbf6-25e622cdfad5.png new file mode 100644 index 0000000..db46e78 Binary files /dev/null and b/screen/c29ea438-ee74-4ba1-bbf6-25e622cdfad5.png differ diff --git a/screen/mm_facetoface_collect_qrcode_1738583247120.png b/screen/mm_facetoface_collect_qrcode_1738583247120.png new file mode 100644 index 0000000..780fe33 Binary files /dev/null and b/screen/mm_facetoface_collect_qrcode_1738583247120.png differ diff --git a/screen/qrcode_for_gh_c985615b5f2b_258.jpg b/screen/qrcode_for_gh_c985615b5f2b_258.jpg new file mode 100644 index 0000000..92a714e Binary files /dev/null and b/screen/qrcode_for_gh_c985615b5f2b_258.jpg differ diff --git a/screen/截屏2025-01-04 09.44.48.png b/screen/截屏2025-01-04 09.44.48.png new file mode 100644 index 0000000..5ca8ecb Binary files /dev/null and b/screen/截屏2025-01-04 09.44.48.png differ diff --git a/test/get_veri_code_test.py b/test/get_veri_code_test.py new file mode 100644 index 0000000..fc152e7 --- /dev/null +++ b/test/get_veri_code_test.py @@ -0,0 +1,126 @@ +from DrissionPage import ChromiumOptions, Chromium +from DrissionPage.common import Keys +import time +import re +import sys +import os + + +def get_extension_path(): + """获取插件路径""" + root_dir = os.getcwd() + extension_path = os.path.join(root_dir, "turnstilePatch") + + if hasattr(sys, "_MEIPASS"): + print("运行在打包环境中") + extension_path = os.path.join(sys._MEIPASS, "turnstilePatch") + + print(f"尝试加载插件路径: {extension_path}") + + if not os.path.exists(extension_path): + raise FileNotFoundError( + f"插件不存在: {extension_path}\n请确保 turnstilePatch 文件夹在正确位置" + ) + + return extension_path + + +def get_browser_options(): + co = ChromiumOptions() + try: + extension_path = get_extension_path() + co.add_extension(extension_path) + except FileNotFoundError as e: + print(f"警告: {e}") + + co.set_user_agent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36" + ) + co.set_pref("credentials_enable_service", False) + co.set_argument("--hide-crash-restore-bubble") + co.auto_port() + + # Mac 系统特殊处理 + if sys.platform == "darwin": + co.set_argument("--no-sandbox") + co.set_argument("--disable-gpu") + + return co + + +def get_veri_code(username): + # 使用相同的浏览器配置 + co = get_browser_options() + browser = Chromium(co) + code = None + + try: + # 获取当前标签页 + tab = browser.latest_tab + tab.run_js("try { turnstile.reset() } catch(e) { }") + + # 打开临时邮箱网站 + tab.get("https://tempmail.plus/zh") + time.sleep(2) + + # 设置邮箱用户名 + while True: + if tab.ele("@id=pre_button"): + # 点击输入框 + tab.actions.click("@id=pre_button") + time.sleep(1) + # 删除之前的内容 + tab.run_js('document.getElementById("pre_button").value = ""') + + # 输入新用户名并回车 + tab.actions.input(username).key_down(Keys.ENTER).key_up(Keys.ENTER) + break + time.sleep(1) + + # 等待并获取新邮件 + while True: + new_mail = tab.ele("@class=mail") + if new_mail: + if new_mail.text: + print("最新的邮件:", new_mail.text) + tab.actions.click("@class=mail") + break + else: + print(new_mail) + break + time.sleep(1) + + # 提取验证码 + if tab.ele("@class=overflow-auto mb-20"): + email_content = tab.ele("@class=overflow-auto mb-20").text + verification_code = re.search( + r"verification code is (\d{6})", email_content + ) + if verification_code: + code = verification_code.group(1) + print("验证码:", code) + else: + print("未找到验证码") + + # 删除邮件 + if tab.ele("@id=delete_mail"): + tab.actions.click("@id=delete_mail") + time.sleep(1) + + if tab.ele("@id=confirm_mail"): + tab.actions.click("@id=confirm_mail") + print("删除邮件") + + except Exception as e: + print(f"发生错误: {str(e)}") + finally: + browser.quit() + + return code + + +# 测试运行 +if __name__ == "__main__": + test_username = "test_user" # 替换为你要测试的用户名 + code = get_veri_code(test_username) + print(f"获取到的验证码: {code}") diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json new file mode 100644 index 0000000..3f4c606 --- /dev/null +++ b/turnstilePatch/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 3, + "name": "Turnstile Patcher", + "version": "2.1", + "content_scripts": [ + { + "js": [ + "./script.js" + ], + "matches": [ + "" + ], + "run_at": "document_start", + "all_frames": true, + "world": "MAIN" + } + ] +} \ No newline at end of file diff --git a/turnstilePatch/readme.txt b/turnstilePatch/readme.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/turnstilePatch/readme.txt @@ -0,0 +1 @@ + diff --git a/turnstilePatch/script.js b/turnstilePatch/script.js new file mode 100644 index 0000000..a46d798 --- /dev/null +++ b/turnstilePatch/script.js @@ -0,0 +1,12 @@ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// old method wouldn't work on 4k screens + +let screenX = getRandomInt(800, 1200); +let screenY = getRandomInt(400, 600); + +Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX }); + +Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY }); \ No newline at end of file