From cc691b9fcacfc24bede388621603edc1f653117d Mon Sep 17 00:00:00 2001 From: adminlove520 <791751568@qq.com> Date: Thu, 19 Mar 2026 23:25:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E5=8F=8A=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GPT_register+duckmail+CPA+autouploadsub2api (DuckMail + OAuth + Sub2Api 注册工具) - 新增 team_all-in-one (ChatGPT Team 一键注册工具) - 新增 Code-Patch 项目 - 新增 ABCard 子模块 (ChatGPT Business/Plus 自动开通) - 新增 cloudflare_temp_email 子模块 (Cloudflare 临时邮箱服务) - 添加 .gitignore 文件 - 更新 README.md (新增项目导航、子模块说明) - 添加 CHANGELOG.md --- .gitignore | 35 + .gitmodules | 3 + ABCard | 1 + CHANGELOG.md | 65 + Code-Patch/.env.example | 23 + Code-Patch/.gitignore | 1247 ++++++++ Code-Patch/README.md | 75 + Code-Patch/backend/database.py | 106 + Code-Patch/backend/main.py | 1379 +++++++++ Code-Patch/backend/register.py | 566 ++++ Code-Patch/frontend/index.html | 12 + Code-Patch/frontend/package-lock.json | 1572 +++++++++++ Code-Patch/frontend/package.json | 21 + Code-Patch/frontend/src/App.vue | 95 + Code-Patch/frontend/src/api/index.js | 104 + .../frontend/src/composables/useCheckState.js | 57 + .../frontend/src/composables/useWebSocket.js | 40 + Code-Patch/frontend/src/main.js | 18 + Code-Patch/frontend/src/router/index.js | 18 + Code-Patch/frontend/src/styles/variables.css | 35 + Code-Patch/frontend/src/utils/format.js | 28 + .../frontend/src/views/AccountsView.vue | 702 +++++ .../frontend/src/views/RegisterView.vue | 418 +++ .../frontend/src/views/SchedulesView.vue | 483 ++++ .../frontend/src/views/SessionsView.vue | 324 +++ Code-Patch/frontend/vite.config.js | 43 + .../README.md | 137 + .../chatgpt_register.py | 2515 +++++++++++++++++ .../config.json | 34 + .../requirements.txt | 3 + .../server.py | 957 +++++++ .../web/app.js | 1018 +++++++ .../web/index.html | 389 +++ .../web/style.css | 2203 +++++++++++++++ README.md | 66 +- .../.cursor/rules/aze-mcp-messenger.mdc | 14 + team_all-in-one/README.md | 147 + team_all-in-one/app.py | 460 +++ team_all-in-one/config.json | 24 + team_all-in-one/config_loader.py | 2135 ++++++++++++++ team_all-in-one/static/mac_style.css | 416 +++ team_all-in-one/static/style.css | 611 ++++ team_all-in-one/templates/index.html | 458 +++ 43 files changed, 19047 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 160000 ABCard create mode 100644 CHANGELOG.md create mode 100644 Code-Patch/.env.example create mode 100644 Code-Patch/.gitignore create mode 100644 Code-Patch/README.md create mode 100644 Code-Patch/backend/database.py create mode 100644 Code-Patch/backend/main.py create mode 100644 Code-Patch/backend/register.py create mode 100644 Code-Patch/frontend/index.html create mode 100644 Code-Patch/frontend/package-lock.json create mode 100644 Code-Patch/frontend/package.json create mode 100644 Code-Patch/frontend/src/App.vue create mode 100644 Code-Patch/frontend/src/api/index.js create mode 100644 Code-Patch/frontend/src/composables/useCheckState.js create mode 100644 Code-Patch/frontend/src/composables/useWebSocket.js create mode 100644 Code-Patch/frontend/src/main.js create mode 100644 Code-Patch/frontend/src/router/index.js create mode 100644 Code-Patch/frontend/src/styles/variables.css create mode 100644 Code-Patch/frontend/src/utils/format.js create mode 100644 Code-Patch/frontend/src/views/AccountsView.vue create mode 100644 Code-Patch/frontend/src/views/RegisterView.vue create mode 100644 Code-Patch/frontend/src/views/SchedulesView.vue create mode 100644 Code-Patch/frontend/src/views/SessionsView.vue create mode 100644 Code-Patch/frontend/vite.config.js create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/README.md create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/chatgpt_register.py create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/config.json create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/requirements.txt create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/server.py create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/web/app.js create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/web/index.html create mode 100644 GPT_register+duckmail+CPA+autouploadsub2api/web/style.css create mode 100644 team_all-in-one/.cursor/rules/aze-mcp-messenger.mdc create mode 100644 team_all-in-one/README.md create mode 100644 team_all-in-one/app.py create mode 100644 team_all-in-one/config.json create mode 100644 team_all-in-one/config_loader.py create mode 100644 team_all-in-one/static/mac_style.css create mode 100644 team_all-in-one/static/style.css create mode 100644 team_all-in-one/templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2430a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +eggs/ +*.egg + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log + +# Environment +.env +.env.local + +# Token & Account Data +codex_tokens/ +ak.txt +rk.txt +registered_accounts.txt +registered_accounts.csv +invite_tracker.json +stable_proxy.txt diff --git a/.gitmodules b/.gitmodules index afadac6..03382ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "cloudflare_temp_email"] path = cloudflare_temp_email url = https://github.com/dreamhunter2333/cloudflare_temp_email.git +[submodule "ABCard"] + path = ABCard + url = https://github.com/kaima2022/ABCard.git diff --git a/ABCard b/ABCard new file mode 160000 index 0000000..c934141 --- /dev/null +++ b/ABCard @@ -0,0 +1 @@ +Subproject commit c934141c5cd2ab9cab876b8f69ad9394349dc3bb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b24ac8d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] - 2026-03-19 + +### Added + +- **GPT_register+duckmail+CPA+autouploadsub2api** - ChatGPT 批量自动注册工具(DuckMail + OAuth + Sub2Api 版) + - 支持 DuckMail 临时邮箱并发注册 + - 自动获取 OTP 验证码 + - OAuth 登录获取 Token + - 可选自动上传 Token 到 Sub2Api 平台 + - Web 管理界面(端口 18421) + +- **team_all-in-one** - ChatGPT Team 一键注册工具 + - Flask Web 管理界面 + - 支持 GPTMail、NPCMail 多种临时邮箱 + - 多线程批量注册 + - OAuth 自动授权 + - Token 导出功能 + - Sub2Api 平台上传支持 + +- **cloudflare_temp_email** (submodule) - Cloudflare 临时邮箱服务 + - 基于 Cloudflare 免费服务构建 + - Rust WASM 邮件解析,高性能 + - AI 邮件识别,自动提取验证码 + - 支持 SMTP/IMAP 代理 + - Telegram Bot 集成 + - 用户管理,支持 OAuth2、Passkey 登录 + +- **ABCard** (submodule) - ChatGPT Business/Plus 自动开通工具 + - 全自动注册 ChatGPT 账号 + - 开通 Business (5席位 $0) 或 Plus (个人版 $0) + - Xvfb + Chrome 自动支付,绕过 hCaptcha + - Web UI (Streamlit) 操作界面 + - 兑换码管控系统 + +### Updated + +- 项目结构优化,整合多个注册工具 +- 根目录 README 添加新子项目导航 + +## [1.0.0] - 2025-xx-xx + +### Added + +- **CPAtools** - Codex 账号管理工具 +- **GPT-team** - GPT 团队全自动注册工具 +- **chatgpt_register_duckmail** - DuckMail 注册工具 +- **codex** - Codex 相关工具 +- **freemail** - 临时邮箱服务 +- **merge-mailtm-share** - MailTM 邮箱合并工具 +- **ob12api** - OB12 API 服务 +- **openai_pool_orchestrator_v5** - OpenAI 账号池管理工具 +- **openai_pool_orchestrator-V6** - OpenAI 账号池编排器 +- **ClashVerge_** - ClashVerge 非港轮询脚本 +- **any-auto-register** (submodule) - 多平台账号自动注册工具 + +--- + +**Note**: This changelog documents the major additions and changes to the AI-Account-Toolkit project. For detailed changes to individual submodules, please refer to their respective repositories. diff --git a/Code-Patch/.env.example b/Code-Patch/.env.example new file mode 100644 index 0000000..eb16ae9 --- /dev/null +++ b/Code-Patch/.env.example @@ -0,0 +1,23 @@ +# Ports +# If 5173 is occupied, change this (e.g. 5174) +FRONTEND_PORT=5174 + +# Backend listen address/port +BACKEND_HOST=127.0.0.1 +BACKEND_PORT=8008 + +# Optional: override backend targets used by the frontend dev proxy +# BACKEND_ORIGIN=http://127.0.0.1:8000 +# BACKEND_WS_ORIGIN=ws://127.0.0.1:8000 + +# Optional: override CORS allowlist (comma-separated) +# FRONTEND_ORIGINS=http://localhost:5174,http://127.0.0.1:5174 + +# Proxy pool (comma-separated, used as default for register/check/import) +# Falls back to system proxy (HTTPS_PROXY/HTTP_PROXY) if not set +# PROXY_POOL=http://127.0.0.1:10808,socks5://127.0.0.1:10810 + +# Mail worker (required for register-related features) +# WORKER_URL= +# EMAIL_DOMAIN= +# ADMIN_AUTH= diff --git a/Code-Patch/.gitignore b/Code-Patch/.gitignore new file mode 100644 index 0000000..6f4b7e2 --- /dev/null +++ b/Code-Patch/.gitignore @@ -0,0 +1,1247 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +.env + +# Database +accounts.db +accounts.csv + +# Node +frontend/node_modules/ +frontend/dist/ + +# IDE +.idea/ +.vscode/ +*.iml + +# Claude +.claude/ +/backend/.venv/bin/activate +/backend/.venv/bin/activate.csh +/backend/.venv/bin/activate.fish +/backend/.venv/bin/Activate.ps1 +/backend/.venv/bin/curl-cffi +/backend/.venv/bin/dotenv +/backend/.venv/bin/fastapi +/backend/.venv/bin/pip +/backend/.venv/bin/pip3 +/backend/.venv/bin/pip3.12 +/backend/.venv/bin/python +/backend/.venv/bin/python3 +/backend/.venv/bin/python3.12 +/backend/.venv/bin/uvicorn +/backend/.venv/bin/watchfiles +/backend/.venv/bin/websockets +/backend/.venv/lib/python3.12/site-packages/_yaml/__init__.py +/backend/.venv/lib/python3.12/site-packages/annotated_doc/__init__.py +/backend/.venv/lib/python3.12/site-packages/annotated_doc/main.py +/backend/.venv/lib/python3.12/site-packages/annotated_doc/py.typed +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/annotated_doc-0.0.4.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/annotated_types/__init__.py +/backend/.venv/lib/python3.12/site-packages/annotated_types/py.typed +/backend/.venv/lib/python3.12/site-packages/annotated_types/test_cases.py +/backend/.venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/anyio/_backends/__init__.py +/backend/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py +/backend/.venv/lib/python3.12/site-packages/anyio/_backends/_trio.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/__init__.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_fileio.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_resources.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_signals.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_sockets.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_streams.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_tasks.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_testing.py +/backend/.venv/lib/python3.12/site-packages/anyio/_core/_typedattr.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/__init__.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_resources.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_sockets.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_streams.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_tasks.py +/backend/.venv/lib/python3.12/site-packages/anyio/abc/_testing.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/__init__.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/buffered.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/file.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/memory.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/stapled.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/text.py +/backend/.venv/lib/python3.12/site-packages/anyio/streams/tls.py +/backend/.venv/lib/python3.12/site-packages/anyio/__init__.py +/backend/.venv/lib/python3.12/site-packages/anyio/from_thread.py +/backend/.venv/lib/python3.12/site-packages/anyio/functools.py +/backend/.venv/lib/python3.12/site-packages/anyio/lowlevel.py +/backend/.venv/lib/python3.12/site-packages/anyio/py.typed +/backend/.venv/lib/python3.12/site-packages/anyio/pytest_plugin.py +/backend/.venv/lib/python3.12/site-packages/anyio/to_interpreter.py +/backend/.venv/lib/python3.12/site-packages/anyio/to_process.py +/backend/.venv/lib/python3.12/site-packages/anyio/to_thread.py +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/certifi/__init__.py +/backend/.venv/lib/python3.12/site-packages/certifi/__main__.py +/backend/.venv/lib/python3.12/site-packages/certifi/cacert.pem +/backend/.venv/lib/python3.12/site-packages/certifi/core.py +/backend/.venv/lib/python3.12/site-packages/certifi/py.typed +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/cffi/__init__.py +/backend/.venv/lib/python3.12/site-packages/cffi/_cffi_errors.h +/backend/.venv/lib/python3.12/site-packages/cffi/_cffi_include.h +/backend/.venv/lib/python3.12/site-packages/cffi/_embedding.h +/backend/.venv/lib/python3.12/site-packages/cffi/_imp_emulation.py +/backend/.venv/lib/python3.12/site-packages/cffi/_shimmed_dist_utils.py +/backend/.venv/lib/python3.12/site-packages/cffi/api.py +/backend/.venv/lib/python3.12/site-packages/cffi/backend_ctypes.py +/backend/.venv/lib/python3.12/site-packages/cffi/cffi_opcode.py +/backend/.venv/lib/python3.12/site-packages/cffi/commontypes.py +/backend/.venv/lib/python3.12/site-packages/cffi/cparser.py +/backend/.venv/lib/python3.12/site-packages/cffi/error.py +/backend/.venv/lib/python3.12/site-packages/cffi/ffiplatform.py +/backend/.venv/lib/python3.12/site-packages/cffi/lock.py +/backend/.venv/lib/python3.12/site-packages/cffi/model.py +/backend/.venv/lib/python3.12/site-packages/cffi/parse_c_type.h +/backend/.venv/lib/python3.12/site-packages/cffi/pkgconfig.py +/backend/.venv/lib/python3.12/site-packages/cffi/recompiler.py +/backend/.venv/lib/python3.12/site-packages/cffi/setuptools_ext.py +/backend/.venv/lib/python3.12/site-packages/cffi/vengine_cpy.py +/backend/.venv/lib/python3.12/site-packages/cffi/vengine_gen.py +/backend/.venv/lib/python3.12/site-packages/cffi/verifier.py +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/licenses/AUTHORS +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/click/__init__.py +/backend/.venv/lib/python3.12/site-packages/click/_compat.py +/backend/.venv/lib/python3.12/site-packages/click/_termui_impl.py +/backend/.venv/lib/python3.12/site-packages/click/_textwrap.py +/backend/.venv/lib/python3.12/site-packages/click/_utils.py +/backend/.venv/lib/python3.12/site-packages/click/_winconsole.py +/backend/.venv/lib/python3.12/site-packages/click/core.py +/backend/.venv/lib/python3.12/site-packages/click/decorators.py +/backend/.venv/lib/python3.12/site-packages/click/exceptions.py +/backend/.venv/lib/python3.12/site-packages/click/formatting.py +/backend/.venv/lib/python3.12/site-packages/click/globals.py +/backend/.venv/lib/python3.12/site-packages/click/parser.py +/backend/.venv/lib/python3.12/site-packages/click/py.typed +/backend/.venv/lib/python3.12/site-packages/click/shell_completion.py +/backend/.venv/lib/python3.12/site-packages/click/termui.py +/backend/.venv/lib/python3.12/site-packages/click/testing.py +/backend/.venv/lib/python3.12/site-packages/click/types.py +/backend/.venv/lib/python3.12/site-packages/click/utils.py +/backend/.venv/lib/python3.12/site-packages/click-8.3.1.dist-info/licenses/LICENSE.txt +/backend/.venv/lib/python3.12/site-packages/click-8.3.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/click-8.3.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/click-8.3.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/click-8.3.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/curl_cffi/.dylibs/libcurl-impersonate.4.dylib +/backend/.venv/lib/python3.12/site-packages/curl_cffi/.dylibs/libidn2.0.dylib +/backend/.venv/lib/python3.12/site-packages/curl_cffi/.dylibs/libintl.8.dylib +/backend/.venv/lib/python3.12/site-packages/curl_cffi/.dylibs/libunistring.5.dylib +/backend/.venv/lib/python3.12/site-packages/curl_cffi/.dylibs/libzstd.1.5.7.dylib +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/__init__.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/cookies.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/errors.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/exceptions.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/headers.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/impersonate.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/models.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/session.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/utils.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/requests/websockets.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/__init__.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/__version__.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/_asyncio_selector.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/_wrapper.abi3.so +/backend/.venv/lib/python3.12/site-packages/curl_cffi/aio.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/cli.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/const.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/curl.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi/py.typed +/backend/.venv/lib/python3.12/site-packages/curl_cffi/utils.py +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/curl_cffi-0.14.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/dotenv/__init__.py +/backend/.venv/lib/python3.12/site-packages/dotenv/__main__.py +/backend/.venv/lib/python3.12/site-packages/dotenv/cli.py +/backend/.venv/lib/python3.12/site-packages/dotenv/ipython.py +/backend/.venv/lib/python3.12/site-packages/dotenv/main.py +/backend/.venv/lib/python3.12/site-packages/dotenv/parser.py +/backend/.venv/lib/python3.12/site-packages/dotenv/py.typed +/backend/.venv/lib/python3.12/site-packages/dotenv/variables.py +/backend/.venv/lib/python3.12/site-packages/dotenv/version.py +/backend/.venv/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/dependencies.md +/backend/.venv/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/other-tools.md +/backend/.venv/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/streaming.md +/backend/.venv/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/SKILL.md +/backend/.venv/lib/python3.12/site-packages/fastapi/_compat/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/_compat/shared.py +/backend/.venv/lib/python3.12/site-packages/fastapi/_compat/v2.py +/backend/.venv/lib/python3.12/site-packages/fastapi/dependencies/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/dependencies/models.py +/backend/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/cors.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/gzip.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/httpsredirect.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/trustedhost.py +/backend/.venv/lib/python3.12/site-packages/fastapi/middleware/wsgi.py +/backend/.venv/lib/python3.12/site-packages/fastapi/openapi/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/openapi/constants.py +/backend/.venv/lib/python3.12/site-packages/fastapi/openapi/docs.py +/backend/.venv/lib/python3.12/site-packages/fastapi/openapi/models.py +/backend/.venv/lib/python3.12/site-packages/fastapi/openapi/utils.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/api_key.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/base.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/http.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/oauth2.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/open_id_connect_url.py +/backend/.venv/lib/python3.12/site-packages/fastapi/security/utils.py +/backend/.venv/lib/python3.12/site-packages/fastapi/__init__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/__main__.py +/backend/.venv/lib/python3.12/site-packages/fastapi/applications.py +/backend/.venv/lib/python3.12/site-packages/fastapi/background.py +/backend/.venv/lib/python3.12/site-packages/fastapi/cli.py +/backend/.venv/lib/python3.12/site-packages/fastapi/concurrency.py +/backend/.venv/lib/python3.12/site-packages/fastapi/datastructures.py +/backend/.venv/lib/python3.12/site-packages/fastapi/encoders.py +/backend/.venv/lib/python3.12/site-packages/fastapi/exception_handlers.py +/backend/.venv/lib/python3.12/site-packages/fastapi/exceptions.py +/backend/.venv/lib/python3.12/site-packages/fastapi/logger.py +/backend/.venv/lib/python3.12/site-packages/fastapi/param_functions.py +/backend/.venv/lib/python3.12/site-packages/fastapi/params.py +/backend/.venv/lib/python3.12/site-packages/fastapi/py.typed +/backend/.venv/lib/python3.12/site-packages/fastapi/requests.py +/backend/.venv/lib/python3.12/site-packages/fastapi/responses.py +/backend/.venv/lib/python3.12/site-packages/fastapi/routing.py +/backend/.venv/lib/python3.12/site-packages/fastapi/sse.py +/backend/.venv/lib/python3.12/site-packages/fastapi/staticfiles.py +/backend/.venv/lib/python3.12/site-packages/fastapi/templating.py +/backend/.venv/lib/python3.12/site-packages/fastapi/testclient.py +/backend/.venv/lib/python3.12/site-packages/fastapi/types.py +/backend/.venv/lib/python3.12/site-packages/fastapi/utils.py +/backend/.venv/lib/python3.12/site-packages/fastapi/websockets.py +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/fastapi-0.135.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/h11/__init__.py +/backend/.venv/lib/python3.12/site-packages/h11/_abnf.py +/backend/.venv/lib/python3.12/site-packages/h11/_connection.py +/backend/.venv/lib/python3.12/site-packages/h11/_events.py +/backend/.venv/lib/python3.12/site-packages/h11/_headers.py +/backend/.venv/lib/python3.12/site-packages/h11/_readers.py +/backend/.venv/lib/python3.12/site-packages/h11/_receivebuffer.py +/backend/.venv/lib/python3.12/site-packages/h11/_state.py +/backend/.venv/lib/python3.12/site-packages/h11/_util.py +/backend/.venv/lib/python3.12/site-packages/h11/_version.py +/backend/.venv/lib/python3.12/site-packages/h11/_writers.py +/backend/.venv/lib/python3.12/site-packages/h11/py.typed +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/httptools/parser/__init__.py +/backend/.venv/lib/python3.12/site-packages/httptools/parser/cparser.pxd +/backend/.venv/lib/python3.12/site-packages/httptools/parser/errors.py +/backend/.venv/lib/python3.12/site-packages/httptools/parser/parser.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/httptools/parser/parser.pyi +/backend/.venv/lib/python3.12/site-packages/httptools/parser/parser.pyx +/backend/.venv/lib/python3.12/site-packages/httptools/parser/protocol.py +/backend/.venv/lib/python3.12/site-packages/httptools/parser/python.pxd +/backend/.venv/lib/python3.12/site-packages/httptools/parser/url_cparser.pxd +/backend/.venv/lib/python3.12/site-packages/httptools/parser/url_parser.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/httptools/parser/url_parser.pyi +/backend/.venv/lib/python3.12/site-packages/httptools/parser/url_parser.pyx +/backend/.venv/lib/python3.12/site-packages/httptools/__init__.py +/backend/.venv/lib/python3.12/site-packages/httptools/_version.py +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/httptools-0.7.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/idna/__init__.py +/backend/.venv/lib/python3.12/site-packages/idna/codec.py +/backend/.venv/lib/python3.12/site-packages/idna/compat.py +/backend/.venv/lib/python3.12/site-packages/idna/core.py +/backend/.venv/lib/python3.12/site-packages/idna/idnadata.py +/backend/.venv/lib/python3.12/site-packages/idna/intranges.py +/backend/.venv/lib/python3.12/site-packages/idna/package_data.py +/backend/.venv/lib/python3.12/site-packages/idna/py.typed +/backend/.venv/lib/python3.12/site-packages/idna/uts46data.py +/backend/.venv/lib/python3.12/site-packages/idna-3.11.dist-info/licenses/LICENSE.md +/backend/.venv/lib/python3.12/site-packages/idna-3.11.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/idna-3.11.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/idna-3.11.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/idna-3.11.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/autocompletion.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/base_command.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/cmdoptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/command_context.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/main.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/main_parser.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/parser.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/progress_bars.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/req_command.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/spinners.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cli/status_codes.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/check.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/completion.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/configuration.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/debug.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/download.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/freeze.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/hash.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/help.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/index.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/inspect.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/install.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/list.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/search.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/show.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/uninstall.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/commands/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/distributions/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/distributions/base.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/distributions/installed.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/distributions/sdist.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/distributions/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/index/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/index/collector.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/index/package_finder.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/index/sources.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/locations/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/locations/_distutils.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/locations/base.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/importlib/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_dists.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_envs.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/_json.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/base.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/metadata/pkg_resources.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/candidate.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/direct_url.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/format_control.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/index.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/installation_report.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/link.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/scheme.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/search_scope.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/selection_prefs.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/target_python.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/models/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/auth.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/download.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/lazy_wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/session.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/network/xmlrpc.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/build_tracker.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/metadata.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/metadata_editable.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/metadata_legacy.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/wheel_editable.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/build/wheel_legacy.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/install/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/install/editable_legacy.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/install/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/check.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/freeze.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/operations/prepare.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/constructors.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/req_file.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/req_install.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/req_set.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/req/req_uninstall.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/legacy/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/legacy/resolver.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/base.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/factory.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/provider.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/reporter.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/requirements.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/resolver.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/resolution/base.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/_jaraco_text.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/_log.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/appdirs.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/compatibility_tags.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/datetime.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/deprecation.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/direct_url_helpers.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/egg_link.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/encoding.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/entrypoints.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/filesystem.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/filetypes.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/glibc.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/hashes.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/inject_securetransport.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/logging.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/misc.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/models.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/packaging.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/setuptools_build.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/subprocess.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/temp_dir.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/unpacking.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/urls.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/virtualenv.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/utils/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/bazaar.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/git.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/mercurial.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/subversion.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/vcs/versioncontrol.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/build_env.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/configuration.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/exceptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/main.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/pyproject.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/self_outdated_check.py +/backend/.venv/lib/python3.12/site-packages/pip/_internal/wheel_builder.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/_cmd.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/adapter.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/cache.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/controller.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/filewrapper.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/heuristics.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/serialize.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/cachecontrol/wrapper.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/certifi/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/certifi/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/certifi/cacert.pem +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/certifi/core.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/cli/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/cli/chardetect.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/metadata/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/metadata/languages.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/big5freq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/big5prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/chardistribution.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/charsetgroupprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/charsetprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/codingstatemachine.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/codingstatemachinedict.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/cp949prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/enums.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/escprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/escsm.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/eucjpprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/euckrfreq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/euckrprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/euctwfreq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/euctwprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/gb2312freq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/gb2312prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/hebrewprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/jisfreq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/johabfreq.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/johabprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/jpcntx.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langbulgarianmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langgreekmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langhebrewmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langhungarianmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langrussianmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langthaimodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/langturkishmodel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/latin1prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/macromanprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/mbcharsetprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/mbcsgroupprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/mbcssm.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/resultdict.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/sbcharsetprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/sbcsgroupprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/sjisprober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/universaldetector.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/utf8prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/utf1632prober.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/chardet/version.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/ansi_test.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/ansitowin32_test.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/initialise_test.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/isatty_test.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/tests/winterm_test.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/ansi.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/ansitowin32.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/initialise.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/win32.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/colorama/winterm.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/database.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/index.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/locators.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/manifest.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/markers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/metadata.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/resources.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/scripts.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/t32.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/t64.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/t64-arm.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/util.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/version.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/w32.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/w64.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/w64-arm.exe +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distlib/wheel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distro/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distro/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/distro/distro.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/codec.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/core.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/idnadata.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/intranges.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/package_data.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/idna/uts46data.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/msgpack/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/msgpack/exceptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/msgpack/ext.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/msgpack/fallback.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/__about__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/_manylinux.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/_musllinux.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/_structures.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/markers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/requirements.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/specifiers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/tags.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/packaging/version.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pkg_resources/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/android.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/api.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/macos.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/unix.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/version.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/platformdirs/windows.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/filters/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/_mapping.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/bbcode.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/groff.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/html.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/img.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/irc.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/latex.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/other.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/pangomarkup.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/rtf.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/svg.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/terminal.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/terminal256.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/_mapping.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/python.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/cmdline.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/console.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/filter.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/formatter.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/lexer.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/modeline.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/plugin.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/regexopt.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/scanner.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/sphinxext.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/style.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/token.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/unistring.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pygments/util.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/diagram/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/actions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/common.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/core.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/exceptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/helpers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/results.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/testing.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/unicode.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyparsing/util.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_impl.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/__version__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/_internal_utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/adapters.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/api.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/auth.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/certs.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/compat.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/cookies.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/exceptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/help.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/hooks.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/models.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/packages.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/sessions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/status_codes.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/structures.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/requests/utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/compat/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/compat/collections_abc.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/providers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/reporters.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/resolvers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/structs.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_cell_widths.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_emoji_codes.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_emoji_replace.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_export_format.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_extension.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_fileno.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_inspect.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_log_render.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_loop.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_null_file.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_palettes.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_pick.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_ratio.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_spinners.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_stack.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_timer.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_win32_console.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_windows.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_windows_renderer.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/_wrap.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/abc.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/align.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/ansi.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/bar.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/box.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/cells.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/color.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/color_triplet.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/columns.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/console.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/constrain.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/containers.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/control.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/default_styles.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/diagnose.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/emoji.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/errors.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/file_proxy.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/filesize.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/highlighter.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/json.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/jupyter.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/layout.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/live.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/live_render.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/logging.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/markup.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/measure.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/padding.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/pager.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/palette.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/panel.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/pretty.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/progress.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/progress_bar.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/prompt.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/protocol.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/region.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/repr.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/rule.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/scope.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/screen.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/segment.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/spinner.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/status.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/style.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/styled.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/syntax.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/table.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/terminal_theme.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/text.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/theme.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/themes.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/traceback.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/rich/tree.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/_asyncio.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/_utils.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/after.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/before.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/before_sleep.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/nap.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/retry.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/stop.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/tornadoweb.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tenacity/wait.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tomli/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tomli/_parser.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tomli/_re.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/tomli/_types.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/appengine.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/securetransport.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/socks.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/six.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/connection.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/proxy.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/queue.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/request.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/response.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/retry.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssl_.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssltransport.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/timeout.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/url.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/util/wait.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/_collections.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/_version.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/connection.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/connectionpool.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/exceptions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/fields.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/filepost.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/poolmanager.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/request.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/urllib3/response.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/webencodings/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/webencodings/labels.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/webencodings/mklabels.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/webencodings/tests.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/webencodings/x_user_defined.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/six.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/typing_extensions.py +/backend/.venv/lib/python3.12/site-packages/pip/_vendor/vendor.txt +/backend/.venv/lib/python3.12/site-packages/pip/__init__.py +/backend/.venv/lib/python3.12/site-packages/pip/__main__.py +/backend/.venv/lib/python3.12/site-packages/pip/__pip-runner__.py +/backend/.venv/lib/python3.12/site-packages/pip/py.typed +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/AUTHORS.txt +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/LICENSE.txt +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/pip-23.2.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/pycparser/__init__.py +/backend/.venv/lib/python3.12/site-packages/pycparser/_ast_gen.py +/backend/.venv/lib/python3.12/site-packages/pycparser/_c_ast.cfg +/backend/.venv/lib/python3.12/site-packages/pycparser/ast_transforms.py +/backend/.venv/lib/python3.12/site-packages/pycparser/c_ast.py +/backend/.venv/lib/python3.12/site-packages/pycparser/c_generator.py +/backend/.venv/lib/python3.12/site-packages/pycparser/c_lexer.py +/backend/.venv/lib/python3.12/site-packages/pycparser/c_parser.py +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_config.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_core_metadata.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_core_utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_dataclasses.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_decorators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_decorators_v1.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_discriminated_union.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_docs_extraction.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_fields.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_forward_ref.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_generics.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_git.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_import_utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_internal_dataclass.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_known_annotated_metadata.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_mock_val_ser.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_repr.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_schema_gather.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_schema_generation_shared.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_serializers.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_signature.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_typing_extra.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_internal/_validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/class_validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/config.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/copy_internals.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/decorator.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/json.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/parse.py +/backend/.venv/lib/python3.12/site-packages/pydantic/deprecated/tools.py +/backend/.venv/lib/python3.12/site-packages/pydantic/experimental/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/experimental/arguments_schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic/experimental/missing_sentinel.py +/backend/.venv/lib/python3.12/site-packages/pydantic/experimental/pipeline.py +/backend/.venv/lib/python3.12/site-packages/pydantic/plugin/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/plugin/_loader.py +/backend/.venv/lib/python3.12/site-packages/pydantic/plugin/_schema_validator.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/annotated_types.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/class_validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/color.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/config.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/dataclasses.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/datetime_parse.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/decorator.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/env_settings.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/error_wrappers.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/errors.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/fields.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/generics.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/json.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/main.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/mypy.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/networks.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/parse.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/py.typed +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/tools.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/types.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/typing.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/v1/version.py +/backend/.venv/lib/python3.12/site-packages/pydantic/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic/_migration.py +/backend/.venv/lib/python3.12/site-packages/pydantic/alias_generators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/aliases.py +/backend/.venv/lib/python3.12/site-packages/pydantic/annotated_handlers.py +/backend/.venv/lib/python3.12/site-packages/pydantic/class_validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/color.py +/backend/.venv/lib/python3.12/site-packages/pydantic/config.py +/backend/.venv/lib/python3.12/site-packages/pydantic/dataclasses.py +/backend/.venv/lib/python3.12/site-packages/pydantic/datetime_parse.py +/backend/.venv/lib/python3.12/site-packages/pydantic/decorator.py +/backend/.venv/lib/python3.12/site-packages/pydantic/env_settings.py +/backend/.venv/lib/python3.12/site-packages/pydantic/error_wrappers.py +/backend/.venv/lib/python3.12/site-packages/pydantic/errors.py +/backend/.venv/lib/python3.12/site-packages/pydantic/fields.py +/backend/.venv/lib/python3.12/site-packages/pydantic/functional_serializers.py +/backend/.venv/lib/python3.12/site-packages/pydantic/functional_validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/generics.py +/backend/.venv/lib/python3.12/site-packages/pydantic/json.py +/backend/.venv/lib/python3.12/site-packages/pydantic/json_schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic/main.py +/backend/.venv/lib/python3.12/site-packages/pydantic/mypy.py +/backend/.venv/lib/python3.12/site-packages/pydantic/networks.py +/backend/.venv/lib/python3.12/site-packages/pydantic/parse.py +/backend/.venv/lib/python3.12/site-packages/pydantic/py.typed +/backend/.venv/lib/python3.12/site-packages/pydantic/root_model.py +/backend/.venv/lib/python3.12/site-packages/pydantic/schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic/tools.py +/backend/.venv/lib/python3.12/site-packages/pydantic/type_adapter.py +/backend/.venv/lib/python3.12/site-packages/pydantic/types.py +/backend/.venv/lib/python3.12/site-packages/pydantic/typing.py +/backend/.venv/lib/python3.12/site-packages/pydantic/utils.py +/backend/.venv/lib/python3.12/site-packages/pydantic/validate_call_decorator.py +/backend/.venv/lib/python3.12/site-packages/pydantic/validators.py +/backend/.venv/lib/python3.12/site-packages/pydantic/version.py +/backend/.venv/lib/python3.12/site-packages/pydantic/warnings.py +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/pydantic-2.12.5.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/pydantic_core/__init__.py +/backend/.venv/lib/python3.12/site-packages/pydantic_core/_pydantic_core.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/pydantic_core/_pydantic_core.pyi +/backend/.venv/lib/python3.12/site-packages/pydantic_core/core_schema.py +/backend/.venv/lib/python3.12/site-packages/pydantic_core/py.typed +/backend/.venv/lib/python3.12/site-packages/pydantic_core-2.41.5.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/pydantic_core-2.41.5.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/pydantic_core-2.41.5.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/pydantic_core-2.41.5.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/pydantic_core-2.41.5.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/__init__.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/authentication.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/base.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/cors.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/gzip.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/sessions.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/trustedhost.py +/backend/.venv/lib/python3.12/site-packages/starlette/middleware/wsgi.py +/backend/.venv/lib/python3.12/site-packages/starlette/__init__.py +/backend/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py +/backend/.venv/lib/python3.12/site-packages/starlette/_utils.py +/backend/.venv/lib/python3.12/site-packages/starlette/applications.py +/backend/.venv/lib/python3.12/site-packages/starlette/authentication.py +/backend/.venv/lib/python3.12/site-packages/starlette/background.py +/backend/.venv/lib/python3.12/site-packages/starlette/concurrency.py +/backend/.venv/lib/python3.12/site-packages/starlette/config.py +/backend/.venv/lib/python3.12/site-packages/starlette/convertors.py +/backend/.venv/lib/python3.12/site-packages/starlette/datastructures.py +/backend/.venv/lib/python3.12/site-packages/starlette/endpoints.py +/backend/.venv/lib/python3.12/site-packages/starlette/exceptions.py +/backend/.venv/lib/python3.12/site-packages/starlette/formparsers.py +/backend/.venv/lib/python3.12/site-packages/starlette/py.typed +/backend/.venv/lib/python3.12/site-packages/starlette/requests.py +/backend/.venv/lib/python3.12/site-packages/starlette/responses.py +/backend/.venv/lib/python3.12/site-packages/starlette/routing.py +/backend/.venv/lib/python3.12/site-packages/starlette/schemas.py +/backend/.venv/lib/python3.12/site-packages/starlette/staticfiles.py +/backend/.venv/lib/python3.12/site-packages/starlette/status.py +/backend/.venv/lib/python3.12/site-packages/starlette/templating.py +/backend/.venv/lib/python3.12/site-packages/starlette/testclient.py +/backend/.venv/lib/python3.12/site-packages/starlette/types.py +/backend/.venv/lib/python3.12/site-packages/starlette/websockets.py +/backend/.venv/lib/python3.12/site-packages/starlette-0.52.1.dist-info/licenses/LICENSE.md +/backend/.venv/lib/python3.12/site-packages/starlette-0.52.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/starlette-0.52.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/starlette-0.52.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/starlette-0.52.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/typing_inspection/__init__.py +/backend/.venv/lib/python3.12/site-packages/typing_inspection/introspection.py +/backend/.venv/lib/python3.12/site-packages/typing_inspection/py.typed +/backend/.venv/lib/python3.12/site-packages/typing_inspection/typing_objects.py +/backend/.venv/lib/python3.12/site-packages/typing_inspection/typing_objects.pyi +/backend/.venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/uvicorn/lifespan/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/lifespan/off.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/lifespan/on.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/loops/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/loops/asyncio.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/loops/auto.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/loops/uvloop.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/middleware/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/middleware/asgi2.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/middleware/message_logger.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/middleware/wsgi.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/auto.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/flow_control.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/auto.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/wsproto_impl.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/protocols/utils.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/__main__.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/_compat.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/_subprocess.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/_types.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/config.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/importer.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/logging.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/main.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/py.typed +/backend/.venv/lib/python3.12/site-packages/uvicorn/server.py +/backend/.venv/lib/python3.12/site-packages/uvicorn/workers.py +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/licenses/LICENSE.md +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/REQUESTED +/backend/.venv/lib/python3.12/site-packages/uvicorn-0.41.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/async_.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/async_.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/basetransport.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/basetransport.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/check.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/check.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/fsevent.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/fsevent.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/handle.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/handle.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/idle.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/idle.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/pipe.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/pipe.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/poll.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/poll.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/process.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/process.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/stream.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/stream.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/streamserver.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/streamserver.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/tcp.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/tcp.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/timer.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/timer.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/udp.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/handles/udp.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/consts.pxi +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/debug.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/flowcontrol.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/python.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/stdlib.pxi +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/system.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/includes/uv.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/__init__.py +/backend/.venv/lib/python3.12/site-packages/uvloop/_noop.py +/backend/.venv/lib/python3.12/site-packages/uvloop/_testbase.py +/backend/.venv/lib/python3.12/site-packages/uvloop/_version.py +/backend/.venv/lib/python3.12/site-packages/uvloop/cbhandles.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/cbhandles.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/dns.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/errors.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/loop.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/uvloop/loop.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/loop.pyi +/backend/.venv/lib/python3.12/site-packages/uvloop/loop.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/lru.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/pseudosock.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/py.typed +/backend/.venv/lib/python3.12/site-packages/uvloop/request.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/request.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/server.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/server.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop/sslproto.pxd +/backend/.venv/lib/python3.12/site-packages/uvloop/sslproto.pyx +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/licenses/LICENSE-APACHE +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/licenses/LICENSE-MIT +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/uvloop-0.22.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/watchfiles/__init__.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/__main__.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/_rust_notify.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/watchfiles/_rust_notify.pyi +/backend/.venv/lib/python3.12/site-packages/watchfiles/cli.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/filters.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/main.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/py.typed +/backend/.venv/lib/python3.12/site-packages/watchfiles/run.py +/backend/.venv/lib/python3.12/site-packages/watchfiles/version.py +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/watchfiles-1.1.1.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/__init__.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/async_timeout.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/client.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/compatibility.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/connection.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/messages.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/router.py +/backend/.venv/lib/python3.12/site-packages/websockets/asyncio/server.py +/backend/.venv/lib/python3.12/site-packages/websockets/extensions/__init__.py +/backend/.venv/lib/python3.12/site-packages/websockets/extensions/base.py +/backend/.venv/lib/python3.12/site-packages/websockets/extensions/permessage_deflate.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/__init__.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/auth.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/client.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/exceptions.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/framing.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/handshake.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/http.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/protocol.py +/backend/.venv/lib/python3.12/site-packages/websockets/legacy/server.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/__init__.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/client.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/connection.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/messages.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/router.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/server.py +/backend/.venv/lib/python3.12/site-packages/websockets/sync/utils.py +/backend/.venv/lib/python3.12/site-packages/websockets/__init__.py +/backend/.venv/lib/python3.12/site-packages/websockets/__main__.py +/backend/.venv/lib/python3.12/site-packages/websockets/auth.py +/backend/.venv/lib/python3.12/site-packages/websockets/cli.py +/backend/.venv/lib/python3.12/site-packages/websockets/client.py +/backend/.venv/lib/python3.12/site-packages/websockets/connection.py +/backend/.venv/lib/python3.12/site-packages/websockets/datastructures.py +/backend/.venv/lib/python3.12/site-packages/websockets/exceptions.py +/backend/.venv/lib/python3.12/site-packages/websockets/frames.py +/backend/.venv/lib/python3.12/site-packages/websockets/headers.py +/backend/.venv/lib/python3.12/site-packages/websockets/http.py +/backend/.venv/lib/python3.12/site-packages/websockets/http11.py +/backend/.venv/lib/python3.12/site-packages/websockets/imports.py +/backend/.venv/lib/python3.12/site-packages/websockets/protocol.py +/backend/.venv/lib/python3.12/site-packages/websockets/proxy.py +/backend/.venv/lib/python3.12/site-packages/websockets/py.typed +/backend/.venv/lib/python3.12/site-packages/websockets/server.py +/backend/.venv/lib/python3.12/site-packages/websockets/speedups.c +/backend/.venv/lib/python3.12/site-packages/websockets/speedups.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/websockets/speedups.pyi +/backend/.venv/lib/python3.12/site-packages/websockets/streams.py +/backend/.venv/lib/python3.12/site-packages/websockets/typing.py +/backend/.venv/lib/python3.12/site-packages/websockets/uri.py +/backend/.venv/lib/python3.12/site-packages/websockets/utils.py +/backend/.venv/lib/python3.12/site-packages/websockets/version.py +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/licenses/LICENSE +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/entry_points.txt +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/INSTALLER +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/METADATA +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/RECORD +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/top_level.txt +/backend/.venv/lib/python3.12/site-packages/websockets-16.0.dist-info/WHEEL +/backend/.venv/lib/python3.12/site-packages/yaml/__init__.py +/backend/.venv/lib/python3.12/site-packages/yaml/_yaml.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/yaml/composer.py +/backend/.venv/lib/python3.12/site-packages/yaml/constructor.py +/backend/.venv/lib/python3.12/site-packages/yaml/cyaml.py +/backend/.venv/lib/python3.12/site-packages/yaml/dumper.py +/backend/.venv/lib/python3.12/site-packages/yaml/emitter.py +/backend/.venv/lib/python3.12/site-packages/yaml/error.py +/backend/.venv/lib/python3.12/site-packages/yaml/events.py +/backend/.venv/lib/python3.12/site-packages/yaml/loader.py +/backend/.venv/lib/python3.12/site-packages/yaml/nodes.py +/backend/.venv/lib/python3.12/site-packages/yaml/parser.py +/backend/.venv/lib/python3.12/site-packages/yaml/reader.py +/backend/.venv/lib/python3.12/site-packages/yaml/representer.py +/backend/.venv/lib/python3.12/site-packages/yaml/resolver.py +/backend/.venv/lib/python3.12/site-packages/yaml/scanner.py +/backend/.venv/lib/python3.12/site-packages/yaml/serializer.py +/backend/.venv/lib/python3.12/site-packages/yaml/tokens.py +/backend/.venv/lib/python3.12/site-packages/_cffi_backend.cpython-312-darwin.so +/backend/.venv/lib/python3.12/site-packages/typing_extensions.py +/backend/.venv/pyvenv.cfg +/backend/backend.log diff --git a/Code-Patch/README.md b/Code-Patch/README.md new file mode 100644 index 0000000..3f72dcf --- /dev/null +++ b/Code-Patch/README.md @@ -0,0 +1,75 @@ +# Code-Patch + +## 配置端口(.env) + +`.env` 在项目根目录。推荐先复制一份示例: + +```bash +cp .env.example .env +``` + +常用配置项: + +- `FRONTEND_PORT`:前端端口(默认 `5173`,如果被占用就改成 `5174/5175/...`) +- `BACKEND_HOST` / `APP_HOST`:后端监听地址(推荐 `127.0.0.1`) +- `BACKEND_PORT` / `APP_PORT`:后端端口(默认 `8000`) + +修改端口后,需要把前后端都重启一次(后端的 CORS 白名单依赖 `FRONTEND_PORT`)。 + +## 启动 + +### 后端 + +```bash +python3 -m venv backend/.venv +backend/.venv/bin/pip install fastapi "uvicorn[standard]" pydantic python-dotenv curl-cffi + +cd backend +./.venv/bin/python main.py +``` + +后端接口文档:`http://127.0.0.1:8000/docs`(端口以 `.env` 为准) + +### 前端 + +```bash +cd frontend +npm install +npm run dev +``` + +## 重启命令(分别) + +### 重启后端 + +```bash +# 停止占用端口的进程(把 8000 换成你的 BACKEND_PORT) +PID=$(lsof -tiTCP:8000 -sTCP:LISTEN) && kill $PID + +# 重新启动 +cd backend +./.venv/bin/python main.py +``` + +### 重启前端 + +```bash +# 停止占用端口的进程(把 5174 换成你的 FRONTEND_PORT,默认 5173) +PID=$(lsof -tiTCP:5174 -sTCP:LISTEN) && kill $PID + +cd frontend +npm run dev +``` + +### 核心功能 +- 批量注册 — 代理池 + 可调并发,实时 WebSocket 进度推送,支持暂停 / 继续 + +- 账号管理 — 多维度筛选(关键词 / 状态 / 存活),支持 CSV 导入导出 + +- 存活检测 — 一键批量检活,自动标记存活 / 死亡状态 + +- Token 自动刷新 — 后台定时刷新 refresh_token,保持账号活跃 + +- 任务中心 — 支持单次 / 每日定时任务,覆盖注册、检活、清理三种任务类型 + +- 代理检测 — 注册前可预先检测代理可用性及出口 IP diff --git a/Code-Patch/backend/database.py b/Code-Patch/backend/database.py new file mode 100644 index 0000000..897bd3a --- /dev/null +++ b/Code-Patch/backend/database.py @@ -0,0 +1,106 @@ +import os +import sqlite3 +from contextlib import contextmanager + +# accounts.db 放在根目录(backend/ 的上一级) +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DB_PATH = os.path.join(ROOT_DIR, "accounts.db") + +SCHEMA = """ +PRAGMA journal_mode=WAL; + +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL, + proxies TEXT NOT NULL, + proxy_count INTEGER NOT NULL, + requested INTEGER NOT NULL, + concurrency INTEGER NOT NULL DEFAULT 1, + success INTEGER NOT NULL DEFAULT 0, + failed INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'running' +); + +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL REFERENCES sessions(id), + created_at TEXT NOT NULL, + email TEXT, + account_id TEXT, + refresh_token TEXT, + id_token TEXT, + access_token TEXT, + expired TEXT, + last_refresh TEXT, + proxy_used TEXT, + error TEXT +); + +CREATE INDEX IF NOT EXISTS idx_accounts_session ON accounts(session_id); + +CREATE TABLE IF NOT EXISTS schedules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + task_type TEXT NOT NULL DEFAULT 'register', + proxies TEXT NOT NULL, + target INTEGER NOT NULL DEFAULT 0, + concurrency INTEGER NOT NULL DEFAULT 3, + check_filter TEXT NOT NULL DEFAULT 'all', + schedule_type TEXT NOT NULL DEFAULT 'once', + run_time TEXT NOT NULL, + next_run TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + last_run_at TEXT, + last_session_id INTEGER +); + +CREATE TABLE IF NOT EXISTS schedule_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + schedule_id INTEGER NOT NULL REFERENCES schedules(id), + started_at TEXT NOT NULL, + finished_at TEXT, + task_type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'running', + detail TEXT NOT NULL DEFAULT '' +); + +CREATE INDEX IF NOT EXISTS idx_schedule_runs_sid ON schedule_runs(schedule_id); +""" + + +def init_db(): + with get_conn() as conn: + conn.executescript(SCHEMA) + # 迁移:为旧数据库添加新字段 + for col in ["alive TEXT", "checked_at TEXT", "plan_type TEXT", + "auto_refresh INTEGER DEFAULT 1", "last_auto_refresh TEXT", + "exit_ip TEXT", "usage_json TEXT"]: + try: + conn.execute(f"ALTER TABLE accounts ADD COLUMN {col}") + except Exception: + pass # 列已存在 + # 确保所有账号都开启自动刷新 + conn.execute("UPDATE accounts SET auto_refresh=1 WHERE auto_refresh=0 OR auto_refresh IS NULL") + # 迁移 schedules 新字段 + for col in ["task_type TEXT DEFAULT 'register'", "check_filter TEXT DEFAULT 'all'", + "check_limit INTEGER DEFAULT 0", "auto_clean INTEGER DEFAULT 0"]: + try: + conn.execute(f"ALTER TABLE schedules ADD COLUMN {col}") + except Exception: + pass + + +@contextmanager +def get_conn(): + conn = sqlite3.connect(DB_PATH, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() diff --git a/Code-Patch/backend/main.py b/Code-Patch/backend/main.py new file mode 100644 index 0000000..5061504 --- /dev/null +++ b/Code-Patch/backend/main.py @@ -0,0 +1,1379 @@ +import asyncio +import base64 +import csv +import io +import json +import logging +import os +import random +import time +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timezone, timedelta +from typing import Optional + +logger = logging.getLogger(__name__) + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, field_validator + +from register import run as _register_account, check_alive as _check_alive, check_proxy as _check_proxy +from database import get_conn, init_db + +# .env 在根目录(backend/ 的上一级) +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +load_dotenv(os.path.join(ROOT_DIR, ".env")) + + +def _env_first(*keys: str, default: str = "") -> str: + for key in keys: + value = os.getenv(key, "").strip() + if value: + return value + return default + +# --------------------------------------------------------------------------- +# 读取系统代理 +# --------------------------------------------------------------------------- + +def _get_system_proxy() -> str: + for key in ("HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"): + val = os.environ.get(key, "").strip() + if val: + return val + try: + import winreg + reg_key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", + ) + enabled, _ = winreg.QueryValueEx(reg_key, "ProxyEnable") + if enabled: + server, _ = winreg.QueryValueEx(reg_key, "ProxyServer") + server = server.strip() + if "=" in server: + for part in server.split(";"): + part = part.strip() + if part.startswith("http="): + server = part[5:] + break + if part.startswith("https="): + server = part[6:] + if server and "://" not in server: + server = "http://" + server + return server + except Exception: + pass + return "" + +# --------------------------------------------------------------------------- +# App 初始化 +# --------------------------------------------------------------------------- + +app = FastAPI(title="Account Registrar API") + +FRONTEND_PORT = int(_env_first("FRONTEND_PORT", default="5173")) +FRONTEND_ORIGINS = os.getenv("FRONTEND_ORIGINS", "").strip() +if FRONTEND_ORIGINS: + allow_origins = [o.strip() for o in FRONTEND_ORIGINS.split(",") if o.strip()] +else: + allow_origins = [ + f"http://localhost:{FRONTEND_PORT}", + f"http://127.0.0.1:{FRONTEND_PORT}", + ] + +app.add_middleware( + CORSMiddleware, + allow_origins=allow_origins, + allow_methods=["*"], + allow_headers=["*"], +) + +executor = ThreadPoolExecutor(max_workers=1000) + +# session_id -> list[asyncio.Queue](每个 WS 客户端一个队列) +active_ws: dict[int, list[asyncio.Queue]] = defaultdict(list) + +# session_id -> asyncio.Event(用于暂停/恢复) +session_pause_events: dict[int, asyncio.Event] = {} + +# check_session_id -> list[asyncio.Queue] +active_check_ws: dict[str, list[asyncio.Queue]] = defaultdict(list) + + +@app.on_event("startup") +async def startup(): + init_db() + # 修正重启后遗留的运行中/已暂停状态 + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='done' WHERE status IN ('running','paused','importing')") + asyncio.create_task(_auto_refresh_loop()) + asyncio.create_task(_schedule_loop()) + + +# --------------------------------------------------------------------------- +# 工具函数 +# --------------------------------------------------------------------------- + +def _now() -> str: + return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + + +async def _broadcast(session_id: int, msg: dict): + for q in list(active_ws.get(session_id, [])): + try: + await q.put(msg) + except Exception: + pass + + + +def _build_account_where( + session_id: Optional[int], + status: Optional[str], + search: Optional[str], + alive: Optional[str] = None, +) -> tuple[str, list]: + conditions: list[str] = [] + params: list = [] + if session_id is not None: + conditions.append("session_id = ?") + params.append(session_id) + if status == "success": + conditions.append("error IS NULL") + elif status == "failed": + conditions.append("error IS NOT NULL") + if search: + kw = f"%{search}%" + conditions.append("(email LIKE ? OR account_id LIKE ?)") + params.extend([kw, kw]) + if alive == "unchecked": + conditions.append("alive IS NULL") + elif alive in ("alive", "dead", "error"): + conditions.append("alive = ?") + params.append(alive) + where = ("WHERE " + " AND ".join(conditions)) if conditions else "" + return where, params + + +# --------------------------------------------------------------------------- +# 代理预检 +# --------------------------------------------------------------------------- + +async def _filter_proxies(proxy_list: list[str], concurrency: int = 10) -> list[str]: + """并发检测代理可用性,返回可用的代理列表。相同地址只检测一次。""" + unique = list(dict.fromkeys(proxy_list)) # 去重保序 + loop = asyncio.get_event_loop() + sem = asyncio.Semaphore(concurrency) + checked = {} # proxy -> (ok, reason) + + async def _test(proxy: str): + async with sem: + ok, reason = await loop.run_in_executor(executor, _check_proxy, proxy) + checked[proxy] = (ok, reason) + if not ok: + logger.info("代理不可用: %s -> %s", proxy, reason) + + await asyncio.gather(*[_test(p) for p in unique]) + # 按原始列表顺序返回可用的(保留重复项,因为多个相同代理 = 代理池轮换出口) + return [p for p in proxy_list if checked.get(p, (False,))[0]] + + +# --------------------------------------------------------------------------- +# 注册后台任务 +# --------------------------------------------------------------------------- + +async def _run_session(session_id: int, proxy_list: list[str], target: int, concurrency: int): + """持续注册直到 **成功数** 达到 target,失败会自动重试。""" + # 预检代理 + valid_proxies = await _filter_proxies(proxy_list) + if not valid_proxies: + logger.warning("session %s: 所有代理均不可用", session_id) + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='failed' WHERE id=?", (session_id,)) + return + logger.info("session %s: %d/%d 代理可用", session_id, len(valid_proxies), len(proxy_list)) + proxy_list = valid_proxies + + loop = asyncio.get_event_loop() + sem = asyncio.Semaphore(concurrency) + + # 暂停控制:Event set = 运行中,clear = 暂停 + pause_event = asyncio.Event() + pause_event.set() + session_pause_events[session_id] = pause_event + + # 用 asyncio 锁保护计数器,避免并发竞争 + lock = asyncio.Lock() + counters = {"success": 0, "failed": 0, "consecutive_fails": 0} + max_consecutive_fails = max(target * 3, 50) + + async def _do_one(): + # 随机启动延迟,避免并发请求同时发出 + await asyncio.sleep(random.uniform(0.2, 1.5)) + proxy = random.choice(proxy_list) + t0 = time.time() + try: + result_str = await loop.run_in_executor(executor, _register_account, proxy) + elapsed = round(time.time() - t0, 1) + + if result_str is None: + raise RuntimeError("Account creation failed (server rejected)") + + data = json.loads(result_str) + with get_conn() as conn: + conn.execute( + """INSERT INTO accounts + (session_id, created_at, email, account_id, refresh_token, + id_token, access_token, expired, last_refresh, proxy_used, + auto_refresh, exit_ip) + VALUES (?,?,?,?,?,?,?,?,?,?,1,?)""", + ( + session_id, _now(), + data.get("email"), data.get("account_id"), + data.get("refresh_token"), data.get("id_token"), + data.get("access_token"), data.get("expired"), + data.get("last_refresh"), proxy, + data.get("exit_ip"), + ), + ) + conn.execute( + "UPDATE sessions SET success = success + 1 WHERE id = ?", + (session_id,), + ) + + async with lock: + counters["success"] += 1 + counters["consecutive_fails"] = 0 + idx = counters["success"] + + await _broadcast(session_id, { + "type": "success", + "index": idx, + "email": data.get("email"), + "proxy": proxy, + "elapsed": elapsed, + }) + + # 成功后随机等待 + await asyncio.sleep(random.uniform(3, 8)) + + except Exception as exc: + elapsed = round(time.time() - t0, 1) + err_msg = str(exc) + with get_conn() as conn: + conn.execute( + """INSERT INTO accounts + (session_id, created_at, proxy_used, error) + VALUES (?,?,?,?)""", + (session_id, _now(), proxy, err_msg), + ) + conn.execute( + "UPDATE sessions SET failed = failed + 1 WHERE id = ?", + (session_id,), + ) + + async with lock: + counters["failed"] += 1 + counters["consecutive_fails"] += 1 + + await _broadcast(session_id, { + "type": "failed", + "error": err_msg, + "proxy": proxy, + "elapsed": elapsed, + }) + + # 失败后短暂等待再重试 + await asyncio.sleep(random.uniform(1, 3)) + + async def _worker(): + while True: + # 暂停时在此等待 + await pause_event.wait() + async with lock: + done = counters["success"] >= target + stopped = counters["consecutive_fails"] >= max_consecutive_fails + if done or stopped: + break + async with sem: + await pause_event.wait() + async with lock: + if counters["success"] >= target: + break + await _do_one() + + workers = [asyncio.create_task(_worker()) for _ in range(min(concurrency, target))] + await asyncio.gather(*workers) + session_pause_events.pop(session_id, None) + + with get_conn() as conn: + row = conn.execute( + "SELECT success, failed FROM sessions WHERE id = ?", (session_id,) + ).fetchone() + conn.execute( + "UPDATE sessions SET status = 'done' WHERE id = ?", (session_id,) + ) + + await _broadcast(session_id, { + "type": "done", + "success": row["success"] if row else 0, + "failed": row["failed"] if row else 0, + "total": target, + }) + + +# --------------------------------------------------------------------------- +# WebSocket +# --------------------------------------------------------------------------- + +@app.websocket("/ws/sessions/{session_id}") +async def ws_session(websocket: WebSocket, session_id: int): + await websocket.accept() + + with get_conn() as conn: + row = conn.execute( + "SELECT status, success, failed, requested FROM sessions WHERE id = ?", + (session_id,), + ).fetchone() + + if row and row["status"] == "done": + await websocket.send_json({ + "type": "done", + "success": row["success"], + "failed": row["failed"], + "total": row["requested"], + }) + await websocket.close() + return + + q: asyncio.Queue = asyncio.Queue() + active_ws[session_id].append(q) + + try: + while True: + try: + msg = await asyncio.wait_for(q.get(), timeout=120) + except asyncio.TimeoutError: + await websocket.send_json({"type": "ping"}) + continue + await websocket.send_json(msg) + if msg.get("type") == "done": + break + except (WebSocketDisconnect, Exception): + pass + finally: + try: + active_ws[session_id].remove(q) + except ValueError: + pass + + +# --------------------------------------------------------------------------- +# 系统代理 +# --------------------------------------------------------------------------- + +def _get_proxy_pool() -> str: + """从 .env PROXY_POOL 读取代理池,支持逗号或换行分隔。""" + raw = os.getenv("PROXY_POOL", "").strip() + if not raw: + # 回退到系统代理 + sp = _get_system_proxy() + return sp + # 统一逗号分隔 → 换行 + lines = [p.strip() for p in raw.replace(",", "\n").splitlines() if p.strip()] + return "\n".join(lines) + + +@app.get("/api/system-proxy") +async def system_proxy(): + return {"proxy": _get_proxy_pool()} + + +# --------------------------------------------------------------------------- +# Sessions +# --------------------------------------------------------------------------- + +class StartSessionRequest(BaseModel): + proxies: str + count: int + concurrency: int = 3 + + @field_validator("count") + @classmethod + def count_range(cls, v): + if v < 1: + raise ValueError("count must be >= 1") + return v + + @field_validator("concurrency") + @classmethod + def concurrency_range(cls, v): + if not (1 <= v <= 1000): + raise ValueError("concurrency must be 1-1000") + return v + + +@app.post("/api/sessions", status_code=201) +async def start_session(req: StartSessionRequest): + proxy_list = [p.strip() for p in req.proxies.splitlines() if p.strip()] + if not proxy_list: + raise HTTPException(400, "至少需要一个代理地址") + + with get_conn() as conn: + cur = conn.execute( + """INSERT INTO sessions (created_at, proxies, proxy_count, requested, concurrency) + VALUES (?,?,?,?,?)""", + (_now(), req.proxies, len(proxy_list), req.count, req.concurrency), + ) + session_id = cur.lastrowid + + active_ws[session_id] # 预初始化 defaultdict + asyncio.create_task(_run_session(session_id, proxy_list, req.count, req.concurrency)) + + return {"session_id": session_id} + + +@app.get("/api/sessions/active") +async def get_active_session(): + """查询当前运行中或暂停的 session(用于前端恢复进度界面)。""" + with get_conn() as conn: + row = conn.execute( + "SELECT * FROM sessions WHERE status IN ('running','paused') ORDER BY id DESC LIMIT 1" + ).fetchone() + if not row: + return {"session": None} + return {"session": dict(row)} + + +@app.post("/api/sessions/{session_id}/pause") +async def pause_session(session_id: int): + ev = session_pause_events.get(session_id) + if not ev: + # 任务已不在内存中,修正数据库状态 + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='done' WHERE id=? AND status IN ('running','paused')", (session_id,)) + raise HTTPException(409, "任务已结束,状态已修正") + ev.clear() + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='paused' WHERE id=?", (session_id,)) + await _broadcast(session_id, {"type": "paused"}) + return {"status": "paused"} + + +@app.post("/api/sessions/{session_id}/resume") +async def resume_session(session_id: int): + ev = session_pause_events.get(session_id) + if not ev: + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='done' WHERE id=? AND status IN ('running','paused')", (session_id,)) + raise HTTPException(409, "任务已结束,状态已修正") + ev.set() + with get_conn() as conn: + conn.execute("UPDATE sessions SET status='running' WHERE id=?", (session_id,)) + await _broadcast(session_id, {"type": "resumed"}) + return {"status": "running"} + + +@app.get("/api/sessions") +async def list_sessions(): + with get_conn() as conn: + rows = conn.execute("SELECT * FROM sessions ORDER BY id DESC").fetchall() + # 统计每个 session 的出口 IP 使用情况 + ip_stats = conn.execute( + """SELECT session_id, + COUNT(DISTINCT exit_ip) AS unique_ips, + COUNT(exit_ip) AS total_uses + FROM accounts + WHERE exit_ip IS NOT NULL + GROUP BY session_id""" + ).fetchall() + ip_map = {r["session_id"]: {"unique_ips": r["unique_ips"], "reused_ips": r["total_uses"] - r["unique_ips"]} for r in ip_stats} + result = [] + for r in rows: + d = dict(r) + stats = ip_map.get(d["id"], {"unique_ips": 0, "reused_ips": 0}) + d.update(stats) + result.append(d) + return result + + +@app.get("/api/sessions/{session_id}/export") +async def export_session(session_id: int): + with get_conn() as conn: + rows = conn.execute( + """SELECT email, account_id, refresh_token, id_token, access_token, + expired, last_refresh, proxy_used, created_at + FROM accounts + WHERE session_id = ? AND error IS NULL + ORDER BY id""", + (session_id,), + ).fetchall() + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow([ + "email", "account_id", "refresh_token", "id_token", "access_token", + "expired", "last_refresh", "proxy_used", "created_at", + ]) + for row in rows: + writer.writerow(list(row)) + output.seek(0) + + return StreamingResponse( + io.BytesIO(output.getvalue().encode("utf-8")), + media_type="text/csv", + headers={"Content-Disposition": f'attachment; filename="session_{session_id}.csv"'}, + ) + + +# --------------------------------------------------------------------------- +# Accounts +# --------------------------------------------------------------------------- + +@app.get("/api/accounts") +async def list_accounts( + session_id: Optional[int] = None, + status: Optional[str] = None, + search: Optional[str] = None, + alive: Optional[str] = None, + page: int = 1, + page_size: int = 50, +): + page_size = min(page_size, 200) + offset = (page - 1) * page_size + where, params = _build_account_where(session_id, status, search, alive) + + with get_conn() as conn: + total = conn.execute( + f"SELECT COUNT(*) FROM accounts {where}", params + ).fetchone()[0] + rows = conn.execute( + f"""SELECT id, session_id, created_at, email, account_id, + expired, proxy_used, error, alive, checked_at, plan_type, + auto_refresh, last_auto_refresh, exit_ip, usage_json + FROM accounts {where} + ORDER BY id DESC LIMIT ? OFFSET ?""", + params + [page_size, offset], + ).fetchall() + + return {"total": total, "page": page, "page_size": page_size, "items": [dict(r) for r in rows]} + + +@app.get("/api/accounts/export") +async def export_accounts( + session_id: Optional[int] = None, + status: Optional[str] = None, + search: Optional[str] = None, + alive: Optional[str] = None, +): + where, params = _build_account_where(session_id, status, search, alive) + with get_conn() as conn: + rows = conn.execute( + f"""SELECT email, account_id, refresh_token, id_token, access_token, + expired, last_refresh, proxy_used, created_at + FROM accounts {where} + ORDER BY id DESC""", + params, + ).fetchall() + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow([ + "email", "account_id", "refresh_token", "id_token", "access_token", + "expired", "last_refresh", "proxy_used", "created_at", + ]) + for row in rows: + writer.writerow(list(row)) + output.seek(0) + + filename = "accounts_search.csv" if search else "accounts.csv" + return StreamingResponse( + io.BytesIO(output.getvalue().encode("utf-8")), + media_type="text/csv", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + +class ImportAccountsRequest(BaseModel): + tokens: str + proxy: str = "" + concurrency: int = 3 + + @field_validator("concurrency") + @classmethod + def concurrency_range(cls, v): + if not (1 <= v <= 10): + raise ValueError("concurrency must be 1-10") + return v + + +def _parse_import_lines(raw: str) -> list[str]: + """将导入文本解析为行列表,支持 CSV 格式(自动转为 JSON 行)。""" + lines = [l.strip() for l in raw.splitlines() if l.strip()] + if not lines: + return [] + # 检测是否为 CSV:首行包含 refresh_token 表头 + header = lines[0].lower() + if "refresh_token" in header and "," in header: + reader = csv.DictReader(io.StringIO(raw)) + result = [] + for row in reader: + rt = (row.get("refresh_token") or "").strip() + if rt: + result.append(json.dumps({k: v for k, v in row.items() if v}, ensure_ascii=False)) + return result + return lines + + +@app.post("/api/accounts/import", status_code=201) +async def import_accounts(req: ImportAccountsRequest): + lines = _parse_import_lines(req.tokens) + if not lines: + raise HTTPException(400, "请输入至少一个 token") + proxy = req.proxy.strip() or _get_proxy_pool() + if not proxy: + raise HTTPException(400, "请提供代理地址") + + with get_conn() as conn: + cur = conn.execute( + """INSERT INTO sessions (created_at, proxies, proxy_count, requested, concurrency, status) + VALUES (?,?,?,?,?,?)""", + (_now(), proxy, 1, len(lines), req.concurrency, "importing"), + ) + session_id = cur.lastrowid + + import_id = str(_uuid.uuid4()) + active_check_ws[import_id] + asyncio.create_task(_run_import_session(import_id, session_id, lines, proxy, req.concurrency)) + return {"import_id": import_id, "session_id": session_id, "total": len(lines)} + + +@app.put("/api/accounts/{account_id_pk}/auto-refresh") +async def set_auto_refresh(account_id_pk: int, enabled: bool): + with get_conn() as conn: + conn.execute( + "UPDATE accounts SET auto_refresh=? WHERE id=?", + (1 if enabled else 0, account_id_pk), + ) + return {"auto_refresh": enabled} + + +@app.get("/api/accounts/{account_id_pk}") +async def get_account(account_id_pk: int): + with get_conn() as conn: + row = conn.execute( + "SELECT * FROM accounts WHERE id = ?", (account_id_pk,) + ).fetchone() + if not row: + raise HTTPException(404, "Account not found") + return dict(row) + + +@app.delete("/api/accounts/dead") +async def delete_dead_accounts(): + """删除所有已失效(alive='dead')的账号。""" + with get_conn() as conn: + result = conn.execute("DELETE FROM accounts WHERE alive = 'dead'") + count = result.rowcount + return {"deleted": count} + + +# --------------------------------------------------------------------------- +# 存活检测 +# --------------------------------------------------------------------------- + +import uuid as _uuid + +class CheckSessionRequest(BaseModel): + account_ids: list[int] + proxies: str + concurrency: int = 5 + + @field_validator("concurrency") + @classmethod + def concurrency_range(cls, v): + if not (1 <= v <= 1000): + raise ValueError("concurrency must be 1-1000") + return v + + +async def _broadcast_check(check_id: str, msg: dict): + for q in list(active_check_ws.get(check_id, [])): + try: + await q.put(msg) + except Exception: + pass + + +async def _run_check_session( + check_id: str, account_ids: list[int], proxy_list: list[str], concurrency: int +): + # 预检代理 + valid_proxies = await _filter_proxies(proxy_list) + if not valid_proxies: + logger.warning("check %s: 所有代理均不可用", check_id) + _broadcast(f"check:{check_id}", {"type": "done", "detail": "所有代理均不可用"}) + return + logger.info("check %s: %d/%d 代理可用", check_id, len(valid_proxies), len(proxy_list)) + proxy_list = valid_proxies + + loop = asyncio.get_event_loop() + sem = asyncio.Semaphore(concurrency) + total = len(account_ids) + + async def _check_one(acct_id: int): + async with sem: + # 随机启动延迟,避免并发请求同时发出 + await asyncio.sleep(random.uniform(0.2, 1.0)) + # 获取 refresh_token + with get_conn() as conn: + row = conn.execute( + "SELECT refresh_token FROM accounts WHERE id = ?", (acct_id,) + ).fetchone() + if not row or not row["refresh_token"]: + with get_conn() as conn: + conn.execute( + "UPDATE accounts SET alive='error', checked_at=? WHERE id=?", + (_now(), acct_id), + ) + await _broadcast_check(check_id, { + "type": "result", "account_id": acct_id, "alive": "error" + }) + return + + proxy = random.choice(proxy_list) + result = await loop.run_in_executor( + executor, _check_alive, row["refresh_token"], proxy + ) + alive_status, new_access, new_refresh, new_id, plan_type, expires_at, usage_json = result + + with get_conn() as conn: + if alive_status == "alive": + conn.execute( + """UPDATE accounts + SET alive=?, checked_at=?, + access_token=COALESCE(?,access_token), + refresh_token=COALESCE(?,refresh_token), + id_token=COALESCE(?,id_token), + plan_type=COALESCE(?,plan_type), + expired=COALESCE(?,expired), + usage_json=COALESCE(?,usage_json) + WHERE id=?""", + (alive_status, _now(), new_access, new_refresh, new_id, + plan_type, expires_at, usage_json, acct_id), + ) + else: + conn.execute( + "UPDATE accounts SET alive=?, checked_at=? WHERE id=?", + (alive_status, _now(), acct_id), + ) + + await _broadcast_check(check_id, { + "type": "result", "account_id": acct_id, "alive": alive_status + }) + + await asyncio.gather(*[_check_one(aid) for aid in account_ids]) + + # 统计 + with get_conn() as conn: + stats = conn.execute( + """SELECT alive, COUNT(*) as cnt FROM accounts + WHERE id IN ({}) GROUP BY alive""".format( + ",".join("?" * len(account_ids)) + ), + account_ids, + ).fetchall() + stat_map = {r["alive"]: r["cnt"] for r in stats} + + await _broadcast_check(check_id, { + "type": "done", + "total": total, + "alive": stat_map.get("alive", 0), + "dead": stat_map.get("dead", 0), + "error": stat_map.get("error", 0), + }) + + +@app.post("/api/check-sessions", status_code=201) +async def start_check_session(req: CheckSessionRequest): + if not req.account_ids: + raise HTTPException(400, "account_ids 不能为空") + proxy_list = [p.strip() for p in req.proxies.splitlines() if p.strip()] + if not proxy_list: + raise HTTPException(400, "至少需要一个代理地址") + + check_id = str(_uuid.uuid4()) + active_check_ws[check_id] # 预初始化 + asyncio.create_task( + _run_check_session(check_id, req.account_ids, proxy_list, req.concurrency) + ) + return {"check_id": check_id, "total": len(req.account_ids)} + + +@app.websocket("/ws/check/{check_id}") +async def ws_check(websocket: WebSocket, check_id: str): + await websocket.accept() + q: asyncio.Queue = asyncio.Queue() + active_check_ws[check_id].append(q) + try: + while True: + try: + msg = await asyncio.wait_for(q.get(), timeout=120) + except asyncio.TimeoutError: + await websocket.send_json({"type": "ping"}) + continue + await websocket.send_json(msg) + if msg.get("type") == "done": + break + except (WebSocketDisconnect, Exception): + pass + finally: + try: + active_check_ws[check_id].remove(q) + except ValueError: + pass + + +# --------------------------------------------------------------------------- +# 导入账号 +# --------------------------------------------------------------------------- + +def _extract_from_id_token(id_token: str) -> tuple: + """从 JWT id_token 中解析 email 和 account_id(不校验签名)。""" + if not id_token or id_token.count(".") < 2: + return "", "" + payload_b64 = id_token.split(".")[1] + pad = "=" * ((4 - (len(payload_b64) % 4)) % 4) + try: + payload = json.loads(base64.urlsafe_b64decode((payload_b64 + pad).encode("ascii")).decode("utf-8")) + email = str(payload.get("email") or "") + auth_claims = payload.get("https://api.openai.com/auth") or {} + account_id = str(auth_claims.get("chatgpt_account_id") or "") + return email, account_id + except Exception: + return "", "" + + +async def _run_import_session(import_id: str, session_id: int, lines: list, proxy: str, concurrency: int): + # 预检代理 + loop = asyncio.get_event_loop() + ok, reason = await loop.run_in_executor(executor, _check_proxy, proxy) + if not ok: + logger.warning("import %s: 代理不可用: %s -> %s", import_id, proxy, reason) + _broadcast(f"session:{import_id}", {"type": "done", "detail": f"代理不可用: {reason}"}) + return + + sem = asyncio.Semaphore(concurrency) + + async def _import_one(line: str): + async with sem: + # 随机启动延迟,避免并发请求同时发出 + await asyncio.sleep(random.uniform(0.2, 1.0)) + refresh_token = None + extra = {} + try: + obj = json.loads(line) + refresh_token = obj.get("refresh_token") or obj.get("token") + extra = {k: obj.get(k) for k in ("access_token", "id_token", "email", "account_id", "expired", "last_refresh")} + except (json.JSONDecodeError, ValueError): + refresh_token = line.strip() + + if not refresh_token: + with get_conn() as conn: + conn.execute("UPDATE sessions SET failed = failed + 1 WHERE id = ?", (session_id,)) + await _broadcast_check(import_id, {"type": "result", "alive": "error", "email": None}) + return + + result = await loop.run_in_executor(executor, _check_alive, refresh_token, proxy) + alive_status, new_access, new_refresh, new_id, plan_type, expires_at, usage_json = result + + # 从 id_token 里解析 email / account_id + email = extra.get("email") or "" + account_id = extra.get("account_id") or "" + if new_id and (not email or not account_id): + em, aid = _extract_from_id_token(new_id) + email = email or em + account_id = account_id or aid + + now = _now() + with get_conn() as conn: + cur = conn.execute( + """INSERT INTO accounts + (session_id, created_at, email, account_id, refresh_token, id_token, + access_token, expired, last_refresh, proxy_used, alive, checked_at, + plan_type, auto_refresh, usage_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,1,?)""", + ( + session_id, now, + email or None, + account_id or None, + new_refresh or refresh_token, + new_id or extra.get("id_token"), + new_access or extra.get("access_token"), + expires_at or extra.get("expired"), + extra.get("last_refresh") or now, + proxy, + alive_status, + now, + plan_type, + usage_json, + ), + ) + acct_id = cur.lastrowid + if alive_status == "alive": + conn.execute("UPDATE sessions SET success = success + 1 WHERE id = ?", (session_id,)) + else: + conn.execute("UPDATE sessions SET failed = failed + 1 WHERE id = ?", (session_id,)) + + await _broadcast_check(import_id, { + "type": "result", + "account_id": acct_id, + "alive": alive_status, + "email": email or None, + }) + + await asyncio.gather(*[_import_one(line) for line in lines]) + + with get_conn() as conn: + row = conn.execute("SELECT success, failed FROM sessions WHERE id = ?", (session_id,)).fetchone() + conn.execute("UPDATE sessions SET status = 'done' WHERE id = ?", (session_id,)) + + await _broadcast_check(import_id, { + "type": "done", + "total": len(lines), + "alive": row["success"] if row else 0, + "dead": 0, + "error": row["failed"] if row else 0, + }) + + +# --------------------------------------------------------------------------- +# 自动保活 +# --------------------------------------------------------------------------- + +async def _do_auto_refresh(): + pool_str = _get_proxy_pool() + proxy_pool = [p.strip() for p in pool_str.splitlines() if p.strip()] if pool_str else [] + + # 刷新 10 分钟内即将过期的账号(或 expired 为空) + threshold = (datetime.now(timezone.utc) + timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ") + + with get_conn() as conn: + rows = conn.execute( + """SELECT id, refresh_token, proxy_used FROM accounts + WHERE auto_refresh=1 AND alive != 'dead' AND refresh_token IS NOT NULL + AND (expired IS NULL OR expired <= ?)""", + (threshold,), + ).fetchall() + + if not rows: + return + + logger.info(f"Auto-refresh: 刷新 {len(rows)} 个账号") + loop = asyncio.get_event_loop() + sem = asyncio.Semaphore(3) + + async def _refresh_one(row): + proxy = random.choice(proxy_pool) if proxy_pool else row["proxy_used"] + if not proxy: + return + async with sem: + result = await loop.run_in_executor(executor, _check_alive, row["refresh_token"], proxy) + alive_status, new_access, new_refresh, new_id, plan_type, expires_at, usage_json = result + with get_conn() as conn: + conn.execute( + """UPDATE accounts + SET alive=?, checked_at=?, last_auto_refresh=?, + access_token=COALESCE(?,access_token), + refresh_token=COALESCE(?,refresh_token), + id_token=COALESCE(?,id_token), + plan_type=COALESCE(?,plan_type), + expired=COALESCE(?,expired), + usage_json=COALESCE(?,usage_json) + WHERE id=?""", + (alive_status, _now(), _now(), + new_access, new_refresh, new_id, plan_type, expires_at, usage_json, + row["id"]), + ) + + await asyncio.gather(*[_refresh_one(row) for row in rows]) + + +async def _auto_refresh_loop(): + while True: + try: + await asyncio.sleep(1800) # 每 30 分钟检查一次 + await _do_auto_refresh() + except Exception as e: + logger.error(f"Auto-refresh 异常: {e}") + + +# --------------------------------------------------------------------------- +# 定时任务 +# --------------------------------------------------------------------------- + +class ScheduleRequest(BaseModel): + name: str = "" + task_type: str = "register" # 'register' | 'check' | 'refresh' | 'clean' + proxies: str = "" + target: int = 0 + concurrency: int = 3 + check_filter: str = "all" # 'all' | 'alive' | 'unchecked' + check_limit: int = 0 # 0 = 全部 + auto_clean: bool = False # 检测后自动清理 dead + schedule_type: str # 'once' | 'daily' + run_time: str # once: "2026-03-20T10:30:00" | daily: "10:30" + + @field_validator("schedule_type") + @classmethod + def validate_type(cls, v): + if v not in ("once", "daily"): + raise ValueError("schedule_type must be 'once' or 'daily'") + return v + + @field_validator("task_type") + @classmethod + def validate_task_type(cls, v): + if v not in ("register", "check", "refresh", "clean"): + raise ValueError("task_type must be 'register', 'check', 'refresh' or 'clean'") + return v + + +def _calc_next_run(schedule_type: str, run_time: str) -> str: + """计算下次运行时间(本地时间)。""" + now = datetime.now() + if schedule_type == "once": + return run_time + # daily: run_time 格式 "HH:MM" + h, m = map(int, run_time.split(":")) + next_run = now.replace(hour=h, minute=m, second=0, microsecond=0) + if next_run <= now: + next_run += timedelta(days=1) + return next_run.strftime("%Y-%m-%dT%H:%M:%S") + + +@app.get("/api/schedules") +async def list_schedules(): + with get_conn() as conn: + rows = conn.execute("SELECT * FROM schedules ORDER BY id DESC").fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/schedules", status_code=201) +async def create_schedule(req: ScheduleRequest): + proxy_list = [p.strip() for p in req.proxies.splitlines() if p.strip()] + if req.task_type != "clean" and not proxy_list: + raise HTTPException(400, "至少需要一个代理地址") + next_run = _calc_next_run(req.schedule_type, req.run_time) + with get_conn() as conn: + cur = conn.execute( + """INSERT INTO schedules + (created_at, name, task_type, proxies, target, concurrency, + check_filter, check_limit, auto_clean, schedule_type, run_time, next_run, enabled) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1)""", + (_now(), req.name, req.task_type, req.proxies, req.target, req.concurrency, + req.check_filter, req.check_limit, int(req.auto_clean), + req.schedule_type, req.run_time, next_run), + ) + sid = cur.lastrowid + return {"id": sid} + + +@app.put("/api/schedules/{schedule_id}") +async def update_schedule(schedule_id: int, req: ScheduleRequest): + next_run = _calc_next_run(req.schedule_type, req.run_time) + with get_conn() as conn: + conn.execute( + """UPDATE schedules + SET name=?, task_type=?, proxies=?, target=?, concurrency=?, + check_filter=?, check_limit=?, auto_clean=?, + schedule_type=?, run_time=?, next_run=? + WHERE id=?""", + (req.name, req.task_type, req.proxies, req.target, req.concurrency, + req.check_filter, req.check_limit, int(req.auto_clean), + req.schedule_type, req.run_time, next_run, schedule_id), + ) + return {"ok": True} + + +@app.put("/api/schedules/{schedule_id}/toggle") +async def toggle_schedule(schedule_id: int): + with get_conn() as conn: + row = conn.execute("SELECT enabled, schedule_type, run_time FROM schedules WHERE id=?", (schedule_id,)).fetchone() + if not row: + raise HTTPException(404) + new_enabled = 0 if row["enabled"] else 1 + updates = {"enabled": new_enabled} + if new_enabled: + updates["next_run"] = _calc_next_run(row["schedule_type"], row["run_time"]) + conn.execute( + "UPDATE schedules SET enabled=?, next_run=? WHERE id=?", + (new_enabled, updates.get("next_run", None), schedule_id), + ) + return {"enabled": bool(new_enabled)} + + +@app.delete("/api/schedules/{schedule_id}") +async def delete_schedule(schedule_id: int): + with get_conn() as conn: + conn.execute("DELETE FROM schedules WHERE id=?", (schedule_id,)) + return {"ok": True} + + +@app.get("/api/schedules/{schedule_id}/runs") +async def get_schedule_runs(schedule_id: int): + with get_conn() as conn: + rows = conn.execute( + "SELECT * FROM schedule_runs WHERE schedule_id=? ORDER BY id DESC LIMIT 50", + (schedule_id,), + ).fetchall() + return [dict(r) for r in rows] + + +@app.get("/api/schedule-runs") +async def get_all_runs(limit: int = 50): + """获取所有任务的最近执行记录。""" + with get_conn() as conn: + rows = conn.execute( + """SELECT r.*, s.name as schedule_name + FROM schedule_runs r + LEFT JOIN schedules s ON s.id = r.schedule_id + ORDER BY r.id DESC LIMIT ?""", + (limit,), + ).fetchall() + return [dict(r) for r in rows] + + +async def _check_schedules(): + """检查是否有到期的定时任务,触发注册/检测/刷新/清理。""" + now_str = _now() + with get_conn() as conn: + rows = conn.execute( + "SELECT * FROM schedules WHERE enabled=1 AND next_run <= ?", + (now_str,), + ).fetchall() + + for sched in rows: + sched = dict(sched) + proxy_list = [p.strip() for p in (sched.get("proxies") or "").splitlines() if p.strip()] + + task_type = sched.get("task_type") or "register" + logger.info("定时任务触发: id=%s name=%s type=%s", sched["id"], sched["name"], task_type) + + # 创建执行记录 + with get_conn() as conn: + cur = conn.execute( + "INSERT INTO schedule_runs (schedule_id, started_at, task_type, status, detail) VALUES (?,?,?,?,?)", + (sched["id"], now_str, task_type, "running", ""), + ) + run_id = cur.lastrowid + + session_id = None + + if task_type == "register": + if not proxy_list: + _finish_run(run_id, "failed", "无可用代理") + continue + with get_conn() as conn: + cur = conn.execute( + """INSERT INTO sessions (created_at, proxies, proxy_count, requested, concurrency) + VALUES (?,?,?,?,?)""", + (_now(), sched["proxies"], len(proxy_list), sched["target"], sched["concurrency"]), + ) + session_id = cur.lastrowid + active_ws[session_id] + asyncio.create_task( + _tracked_register(run_id, session_id, proxy_list, sched["target"], sched["concurrency"]) + ) + + elif task_type == "check": + if not proxy_list: + _finish_run(run_id, "failed", "无可用代理") + continue + check_filter = sched.get("check_filter") or "all" + check_limit = sched.get("check_limit") or 0 + auto_clean = bool(sched.get("auto_clean")) + with get_conn() as conn: + limit_clause = f" ORDER BY RANDOM() LIMIT {check_limit}" if check_limit > 0 else "" + if check_filter == "alive": + sql = f"SELECT id FROM accounts WHERE alive='alive' AND refresh_token IS NOT NULL{limit_clause}" + elif check_filter == "unchecked": + sql = f"SELECT id FROM accounts WHERE alive IS NULL AND refresh_token IS NOT NULL{limit_clause}" + else: + sql = f"SELECT id FROM accounts WHERE error IS NULL AND refresh_token IS NOT NULL{limit_clause}" + acct_rows = conn.execute(sql).fetchall() + + account_ids = [r["id"] for r in acct_rows] + if account_ids: + check_id = str(_uuid.uuid4()) + active_check_ws[check_id] + asyncio.create_task( + _tracked_check(run_id, check_id, account_ids, proxy_list, sched["concurrency"], auto_clean) + ) + else: + _finish_run(run_id, "done", "无需检测的账号") + + elif task_type == "refresh": + if not proxy_list: + _finish_run(run_id, "failed", "无可用代理") + continue + check_limit = sched.get("check_limit") or 0 + with get_conn() as conn: + limit_clause = f" ORDER BY RANDOM() LIMIT {check_limit}" if check_limit > 0 else "" + acct_rows = conn.execute( + f"SELECT id FROM accounts WHERE error IS NULL AND refresh_token IS NOT NULL{limit_clause}" + ).fetchall() + account_ids = [r["id"] for r in acct_rows] + if account_ids: + check_id = str(_uuid.uuid4()) + active_check_ws[check_id] + asyncio.create_task( + _tracked_refresh(run_id, check_id, account_ids, proxy_list, sched["concurrency"]) + ) + else: + _finish_run(run_id, "done", "无需刷新的账号") + + elif task_type == "clean": + with get_conn() as conn: + result = conn.execute("DELETE FROM accounts WHERE alive = 'dead'") + count = result.rowcount + _finish_run(run_id, "done", f"清理 {count} 个失效账号") + + # 更新定时任务状态 + with get_conn() as conn: + if sched["schedule_type"] == "once": + conn.execute( + "UPDATE schedules SET enabled=0, last_run_at=?, last_session_id=?, next_run=NULL WHERE id=?", + (now_str, session_id, sched["id"]), + ) + else: + next_run = _calc_next_run("daily", sched["run_time"]) + conn.execute( + "UPDATE schedules SET last_run_at=?, last_session_id=?, next_run=? WHERE id=?", + (now_str, session_id, next_run, sched["id"]), + ) + + +def _finish_run(run_id: int, status: str, detail: str): + with get_conn() as conn: + conn.execute( + "UPDATE schedule_runs SET finished_at=?, status=?, detail=? WHERE id=?", + (_now(), status, detail, run_id), + ) + + +def _update_run_detail(run_id: int, detail: str): + with get_conn() as conn: + conn.execute("UPDATE schedule_runs SET detail=? WHERE id=?", (detail, run_id)) + + +async def _tracked_register(run_id, session_id, proxy_list, target, concurrency): + """注册任务包装:实时更新进度,完成后更新执行记录。""" + try: + # 启动一个后台任务定期更新进度 + done_event = asyncio.Event() + + async def _poll_progress(): + while not done_event.is_set(): + await asyncio.sleep(5) + with get_conn() as conn: + row = conn.execute("SELECT success, failed FROM sessions WHERE id=?", (session_id,)).fetchone() + if row: + _update_run_detail(run_id, f"成功 {row['success']} / 目标 {target},失败 {row['failed']}") + + poll_task = asyncio.create_task(_poll_progress()) + await _run_session(session_id, proxy_list, target, concurrency) + done_event.set() + await poll_task + + with get_conn() as conn: + row = conn.execute("SELECT success, failed FROM sessions WHERE id=?", (session_id,)).fetchone() + detail = f"成功 {row['success']} / 目标 {target},失败 {row['failed']}" if row else "" + _finish_run(run_id, "done", detail) + except Exception as e: + _finish_run(run_id, "failed", str(e)) + + +async def _tracked_check(run_id, check_id, account_ids, proxy_list, concurrency, auto_clean): + """检测任务包装:实时更新进度,完成后可选自动清理。""" + total = len(account_ids) + try: + done_event = asyncio.Event() + + async def _poll_progress(): + while not done_event.is_set(): + await asyncio.sleep(5) + with get_conn() as conn: + row = conn.execute( + "SELECT COUNT(*) as checked FROM accounts WHERE id IN ({}) AND checked_at >= ?".format( + ",".join("?" * total) + ), + [*account_ids, _now()[:10]], + ).fetchone() + checked = row["checked"] if row else 0 + _update_run_detail(run_id, f"已检测 {checked} / {total}") + + poll_task = asyncio.create_task(_poll_progress()) + await _run_check_session(check_id, account_ids, proxy_list, concurrency) + done_event.set() + await poll_task + + cleaned = 0 + if auto_clean: + with get_conn() as conn: + result = conn.execute("DELETE FROM accounts WHERE alive = 'dead'") + cleaned = result.rowcount + detail = f"检测 {total} 个账号" + if cleaned: + detail += f",清理 {cleaned} 个失效" + _finish_run(run_id, "done", detail) + except Exception as e: + _finish_run(run_id, "failed", str(e)) + + +async def _tracked_refresh(run_id, check_id, account_ids, proxy_list, concurrency): + """刷新任务包装:实时更新进度,完成后更新执行记录。""" + total = len(account_ids) + try: + done_event = asyncio.Event() + + async def _poll_progress(): + while not done_event.is_set(): + await asyncio.sleep(5) + with get_conn() as conn: + row = conn.execute( + "SELECT COUNT(*) as done FROM accounts WHERE id IN ({}) AND last_auto_refresh >= ?".format( + ",".join("?" * total) + ), + [*account_ids, _now()[:10]], + ).fetchone() + done_count = row["done"] if row else 0 + _update_run_detail(run_id, f"已刷新 {done_count} / {total}") + + poll_task = asyncio.create_task(_poll_progress()) + await _run_check_session(check_id, account_ids, proxy_list, concurrency) + done_event.set() + await poll_task + _finish_run(run_id, "done", f"刷新 {total} 个账号") + except Exception as e: + _finish_run(run_id, "failed", str(e)) + + +async def _schedule_loop(): + while True: + try: + await asyncio.sleep(30) # 每 30 秒检查一次 + await _check_schedules() + except Exception as e: + logger.error(f"Schedule loop 异常: {e}") + + +if __name__ == '__main__': + import uvicorn + backend_host = _env_first("BACKEND_HOST", "APP_HOST", default="127.0.0.1") + backend_port = int(_env_first("BACKEND_PORT", "APP_PORT", default="8000")) + uvicorn.run(app, host=backend_host, port=backend_port) diff --git a/Code-Patch/backend/register.py b/Code-Patch/backend/register.py new file mode 100644 index 0000000..7ca197d --- /dev/null +++ b/Code-Patch/backend/register.py @@ -0,0 +1,566 @@ +import base64 +import hashlib +import json +import logging +import os +import random +import re +import secrets +import string +import time +import urllib +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from typing import Any, Dict + +from curl_cffi import requests +from dotenv import load_dotenv + +logger = logging.getLogger("uvicorn.error") + +# .env 在根目录(backend/ 的上一级) +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +load_dotenv(os.path.join(ROOT_DIR, ".env")) + +WORKER_URL = os.getenv("WORKER_URL", "").strip() +EMAIL_DOMAIN = os.getenv("EMAIL_DOMAIN", "").strip() +ADMIN_AUTH = os.getenv("ADMIN_AUTH", "").strip() + +# --------------------------------------------------------------------------- +# 临时邮箱 +# --------------------------------------------------------------------------- + +def _random_profile() -> str: + """生成随机姓名 + 成年出生日期(18-55岁)的 JSON 字符串。""" + first = ''.join(random.choices(string.ascii_lowercase, k=random.randint(4, 8))).capitalize() + last = ''.join(random.choices(string.ascii_lowercase, k=random.randint(4, 8))).capitalize() + name = f"{first} {last}" + + today = time.gmtime() + age = random.randint(18, 55) + year = today.tm_year - age + month = random.randint(1, 12) + # 确保出生日期不超过今天 + max_day = [31,28,31,30,31,30,31,31,30,31,30,31][month - 1] + if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): + if month == 2: + max_day = 29 + day = random.randint(1, max_day) + birthdate = f"{year}-{month:02d}-{day:02d}" + + return json.dumps({"name": name, "birthdate": birthdate}, separators=(",", ":")) + + +def generate_random_name() -> str: + letters1 = ''.join(random.choices(string.ascii_lowercase, k=5)) + numbers = ''.join(random.choices(string.digits, k=random.randint(1, 3))) + letters2 = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3))) + return letters1 + numbers + letters2 + + +def get_email() -> tuple[str, str]: + if not WORKER_URL or not EMAIL_DOMAIN or not ADMIN_AUTH: + raise RuntimeError("Missing env: WORKER_URL / EMAIL_DOMAIN / ADMIN_AUTH (set them in project root .env)") + name = generate_random_name() + res = requests.post( + f"{WORKER_URL}/admin/new_address", + json={"enablePrefix": True, "name": name, "domain": EMAIL_DOMAIN}, + headers={"x-admin-auth": ADMIN_AUTH, "Content-Type": "application/json"} + ) + logger.info("get_email status=%s body=%s", res.status_code, res.text[:200]) + if res.status_code != 200: + raise RuntimeError(f"邮件服务返回 {res.status_code}: {res.text[:200]}") + data = res.json() + return data["address"], data["jwt"] + + +def get_oai_code(email: str, jwt: str) -> str: + if not WORKER_URL or not ADMIN_AUTH: + raise RuntimeError("Missing env: WORKER_URL / ADMIN_AUTH (set them in project root .env)") + regex = r"(? str: + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + +def _sha256_b64url_no_pad(s: str) -> str: + return _b64url_no_pad(hashlib.sha256(s.encode("ascii")).digest()) + + +def _random_state(nbytes: int = 16) -> str: + return secrets.token_urlsafe(nbytes) + + +def _pkce_verifier() -> str: + return secrets.token_urlsafe(64) + + +def _parse_callback_url(callback_url: str) -> Dict[str, str]: + candidate = callback_url.strip() + if not candidate: + return {"code": "", "state": "", "error": "", "error_description": ""} + + if "://" not in candidate: + if candidate.startswith("?"): + candidate = f"http://localhost{candidate}" + elif any(ch in candidate for ch in "/?#") or ":" in candidate: + candidate = f"http://{candidate}" + elif "=" in candidate: + candidate = f"http://localhost/?{candidate}" + + parsed = urllib.parse.urlparse(candidate) + query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True) + fragment = urllib.parse.parse_qs(parsed.fragment, keep_blank_values=True) + + for key, values in fragment.items(): + if key not in query or not query[key] or not (query[key][0] or "").strip(): + query[key] = values + + def get1(k: str) -> str: + v = query.get(k, [""]) + return (v[0] or "").strip() + + code = get1("code") + state = get1("state") + error = get1("error") + error_description = get1("error_description") + + if code and not state and "#" in code: + code, state = code.split("#", 1) + if not error and error_description: + error, error_description = error_description, "" + + return {"code": code, "state": state, "error": error, "error_description": error_description} + + +def _jwt_claims_no_verify(id_token: str) -> Dict[str, Any]: + if not id_token or id_token.count(".") < 2: + return {} + payload_b64 = id_token.split(".")[1] + pad = "=" * ((4 - (len(payload_b64) % 4)) % 4) + try: + payload = base64.urlsafe_b64decode((payload_b64 + pad).encode("ascii")) + return json.loads(payload.decode("utf-8")) + except Exception: + return {} + + +def _to_int(v: Any) -> int: + try: + return int(v) + except (TypeError, ValueError): + return 0 + + +def _post_form(url: str, data: Dict[str, str], timeout: int = 30) -> Dict[str, Any]: + body = urllib.parse.urlencode(data).encode("utf-8") + req = urllib.request.Request( + url, data=body, method="POST", + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read() + if resp.status != 200: + raise RuntimeError(f"token exchange failed: {resp.status}: {raw.decode('utf-8', 'replace')}") + return json.loads(raw.decode("utf-8")) + except urllib.error.HTTPError as exc: + raw = exc.read() + raise RuntimeError(f"token exchange failed: {exc.code}: {raw.decode('utf-8', 'replace')}") from exc + + +@dataclass(frozen=True) +class OAuthStart: + auth_url: str + state: str + code_verifier: str + redirect_uri: str + + +def generate_oauth_url( + *, redirect_uri: str = DEFAULT_REDIRECT_URI, scope: str = DEFAULT_SCOPE +) -> OAuthStart: + state = _random_state() + code_verifier = _pkce_verifier() + code_challenge = _sha256_b64url_no_pad(code_verifier) + params = { + "client_id": CLIENT_ID, "response_type": "code", + "redirect_uri": redirect_uri, "scope": scope, + "state": state, "code_challenge": code_challenge, + "code_challenge_method": "S256", "prompt": "login", + "id_token_add_organizations": "true", "codex_cli_simplified_flow": "true", + } + auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}" + return OAuthStart(auth_url=auth_url, state=state, code_verifier=code_verifier, redirect_uri=redirect_uri) + + +def submit_callback_url( + *, callback_url: str, expected_state: str, code_verifier: str, + redirect_uri: str = DEFAULT_REDIRECT_URI +) -> str: + cb = _parse_callback_url(callback_url) + if cb["error"]: + raise RuntimeError(f"oauth error: {cb['error']}: {cb['error_description']}".strip()) + if not cb["code"]: + raise ValueError("callback url missing ?code=") + if not cb["state"]: + raise ValueError("callback url missing ?state=") + if cb["state"] != expected_state: + raise ValueError("state mismatch") + + token_resp = _post_form(TOKEN_URL, { + "grant_type": "authorization_code", "client_id": CLIENT_ID, + "code": cb["code"], "redirect_uri": redirect_uri, "code_verifier": code_verifier, + }) + + access_token = (token_resp.get("access_token") or "").strip() + refresh_token = (token_resp.get("refresh_token") or "").strip() + id_token = (token_resp.get("id_token") or "").strip() + expires_in = _to_int(token_resp.get("expires_in")) + + claims = _jwt_claims_no_verify(id_token) + email = str(claims.get("email") or "").strip() + auth_claims = claims.get("https://api.openai.com/auth") or {} + account_id = str(auth_claims.get("chatgpt_account_id") or "").strip() + + now = int(time.time()) + expired_rfc3339 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + max(expires_in, 0))) + now_rfc3339 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)) + + return json.dumps({ + "id_token": id_token, "access_token": access_token, + "refresh_token": refresh_token, "account_id": account_id, + "last_refresh": now_rfc3339, "email": email, + "type": "codex", "expired": expired_rfc3339, + }, ensure_ascii=False, separators=(",", ":")) + +# --------------------------------------------------------------------------- +# 反检测工具 +# --------------------------------------------------------------------------- + +_IMPERSONATE_POOL = [ + "chrome110", "chrome116", "chrome120", "chrome123", "chrome124", + "chrome131", "edge101", + "safari15_5", "safari17_0", +] + +# Chrome/Edge Client Hints — 缺少这些头是很大的指纹特征 +_CLIENT_HINTS = { + "chrome110": ('"Chromium";v="110", "Google Chrome";v="110", "Not_A Brand";v="24"', "Windows"), + "chrome116": ('"Chromium";v="116", "Google Chrome";v="116", "Not_A Brand";v="24"', "Windows"), + "chrome120": ('"Chromium";v="120", "Google Chrome";v="120", "Not_A Brand";v="24"', "macOS"), + "chrome123": ('"Chromium";v="123", "Google Chrome";v="123", "Not_A Brand";v="24"', "Windows"), + "chrome124": ('"Chromium";v="124", "Google Chrome";v="124", "Not_A Brand";v="24"', "macOS"), + "chrome131": ('"Chromium";v="131", "Google Chrome";v="131", "Not_A Brand";v="24"', "Windows"), + "edge101": ('"Chromium";v="101", "Microsoft Edge";v="101", "Not_A Brand";v="99"', "Windows"), + # Safari 不发送 Client Hints +} + +_ACCEPT_LANGUAGES = [ + "en-US,en;q=0.9", + "en-GB,en;q=0.9", + "en-US,en;q=0.9,ja;q=0.8", + "en,en-US;q=0.9", + "en-US,en;q=0.8,zh-CN;q=0.7", +] + + +def _human_delay(lo: float = 0.5, hi: float = 2.5): + """模拟人类操作间隔,带轻微随机抖动。""" + base = random.uniform(lo, hi) + # 5% 概率额外停顿,模拟真人偶尔走神 + if random.random() < 0.05: + base += random.uniform(0.5, 1.5) + time.sleep(base) + + +def _make_session(proxy: str) -> requests.Session: + """创建带随机浏览器指纹的 session,包含 Client Hints。""" + fp = random.choice(_IMPERSONATE_POOL) + s = requests.Session( + proxies={"http": proxy, "https": proxy}, + impersonate=fp, + ) + headers = {"accept-language": random.choice(_ACCEPT_LANGUAGES)} + # Chrome/Edge 需要 Client Hints + if fp in _CLIENT_HINTS: + ua_str, platform = _CLIENT_HINTS[fp] + headers["sec-ch-ua"] = ua_str + headers["sec-ch-ua-mobile"] = "?0" + headers["sec-ch-ua-platform"] = f'"{platform}"' + s.headers.update(headers) + return s + + +def check_proxy(proxy: str, timeout: int = 8) -> tuple[bool, str]: + """ + 检测代理是否可用(仅验证连通性)。 + 地区检查由注册流程中的 _check_loc 负责,因为代理池每次出口 IP 可能不同。 + 返回: (可用, 信息) + """ + try: + resp = requests.get( + "https://cloudflare.com/cdn-cgi/trace", + proxies={"http": proxy, "https": proxy}, + timeout=timeout, + impersonate=random.choice(_IMPERSONATE_POOL), + ) + if resp.status_code != 200: + return False, f"HTTP {resp.status_code}" + loc_m = re.search(r"^loc=(.+)$", resp.text, re.MULTILINE) + loc = loc_m.group(1).strip() if loc_m else "unknown" + return True, loc + except Exception as e: + return False, str(e)[:80] + + +# --------------------------------------------------------------------------- +# 主入口 +# --------------------------------------------------------------------------- + +def run(proxy: str) -> str: + s = _make_session(proxy) + + trace_resp = s.get("https://cloudflare.com/cdn-cgi/trace", timeout=10) + logger.info("trace status=%s len=%s", trace_resp.status_code, len(trace_resp.text)) + trace = trace_resp.text + ip_re = re.search(r"^ip=(.+)$", trace, re.MULTILINE) + loc_re = re.search(r"^loc=(.+)$", trace, re.MULTILINE) + loc = loc_re.group(1) if loc_re else None + exit_ip = ip_re.group(1) if ip_re else None + logger.info("proxy=%s exit_ip=%s loc=%s", proxy, exit_ip, loc) + if loc in ("CN", "HK"): + raise RuntimeError(f"检查代理哦w (loc={loc}, ip={exit_ip})") + + email, jwt = get_email() + logger.info("email=%s", email) + oauth = generate_oauth_url() + + _human_delay(0.3, 1.0) + s.get(oauth.auth_url) + did = s.cookies.get("oai-did") + + _human_delay(0.5, 1.5) + signup_body = f'{{"username":{{"value":"{email}","kind":"email"}},"screen_hint":"signup"}}' + sen_req_body = f'{{"p":"","id":"{did}","flow":"authorize_continue"}}' + sen_resp = s.post( + "https://sentinel.openai.com/backend-api/sentinel/req", + headers={ + "origin": "https://sentinel.openai.com", + "referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html?sv=20260219f9f6", + "content-type": "text/plain;charset=UTF-8", + }, + data=sen_req_body, + ) + logger.info("sentinel status=%s body=%s", sen_resp.status_code, sen_resp.text[:200]) + sentinel_token = sen_resp.json()["token"] + sentinel = f'{{"p": "", "t": "", "c": "{sentinel_token}", "id": "{did}", "flow": "authorize_continue"}}' + + _human_delay(0.8, 2.0) + signup_resp = s.post( + "https://auth.openai.com/api/accounts/authorize/continue", + headers={"referer": "https://auth.openai.com/create-account", "accept": "application/json", + "content-type": "application/json", "openai-sentinel-token": sentinel}, + data=signup_body, + ) + logger.info("signup status=%s body=%s", signup_resp.status_code, signup_resp.text[:200]) + + _human_delay(0.5, 1.5) + otp_resp = s.post( + "https://auth.openai.com/api/accounts/passwordless/send-otp", + headers={"referer": "https://auth.openai.com/create-account/password", + "accept": "application/json", "content-type": "application/json"}, + ) + logger.info("otp status=%s", otp_resp.status_code) + + code = get_oai_code(email, jwt) + logger.info("otp code=%s", code) + + _human_delay(1.0, 3.0) + code_resp = s.post( + "https://auth.openai.com/api/accounts/email-otp/validate", + headers={"referer": "https://auth.openai.com/email-verification", "accept": "application/json", + "content-type": "application/json"}, + data=f'{{"code":"{code}"}}', + ) + logger.info("validate status=%s", code_resp.status_code) + + _human_delay(1.0, 3.0) + create_resp = s.post( + "https://auth.openai.com/api/accounts/create_account", + headers={"referer": "https://auth.openai.com/about-you", "accept": "application/json", + "content-type": "application/json"}, + data=_random_profile(), + ) + logger.info("create status=%s", create_resp.status_code) + if create_resp.status_code != 200: + logger.warning("create failed: %s", create_resp.text[:200]) + return None + + auth = s.cookies.get("oai-client-auth-session") + auth = json.loads(base64.b64decode(auth.split(".")[0])) + workspace_id = auth["workspaces"][0]["id"] + + _human_delay(0.5, 1.5) + select_resp = s.post( + "https://auth.openai.com/api/accounts/workspace/select", + headers={"referer": "https://auth.openai.com/sign-in-with-chatgpt/codex/consent", + "content-type": "application/json"}, + data=f'{{"workspace_id":"{workspace_id}"}}', + ) + logger.info("select status=%s", select_resp.status_code) + + continue_url = select_resp.json()["continue_url"] + r = s.get(continue_url, allow_redirects=False) + r = s.get(r.headers.get("Location"), allow_redirects=False) + r = s.get(r.headers.get("Location"), allow_redirects=False) + cbk = r.headers.get("Location") + + result_str = submit_callback_url( + callback_url=cbk, + code_verifier=oauth.code_verifier, + redirect_uri=oauth.redirect_uri, + expected_state=oauth.state, + ) + # 注入出口 IP 到返回数据 + result = json.loads(result_str) + result["exit_ip"] = exit_ip + return json.dumps(result, ensure_ascii=False, separators=(",", ":")) + + +# --------------------------------------------------------------------------- +# 存活检测 +# --------------------------------------------------------------------------- + +CHATGPT_API_BASE = "https://chatgpt.com/backend-api" + + +CODEX_USAGE_URL = f"{CHATGPT_API_BASE}/wham/usage" + +_CODEX_VERSIONS = ["0.74.0", "0.75.0", "0.76.0", "0.77.0", "0.78.0"] +_CODEX_PLATFORMS = [ + "(Debian 13.0.0; x86_64) WindowsTerminal", + "(Ubuntu 22.04; x86_64) WindowsTerminal", + "(macOS 14.5; arm64) Terminal", + "(Windows 11; x86_64) WindowsTerminal", + "(Debian 12.0.0; x86_64) tmux", +] + + +def _codex_ua() -> str: + ver = random.choice(_CODEX_VERSIONS) + plat = random.choice(_CODEX_PLATFORMS) + return f"codex_cli_rs/{ver} {plat}" + + +def check_alive(refresh_token: str, proxy: str) -> tuple: + """ + 三步验证账号存活: + 1. 用 refresh_token 换新的 access_token + 2. 从 id_token 中解析 account_id + 3. 用 access_token + account_id 调用 /backend-api/wham/usage 检查配额 + + 返回: (status, access_token, refresh_token, id_token, plan_type, expires_at, usage_json) + status: 'alive' | 'dead' | 'error' + """ + s = _make_session(proxy) + + # Step 1: 刷新 token + try: + resp = s.post( + TOKEN_URL, + data={ + "grant_type": "refresh_token", + "client_id": CLIENT_ID, + "refresh_token": refresh_token, + }, + headers={"Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json"}, + timeout=20, + ) + if resp.status_code in (400, 401): + return "dead", None, None, None, None, None, None + if resp.status_code != 200: + return "error", None, None, None, None, None, None + + token_data = resp.json() + new_access = token_data.get("access_token") + new_refresh = token_data.get("refresh_token") + new_id = token_data.get("id_token") + if not new_access: + return "dead", None, None, None, None, None, None + except Exception: + return "error", None, None, None, None, None, None + + # Step 2: 从 id_token 解析 account_id + account_id = "" + if new_id: + claims = _jwt_claims_no_verify(new_id) + auth_claims = claims.get("https://api.openai.com/auth") or {} + account_id = str(auth_claims.get("chatgpt_account_id") or "").strip() + + _human_delay(0.3, 1.0) + + # Step 3: 用 wham/usage 接口检查配额状态 + try: + usage_headers = { + "Content-Type": "application/json", + "User-Agent": _codex_ua(), + "Authorization": f"Bearer {new_access}", + } + if account_id: + usage_headers["Chatgpt-Account-Id"] = account_id + + usage_resp = s.get( + CODEX_USAGE_URL, + headers=usage_headers, + timeout=20, + ) + + if usage_resp.status_code in (401, 403): + return "dead", None, None, None, None, None, None + if usage_resp.status_code != 200: + return "error", new_access, new_refresh, new_id, None, None, None + + data = usage_resp.json() + plan_type = data.get("plan_type") + usage_json = json.dumps(data, ensure_ascii=False, separators=(",", ":")) + + # 从 token expires_in 计算过期时间 + expires_in = _to_int(token_data.get("expires_in")) + now = int(time.time()) + expires_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + max(expires_in, 0))) + + return "alive", new_access, new_refresh, new_id, plan_type, expires_at, usage_json + except Exception: + # wham/usage 失败但 token 刷新成功,保守标记为 error + return "error", new_access, new_refresh, new_id, None, None, None diff --git a/Code-Patch/frontend/index.html b/Code-Patch/frontend/index.html new file mode 100644 index 0000000..7314f05 --- /dev/null +++ b/Code-Patch/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Account Registrar + + +
+ + + diff --git a/Code-Patch/frontend/package-lock.json b/Code-Patch/frontend/package-lock.json new file mode 100644 index 0000000..a839f90 --- /dev/null +++ b/Code-Patch/frontend/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "code-patch", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-patch", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.0", + "axios": "^1.7.0", + "element-plus": "^2.7.0", + "vue": "^3.4.0", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "dependencies": { + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "dependencies": { + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "vue": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.5", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.5.tgz", + "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.29", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/Code-Patch/frontend/package.json b/Code-Patch/frontend/package.json new file mode 100644 index 0000000..8e54a3b --- /dev/null +++ b/Code-Patch/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "code-patch", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.3.0", + "element-plus": "^2.7.0", + "@element-plus/icons-vue": "^2.3.0", + "axios": "^1.7.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.2.0" + } +} diff --git a/Code-Patch/frontend/src/App.vue b/Code-Patch/frontend/src/App.vue new file mode 100644 index 0000000..78f7446 --- /dev/null +++ b/Code-Patch/frontend/src/App.vue @@ -0,0 +1,95 @@ + + + + + + + diff --git a/Code-Patch/frontend/src/api/index.js b/Code-Patch/frontend/src/api/index.js new file mode 100644 index 0000000..220124b --- /dev/null +++ b/Code-Patch/frontend/src/api/index.js @@ -0,0 +1,104 @@ +import axios from 'axios' + +const http = axios.create({ baseURL: '/api', timeout: 30000 }) + +// Normalise error messages so callers can use err.message uniformly +http.interceptors.response.use( + r => r, + err => { + const msg = err.response?.data?.detail || err.message || '请求失败' + return Promise.reject(new Error(msg)) + } +) + +export const getSystemProxy = () => http.get('/system-proxy') +export const startSession = (data) => http.post('/sessions', data) +export const getSessions = () => http.get('/sessions') +export const getActiveSession = () => http.get('/sessions/active') +export const getAccounts = (params) => http.get('/accounts', { params }) +export const getAccount = (id) => http.get(`/accounts/${id}`) + +export function exportSessionUrl(sessionId) { + return `/api/sessions/${sessionId}/export` +} + +export function exportAccountsUrl(params = {}) { + const qs = new URLSearchParams() + if (params.search) qs.set('search', params.search) + if (params.status) qs.set('status', params.status) + if (params.session_id) qs.set('session_id', params.session_id) + if (params.alive) qs.set('alive', params.alive) + const q = qs.toString() + return `/api/accounts/export${q ? '?' + q : ''}` +} + +export const pauseSession = (id) => http.post(`/sessions/${id}/pause`) +export const resumeSession = (id) => http.post(`/sessions/${id}/resume`) +export const getSchedules = () => http.get('/schedules') +export const createSchedule = (data) => http.post('/schedules', data) +export const updateSchedule = (id, data) => http.put(`/schedules/${id}`, data) +export const toggleSchedule = (id) => http.put(`/schedules/${id}/toggle`) +export const deleteSchedule = (id) => http.delete(`/schedules/${id}`) +export const getScheduleRuns = (id) => http.get(`/schedules/${id}/runs`) +export const getAllRuns = (limit = 50) => http.get('/schedule-runs', { params: { limit } }) + +export const startCheckSession = (data) => http.post('/check-sessions', data) +export const importAccounts = (data) => http.post('/accounts/import', data) +export const deleteDeadAccounts = () => http.delete('/accounts/dead') +export const setAutoRefresh = (id, enabled) => http.put(`/accounts/${id}/auto-refresh`, null, { params: { enabled } }) + +/** + * Open a WebSocket for a registration session. + */ +export function openSessionWS(sessionId, { onSuccess, onFailed, onDone, onError } = {}) { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket(`${proto}//${location.host}/ws/sessions/${sessionId}`) + + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) + if (msg.type === 'success') onSuccess?.(msg) + else if (msg.type === 'failed') onFailed?.(msg) + else if (msg.type === 'done') onDone?.(msg) + // ignore 'ping' + } + ws.onerror = (e) => onError?.(e) + + return ws +} + +/** + * Open a WebSocket for a liveness check session. + */ +export function openCheckWS(checkId, { onResult, onDone, onError } = {}) { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket(`${proto}//${location.host}/ws/check/${checkId}`) + + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) + if (msg.type === 'result') onResult?.(msg) + else if (msg.type === 'done') onDone?.(msg) + // ignore 'ping' + } + ws.onerror = (e) => onError?.(e) + + return ws +} + +/** + * Open a WebSocket for an import session. + */ +export function openImportWS(importId, { onResult, onDone, onError } = {}) { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket(`${proto}//${location.host}/ws/sessions/${importId}`) + + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) + if (msg.type === 'success') onResult?.({ ...msg, alive: 'alive' }) + else if (msg.type === 'failed') onResult?.({ ...msg, alive: 'dead' }) + else if (msg.type === 'done') onDone?.(msg) + // ignore 'ping' + } + ws.onerror = (e) => onError?.(e) + + return ws +} diff --git a/Code-Patch/frontend/src/composables/useCheckState.js b/Code-Patch/frontend/src/composables/useCheckState.js new file mode 100644 index 0000000..5459d0c --- /dev/null +++ b/Code-Patch/frontend/src/composables/useCheckState.js @@ -0,0 +1,57 @@ +import { ref, reactive, computed } from 'vue' +import { openCheckWS } from '../api/index.js' + +// 全局状态 — 模块级别,切换页面不会丢失 +const checking = ref(false) +const checkProgress = reactive({ total: 0, done: 0, alive: 0, dead: 0, error: 0 }) +let checkWs = null +let onDoneCallback = null + +const checkPct = computed(() => { + if (!checkProgress.total) return 0 + return Math.round((checkProgress.done / checkProgress.total) * 100) +}) + +function startCheck(checkId, total, { onResult, onDone } = {}) { + // 关闭之前的连接 + checkWs?.close() + + checking.value = true + checkProgress.total = total + checkProgress.done = 0 + checkProgress.alive = 0 + checkProgress.dead = 0 + checkProgress.error = 0 + onDoneCallback = onDone || null + + checkWs = openCheckWS(checkId, { + onResult(msg) { + checkProgress.done++ + if (msg.alive === 'alive') checkProgress.alive++ + else if (msg.alive === 'dead') checkProgress.dead++ + else checkProgress.error++ + onResult?.(msg) + }, + onDone(msg) { + checking.value = false + checkWs?.close() + checkWs = null + onDoneCallback?.(msg) + }, + onError() { + checking.value = false + checkWs?.close() + checkWs = null + }, + }) +} + +function stopCheck() { + checkWs?.close() + checkWs = null + checking.value = false +} + +export function useCheckState() { + return { checking, checkProgress, checkPct, startCheck, stopCheck } +} diff --git a/Code-Patch/frontend/src/composables/useWebSocket.js b/Code-Patch/frontend/src/composables/useWebSocket.js new file mode 100644 index 0000000..7657abd --- /dev/null +++ b/Code-Patch/frontend/src/composables/useWebSocket.js @@ -0,0 +1,40 @@ +import { onUnmounted } from 'vue' + +/** + * Composable for managing a WebSocket connection. + * Automatically closes the socket when the component is unmounted. + * + * @param {string} path - Path template, e.g. '/ws/sessions/{id}' + * @param {object} handlers - { onMessage(msg), onError(e) } + * @returns {{ open(id: string|number): void, close(): void }} + */ +export function useWebSocket(path, handlers = {}) { + let ws = null + + function open(id) { + close() + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' + const url = `${proto}//${location.host}${path.replace('{id}', id)}` + ws = new WebSocket(url) + + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) + if (msg.type !== 'ping') { + handlers.onMessage?.(msg) + } + } + + ws.onerror = (e) => handlers.onError?.(e) + } + + function close() { + if (ws) { + ws.close() + ws = null + } + } + + onUnmounted(close) + + return { open, close } +} diff --git a/Code-Patch/frontend/src/main.js b/Code-Patch/frontend/src/main.js new file mode 100644 index 0000000..f041fef --- /dev/null +++ b/Code-Patch/frontend/src/main.js @@ -0,0 +1,18 @@ +import './styles/variables.css' +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router/index.js' + +const app = createApp(App) + +app.use(ElementPlus) +app.use(router) + +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.mount('#app') diff --git a/Code-Patch/frontend/src/router/index.js b/Code-Patch/frontend/src/router/index.js new file mode 100644 index 0000000..afa9140 --- /dev/null +++ b/Code-Patch/frontend/src/router/index.js @@ -0,0 +1,18 @@ +import { createRouter, createWebHistory } from 'vue-router' +import RegisterView from '../views/RegisterView.vue' +import SessionsView from '../views/SessionsView.vue' +import AccountsView from '../views/AccountsView.vue' +import SchedulesView from '../views/SchedulesView.vue' + +const routes = [ + { path: '/', redirect: '/register' }, + { path: '/register', component: RegisterView, name: 'Register' }, + { path: '/sessions', component: SessionsView, name: 'Sessions' }, + { path: '/accounts', component: AccountsView, name: 'Accounts' }, + { path: '/schedules', component: SchedulesView, name: 'Schedules' }, +] + +export default createRouter({ + history: createWebHistory(), + routes, +}) diff --git a/Code-Patch/frontend/src/styles/variables.css b/Code-Patch/frontend/src/styles/variables.css new file mode 100644 index 0000000..06baa2b --- /dev/null +++ b/Code-Patch/frontend/src/styles/variables.css @@ -0,0 +1,35 @@ +:root { + /* Sidebar */ + --color-sidebar-bg: #1a2332; + --color-sidebar-text: #8b9ab4; + --color-sidebar-active: #ffffff; + --color-sidebar-hover: #2d3f52; + + /* Page */ + --color-page-bg: #f0f2f5; + + /* Log / Terminal */ + --color-log-bg: #0d1117; + --color-log-success: #3fb950; + --color-log-failed: #f85149; + --color-log-meta: #8b949e; + + /* Typography */ + --font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 24px; + --space-2xl: 32px; + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + /* Shadows */ + --shadow-card: 0 1px 3px rgba(0, 0, 0, .08), 0 1px 2px rgba(0, 0, 0, .06); +} diff --git a/Code-Patch/frontend/src/utils/format.js b/Code-Patch/frontend/src/utils/format.js new file mode 100644 index 0000000..27cc003 --- /dev/null +++ b/Code-Patch/frontend/src/utils/format.js @@ -0,0 +1,28 @@ +/** + * Format an ISO timestamp to a human-readable local string. + * Returns '—' for null/undefined values. + */ +export function formatTime(iso) { + if (!iso) return '—' + return new Date(iso).toLocaleString('zh-CN', { hour12: false }) +} + +/** + * Map alive status value to an Element Plus tag type. + */ +export function aliveTagType(v) { + if (v === 'alive') return 'success' + if (v === 'dead') return 'danger' + if (v === 'error') return 'warning' + return 'info' +} + +/** + * Map alive status value to a display label. + */ +export function aliveLabel(v) { + if (v === 'alive') return '存活' + if (v === 'dead') return '已失效' + if (v === 'error') return '检测异常' + return '未检测' +} diff --git a/Code-Patch/frontend/src/views/AccountsView.vue b/Code-Patch/frontend/src/views/AccountsView.vue new file mode 100644 index 0000000..e729920 --- /dev/null +++ b/Code-Patch/frontend/src/views/AccountsView.vue @@ -0,0 +1,702 @@ + + + + + diff --git a/Code-Patch/frontend/src/views/RegisterView.vue b/Code-Patch/frontend/src/views/RegisterView.vue new file mode 100644 index 0000000..c59a86f --- /dev/null +++ b/Code-Patch/frontend/src/views/RegisterView.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/Code-Patch/frontend/src/views/SchedulesView.vue b/Code-Patch/frontend/src/views/SchedulesView.vue new file mode 100644 index 0000000..39a3c97 --- /dev/null +++ b/Code-Patch/frontend/src/views/SchedulesView.vue @@ -0,0 +1,483 @@ + + + + + diff --git a/Code-Patch/frontend/src/views/SessionsView.vue b/Code-Patch/frontend/src/views/SessionsView.vue new file mode 100644 index 0000000..b8f9aa3 --- /dev/null +++ b/Code-Patch/frontend/src/views/SessionsView.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/Code-Patch/frontend/vite.config.js b/Code-Patch/frontend/vite.config.js new file mode 100644 index 0000000..723d3e0 --- /dev/null +++ b/Code-Patch/frontend/vite.config.js @@ -0,0 +1,43 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const repoRoot = path.resolve(__dirname, '..') + +function toWsOrigin(httpOrigin) { + if (httpOrigin.startsWith('https://')) return httpOrigin.replace('https://', 'wss://') + if (httpOrigin.startsWith('http://')) return httpOrigin.replace('http://', 'ws://') + return httpOrigin +} + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, repoRoot, '') + + const frontendPort = Number(env.FRONTEND_PORT || 5173) + const backendHost = (env.BACKEND_HOST || env.APP_HOST || '127.0.0.1').trim() || '127.0.0.1' + const backendPort = Number(env.BACKEND_PORT || env.APP_PORT || 8000) + const backendOrigin = (env.BACKEND_ORIGIN || `http://${backendHost}:${backendPort}`).trim() + const backendWsOrigin = (env.BACKEND_WS_ORIGIN || toWsOrigin(backendOrigin)).trim() + + return { + envDir: repoRoot, + plugins: [vue()], + server: { + port: frontendPort, + proxy: { + '/api': { + target: backendOrigin, + changeOrigin: true, + }, + '/ws': { + target: backendWsOrigin, + ws: true, + changeOrigin: true, + }, + }, + }, + } +}) diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/README.md b/GPT_register+duckmail+CPA+autouploadsub2api/README.md new file mode 100644 index 0000000..ecaa1b9 --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/README.md @@ -0,0 +1,137 @@ +# ChatGPT 批量自动注册工具 (DuckMail + OAuth + Sub2Api 版) + +## 项目概述 + +这是一个功能完整的 ChatGPT 批量自动注册工具,支持使用 DuckMail 临时邮箱进行并发注册,自动获取 OTP 验证码,并可通过 OAuth 方式自动授权上传账号 Token 到 Sub2Api 平台。 + +## 功能特性 + +- **DuckMail 临时邮箱**:自动生成临时邮箱账号,接收验证码 +- **并发注册**:支持多线程并发批量注册账号 +- **代理验证**:自动验证代理 IP 是否可用,支持代理池管理 +- **OAuth 登录**:支持 OAuth 方式自动登录获取 Token +- **Sub2Api 上传**:注册成功后自动将 Token 上传到 Sub2Api 平台 +- **Web 管理界面**:提供可视化 Web 界面查看注册进度和日志 + +## 项目结构 + +``` +GPT_register+duckmail+CPA+autouploadsub2api/ +├── chatgpt_register.py # 主注册脚本(并发版) +├── server.py # FastAPI Web 服务 +├── config.json # 配置文件 +├── requirements.txt # Python 依赖 +├── stable_proxy.txt # 稳定代理列表 +├── web/ # Web 前端 +│ ├── index.html +│ ├── app.js +│ └── style.css +└── codex_tokens/ # 生成的 Token 存储目录 +``` + +## 环境要求 + +- Python 3.10+ +- curl_cffi >= 0.14.0 +- fastapi >= 0.100.0 +- uvicorn >= 0.20.0 + +## 快速开始 + +### 1. 安装依赖 + +```bash +cd GPT_register+duckmail+CPA+autouploadsub2api +pip install -r requirements.txt +``` + +### 2. 配置 config.json + +```json +{ + "total_accounts": 3, + "duckmail_api_base": "https://api.duckmail.sbs", + "duckmail_bearer": "your-duckmail-api-key", + "proxy": "", + "proxy_list_url": "https://github.com/proxifly/free-proxy-list/blob/main/proxies/countries/US/data.txt", + "proxy_validate_enabled": true, + "stable_proxy": "http://127.0.0.1:7890", + "prefer_stable_proxy": true, + "output_file": "registered_accounts.txt", + "enable_oauth": true, + "oauth_required": true, + "ak_file": "ak.txt", + "rk_file": "rk.txt", + "sub2api_base_url": "https://your-sub2api.com", + "sub2api_bearer": "your-sub2api-token", + "auto_upload_sub2api": false, + "sub2api_group_ids": [2] +} +``` + +### 3. 配置说明 + +| 配置项 | 说明 | +|--------|------| +| `total_accounts` | 需要注册的账号数量 | +| `duckmail_api_base` | DuckMail API 地址 | +| `duckmail_bearer` | DuckMail API 密钥 | +| `proxy_list_url` | 代理列表 URL | +| `proxy_validate_enabled` | 是否启用代理验证 | +| `stable_proxy` | 稳定代理地址 | +| `prefer_stable_proxy` | 优先使用稳定代理 | +| `auto_upload_sub2api` | 注册成功后自动上传到 Sub2Api | +| `sub2api_group_ids` | Sub2Api 分组 ID | + +### 4. 运行方式 + +#### 方式一:Web 界面启动(推荐) + +```bash +python server.py +``` + +然后访问 `http://localhost:18421` + +#### 方式二:直接运行注册脚本 + +```bash +python chatgpt_register.py +``` + +## 环境变量 + +所有配置项都支持通过环境变量覆盖: + +```bash +export DUCKMAIL_BEARER="your-api-key" +export PROXY="http://proxy:port" +export TOTAL_ACCOUNTS=10 +export AUTO_UPLOAD_SUB2API=true +``` + +## 输出文件 + +- `registered_accounts.txt` - 注册成功的账号信息 +- `stable_proxy.txt` - 验证通过的稳定代理 +- `codex_tokens/` - 生成的 Token JSON 文件 + +## 注意事项 + +1. 请确保 DuckMail API 密钥有效 +2. 代理需要稳定,建议使用住宅代理 +3. 注册过程请遵守 OpenAI 服务条款 +4. 批量注册时注意控制频率,避免触发限制 +5. Sub2Api 上传功能需要正确的 API 配置 + +## 端口说明 + +- **18421** - Web 管理服务端口 + +## 许可证 + +仅供学习和研究使用,请遵守相关服务条款。 + +--- + +**更新日期**:2026-03-19 diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/chatgpt_register.py b/GPT_register+duckmail+CPA+autouploadsub2api/chatgpt_register.py new file mode 100644 index 0000000..efe3363 --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/chatgpt_register.py @@ -0,0 +1,2515 @@ +""" +ChatGPT 批量自动注册工具 (并发版) - DuckMail 临时邮箱版 +依赖: pip install curl_cffi +功能: 使用 DuckMail 临时邮箱,并发自动注册 ChatGPT 账号,自动获取 OTP 验证码 +""" + +import os +import re +import uuid +import json +import random +import string +import time +import sys +import threading +import traceback +import secrets +import hashlib +import base64 +import urllib.request +import urllib.error +from concurrent.futures import ThreadPoolExecutor, as_completed +from urllib.parse import urlparse, parse_qs, urlencode + +from curl_cffi import requests as curl_requests + +# ================= 加载配置 ================= +def _load_config(): + """从 config.json 加载配置,环境变量优先级更高""" + config = { + "total_accounts": 3, + "duckmail_api_base": "https://api.duckmail.sbs", + "duckmail_bearer": "", + "proxy": "", + "proxy_list_url": "https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/countries/US/data.txt", + "proxy_validate_enabled": True, + "proxy_validate_timeout_seconds": 6, + "proxy_validate_workers": 40, + "proxy_validate_test_url": "https://auth.openai.com/", + "proxy_max_retries_per_request": 30, + "proxy_bad_ttl_seconds": 180, + "proxy_retry_attempts_per_account": 20, + "stable_proxy_file": "stable_proxy.txt", + "stable_proxy": "", + "prefer_stable_proxy": True, + "output_file": "registered_accounts.txt", + "enable_oauth": True, + "oauth_required": True, + "oauth_issuer": "https://auth.openai.com", + "oauth_client_id": "app_EMoamEEZ73f0CkXaXp7hrann", + "oauth_redirect_uri": "http://localhost:1455/auth/callback", + "ak_file": "ak.txt", + "rk_file": "rk.txt", + "token_json_dir": "codex_tokens", + "sub2api_base_url": "", + "sub2api_bearer": "", + "sub2api_email": "", + "sub2api_password": "", + "auto_upload_sub2api": False, + "sub2api_group_ids": [2], + } + + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + if os.path.exists(config_path): + try: + with open(config_path, "r", encoding="utf-8") as f: + file_config = json.load(f) + config.update(file_config) + except Exception as e: + print(f"⚠️ 加载 config.json 失败: {e}") + + # 环境变量优先级更高 + config["duckmail_api_base"] = os.environ.get("DUCKMAIL_API_BASE", config["duckmail_api_base"]) + config["duckmail_bearer"] = os.environ.get("DUCKMAIL_BEARER", config["duckmail_bearer"]) + config["proxy"] = os.environ.get("PROXY", config["proxy"]) + config["proxy_list_url"] = os.environ.get("PROXY_LIST_URL", config["proxy_list_url"]) + config["proxy_validate_enabled"] = os.environ.get("PROXY_VALIDATE_ENABLED", config["proxy_validate_enabled"]) + config["proxy_validate_timeout_seconds"] = float(os.environ.get( + "PROXY_VALIDATE_TIMEOUT_SECONDS", config["proxy_validate_timeout_seconds"] + )) + config["proxy_validate_workers"] = int(os.environ.get("PROXY_VALIDATE_WORKERS", config["proxy_validate_workers"])) + config["proxy_validate_test_url"] = os.environ.get("PROXY_VALIDATE_TEST_URL", config["proxy_validate_test_url"]) + config["total_accounts"] = int(os.environ.get("TOTAL_ACCOUNTS", config["total_accounts"])) + config["proxy_max_retries_per_request"] = int(os.environ.get( + "PROXY_MAX_RETRIES_PER_REQUEST", config["proxy_max_retries_per_request"] + )) + config["proxy_bad_ttl_seconds"] = int(os.environ.get("PROXY_BAD_TTL_SECONDS", config["proxy_bad_ttl_seconds"])) + config["proxy_retry_attempts_per_account"] = int(os.environ.get( + "PROXY_RETRY_ATTEMPTS_PER_ACCOUNT", config["proxy_retry_attempts_per_account"] + )) + config["stable_proxy_file"] = os.environ.get("STABLE_PROXY_FILE", config["stable_proxy_file"]) + config["stable_proxy"] = os.environ.get("STABLE_PROXY", config["stable_proxy"]) + config["prefer_stable_proxy"] = os.environ.get("PREFER_STABLE_PROXY", config["prefer_stable_proxy"]) + config["enable_oauth"] = os.environ.get("ENABLE_OAUTH", config["enable_oauth"]) + config["oauth_required"] = os.environ.get("OAUTH_REQUIRED", config["oauth_required"]) + config["oauth_issuer"] = os.environ.get("OAUTH_ISSUER", config["oauth_issuer"]) + config["oauth_client_id"] = os.environ.get("OAUTH_CLIENT_ID", config["oauth_client_id"]) + config["oauth_redirect_uri"] = os.environ.get("OAUTH_REDIRECT_URI", config["oauth_redirect_uri"]) + config["ak_file"] = os.environ.get("AK_FILE", config["ak_file"]) + config["rk_file"] = os.environ.get("RK_FILE", config["rk_file"]) + config["token_json_dir"] = os.environ.get("TOKEN_JSON_DIR", config["token_json_dir"]) + config["sub2api_base_url"] = os.environ.get("SUB2API_BASE_URL", config["sub2api_base_url"]) + config["sub2api_bearer"] = os.environ.get("SUB2API_BEARER", config["sub2api_bearer"]) + config["sub2api_email"] = os.environ.get("SUB2API_EMAIL", config["sub2api_email"]) + config["sub2api_password"] = os.environ.get("SUB2API_PASSWORD", config["sub2api_password"]) + config["auto_upload_sub2api"] = os.environ.get("AUTO_UPLOAD_SUB2API", config["auto_upload_sub2api"]) + _raw_group_ids = os.environ.get("SUB2API_GROUP_IDS") + if _raw_group_ids: + try: + config["sub2api_group_ids"] = [int(x.strip()) for x in _raw_group_ids.split(",") if x.strip().isdigit()] + except Exception: + pass + + return config + + +def _as_bool(value): + if isinstance(value, bool): + return value + if value is None: + return False + return str(value).strip().lower() in {"1", "true", "yes", "y", "on"} + + +_CONFIG = _load_config() +DUCKMAIL_API_BASE = _CONFIG["duckmail_api_base"] +DUCKMAIL_BEARER = _CONFIG["duckmail_bearer"] +DEFAULT_TOTAL_ACCOUNTS = _CONFIG["total_accounts"] +DEFAULT_PROXY = _CONFIG["proxy"] +PROXY_LIST_URL = _CONFIG["proxy_list_url"] +PROXY_VALIDATE_ENABLED = _as_bool(_CONFIG.get("proxy_validate_enabled", True)) +PROXY_VALIDATE_TIMEOUT_SECONDS = max(1.0, float(_CONFIG.get("proxy_validate_timeout_seconds", 6))) +PROXY_VALIDATE_WORKERS = max(1, int(_CONFIG.get("proxy_validate_workers", 40))) +PROXY_VALIDATE_TEST_URL = str(_CONFIG.get("proxy_validate_test_url", "https://auth.openai.com/")).strip() or "https://auth.openai.com/" +PROXY_MAX_RETRIES_PER_REQUEST = max(1, int(_CONFIG.get("proxy_max_retries_per_request", 30))) +PROXY_BAD_TTL_SECONDS = max(10, int(_CONFIG.get("proxy_bad_ttl_seconds", 180))) +PROXY_RETRY_ATTEMPTS_PER_ACCOUNT = max(1, int(_CONFIG.get("proxy_retry_attempts_per_account", 20))) +STABLE_PROXY_FILE = _CONFIG.get("stable_proxy_file", "stable_proxy.txt") +STABLE_PROXY_RAW = _CONFIG.get("stable_proxy", "") +PREFER_STABLE_PROXY = _as_bool(_CONFIG.get("prefer_stable_proxy", True)) +DEFAULT_OUTPUT_FILE = _CONFIG["output_file"] +ENABLE_OAUTH = _as_bool(_CONFIG.get("enable_oauth", True)) +OAUTH_REQUIRED = _as_bool(_CONFIG.get("oauth_required", True)) +OAUTH_ISSUER = _CONFIG["oauth_issuer"].rstrip("/") +OAUTH_CLIENT_ID = _CONFIG["oauth_client_id"] +OAUTH_REDIRECT_URI = _CONFIG["oauth_redirect_uri"] +AK_FILE = _CONFIG["ak_file"] +RK_FILE = _CONFIG["rk_file"] +TOKEN_JSON_DIR = _CONFIG["token_json_dir"] +SUB2API_BASE_URL = str(_CONFIG.get("sub2api_base_url", "") or "").strip().rstrip("/") +SUB2API_BEARER = str(_CONFIG.get("sub2api_bearer", "") or "").strip() +SUB2API_EMAIL = str(_CONFIG.get("sub2api_email", "") or "").strip() +SUB2API_PASSWORD = str(_CONFIG.get("sub2api_password", "") or "").strip() +AUTO_UPLOAD_SUB2API = _as_bool(_CONFIG.get("auto_upload_sub2api", False)) +_raw = _CONFIG.get("sub2api_group_ids", [2]) +SUB2API_GROUP_IDS = [int(x) for x in (_raw if isinstance(_raw, list) else [_raw]) if str(x).strip().lstrip("-").isdigit()] + +# Sub2Api bearer token 可能在运行时通过登录刷新,使用可变容器保存 +_sub2api_bearer_holder = [SUB2API_BEARER] +_sub2api_auth_lock = threading.Lock() + +if not DUCKMAIL_BEARER: + print("⚠️ 警告: 未设置 DUCKMAIL_BEARER,请在 config.json 中设置或设置环境变量") + print(" 文件: config.json -> duckmail_bearer") + print(" 环境变量: export DUCKMAIL_BEARER='your_api_key_here'") + +# 全局线程锁 +_print_lock = threading.Lock() +_file_lock = threading.Lock() +# 停止信号:外部调用 _stop_event.set() 可中断注册循环 +_stop_event = threading.Event() + + +def _normalize_proxy(proxy: str): + if not proxy: + return None + value = str(proxy).strip() + if not value: + return None + if "://" in value: + return value + return f"http://{value}" + + +STABLE_PROXY = _normalize_proxy(STABLE_PROXY_RAW) + + +def _normalize_proxy_list_url(url: str): + value = (url or "").strip() + if not value: + return "https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/countries/US/data.txt" + + m = re.match(r"^https?://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)$", value) + if m: + owner, repo, branch, path = m.groups() + return f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" + return value + + +class ProxyPool: + """线程安全代理池:使用 HTTP/SOCKS 代理并轮询""" + + def __init__(self, list_url: str, fallback_proxy: str = None, + max_retries_per_request: int = 30, bad_ttl_seconds: int = 180, + validate_enabled: bool = True, validate_timeout_seconds: float = 6, + validate_workers: int = 40, validate_test_url: str = "https://auth.openai.com/", + prefer_stable_proxy: bool = True): + self.list_url = _normalize_proxy_list_url(list_url) + self.fallback_proxy = _normalize_proxy(fallback_proxy) + self.max_retries_per_request = max(1, int(max_retries_per_request)) + self.bad_ttl_seconds = max(10, int(bad_ttl_seconds)) + self.validate_enabled = bool(validate_enabled) + self.validate_timeout_seconds = max(1.0, float(validate_timeout_seconds)) + self.validate_workers = max(1, int(validate_workers)) + self.validate_test_url = str(validate_test_url).strip() or "https://auth.openai.com/" + self.prefer_stable_proxy = bool(prefer_stable_proxy) + self._lock = threading.Lock() + self._loaded = False + self._proxies = [] + self._index = 0 + self._bad_until = {} + self._last_fetched_count = 0 + self._last_valid_count = 0 + self._stable_proxy = None + self._last_error = "" + + def set_fallback(self, proxy: str): + normalized = _normalize_proxy(proxy) + if normalized: + with self._lock: + self.fallback_proxy = normalized + + def set_stable_proxy(self, proxy: str): + normalized = _normalize_proxy(proxy) + if not normalized: + return + with self._lock: + self._stable_proxy = normalized + self._bad_until.pop(normalized, None) + + def set_prefer_stable_proxy(self, enabled: bool): + with self._lock: + self.prefer_stable_proxy = bool(enabled) + + def get_stable_proxy(self): + with self._lock: + return self._stable_proxy + + def _fetch_proxies(self): + res = curl_requests.get(self.list_url, timeout=20) + if res.status_code != 200: + raise Exception(f"HTTP {res.status_code}") + + proxies = [] + seen = set() + for raw_line in res.text.splitlines(): + line = raw_line.strip() + if not line: + continue + if not ( + line.startswith("http://") + or line.startswith("socks4://") + or line.startswith("socks5://") + ): + continue + if line in seen: + continue + seen.add(line) + proxies.append(line) + return proxies + + def _validate_single_proxy(self, proxy: str): + try: + res = curl_requests.get( + self.validate_test_url, + timeout=self.validate_timeout_seconds, + allow_redirects=False, + proxies={"http": proxy, "https": proxy}, + impersonate="chrome131", + ) + return 200 <= res.status_code < 500 + except Exception: + return False + + def _filter_valid_proxies(self, proxies): + if not self.validate_enabled or not proxies: + return list(proxies) + + workers = min(self.validate_workers, len(proxies)) + valid = [] + total = len(proxies) + done = 0 + started_at = time.time() + last_log_at = started_at + + with _print_lock: + print( + f"[ProxyCheck] 开始校验代理: 总数 {total}, 并发 {workers}, " + f"超时 {self.validate_timeout_seconds}s, 测试URL {self.validate_test_url}" + ) + + with ThreadPoolExecutor(max_workers=workers) as executor: + futures = {executor.submit(self._validate_single_proxy, proxy): proxy for proxy in proxies} + for future in as_completed(futures): + proxy = futures[future] + done += 1 + try: + if future.result(): + valid.append(proxy) + except Exception: + pass + + now = time.time() + if done == total or (now - last_log_at) >= 1.5: + with _print_lock: + print(f"[ProxyCheck] 进度 {done}/{total}, 可用 {len(valid)}") + last_log_at = now + + elapsed = time.time() - started_at + with _print_lock: + print(f"[ProxyCheck] 校验完成: 可用 {len(valid)}/{total}, 耗时 {elapsed:.1f}s") + return valid + + def refresh(self, force=False): + with self._lock: + if self._loaded and not force: + return + + proxies = [] + fetched_proxies = [] + last_error = "" + try: + fetched_proxies = self._fetch_proxies() + proxies = self._filter_valid_proxies(fetched_proxies) + if self.validate_enabled and fetched_proxies and not proxies: + last_error = "代理校验后无可用代理" + except Exception as e: + last_error = str(e) + + with self._lock: + self._last_fetched_count = len(fetched_proxies) + self._last_valid_count = len(proxies) if self.validate_enabled else len(fetched_proxies) + if proxies: + random.shuffle(proxies) + self._proxies = proxies + self._index = 0 + self._bad_until = {} + self._last_error = "" + elif not self._proxies: + self._proxies = [self.fallback_proxy] if self.fallback_proxy else [] + self._index = 0 + self._last_error = last_error + else: + self._last_error = last_error + self._loaded = True + + def next_proxy(self): + self.refresh() + with self._lock: + if not self._proxies: + return None + now = time.time() + stable = self._stable_proxy if self.prefer_stable_proxy else None + if stable: + stable_bad_until = self._bad_until.get(stable, 0) + if stable_bad_until and stable_bad_until <= now: + self._bad_until.pop(stable, None) + stable_bad_until = 0 + if stable_bad_until > now: + self._stable_proxy = None + else: + return stable + + total = len(self._proxies) + for _ in range(total): + proxy = self._proxies[self._index] + self._index = (self._index + 1) % total + + bad_until = self._bad_until.get(proxy, 0) + if bad_until and bad_until <= now: + self._bad_until.pop(proxy, None) + bad_until = 0 + + if bad_until > now: + continue + return proxy + + fallback = self.fallback_proxy + if fallback: + bad_until = self._bad_until.get(fallback, 0) + if bad_until and bad_until <= now: + self._bad_until.pop(fallback, None) + bad_until = 0 + if bad_until <= now: + return fallback + # 所有代理都在冷却时,仍尝试一个代理,避免长时间完全不可用 + proxy = self._proxies[self._index] + self._index = (self._index + 1) % total + return proxy + + def report_bad(self, proxy: str, error=None): + normalized = _normalize_proxy(proxy) + if not normalized: + return + + until = time.time() + self.bad_ttl_seconds + with self._lock: + self._bad_until[normalized] = until + if self._stable_proxy == normalized: + self._stable_proxy = None + if error: + self._last_error = f"{normalized} -> {str(error)[:160]}" + + def report_success(self, proxy: str): + normalized = _normalize_proxy(proxy) + if not normalized: + return + with self._lock: + self._stable_proxy = normalized + self._bad_until.pop(normalized, None) + + def request_retry_limit(self): + self.refresh() + with self._lock: + pool_size = len(self._proxies) + if self.fallback_proxy and self.fallback_proxy not in self._proxies: + pool_size += 1 + max_retries = self.max_retries_per_request + return max(1, min(max_retries, max(1, pool_size))) + + def info(self): + with self._lock: + now = time.time() + bad_count = 0 + for until in self._bad_until.values(): + if until > now: + bad_count += 1 + return { + "list_url": self.list_url, + "count": len(self._proxies), + "fetched_count": self._last_fetched_count, + "validated_count": self._last_valid_count, + "validate_enabled": self.validate_enabled, + "validate_test_url": self.validate_test_url, + "validate_timeout_seconds": self.validate_timeout_seconds, + "validate_workers": self.validate_workers, + "bad_count": bad_count, + "fallback_proxy": self.fallback_proxy, + "stable_proxy": self._stable_proxy, + "prefer_stable_proxy": self.prefer_stable_proxy, + "max_retries_per_request": self.max_retries_per_request, + "bad_ttl_seconds": self.bad_ttl_seconds, + "last_error": self._last_error, + } + + +_proxy_pool = ProxyPool( + PROXY_LIST_URL, + fallback_proxy=DEFAULT_PROXY, + max_retries_per_request=PROXY_MAX_RETRIES_PER_REQUEST, + bad_ttl_seconds=PROXY_BAD_TTL_SECONDS, + validate_enabled=PROXY_VALIDATE_ENABLED, + validate_timeout_seconds=PROXY_VALIDATE_TIMEOUT_SECONDS, + validate_workers=PROXY_VALIDATE_WORKERS, + validate_test_url=PROXY_VALIDATE_TEST_URL, + prefer_stable_proxy=PREFER_STABLE_PROXY, +) +_stable_proxy_loaded = False + + +def _get_proxy_pool(fallback_proxy=None): + global _stable_proxy_loaded + _proxy_pool.set_prefer_stable_proxy(PREFER_STABLE_PROXY) + if not _stable_proxy_loaded: + stable = STABLE_PROXY or _load_stable_proxy_from_file() + if stable: + _proxy_pool.set_stable_proxy(stable) + _stable_proxy_loaded = True + if fallback_proxy: + _proxy_pool.set_fallback(fallback_proxy) + return _proxy_pool + + +def _is_proxy_related_error(exc: Exception): + class_name = exc.__class__.__name__.lower() + if "proxy" in class_name: + return True + + curl_code = getattr(exc, "code", None) + if curl_code in {5, 6, 7, 28, 35, 47, 52, 55, 56, 97}: + return True + + msg = str(exc).lower() + keywords = [ + "proxy", + "connect tunnel failed", + "could not connect", + "connection refused", + "timed out", + ] + for word in keywords: + if word in msg: + return True + return False + + +def _enable_proxy_rotation(session, fallback_proxy=None, fixed_proxy=None): + pool = _get_proxy_pool(fallback_proxy) + fixed_proxy = _normalize_proxy(fixed_proxy) + original_request = session.request + if getattr(original_request, "_proxy_rotation_wrapped", False): + return session + + def _request_with_rotating_proxy(method, url, **kwargs): + if kwargs.get("proxies"): + return original_request(method, url, **kwargs) + + if fixed_proxy: + req_kwargs = dict(kwargs) + req_kwargs["proxies"] = {"http": fixed_proxy, "https": fixed_proxy} + try: + return original_request(method, url, **req_kwargs) + except Exception as e: + if _is_proxy_related_error(e): + pool.report_bad(fixed_proxy, error=e) + raise + + retry_limit = pool.request_retry_limit() + last_error = None + + for _ in range(retry_limit): + proxy = pool.next_proxy() + req_kwargs = kwargs + if proxy: + req_kwargs = dict(kwargs) + req_kwargs["proxies"] = {"http": proxy, "https": proxy} + + try: + return original_request(method, url, **req_kwargs) + except Exception as e: + last_error = e + if not proxy: + raise + if not _is_proxy_related_error(e): + raise + pool.report_bad(proxy, error=e) + + if last_error: + raise last_error + return original_request(method, url, **kwargs) + + _request_with_rotating_proxy._proxy_rotation_wrapped = True + session.request = _request_with_rotating_proxy + return session + + +# Chrome 指纹配置: impersonate 与 sec-ch-ua 必须匹配真实浏览器 +_CHROME_PROFILES = [ + { + "major": 131, "impersonate": "chrome131", + "build": 6778, "patch_range": (69, 205), + "sec_ch_ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + }, + { + "major": 133, "impersonate": "chrome133a", + "build": 6943, "patch_range": (33, 153), + "sec_ch_ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', + }, + { + "major": 136, "impersonate": "chrome136", + "build": 7103, "patch_range": (48, 175), + "sec_ch_ua": '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"', + }, + { + "major": 142, "impersonate": "chrome142", + "build": 7540, "patch_range": (30, 150), + "sec_ch_ua": '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + }, +] + + +def _random_chrome_version(): + profile = random.choice(_CHROME_PROFILES) + major = profile["major"] + build = profile["build"] + patch = random.randint(*profile["patch_range"]) + full_ver = f"{major}.0.{build}.{patch}" + ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{full_ver} Safari/537.36" + return profile["impersonate"], major, full_ver, ua, profile["sec_ch_ua"] + + +def _random_delay(low=0.3, high=1.0): + time.sleep(random.uniform(low, high)) + + +def _make_trace_headers(): + trace_id = random.randint(10**17, 10**18 - 1) + parent_id = random.randint(10**17, 10**18 - 1) + tp = f"00-{uuid.uuid4().hex}-{format(parent_id, '016x')}-01" + return { + "traceparent": tp, "tracestate": "dd=s:1;o:rum", + "x-datadog-origin": "rum", "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": str(trace_id), "x-datadog-parent-id": str(parent_id), + } + + +def _generate_pkce(): + code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii") + digest = hashlib.sha256(code_verifier.encode("ascii")).digest() + code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + return code_verifier, code_challenge + + +class SentinelTokenGenerator: + """纯 Python 版本 sentinel token 生成器(PoW)""" + + MAX_ATTEMPTS = 500000 + ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + + def __init__(self, device_id=None, user_agent=None): + self.device_id = device_id or str(uuid.uuid4()) + self.user_agent = user_agent or ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/145.0.0.0 Safari/537.36" + ) + self.requirements_seed = str(random.random()) + self.sid = str(uuid.uuid4()) + + @staticmethod + def _fnv1a_32(text: str): + h = 2166136261 + for ch in text: + h ^= ord(ch) + h = (h * 16777619) & 0xFFFFFFFF + h ^= (h >> 16) + h = (h * 2246822507) & 0xFFFFFFFF + h ^= (h >> 13) + h = (h * 3266489909) & 0xFFFFFFFF + h ^= (h >> 16) + h &= 0xFFFFFFFF + return format(h, "08x") + + def _get_config(self): + now_str = time.strftime( + "%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", + time.gmtime(), + ) + perf_now = random.uniform(1000, 50000) + time_origin = time.time() * 1000 - perf_now + nav_prop = random.choice([ + "vendorSub", "productSub", "vendor", "maxTouchPoints", + "scheduling", "userActivation", "doNotTrack", "geolocation", + "connection", "plugins", "mimeTypes", "pdfViewerEnabled", + "webkitTemporaryStorage", "webkitPersistentStorage", + "hardwareConcurrency", "cookieEnabled", "credentials", + "mediaDevices", "permissions", "locks", "ink", + ]) + nav_val = f"{nav_prop}-undefined" + + return [ + "1920x1080", + now_str, + 4294705152, + random.random(), + self.user_agent, + "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", + None, + None, + "en-US", + "en-US,en", + random.random(), + nav_val, + random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]), + random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]), + perf_now, + self.sid, + "", + random.choice([4, 8, 12, 16]), + time_origin, + ] + + @staticmethod + def _base64_encode(data): + raw = json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return base64.b64encode(raw).decode("ascii") + + def _run_check(self, start_time, seed, difficulty, config, nonce): + config[3] = nonce + config[9] = round((time.time() - start_time) * 1000) + data = self._base64_encode(config) + hash_hex = self._fnv1a_32(seed + data) + diff_len = len(difficulty) + if hash_hex[:diff_len] <= difficulty: + return data + "~S" + return None + + def generate_token(self, seed=None, difficulty=None): + seed = seed if seed is not None else self.requirements_seed + difficulty = str(difficulty or "0") + start_time = time.time() + config = self._get_config() + + for i in range(self.MAX_ATTEMPTS): + result = self._run_check(start_time, seed, difficulty, config, i) + if result: + return "gAAAAAB" + result + return "gAAAAAB" + self.ERROR_PREFIX + self._base64_encode(str(None)) + + def generate_requirements_token(self): + config = self._get_config() + config[3] = 1 + config[9] = round(random.uniform(5, 50)) + data = self._base64_encode(config) + return "gAAAAAC" + data + + +def fetch_sentinel_challenge(session, device_id, flow="authorize_continue", user_agent=None, + sec_ch_ua=None, impersonate=None): + generator = SentinelTokenGenerator(device_id=device_id, user_agent=user_agent) + req_body = { + "p": generator.generate_requirements_token(), + "id": device_id, + "flow": flow, + } + headers = { + "Content-Type": "text/plain;charset=UTF-8", + "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html", + "Origin": "https://sentinel.openai.com", + "User-Agent": user_agent or "Mozilla/5.0", + "sec-ch-ua": sec_ch_ua or '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + } + + kwargs = { + "data": json.dumps(req_body), + "headers": headers, + "timeout": 20, + } + if impersonate: + kwargs["impersonate"] = impersonate + + try: + resp = session.post("https://sentinel.openai.com/backend-api/sentinel/req", **kwargs) + except Exception: + return None + + if resp.status_code != 200: + return None + + try: + return resp.json() + except Exception: + return None + + +def build_sentinel_token(session, device_id, flow="authorize_continue", user_agent=None, + sec_ch_ua=None, impersonate=None): + challenge = fetch_sentinel_challenge( + session, + device_id, + flow=flow, + user_agent=user_agent, + sec_ch_ua=sec_ch_ua, + impersonate=impersonate, + ) + if not challenge: + return None + + c_value = challenge.get("token", "") + if not c_value: + return None + + pow_data = challenge.get("proofofwork") or {} + generator = SentinelTokenGenerator(device_id=device_id, user_agent=user_agent) + + if pow_data.get("required") and pow_data.get("seed"): + p_value = generator.generate_token( + seed=pow_data.get("seed"), + difficulty=pow_data.get("difficulty", "0"), + ) + else: + p_value = generator.generate_requirements_token() + + return json.dumps({ + "p": p_value, + "t": "", + "c": c_value, + "id": device_id, + "flow": flow, + }, separators=(",", ":")) + + +def _extract_code_from_url(url: str): + if not url or "code=" not in url: + return None + try: + return parse_qs(urlparse(url).query).get("code", [None])[0] + except Exception: + return None + + +def _decode_jwt_payload(token: str): + try: + parts = token.split(".") + if len(parts) != 3: + return {} + payload = parts[1] + padding = 4 - len(payload) % 4 + if padding != 4: + payload += "=" * padding + decoded = base64.urlsafe_b64decode(payload) + return json.loads(decoded) + except Exception: + return {} + + +def _save_codex_tokens(email: str, tokens: dict): + access_token = tokens.get("access_token", "") + refresh_token = tokens.get("refresh_token", "") + id_token = tokens.get("id_token", "") + + if access_token: + with _file_lock: + with open(AK_FILE, "a", encoding="utf-8") as f: + f.write(f"{access_token}\n") + + if refresh_token: + with _file_lock: + with open(RK_FILE, "a", encoding="utf-8") as f: + f.write(f"{refresh_token}\n") + + if not access_token: + return + + payload = _decode_jwt_payload(access_token) + auth_info = payload.get("https://api.openai.com/auth", {}) + account_id = auth_info.get("chatgpt_account_id", "") + + exp_timestamp = payload.get("exp") + expired_str = "" + if isinstance(exp_timestamp, int) and exp_timestamp > 0: + from datetime import datetime, timezone, timedelta + + exp_dt = datetime.fromtimestamp(exp_timestamp, tz=timezone(timedelta(hours=8))) + expired_str = exp_dt.strftime("%Y-%m-%dT%H:%M:%S+08:00") + + from datetime import datetime, timezone, timedelta + + now = datetime.now(tz=timezone(timedelta(hours=8))) + token_data = { + "type": "codex", + "email": email, + "expired": expired_str, + "id_token": id_token, + "account_id": account_id, + "access_token": access_token, + "last_refresh": now.strftime("%Y-%m-%dT%H:%M:%S+08:00"), + "refresh_token": refresh_token, + } + + base_dir = os.path.dirname(os.path.abspath(__file__)) + token_dir = TOKEN_JSON_DIR if os.path.isabs(TOKEN_JSON_DIR) else os.path.join(base_dir, TOKEN_JSON_DIR) + os.makedirs(token_dir, exist_ok=True) + + token_path = os.path.join(token_dir, f"{email}.json") + with _file_lock: + with open(token_path, "w", encoding="utf-8") as f: + json.dump(token_data, f, ensure_ascii=False) + + +def _stable_proxy_path(): + base_dir = os.path.dirname(os.path.abspath(__file__)) + return STABLE_PROXY_FILE if os.path.isabs(STABLE_PROXY_FILE) else os.path.join(base_dir, STABLE_PROXY_FILE) + + +def _load_stable_proxy_from_file(): + path = _stable_proxy_path() + if not os.path.exists(path): + return None + try: + with open(path, "r", encoding="utf-8") as f: + line = f.readline().strip() + return _normalize_proxy(line) + except Exception: + return None + + +def _save_stable_proxy_to_config(proxy: str): + normalized = _normalize_proxy(proxy) + if not normalized: + return + + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + if not os.path.exists(config_path): + return + + try: + with _file_lock: + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + config["stable_proxy"] = normalized + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + f.write("\n") + except Exception: + return + + +def _save_stable_proxy_to_file(proxy: str): + normalized = _normalize_proxy(proxy) + if not normalized: + return + path = _stable_proxy_path() + with _file_lock: + with open(path, "w", encoding="utf-8") as f: + f.write(f"{normalized}\n") + + +def _sub2api_login() -> str: + """登录 Sub2Api,刷新 bearer token,返回新 token 或空字符串""" + if not SUB2API_BASE_URL or not SUB2API_EMAIL or not SUB2API_PASSWORD: + return "" + url = f"{SUB2API_BASE_URL}/api/v1/auth/login" + try: + resp = curl_requests.post( + url, + json={"email": SUB2API_EMAIL, "password": SUB2API_PASSWORD}, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + impersonate="chrome131", + timeout=15, + ) + data = resp.json() + token = ( + data.get("token") + or data.get("access_token") + or (data.get("data") or {}).get("token") + or (data.get("data") or {}).get("access_token") + or "" + ) + return str(token).strip() + except Exception as e: + with _print_lock: + print(f"[Sub2Api] 登录失败: {e}") + return "" + + +def _build_sub2api_account_payload(email: str, tokens: dict) -> dict: + """构建 POST /api/v1/admin/accounts 所需的完整 payload""" + access_token = tokens.get("access_token", "") + refresh_token = tokens.get("refresh_token", "") + id_token = tokens.get("id_token", "") + + at_payload = _decode_jwt_payload(access_token) if access_token else {} + at_auth = at_payload.get("https://api.openai.com/auth") or {} + chatgpt_account_id = at_auth.get("chatgpt_account_id", "") or tokens.get("account_id", "") + chatgpt_user_id = at_auth.get("chatgpt_user_id", "") + exp_timestamp = at_payload.get("exp", 0) + expires_at = exp_timestamp if isinstance(exp_timestamp, int) and exp_timestamp > 0 else int(time.time()) + 863999 + + it_payload = _decode_jwt_payload(id_token) if id_token else {} + it_auth = it_payload.get("https://api.openai.com/auth") or {} + organization_id = it_auth.get("organization_id", "") + if not organization_id: + orgs = it_auth.get("organizations") or [] + if orgs: + organization_id = (orgs[0] or {}).get("id", "") + + return { + "name": email, + "notes": "", + "platform": "openai", + "type": "oauth", + "credentials": { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in": 863999, + "expires_at": expires_at, + "chatgpt_account_id": chatgpt_account_id, + "chatgpt_user_id": chatgpt_user_id, + "organization_id": organization_id, + }, + "extra": {"email": email}, + "group_ids": SUB2API_GROUP_IDS, + "concurrency": 10, + "priority": 1, + "auto_pause_on_expired": True, + } + + +def _push_account_to_sub2api(email: str, tokens: dict) -> bool: + """ + 上传完整账号信息到 Sub2Api。 + POST {SUB2API_BASE_URL}/api/v1/admin/accounts + 若返回 401 且配置了邮箱/密码,则自动重新登录后重试一次。 + 返回 True 表示成功。 + """ + if not SUB2API_BASE_URL or not tokens.get("refresh_token"): + return False + + url = f"{SUB2API_BASE_URL}/api/v1/admin/accounts" + payload = _build_sub2api_account_payload(email, tokens) + + def _do_request(bearer: str): + try: + resp = curl_requests.post( + url, + json=payload, + headers={ + "Authorization": f"Bearer {bearer}", + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "Referer": f"{SUB2API_BASE_URL}/admin/accounts", + }, + impersonate="chrome131", + timeout=20, + ) + return resp.status_code, resp.text + except Exception as e: + return 0, str(e) + + bearer = _sub2api_bearer_holder[0] + status, body = _do_request(bearer) + + if status == 401 and SUB2API_EMAIL and SUB2API_PASSWORD: + with _sub2api_auth_lock: + if _sub2api_bearer_holder[0] == bearer: + new_token = _sub2api_login() + if new_token: + _sub2api_bearer_holder[0] = new_token + bearer = _sub2api_bearer_holder[0] + status, body = _do_request(bearer) + + ok = status in (200, 201) + with _print_lock: + if ok: + print(f"[Sub2Api] 上传成功 (HTTP {status})") + else: + print(f"[Sub2Api] 上传失败 (HTTP {status}): {body[:500]}") + return ok + + +def _generate_password(length=14): + lower = string.ascii_lowercase + upper = string.ascii_uppercase + digits = string.digits + special = "!@#$%&*" + pwd = [random.choice(lower), random.choice(upper), + random.choice(digits), random.choice(special)] + all_chars = lower + upper + digits + special + pwd += [random.choice(all_chars) for _ in range(length - 4)] + random.shuffle(pwd) + return "".join(pwd) + + +# ================= DuckMail 邮箱函数 ================= + +def _create_duckmail_session(): + """创建带重试的 DuckMail 请求会话""" + session = curl_requests.Session() + session.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Accept": "application/json", + "Content-Type": "application/json", + }) + return _enable_proxy_rotation(session) + + +def create_temp_email(): + """创建 DuckMail 临时邮箱,返回 (email, password, mail_token)""" + if not DUCKMAIL_BEARER: + raise Exception("DUCKMAIL_BEARER 未设置,无法创建临时邮箱") + + # 生成随机邮箱前缀 8-13 位 + chars = string.ascii_lowercase + string.digits + length = random.randint(8, 13) + email_local = "".join(random.choice(chars) for _ in range(length)) + email = f"{email_local}@duckmail.sbs" + password = _generate_password() + + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {DUCKMAIL_BEARER}"} + session = _create_duckmail_session() + + try: + # 1. 创建账号 + payload = {"address": email, "password": password} + res = session.post( + f"{api_base}/accounts", + json=payload, + headers=headers, + timeout=15, + impersonate="chrome131" + ) + + if res.status_code not in [200, 201]: + raise Exception(f"创建邮箱失败: {res.status_code} - {res.text[:200]}") + + # 2. 获取 Token(用于读取邮件) + time.sleep(0.5) + token_payload = {"address": email, "password": password} + token_res = session.post( + f"{api_base}/token", + json=token_payload, + timeout=15, + impersonate="chrome131" + ) + + if token_res.status_code == 200: + token_data = token_res.json() + mail_token = token_data.get("token") + if mail_token: + return email, password, mail_token + + raise Exception(f"获取邮件 Token 失败: {token_res.status_code}") + + except Exception as e: + raise Exception(f"DuckMail 创建邮箱失败: {e}") + + +def _fetch_emails_duckmail(mail_token: str): + """从 DuckMail 获取邮件列表""" + try: + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {mail_token}"} + session = _create_duckmail_session() + + res = session.get( + f"{api_base}/messages", + headers=headers, + timeout=15, + impersonate="chrome131" + ) + + if res.status_code == 200: + data = res.json() + # DuckMail API 返回格式可能是 hydra:member 或 member + messages = data.get("hydra:member") or data.get("member") or data.get("data") or [] + return messages + return [] + except Exception as e: + return [] + + +def _fetch_email_detail_duckmail(mail_token: str, msg_id: str): + """获取 DuckMail 单封邮件详情""" + try: + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {mail_token}"} + session = _create_duckmail_session() + + # 处理 msg_id 格式 + if isinstance(msg_id, str) and msg_id.startswith("/messages/"): + msg_id = msg_id.split("/")[-1] + + res = session.get( + f"{api_base}/messages/{msg_id}", + headers=headers, + timeout=15, + impersonate="chrome131" + ) + + if res.status_code == 200: + return res.json() + except Exception: + pass + return None + + +def _extract_verification_code(email_content: str): + """从邮件内容提取 6 位验证码""" + if not email_content: + return None + + patterns = [ + r"Verification code:?\s*(\d{6})", + r"code is\s*(\d{6})", + r"代码为[::]?\s*(\d{6})", + r"验证码[::]?\s*(\d{6})", + r">\s*(\d{6})\s*<", + r"(? 0: + # 获取最新邮件详情 + first_msg = messages[0] + msg_id = first_msg.get("id") or first_msg.get("@id") + + if msg_id: + detail = _fetch_email_detail_duckmail(mail_token, msg_id) + if detail: + # DuckMail 的邮件内容在 text 或 html 字段 + content = detail.get("text") or detail.get("html") or "" + code = _extract_verification_code(content) + if code: + return code + + time.sleep(3) + + return None + + +def _random_name(): + first = random.choice([ + "James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Ethan", "Sophia", + "Lucas", "Mia", "Mason", "Isabella", "Logan", "Charlotte", "Alexander", + "Amelia", "Benjamin", "Harper", "William", "Evelyn", "Henry", "Abigail", + "Sebastian", "Emily", "Jack", "Elizabeth", + ]) + last = random.choice([ + "Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", + "Clark", "Hall", "Young", "Anderson", "Thomas", "Jackson", "White", + "Harris", "Martin", "Thompson", "Garcia", "Robinson", "Lewis", + "Walker", "Allen", "King", "Wright", "Scott", "Green", + ]) + return f"{first} {last}" + + +def _random_birthdate(): + y = random.randint(1985, 2002) + m = random.randint(1, 12) + d = random.randint(1, 28) + return f"{y}-{m:02d}-{d:02d}" + + +class ChatGPTRegister: + BASE = "https://chatgpt.com" + AUTH = "https://auth.openai.com" + + def __init__(self, proxy: str = None, tag: str = "", fixed_proxy: str = None): + self.tag = tag # 线程标识,用于日志 + self.device_id = str(uuid.uuid4()) + self.auth_session_logging_id = str(uuid.uuid4()) + self.impersonate, self.chrome_major, self.chrome_full, self.ua, self.sec_ch_ua = _random_chrome_version() + + self.session = curl_requests.Session(impersonate=self.impersonate) + + self.proxy = _normalize_proxy(proxy) + self.fixed_proxy = _normalize_proxy(fixed_proxy) + _enable_proxy_rotation(self.session, fallback_proxy=self.proxy, fixed_proxy=self.fixed_proxy) + + self.session.headers.update({ + "User-Agent": self.ua, + "Accept-Language": random.choice([ + "en-US,en;q=0.9", "en-US,en;q=0.9,zh-CN;q=0.8", + "en,en-US;q=0.9", "en-US,en;q=0.8", + ]), + "sec-ch-ua": self.sec_ch_ua, "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', "sec-ch-ua-arch": '"x86"', + "sec-ch-ua-bitness": '"64"', + "sec-ch-ua-full-version": f'"{self.chrome_full}"', + "sec-ch-ua-platform-version": f'"{random.randint(10, 15)}.0.0"', + }) + + self.session.cookies.set("oai-did", self.device_id, domain="chatgpt.com") + self._callback_url = None + + def _log(self, step, method, url, status, body=None): + prefix = f"[{self.tag}] " if self.tag else "" + lines = [ + f"\n{'='*60}", + f"{prefix}[Step] {step}", + f"{prefix}[{method}] {url}", + f"{prefix}[Status] {status}", + ] + if body: + try: + lines.append(f"{prefix}[Response] {json.dumps(body, indent=2, ensure_ascii=False)[:1000]}") + except Exception: + lines.append(f"{prefix}[Response] {str(body)[:1000]}") + lines.append(f"{'='*60}") + with _print_lock: + print("\n".join(lines)) + + def _print(self, msg): + prefix = f"[{self.tag}] " if self.tag else "" + with _print_lock: + print(f"{prefix}{msg}") + + def _parse_json_or_raise(self, response, step_name: str): + if response.status_code >= 400: + raise Exception(f"{step_name} 被拦截 ({response.status_code})") + + try: + data = response.json() + except Exception: + body = (response.text or "")[:200].replace("\n", " ") + raise Exception( + f"{step_name} 返回非 JSON (status={response.status_code}, body={body})" + ) + return data + + # ==================== DuckMail 临时邮箱 ==================== + + def _create_duckmail_session(self): + """创建带重试的 DuckMail 请求会话""" + session = curl_requests.Session() + session.headers.update({ + "User-Agent": self.ua, + "Accept": "application/json", + "Content-Type": "application/json", + }) + return _enable_proxy_rotation(session, fallback_proxy=self.proxy, fixed_proxy=self.fixed_proxy) + + def create_temp_email(self): + """创建 DuckMail 临时邮箱,返回 (email, password, mail_token)""" + if not DUCKMAIL_BEARER: + raise Exception("DUCKMAIL_BEARER 未设置,无法创建临时邮箱") + + # 生成随机邮箱前缀 8-13 位 + chars = string.ascii_lowercase + string.digits + length = random.randint(8, 13) + email_local = "".join(random.choice(chars) for _ in range(length)) + email = f"{email_local}@duckmail.sbs" + password = _generate_password() + + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {DUCKMAIL_BEARER}"} + session = self._create_duckmail_session() + + try: + # 1. 创建账号 + payload = {"address": email, "password": password} + res = session.post( + f"{api_base}/accounts", + json=payload, + headers=headers, + timeout=15, + impersonate=self.impersonate + ) + + if res.status_code not in [200, 201]: + raise Exception(f"创建邮箱失败: {res.status_code} - {res.text[:200]}") + + # 2. 获取 Token(用于读取邮件) + time.sleep(0.5) + token_payload = {"address": email, "password": password} + token_res = session.post( + f"{api_base}/token", + json=token_payload, + timeout=15, + impersonate=self.impersonate + ) + + if token_res.status_code == 200: + token_data = token_res.json() + mail_token = token_data.get("token") + if mail_token: + return email, password, mail_token + + raise Exception(f"获取邮件 Token 失败: {token_res.status_code}") + + except Exception as e: + raise Exception(f"DuckMail 创建邮箱失败: {e}") + + def _fetch_emails_duckmail(self, mail_token: str): + """从 DuckMail 获取邮件列表""" + try: + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {mail_token}"} + session = self._create_duckmail_session() + + res = session.get( + f"{api_base}/messages", + headers=headers, + timeout=15, + impersonate=self.impersonate + ) + + if res.status_code == 200: + data = res.json() + messages = data.get("hydra:member") or data.get("member") or data.get("data") or [] + return messages + return [] + except Exception: + return [] + + def _fetch_email_detail_duckmail(self, mail_token: str, msg_id: str): + """获取 DuckMail 单封邮件详情""" + try: + api_base = DUCKMAIL_API_BASE.rstrip("/") + headers = {"Authorization": f"Bearer {mail_token}"} + session = self._create_duckmail_session() + + if isinstance(msg_id, str) and msg_id.startswith("/messages/"): + msg_id = msg_id.split("/")[-1] + + res = session.get( + f"{api_base}/messages/{msg_id}", + headers=headers, + timeout=15, + impersonate=self.impersonate + ) + + if res.status_code == 200: + return res.json() + except Exception: + pass + return None + + def _extract_verification_code(self, email_content: str): + """从邮件内容提取 6 位验证码""" + if not email_content: + return None + + patterns = [ + r"Verification code:?\s*(\d{6})", + r"code is\s*(\d{6})", + r"代码为[::]?\s*(\d{6})", + r"验证码[::]?\s*(\d{6})", + r">\s*(\d{6})\s*<", + r"(? 0: + first_msg = messages[0] + msg_id = first_msg.get("id") or first_msg.get("@id") + + if msg_id: + detail = self._fetch_email_detail_duckmail(mail_token, msg_id) + if detail: + content = detail.get("text") or detail.get("html") or "" + code = self._extract_verification_code(content) + if code: + self._print(f"[OTP] 验证码: {code}") + return code + + elapsed = int(time.time() - start_time) + self._print(f"[OTP] 等待中... ({elapsed}s/{timeout}s)") + time.sleep(3) + + self._print(f"[OTP] 超时 ({timeout}s)") + return None + + # ==================== 注册流程 ==================== + + def visit_homepage(self): + url = f"{self.BASE}/" + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + self._log("0. Visit homepage", "GET", url, r.status_code, + {"cookies_count": len(self.session.cookies)}) + if r.status_code != 200: + raise Exception(f"Visit homepage 被拦截 ({r.status_code})") + + def get_csrf(self) -> str: + url = f"{self.BASE}/api/auth/csrf" + r = self.session.get(url, headers={"Accept": "application/json", "Referer": f"{self.BASE}/"}) + data = self._parse_json_or_raise(r, "Get CSRF") + token = data.get("csrfToken", "") + self._log("1. Get CSRF", "GET", url, r.status_code, data) + if not token: + raise Exception("Failed to get CSRF token") + return token + + def signin(self, email: str, csrf: str) -> str: + url = f"{self.BASE}/api/auth/signin/openai" + params = { + "prompt": "login", "ext-oai-did": self.device_id, + "auth_session_logging_id": self.auth_session_logging_id, + "screen_hint": "login_or_signup", "login_hint": email, + } + form_data = {"callbackUrl": f"{self.BASE}/", "csrfToken": csrf, "json": "true"} + r = self.session.post(url, params=params, data=form_data, headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", "Referer": f"{self.BASE}/", "Origin": self.BASE, + }) + data = self._parse_json_or_raise(r, "Signin") + authorize_url = data.get("url", "") + self._log("2. Signin", "POST", url, r.status_code, data) + if not authorize_url: + raise Exception("Failed to get authorize URL") + return authorize_url + + def authorize(self, url: str) -> str: + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.BASE}/", "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + final_url = str(r.url) + self._log("3. Authorize", "GET", url, r.status_code, {"final_url": final_url}) + if r.status_code >= 400: + raise Exception(f"Authorize 被拦截 ({r.status_code})") + return final_url + + def register(self, email: str, password: str): + url = f"{self.AUTH}/api/accounts/user/register" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/create-account/password", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"username": email, "password": password}, headers=headers) + try: data = r.json() + except Exception: data = {"text": r.text[:500]} + self._log("4. Register", "POST", url, r.status_code, data) + return r.status_code, data + + def send_otp(self): + url = f"{self.AUTH}/api/accounts/email-otp/send" + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.AUTH}/create-account/password", "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + try: data = r.json() + except Exception: data = {"final_url": str(r.url), "status": r.status_code} + self._log("5. Send OTP", "GET", url, r.status_code, data) + return r.status_code, data + + def validate_otp(self, code: str): + url = f"{self.AUTH}/api/accounts/email-otp/validate" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/email-verification", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"code": code}, headers=headers) + try: data = r.json() + except Exception: data = {"text": r.text[:500]} + self._log("6. Validate OTP", "POST", url, r.status_code, data) + return r.status_code, data + + def create_account(self, name: str, birthdate: str): + url = f"{self.AUTH}/api/accounts/create_account" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/about-you", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"name": name, "birthdate": birthdate}, headers=headers) + try: data = r.json() + except Exception: data = {"text": r.text[:500]} + self._log("7. Create Account", "POST", url, r.status_code, data) + if isinstance(data, dict): + cb = data.get("continue_url") or data.get("url") or data.get("redirect_url") + if cb: + self._callback_url = cb + return r.status_code, data + + def callback(self, url: str = None): + if not url: + url = self._callback_url + if not url: + self._print("[!] No callback URL, skipping.") + return None, None + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + self._log("8. Callback", "GET", url, r.status_code, {"final_url": str(r.url)}) + return r.status_code, {"final_url": str(r.url)} + + # ==================== 自动注册主流程 ==================== + + def run_register(self, email, password, name, birthdate, mail_token): + """使用 DuckMail 的注册流程""" + self.visit_homepage() + _random_delay(0.3, 0.8) + csrf = self.get_csrf() + _random_delay(0.2, 0.5) + auth_url = self.signin(email, csrf) + _random_delay(0.3, 0.8) + + final_url = self.authorize(auth_url) + final_path = urlparse(final_url).path + _random_delay(0.3, 0.8) + + self._print(f"Authorize → {final_path}") + + need_otp = False + + if "create-account/password" in final_path: + self._print("全新注册流程") + _random_delay(0.5, 1.0) + status, data = self.register(email, password) + if status != 200: + raise Exception(f"Register 失败 ({status}): {data}") + # register 之后可能还需要 send_otp(全新注册流程中 OTP 不一定在 authorize 时发送) + _random_delay(0.3, 0.8) + self.send_otp() + need_otp = True + elif "email-verification" in final_path or "email-otp" in final_path: + self._print("跳到 OTP 验证阶段 (authorize 已触发 OTP,不再重复发送)") + # 不调用 send_otp(),因为 authorize 重定向到 email-verification 时服务器已发送 OTP + need_otp = True + elif "about-you" in final_path: + self._print("跳到填写信息阶段") + _random_delay(0.5, 1.0) + self.create_account(name, birthdate) + _random_delay(0.3, 0.5) + self.callback() + return True + elif "callback" in final_path or "chatgpt.com" in final_url: + self._print("账号已完成注册") + return True + else: + self._print(f"未知跳转: {final_url}") + self.register(email, password) + self.send_otp() + need_otp = True + + if need_otp: + # 使用 DuckMail 等待验证码 + otp_code = self.wait_for_verification_email(mail_token) + if not otp_code: + raise Exception("未能获取验证码") + + _random_delay(0.3, 0.8) + status, data = self.validate_otp(otp_code) + if status != 200: + self._print("验证码失败,重试...") + self.send_otp() + _random_delay(1.0, 2.0) + otp_code = self.wait_for_verification_email(mail_token, timeout=60) + if not otp_code: + raise Exception("重试后仍未获取验证码") + _random_delay(0.3, 0.8) + status, data = self.validate_otp(otp_code) + if status != 200: + raise Exception(f"验证码失败 ({status}): {data}") + + _random_delay(0.5, 1.5) + status, data = self.create_account(name, birthdate) + if status != 200: + raise Exception(f"Create account 失败 ({status}): {data}") + _random_delay(0.2, 0.5) + self.callback() + return True + + def _decode_oauth_session_cookie(self): + jar = getattr(self.session.cookies, "jar", None) + if jar is not None: + cookie_items = list(jar) + else: + cookie_items = [] + + for c in cookie_items: + name = getattr(c, "name", "") or "" + if "oai-client-auth-session" not in name: + continue + + raw_val = (getattr(c, "value", "") or "").strip() + if not raw_val: + continue + + candidates = [raw_val] + try: + from urllib.parse import unquote + + decoded = unquote(raw_val) + if decoded != raw_val: + candidates.append(decoded) + except Exception: + pass + + for val in candidates: + try: + if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")): + val = val[1:-1] + + part = val.split(".")[0] if "." in val else val + pad = 4 - len(part) % 4 + if pad != 4: + part += "=" * pad + raw = base64.urlsafe_b64decode(part) + data = json.loads(raw.decode("utf-8")) + if isinstance(data, dict): + return data + except Exception: + continue + return None + + def _oauth_allow_redirect_extract_code(self, url: str, referer: str = None): + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + "User-Agent": self.ua, + } + if referer: + headers["Referer"] = referer + + try: + resp = self.session.get( + url, + headers=headers, + allow_redirects=True, + timeout=30, + impersonate=self.impersonate, + ) + final_url = str(resp.url) + code = _extract_code_from_url(final_url) + if code: + self._print("[OAuth] allow_redirect 命中最终 URL code") + return code + + for r in getattr(resp, "history", []) or []: + loc = r.headers.get("Location", "") + code = _extract_code_from_url(loc) + if code: + self._print("[OAuth] allow_redirect 命中 history Location code") + return code + code = _extract_code_from_url(str(r.url)) + if code: + self._print("[OAuth] allow_redirect 命中 history URL code") + return code + except Exception as e: + maybe_localhost = re.search(r'(https?://localhost[^\s\'\"]+)', str(e)) + if maybe_localhost: + code = _extract_code_from_url(maybe_localhost.group(1)) + if code: + self._print("[OAuth] allow_redirect 从 localhost 异常提取 code") + return code + self._print(f"[OAuth] allow_redirect 异常: {e}") + + return None + + def _oauth_follow_for_code(self, start_url: str, referer: str = None, max_hops: int = 16): + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + "User-Agent": self.ua, + } + if referer: + headers["Referer"] = referer + + current_url = start_url + last_url = start_url + + for hop in range(max_hops): + try: + resp = self.session.get( + current_url, + headers=headers, + allow_redirects=False, + timeout=30, + impersonate=self.impersonate, + ) + except Exception as e: + maybe_localhost = re.search(r'(https?://localhost[^\s\'\"]+)', str(e)) + if maybe_localhost: + code = _extract_code_from_url(maybe_localhost.group(1)) + if code: + self._print(f"[OAuth] follow[{hop + 1}] 命中 localhost 回调") + return code, maybe_localhost.group(1) + self._print(f"[OAuth] follow[{hop + 1}] 请求异常: {e}") + return None, last_url + + last_url = str(resp.url) + self._print(f"[OAuth] follow[{hop + 1}] {resp.status_code} {last_url[:140]}") + code = _extract_code_from_url(last_url) + if code: + return code, last_url + + if resp.status_code in (301, 302, 303, 307, 308): + loc = resp.headers.get("Location", "") + if not loc: + return None, last_url + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code, loc + current_url = loc + headers["Referer"] = last_url + continue + + return None, last_url + + return None, last_url + + def _oauth_submit_workspace_and_org(self, consent_url: str): + session_data = self._decode_oauth_session_cookie() + if not session_data: + jar = getattr(self.session.cookies, "jar", None) + if jar is not None: + cookie_names = [getattr(c, "name", "") for c in list(jar)] + else: + cookie_names = list(self.session.cookies.keys()) + self._print(f"[OAuth] 无法解码 oai-client-auth-session, cookies={cookie_names[:12]}") + return None + + workspaces = session_data.get("workspaces", []) + if not workspaces: + self._print("[OAuth] session 中没有 workspace 信息") + return None + + workspace_id = (workspaces[0] or {}).get("id") + if not workspace_id: + self._print("[OAuth] workspace_id 为空") + return None + + h = { + "Accept": "application/json", + "Content-Type": "application/json", + "Origin": OAUTH_ISSUER, + "Referer": consent_url, + "User-Agent": self.ua, + "oai-device-id": self.device_id, + } + h.update(_make_trace_headers()) + + resp = self.session.post( + f"{OAUTH_ISSUER}/api/accounts/workspace/select", + json={"workspace_id": workspace_id}, + headers=h, + allow_redirects=False, + timeout=30, + impersonate=self.impersonate, + ) + self._print(f"[OAuth] workspace/select -> {resp.status_code}") + + if resp.status_code in (301, 302, 303, 307, 308): + loc = resp.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code + code, _ = self._oauth_follow_for_code(loc, referer=consent_url) + if not code: + code = self._oauth_allow_redirect_extract_code(loc, referer=consent_url) + return code + + if resp.status_code != 200: + self._print(f"[OAuth] workspace/select 失败: {resp.status_code}") + return None + + try: + ws_data = resp.json() + except Exception: + self._print("[OAuth] workspace/select 响应不是 JSON") + return None + + ws_next = ws_data.get("continue_url", "") + orgs = ws_data.get("data", {}).get("orgs", []) + ws_page = (ws_data.get("page") or {}).get("type", "") + self._print(f"[OAuth] workspace/select page={ws_page or '-'} next={(ws_next or '-')[:140]}") + + org_id = None + project_id = None + if orgs: + org_id = (orgs[0] or {}).get("id") + projects = (orgs[0] or {}).get("projects", []) + if projects: + project_id = (projects[0] or {}).get("id") + + if org_id: + org_body = {"org_id": org_id} + if project_id: + org_body["project_id"] = project_id + + h_org = dict(h) + if ws_next: + h_org["Referer"] = ws_next if ws_next.startswith("http") else f"{OAUTH_ISSUER}{ws_next}" + + resp_org = self.session.post( + f"{OAUTH_ISSUER}/api/accounts/organization/select", + json=org_body, + headers=h_org, + allow_redirects=False, + timeout=30, + impersonate=self.impersonate, + ) + self._print(f"[OAuth] organization/select -> {resp_org.status_code}") + if resp_org.status_code in (301, 302, 303, 307, 308): + loc = resp_org.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code + code, _ = self._oauth_follow_for_code(loc, referer=h_org.get("Referer")) + if not code: + code = self._oauth_allow_redirect_extract_code(loc, referer=h_org.get("Referer")) + return code + + if resp_org.status_code == 200: + try: + org_data = resp_org.json() + except Exception: + self._print("[OAuth] organization/select 响应不是 JSON") + return None + + org_next = org_data.get("continue_url", "") + org_page = (org_data.get("page") or {}).get("type", "") + self._print(f"[OAuth] organization/select page={org_page or '-'} next={(org_next or '-')[:140]}") + if org_next: + if org_next.startswith("/"): + org_next = f"{OAUTH_ISSUER}{org_next}" + code, _ = self._oauth_follow_for_code(org_next, referer=h_org.get("Referer")) + if not code: + code = self._oauth_allow_redirect_extract_code(org_next, referer=h_org.get("Referer")) + return code + + if ws_next: + if ws_next.startswith("/"): + ws_next = f"{OAUTH_ISSUER}{ws_next}" + code, _ = self._oauth_follow_for_code(ws_next, referer=consent_url) + if not code: + code = self._oauth_allow_redirect_extract_code(ws_next, referer=consent_url) + return code + + return None + + def perform_codex_oauth_login_http(self, email: str, password: str, mail_token: str = None): + self._print("[OAuth] 开始执行 Codex OAuth 纯协议流程...") + + # 兼容两种 domain 形式,确保 auth 域也带 oai-did + self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com") + self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com") + + code_verifier, code_challenge = _generate_pkce() + state = secrets.token_urlsafe(24) + + authorize_params = { + "response_type": "code", + "client_id": OAUTH_CLIENT_ID, + "redirect_uri": OAUTH_REDIRECT_URI, + "scope": "openid profile email offline_access", + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "state": state, + } + authorize_url = f"{OAUTH_ISSUER}/oauth/authorize?{urlencode(authorize_params)}" + + def _oauth_json_headers(referer: str): + h = { + "Accept": "application/json", + "Content-Type": "application/json", + "Origin": OAUTH_ISSUER, + "Referer": referer, + "User-Agent": self.ua, + "oai-device-id": self.device_id, + } + h.update(_make_trace_headers()) + return h + + def _bootstrap_oauth_session(): + self._print("[OAuth] 1/7 GET /oauth/authorize") + try: + r = self.session.get( + authorize_url, + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.BASE}/", + "Upgrade-Insecure-Requests": "1", + "User-Agent": self.ua, + }, + allow_redirects=True, + timeout=30, + impersonate=self.impersonate, + ) + except Exception as e: + self._print(f"[OAuth] /oauth/authorize 异常: {e}") + return False, "" + + final_url = str(r.url) + redirects = len(getattr(r, "history", []) or []) + self._print(f"[OAuth] /oauth/authorize -> {r.status_code}, final={(final_url or '-')[:140]}, redirects={redirects}") + + has_login = any(getattr(c, "name", "") == "login_session" for c in self.session.cookies) + self._print(f"[OAuth] login_session: {'已获取' if has_login else '未获取'}") + + if not has_login: + self._print("[OAuth] 未拿到 login_session,尝试访问 oauth2 auth 入口") + oauth2_url = f"{OAUTH_ISSUER}/api/oauth/oauth2/auth" + try: + r2 = self.session.get( + oauth2_url, + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": authorize_url, + "Upgrade-Insecure-Requests": "1", + "User-Agent": self.ua, + }, + params=authorize_params, + allow_redirects=True, + timeout=30, + impersonate=self.impersonate, + ) + final_url = str(r2.url) + redirects2 = len(getattr(r2, "history", []) or []) + self._print(f"[OAuth] /api/oauth/oauth2/auth -> {r2.status_code}, final={(final_url or '-')[:140]}, redirects={redirects2}") + except Exception as e: + self._print(f"[OAuth] /api/oauth/oauth2/auth 异常: {e}") + + has_login = any(getattr(c, "name", "") == "login_session" for c in self.session.cookies) + self._print(f"[OAuth] login_session(重试): {'已获取' if has_login else '未获取'}") + + return has_login, final_url + + def _post_authorize_continue(referer_url: str): + sentinel_authorize = build_sentinel_token( + self.session, + self.device_id, + flow="authorize_continue", + user_agent=self.ua, + sec_ch_ua=self.sec_ch_ua, + impersonate=self.impersonate, + ) + if not sentinel_authorize: + self._print("[OAuth] authorize_continue 的 sentinel token 获取失败") + return None + + headers_continue = _oauth_json_headers(referer_url) + headers_continue["openai-sentinel-token"] = sentinel_authorize + + try: + return self.session.post( + f"{OAUTH_ISSUER}/api/accounts/authorize/continue", + json={"username": {"kind": "email", "value": email}}, + headers=headers_continue, + timeout=30, + allow_redirects=False, + impersonate=self.impersonate, + ) + except Exception as e: + self._print(f"[OAuth] authorize/continue 异常: {e}") + return None + + has_login_session, authorize_final_url = _bootstrap_oauth_session() + if not authorize_final_url: + return None + + continue_referer = authorize_final_url if authorize_final_url.startswith(OAUTH_ISSUER) else f"{OAUTH_ISSUER}/log-in" + + self._print("[OAuth] 2/7 POST /api/accounts/authorize/continue") + resp_continue = _post_authorize_continue(continue_referer) + if resp_continue is None: + return None + + self._print(f"[OAuth] /authorize/continue -> {resp_continue.status_code}") + if resp_continue.status_code == 400 and "invalid_auth_step" in (resp_continue.text or ""): + self._print("[OAuth] invalid_auth_step,重新 bootstrap 后重试一次") + has_login_session, authorize_final_url = _bootstrap_oauth_session() + if not authorize_final_url: + return None + continue_referer = authorize_final_url if authorize_final_url.startswith(OAUTH_ISSUER) else f"{OAUTH_ISSUER}/log-in" + resp_continue = _post_authorize_continue(continue_referer) + if resp_continue is None: + return None + self._print(f"[OAuth] /authorize/continue(重试) -> {resp_continue.status_code}") + + if resp_continue.status_code != 200: + self._print(f"[OAuth] 邮箱提交失败: {resp_continue.text[:180]}") + return None + + try: + continue_data = resp_continue.json() + except Exception: + self._print("[OAuth] authorize/continue 响应解析失败") + return None + + continue_url = continue_data.get("continue_url", "") + page_type = (continue_data.get("page") or {}).get("type", "") + self._print(f"[OAuth] continue page={page_type or '-'} next={(continue_url or '-')[:140]}") + + self._print("[OAuth] 3/7 POST /api/accounts/password/verify") + sentinel_pwd = build_sentinel_token( + self.session, + self.device_id, + flow="password_verify", + user_agent=self.ua, + sec_ch_ua=self.sec_ch_ua, + impersonate=self.impersonate, + ) + if not sentinel_pwd: + self._print("[OAuth] password_verify 的 sentinel token 获取失败") + return None + + headers_verify = _oauth_json_headers(f"{OAUTH_ISSUER}/log-in/password") + headers_verify["openai-sentinel-token"] = sentinel_pwd + + try: + resp_verify = self.session.post( + f"{OAUTH_ISSUER}/api/accounts/password/verify", + json={"password": password}, + headers=headers_verify, + timeout=30, + allow_redirects=False, + impersonate=self.impersonate, + ) + except Exception as e: + self._print(f"[OAuth] password/verify 异常: {e}") + return None + + self._print(f"[OAuth] /password/verify -> {resp_verify.status_code}") + if resp_verify.status_code != 200: + self._print(f"[OAuth] 密码校验失败: {resp_verify.text[:180]}") + return None + + try: + verify_data = resp_verify.json() + except Exception: + self._print("[OAuth] password/verify 响应解析失败") + return None + + continue_url = verify_data.get("continue_url", "") or continue_url + page_type = (verify_data.get("page") or {}).get("type", "") or page_type + self._print(f"[OAuth] verify page={page_type or '-'} next={(continue_url or '-')[:140]}") + + need_oauth_otp = ( + page_type == "email_otp_verification" + or "email-verification" in (continue_url or "") + or "email-otp" in (continue_url or "") + ) + + if need_oauth_otp: + self._print("[OAuth] 4/7 检测到邮箱 OTP 验证") + if not mail_token: + self._print("[OAuth] OAuth 阶段需要邮箱 OTP,但未提供 mail_token") + return None + + headers_otp = _oauth_json_headers(f"{OAUTH_ISSUER}/email-verification") + tried_codes = set() + otp_success = False + otp_deadline = time.time() + 120 + + while time.time() < otp_deadline and not otp_success: + messages = self._fetch_emails_duckmail(mail_token) or [] + candidate_codes = [] + + for msg in messages[:12]: + msg_id = msg.get("id") or msg.get("@id") + if not msg_id: + continue + detail = self._fetch_email_detail_duckmail(mail_token, msg_id) + if not detail: + continue + content = detail.get("text") or detail.get("html") or "" + code = self._extract_verification_code(content) + if code and code not in tried_codes: + candidate_codes.append(code) + + if not candidate_codes: + elapsed = int(120 - max(0, otp_deadline - time.time())) + self._print(f"[OAuth] OTP 等待中... ({elapsed}s/120s)") + time.sleep(2) + continue + + for otp_code in candidate_codes: + tried_codes.add(otp_code) + self._print(f"[OAuth] 尝试 OTP: {otp_code}") + try: + resp_otp = self.session.post( + f"{OAUTH_ISSUER}/api/accounts/email-otp/validate", + json={"code": otp_code}, + headers=headers_otp, + timeout=30, + allow_redirects=False, + impersonate=self.impersonate, + ) + except Exception as e: + self._print(f"[OAuth] email-otp/validate 异常: {e}") + continue + + self._print(f"[OAuth] /email-otp/validate -> {resp_otp.status_code}") + if resp_otp.status_code != 200: + self._print(f"[OAuth] OTP 无效,继续尝试下一条: {resp_otp.text[:160]}") + continue + + try: + otp_data = resp_otp.json() + except Exception: + self._print("[OAuth] email-otp/validate 响应解析失败") + continue + + continue_url = otp_data.get("continue_url", "") or continue_url + page_type = (otp_data.get("page") or {}).get("type", "") or page_type + self._print(f"[OAuth] OTP 验证通过 page={page_type or '-'} next={(continue_url or '-')[:140]}") + otp_success = True + break + + if not otp_success: + time.sleep(2) + + if not otp_success: + self._print(f"[OAuth] OAuth 阶段 OTP 验证失败,已尝试 {len(tried_codes)} 个验证码") + return None + + code = None + consent_url = continue_url + if consent_url and consent_url.startswith("/"): + consent_url = f"{OAUTH_ISSUER}{consent_url}" + + if not consent_url and "consent" in page_type: + consent_url = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + + if consent_url: + code = _extract_code_from_url(consent_url) + + if not code and consent_url: + self._print("[OAuth] 5/7 跟随 continue_url 提取 code") + code, _ = self._oauth_follow_for_code(consent_url, referer=f"{OAUTH_ISSUER}/log-in/password") + + consent_hint = ( + ("consent" in (consent_url or "")) + or ("sign-in-with-chatgpt" in (consent_url or "")) + or ("workspace" in (consent_url or "")) + or ("organization" in (consent_url or "")) + or ("consent" in page_type) + or ("organization" in page_type) + ) + + if not code and consent_hint: + if not consent_url: + consent_url = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + self._print("[OAuth] 6/7 执行 workspace/org 选择") + code = self._oauth_submit_workspace_and_org(consent_url) + + if not code: + fallback_consent = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + self._print("[OAuth] 6/7 回退 consent 路径重试") + code = self._oauth_submit_workspace_and_org(fallback_consent) + if not code: + code, _ = self._oauth_follow_for_code(fallback_consent, referer=f"{OAUTH_ISSUER}/log-in/password") + + if not code: + self._print("[OAuth] 未获取到 authorization code") + return None + + self._print("[OAuth] 7/7 POST /oauth/token") + token_resp = self.session.post( + f"{OAUTH_ISSUER}/oauth/token", + headers={"Content-Type": "application/x-www-form-urlencoded", "User-Agent": self.ua}, + data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": OAUTH_REDIRECT_URI, + "client_id": OAUTH_CLIENT_ID, + "code_verifier": code_verifier, + }, + timeout=60, + impersonate=self.impersonate, + ) + self._print(f"[OAuth] /oauth/token -> {token_resp.status_code}") + + if token_resp.status_code != 200: + self._print(f"[OAuth] token 交换失败: {token_resp.status_code} {token_resp.text[:200]}") + return None + + try: + data = token_resp.json() + except Exception: + self._print("[OAuth] token 响应解析失败") + return None + + if not data.get("access_token"): + self._print("[OAuth] token 响应缺少 access_token") + return None + + self._print("[OAuth] Codex Token 获取成功") + return data + + +# ==================== 并发批量注册 ==================== + +def _register_one(idx, total, proxy, output_file): + """单个注册任务 (在线程中运行) - 使用 DuckMail 临时邮箱""" + pool = _get_proxy_pool(fallback_proxy=proxy) + last_error = "unknown error" + + for attempt in range(1, PROXY_RETRY_ATTEMPTS_PER_ACCOUNT + 1): + if _stop_event.is_set(): + return False, None, "已手动停止" + reg = None + current_proxy = pool.next_proxy() + proxy_label = current_proxy or "direct" + + try: + reg = ChatGPTRegister( + proxy=current_proxy, + fixed_proxy=current_proxy, + tag=f"{idx}-try{attempt}", + ) + reg._print( + f"[Proxy] 尝试 {attempt}/{PROXY_RETRY_ATTEMPTS_PER_ACCOUNT}: {proxy_label}" + ) + + # 1. 创建 DuckMail 临时邮箱 + reg._print("[DuckMail] 创建临时邮箱...") + email, email_pwd, mail_token = reg.create_temp_email() + tag = email.split("@")[0] + reg.tag = tag # 更新 tag + + chatgpt_password = _generate_password() + name = _random_name() + birthdate = _random_birthdate() + + with _print_lock: + print(f"\n{'='*60}") + print(f" [{idx}/{total}] 注册: {email}") + print(f" ChatGPT密码: {chatgpt_password}") + print(f" 邮箱密码: {email_pwd}") + print(f" 姓名: {name} | 生日: {birthdate}") + print(f" 代理: {proxy_label}") + print(f"{'='*60}") + + # 2. 执行注册流程 + reg.run_register(email, chatgpt_password, name, birthdate, mail_token) + + # 3. OAuth(可选) + oauth_ok = True + if ENABLE_OAUTH: + reg._print("[OAuth] 开始获取 Codex Token...") + tokens = reg.perform_codex_oauth_login_http(email, chatgpt_password, mail_token=mail_token) + oauth_ok = bool(tokens and tokens.get("access_token")) + if oauth_ok: + _save_codex_tokens(email, tokens) + reg._print("[OAuth] Token 已保存") + if AUTO_UPLOAD_SUB2API and SUB2API_BASE_URL and tokens.get("refresh_token"): + reg._print("[Sub2Api] 正在上传账号...") + _push_account_to_sub2api(email, tokens) + else: + msg = "OAuth 获取失败" + if OAUTH_REQUIRED: + raise Exception(f"{msg}(oauth_required=true)") + reg._print(f"[OAuth] {msg}(按配置继续)") + + # 4. 成功后固定此代理(后续优先使用) + if current_proxy: + pool.report_success(current_proxy) + _save_stable_proxy_to_file(current_proxy) + _save_stable_proxy_to_config(current_proxy) + + # 5. 线程安全写入结果 + with _file_lock: + with open(output_file, "a", encoding="utf-8") as out: + out.write( + f"{email}----{chatgpt_password}----{email_pwd}" + f"----oauth={'ok' if oauth_ok else 'fail'}----proxy={proxy_label}\n" + ) + + with _print_lock: + print(f"\n[OK] [{tag}] {email} 注册成功! 代理: {proxy_label}") + return True, email, None + + except Exception as e: + last_error = str(e) + if current_proxy: + pool.report_bad(current_proxy, error=e) + + with _print_lock: + print( + f"\n[FAIL] [{idx}] 尝试 {attempt}/{PROXY_RETRY_ATTEMPTS_PER_ACCOUNT} " + f"失败: {last_error} | 代理: {proxy_label}" + ) + + if attempt >= PROXY_RETRY_ATTEMPTS_PER_ACCOUNT: + with _print_lock: + traceback.print_exc() + break + + return False, None, f"代理重试耗尽: {last_error}" + + +def run_batch(total_accounts: int = 3, output_file="registered_accounts.txt", + max_workers=3, proxy=None): + """并发批量注册 - DuckMail 临时邮箱版""" + + _stop_event.clear() + + if not DUCKMAIL_BEARER: + print("❌ 错误: 未设置 DUCKMAIL_BEARER 环境变量") + print(" 请设置: export DUCKMAIL_BEARER='your_api_key_here'") + print(" 或: set DUCKMAIL_BEARER=your_api_key_here (Windows)") + return + + pool = _get_proxy_pool(fallback_proxy=proxy) + pool.refresh(force=True) + proxy_info = pool.info() + + actual_workers = min(max_workers, total_accounts) + print(f"\n{'#'*60}") + print(f" ChatGPT 批量自动注册 (DuckMail 临时邮箱版)") + print(f" 注册数量: {total_accounts} | 并发数: {actual_workers}") + print(f" DuckMail: {DUCKMAIL_API_BASE}") + print(f" 代理源: {proxy_info['list_url']}") + print(f" 优先稳定代理: {'是' if proxy_info['prefer_stable_proxy'] else '否'}") + print(f" 账号级代理重试: {PROXY_RETRY_ATTEMPTS_PER_ACCOUNT} 次/账号") + print(f" 代理校验: {'开启' if proxy_info['validate_enabled'] else '关闭'}") + if proxy_info["validate_enabled"]: + print(f" 校验目标: {proxy_info['validate_test_url']}") + print(f" 校验超时: {proxy_info['validate_timeout_seconds']} 秒 | 校验并发: {proxy_info['validate_workers']}") + print(f" 校验通过: {proxy_info['validated_count']}/{proxy_info['fetched_count']}") + print(f" 代理池(HTTP/SOCKS): {proxy_info['count']} 个") + print(f" 代理重试: 单请求最多 {proxy_info['max_retries_per_request']} 次") + print(f" 失效冷却: {proxy_info['bad_ttl_seconds']} 秒") + if proxy_info["bad_count"] > 0: + print(f" 当前冷却代理: {proxy_info['bad_count']} 个") + if proxy_info["fallback_proxy"]: + print(f" 兜底代理: {proxy_info['fallback_proxy']}") + if proxy_info["stable_proxy"]: + print(f" 稳定代理: {proxy_info['stable_proxy']}") + print(f" 稳定代理文件: {_stable_proxy_path()}") + if proxy_info["last_error"]: + print(f" 代理拉取告警: {proxy_info['last_error'][:200]}") + print(f" OAuth: {'开启' if ENABLE_OAUTH else '关闭'} | required: {'是' if OAUTH_REQUIRED else '否'}") + if ENABLE_OAUTH: + print(f" OAuth Issuer: {OAUTH_ISSUER}") + print(f" OAuth Client: {OAUTH_CLIENT_ID}") + print(f" Token输出: {TOKEN_JSON_DIR}/, {AK_FILE}, {RK_FILE}") + print(f" 输出文件: {output_file}") + print(f"{'#'*60}\n") + + success_count = 0 + fail_count = 0 + start_time = time.time() + + with ThreadPoolExecutor(max_workers=actual_workers) as executor: + futures = {} + for idx in range(1, total_accounts + 1): + future = executor.submit( + _register_one, idx, total_accounts, proxy, output_file + ) + futures[future] = idx + + for future in as_completed(futures): + idx = futures[future] + try: + ok, email, err = future.result() + if ok: + success_count += 1 + else: + fail_count += 1 + print(f" [账号 {idx}] 失败: {err}") + except Exception as e: + fail_count += 1 + with _print_lock: + print(f"[FAIL] 账号 {idx} 线程异常: {e}") + + elapsed = time.time() - start_time + avg = elapsed / total_accounts if total_accounts else 0 + print(f"\n{'#'*60}") + print(f" 注册完成! 耗时 {elapsed:.1f} 秒") + print(f" 总数: {total_accounts} | 成功: {success_count} | 失败: {fail_count}") + print(f" 平均速度: {avg:.1f} 秒/个") + if success_count > 0: + print(f" 结果文件: {output_file}") + print(f"{'#'*60}") + + +def main(): + print("=" * 60) + print(" ChatGPT 批量自动注册工具 (DuckMail 临时邮箱版)") + print("=" * 60) + + # 检查 DuckMail 配置 + if not DUCKMAIL_BEARER: + print("\n⚠️ 警告: 未设置 DUCKMAIL_BEARER") + print(" 请编辑 config.json 设置 duckmail_bearer,或设置环境变量:") + print(" Windows: set DUCKMAIL_BEARER=your_api_key_here") + print(" Linux/Mac: export DUCKMAIL_BEARER='your_api_key_here'") + print("\n 按 Enter 继续尝试运行 (可能会失败)...") + input() + + env_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") \ + or os.environ.get("ALL_PROXY") or os.environ.get("all_proxy") + default_fallback_proxy = _normalize_proxy(DEFAULT_PROXY) + env_fallback_proxy = _normalize_proxy(env_proxy) + proxy = default_fallback_proxy or env_fallback_proxy + proxy_source = "config.json(proxy)" if default_fallback_proxy else ( + "环境变量(HTTPS_PROXY/ALL_PROXY)" if env_fallback_proxy else "未配置" + ) + + print(f"[Info] 代理池地址: {_normalize_proxy_list_url(PROXY_LIST_URL)}") + print("[Info] 代理模式: 自动拉取 US 列表,使用 http/socks 代理并轮询") + print(f"[Info] 代理校验: {'开启' if PROXY_VALIDATE_ENABLED else '关闭'} | 目标: {PROXY_VALIDATE_TEST_URL}") + print(f"[Info] 优先稳定代理开关: {'开启' if PREFER_STABLE_PROXY else '关闭'}") + print(f"[Info] 账号失败自动换代理重试: {PROXY_RETRY_ATTEMPTS_PER_ACCOUNT} 次") + if proxy: + print(f"[Info] 兜底代理来源: {proxy_source} -> {proxy}") + else: + print("[Info] 未配置兜底代理,远端列表为空时将直连") + + # 输入注册数量 + count_input = input(f"\n注册账号数量 (默认 {DEFAULT_TOTAL_ACCOUNTS}): ").strip() + total_accounts = int(count_input) if count_input.isdigit() and int(count_input) > 0 else DEFAULT_TOTAL_ACCOUNTS + + workers_input = input("并发数 (默认 3): ").strip() + max_workers = int(workers_input) if workers_input.isdigit() and int(workers_input) > 0 else 3 + + run_batch(total_accounts=total_accounts, output_file=DEFAULT_OUTPUT_FILE, + max_workers=max_workers, proxy=proxy) + + +if __name__ == "__main__": + main() diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/config.json b/GPT_register+duckmail+CPA+autouploadsub2api/config.json new file mode 100644 index 0000000..fa9a6be --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/config.json @@ -0,0 +1,34 @@ +{ + "_comment": "ChatGPT 批量自动注册工具配置文件 (DuckMail 临时邮箱版)", + "total_accounts": 3, + "duckmail_api_base": "https://api.duckmail.sbs", + "duckmail_bearer": "", + "proxy": "", + "proxy_list_url": "https://github.com/proxifly/free-proxy-list/blob/main/proxies/countries/US/data.txt", + "proxy_validate_enabled": true, + "proxy_validate_timeout_seconds": 6, + "proxy_validate_workers": 40, + "proxy_validate_test_url": "https://auth.openai.com/", + "proxy_max_retries_per_request": 30, + "proxy_bad_ttl_seconds": 180, + "proxy_retry_attempts_per_account": 20, + "stable_proxy_file": "stable_proxy.txt", + "stable_proxy": "http://127.0.0.1:7890", + "prefer_stable_proxy": true, + "output_file": "registered_accounts.txt", + "enable_oauth": true, + "oauth_required": true, + "oauth_issuer": "https://auth.openai.com", + "oauth_client_id": "", + "oauth_redirect_uri": "http://localhost:1455/auth/callback", + "ak_file": "ak.txt", + "rk_file": "rk.txt", + "token_json_dir": "codex_tokens", + "_comment_sub2api": "Sub2Api 平台上传配置,auto_upload_sub2api=true 时注册成功后自动上传 refresh_token", + "sub2api_base_url": "", + "sub2api_bearer": "", + "sub2api_email": "", + "sub2api_password": "", + "auto_upload_sub2api": false, + "sub2api_group_ids": [2] +} diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/requirements.txt b/GPT_register+duckmail+CPA+autouploadsub2api/requirements.txt new file mode 100644 index 0000000..68b83e0 --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/requirements.txt @@ -0,0 +1,3 @@ +curl_cffi>=0.14.0 +fastapi>=0.100.0 +uvicorn>=0.20.0 diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/server.py b/GPT_register+duckmail+CPA+autouploadsub2api/server.py new file mode 100644 index 0000000..685407e --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/server.py @@ -0,0 +1,957 @@ +""" +ChatGPT Register 批量注册 Web 管理服务 +FastAPI backend — port 18421 +""" +import asyncio +import copy +import json +import multiprocessing +import os +import socket +import sys +import threading +import time +import uuid +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime +from pathlib import Path +from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple + +# macOS/Linux 用 fork 避免 spawn 重新导入 __main__ 引发端口冲突 +_MP_CTX = multiprocessing.get_context("fork" if sys.platform != "win32" else "spawn") + +import uvicorn +from fastapi import FastAPI, HTTPException +from fastapi.concurrency import run_in_threadpool +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +BASE_DIR = Path(__file__).parent +WEB_DIR = BASE_DIR / "web" +CONFIG_FILE = BASE_DIR / "config.json" +TOKENS_DIR = BASE_DIR / "codex_tokens" +TOKENS_DIR.mkdir(exist_ok=True) + +app = FastAPI(title="ChatGPT Register") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) +app.mount("/static", StaticFiles(directory=str(WEB_DIR)), name="static") + + +# ============================================================ +# SSE / Log infrastructure +# ============================================================ +_log_lock = threading.Lock() +_log_entries: List[Dict] = [] +_sse_lock = threading.Lock() +_sse_subscribers: List[Tuple] = [] # (loop, asyncio.Queue) + + +def _push_log(level: str, message: str, step: str = "") -> None: + ts = datetime.now().strftime("%H:%M:%S") + entry: Dict[str, Any] = {"ts": ts, "level": level, "message": message} + if step: + entry["step"] = step + with _log_lock: + _log_entries.append(entry) + if len(_log_entries) > 2000: + _log_entries.pop(0) + _broadcast_sse({"type": "log.appended", "log": entry}) + + +def _broadcast_sse(payload: Dict) -> None: + with _sse_lock: + subs = list(_sse_subscribers) + for loop, q in subs: + def _enqueue(tq=q, d=payload): + try: + tq.put_nowait(copy.deepcopy(d)) + except asyncio.QueueFull: + pass + try: + loop.call_soon_threadsafe(_enqueue) + except RuntimeError: + pass + + +class _QueueWriter: + """Subprocess stdout → multiprocessing.Queue,每行一条消息。""" + + def __init__(self, q): + self._q = q + self._buf = "" + + def write(self, text: str) -> int: + self._buf += text + while "\n" in self._buf: + line, self._buf = self._buf.split("\n", 1) + line = line.strip() + if line: + try: + self._q.put(line) + except Exception: + pass + return len(text) + + def flush(self): + pass + + def isatty(self): + return False + + +def _worker_process_fn(total_accounts: int, max_workers: int, proxy: Optional[str], + output_file: str, log_queue) -> None: + """在独立子进程中运行注册任务,stdout 重定向到队列。""" + import sys + sys.stdout = _QueueWriter(log_queue) + sys.stderr = sys.stdout + try: + import chatgpt_register as reg + reg.run_batch( + total_accounts=total_accounts, + output_file=output_file, + max_workers=max_workers, + proxy=proxy, + ) + except Exception as e: + try: + log_queue.put(f"[ERROR] 任务异常: {e}") + except Exception: + pass + finally: + try: + log_queue.put(None) # sentinel + except Exception: + pass + + +def _log_level(line: str) -> str: + ll = line.lower() + if "[ok]" in ll or ("成功" in ll and "[fail]" not in ll): + return "success" + if "[fail]" in ll or "失败" in ll or "错误" in ll or "error" in ll: + return "error" + if "⚠" in line or "警告" in ll or "warn" in ll: + return "warn" + return "info" + + +def _log_reader_fn(log_queue) -> None: + """主进程中读取子进程日志队列,推送到 SSE。""" + while True: + try: + line = log_queue.get(timeout=1.0) + except Exception: + if _task_process is None or not _task_process.is_alive(): + break + continue + if line is None: + break + _push_log(_log_level(line), line) + _push_log("info", "任务已结束", step="stopped") + _set_task(status="idle", finished_at=datetime.now().isoformat(timespec="seconds")) + + +# ============================================================ +# Task state +# ============================================================ +_task_lock = threading.RLock() +_task: Dict[str, Any] = { + "status": "idle", + "run_id": None, + "started_at": None, + "finished_at": None, + "worker_count": 1, + "total_accounts": 0, + "success": 0, + "fail": 0, +} +_task_process: Optional[multiprocessing.Process] = None +_log_reader_thread: Optional[threading.Thread] = None + + +def _get_snapshot() -> Dict: + with _task_lock: + t = copy.deepcopy(_task) + return { + "task": { + "run_id": t["run_id"], + "status": t["status"], + "revision": 0, + "started_at": t["started_at"], + "finished_at": t["finished_at"], + }, + "runtime": {"run_id": t["run_id"], "revision": 0, "workers": []}, + "stats": { + "success": t["success"], + "fail": t["fail"], + "total": t["success"] + t["fail"], + }, + "server_time": datetime.now().isoformat(timespec="seconds"), + } + + +def _set_task(**kwargs) -> None: + with _task_lock: + _task.update(kwargs) + snap = _get_snapshot() + _broadcast_sse({"type": "task.updated", **snap}) + + +# ============================================================ +# Config +# ============================================================ +def _load_config() -> Dict: + if CONFIG_FILE.exists(): + try: + return json.loads(CONFIG_FILE.read_text(encoding="utf-8")) + except Exception: + pass + return {} + + +def _save_config(cfg: Dict) -> None: + CONFIG_FILE.write_text( + json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + +# ============================================================ +# Sub2Api Client (curl_cffi based) +# ============================================================ +from curl_cffi import requests as cffi_req # noqa: E402 + +_sub2api_bearer_lock = threading.Lock() +_sub2api_bearer_cache: List[str] = [""] + + +def _cffi_sub2api_login(base_url: str, email: str, password: str) -> str: + try: + resp = cffi_req.post( + f"{base_url}/api/v1/auth/login", + json={"email": email, "password": password}, + impersonate="chrome131", + timeout=15, + ) + data = resp.json() + return str( + data.get("token") or data.get("access_token") + or (data.get("data") or {}).get("token") + or (data.get("data") or {}).get("access_token") + or "" + ).strip() + except Exception: + return "" + + +def _cffi_sub2api_headers(token: str) -> Dict: + return { + "Authorization": f"Bearer {token}", + "Accept": "application/json", + "Content-Type": "application/json", + } + + +def _cffi_sub2api_req(method: str, path: str, cfg: Dict, **kwargs): + base_url = str(cfg.get("sub2api_base_url", "") or "").rstrip("/") + if not base_url: + raise ValueError("Sub2Api 未配置地址") + + bearer = str(cfg.get("sub2api_bearer", "") or "").strip() + if bearer: + _sub2api_bearer_cache[0] = bearer + + if not _sub2api_bearer_cache[0]: + email = str(cfg.get("sub2api_email", "") or "").strip() + password = str(cfg.get("sub2api_password", "") or "").strip() + if email and password: + with _sub2api_bearer_lock: + if not _sub2api_bearer_cache[0]: + token = _cffi_sub2api_login(base_url, email, password) + if token: + _sub2api_bearer_cache[0] = token + + token = _sub2api_bearer_cache[0] + kwargs.setdefault("timeout", 20) + kwargs.setdefault("impersonate", "chrome131") + resp = cffi_req.request( + method, f"{base_url}{path}", + headers=_cffi_sub2api_headers(token), + **kwargs, + ) + if resp.status_code == 401: + email = str(cfg.get("sub2api_email", "") or "").strip() + password = str(cfg.get("sub2api_password", "") or "").strip() + if email and password: + with _sub2api_bearer_lock: + new = _cffi_sub2api_login(base_url, email, password) + if new: + _sub2api_bearer_cache[0] = new + resp = cffi_req.request( + method, f"{base_url}{path}", + headers=_cffi_sub2api_headers(_sub2api_bearer_cache[0]), + **kwargs, + ) + return resp + + +def _sub2api_list_all(cfg: Dict) -> List[Dict]: + all_items: List[Dict] = [] + page = 1 + while True: + resp = _cffi_sub2api_req( + "GET", "/api/v1/admin/accounts", cfg, + params={"page": page, "page_size": 100, "platform": "openai", "type": "oauth"}, + ) + resp.raise_for_status() + data = resp.json() + payload = data.get("data") if isinstance(data.get("data"), dict) else data + items = payload.get("items") or [] + if not isinstance(items, list): + break + all_items.extend(i for i in items if isinstance(i, dict)) + total = payload.get("total", 0) + if not items or len(items) < 100: + break + if isinstance(total, int) and total > 0 and len(all_items) >= total: + break + page += 1 + return all_items + + +def _sub2api_refresh_account(cfg: Dict, account_id: int) -> bool: + try: + resp = _cffi_sub2api_req("POST", f"/api/v1/admin/accounts/{account_id}/refresh", cfg) + return resp.status_code in (200, 201) + except Exception: + return False + + +def _sub2api_delete_account(cfg: Dict, account_id: int) -> bool: + try: + resp = _cffi_sub2api_req("DELETE", f"/api/v1/admin/accounts/{account_id}", cfg) + return resp.status_code in (200, 204) + except Exception: + return False + + +def _is_abnormal(status: Any) -> bool: + return str(status or "").strip().lower() in ("error", "disabled") + + +def _account_identity(item: Dict) -> Tuple[str, str]: + email = "" + rt = "" + extra = item.get("extra") + if isinstance(extra, dict): + email = str(extra.get("email") or "").strip().lower() + if not email: + name = str(item.get("name") or "").strip().lower() + if "@" in name: + email = name + creds = item.get("credentials") + if isinstance(creds, dict): + rt = str(creds.get("refresh_token") or "").strip() + return email, rt + + +def _build_dedupe_plan(all_accounts: List[Dict]) -> Dict: + def sort_key(item): + raw = item.get("updated_at") or item.get("updatedAt") or "" + try: + ts = datetime.fromisoformat(str(raw).replace("Z", "+00:00")).timestamp() if raw else 0.0 + except Exception: + ts = 0.0 + try: + item_id = int(item.get("id") or 0) + except Exception: + item_id = 0 + return (ts, item_id) + + id_to_item: Dict[int, Dict] = {} + parent: Dict[int, int] = {} + key_to_ids: Dict[str, List[int]] = {} + + for item in all_accounts: + try: + acc_id = int(item.get("id") or 0) + except Exception: + continue + if acc_id <= 0: + continue + id_to_item[acc_id] = item + parent[acc_id] = acc_id + email, rt = _account_identity(item) + if email: + key_to_ids.setdefault(f"email:{email}", []).append(acc_id) + if rt: + key_to_ids.setdefault(f"rt:{rt}", []).append(acc_id) + + def find(x): + root = x + while parent[root] != root: + root = parent[root] + while parent[x] != x: + nxt = parent[x] + parent[x] = root + x = nxt + return root + + def union(a, b): + ra, rb = find(a), find(b) + if ra != rb: + parent[rb] = ra + + for ids in key_to_ids.values(): + if len(ids) > 1: + for acc_id in ids[1:]: + union(ids[0], acc_id) + + components: Dict[int, List[int]] = {} + for acc_id in id_to_item: + components.setdefault(find(acc_id), []).append(acc_id) + + dup_groups = [ids for ids in components.values() if len(ids) > 1] + delete_ids: List[int] = [] + for group_ids in dup_groups: + group_items = [id_to_item[i] for i in group_ids] + keep = max(group_items, key=sort_key) + try: + keep_id = int(keep.get("id") or 0) + except Exception: + keep_id = 0 + delete_ids.extend(i for i in group_ids if i != keep_id) + + return { + "duplicate_groups": len(dup_groups), + "duplicate_accounts": sum(len(g) for g in dup_groups), + "delete_ids": delete_ids, + } + + +def _parallel_run(fn, items: List, workers: int = 8) -> Dict: + ok_ids, fail_ids = [], [] + if not items: + return {"ok": ok_ids, "fail": fail_ids} + w = min(workers, len(items)) + with ThreadPoolExecutor(max_workers=w) as ex: + futs = {ex.submit(fn, i): i for i in items} + for fut in as_completed(futs): + i = futs[fut] + try: + if fut.result(): + ok_ids.append(i) + else: + fail_ids.append(i) + except Exception: + fail_ids.append(i) + return {"ok": ok_ids, "fail": fail_ids} + + +# ============================================================ +# Token helpers +# ============================================================ +def _list_tokens() -> List[Dict]: + tokens = [] + for fpath in sorted(TOKENS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True): + try: + data = json.loads(fpath.read_text(encoding="utf-8")) + except Exception: + continue + tokens.append({ + "filename": fpath.name, + "email": data.get("email", fpath.stem), + "expired": data.get("expired", ""), + "uploaded_platforms": data.get("uploaded_platforms", []), + "content": data, + }) + return tokens + + +# ============================================================ +# Task runner (subprocess-based) +# ============================================================ + + +# ============================================================ +# FastAPI routes +# ============================================================ +@app.get("/", response_class=HTMLResponse) +async def serve_index(): + index_file = WEB_DIR / "index.html" + if not index_file.exists(): + raise HTTPException(status_code=404, detail="index.html not found") + return index_file.read_text(encoding="utf-8") + + +@app.get("/api/logs") +async def sse_logs(): + loop = asyncio.get_running_loop() + q: asyncio.Queue = asyncio.Queue(maxsize=500) + with _sse_lock: + _sse_subscribers.append((loop, q)) + + async def event_generator() -> AsyncGenerator[str, None]: + with _log_lock: + backlog = list(_log_entries[-100:]) + for entry in backlog: + yield f"event: log.appended\ndata: {json.dumps({'type': 'log.appended', 'log': entry}, ensure_ascii=False)}\n\n" + yield f"event: connected\ndata: {json.dumps({'type': 'connected', 'snapshot': _get_snapshot()}, ensure_ascii=False)}\n\n" + try: + while True: + try: + payload = await asyncio.wait_for(q.get(), timeout=25) + event_type = payload.get("type", "message") + yield f"event: {event_type}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n" + except asyncio.TimeoutError: + yield ": heartbeat\n\n" + except asyncio.CancelledError: + pass + finally: + with _sse_lock: + try: + _sse_subscribers.remove((loop, q)) + except ValueError: + pass + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +@app.get("/api/status") +async def get_status(): + return _get_snapshot() + + +class StartRequest(BaseModel): + total_accounts: int = 3 + worker_count: int = 1 + + +@app.post("/api/start") +async def start_task(req: StartRequest): + global _task_process, _log_reader_thread + with _task_lock: + if _task["status"] not in ("idle",): + raise HTTPException(status_code=400, detail="任务已在运行") + run_id = uuid.uuid4().hex[:12] + cfg = _load_config() + proxy = str(cfg.get("proxy", "") or cfg.get("stable_proxy", "") or "").strip() or None + output_file = str(BASE_DIR / "registered_accounts.txt") + + _set_task( + status="running", + run_id=run_id, + started_at=datetime.now().isoformat(timespec="seconds"), + finished_at=None, + total_accounts=req.total_accounts, + worker_count=req.worker_count, + success=0, + fail=0, + ) + _push_log("info", f"任务启动: 注册 {req.total_accounts} 个账号, {req.worker_count} 线程", step="start") + + log_queue = _MP_CTX.Queue() + _task_process = _MP_CTX.Process( + target=_worker_process_fn, + args=(req.total_accounts, req.worker_count, proxy, output_file, log_queue), + daemon=True, + ) + _task_process.start() + + _log_reader_thread = threading.Thread( + target=_log_reader_fn, + args=(log_queue,), + daemon=True, + ) + _log_reader_thread.start() + return _get_snapshot() + + +@app.post("/api/stop") +async def stop_task(): + global _task_process + with _task_lock: + if _task["status"] == "idle": + raise HTTPException(status_code=400, detail="没有运行中的任务") + if _task_process and _task_process.is_alive(): + _task_process.kill() + _task_process = None + _push_log("warn", "任务已强制终止", step="stopped") + _set_task(status="idle", finished_at=datetime.now().isoformat(timespec="seconds")) + return _get_snapshot() + + +@app.get("/api/tokens") +async def list_tokens_api(): + tokens = await run_in_threadpool(_list_tokens) + return {"tokens": tokens} + + +@app.delete("/api/tokens/{filename}") +async def delete_token(filename: str): + if "/" in filename or "\\" in filename or ".." in filename: + raise HTTPException(status_code=400, detail="无效文件名") + fpath = TOKENS_DIR / filename + if not fpath.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + fpath.unlink() + return {"ok": True} + + +@app.get("/api/config") +async def get_config(): + cfg = _load_config() + safe = dict(cfg) + if safe.get("sub2api_password"): + safe["sub2api_password"] = "**masked**" + if safe.get("duckmail_bearer"): + safe["duckmail_bearer_preview"] = safe["duckmail_bearer"][:20] + "..." + return safe + + +@app.post("/api/config") +async def save_config_api(body: Dict[str, Any]): + cfg = _load_config() + if body.get("sub2api_password") == "**masked**": + body.pop("sub2api_password") + body.pop("duckmail_bearer_preview", None) + cfg.update(body) + await run_in_threadpool(_save_config, cfg) + # Invalidate bearer cache if credentials changed + new_bearer = str(body.get("sub2api_bearer", "") or "").strip() + if new_bearer: + _sub2api_bearer_cache[0] = new_bearer + elif "sub2api_email" in body or "sub2api_password" in body: + _sub2api_bearer_cache[0] = "" + return {"ok": True} + + +# Sub2Api pool status +@app.get("/api/sub2api/pool/status") +async def sub2api_pool_status(): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + return {"configured": False} + try: + all_accounts = await run_in_threadpool(_sub2api_list_all, cfg) + total = len(all_accounts) + error = sum(1 for a in all_accounts if _is_abnormal(a.get("status"))) + normal = total - error + threshold = int(cfg.get("sub2api_min_candidates", 200) or 200) + pct = round(normal / threshold * 100, 1) if threshold > 0 else 100.0 + return { + "configured": True, + "total": total, + "candidates": normal, + "error_count": error, + "threshold": threshold, + "healthy": normal >= threshold, + "percent": pct, + "error": None, + } + except Exception as e: + return { + "configured": True, + "total": 0, "candidates": 0, "error_count": 0, + "threshold": int(cfg.get("sub2api_min_candidates", 200) or 200), + "healthy": False, "percent": 0, "error": str(e), + } + + +@app.post("/api/sub2api/pool/check") +async def sub2api_pool_check(): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + return {"ok": False, "message": "Sub2Api 未配置地址"} + try: + all_accounts = await run_in_threadpool(_sub2api_list_all, cfg) + total = len(all_accounts) + error = sum(1 for a in all_accounts if _is_abnormal(a.get("status"))) + normal = total - error + return { + "ok": True, + "total": total, "normal": normal, "error": error, + "message": f"连接成功,共 {total} 个账号,{normal} 正常,{error} 异常", + } + except Exception as e: + return {"ok": False, "message": f"连接失败: {e}"} + + +@app.post("/api/sub2api/pool/maintain") +async def sub2api_pool_maintain(): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + + def _maintain(): + t0 = time.time() + all_accounts = _sub2api_list_all(cfg) + error_ids = [ + int(a.get("id") or 0) for a in all_accounts + if _is_abnormal(a.get("status")) and int(a.get("id") or 0) > 0 + ] + refreshed = _parallel_run(lambda i: _sub2api_refresh_account(cfg, i), error_ids, 8) + if refreshed["ok"]: + time.sleep(2) + all_after = _sub2api_list_all(cfg) + still_error_ids = [ + int(a.get("id") or 0) for a in all_after + if _is_abnormal(a.get("status")) and int(a.get("id") or 0) > 0 + ] + plan = _build_dedupe_plan(all_after) + del_ids = list(set(still_error_ids + plan["delete_ids"])) + deleted = _parallel_run(lambda i: _sub2api_delete_account(cfg, i), del_ids, 12) + return { + "total": len(all_after), + "error_count": len(still_error_ids), + "refreshed": len(refreshed["ok"]), + "duplicate_groups": plan["duplicate_groups"], + "duplicate_accounts": plan["duplicate_accounts"], + "deleted_ok": len(deleted["ok"]), + "deleted_fail": len(deleted["fail"]), + "duration_ms": int((time.time() - t0) * 1000), + } + + return await run_in_threadpool(_maintain) + + +class DedupRequest(BaseModel): + dry_run: bool = True + timeout: int = 20 + + +@app.post("/api/sub2api/pool/dedupe") +async def sub2api_pool_dedupe(req: DedupRequest): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + + def _dedupe(): + all_accounts = _sub2api_list_all(cfg) + plan = _build_dedupe_plan(all_accounts) + deleted_ok = deleted_fail = 0 + if not req.dry_run and plan["delete_ids"]: + result = _parallel_run(lambda i: _sub2api_delete_account(cfg, i), plan["delete_ids"], 12) + deleted_ok = len(result["ok"]) + deleted_fail = len(result["fail"]) + return { + "dry_run": req.dry_run, + "total": len(all_accounts), + "duplicate_groups": plan["duplicate_groups"], + "duplicate_accounts": plan["duplicate_accounts"], + "to_delete": len(plan["delete_ids"]), + "deleted_ok": deleted_ok, + "deleted_fail": deleted_fail, + } + + return await run_in_threadpool(_dedupe) + + +@app.get("/api/sub2api/accounts") +async def sub2api_accounts( + page: int = 1, + page_size: int = 20, + status: str = "all", + keyword: str = "", +): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + return { + "configured": False, "items": [], "total": 0, + "filtered_total": 0, "page": 1, "page_size": page_size, "total_pages": 1, + } + try: + def _fetch(): + all_accounts = _sub2api_list_all(cfg) + kw = keyword.strip().lower() + filtered = [] + for a in all_accounts: + a_status = str(a.get("status", "")).lower() + if status == "normal" and _is_abnormal(a_status): + continue + if status == "abnormal" and not _is_abnormal(a_status): + continue + if status == "error" and a_status != "error": + continue + if status == "disabled" and a_status != "disabled": + continue + if kw: + email, _ = _account_identity(a) + name = str(a.get("name", "")).lower() + a_id = str(a.get("id", "")) + if kw not in email and kw not in name and kw not in a_id: + continue + filtered.append(a) + + filtered.sort( + key=lambda a: str(a.get("updated_at") or a.get("updatedAt") or ""), + reverse=True, + ) + filtered_total = len(filtered) + pg = max(1, page) + ps = max(1, min(page_size, 200)) + start = (pg - 1) * ps + page_items = filtered[start:start + ps] + total_pages = max(1, (filtered_total + ps - 1) // ps) + + result = [] + for a in page_items: + email, _ = _account_identity(a) + try: + acc_id = int(a.get("id") or 0) + except Exception: + acc_id = 0 + result.append({ + "id": acc_id, + "email": email or str(a.get("name", "")), + "name": str(a.get("name", "")), + "status": str(a.get("status", "unknown")).lower(), + "updated_at": a.get("updated_at") or a.get("updatedAt") or "", + "created_at": a.get("created_at") or a.get("createdAt") or "", + "is_duplicate": False, + }) + return { + "configured": True, + "items": result, + "total": len(all_accounts), + "filtered_total": filtered_total, + "page": pg, + "page_size": ps, + "total_pages": total_pages, + } + + return await run_in_threadpool(_fetch) + except Exception as e: + return { + "configured": True, "error": str(e), + "items": [], "total": 0, "filtered_total": 0, + "page": 1, "page_size": page_size, "total_pages": 1, + } + + +class ProbeRequest(BaseModel): + account_ids: List[int] = [] + timeout: int = 30 + + +@app.post("/api/sub2api/accounts/probe") +async def sub2api_accounts_probe(req: ProbeRequest): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + ids = [i for i in req.account_ids if isinstance(i, int) and i > 0] + if not ids: + raise HTTPException(status_code=400, detail="请提供账号 ID 列表") + + def _probe(): + result = _parallel_run(lambda i: _sub2api_refresh_account(cfg, i), ids, 8) + if result["ok"]: + time.sleep(2) + return { + "requested": len(ids), + "refreshed_ok": len(result["ok"]), + "refreshed_fail": len(result["fail"]), + "recovered": len(result["ok"]), + "still_abnormal": len(result["fail"]), + } + + return await run_in_threadpool(_probe) + + +class HandleExceptionRequest(BaseModel): + account_ids: List[int] = [] + timeout: int = 30 + delete_unresolved: bool = True + + +@app.post("/api/sub2api/accounts/handle-exception") +async def sub2api_handle_exception(req: HandleExceptionRequest): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + + def _handle(): + ids = [i for i in req.account_ids if isinstance(i, int) and i > 0] + if not ids: + all_acc = _sub2api_list_all(cfg) + ids = [ + int(a.get("id") or 0) for a in all_acc + if _is_abnormal(a.get("status")) and int(a.get("id") or 0) > 0 + ] + targeted = len(ids) + refreshed = _parallel_run(lambda i: _sub2api_refresh_account(cfg, i), ids, 8) + if refreshed["ok"]: + time.sleep(2) + deleted_ok = deleted_fail = 0 + if req.delete_unresolved and refreshed["fail"]: + deleted = _parallel_run(lambda i: _sub2api_delete_account(cfg, i), refreshed["fail"], 12) + deleted_ok = len(deleted["ok"]) + deleted_fail = len(deleted["fail"]) + return { + "targeted": targeted, + "refreshed_ok": len(refreshed["ok"]), + "refreshed_fail": len(refreshed["fail"]), + "recovered": len(refreshed["ok"]), + "remaining_abnormal": max(0, len(refreshed["fail"]) - deleted_ok), + "deleted_ok": deleted_ok, + "deleted_fail": deleted_fail, + } + + return await run_in_threadpool(_handle) + + +class DeleteRequest(BaseModel): + account_ids: List[int] = [] + timeout: int = 20 + + +@app.post("/api/sub2api/accounts/delete") +async def sub2api_accounts_delete(req: DeleteRequest): + cfg = _load_config() + if not str(cfg.get("sub2api_base_url", "") or "").strip(): + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + ids = [i for i in req.account_ids if isinstance(i, int) and i > 0] + if not ids: + raise HTTPException(status_code=400, detail="请提供账号 ID 列表") + + result = await run_in_threadpool( + lambda: _parallel_run(lambda i: _sub2api_delete_account(cfg, i), ids, 12) + ) + return { + "requested": len(ids), + "deleted_ok": len(result["ok"]), + "deleted_fail": len(result["fail"]), + "deleted_ok_ids": result["ok"], + "failed_ids": result["fail"], + } + + +# ============================================================ +# Entrypoint +# ============================================================ +if __name__ == "__main__": + print("=" * 50) + print(" ChatGPT Register 管理界面") + print(" 访问: http://localhost:18421") + print(" 按 Ctrl+C 退出") + print("=" * 50) + try: + uvicorn.run(app, host="0.0.0.0", port=18421, log_level="warning") + except KeyboardInterrupt: + pass + finally: + if _task_process and _task_process.is_alive(): + _task_process.kill() + sys.exit(0) diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/web/app.js b/GPT_register+duckmail+CPA+autouploadsub2api/web/app.js new file mode 100644 index 0000000..b7bbd49 --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/web/app.js @@ -0,0 +1,1018 @@ +/** + * ChatGPT Register — Web UI + */ + +// ========================================== +// 状态 +// ========================================== +const state = { + task: { status: 'idle', run_id: null, revision: -1 }, + stats: { success: 0, fail: 0, total: 0 }, + ui: { + autoScroll: true, + logCount: 0, + eventSource: null, + sub2apiAccounts: [], + sub2apiAccountFilter: { status: 'all', keyword: '' }, + sub2apiAccountPager: { page: 1, pageSize: 20, total: 0, filteredTotal: 0, totalPages: 1 }, + selectedSub2ApiAccountIds: new Set(), + sub2apiAccountsLoading: false, + sub2apiAccountActionBusy: false, + tokens: [], + }, +}; + +const $ = id => document.getElementById(id); +const DOM = {}; + +const STATUS_LABEL_MAP = { + idle: '空闲', starting: '启动中', running: '运行中', + stopping: '停止中', stopped: '已停止', +}; + +const SUB2API_ABNORMAL = new Set(['error', 'disabled']); + +// ========================================== +// 初始化 +// ========================================== +document.addEventListener('DOMContentLoaded', () => { + Object.assign(DOM, { + statusBadge: $('statusBadge'), statusText: $('statusText'), statusDot: $('statusDot'), + btnStart: $('btnStart'), btnStop: $('btnStop'), + statSuccess: $('statSuccess'), statFail: $('statFail'), statTotal: $('statTotal'), + logBody: $('logBody'), logCount: $('logCount'), + clearLogBtn: $('clearLogBtn'), progressFill: $('progressFill'), + segmentIndicator: $('segmentIndicator'), + autoScrollCheck: $('autoScrollCheck'), + multithreadCheck: $('multithreadCheck'), threadCountInput: $('threadCountInput'), + totalAccountsInput: $('totalAccountsInput'), + themeToggleBtn: $('themeToggleBtn'), + sidebarTokenList: $('sidebarTokenList'), + tokenRefreshBtn: $('tokenRefreshBtn'), + // Sub2Api pool + headerSub2apiChip: $('headerSub2apiChip'), + headerSub2apiLabel: $('headerSub2apiLabel'), + headerSub2apiDelta: $('headerSub2apiDelta'), + headerSub2apiBar: $('headerSub2apiBar'), + sub2apiPoolTotal: $('sub2apiPoolTotal'), sub2apiPoolNormal: $('sub2apiPoolNormal'), + sub2apiPoolError: $('sub2apiPoolError'), sub2apiPoolThreshold: $('sub2apiPoolThreshold'), + sub2apiPoolPercent: $('sub2apiPoolPercent'), + sub2apiPoolRefreshBtn: $('sub2apiPoolRefreshBtn'), + sub2apiPoolMaintainBtn: $('sub2apiPoolMaintainBtn'), + sub2apiPoolMaintainStatus: $('sub2apiPoolMaintainStatus'), + sub2apiAccountStatusFilter: $('sub2apiAccountStatusFilter'), + sub2apiAccountKeyword: $('sub2apiAccountKeyword'), + sub2apiAccountApplyBtn: $('sub2apiAccountApplyBtn'), + sub2apiAccountResetBtn: $('sub2apiAccountResetBtn'), + sub2apiAccountSelectAll: $('sub2apiAccountSelectAll'), + sub2apiAccountSelection: $('sub2apiAccountSelection'), + sub2apiAccountProbeBtn: $('sub2apiAccountProbeBtn'), + sub2apiAccountExceptionBtn: $('sub2apiAccountExceptionBtn'), + sub2apiDuplicateScanBtn: $('sub2apiDuplicateScanBtn'), + sub2apiDuplicateCleanBtn: $('sub2apiDuplicateCleanBtn'), + sub2apiAccountDeleteBtn: $('sub2apiAccountDeleteBtn'), + sub2apiAccountList: $('sub2apiAccountList'), + sub2apiAccountActionStatus: $('sub2apiAccountActionStatus'), + sub2apiAccountPrevBtn: $('sub2apiAccountPrevBtn'), + sub2apiAccountNextBtn: $('sub2apiAccountNextBtn'), + sub2apiAccountPageInfo: $('sub2apiAccountPageInfo'), + sub2apiAccountPageSize: $('sub2apiAccountPageSize'), + // Config + duckmailApiBase: $('duckmailApiBase'), duckmailBearer: $('duckmailBearer'), + duckmailSaveBtn: $('duckmailSaveBtn'), duckmailStatus: $('duckmailStatus'), + proxyListUrl: $('proxyListUrl'), + proxyInput: $('proxyInput'), stableProxyInput: $('stableProxyInput'), + proxyValidateTimeout: $('proxyValidateTimeout'), proxyValidateWorkers: $('proxyValidateWorkers'), + proxyValidateEnabled: $('proxyValidateEnabled'), preferStableProxy: $('preferStableProxy'), + proxySaveBtn: $('proxySaveBtn'), proxyStatus: $('proxyStatus'), + sub2apiBaseUrl: $('sub2apiBaseUrl'), sub2apiMinCandidates: $('sub2apiMinCandidates'), + sub2apiEmail: $('sub2apiEmail'), sub2apiPassword: $('sub2apiPassword'), + sub2apiGroupIds: $('sub2apiGroupIds'), autoUploadSub2api: $('autoUploadSub2api'), + sub2apiTestBtn: $('sub2apiTestBtn'), sub2apiSaveBtn: $('sub2apiSaveBtn'), + sub2apiConfigStatus: $('sub2apiConfigStatus'), + }); + + connectSSE(); + loadConfig(); + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts(); + loadTokens(); + initThemeSwitch(); + initCollapsibles(); + initTabs(); + + DOM.btnStart.addEventListener('click', startTask); + DOM.btnStop.addEventListener('click', stopTask); + DOM.clearLogBtn.addEventListener('click', clearLog); + if (DOM.tokenRefreshBtn) DOM.tokenRefreshBtn.addEventListener('click', loadTokens); + + if (DOM.sub2apiPoolRefreshBtn) DOM.sub2apiPoolRefreshBtn.addEventListener('click', () => { pollSub2ApiPoolStatus(); loadSub2ApiAccounts(); }); + if (DOM.sub2apiPoolMaintainBtn) DOM.sub2apiPoolMaintainBtn.addEventListener('click', triggerSub2ApiMaintenance); + if (DOM.sub2apiAccountApplyBtn) DOM.sub2apiAccountApplyBtn.addEventListener('click', applySub2ApiAccountFilter); + if (DOM.sub2apiAccountResetBtn) DOM.sub2apiAccountResetBtn.addEventListener('click', resetSub2ApiAccountFilter); + if (DOM.sub2apiAccountKeyword) DOM.sub2apiAccountKeyword.addEventListener('keydown', e => { if (e.key === 'Enter') applySub2ApiAccountFilter(); }); + if (DOM.sub2apiAccountPrevBtn) DOM.sub2apiAccountPrevBtn.addEventListener('click', () => changeSub2ApiAccountPage(-1)); + if (DOM.sub2apiAccountNextBtn) DOM.sub2apiAccountNextBtn.addEventListener('click', () => changeSub2ApiAccountPage(1)); + if (DOM.sub2apiAccountPageSize) DOM.sub2apiAccountPageSize.addEventListener('change', changeSub2ApiAccountPageSize); + if (DOM.sub2apiAccountSelectAll) DOM.sub2apiAccountSelectAll.addEventListener('change', toggleSelectAllSub2ApiAccounts); + if (DOM.sub2apiAccountProbeBtn) DOM.sub2apiAccountProbeBtn.addEventListener('click', triggerSelectedSub2ApiProbe); + if (DOM.sub2apiAccountExceptionBtn) DOM.sub2apiAccountExceptionBtn.addEventListener('click', triggerSub2ApiExceptionHandling); + if (DOM.sub2apiDuplicateScanBtn) DOM.sub2apiDuplicateScanBtn.addEventListener('click', previewSub2ApiDuplicates); + if (DOM.sub2apiDuplicateCleanBtn) DOM.sub2apiDuplicateCleanBtn.addEventListener('click', cleanupSub2ApiDuplicates); + if (DOM.sub2apiAccountDeleteBtn) DOM.sub2apiAccountDeleteBtn.addEventListener('click', triggerSelectedSub2ApiDelete); + + if (DOM.sub2apiAccountList) { + DOM.sub2apiAccountList.addEventListener('click', async e => { + const probeBtn = e.target.closest('.sub2api-account-probe-btn'); + if (probeBtn) { await runSub2ApiAccountProbe([parseInt(probeBtn.dataset.accountId, 10)], `账号 ${probeBtn.dataset.accountId}`); return; } + const deleteBtn = e.target.closest('.sub2api-account-delete-btn'); + if (deleteBtn) await runSub2ApiAccountDelete([parseInt(deleteBtn.dataset.accountId, 10)], decodeURIComponent(deleteBtn.dataset.email || '')); + }); + DOM.sub2apiAccountList.addEventListener('change', e => { + const cb = e.target.closest('.sub2api-account-check'); + if (!cb) return; + const id = parseInt(cb.dataset.accountId, 10); + if (cb.checked) state.ui.selectedSub2ApiAccountIds.add(id); + else state.ui.selectedSub2ApiAccountIds.delete(id); + const row = cb.closest('.sub2api-account-item'); + if (row) row.classList.toggle('selected', cb.checked); + refreshSub2ApiSelectionState(); + }); + } + + DOM.logBody.addEventListener('scroll', () => { + const el = DOM.logBody; + state.ui.autoScroll = el.scrollTop + el.clientHeight >= el.scrollHeight - 20; + if (DOM.autoScrollCheck) DOM.autoScrollCheck.checked = state.ui.autoScroll; + }); + if (DOM.autoScrollCheck) { + DOM.autoScrollCheck.addEventListener('change', () => { + state.ui.autoScroll = DOM.autoScrollCheck.checked; + if (state.ui.autoScroll) DOM.logBody.scrollTop = DOM.logBody.scrollHeight; + }); + } + + if (DOM.duckmailSaveBtn) DOM.duckmailSaveBtn.addEventListener('click', saveDuckmailConfig); + if (DOM.proxySaveBtn) DOM.proxySaveBtn.addEventListener('click', saveProxyConfig); + if (DOM.sub2apiTestBtn) DOM.sub2apiTestBtn.addEventListener('click', testSub2ApiPoolConnection); + if (DOM.sub2apiSaveBtn) DOM.sub2apiSaveBtn.addEventListener('click', saveSub2ApiConfig); + + setInterval(pollSub2ApiPoolStatus, 30000); + setInterval(() => loadSub2ApiAccounts({ silent: true }), 60000); + setInterval(loadTokens, 60000); +}); + +// ========================================== +// Tab 导航 +// ========================================== +function initTabs() { + document.querySelectorAll('.tab-btn').forEach((btn, index) => { + btn.addEventListener('click', () => switchMainTab(btn.dataset.tab)); + }); + switchMainTab('tabDashboard'); +} + +function switchMainTab(tabId) { + const next = tabId === 'tabConfig' ? 'tabConfig' : 'tabDashboard'; + document.querySelectorAll('.tab-btn').forEach((btn, i) => { + const active = btn.dataset.tab === next; + btn.classList.toggle('active', active); + btn.setAttribute('aria-selected', active ? 'true' : 'false'); + if (active && DOM.segmentIndicator) DOM.segmentIndicator.setAttribute('data-active', String(i)); + }); + document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === next)); +} + +function initCollapsibles() { + document.querySelectorAll('.collapsible-trigger').forEach(trigger => { + trigger.addEventListener('click', () => { + const section = trigger.closest('.collapsible'); + if (!section) return; + const body = section.querySelector('.collapsible-body'); + if (!body) return; + const isOpen = section.classList.contains('open'); + section.classList.toggle('open', !isOpen); + body.style.display = isOpen ? 'none' : 'block'; + }); + }); +} + +// ========================================== +// SSE +// ========================================== +function connectSSE() { + if (state.ui.eventSource) state.ui.eventSource.close(); + const es = new EventSource('/api/logs'); + state.ui.eventSource = es; + + const handle = (sourceType, raw) => { + try { + const payload = raw?.data ? JSON.parse(raw.data) : {}; + if (!payload.type && sourceType !== 'message') payload.type = sourceType; + applySseEvent(payload); + } catch {} + }; + + ['connected', 'snapshot', 'task.updated', 'stats.updated', 'log.appended'].forEach(name => { + es.addEventListener(name, e => handle(name, e)); + }); + es.onmessage = e => handle('message', e); + es.onerror = () => setTimeout(connectSSE, 3000); +} + +function applySseEvent(event) { + if (!event || typeof event !== 'object') return; + const type = String(event.type || ''); + + if (type === 'connected') { + appendLog({ ts: event.ts || '', level: 'connected', message: '实时事件已连接' }); + if (event.snapshot) applySnapshot(event.snapshot); + return; + } + if (type === 'log.appended') { + const log = event.log && typeof event.log === 'object' ? event.log : event; + appendLog(log); + // Count success/fail from log messages + if (log.level === 'success' && log.message && log.message.includes('注册成功')) { + state.stats.success++; + state.stats.total = state.stats.success + state.stats.fail; + syncStatsUI(); + } else if (log.level === 'error' && log.message && (log.message.includes('失败') || log.message.includes('FAIL'))) { + // Don't double count + } + return; + } + if (type === 'task.updated') { + if (event.task) state.task = { ...state.task, ...event.task }; + if (event.stats) { state.stats = { ...state.stats, ...event.stats }; syncStatsUI(); } + syncTaskChrome(); + return; + } + if (type === 'snapshot' || (event.task && event.stats)) { + applySnapshot(event.snapshot || event); + } +} + +function applySnapshot(snap) { + if (!snap) return; + if (snap.task) state.task = { ...state.task, ...snap.task }; + if (snap.stats) { state.stats = { ...state.stats, ...snap.stats }; } + syncTaskChrome(); + syncStatsUI(); +} + +// ========================================== +// Log rendering +// ========================================== +const LEVEL_ICON = { info: '›', success: '✓', error: '✗', warn: '⚠', connected: '⟳' }; + +function appendLog(event) { + const { ts, level, message, step } = event; + state.ui.logCount++; + const entry = document.createElement('div'); + entry.className = 'log-entry'; + entry.innerHTML = ` + ${escapeHtml(ts || '')} + ${LEVEL_ICON[level] || '·'} + ${escapeHtml(message || '')} + ${step ? `${escapeHtml(step)}` : ''} + `; + DOM.logBody.appendChild(entry); + DOM.logCount.textContent = state.ui.logCount; + if (state.ui.autoScroll) DOM.logBody.scrollTop = DOM.logBody.scrollHeight; + if (DOM.logBody.children.length > 2000) DOM.logBody.firstElementChild.remove(); +} + +function clearLog() { + DOM.logBody.innerHTML = ''; + state.ui.logCount = 0; + DOM.logCount.textContent = '0'; +} + +// ========================================== +// Task control +// ========================================== +function getWorkerCount() { + if (!DOM.multithreadCheck?.checked) return 1; + return Math.max(1, parseInt(DOM.threadCountInput?.value || '1', 10) || 1); +} + +function getTotalAccounts() { + return Math.max(1, parseInt(DOM.totalAccountsInput?.value || '3', 10) || 3); +} + +async function startTask() { + try { + state.stats = { success: 0, fail: 0, total: 0 }; + syncStatsUI(); + const res = await fetch('/api/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ worker_count: getWorkerCount(), total_accounts: getTotalAccounts() }), + }); + const data = await res.json(); + if (!res.ok) { showToast(data.detail || '启动失败', 'error'); return; } + applySnapshot(data); + showToast(`注册任务已启动 (${getTotalAccounts()} 个账号, ${getWorkerCount()} 线程)`, 'success'); + } catch (e) { + showToast('启动失败: ' + e.message, 'error'); + } +} + +async function stopTask() { + try { + const res = await fetch('/api/stop', { method: 'POST' }); + const data = await res.json(); + if (!res.ok) { showToast(data.detail || '停止失败', 'error'); return; } + applySnapshot(data); + showToast('停止指令已发送', 'info'); + } catch (e) { + showToast('停止失败: ' + e.message, 'error'); + } +} + +function syncTaskChrome() { + const status = state.task.status || 'idle'; + DOM.statusBadge.className = `status-badge ${status}`; + DOM.statusText.textContent = STATUS_LABEL_MAP[status] || status; + + const isRunning = status === 'running'; + const isStopping = status === 'stopping'; + const canStop = isRunning; + DOM.btnStart.disabled = isRunning || isStopping; + DOM.btnStop.disabled = !canStop; + DOM.progressFill.className = isRunning ? 'progress-fill running' : (isStopping ? 'progress-fill stopping' : 'progress-fill'); +} + +function syncStatsUI() { + if (DOM.statSuccess) DOM.statSuccess.textContent = state.stats.success; + if (DOM.statFail) DOM.statFail.textContent = state.stats.fail; + if (DOM.statTotal) DOM.statTotal.textContent = state.stats.total; +} + +// ========================================== +// Tokens (sidebar) +// ========================================== +async function loadTokens() { + try { + const res = await fetch('/api/tokens'); + const data = await res.json(); + state.ui.tokens = data.tokens || []; + renderSidebarTokens(); + } catch {} +} + +function renderSidebarTokens() { + const el = DOM.sidebarTokenList; + if (!el) return; + const tokens = state.ui.tokens || []; + if (!tokens.length) { + el.innerHTML = '
暂无 Token
'; + return; + } + el.innerHTML = tokens.slice(0, 50).map(t => { + const platforms = Array.isArray(t.uploaded_platforms) ? t.uploaded_platforms : []; + const badge = platforms.includes('sub2api') + ? '已上传' + : ''; + return `
+ ${escapeHtml(t.email || t.filename)} + ${badge} +
`; + }).join(''); +} + +// ========================================== +// Sub2Api Pool Status +// ========================================== +async function pollSub2ApiPoolStatus() { + try { + const res = await fetch('/api/sub2api/pool/status'); + const data = await res.json(); + + if (!data.configured) { + ['sub2apiPoolTotal', 'sub2apiPoolNormal', 'sub2apiPoolError', 'sub2apiPoolThreshold', 'sub2apiPoolPercent'].forEach(id => { + const el = $(id); if (el) el.textContent = '--'; + }); + updateHeaderSub2Api(null); + return; + } + + const normal = data.candidates || 0; + const error = data.error_count || 0; + const total = data.total || 0; + const threshold = data.threshold || 0; + const fillPct = threshold > 0 ? Math.round(normal / threshold * 100) : 100; + + if (DOM.sub2apiPoolTotal) DOM.sub2apiPoolTotal.textContent = total; + if (DOM.sub2apiPoolNormal) DOM.sub2apiPoolNormal.textContent = normal; + if (DOM.sub2apiPoolError) { + DOM.sub2apiPoolError.textContent = error; + DOM.sub2apiPoolError.className = `stat-value ${error > 0 ? 'red' : 'green'}`; + } + if (DOM.sub2apiPoolThreshold) DOM.sub2apiPoolThreshold.textContent = threshold; + if (DOM.sub2apiPoolPercent) { + DOM.sub2apiPoolPercent.textContent = fillPct + '%'; + DOM.sub2apiPoolPercent.className = `stat-value ${fillPct >= 100 ? 'green' : fillPct >= 80 ? '' : 'red'}`; + } + updateHeaderSub2Api({ normal, threshold, fillPct, error }); + } catch {} +} + +function updateHeaderSub2Api(data) { + if (!data) { + if (DOM.headerSub2apiLabel) DOM.headerSub2apiLabel.textContent = '-- / --'; + if (DOM.headerSub2apiDelta) DOM.headerSub2apiDelta.textContent = '--'; + if (DOM.headerSub2apiBar) DOM.headerSub2apiBar.style.width = '0%'; + setChipStatus(DOM.headerSub2apiChip, 'idle'); + return; + } + const { normal, threshold, fillPct, error } = data; + const st = error > 0 ? 'danger' : (fillPct > 110 ? 'over' : fillPct >= 100 ? 'ok' : fillPct >= 80 ? 'warn' : 'danger'); + if (DOM.headerSub2apiLabel) DOM.headerSub2apiLabel.textContent = `${normal} / ${threshold}`; + if (DOM.headerSub2apiDelta) { + const delta = Math.round(fillPct - 100); + DOM.headerSub2apiDelta.textContent = delta === 0 ? '0%' : `${delta > 0 ? '+' : ''}${delta}%`; + DOM.headerSub2apiDelta.className = `pool-chip-delta ${st === 'idle' ? '' : st}`.trim(); + } + if (DOM.headerSub2apiBar) { + DOM.headerSub2apiBar.style.width = Math.min(100, fillPct) + '%'; + DOM.headerSub2apiBar.className = `pool-chip-fill ${st === 'idle' ? '' : st}`.trim(); + } + setChipStatus(DOM.headerSub2apiChip, st); +} + +function setChipStatus(chip, st) { + if (!chip) return; + chip.classList.remove('status-idle', 'status-warn', 'status-danger', 'status-ok', 'status-over'); + chip.classList.add(`status-${st}`); +} + +async function triggerSub2ApiMaintenance() { + DOM.sub2apiPoolMaintainBtn.disabled = true; + DOM.sub2apiPoolMaintainBtn.textContent = '维护中...'; + DOM.sub2apiPoolMaintainStatus.textContent = '正在维护...'; + try { + const res = await fetch('/api/sub2api/pool/maintain', { method: 'POST' }); + const data = await res.json(); + if (res.ok) { + const sec = (data.duration_ms / 1000).toFixed(2); + const msg = `维护完成: 异常 ${data.error_count || 0}, 刷新恢复 ${data.refreshed || 0}, 重复组 ${data.duplicate_groups || 0}, 删除 ${data.deleted_ok || 0}, ${sec}s`; + DOM.sub2apiPoolMaintainStatus.textContent = msg; + showToast(msg, 'success'); + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts({ silent: true }); + } else { + DOM.sub2apiPoolMaintainStatus.textContent = data.detail || '维护失败'; + showToast(data.detail || '维护失败', 'error'); + } + } catch (e) { + DOM.sub2apiPoolMaintainStatus.textContent = '请求失败: ' + e.message; + showToast('维护失败', 'error'); + } finally { + DOM.sub2apiPoolMaintainBtn.disabled = false; + DOM.sub2apiPoolMaintainBtn.textContent = '维护'; + } +} + +async function testSub2ApiPoolConnection() { + DOM.sub2apiTestBtn.disabled = true; + DOM.sub2apiConfigStatus.textContent = '测试中...'; + try { + const res = await fetch('/api/sub2api/pool/check', { method: 'POST' }); + const data = await res.json(); + DOM.sub2apiConfigStatus.textContent = data.message || (data.ok ? '连接成功' : '连接失败'); + showToast(data.ok ? 'Sub2Api 连接成功' : 'Sub2Api 连接失败', data.ok ? 'success' : 'error'); + } catch (e) { + DOM.sub2apiConfigStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.sub2apiTestBtn.disabled = false; + } +} + +// ========================================== +// Sub2Api Accounts +// ========================================== +function applySub2ApiAccountFilter() { + state.ui.sub2apiAccountFilter.status = DOM.sub2apiAccountStatusFilter?.value || 'all'; + state.ui.sub2apiAccountFilter.keyword = DOM.sub2apiAccountKeyword?.value.trim() || ''; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +function resetSub2ApiAccountFilter() { + state.ui.sub2apiAccountFilter = { status: 'all', keyword: '' }; + if (DOM.sub2apiAccountStatusFilter) DOM.sub2apiAccountStatusFilter.value = 'all'; + if (DOM.sub2apiAccountKeyword) DOM.sub2apiAccountKeyword.value = ''; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +async function loadSub2ApiAccounts({ silent = false } = {}) { + if (!DOM.sub2apiAccountList || state.ui.sub2apiAccountsLoading) return; + state.ui.sub2apiAccountsLoading = true; + if (!silent && DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + DOM.sub2apiAccountActionStatus.textContent = '正在加载...'; + } + try { + const p = state.ui.sub2apiAccountPager; + const f = state.ui.sub2apiAccountFilter; + const params = new URLSearchParams({ + page: String(p.page || 1), + page_size: String(p.pageSize || 20), + status: String(f.status || 'all'), + keyword: String(f.keyword || ''), + }); + const res = await fetch(`/api/sub2api/accounts?${params}`); + const data = await res.json(); + if (!data.configured) { + renderSub2ApiAccountList('请先完成 Sub2Api 平台配置'); + if (DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) + DOM.sub2apiAccountActionStatus.textContent = 'Sub2Api 未配置'; + return; + } + state.ui.sub2apiAccounts = Array.isArray(data.items) ? data.items : []; + state.ui.sub2apiAccountPager.page = parseInt(data.page, 10) || 1; + state.ui.sub2apiAccountPager.pageSize = parseInt(data.page_size, 10) || 20; + state.ui.sub2apiAccountPager.total = parseInt(data.total, 10) || 0; + state.ui.sub2apiAccountPager.filteredTotal = parseInt(data.filtered_total, 10) || 0; + state.ui.sub2apiAccountPager.totalPages = parseInt(data.total_pages, 10) || 1; + renderSub2ApiAccountList(); + if (!silent && DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + const pp = state.ui.sub2apiAccountPager; + DOM.sub2apiAccountActionStatus.textContent = `第 ${pp.page}/${pp.totalPages} 页,共 ${pp.filteredTotal} 个账号`; + } + } catch (e) { + renderSub2ApiAccountList('账号列表加载失败'); + if (DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) + DOM.sub2apiAccountActionStatus.textContent = '加载失败: ' + e.message; + } finally { + state.ui.sub2apiAccountsLoading = false; + refreshSub2ApiSelectionState(); + } +} + +function renderSub2ApiAccountList(emptyMessage = '') { + const accounts = state.ui.sub2apiAccounts || []; + const pager = state.ui.sub2apiAccountPager; + if (!DOM.sub2apiAccountList) return; + if (!accounts.length) { + const msg = emptyMessage || '暂无账号'; + DOM.sub2apiAccountList.innerHTML = `
${escapeHtml(msg)}
`; + } else { + DOM.sub2apiAccountList.innerHTML = accounts.map(renderSub2ApiAccountItem).join(''); + } + updateSub2ApiPagerUI(); + refreshSub2ApiSelectionState(); +} + +function renderSub2ApiAccountItem(account) { + const id = Number(account.id || 0); + const email = account.email || account.name || `账号 ${id}`; + const status = String(account.status || 'unknown').toLowerCase(); + const isAbnormal = SUB2API_ABNORMAL.has(status); + const selected = state.ui.selectedSub2ApiAccountIds.has(id); + const statusLabel = { error: '异常', disabled: '禁用', normal: '正常', active: '正常', unknown: '未知' }[status] || status; + const statusClass = status === 'disabled' ? 'warn' : (isAbnormal ? 'danger' : 'ok'); + return ` +
+ +
+
+ ${escapeHtml(email)} + +
+
ID: ${id} · ${escapeHtml(formatTime(account.updated_at))}
+
+
+ + +
+
`; +} + +function updateSub2ApiPagerUI() { + const { page, totalPages, pageSize } = state.ui.sub2apiAccountPager; + if (DOM.sub2apiAccountPageInfo) + DOM.sub2apiAccountPageInfo.textContent = `第 ${page}/${totalPages} 页 · 每页 ${pageSize} 条`; + if (DOM.sub2apiAccountPageSize && String(DOM.sub2apiAccountPageSize.value) !== String(pageSize)) + DOM.sub2apiAccountPageSize.value = String(pageSize); + if (DOM.sub2apiAccountPrevBtn) DOM.sub2apiAccountPrevBtn.disabled = state.ui.sub2apiAccountActionBusy || page <= 1; + if (DOM.sub2apiAccountNextBtn) DOM.sub2apiAccountNextBtn.disabled = state.ui.sub2apiAccountActionBusy || page >= totalPages; +} + +function changeSub2ApiAccountPage(delta) { + const next = (state.ui.sub2apiAccountPager.page || 1) + delta; + if (next < 1 || next > state.ui.sub2apiAccountPager.totalPages) return; + state.ui.sub2apiAccountPager.page = next; + loadSub2ApiAccounts(); +} + +function changeSub2ApiAccountPageSize() { + state.ui.sub2apiAccountPager.pageSize = parseInt(DOM.sub2apiAccountPageSize?.value || '20', 10) || 20; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +function refreshSub2ApiSelectionState() { + const accounts = state.ui.sub2apiAccounts || []; + const visibleIds = accounts.map(a => a.id).filter(id => Number.isInteger(id) && id > 0); + const selectedVisible = visibleIds.filter(id => state.ui.selectedSub2ApiAccountIds.has(id)).length; + const selectedTotal = state.ui.selectedSub2ApiAccountIds.size; + if (DOM.sub2apiAccountSelection) + DOM.sub2apiAccountSelection.textContent = `已选 ${selectedTotal} 个`; + if (DOM.sub2apiAccountSelectAll) { + DOM.sub2apiAccountSelectAll.checked = visibleIds.length > 0 && selectedVisible === visibleIds.length; + DOM.sub2apiAccountSelectAll.indeterminate = selectedVisible > 0 && selectedVisible < visibleIds.length; + } +} + +function toggleSelectAllSub2ApiAccounts() { + const shouldSelect = !!DOM.sub2apiAccountSelectAll?.checked; + (state.ui.sub2apiAccounts || []).forEach(a => { + const id = Number(a.id || 0); + if (id <= 0) return; + if (shouldSelect) state.ui.selectedSub2ApiAccountIds.add(id); + else state.ui.selectedSub2ApiAccountIds.delete(id); + }); + renderSub2ApiAccountList(); +} + +function getSelectedIds() { + return Array.from(state.ui.selectedSub2ApiAccountIds).filter(id => Number.isInteger(id) && id > 0); +} + +function setSub2ApiAccountBusy(busy) { + state.ui.sub2apiAccountActionBusy = busy; + [ + DOM.sub2apiAccountApplyBtn, DOM.sub2apiAccountResetBtn, DOM.sub2apiAccountProbeBtn, + DOM.sub2apiAccountExceptionBtn, DOM.sub2apiDuplicateScanBtn, DOM.sub2apiDuplicateCleanBtn, + DOM.sub2apiAccountDeleteBtn, DOM.sub2apiAccountPrevBtn, DOM.sub2apiAccountNextBtn, + ].forEach(btn => { if (btn) btn.disabled = busy; }); + if (!busy) updateSub2ApiPagerUI(); +} + +async function runSub2ApiAccountProbe(ids, label = '选中账号') { + if (state.ui.sub2apiAccountActionBusy) return; + if (!ids.length) { showToast('请先选择账号', 'error'); return; } + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = `正在测活 ${ids.length} 个账号...`; + try { + const res = await fetch('/api/sub2api/accounts/probe', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '测活失败'); + const msg = `${label}: 刷新成功 ${data.refreshed_ok || 0}, 恢复 ${data.recovered || 0}, 仍异常 ${data.still_abnormal || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '测活失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { setSub2ApiAccountBusy(false); } +} + +async function triggerSelectedSub2ApiProbe() { + await runSub2ApiAccountProbe(getSelectedIds()); +} + +async function triggerSub2ApiExceptionHandling() { + if (state.ui.sub2apiAccountActionBusy) return; + const ids = getSelectedIds(); + const confirmMsg = ids.length + ? `确认处理 ${ids.length} 个已选账号?先测活,仍异常则删除。` + : '未选择账号,将处理整个池中的异常账号。是否继续?'; + if (!confirm(confirmMsg)) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = '正在处理异常账号...'; + try { + const res = await fetch('/api/sub2api/accounts/handle-exception', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids, delete_unresolved: true }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '处理失败'); + const msg = `处理完成: 目标 ${data.targeted || 0}, 恢复 ${data.recovered || 0}, 删除 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '处理失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { setSub2ApiAccountBusy(false); } +} + +async function runSub2ApiAccountDelete(ids, label = '选中账号', requireConfirm = true) { + if (state.ui.sub2apiAccountActionBusy) return; + if (!ids.length) { showToast('请先选择账号', 'error'); return; } + if (requireConfirm && !confirm(`确认删除 ${label}(共 ${ids.length} 个)?`)) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = `正在删除 ${ids.length} 个账号...`; + try { + const res = await fetch('/api/sub2api/accounts/delete', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '删除失败'); + ids.forEach(id => state.ui.selectedSub2ApiAccountIds.delete(id)); + const msg = `删除完成: 成功 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '删除失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { setSub2ApiAccountBusy(false); } +} + +async function triggerSelectedSub2ApiDelete() { + await runSub2ApiAccountDelete(getSelectedIds()); +} + +async function previewSub2ApiDuplicates() { + if (state.ui.sub2apiAccountActionBusy) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = '正在检测重复账号...'; + try { + const res = await fetch('/api/sub2api/pool/dedupe', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dry_run: true }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '检测失败'); + const msg = `重复预检: 重复组 ${data.duplicate_groups || 0}, 重复账号 ${data.duplicate_accounts || 0}, 可删 ${data.to_delete || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + } catch (e) { + const msg = '检测失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { setSub2ApiAccountBusy(false); } +} + +async function cleanupSub2ApiDuplicates() { + if (state.ui.sub2apiAccountActionBusy) return; + if (!confirm('确认清理重复账号?将保留每组最新账号,其余删除。')) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = '正在清理重复账号...'; + try { + const res = await fetch('/api/sub2api/pool/dedupe', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dry_run: false }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '清理失败'); + const msg = `重复清理完成: 删除成功 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '清理失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { setSub2ApiAccountBusy(false); } +} + +// ========================================== +// Config +// ========================================== +async function loadConfig() { + try { + const res = await fetch('/api/config'); + const cfg = await res.json(); + if (DOM.duckmailApiBase) DOM.duckmailApiBase.value = cfg.duckmail_api_base || ''; + if (DOM.proxyListUrl) DOM.proxyListUrl.value = cfg.proxy_list_url || ''; + if (DOM.proxyInput) DOM.proxyInput.value = cfg.proxy || ''; + if (DOM.stableProxyInput) DOM.stableProxyInput.value = cfg.stable_proxy || ''; + if (DOM.proxyValidateTimeout) DOM.proxyValidateTimeout.value = cfg.proxy_validate_timeout_seconds || 6; + if (DOM.proxyValidateWorkers) DOM.proxyValidateWorkers.value = cfg.proxy_validate_workers || 40; + if (DOM.proxyValidateEnabled) DOM.proxyValidateEnabled.checked = cfg.proxy_validate_enabled !== false; + if (DOM.preferStableProxy) DOM.preferStableProxy.checked = cfg.prefer_stable_proxy !== false; + if (DOM.sub2apiBaseUrl) DOM.sub2apiBaseUrl.value = cfg.sub2api_base_url || ''; + if (DOM.sub2apiMinCandidates) DOM.sub2apiMinCandidates.value = cfg.sub2api_min_candidates || 200; + if (DOM.sub2apiEmail) DOM.sub2apiEmail.value = cfg.sub2api_email || ''; + if (DOM.sub2apiGroupIds) { + const ids = Array.isArray(cfg.sub2api_group_ids) ? cfg.sub2api_group_ids.join(',') : String(cfg.sub2api_group_ids || ''); + DOM.sub2apiGroupIds.value = ids; + } + if (DOM.autoUploadSub2api) DOM.autoUploadSub2api.checked = !!cfg.auto_upload_sub2api; + if (DOM.totalAccountsInput) DOM.totalAccountsInput.value = cfg.total_accounts || 3; + } catch {} +} + +async function saveDuckmailConfig() { + DOM.duckmailSaveBtn.disabled = true; + try { + const body = { duckmail_api_base: DOM.duckmailApiBase?.value.trim() || '' }; + const bearer = DOM.duckmailBearer?.value.trim(); + if (bearer) body.duckmail_bearer = bearer; + const res = await fetch('/api/config', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (res.ok) { + showToast('DuckMail 配置已保存', 'success'); + if (DOM.duckmailStatus) DOM.duckmailStatus.textContent = '已保存'; + if (DOM.duckmailBearer) DOM.duckmailBearer.value = ''; + } else { + showToast('保存失败', 'error'); + } + } catch (e) { showToast('保存失败: ' + e.message, 'error'); } + finally { DOM.duckmailSaveBtn.disabled = false; } +} + +async function saveProxyConfig() { + DOM.proxySaveBtn.disabled = true; + try { + const body = { + proxy_list_url: DOM.proxyListUrl?.value.trim() || '', + proxy: DOM.proxyInput?.value.trim() || '', + stable_proxy: DOM.stableProxyInput?.value.trim() || '', + proxy_validate_timeout_seconds: parseFloat(DOM.proxyValidateTimeout?.value || '6') || 6, + proxy_validate_workers: parseInt(DOM.proxyValidateWorkers?.value || '40', 10) || 40, + proxy_validate_enabled: DOM.proxyValidateEnabled?.checked !== false, + prefer_stable_proxy: DOM.preferStableProxy?.checked !== false, + }; + const res = await fetch('/api/config', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (res.ok) { + showToast('代理配置已保存', 'success'); + if (DOM.proxyStatus) DOM.proxyStatus.textContent = '已保存'; + } else { + showToast('保存失败', 'error'); + } + } catch (e) { showToast('保存失败: ' + e.message, 'error'); } + finally { DOM.proxySaveBtn.disabled = false; } +} + +async function saveSub2ApiConfig() { + DOM.sub2apiSaveBtn.disabled = true; + try { + const groupIdsRaw = DOM.sub2apiGroupIds?.value.trim() || '2'; + const groupIds = groupIdsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => Number.isFinite(n) && n > 0); + const body = { + sub2api_base_url: DOM.sub2apiBaseUrl?.value.trim() || '', + sub2api_email: DOM.sub2apiEmail?.value.trim() || '', + sub2api_min_candidates: parseInt(DOM.sub2apiMinCandidates?.value || '200', 10) || 200, + sub2api_group_ids: groupIds.length ? groupIds : [2], + auto_upload_sub2api: DOM.autoUploadSub2api?.checked || false, + }; + const pwd = DOM.sub2apiPassword?.value.trim(); + if (pwd && pwd !== '**masked**') body.sub2api_password = pwd; + const res = await fetch('/api/config', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (res.ok) { + showToast('Sub2Api 配置已保存', 'success'); + if (DOM.sub2apiConfigStatus) DOM.sub2apiConfigStatus.textContent = '已保存'; + if (DOM.sub2apiPassword) DOM.sub2apiPassword.value = ''; + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts(); + } else { + const data = await res.json(); + showToast(data.detail || '保存失败', 'error'); + } + } catch (e) { showToast('保存失败: ' + e.message, 'error'); } + finally { DOM.sub2apiSaveBtn.disabled = false; } +} + +// ========================================== +// Theme +// ========================================== +const THEME_KEY = 'chatgpt_register_theme_v1'; + +function initThemeSwitch() { + const btn = DOM.themeToggleBtn; + if (!btn) return; + let saved = 'dark'; + try { const v = localStorage.getItem(THEME_KEY); if (v === 'light' || v === 'dark') saved = v; } catch {} + applyTheme(saved); + btn.addEventListener('click', () => { + const next = document.body.classList.contains('theme-light') ? 'dark' : 'light'; + applyTheme(next); + try { localStorage.setItem(THEME_KEY, next); } catch {} + }); +} + +function applyTheme(theme) { + const isLight = theme === 'light'; + document.body.classList.toggle('theme-light', isLight); + const btn = DOM.themeToggleBtn; + if (!btn) return; + const lbl = btn.querySelector('.theme-toggle-label'); + if (lbl) lbl.textContent = isLight ? '明亮' : '黑暗'; + btn.setAttribute('aria-label', `切换到${isLight ? '黑暗' : '明亮'}主题`); +} + +// ========================================== +// Toast +// ========================================== +const TOAST_ICONS = { success: '✓', error: '✗', info: 'ℹ' }; + +function showToast(msg, type = 'info') { + const container = $('toastContainer'); + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = `${TOAST_ICONS[type] || TOAST_ICONS.info}${escapeHtml(msg)}`; + container.appendChild(toast); + setTimeout(() => { + toast.style.animation = 'toast-out .25s var(--ease-spring) forwards'; + toast.addEventListener('animationend', () => toast.remove()); + }, 3200); +} + +// ========================================== +// Utils +// ========================================== +function escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +} + +function formatTime(timeStr) { + if (!timeStr) return '--'; + try { + const d = new Date(timeStr); + if (isNaN(d)) return timeStr; + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch { return timeStr; } +} + +// ========================================== +// Drag resize + localStorage +// ========================================== +(function initResizable() { + const STORAGE_KEY = 'chatgpt_register_layout_v1'; + const shell = document.querySelector('.app-shell'); + const resizeLeft = document.getElementById('resizeLeft'); + const resizeRight = document.getElementById('resizeRight'); + if (!shell) return; + + function getTrackPx(index) { + const tracks = getComputedStyle(shell).gridTemplateColumns.match(/[\d.]+px/g) || []; + const val = tracks[index] ? parseFloat(tracks[index]) : NaN; + return Number.isFinite(val) ? val : NaN; + } + + function loadLayout() { + try { + const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (!saved) return; + const maxW = shell.getBoundingClientRect().width || window.innerWidth; + if (saved.left >= 200 && saved.left <= maxW * 0.4) shell.style.setProperty('--col-left', saved.left + 'px'); + if (saved.right >= 240 && saved.right <= maxW * 0.4) shell.style.setProperty('--col-right', saved.right + 'px'); + } catch {} + } + + function saveLayout() { + const data = {}; + const left = getTrackPx(0); if (Number.isFinite(left) && left > 0) data.left = left; + const right = getTrackPx(4); if (Number.isFinite(right) && right > 0) data.right = right; + if (Object.keys(data).length) try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {} + } + + function initHandle(handle, prop, minW, getStart) { + if (!handle) return; + handle.addEventListener('mousedown', e => { + e.preventDefault(); + document.body.classList.add('resizing'); + handle.classList.add('active'); + const startX = e.clientX; + const startVal = getStart(); + const totalW = shell.getBoundingClientRect().width; + const onMove = ev => { + const delta = prop === '--col-left' ? ev.clientX - startX : startX - ev.clientX; + shell.style.setProperty(prop, Math.max(minW, Math.min(startVal + delta, totalW * 0.4)) + 'px'); + }; + const onUp = () => { + document.body.classList.remove('resizing'); + handle.classList.remove('active'); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + saveLayout(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } + + initHandle(resizeLeft, '--col-left', 200, () => getTrackPx(0) || 260); + initHandle(resizeRight, '--col-right', 240, () => getTrackPx(4) || 340); + loadLayout(); +})(); diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/web/index.html b/GPT_register+duckmail+CPA+autouploadsub2api/web/index.html new file mode 100644 index 0000000..b9429b9 --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/web/index.html @@ -0,0 +1,389 @@ + + + + + + + ChatGPT Register + + + + + + +
+
+ +

ChatGPT Register

+
+ +
+
+ Sub2Api + -- / -- + -- +
+
+
+
+
+ + 空闲 +
+ +
+
+ + +
+
+
+ + +
+
+ + + + + +
+ + +
+
+
+
+ + 实时日志 + 0 +
+
+ + +
+
+
+
等待任务启动...
+
+
+
+ + +
+ + + + +
+
+ + +
+
+ + +
+
+ DuckMail 邮箱配置 + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ 代理配置 + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ Sub2Api 平台配置 + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+ + + +
+
+
+ +
+
+ + +
+ + + + + diff --git a/GPT_register+duckmail+CPA+autouploadsub2api/web/style.css b/GPT_register+duckmail+CPA+autouploadsub2api/web/style.css new file mode 100644 index 0000000..abfe30c --- /dev/null +++ b/GPT_register+duckmail+CPA+autouploadsub2api/web/style.css @@ -0,0 +1,2203 @@ +/* ========================================== + OpenAI Pool Orchestrator — iOS Flat Design v2.1 + Modern · Flat · iOS Switch Style + ========================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); + +/* ---------- Design Tokens ---------- */ +:root { + /* Surfaces — layered depth */ + --bg-base: #000000; + --bg-surface: #1c1c1e; + --bg-card: #2c2c2e; + --bg-elevated: #3a3a3c; + --bg-hover: rgba(255,255,255,.06); + --bg-inset: rgba(0,0,0,.25); + + /* Borders — subtle definition */ + --border: rgba(255,255,255,.08); + --border-card: rgba(255,255,255,.06); + --border-focused: rgba(10,132,255,.5); + --separator: rgba(255,255,255,.05); + + /* iOS System Colors */ + --accent-blue: #0a84ff; + --accent-blue-dim: rgba(10,132,255,.15); + --accent-green: #30d158; + --accent-green-dim: rgba(48,209,88,.12); + --accent-red: #ff453a; + --accent-red-dim: rgba(255,69,58,.12); + --accent-yellow: #ffd60a; + --accent-orange: #ff9f0a; + --accent-orange-dim: rgba(255,159,10,.12); + --accent-purple: #bf5af2; + --accent-teal: #64d2ff; + --accent-teal-dim: rgba(100,210,255,.12); + + /* Text */ + --text-primary: rgba(255,255,255,.92); + --text-secondary: rgba(255,255,255,.55); + --text-muted: rgba(255,255,255,.30); + + /* Radius — iOS rounded corners */ + --radius-xs: 6px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + --radius-pill: 999px; + + /* Shadows */ + --shadow-card: 0 1px 3px rgba(0,0,0,.3), 0 0 0 1px var(--border-card); + --shadow-card-hover: 0 4px 16px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.1); + --shadow-elevated: 0 8px 30px rgba(0,0,0,.5); + --shadow-button: 0 1px 2px rgba(0,0,0,.3); + --shadow-glow-blue: 0 0 20px rgba(10,132,255,.15); + --shadow-glow-green: 0 0 20px rgba(48,209,88,.15); + --shadow-glow-red: 0 0 20px rgba(255,69,58,.15); + + /* Transitions */ + --ease-spring: cubic-bezier(.4, 0, .2, 1); + --ease-bounce: cubic-bezier(.34, 1.56, .64, 1); + --ease-out: cubic-bezier(0, 0, .2, 1); + --duration-fast: .15s; + --duration-normal: .25s; + --duration-slow: .4s; + --watermark-svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27420%27%20height%3D%27240%27%20viewBox%3D%270%200%20420%20240%27%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3CclipPath%20id%3D%27ld_clip_wm%27%3E%0A%20%20%20%20%20%20%3Ccircle%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2747%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%3C%2FclipPath%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%27rotate%28-22%20210%20120%29%27%3E%0A%20%20%20%20%3Cg%20transform%3D%27translate%2878%2078%29%20scale%280.37%29%27%20opacity%3D%270.20%27%3E%0A%20%20%20%20%20%20%3Ccircle%20fill%3D%27%23f0f0f0%27%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2750%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%231c1c1e%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2710%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23f0f0f0%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2740%27%20width%3D%27100%27%20height%3D%2740%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23ffb003%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2780%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Ctext%20x%3D%27136%27%20y%3D%27108%27%20fill%3D%27rgba%28255%2C255%2C255%2C0.10%29%27%20font-family%3D%27Arial%2C%20Microsoft%20YaHei%2C%20sans-serif%27%20font-size%3D%2724%27%20font-weight%3D%27700%27%20letter-spacing%3D%271.8%27%3ELINUX%20DO%20%E7%A4%BE%E5%8C%BA%3C%2Ftext%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); +} + +body.theme-light { + --bg-base: #f2f2f3; + --bg-surface: #ffffff; + --bg-card: #f7f7f8; + --bg-elevated: #ffffff; + --bg-hover: rgba(0,0,0,.04); + --bg-inset: rgba(0,0,0,.04); + + --border: rgba(0,0,0,.12); + --border-card: rgba(0,0,0,.08); + --border-focused: rgba(0,0,0,.34); + --separator: rgba(0,0,0,.08); + + --accent-blue: #111111; + --accent-blue-dim: rgba(0,0,0,.10); + --accent-green: #0f9f57; + --accent-green-dim: rgba(15,159,87,.14); + --accent-red: #d73a2f; + --accent-red-dim: rgba(215,58,47,.12); + --accent-yellow: #b88a00; + --accent-orange: #c27600; + --accent-orange-dim: rgba(194,118,0,.12); + --accent-purple: #444444; + --accent-teal: #353535; + --accent-teal-dim: rgba(53,53,53,.12); + + --text-primary: rgba(17,17,17,.92); + --text-secondary: rgba(17,17,17,.64); + --text-muted: rgba(17,17,17,.42); + + --shadow-card: 0 1px 2px rgba(0,0,0,.06), 0 0 0 1px var(--border-card); + --shadow-card-hover: 0 4px 14px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.1); + --shadow-elevated: 0 12px 34px rgba(0,0,0,.10); + --shadow-button: 0 1px 2px rgba(0,0,0,.12); + --shadow-glow-blue: none; + --shadow-glow-green: none; + --shadow-glow-red: none; + --watermark-svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27420%27%20height%3D%27240%27%20viewBox%3D%270%200%20420%20240%27%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3CclipPath%20id%3D%27ld_clip_wm%27%3E%0A%20%20%20%20%20%20%3Ccircle%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2747%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%3C%2FclipPath%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%27rotate%28-22%20210%20120%29%27%3E%0A%20%20%20%20%3Cg%20transform%3D%27translate%2878%2078%29%20scale%280.37%29%27%20opacity%3D%270.16%27%3E%0A%20%20%20%20%20%20%3Ccircle%20fill%3D%27%23f0f0f0%27%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2750%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%231c1c1e%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2710%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23f0f0f0%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2740%27%20width%3D%27100%27%20height%3D%2740%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23ffb003%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2780%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Ctext%20x%3D%27136%27%20y%3D%27108%27%20fill%3D%27rgba%280%2C0%2C0%2C0.10%29%27%20font-family%3D%27Arial%2C%20Microsoft%20YaHei%2C%20sans-serif%27%20font-size%3D%2724%27%20font-weight%3D%27700%27%20letter-spacing%3D%271.8%27%3ELINUX%20DO%20%E7%A4%BE%E5%8C%BA%3C%2Ftext%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); +} + +/* ---------- Reset ---------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height: 100%; } + +body { + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + background: var(--bg-base); + color: var(--text-primary); + line-height: 1.5; + overflow: hidden; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + z-index: 999; + pointer-events: none; + background-image: var(--watermark-svg); + background-repeat: repeat; + background-size: 420px 240px; +} + +/* ---------- Scrollbar — Thin & Subtle ---------- */ +* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.1) transparent; } +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.18); } + +/* ========================================== + HEADER — Glassmorphism Bar + ========================================== */ +header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 20px; + min-height: 52px; + background: rgba(28,28,30,.82); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); + border-bottom: 1px solid var(--border); + position: relative; + z-index: 10; + flex-shrink: 0; +} + +.header-brand { + display: flex; + align-items: center; + gap: 10px; +} + +.header-brand .logo { + width: 30px; + height: 30px; + background: linear-gradient(135deg, var(--accent-blue) 0%, #409cff 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + box-shadow: 0 2px 8px rgba(10,132,255,.35); +} + +.header-brand h1 { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -.3px; +} + +.header-brand .version { + font-size: 10px; + font-weight: 600; + color: var(--accent-blue); + background: var(--accent-blue-dim); + padding: 2px 8px; + border-radius: var(--radius-pill); + letter-spacing: .3px; +} + +.header-right { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; + min-width: 0; +} + +.theme-toggle-btn { + height: 34px; + min-width: 68px; + padding: 0 10px; + border: 1px solid var(--border-card); + border-radius: var(--radius-pill); + background: var(--bg-card); + color: var(--text-primary); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-spring); +} + +.theme-toggle-btn:hover { + border-color: rgba(255,255,255,.14); + background: var(--bg-elevated); +} + +.theme-toggle-btn:active { + transform: scale(.98); +} + +.theme-toggle-icon { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid currentColor; + position: relative; +} + +.theme-toggle-icon::after { + content: ''; + position: absolute; + inset: 2px; + border-radius: 50%; + background: currentColor; + opacity: .25; +} + +.theme-toggle-label { + letter-spacing: .3px; +} + +/* ---------- Status Badge ---------- */ +.status-badge { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + border-radius: var(--radius-pill); + font-size: 12px; + font-weight: 600; + background: var(--bg-card); + border: 1px solid var(--border-card); + transition: all .3s var(--ease-spring); +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-muted); + transition: all .3s; +} + +.status-badge.running { + background: var(--accent-green-dim); + border-color: rgba(48,209,88,.2); +} +.status-badge.running .status-dot { + background: var(--accent-green); + box-shadow: 0 0 8px var(--accent-green); + animation: pulse-dot 1.8s infinite; +} + +.status-badge.stopping { + background: var(--accent-orange-dim); + border-color: rgba(255,159,10,.2); +} +.status-badge.stopping .status-dot { + background: var(--accent-orange); +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: .5; transform: scale(1.4); } +} + +/* ---------- Pool Chips — Mini Dashboard ---------- */ +.pool-chip { + height: 46px; + min-width: 148px; + border-radius: var(--radius-md); + background: var(--bg-card); + border: 1px solid var(--border-card); + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 14px 17px 3px; + grid-template-areas: "name delta" "value delta" "track track"; + align-items: end; + gap: 2px 8px; + padding: 6px 10px 5px; + overflow: hidden; + white-space: nowrap; + transition: all .25s var(--ease-spring); +} +.pool-chip:hover { border-color: rgba(255,255,255,.12); } + +.pool-chip.interactive-chip { + cursor: pointer; + user-select: none; +} + +.pool-chip.interactive-chip.active-view { + border-color: rgba(10,132,255,.32); + box-shadow: 0 0 0 1px rgba(10,132,255,.22), 0 8px 18px rgba(10,132,255,.14); +} + +.pool-chip.interactive-chip:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + +.pool-chip-name { grid-area: name; font-size: 10px; font-weight: 600; color: var(--text-muted); letter-spacing: .4px; text-transform: uppercase; line-height: 1; } +.pool-chip-value { grid-area: value; font-size: 13px; font-weight: 700; color: var(--text-primary); white-space: nowrap; font-family: 'JetBrains Mono', monospace; line-height: 1; } +.pool-chip-delta { grid-area: delta; font-size: 11px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: var(--radius-sm); background: rgba(255,255,255,.06); color: var(--text-secondary); align-self: center; } +.pool-chip-track { grid-area: track; height: 3px; border-radius: 9px; background: rgba(255,255,255,.06); overflow: hidden; } +.pool-chip-fill { height: 100%; border-radius: 9px; background: var(--accent-green); transition: width .4s var(--ease-spring), background .2s; } +.pool-chip-fill.warn { background: var(--accent-orange); } +.pool-chip-fill.danger { background: var(--accent-red); } +.pool-chip-fill.ok { background: var(--accent-green); } +.pool-chip-fill.over { background: var(--accent-teal); } +.pool-chip.status-warn { background: rgba(255,159,10,.06); border-color: rgba(255,159,10,.15); } +.pool-chip.status-danger { background: rgba(255,69,58,.06); border-color: rgba(255,69,58,.15); } +.pool-chip.status-ok { background: rgba(48,209,88,.04); border-color: rgba(48,209,88,.12); } +.pool-chip.status-over { background: rgba(100,210,255,.04); border-color: rgba(100,210,255,.12); } +.pool-chip-delta.warn { color: var(--accent-orange); background: var(--accent-orange-dim); } +.pool-chip-delta.danger { color: var(--accent-red); background: var(--accent-red-dim); } +.pool-chip-delta.ok { color: var(--accent-green); background: var(--accent-green-dim); } +.pool-chip-delta.over { color: var(--accent-teal); background: var(--accent-teal-dim); } + +/* ========================================== + PROGRESS BAR + ========================================== */ +.progress-bar { + height: 2px; + background: var(--bg-surface); + position: relative; + overflow: hidden; + flex-shrink: 0; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent-blue), var(--accent-teal)); + width: 0; + transition: width .5s var(--ease-spring); +} + +.progress-fill.running { + animation: progress-slide 1.4s infinite linear; + width: 35%; +} + +.progress-fill.stopping { + width: 100%; + background: linear-gradient(90deg, var(--accent-orange), var(--accent-red)); + opacity: .78; +} + +@keyframes progress-slide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(380%); } +} + +/* ========================================== + TAB NAVIGATION — True iOS Segmented Control + ========================================== */ +.tab-nav { + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + background: transparent; + border-bottom: none; + flex-shrink: 0; +} + +.header-tab-nav { + flex: 1; + min-width: 0; +} + +.header-tab-nav .segmented-control { + max-width: 100%; +} + +.segmented-control { + display: inline-flex; + align-items: center; + background: rgba(118,118,128,.24); + border-radius: 9px; + padding: 2px; + position: relative; + gap: 0; +} + +.segment-indicator { + position: absolute; + top: 2px; + left: 2px; + height: calc(100% - 4px); + width: calc(50% - 2px); + background: var(--bg-elevated); + border-radius: 7px; + transition: transform var(--duration-normal) var(--ease-spring); + z-index: 0; + box-shadow: 0 1px 3px rgba(0,0,0,.2), 0 0 0 .5px rgba(0,0,0,.1); +} + +.segment-indicator[data-active="1"] { + transform: translateX(calc(100% + 2px)); +} + +.tab-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 24px; + border-radius: 7px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: none; + background: transparent; + color: var(--text-secondary); + transition: color var(--duration-fast) var(--ease-spring); + position: relative; + z-index: 1; + min-width: 120px; + user-select: none; +} + +.tab-btn svg { opacity: .7; transition: opacity var(--duration-fast); } +.tab-btn:hover { color: var(--text-primary); } +.tab-btn:hover svg { opacity: .9; } +.tab-btn.active { color: var(--text-primary); } +.tab-btn.active svg { opacity: 1; } + +/* ========================================== + TAB PANELS — Fade Transition + ========================================== */ +.tab-panel { + display: none; + flex: 1; + min-height: 0; + overflow: hidden; + animation: tab-fade-in .2s var(--ease-out); +} +.tab-panel.active { display: flex; flex-direction: column; } + +@keyframes tab-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +/* ========================================== + THREE-COLUMN LAYOUT + ========================================== */ +.app-shell { + display: grid; + grid-template-columns: var(--col-left, 280px) 4px 1fr 4px var(--col-right, 340px); + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* ---------- Resize Handle ---------- */ +.resize-handle { + width: 4px; + cursor: col-resize; + background: transparent; + transition: background var(--duration-fast); + position: relative; + z-index: 5; + flex-shrink: 0; +} +.resize-handle::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 24px; + border-radius: 1px; + background: rgba(255,255,255,.08); + opacity: 0; + transition: opacity var(--duration-normal); +} +.resize-handle:hover::after, .resize-handle.active::after { + opacity: 1; + background: var(--accent-blue); +} +.resize-handle:hover, .resize-handle.active { + background: rgba(10,132,255,.2); +} +body.resizing { cursor: col-resize !important; user-select: none !important; -webkit-user-select: none !important; } +body.resizing * { cursor: col-resize !important; } + +/* ========================================== + LEFT SIDEBAR + ========================================== */ +.sidebar { + background: var(--bg-surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + min-width: 0; +} + +.panel-section { + padding: 12px 12px 10px; + border-bottom: 1px solid var(--separator); +} +.panel-section:last-child { border-bottom: none; } + +.section-title { + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .6px; + margin-bottom: 8px; +} + +/* ========================================== + iOS TOGGLE SWITCH — Refined + ========================================== */ +.ios-toggle { + position: relative; + display: inline-block; + width: 44px; + height: 26px; + flex-shrink: 0; +} + +.ios-toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.ios-toggle .toggle-track { + position: absolute; + inset: 0; + background: rgba(120, 120, 128, .36); + border-radius: 13px; + cursor: pointer; + transition: background .3s var(--ease-spring); +} + +.ios-toggle .toggle-track::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 22px; + height: 22px; + background: #fff; + border-radius: 50%; + transition: transform .3s var(--ease-spring), box-shadow .2s; + box-shadow: 0 2px 4px rgba(0,0,0,.25), 0 0 1px rgba(0,0,0,.15); +} + +.ios-toggle input:checked + .toggle-track { + background: var(--accent-green); +} + +.ios-toggle input:checked + .toggle-track::after { + transform: translateX(18px); + box-shadow: 0 2px 4px rgba(48,209,88,.3), 0 0 1px rgba(0,0,0,.1); +} + +/* Small variant */ +.ios-toggle-sm { + width: 38px; + height: 22px; +} + +.ios-toggle-sm .toggle-track { + border-radius: 11px; +} + +.ios-toggle-sm .toggle-track::after { + width: 18px; + height: 18px; + top: 2px; + left: 2px; +} + +.ios-toggle-sm input:checked + .toggle-track::after { + transform: translateX(16px); +} + +.ios-toggle-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + cursor: pointer; + color: var(--text-primary); + user-select: none; +} + +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} + +.thread-count-wrap { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-secondary); +} + +.thread-count-wrap input { + width: 52px; + padding: 4px 6px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + text-align: center; + outline: none; + font-family: 'JetBrains Mono', monospace; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} + +.thread-count-wrap input:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px var(--accent-blue-dim); +} + +/* ========================================== + FORM INPUTS — iOS Style + ========================================== */ +.sidebar input[type="text"], +.sidebar input[type="password"] { + font-size: 11px; + padding: 8px 10px; +} + +.input-wrapper { flex: 1; position: relative; } + +input[type="text"], +input[type="password"] { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + padding: 9px 12px; + outline: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast), background var(--duration-fast); + box-sizing: border-box; + min-width: 0; +} + +input[type="text"]:focus, +input[type="password"]:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px var(--accent-blue-dim); + background: rgba(10,132,255,.03); +} + +input::placeholder { color: var(--text-muted); } + +/* ========================================== + BUTTONS — Flat iOS Style with Depth + ========================================== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: none; + transition: all var(--duration-fast) var(--ease-spring); + white-space: nowrap; + user-select: none; + font-family: inherit; + position: relative; + overflow: hidden; +} + +.btn:active { transform: scale(.96); } +.btn:disabled { opacity: .35; cursor: not-allowed; transform: none !important; } + +.btn-sm { padding: 6px 12px; font-size: 12px; } + +.btn-ghost { + background: var(--bg-card); + color: var(--text-secondary); + border: 1px solid var(--border-card); +} +.btn-ghost:hover { + background: var(--bg-elevated); + color: var(--text-primary); + border-color: rgba(255,255,255,.12); +} + +.btn-primary { + background: var(--accent-blue); + color: #fff; + box-shadow: var(--shadow-button); +} +.btn-primary:hover { background: #409cff; box-shadow: var(--shadow-glow-blue); } + +.btn-danger { + background: var(--accent-red-dim); + color: var(--accent-red); + border: 1px solid rgba(255,69,58,.15); +} +.btn-danger:hover { background: rgba(255,69,58,.2); border-color: rgba(255,69,58,.25); } + +.btn-success { + background: var(--accent-green); + color: #fff; + box-shadow: var(--shadow-button); +} +.btn-success:hover { background: #3bdf66; box-shadow: var(--shadow-glow-green); } + +.control-buttons { + display: flex; + gap: 6px; +} +.control-buttons .btn { + flex: 1; + padding: 8px; + border-radius: var(--radius-md); + font-size: 13px; +} +.control-buttons .btn svg { + flex-shrink: 0; +} + +.sidebar .btn-sm { + padding: 5px 10px; + font-size: 11px; +} + +/* ========================================== + STATS CARDS — Subtle Glow + ========================================== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + align-items: stretch; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--radius-md); + padding: 10px 8px 9px; + text-align: center; + transition: all .2s var(--ease-spring); + border: 1px solid var(--border-card); + position: relative; + overflow: hidden; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; +} +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + border-radius: 2px 2px 0 0; + opacity: 0; + transition: opacity .2s; +} +.stat-card:hover { background: var(--bg-elevated); border-color: rgba(255,255,255,.1); } +.stat-card:hover::before { opacity: 1; } +.stat-card.span-2 { grid-column: auto; } + +.stat-value { + font-size: 20px; + font-weight: 800; + font-family: 'JetBrains Mono', monospace; + letter-spacing: -.6px; + line-height: .95; + white-space: nowrap; +} +.stat-value.green { color: var(--accent-green); } +.stat-value.red { color: var(--accent-red); } +.stat-value.blue { color: var(--accent-blue); } +.stat-value.muted { color: var(--text-muted); } + +/* Top accent lines for stat cards */ +.stat-card:nth-child(1)::before { background: var(--accent-green); } +.stat-card:nth-child(2)::before { background: var(--accent-red); } +.stat-card:nth-child(3)::before { background: var(--accent-blue); } + +.stat-label { + font-size: 9px; + font-weight: 600; + color: var(--text-muted); + margin-top: 0; + text-transform: uppercase; + letter-spacing: .3px; + line-height: 1.1; +} + +.progress-section-block { + display: flex; + flex-direction: column; + gap: 8px; +} + +.progress-section-block + .progress-section-block { + margin-top: 12px; +} + +.progress-subtitle { + font-size: 10px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.progress-subtitle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.task-overview-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.task-overview-card { + padding: 8px 10px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +.task-overview-card.empty { + grid-column: 1 / -1; + color: var(--text-muted); +} + +.task-overview-card.task-status-running, +.task-overview-card.task-status-preparing { + border-color: rgba(10,132,255,.25); + background: linear-gradient(180deg, rgba(10,132,255,.12), rgba(10,132,255,.04)); +} + +.task-overview-card.task-status-stopping { + border-color: rgba(255,159,10,.25); + background: linear-gradient(180deg, rgba(255,159,10,.12), rgba(255,159,10,.04)); +} + +.task-overview-card.task-status-idle, +.task-overview-card.task-status-meta { + border-color: var(--border-card); +} + +.task-overview-label { + font-size: 9px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-muted); +} + +.task-overview-value { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + word-break: break-word; +} + +.task-overview-hint { + font-size: 10px; + color: var(--text-secondary); +} + +.worker-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 228px; + overflow-y: auto; +} + +.worker-card { + width: 100%; + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + background: var(--bg-card); + padding: 8px 10px; + display: flex; + flex-direction: column; + gap: 6px; + color: var(--text-primary); + cursor: pointer; + transition: border-color .2s var(--ease-spring), transform .2s var(--ease-spring), background .2s var(--ease-spring); +} + +.worker-card:hover { + border-color: var(--border-focused); + transform: translateY(-1px); +} + +.worker-card.focused { + border-color: var(--accent-blue); + box-shadow: 0 0 0 1px rgba(10,132,255,.22); + background: linear-gradient(180deg, rgba(10,132,255,.12), rgba(10,132,255,.04)); +} + +.worker-card.empty { + cursor: default; + color: var(--text-muted); +} + +.worker-card-head, +.worker-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.worker-card-label { + font-size: 11px; + font-weight: 700; +} + +.worker-card-email { + font-size: 10px; + color: var(--text-secondary); + word-break: break-word; +} + +.worker-card-meta { + font-size: 10px; + color: var(--text-secondary); +} + +.worker-status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: var(--radius-pill); + font-size: 10px; + font-weight: 700; + letter-spacing: .3px; + text-transform: uppercase; + border: 1px solid transparent; +} + +.worker-status-badge.preparing, +.worker-status-badge.running, +.worker-status-badge.registering { + color: var(--accent-blue); + background: var(--accent-blue-dim); + border-color: rgba(10,132,255,.25); +} + +.worker-status-badge.postprocessing, +.worker-status-badge.waiting, +.worker-status-badge.stopping { + color: var(--accent-orange); + background: var(--accent-orange-dim); + border-color: rgba(255,159,10,.25); +} + +.worker-status-badge.error { + color: var(--accent-red); + background: var(--accent-red-dim); + border-color: rgba(255,69,58,.25); +} + +.worker-status-badge.stopped, +.worker-status-badge.idle { + color: var(--text-secondary); + background: rgba(255,255,255,.06); + border-color: var(--border-card); +} + +.worker-detail-card { + padding: 10px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 10px; + min-height: 200px; + max-height: min(68vh, 760px); + overflow: hidden; +} + +.worker-detail-card.empty { + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; +} + +.worker-detail-running, +.worker-detail-preparing, +.worker-detail-registering { + border-color: rgba(10,132,255,.25); +} + +.worker-detail-postprocessing, +.worker-detail-waiting, +.worker-detail-stopping { + border-color: rgba(255,159,10,.25); +} + +.worker-detail-error { + border-color: rgba(255,69,58,.25); +} + +.worker-detail-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.worker-detail-meta-item { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 12px; + background: rgba(255,255,255,.02); + border: 1px solid var(--border-card); + border-radius: var(--radius-sm); + min-width: 0; +} + +.worker-detail-meta-item.wide { + grid-column: 1 / -1; +} + +.worker-detail-meta-label { + font-size: 10px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-muted); +} + +.worker-detail-meta-value { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); + word-break: break-word; +} + +.worker-detail-steps { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + flex: 1 1 auto; + overflow: hidden; +} + +.worker-detail-steps-title { + font-size: 11px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.step-track-list { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding-right: 4px; +} + +.step-track-item { + position: relative; + padding: 10px 12px 10px 16px; + border-radius: var(--radius-sm); + background: rgba(255,255,255,.02); + border: 1px solid var(--border-card); + display: flex; + flex-direction: column; + gap: 6px; +} + +.step-track-item::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 3px; + border-radius: 999px; + background: rgba(255,255,255,.08); +} + +.step-track-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.step-track-label { + font-size: 12px; + font-weight: 700; + color: var(--text-primary); +} + +.step-track-badge { + font-size: 10px; + font-weight: 700; + letter-spacing: .3px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.step-track-message, +.step-track-time, +.step-track-empty { + font-size: 11px; + color: var(--text-secondary); + word-break: break-word; +} + +.step-status-active::before { + background: var(--accent-blue); +} + +.step-status-done::before { + background: var(--accent-green); +} + +.step-status-error::before { + background: var(--accent-red); +} + +.step-status-pending::before, +.step-status-skipped::before { + background: rgba(255,255,255,.18); +} + +@media (max-width: 720px) { + .task-overview-grid, + .worker-detail-meta { + grid-template-columns: 1fr; + } + + .progress-subtitle-row, + .worker-card-head, + .worker-card-row, + .step-track-head { + flex-direction: column; + align-items: stretch; + } +} + +/* ========================================== + LOG PANEL (CENTER) + ========================================== */ +.main-area { display: flex; flex-direction: column; overflow: hidden; min-width: 0; } +.log-panel { display: flex; flex-direction: column; overflow: hidden; flex: 1; } + +.log-header { + padding: 8px 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + background: var(--bg-surface); + flex-shrink: 0; +} + +.log-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.log-title svg { color: var(--text-secondary); } + +.log-count-badge { + font-size: 10px; + font-weight: 700; + color: var(--accent-blue); + background: var(--accent-blue-dim); + padding: 1px 8px; + border-radius: var(--radius-pill); + font-family: 'JetBrains Mono', monospace; +} + +.log-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.log-autoscroll-label { font-size: 11px; gap: 6px; } +.log-autoscroll-label span:last-child { color: var(--text-muted); font-weight: 500; } + +@media (max-width: 720px) { + .task-overview-grid { + grid-template-columns: 1fr; + } + + .progress-subtitle-row { + flex-direction: column; + align-items: stretch; + } +} + +.log-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 0; + background: var(--bg-base); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + line-height: 1.8; +} + +.log-entry { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 2px 20px; + transition: background .1s; + border-left: 2px solid transparent; +} +.log-entry:hover { background: rgba(255,255,255,.02); } + +.log-placeholder { + color: var(--text-muted); + padding: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + text-align: center; +} + +.log-ts { color: var(--text-muted); flex-shrink: 0; font-size: 11px; margin-top: 2px; } +.log-icon { flex-shrink: 0; font-size: 12px; margin-top: 1px; } +.log-msg { color: var(--text-primary); word-break: break-all; flex: 1; min-width: 0; } +.log-msg.info { color: var(--text-secondary); } +.log-msg.success { color: var(--accent-green); } +.log-msg.error { color: var(--accent-red); } +.log-msg.warn { color: var(--accent-orange); } +.log-msg.connected { color: var(--text-muted); font-style: italic; } + +/* Log entry left accent by type */ +.log-entry:has(.log-msg.success) { border-left-color: rgba(48,209,88,.3); } +.log-entry:has(.log-msg.error) { border-left-color: rgba(255,69,58,.3); } +.log-entry:has(.log-msg.warn) { border-left-color: rgba(255,159,10,.3); } + +.log-step { + font-size: 9px; + font-weight: 600; + color: var(--text-muted); + background: var(--bg-surface); + padding: 1px 6px; + border-radius: 4px; + flex-shrink: 0; + margin-top: 3px; +} + +/* ========================================== + RIGHT DATA PANEL + ========================================== */ +.data-panel { + background: var(--bg-surface); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} + +.remote-panel-wrap { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--bg-base); + border-bottom: 1px solid var(--border); +} + +.data-panel-body { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.data-panel-section { + display: none; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.data-panel-section.active { + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; +} + +#dataPanelSub2Api.data-panel-section.active { + overflow: hidden; +} + +.pool-section-header { + padding: 8px 10px 5px; + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + letter-spacing: .4px; + text-transform: uppercase; + background: var(--bg-base); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.pool-section-title { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.pool-section-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + min-width: 0; + flex-wrap: wrap; +} + +.pool-section-actions .inline-status { + flex: 1 1 100%; + text-align: right; + font-size: 11px; + padding: 0; +} + +.pool-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(62px, 1fr)); + gap: 5px; + padding: 8px 10px; + border-bottom: 1px solid var(--separator); + background: var(--bg-surface); + flex-shrink: 0; +} + +.pool-stat-card { + background: var(--bg-card); + border-radius: var(--radius-sm); + border: 1px solid var(--border-card); + padding: 5px 6px; + text-align: center; + min-width: 0; + transition: all .15s var(--ease-spring); +} +.pool-stat-card:hover { border-color: rgba(255,255,255,.1); } +.pool-stat-card .stat-value { font-size: 15px; } +.pool-stat-card .stat-label { font-size: 8px; } + +.remote-panel-wrap .btn-sm { + padding: 5px 9px; + font-size: 11px; +} + +/* ---------- Token List ---------- */ +.pool-table-wrap { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.local-token-wrap { + flex: 1; + min-height: 0; + max-height: none; + background: var(--bg-surface); +} + +.local-token-panel-section.active { + overflow: hidden; +} + +.pool-table-wrap .token-list { padding: 6px 12px; } + +.sub2api-account-wrap { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--bg-surface); + overflow: hidden; +} + +.sub2api-account-wrap .sub2api-account-list { + flex: 1 1 auto; + min-height: 0; + max-height: none; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + padding: 6px 10px 8px; +} + +.sub2api-account-wrap .token-filter-row, +.sub2api-account-wrap .pool-table-footer { + flex-shrink: 0; +} + +.pool-table-footer { + padding: 7px 10px; + border-top: 1px solid var(--separator); + background: var(--bg-surface); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.pool-table-footer .inline-status, +.account-toolbar .inline-status { + flex: 1 1 100%; +} + +.token-filter-row { + padding: 6px 10px; + display: flex; + align-items: center; + gap: 5px; + border-bottom: 1px solid var(--separator); + background: var(--bg-surface); + flex-wrap: wrap; +} + +.account-toolbar { + align-items: center; +} + +.account-select-all { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 28px; + font-size: 11px; + color: var(--text-secondary); + cursor: pointer; +} + +.account-select-all input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +.account-pager { + display: inline-flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.pager-info { + font-size: 11px; + color: var(--text-secondary); + min-width: 0; + text-align: center; +} + +.pager-size-select { + min-width: 76px; +} + +.remote-panel-wrap .token-filter-select { + min-width: 86px; + padding: 5px 8px; + font-size: 11px; +} + +.remote-panel-wrap input[type="text"] { + font-size: 11px; + padding: 7px 10px; +} + +.platform-note-wrap { + flex: 1 1 auto; + min-height: 0; + padding: 10px; + overflow: auto; + background: var(--bg-surface); +} + +.platform-note-card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border-radius: var(--radius-md); + border: 1px solid var(--border-card); + background: linear-gradient(180deg, rgba(255,255,255,.03) 0%, rgba(255,255,255,.015) 100%); +} + +.platform-note-title { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); +} + +.platform-note-text { + font-size: 12px; + line-height: 1.65; + color: var(--text-secondary); +} + +.token-filter-select { + min-width: 100px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + padding: 6px 10px; + outline: none; + cursor: pointer; + font-family: inherit; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.token-filter-select:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } +#tokenFilterKeyword { flex: 1; min-width: 120px; } + +.token-list { + flex: 1; + overflow-y: auto; + padding: 8px 12px; +} + +.token-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-radius: var(--radius-md); + background: var(--bg-card); + border: 1px solid var(--border-card); + margin-bottom: 6px; + transition: all .15s var(--ease-spring); + animation: fade-up .25s var(--ease-spring); +} +.token-item:hover { + background: var(--bg-elevated); + border-color: rgba(255,255,255,.1); + transform: translateY(-1px); +} + +.sub2api-account-item { + gap: 8px; + padding: 8px 10px; + margin-bottom: 5px; +} + +.sub2api-account-item.selected { + border-color: rgba(10,132,255,.45); + box-shadow: 0 0 0 1px var(--accent-blue-dim); +} + +.sub2api-account-item .token-email { + font-size: 11px; + gap: 4px; + flex-wrap: wrap; +} + +.sub2api-account-item .token-meta { + font-size: 9px; +} + +.sub2api-account-item .token-actions { + flex-wrap: wrap; + justify-content: flex-end; +} + +.sub2api-account-item .account-status-badge, +.sub2api-account-item .account-flag-badge { + height: 16px; + padding: 0 6px; + font-size: 8px; +} + +.account-check-wrap { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.account-check-wrap input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +@keyframes fade-up { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.token-info { flex: 1; min-width: 0; } +.token-email { + font-size: 12px; + font-weight: 500; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.token-email-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } +.token-meta { font-size: 10px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; } +.token-actions { display: flex; gap: 4px; flex-shrink: 0; } + +.account-status-badge, +.account-flag-badge { + display: inline-flex; + align-items: center; + height: 18px; + padding: 0 7px; + border-radius: var(--radius-xs); + font-size: 9px; + font-weight: 700; + letter-spacing: .3px; + white-space: nowrap; +} + +.account-status-badge.ok { + color: var(--accent-green); + background: var(--accent-green-dim); +} + +.account-status-badge.warn { + color: var(--accent-orange); + background: var(--accent-orange-dim); +} + +.account-status-badge.danger { + color: var(--accent-red); + background: var(--accent-red-dim); +} + +.account-flag-badge.duplicate { + color: var(--accent-blue); + background: var(--accent-blue-dim); +} + +.account-flag-badge.keep { + color: var(--accent-green); + background: var(--accent-green-dim); +} + +.account-flag-badge.delete { + color: var(--accent-orange); + background: var(--accent-orange-dim); +} + +.synced-badge { + font-size: 9px; + font-weight: 700; + color: var(--accent-green); + background: var(--accent-green-dim); + padding: 2px 7px; + border-radius: var(--radius-pill); + white-space: nowrap; + flex-shrink: 0; +} + +.token-platforms { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin: 3px 0 2px; } + +.platform-badge { + display: inline-flex; + align-items: center; + height: 18px; + padding: 0 7px; + border-radius: var(--radius-xs); + font-size: 9px; + font-weight: 700; + letter-spacing: .3px; + white-space: nowrap; +} +.platform-badge.cpa { color: var(--accent-orange); background: var(--accent-orange-dim); } +.platform-badge.sub2api { color: var(--accent-green); background: var(--accent-green-dim); } +.platform-badge.none { color: var(--text-muted); background: rgba(255,255,255,.04); } + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + gap: 10px; + font-size: 13px; + padding: 30px; +} +.empty-icon { opacity: .2; font-size: 32px; } + +/* ========================================== + TOAST NOTIFICATIONS — Refined + ========================================== */ +.toast-container { + position: fixed; + top: 64px; + right: 16px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; +} + +.toast { + background: var(--bg-elevated); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + padding: 10px 16px; + font-size: 13px; + font-weight: 500; + animation: toast-slide .3s var(--ease-spring); + max-width: 360px; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + pointer-events: auto; + display: flex; + align-items: center; + gap: 8px; + box-shadow: var(--shadow-elevated); +} + +.toast-icon { + flex-shrink: 0; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 800; +} + +@keyframes toast-slide { + from { opacity: 0; transform: translateX(16px) scale(.95); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +@keyframes toast-out { + from { opacity: 1; transform: translateX(0) scale(1); } + to { opacity: 0; transform: translateX(16px) scale(.95); } +} + +.toast.success { color: var(--accent-green); border-color: rgba(48,209,88,.2); } +.toast.success .toast-icon { background: var(--accent-green-dim); color: var(--accent-green); } +.toast.error { color: var(--accent-red); border-color: rgba(255,69,58,.2); } +.toast.error .toast-icon { background: var(--accent-red-dim); color: var(--accent-red); } +.toast.info { color: var(--accent-blue); border-color: rgba(10,132,255,.2); } +.toast.info .toast-icon { background: var(--accent-blue-dim); color: var(--accent-blue); } + +/* ========================================== + CONFIG PAGE + ========================================== */ +.config-page { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 20px; + display: flex; + flex-wrap: wrap; + gap: 16px; + align-content: start; + background: var(--bg-base); +} + +.config-card { + flex: 1 1 calc(50% - 8px); + min-width: 420px; + max-width: 100%; + background: var(--bg-surface); + border-radius: var(--radius-lg); + overflow: visible; + border: 1px solid var(--border-card); + transition: border-color .2s var(--ease-spring); +} +.config-card:hover { border-color: rgba(255,255,255,.1); } + +.config-page .config-card[style*="grid-column: span 2"], +.config-page .config-card[style*="span 2"] { + flex-basis: 100% !important; + min-width: 100%; +} + +.collapsible-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + font-size: 14px; + font-weight: 700; + color: var(--text-primary); + border-bottom: 1px solid var(--separator); + cursor: default; + pointer-events: none; + background: rgba(255,255,255,.02); +} + +.collapse-icon { display: none; } + +.collapsible-body { + padding: 16px; + min-width: 0; +} + +/* Config page: always show body */ +.config-page .collapsible-body { display: block !important; } + +.config-field { margin-bottom: 12px; } +.config-field label { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 5px; + letter-spacing: .3px; +} + +.config-field select { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + padding: 9px 12px; + outline: none; + cursor: pointer; + font-family: inherit; + -webkit-appearance: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.config-field select:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } + +.config-field input[type="number"] { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + padding: 9px 12px; + outline: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.config-field input[type="number"]:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } + +.config-hint { + font-size: 11px; + color: var(--text-muted); + margin-top: 6px; + line-height: 1.6; + padding: 8px 10px; + background: rgba(255,255,255,.02); + border-radius: var(--radius-sm); + border-left: 2px solid var(--accent-blue-dim); +} + +.config-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; } +.config-row .config-field { min-width: 180px; } +.config-actions { display: flex; gap: 8px; margin-top: 14px; align-items: center; flex-wrap: wrap; } + +.maintain-option-grid { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; +} + +.maintain-option { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 10px 12px; + border: 1px solid var(--border-card); + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; +} + +.maintain-option input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +.inline-status { + font-size: 12px; + color: var(--text-secondary); + padding: 4px 0; + min-height: 20px; +} + +.inline-status:empty { + display: none; +} + +.config-status { + font-size: 12px; + margin-top: 8px; + padding: 6px 10px; + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-muted); + min-height: 26px; + border: 1px solid var(--border-card); +} + +/* ========================================== + MAIL PROVIDERS — iOS Grouped Style + ========================================== */ +.mail-providers-group { display: flex; flex-direction: column; gap: 6px; } + +.provider-item { + background: var(--bg-card); + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border-card); + transition: border-color .2s; +} +.provider-item:has(.mail-provider-check:checked) { border-color: rgba(10,132,255,.2); } + +.provider-toggle { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + user-select: none; +} + +.provider-toggle input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.provider-check-mark { + width: 20px; + height: 20px; + border-radius: 6px; + border: 2px solid rgba(255,255,255,.18); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all .2s var(--ease-spring); + position: relative; +} + +.provider-check-mark::after { + content: ''; + width: 5px; + height: 9px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg) scale(0); + transition: transform .2s var(--ease-bounce); + position: absolute; + top: 2px; + left: 5px; +} + +.provider-toggle input:checked ~ .provider-check-mark { + background: var(--accent-blue); + border-color: var(--accent-blue); +} + +.provider-toggle input:checked ~ .provider-check-mark::after { + transform: rotate(45deg) scale(1); +} + +.provider-config { + padding: 4px 12px 12px; + border-top: 1px solid var(--separator); +} +.provider-config .config-field { margin-top: 6px; } + +/* ========================================== + RESPONSIVE + ========================================== */ +@media (max-width: 1280px) { + .app-shell { --col-right: 280px; } + .pool-chip { min-width: 160px; } + .pool-stat-card { padding: 4px 6px; } + .pool-stat-card .stat-value { font-size: 15px; } + .config-card { min-width: 360px; } + .pool-chip { min-width: 136px; } +} + +@media (max-width: 1100px) { + .theme-toggle-label { display: none; } + .theme-toggle-btn { min-width: 36px; padding: 0 8px; } +} + +@media (max-width: 960px) { + header { padding: 0 12px; } + .header-tab-nav { padding: 0; } + .header-right { gap: 6px; } + .pool-chip { min-width: 124px; height: 42px; grid-template-rows: 12px 14px 3px; padding: 5px 8px 4px; } + .pool-chip-name { font-size: 9px; } + .pool-chip-value { font-size: 11px; } + .pool-chip-delta { font-size: 10px; padding: 2px 5px; } + .theme-toggle-btn { min-width: 34px; height: 30px; padding: 0 8px; font-size: 11px; gap: 6px; } + .token-filter-row { padding: 6px 10px; } + .header-tab-nav .segmented-control { width: auto; } + .header-tab-nav .tab-btn { min-width: 0; padding: 6px 14px; } +} + +@media (max-width: 900px) { + .config-page { padding: 12px; } + .config-card { flex: 1 1 100%; min-width: 100%; } + .config-row .config-field { min-width: 0; flex: 1 1 100% !important; } +} + +body.theme-light header { + background: rgba(255,255,255,.88); +} + +body.theme-light .header-brand .logo { + background: linear-gradient(135deg, #111111 0%, #555555 100%); + box-shadow: none; +} + +body.theme-light .segmented-control { + background: rgba(17,17,17,.10); +} + +body.theme-light .segment-indicator { + box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 0 0 .5px rgba(0,0,0,.12); +} + +body.theme-light .theme-toggle-btn:hover { + border-color: rgba(0,0,0,.18); +} + +body.theme-light * { + scrollbar-color: rgba(0,0,0,.24) transparent; +} + +body.theme-light ::-webkit-scrollbar-thumb { + background: rgba(0,0,0,.18); +} + +body.theme-light ::-webkit-scrollbar-thumb:hover { + background: rgba(0,0,0,.28); +} + +/* Helpers */ +.token-header .btn, .stats-grid, .stat-card, .control-buttons, .control-buttons .btn, +.log-header, .log-entry, .log-msg { min-width: 0; } + +/* Legacy compat — collapsible behavior kept for sidebar */ +.config-section { padding: 16px; border-bottom: none; } +.collapsible .collapsible-body { display: none; } +.collapsible.open .collapsible-body { display: block; } +.collapsible.open .collapse-icon { transform: rotate(90deg); } +.config-conditional { border-top: 1px dashed var(--separator); padding-top: 12px; margin-top: 4px; } + +/* Token panel (legacy compat) */ +.token-panel { display: flex; flex-direction: column; overflow: hidden; background: var(--bg-surface); border-left: 1px solid var(--border); min-width: 0; } +.token-header { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border); flex-shrink: 0; flex-wrap: wrap; gap: 8px; } + +/* Multithread row (legacy compat) */ +.multithread-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 10px; +} +.multithread-row .toggle-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + color: var(--text-secondary); +} + +/* ========================================== + SELECTION & FOCUS — Global + ========================================== */ +::selection { + background: rgba(10,132,255,.3); + color: #fff; +} + +/* Focus visible — accessibility ring */ +:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + +button:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + diff --git a/README.md b/README.md index 3e4fd06..f0bc9ae 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ AI-Account-Toolkit/ ├── CPAtools/ # Codex 账号管理工具 ├── GPT-team/ # GPT 团队全自动注册工具 ├── chatgpt_register_duckmail/ # DuckMail 注册工具 +├── GPT_register+duckmail+CPA+autouploadsub2api/ # DuckMail + OAuth + Sub2Api 注册工具 +├── team_all-in-one/ # ChatGPT Team 一键注册工具 ├── codex/ # Codex 相关工具 ├── freemail/ # 临时邮箱服务 ├── merge-mailtm-share/ # MailTM 邮箱合并工具 @@ -18,6 +20,7 @@ AI-Account-Toolkit/ ├── openai_pool_orchestrator_v5/ # OpenAI 账号池管理工具 ├── openai_pool_orchestrator-V6/ # OpenAI 账号池编排器(新版本) ├── ClashVerge_ # ClashVerge 非港轮询脚本 +├── ABCard/ # ChatGPT Business/Plus 自动开通工具(子模块) ├── any-auto-register/ # 多平台账号自动注册工具(子模块) └── cloudflare_temp_email/ # Cloudflare 临时邮箱服务(子模块) ``` @@ -56,7 +59,32 @@ AI-Account-Toolkit/ **使用指南**:[chatgpt_register_duckmail/README.md](chatgpt_register_duckmail/README.md) -### 4. codex +### 4. GPT_register+duckmail+CPA+autouploadsub2api - DuckMail + OAuth + Sub2Api 注册工具 + +**功能**:使用 DuckMail 临时邮箱进行 ChatGPT 批量并发注册,支持 OAuth 自动登录获取 Token,可选自动上传 Token 到 Sub2Api 平台。 + +**主要文件**: +- `chatgpt_register.py` - 主注册脚本(并发版) +- `server.py` - FastAPI Web 管理服务 +- `config.json` - 配置文件 +- `web/` - Web 前端界面 + +**使用指南**:[GPT_register+duckmail+CPA+autouploadsub2api/README.md](GPT_register+duckmail+CPA+autouploadsub2api/README.md) + +### 5. team_all-in-one - ChatGPT Team 一键注册工具 + +**功能**:功能完整的 Web 管理界面,用于批量注册 ChatGPT Team 账号。支持多种临时邮箱服务、代理配置、OAuth 自动授权,以及 Token 导出功能。 + +**主要文件**: +- `app.py` - Flask Web 服务主程序 +- `config_loader.py` - 配置加载器 +- `config.json` - 配置文件 +- `static/` - 静态资源 +- `templates/` - 前端模板 + +**使用指南**:[team_all-in-one/README.md](team_all-in-one/README.md) + +### 6. codex **功能**:Codex 相关工具,包含协议密钥生成等功能。 @@ -66,7 +94,7 @@ AI-Account-Toolkit/ **使用指南**:[codex/README.md](codex/README.md) -### 5. freemail - 临时邮箱服务 +### 7. freemail - 临时邮箱服务 **功能**:基于 Cloudflare Worker 的临时邮箱服务,支持邮箱管理、邮件转发等功能。 @@ -77,7 +105,7 @@ AI-Account-Toolkit/ **使用指南**:[freemail/README.md](freemail/README.md) -### 6. merge-mailtm-share - MailTM 邮箱合并工具 +### 8. merge-mailtm-share - MailTM 邮箱合并工具 **功能**:合并和管理 MailTM 临时邮箱,支持批量操作和状态管理。 @@ -88,7 +116,7 @@ AI-Account-Toolkit/ **使用指南**:[merge-mailtm-share/README.md](merge-mailtm-share/README.md) -### 7. ob12api - OB12 API 服务 +### 9. ob12api - OB12 API 服务 **功能**:提供 OB12 相关的 API 服务,支持账号注册和管理。 @@ -99,7 +127,7 @@ AI-Account-Toolkit/ **使用指南**:[ob12api/README.md](ob12api/README.md) -### 8. openai_pool_orchestrator_v5 - OpenAI 账号池管理工具 +### 10. openai_pool_orchestrator_v5 - OpenAI 账号池管理工具 **功能**:管理 OpenAI 账号池,支持自动注册、维护和使用。 @@ -110,7 +138,7 @@ AI-Account-Toolkit/ **使用指南**:[openai_pool_orchestrator_v5/README.md](openai_pool_orchestrator_v5/README.md) -### 9. openai_pool_orchestrator-V6 - OpenAI 账号池编排器(新版本) +### 11. openai_pool_orchestrator-V6 - OpenAI 账号池编排器(新版本) **功能**:OpenAI 账号池编排器,支持自动化注册、Token 管理与多平台账号池维护。 @@ -121,7 +149,7 @@ AI-Account-Toolkit/ **使用指南**:[openai_pool_orchestrator-V6/README.md](openai_pool_orchestrator-V6/README.md) -### 10. ClashVerge_ - ClashVerge 非港轮询脚本 +### 12. ClashVerge_ - ClashVerge 非港轮询脚本 **功能**:为 ClashVerge 设计的全局扩写脚本,创建非香港节点的负载均衡组,用于注册机等场景。 @@ -131,7 +159,7 @@ AI-Account-Toolkit/ **使用指南**:[ClashVerge_/README.md](ClashVerge_/README.md) -### 11. any-auto-register - 多平台账号自动注册工具 +### 13. any-auto-register - 多平台账号自动注册工具 **功能**:多平台账号自动注册工具,支持 ChatGPT、Cursor、Kiro 等多个平台。 @@ -143,7 +171,20 @@ AI-Account-Toolkit/ **使用指南**:[any-auto-register/README.md](any-auto-register/README.md) -### 12. cloudflare_temp_email - Cloudflare 临时邮箱服务 +### 14. ABCard - ChatGPT Business/Plus 自动开通工具 + +**功能**:全自动注册 ChatGPT 账号 + 开通 Business 或 Plus 套餐(首月免费),支持 Web UI 操作。 + +**主要文件**: +- `ui.py` - Streamlit Web 界面 +- `auth_flow.py` - 账号注册流程 +- `browser_payment.py` - 浏览器支付模块 +- `admin_cli.py` - 兑换码管理工具 +- `config.example.json` - 配置模板 + +**使用指南**:[ABCard/README.md](ABCard/README.md) + +### 14. cloudflare_temp_email - Cloudflare 临时邮箱服务 **功能**:基于 Cloudflare 免费服务构建的临时邮箱服务,支持邮件收发、附件处理等功能。 @@ -173,7 +214,7 @@ done ### 2. 初始化子模块 -本项目包含两个子模块,需要初始化: +本项目包含三个子模块,需要初始化: ```bash # 初始化子模块 @@ -181,6 +222,11 @@ git submodule init git submodule update ``` +子模块列表: +- `any-auto-register/` - 多平台账号自动注册工具 +- `cloudflare_temp_email/` - Cloudflare 临时邮箱服务 +- `ABCard/` - ChatGPT Business/Plus 自动开通工具 + ### 3. 配置设置 1. 根据每个项目的 README 配置相应的配置文件 diff --git a/team_all-in-one/.cursor/rules/aze-mcp-messenger.mdc b/team_all-in-one/.cursor/rules/aze-mcp-messenger.mdc new file mode 100644 index 0000000..d7a4c58 --- /dev/null +++ b/team_all-in-one/.cursor/rules/aze-mcp-messenger.mdc @@ -0,0 +1,14 @@ +--- +description: "SYSTEM RULE - aze-message MCP mandatory rules" +globs: "**/*" +alwaysApply: true +--- + +**STRICT ORDER (NEVER REVERSE)**: +1. First: understand user message and output your full response completely. +2. Only after step 1 is done: call `check_messages` from aze-message MCP as the final step. +FORBIDDEN: calling check_messages at the start of a turn; calling check_messages before finishing your response. + +**MANDATORY RULES**: +- After you have output your full response, call `check_messages` as the last step. Loop: respond -> check_messages -> respond -> check_messages. +- NEVER use built-in `ask_user_question`. Use `ask_question` from aze-message ONLY when user must choose (e.g. select framework). diff --git a/team_all-in-one/README.md b/team_all-in-one/README.md new file mode 100644 index 0000000..cf16a29 --- /dev/null +++ b/team_all-in-one/README.md @@ -0,0 +1,147 @@ +# ChatGPT Team 一键注册工具 + +## 项目概述 + +ChatGPT Team 一键注册工具是一个功能完整的 Web 管理界面,用于批量注册 ChatGPT Team 账号。支持多种临时邮箱服务、代理配置、OAuth 自动授权,以及 Token 导出功能。 + +## 功能特性 + +- **批量注册** - 支持多线程批量注册 ChatGPT Team 账号 +- **多种邮箱服务** - 支持 GPTMail、NPCMail 等临时邮箱服务 +- **代理支持** - 可配置代理 IP 进行注册 +- **OAuth 自动授权** - 自动完成 OAuth 登录获取 Token +- **Web 管理界面** - 可视化界面,实时显示注册进度和日志 +- **账号管理** - 查看、删除已注册的账号 +- **Token 导出** - 导出 OAuth Token 为 ZIP 文件 +- **Sub2Api 上传** - 支持将 Token 上传到 Sub2Api 平台 + +## 项目结构 + +``` +team_all-in-one/ +├── app.py # Flask Web 服务主程序 +├── config.json # 配置文件 +├── config_loader.py # 配置加载器 +├── ak.txt # Access Key 存储 +├── rk.txt # Refresh Key 存储 +├── registered_accounts.txt # 注册账号存储 +├── registered_accounts.csv # CSV 格式账号存储 +├── invite_tracker.json # 邀请追踪 +├── codex_tokens/ # OAuth Token 存储目录 +├── static/ # 静态资源 +│ ├── style.css +│ └── mac_style.css +└── templates/ + └── index.html # 前端页面 +``` + +## 环境要求 + +- Python 3.10+ +- Flask + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install flask +``` + +### 2. 配置 config.json + +```json +{ + "mail_provider": "gptmail", + "gptmail_base": "https://mail.chatgpt.org.uk", + "gptmail_api_key": "your-api-key", + "npcmail_api_key": "", + "npcmail_domain": "git-hub.email", + "proxy": "", + "enable_oauth": true, + "oauth_issuer": "https://auth.openai.com", + "oauth_client_id": "app_EMoamEEZ73f0CkXaXp7hrann", + "SUB2API_URL": "", + "SUB2API_TOKEN": "", + "sub_api_key": "", + "sub_plan": "team", + "default_address": { + "street": "", + "city": "", + "state": "", + "zip": "", + "country": "" + }, + "cards": [], + "teams": [] +} +``` + +### 3. 配置说明 + +| 配置项 | 说明 | +|--------|------| +| `mail_provider` | 邮箱服务提供商:`gptmail` 或 `npcmail` | +| `gptmail_api_key` | GPTMail API 密钥 | +| `npcmail_api_key` | NPCMail API 密钥 | +| `npcmail_domain` | NPCMail 域名 | +| `proxy` | 代理地址 | +| `enable_oauth` | 是否启用 OAuth 自动授权 | +| `SUB2API_URL` | Sub2Api 平台 URL | +| `SUB2API_TOKEN` | Sub2Api 平台 Token | +| `sub_plan` | 订阅计划:`team` 或 `plus` | + +### 4. 启动服务 + +```bash +python app.py +``` + +服务启动后访问 `http://localhost:5000` + +## Web 界面功能 + +- **任务控制** - 启动/停止批量注册任务 +- **实时日志** - SSE 实时显示注册进度 +- **账号列表** - 查看所有已注册账号 +- **账号删除** - 批量删除账号 +- **Token 导出** - 导出 OAuth Token +- **配置管理** - 修改配置文件 + +## 输出文件 + +- `registered_accounts.txt` - 注册账号(格式:邮箱----密码----邮箱密码----OAuth状态) +- `registered_accounts.csv` - CSV 格式账号 +- `codex_tokens/` - OAuth Token JSON 文件 + +## API 接口 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/` | GET | Web 界面 | +| `/api/config` | GET/POST | 获取/保存配置 | +| `/api/start` | POST | 启动注册任务 | +| `/api/stop` | POST | 停止注册任务 | +| `/api/status` | GET | 任务状态 | +| `/api/logs` | GET | SSE 日志流 | +| `/api/accounts` | GET/DELETE | 账号列表/删除 | +| `/api/export` | POST | 导出 Token | + +## 注意事项 + +1. 请确保临时邮箱 API 密钥有效 +2. 代理需要稳定,建议使用住宅代理 +3. 注册过程请遵守 OpenAI 服务条款 +4. 批量注册时注意控制频率,避免触发限制 + +## 端口 + +默认端口:`5000` + +## 许可证 + +仅供学习和研究使用,请遵守相关服务条款。 + +--- + +**更新日期**:2026-03-19 diff --git a/team_all-in-one/app.py b/team_all-in-one/app.py new file mode 100644 index 0000000..b34e96c --- /dev/null +++ b/team_all-in-one/app.py @@ -0,0 +1,460 @@ +""" +ChatGPT 批量注册工具 — Web 管理界面 +Flask 后端: 配置管理 / 任务控制 / SSE 实时日志 / 账号管理 / OAuth 导出 +""" + +import os +import io +import csv +import json +import time +import queue +import zipfile +import threading +from datetime import datetime +from flask import Flask, request, jsonify, Response, render_template, send_file + +app = Flask(__name__) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +CONFIG_PATH = os.path.join(BASE_DIR, "config.json") +ACCOUNTS_FILE = os.path.join(BASE_DIR, "registered_accounts.txt") +ACCOUNTS_CSV = os.path.join(BASE_DIR, "registered_accounts.csv") +AK_FILE = os.path.join(BASE_DIR, "ak.txt") +RK_FILE = os.path.join(BASE_DIR, "rk.txt") +TOKEN_DIR = os.path.join(BASE_DIR, "codex_tokens") + +# ── Task state ────────────────────────────────────────────── +_task_lock = threading.Lock() +_task_running = False +_task_thread = None +_task_stop_event = threading.Event() +_task_progress = {"total": 0, "done": 0, "success": 0, "fail": 0} + +# ── SSE log broadcast ────────────────────────────────────── +_log_subscribers: list[queue.Queue] = [] +_log_lock = threading.Lock() +_recent_logs: list[str] = [] +_recent_log_limit = 200 + +def _broadcast_log(line: str): + with _log_lock: + _recent_logs.append(line) + if len(_recent_logs) > _recent_log_limit: + del _recent_logs[:len(_recent_logs) - _recent_log_limit] + dead = [] + for q in _log_subscribers: + try: + q.put_nowait(line) + except queue.Full: + dead.append(q) + for q in dead: + _log_subscribers.remove(q) + +class _LogCapture(io.TextIOBase): + """Captures print() output and broadcasts via SSE while also writing to real stdout.""" + def __init__(self, real_stdout): + self._real = real_stdout + def write(self, s): + if s and s.strip(): + _broadcast_log(s.rstrip("\n\r")) + return self._real.write(s) + def flush(self): + return self._real.flush() + +# ── Config helpers ────────────────────────────────────────── +def _read_config(): + if os.path.exists(CONFIG_PATH): + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + return json.load(f) + return {} + +def _write_config(cfg): + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=4, ensure_ascii=False) + +# ── Account helpers ───────────────────────────────────────── +def _parse_accounts(): + accounts = [] + if not os.path.exists(ACCOUNTS_FILE): + return accounts + with open(ACCOUNTS_FILE, "r", encoding="utf-8") as f: + for i, line in enumerate(f): + line = line.strip() + if not line: + continue + parts = line.split("----") + acc = { + "index": i, + "email": parts[0] if len(parts) > 0 else "", + "password": parts[1] if len(parts) > 1 else "", + "email_password": parts[2] if len(parts) > 2 else "", + "oauth_status": parts[3] if len(parts) > 3 else "", + "raw": line, + } + accounts.append(acc) + return accounts + +def _write_accounts(accounts): + with open(ACCOUNTS_FILE, "w", encoding="utf-8") as f: + for acc in accounts: + f.write(acc["raw"] + "\n") + + +# ═══════════════════════════ ROUTES ═══════════════════════ + +@app.route("/") +def index(): + return render_template("index.html") + + +# ── Config ────────────────────────────────────────────────── +@app.route("/api/config", methods=["GET"]) +def get_config(): + return jsonify(_read_config()) + +@app.route("/api/config", methods=["POST"]) +def save_config(): + cfg = request.get_json(force=True) + _write_config(cfg) + return jsonify({"ok": True}) + + +# ── Task control ──────────────────────────────────────────── +@app.route("/api/start", methods=["POST"]) +def start_task(): + global _task_running, _task_thread, _task_progress + with _task_lock: + if _task_running: + return jsonify({"ok": False, "error": "任务正在运行中"}), 409 + + body = request.get_json(force=True) or {} + count = int(body.get("count", 1)) + workers = int(body.get("workers", 1)) + proxy = body.get("proxy", "").strip() or None + + _task_stop_event.clear() + _task_progress = {"total": count, "done": 0, "success": 0, "fail": 0} + + def _run(): + global _task_running + import sys + real_stdout = sys.__stdout__ + sys.stdout = _LogCapture(real_stdout) + try: + # Reload config_loader with fresh config + import importlib + import config_loader + importlib.reload(config_loader) + config_loader.run_batch( + total_accounts=count, + output_file="registered_accounts.txt", + max_workers=workers, + proxy=proxy, + ) + except Exception as e: + import traceback + _broadcast_log(f"❌ 任务异常: {e}") + _broadcast_log(traceback.format_exc()) + finally: + sys.stdout = real_stdout + with _task_lock: + _task_running = False + _broadcast_log("__TASK_DONE__") + + _task_running = True + _task_thread = threading.Thread(target=_run, daemon=True) + _task_thread.start() + return jsonify({"ok": True}) + + +@app.route("/api/stop", methods=["POST"]) +def stop_task(): + global _task_running + _task_stop_event.set() + # Force stop isn't trivial for threads; we set a flag. + _broadcast_log("⚠️ 收到停止指令,将在当前账号完成后停止") + return jsonify({"ok": True}) + + +@app.route("/api/status", methods=["GET"]) +def task_status(): + return jsonify({ + "running": _task_running, + "progress": _task_progress, + }) + + +# ── SSE Logs ──────────────────────────────────────────────── +@app.route("/api/logs") +def sse_logs(): + q = queue.Queue(maxsize=500) + with _log_lock: + _log_subscribers.append(q) + history = list(_recent_logs) + + def stream(): + try: + for msg in history: + yield f"data: {msg}\n\n" + while True: + try: + msg = q.get(timeout=30) + yield f"data: {msg}\n\n" + except queue.Empty: + yield ": keepalive\n\n" + except GeneratorExit: + pass + finally: + with _log_lock: + if q in _log_subscribers: + _log_subscribers.remove(q) + + return Response(stream(), mimetype="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}) + + +# ── Accounts ──────────────────────────────────────────────── +@app.route("/api/accounts", methods=["GET"]) +def list_accounts(): + return jsonify(_parse_accounts()) + + +@app.route("/api/accounts", methods=["DELETE"]) +def delete_accounts(): + body = request.get_json(force=True) or {} + indices = set(body.get("indices", [])) + mode = body.get("mode", "selected") # "all" or "selected" + + accounts = _parse_accounts() + if mode == "all": + _write_accounts([]) + return jsonify({"ok": True, "deleted": len(accounts)}) + + remaining = [a for a in accounts if a["index"] not in indices] + _write_accounts(remaining) + return jsonify({"ok": True, "deleted": len(accounts) - len(remaining)}) + + +# ── OAuth Export ──────────────────────────────────────────── +@app.route("/api/export", methods=["POST"]) +def export_oauth(): + """ + Export individual .json token files from codex_tokens/ as a ZIP. + - mode: "all" → include every file in codex_tokens/ + - mode: "selected" → only include files whose content contains one of the selected emails + """ + body = request.get_json(force=True) or {} + mode = body.get("mode", "all") # "all" or "selected" + indices = set(body.get("indices", [])) + + # Resolve the email list to filter against + if mode == "selected": + accounts = _parse_accounts() + target_emails = {a["email"] for a in accounts if a["index"] in indices} + else: + target_emails = None # None = include all + + if not os.path.isdir(TOKEN_DIR): + return jsonify({"error": "codex_tokens 目录不存在"}), 404 + + buf = io.BytesIO() + exported = 0 + + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for fname in sorted(os.listdir(TOKEN_DIR)): + if not fname.endswith(".json"): + continue + fpath = os.path.join(TOKEN_DIR, fname) + try: + with open(fpath, "r", encoding="utf-8") as tf: + content = tf.read() + except Exception: + continue + + # Filtering: if selected mode, check if this file's email matches + if target_emails is not None: + # Try to match by filename (email is the filename stem) + stem = fname[:-5] # remove .json + matched = any(em in stem or em in content for em in target_emails) + if not matched: + continue + + # Write directly at ZIP root: .json + zf.writestr(fname, content) + exported += 1 + + if exported == 0: + return jsonify({"error": f"没有找到匹配的 Token 文件(共扫描 codex_tokens/)"}), 404 + + buf.seek(0) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + return send_file( + buf, + mimetype="application/zip", + as_attachment=True, + download_name=f"codex_tokens_{ts}.zip" + ) + + + +# ── 代绑订阅余额查询 ────────────────────────────────────── +@app.route("/api/sub-balance", methods=["POST"]) +def sub_balance(): + """查询代绑订阅 API 余额""" + body = request.get_json(force=True) or {} + api_key = body.get("api_key", "").strip() + if not api_key: + return jsonify({"ok": False, "error": "请填写 API Key"}), 400 + try: + import requests as std_requests + resp = std_requests.get( + "https://sub.zenscaleai.com/api/v1/balance", + headers={"Authorization": f"Bearer {api_key}"}, + timeout=15, + ) + return jsonify({"ok": True, "data": resp.json(), "status": resp.status_code}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}) + + +# ── Team 母号注册 ────────────────────────────────────────── +@app.route("/api/register-master", methods=["POST"]) +def register_master(): + """注册 Team 母号 → 代绑订阅 → 获取 Team 信息 → 自动导入""" + global _task_running, _task_thread + with _task_lock: + if _task_running: + return jsonify({"ok": False, "error": "有任务正在运行中"}), 409 + + body = request.get_json(force=True) or {} + api_key = body.get("api_key", "").strip() + card = body.get("card", "").strip() + plan = body.get("plan", "team").strip() + proxy = body.get("proxy", "").strip() or None + card_number = body.get("card_number", "").strip() # 用于标记已使用 + + if not api_key: + return jsonify({"ok": False, "error": "请填写 API Key"}), 400 + if not card: + return jsonify({"ok": False, "error": "请填写卡信息"}), 400 + + _task_stop_event.clear() + + def _run(): + global _task_running + import sys + real_stdout = sys.__stdout__ + sys.stdout = _LogCapture(real_stdout) + try: + import importlib + import config_loader + importlib.reload(config_loader) + + # 1. 注册账号 + _broadcast_log("### 📝 开始注册 Team 母号 ###") + reg_result = config_loader.register_team_master(proxy=proxy) + + if not reg_result.get("access_token"): + _broadcast_log("❌ 注册成功但未获取到 AccessToken,无法继续") + return + + # 2. 调用代绑订阅 API + _broadcast_log("### 💳 调用代绑订阅 API ###") + import requests as std_requests + try: + sub_resp = std_requests.post( + "https://sub.zenscaleai.com/api/v1/subscribe", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "access_token": reg_result["access_token"], + "card": card, + "plan": plan, + }, + timeout=120, + ) + sub_data = sub_resp.json() + _broadcast_log(f"订阅响应: {json.dumps(sub_data, ensure_ascii=False)}") + + if not sub_data.get("success"): + _broadcast_log(f"❌ 订阅失败: {sub_data.get('error', '未知错误')}") + return + + _broadcast_log(f"✅ 订阅绑定成功! 计划: {sub_data.get('plan', plan)}") + except Exception as e: + _broadcast_log(f"❌ 订阅 API 请求异常: {e}") + return + + # 3. 等待订阅生效 + _broadcast_log("⏳ 等待订阅生效 (5秒)...") + time.sleep(5) + + # 4. 获取 Team 信息 + _broadcast_log("### 🔍 获取 Team 信息 ###") + session_token = reg_result.get("session_token", "") + team_info = config_loader.get_team_info_from_session(session_token, proxy=proxy) + + if not team_info or not team_info.get("account_id"): + _broadcast_log("⚠️ 未能获取 Team 信息,使用基础信息创建") + payload = config_loader._decode_jwt_payload(reg_result["access_token"]) + auth_info = payload.get("https://api.openai.com/auth", {}) + fallback_id = auth_info.get("chatgpt_account_id", "") + team_info = { + "name": f"Team-{fallback_id[:8]}", + "account_id": fallback_id, + "auth_token": reg_result["access_token"], + "session_token": session_token, + } + + # 5. 导入到 config.json + new_team = { + "name": team_info.get("name", "New Team"), + "account_id": team_info.get("account_id", ""), + "auth_token": team_info.get("auth_token", ""), + "session_token": team_info.get("session_token", ""), + "max_invites": 5, + } + cfg = _read_config() + if "teams" not in cfg: + cfg["teams"] = [] + cfg["teams"].append(new_team) + _write_config(cfg) + + # 6. 标记卡为已使用 + if card_number: + cfg2 = _read_config() + for c in cfg2.get("cards", []): + if c.get("number", "").strip() == card_number: + c["used"] = True + break + _write_config(cfg2) + + _broadcast_log(f"✅ Team 信息已导入!") + _broadcast_log(f" Team 名称: {new_team['name']}") + _broadcast_log(f" Account ID: {new_team['account_id']}") + _broadcast_log(f" Auth Token: {new_team['auth_token'][:50]}...") + _broadcast_log(f" Session Token: {(new_team['session_token'] or '')[:50]}...") + _broadcast_log(f"### 🎉 Team 母号注册全流程完成! ###") + + except Exception as e: + import traceback + _broadcast_log(f"❌ 母号注册异常: {e}") + _broadcast_log(traceback.format_exc()) + finally: + sys.stdout = real_stdout + with _task_lock: + _task_running = False + _broadcast_log("__TASK_DONE__") + + _task_running = True + _task_thread = threading.Thread(target=_run, daemon=True) + _task_thread.start() + return jsonify({"ok": True}) + + +if __name__ == "__main__": + print("🚀 ChatGPT 注册管理面板启动: http://localhost:5000") + app.run(host="0.0.0.0", port=5000, debug=False, threaded=True) diff --git a/team_all-in-one/config.json b/team_all-in-one/config.json new file mode 100644 index 0000000..d7a0eba --- /dev/null +++ b/team_all-in-one/config.json @@ -0,0 +1,24 @@ +{ + "mail_provider": "gptmail", + "gptmail_base": "https://mail.chatgpt.org.uk", + "gptmail_api_key": "", + "npcmail_api_key": "", + "npcmail_domain": "git-hub.email", + "proxy": "", + "enable_oauth": true, + "oauth_issuer": "https://auth.openai.com", + "oauth_client_id": "app_EMoamEEZ73f0CkXaXp7hrann", + "SUB2API_URL": "", + "SUB2API_TOKEN": "", + "sub_api_key": "", + "sub_plan": "team", + "default_address": { + "street": "", + "city": "", + "state": "", + "zip": "", + "country": "" + }, + "cards": [], + "teams": [] +} \ No newline at end of file diff --git a/team_all-in-one/config_loader.py b/team_all-in-one/config_loader.py new file mode 100644 index 0000000..ff875fb --- /dev/null +++ b/team_all-in-one/config_loader.py @@ -0,0 +1,2135 @@ +""" +ChatGPT 批量自动注册工具 (纯协议版) - DuckMail 临时邮箱 + Team 邀请 + Codex OAuth(CPA、SUB2API) +依赖: pip install curl_cffi +功能: 纯协议实现注册 → Team 邀请 → Codex OAuth 全流程,无需浏览器 +""" + +import os +import re +import uuid +import json +import random +import string +import time +import sys +import threading +import traceback +import secrets +import hashlib +import base64 +from concurrent.futures import ThreadPoolExecutor, as_completed +from urllib.parse import urlparse, parse_qs, urlencode, quote + +from curl_cffi import requests as curl_requests + + +# ================= 加载配置 ================= +def _load_config(): + """从 config.json 加载配置,环境变量优先级更高""" + config = { + "total_accounts": 4, + "mail_provider": "gptmail", + "gptmail_api_key": "gpt-test", + "gptmail_base": "https://mail.chatgpt.org.uk", + "npcmail_api_key": "", + "npcmail_base": "https://dash.xphdfs.me", + "npcmail_domain": "", + "cmail_api_key": "", + "cmail_base": "", + "cmail_domain": "", + "cmail_expiry_days": 7, + "proxy": "", + "output_file": "registered_accounts.txt", + "csv_file": "registered_accounts.csv", + "invite_tracker_file": "invite_tracker.json", + "enable_oauth": True, + "oauth_required": True, + "oauth_issuer": "https://auth.openai.com", + "oauth_client_id": "app_EMoamEEZ73f0CkXaXp7hrann", + "oauth_redirect_uri": "http://localhost:1455/auth/callback", + "ak_file": "ak.txt", + "rk_file": "rk.txt", + "token_json_dir": "codex_tokens", + "upload_api_url": "", + "upload_api_token": "", + "SUB2API_URL": "", + "SUB2API_TOKEN": "", + "teams": [], + } + + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + if os.path.exists(config_path): + try: + with open(config_path, "r", encoding="utf-8") as f: + file_config = json.load(f) + config.update(file_config) + except Exception as e: + print(f"⚠️ 加载 config.json 失败: {e}") + + config["mail_provider"] = os.environ.get("MAIL_PROVIDER", config["mail_provider"]) + config["gptmail_api_key"] = os.environ.get("GPTMAIL_API_KEY", config["gptmail_api_key"]) + config["gptmail_base"] = os.environ.get("GPTMAIL_BASE", config["gptmail_base"]) + config["npcmail_api_key"] = os.environ.get("NPCMAIL_API_KEY", config.get("npcmail_api_key", "")) + config["npcmail_base"] = os.environ.get("NPCMAIL_BASE", config.get("npcmail_base", "")) + config["npcmail_domain"] = os.environ.get("NPCMAIL_DOMAIN", config.get("npcmail_domain", "")) + config["cmail_api_key"] = os.environ.get("CMAIL_API_KEY", config["cmail_api_key"]) + config["cmail_base"] = os.environ.get("CMAIL_BASE", config["cmail_base"]) + config["cmail_domain"] = os.environ.get("CMAIL_DOMAIN", config["cmail_domain"]) + config["cmail_expiry_days"] = int(os.environ.get("CMAIL_EXPIRY_DAYS", config["cmail_expiry_days"])) + config["proxy"] = os.environ.get("PROXY", config["proxy"]) + config["total_accounts"] = int(os.environ.get("TOTAL_ACCOUNTS", config["total_accounts"])) + config["enable_oauth"] = os.environ.get("ENABLE_OAUTH", config["enable_oauth"]) + config["oauth_required"] = os.environ.get("OAUTH_REQUIRED", config["oauth_required"]) + config["oauth_issuer"] = os.environ.get("OAUTH_ISSUER", config["oauth_issuer"]) + config["oauth_client_id"] = os.environ.get("OAUTH_CLIENT_ID", config["oauth_client_id"]) + config["oauth_redirect_uri"] = os.environ.get("OAUTH_REDIRECT_URI", config["oauth_redirect_uri"]) + config["ak_file"] = os.environ.get("AK_FILE", config["ak_file"]) + config["rk_file"] = os.environ.get("RK_FILE", config["rk_file"]) + config["token_json_dir"] = os.environ.get("TOKEN_JSON_DIR", config["token_json_dir"]) + config["upload_api_url"] = os.environ.get("UPLOAD_API_URL", config["upload_api_url"]) + config["upload_api_token"] = os.environ.get("UPLOAD_API_TOKEN", config["upload_api_token"]) + config["SUB2API_URL"] = os.environ.get("SUB2API_URL", config["SUB2API_URL"]) + config["SUB2API_TOKEN"] = os.environ.get("SUB2API_TOKEN", config["SUB2API_TOKEN"]) + + return config + + +def _as_bool(value): + if isinstance(value, bool): + return value + if value is None: + return False + return str(value).strip().lower() in {"1", "true", "yes", "y", "on"} + + +_CONFIG = _load_config() +MAIL_PROVIDER = (_CONFIG.get("mail_provider") or "gptmail").strip().lower() +GPTMAIL_API_KEY = _CONFIG.get("gptmail_api_key", "gpt-test") +GPTMAIL_BASE = _CONFIG.get("gptmail_base", "https://mail.chatgpt.org.uk").rstrip("/") +CMAIL_API_KEY = _CONFIG.get("npcmail_api_key") or _CONFIG.get("cmail_api_key", "") +CMAIL_BASE = (_CONFIG.get("npcmail_base") or _CONFIG.get("cmail_base", "") or "").rstrip("/") +CMAIL_DOMAIN = (_CONFIG.get("npcmail_domain") or _CONFIG.get("cmail_domain", "") or "").strip() +CMAIL_EXPIRY_DAYS = int(_CONFIG.get("cmail_expiry_days", 7) or 7) +DEFAULT_TOTAL_ACCOUNTS = _CONFIG["total_accounts"] +DEFAULT_PROXY = _CONFIG["proxy"] +DEFAULT_OUTPUT_FILE = _CONFIG["output_file"] +CSV_FILE = _CONFIG.get("csv_file", "registered_accounts.csv") +INVITE_TRACKER_FILE = _CONFIG.get("invite_tracker_file", "invite_tracker.json") +ENABLE_OAUTH = _as_bool(_CONFIG.get("enable_oauth", True)) +OAUTH_REQUIRED = _as_bool(_CONFIG.get("oauth_required", True)) +OAUTH_ISSUER = _CONFIG["oauth_issuer"].rstrip("/") +OAUTH_CLIENT_ID = _CONFIG["oauth_client_id"] +OAUTH_REDIRECT_URI = _CONFIG["oauth_redirect_uri"] +AK_FILE = _CONFIG["ak_file"] +RK_FILE = _CONFIG["rk_file"] +TOKEN_JSON_DIR = _CONFIG["token_json_dir"] +UPLOAD_API_URL = _CONFIG["upload_api_url"] +UPLOAD_API_TOKEN = _CONFIG["upload_api_token"] +SUB2API_URL = _CONFIG["SUB2API_URL"] +SUB2API_TOKEN = _CONFIG["SUB2API_TOKEN"] +TEAMS = _CONFIG.get("teams", []) + +if not TEAMS: + print("⚠️ 警告: 未设置 teams,请在 config.json 中配置 Team 信息") + +# 全局线程锁 +_print_lock = threading.Lock() +_file_lock = threading.Lock() + + +def _mail_provider_name(): + if MAIL_PROVIDER in {"cmail", "npcmail"}: + return "npcmail" + return "gptmail" + + +# ================= Chrome 指纹配置 ================= +_CHROME_PROFILES = [ + { + "major": 131, "impersonate": "chrome131", + "build": 6778, "patch_range": (69, 205), + "sec_ch_ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + }, + { + "major": 133, "impersonate": "chrome133a", + "build": 6943, "patch_range": (33, 153), + "sec_ch_ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', + }, + { + "major": 136, "impersonate": "chrome136", + "build": 7103, "patch_range": (48, 175), + "sec_ch_ua": '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"', + }, + { + "major": 142, "impersonate": "chrome142", + "build": 7540, "patch_range": (30, 150), + "sec_ch_ua": '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + }, +] + + +def _random_chrome_version(): + profile = random.choice(_CHROME_PROFILES) + major = profile["major"] + build = profile["build"] + patch = random.randint(*profile["patch_range"]) + full_ver = f"{major}.0.{build}.{patch}" + ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{full_ver} Safari/537.36" + return profile["impersonate"], major, full_ver, ua, profile["sec_ch_ua"] + + +def _random_delay(low=0.3, high=1.0): + time.sleep(random.uniform(low, high)) + + +def _make_trace_headers(): + trace_id = random.randint(10**17, 10**18 - 1) + parent_id = random.randint(10**17, 10**18 - 1) + tp = f"00-{uuid.uuid4().hex}-{format(parent_id, '016x')}-01" + return { + "traceparent": tp, "tracestate": "dd=s:1;o:rum", + "x-datadog-origin": "rum", "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": str(trace_id), "x-datadog-parent-id": str(parent_id), + } + + +def _generate_pkce(): + code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii") + digest = hashlib.sha256(code_verifier.encode("ascii")).digest() + code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + return code_verifier, code_challenge + + +def _generate_password(length=14): + lower = string.ascii_lowercase + upper = string.ascii_uppercase + digits = string.digits + special = "!@#$%&*" + pwd = [random.choice(lower), random.choice(upper), + random.choice(digits), random.choice(special)] + all_chars = lower + upper + digits + special + pwd += [random.choice(all_chars) for _ in range(length - 4)] + random.shuffle(pwd) + return "".join(pwd) + + +def _random_name(): + first_names = [ + "James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Ethan", "Sophia", + "Lucas", "Mia", "Mason", "Isabella", "Logan", "Charlotte", "Alexander", + "Amelia", "Benjamin", "Harper", "William", "Evelyn", "Henry", "Abigail", + "Sebastian", "Emily", "Jack", "Elizabeth", "Michael", "Robert", "David", + "Joseph", "Thomas", "Christopher", "Daniel", "Matthew", "Anthony", + "Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Susan", "Jessica", + "Sarah", "Karen", "Lisa", "Nancy", "Betty", "Margaret", "Sandra", + "Ashley", "Kimberly", "Donna", "Michelle", "Dorothy", "Carol", + "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", + ] + last_names = [ + "Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", + "Clark", "Hall", "Young", "Anderson", "Thomas", "Jackson", "White", + "Harris", "Martin", "Thompson", "Garcia", "Robinson", "Lewis", + "Walker", "Allen", "King", "Wright", "Scott", "Green", "Adams", + "Nelson", "Baker", "Rivera", "Campbell", "Mitchell", "Carter", + "Roberts", "Phillips", "Evans", "Turner", "Diaz", "Parker", + ] + return f"{random.choice(first_names)} {random.choice(last_names)}" + + +def _random_birthdate(): + y = random.randint(1980, 2002) + m = random.randint(1, 12) + d = random.randint(1, 28) + return f"{y}-{m:02d}-{d:02d}" + + +# ================= Sentinel Token (PoW) ================= + +class SentinelTokenGenerator: + """纯 Python 版本 sentinel token 生成器(PoW)""" + + MAX_ATTEMPTS = 500000 + ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + + def __init__(self, device_id=None, user_agent=None): + self.device_id = device_id or str(uuid.uuid4()) + self.user_agent = user_agent or ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/145.0.0.0 Safari/537.36" + ) + self.requirements_seed = str(random.random()) + self.sid = str(uuid.uuid4()) + + @staticmethod + def _fnv1a_32(text: str): + h = 2166136261 + for ch in text: + h ^= ord(ch) + h = (h * 16777619) & 0xFFFFFFFF + h ^= (h >> 16) + h = (h * 2246822507) & 0xFFFFFFFF + h ^= (h >> 13) + h = (h * 3266489909) & 0xFFFFFFFF + h ^= (h >> 16) + h &= 0xFFFFFFFF + return format(h, "08x") + + def _get_config(self): + now_str = time.strftime( + "%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", + time.gmtime(), + ) + perf_now = random.uniform(1000, 50000) + time_origin = time.time() * 1000 - perf_now + nav_prop = random.choice([ + "vendorSub", "productSub", "vendor", "maxTouchPoints", + "scheduling", "userActivation", "doNotTrack", "geolocation", + "connection", "plugins", "mimeTypes", "pdfViewerEnabled", + "webkitTemporaryStorage", "webkitPersistentStorage", + "hardwareConcurrency", "cookieEnabled", "credentials", + "mediaDevices", "permissions", "locks", "ink", + ]) + nav_val = f"{nav_prop}-undefined" + + return [ + "1920x1080", now_str, 4294705152, random.random(), + self.user_agent, + "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", + None, None, "en-US", "en-US,en", random.random(), nav_val, + random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]), + random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]), + perf_now, self.sid, "", + random.choice([4, 8, 12, 16]), time_origin, + ] + + @staticmethod + def _base64_encode(data): + raw = json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return base64.b64encode(raw).decode("ascii") + + def _run_check(self, start_time, seed, difficulty, config, nonce): + config[3] = nonce + config[9] = round((time.time() - start_time) * 1000) + data = self._base64_encode(config) + hash_hex = self._fnv1a_32(seed + data) + diff_len = len(difficulty) + if hash_hex[:diff_len] <= difficulty: + return data + "~S" + return None + + def generate_token(self, seed=None, difficulty=None): + seed = seed if seed is not None else self.requirements_seed + difficulty = str(difficulty or "0") + start_time = time.time() + config = self._get_config() + for i in range(self.MAX_ATTEMPTS): + result = self._run_check(start_time, seed, difficulty, config, i) + if result: + return "gAAAAAB" + result + return "gAAAAAB" + self.ERROR_PREFIX + self._base64_encode(str(None)) + + def generate_requirements_token(self): + config = self._get_config() + config[3] = 1 + config[9] = round(random.uniform(5, 50)) + data = self._base64_encode(config) + return "gAAAAAC" + data + + +def fetch_sentinel_challenge(session, device_id, flow="authorize_continue", user_agent=None, + sec_ch_ua=None, impersonate=None): + generator = SentinelTokenGenerator(device_id=device_id, user_agent=user_agent) + req_body = {"p": generator.generate_requirements_token(), "id": device_id, "flow": flow} + headers = { + "Content-Type": "text/plain;charset=UTF-8", + "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html", + "Origin": "https://sentinel.openai.com", + "User-Agent": user_agent or "Mozilla/5.0", + "sec-ch-ua": sec_ch_ua or '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', + } + kwargs = {"data": json.dumps(req_body), "headers": headers, "timeout": 20} + if impersonate: + kwargs["impersonate"] = impersonate + try: + resp = session.post("https://sentinel.openai.com/backend-api/sentinel/req", **kwargs) + except Exception: + return None + if resp.status_code != 200: + return None + try: + return resp.json() + except Exception: + return None + + +def build_sentinel_token(session, device_id, flow="authorize_continue", user_agent=None, + sec_ch_ua=None, impersonate=None): + challenge = fetch_sentinel_challenge(session, device_id, flow=flow, user_agent=user_agent, + sec_ch_ua=sec_ch_ua, impersonate=impersonate) + if not challenge: + return None + c_value = challenge.get("token", "") + if not c_value: + return None + pow_data = challenge.get("proofofwork") or {} + generator = SentinelTokenGenerator(device_id=device_id, user_agent=user_agent) + if pow_data.get("required") and pow_data.get("seed"): + p_value = generator.generate_token(seed=pow_data.get("seed"), difficulty=pow_data.get("difficulty", "0")) + else: + p_value = generator.generate_requirements_token() + return json.dumps({"p": p_value, "t": "", "c": c_value, "id": device_id, "flow": flow}, separators=(",", ":")) + + +def _extract_code_from_url(url: str): + if not url or "code=" not in url: + return None + try: + return parse_qs(urlparse(url).query).get("code", [None])[0] + except Exception: + return None + + +def _decode_jwt_payload(token: str): + try: + parts = token.split(".") + if len(parts) != 3: + return {} + payload = parts[1] + padding = 4 - len(payload) % 4 + if padding != 4: + payload += "=" * padding + decoded = base64.urlsafe_b64decode(payload) + return json.loads(decoded) + except Exception: + return {} + + +# ================= Token 保存与上传 ================= +def _build_default_model_mapping() -> dict: + return { + "gpt-3.5-turbo": "gpt-3.5-turbo", + "gpt-3.5-turbo-0125": "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-1106": "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k", + "gpt-4": "gpt-4", + "gpt-4-turbo": "gpt-4-turbo", + "gpt-4-turbo-preview": "gpt-4-turbo-preview", + "gpt-4o": "gpt-4o", + "gpt-4o-2024-08-06": "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20": "gpt-4o-2024-11-20", + "gpt-4o-mini": "gpt-4o-mini", + "gpt-4o-mini-2024-07-18": "gpt-4o-mini-2024-07-18", + "gpt-4.5-preview": "gpt-4.5-preview", + "gpt-4.1": "gpt-4.1", + "gpt-4.1-mini": "gpt-4.1-mini", + "gpt-4.1-nano": "gpt-4.1-nano", + "o1": "o1", + "o1-preview": "o1-preview", + "o1-mini": "o1-mini", + "o1-pro": "o1-pro", + "o3": "o3", + "o3-mini": "o3-mini", + "o3-pro": "o3-pro", + "o4-mini": "o4-mini", + "gpt-5": "gpt-5", + "gpt-5-2025-08-07": "gpt-5-2025-08-07", + "gpt-5-chat": "gpt-5-chat", + "gpt-5-chat-latest": "gpt-5-chat-latest", + "gpt-5-codex": "gpt-5-codex", + "gpt-5.3-codex-spark": "gpt-5.3-codex-spark", + "gpt-5-pro": "gpt-5-pro", + "gpt-5-pro-2025-10-06": "gpt-5-pro-2025-10-06", + "gpt-5-mini": "gpt-5-mini", + "gpt-5-mini-2025-08-07": "gpt-5-mini-2025-08-07", + "gpt-5-nano": "gpt-5-nano", + "gpt-5-nano-2025-08-07": "gpt-5-nano-2025-08-07", + "gpt-5.1": "gpt-5.1", + "gpt-5.1-2025-11-13": "gpt-5.1-2025-11-13", + "gpt-5.1-chat-latest": "gpt-5.1-chat-latest", + "gpt-5.1-codex": "gpt-5.1-codex", + "gpt-5.1-codex-max": "gpt-5.1-codex-max", + "gpt-5.1-codex-mini": "gpt-5.1-codex-mini", + "gpt-5.2": "gpt-5.2", + "gpt-5.2-2025-12-11": "gpt-5.2-2025-12-11", + "gpt-5.2-chat-latest": "gpt-5.2-chat-latest", + "gpt-5.2-codex": "gpt-5.2-codex", + "gpt-5.2-pro": "gpt-5.2-pro", + "gpt-5.2-pro-2025-12-11": "gpt-5.2-pro-2025-12-11", + "gpt-5.4": "gpt-5.4", + "gpt-5.4-2026-03-05": "gpt-5.4-2026-03-05", + "gpt-5.3-codex": "gpt-5.3-codex", + "chatgpt-4o-latest": "chatgpt-4o-latest", + "gpt-4o-audio-preview": "gpt-4o-audio-preview", + "gpt-4o-realtime-preview": "gpt-4o-realtime-preview", + } + + +def _build_codex_account_payload(email: str, tokens: dict) -> dict: + """将 OAuth token 转换为 codex.csun.site /api/v1/admin/accounts 所需的 payload 格式""" + access_token = tokens.get("access_token", "") + refresh_token = tokens.get("refresh_token", "") + id_token = tokens.get("id_token", "") + expires_in = tokens.get("expires_in", 863999) + + # 从 access_token JWT 中提取字段 + at_payload = _decode_jwt_payload(access_token) if access_token else {} + at_auth = at_payload.get("https://api.openai.com/auth", {}) + chatgpt_account_id = at_auth.get("chatgpt_account_id", "") + chatgpt_user_id = at_auth.get("chatgpt_user_id", "") + exp_timestamp = at_payload.get("exp", 0) + expires_at = exp_timestamp if isinstance(exp_timestamp, int) and exp_timestamp > 0 else int(time.time()) + expires_in + + # 从 id_token JWT 中提取 organization_id + it_payload = _decode_jwt_payload(id_token) if id_token else {} + it_auth = it_payload.get("https://api.openai.com/auth", {}) + organization_id = it_auth.get("organization_id", "") + if not organization_id: + orgs = it_auth.get("organizations", []) + if orgs: + organization_id = (orgs[0] or {}).get("id", "") + + return { + "name": email, + "notes": "", + "platform": "openai", + "type": "oauth", + "credentials": { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in": expires_in, + "expires_at": expires_at, + "client_id": OAUTH_CLIENT_ID, + "chatgpt_account_id": chatgpt_account_id, + "chatgpt_user_id": chatgpt_user_id, + "organization_id": organization_id, + "model_mapping": _build_default_model_mapping(), + }, + "extra": { + "email": email, + "openai_oauth_responses_websockets_v2_mode": "off", + "openai_oauth_responses_websockets_v2_enabled": False, + }, + "proxy_id": None, + "concurrency": 10, + "priority": 1, + "rate_multiplier": 1, + "group_ids": [2], #根据实际情况修改分组 + "expires_at": None, + "auto_pause_on_expired": True, + } + +def _save_codex_tokens(email: str, tokens: dict): + access_token = tokens.get("access_token", "") + refresh_token = tokens.get("refresh_token", "") + id_token = tokens.get("id_token", "") + + if access_token: + with _file_lock: + with open(AK_FILE, "a", encoding="utf-8") as f: + f.write(f"{access_token}\n") + + if refresh_token: + with _file_lock: + with open(RK_FILE, "a", encoding="utf-8") as f: + f.write(f"{refresh_token}\n") + + if not access_token: + return + + payload = _decode_jwt_payload(access_token) + auth_info = payload.get("https://api.openai.com/auth", {}) + account_id = auth_info.get("chatgpt_account_id", "") + + exp_timestamp = payload.get("exp") + expired_str = "" + if isinstance(exp_timestamp, int) and exp_timestamp > 0: + from datetime import datetime, timezone + exp_dt = datetime.fromtimestamp(exp_timestamp, tz=timezone.utc) + expired_str = exp_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + + from datetime import datetime, timezone, timedelta + now = datetime.now(tz=timezone.utc) + token_data = { + "id_token": id_token, + "access_token": access_token, + "refresh_token": refresh_token, + "account_id": account_id, + "last_refresh": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + "email": email, + "type": "codex", + "expired": expired_str, + "uploaded_platforms": [], + "cpa_uploaded": False, + "cpa_synced": False, + "uploaded_at": {}, + } + + base_dir = os.path.dirname(os.path.abspath(__file__)) + token_dir = TOKEN_JSON_DIR if os.path.isabs(TOKEN_JSON_DIR) else os.path.join(base_dir, TOKEN_JSON_DIR) + os.makedirs(token_dir, exist_ok=True) + token_path = os.path.join(token_dir, f"{email}.json") + with _file_lock: + with open(token_path, "w", encoding="utf-8") as f: + json.dump(token_data, f, ensure_ascii=False) + + if UPLOAD_API_URL: + # 推送到 CPA + _upload_token_json(token_path) + + if SUB2API_URL and SUB2API_TOKEN: + # 推送到 SUB2API + try: + api_payload = _build_codex_account_payload(email, tokens) + print(f"[SUB2API] 开始推送至 SUB2API:") + print(api_payload) + resp = curl_requests.post( + SUB2API_URL, + headers={ + "Authorization": f"Bearer {SUB2API_TOKEN}", + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "Referer": SUB2API_URL.replace("/api/v1/admin/accounts", "/admin/accounts"), + }, + json=api_payload, + timeout=30, + ) + print(f"[SUB2API] POST {SUB2API_URL} -> {resp.status_code}") + if resp.status_code not in (200, 201): + print(f"[SUB2API] 响应: {resp.text[:300]}") + except Exception as e: + print(f"[SUB2API 请求失败: {e}") + else: + print("[SUB2API] 未配置 SUB2API_URL 或 SUB2API_TOKEN,跳过推送") + + +def _upload_token_json(filepath): + mp = None + try: + from curl_cffi import CurlMime + filename = os.path.basename(filepath) + mp = CurlMime() + mp.addpart(name="file", content_type="application/json", filename=filename, local_path=filepath) + session = curl_requests.Session() + if DEFAULT_PROXY: + session.proxies = {"http": DEFAULT_PROXY, "https": DEFAULT_PROXY} + resp = session.post(UPLOAD_API_URL, multipart=mp, + headers={"Authorization": f"Bearer {UPLOAD_API_TOKEN}"}, + verify=False, timeout=30) + if resp.status_code == 200: + with _print_lock: + print(f" [CPA] Token JSON 已上传到 CPA 管理平台") + else: + with _print_lock: + print(f" [CPA] 上传失败: {resp.status_code} - {resp.text[:200]}") + except Exception as e: + with _print_lock: + print(f" [CPA] 上传异常: {e}") + finally: + if mp: + mp.close() + + +# ================= Team 邀请 ================= + +def load_invite_tracker(): + default = {"teams": {team["account_id"]: [] for team in TEAMS}} + if os.path.exists(INVITE_TRACKER_FILE): + try: + with open(INVITE_TRACKER_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict) and "teams" in data: + return data + except Exception as e: + print(f"⚠️ Failed to load invite tracker: {e}") + return default + + +def save_invite_tracker(tracker): + """保存 invite tracker(调用者应已持有 _file_lock)""" + try: + with open(INVITE_TRACKER_FILE, "w", encoding="utf-8") as f: + json.dump(tracker, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"⚠️ Failed to save invite tracker: {e}") + + +def get_available_team(tracker): + for team in TEAMS: + account_id = team["account_id"] + invited = tracker["teams"].get(account_id, []) + if len(invited) < team["max_invites"]: + return team + return None + + +def _get_fresh_access_token(team: dict, tag: str = ""): + """使用 session_token (cookie) 从 /api/auth/session 获取 fresh access_token""" + prefix = f"[{tag}] " if tag else "" + session_token = team.get("session_token", "") + if not session_token: + # 降级: 直接使用 auth_token + return team.get("auth_token", "") + + s = curl_requests.Session(verify=False, impersonate="chrome120") + if DEFAULT_PROXY: + s.proxies = {"http": DEFAULT_PROXY, "https": DEFAULT_PROXY} + s.cookies.set("__Secure-next-auth.session-token", session_token, domain="chatgpt.com") + + try: + r = s.get("https://chatgpt.com/api/auth/session", timeout=30) + if r.status_code == 200: + data = r.json() + fresh_token = data.get("accessToken", "") + if fresh_token: + with _print_lock: + print(f"{prefix}🔑 获取 fresh access_token 成功 (expires: {data.get('expires', '?')})") + return f"Bearer {fresh_token}" + with _print_lock: + print(f"{prefix}⚠️ 获取 fresh token 失败 (status={r.status_code}),降级使用 auth_token") + except Exception as e: + with _print_lock: + print(f"{prefix}⚠️ 获取 fresh token 异常: {e},降级使用 auth_token") + return team.get("auth_token", "") + + +def invite_to_team(email: str, team: dict, tag: str = ""): + """通过协议发送 Team 邀请 (自动刷新 access_token)""" + prefix = f"[{tag}] " if tag else "" + + # 获取 fresh access token + auth_token = _get_fresh_access_token(team, tag) + + session = curl_requests.Session(verify=False, impersonate="chrome120") + if DEFAULT_PROXY: + session.proxies = {"http": DEFAULT_PROXY, "https": DEFAULT_PROXY} + + headers = { + "accept": "*/*", + "accept-language": "en-US,en;q=0.9", + "authorization": auth_token, + "chatgpt-account-id": team["account_id"], + "content-type": "application/json", + "origin": "https://chatgpt.com", + "referer": "https://chatgpt.com/", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + } + payload = { + "email_addresses": [email], + "role": "standard-user", + "resend_emails": True, + } + invite_url = f"https://chatgpt.com/backend-api/accounts/{team['account_id']}/invites" + try: + response = session.post(invite_url, headers=headers, json=payload, timeout=30) + if response.status_code == 200: + result = response.json() + with _print_lock: + print(f"{prefix}[Invite] 响应: {json.dumps(result, ensure_ascii=False)[:300]}") + if result.get("account_invites"): + with _print_lock: + print(f"{prefix}✅ Successfully invited {email} to {team['name']}") + return True + elif result.get("errored_emails"): + with _print_lock: + print(f"{prefix}⚠️ Invite error for {email}: {result['errored_emails']}") + return False + else: + # 响应200但结构不符预期,视为成功(某些情况下接口直接返回空对象) + with _print_lock: + print(f"{prefix}⚠️ Invite 响应结构未知,视为已发送: {email}") + return True + else: + with _print_lock: + print(f"{prefix}❌ Failed to invite {email}: HTTP {response.status_code}") + print(f"{prefix} Response: {response.text[:200]}") + return False + except Exception as e: + with _print_lock: + print(f"{prefix}❌ Invite request failed: {e}") + return False + + +def auto_invite_to_team(email: str, tag: str = ""): + """自动选择可用 Team 并发送邀请""" + if not TEAMS: + with _print_lock: + print(f"[{tag}] ⚠️ 未配置 teams,跳过邀请" if tag else "⚠️ 未配置 teams,跳过邀请") + return False + + with _file_lock: + tracker = load_invite_tracker() + for account_id, emails in tracker["teams"].items(): + if email in emails: + with _print_lock: + print(f"[{tag}] ⚠️ {email} already invited, skipping" if tag else f"⚠️ {email} already invited") + return False + team = get_available_team(tracker) + if not team: + with _print_lock: + print(f"[{tag}] ❌ All teams are full" if tag else "❌ All teams are full") + return False + + if invite_to_team(email, team, tag=tag): + with _file_lock: + tracker = load_invite_tracker() + account_id = team["account_id"] + if account_id not in tracker["teams"]: + tracker["teams"][account_id] = [] + tracker["teams"][account_id].append(email) + save_invite_tracker(tracker) + count = len(tracker["teams"][account_id]) + with _print_lock: + print(f"[{tag}] Team status: {team['name']} has {count}/{team['max_invites']} invites" + if tag else f" Team status: {team['name']} has {count}/{team['max_invites']} invites") + return True + return False + + +# ================= CSV 保存 ================= + +def save_to_csv(email: str, password: str, dm_password: str = "", oauth_status: str = ""): + import csv + file_exists = os.path.exists(CSV_FILE) + with _file_lock: + with open(CSV_FILE, "a", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + if not file_exists: + writer.writerow(["email", "password", "duckmail_password", "oauth_status", "timestamp"]) + writer.writerow([email, password, dm_password, oauth_status, time.strftime("%Y-%m-%d %H:%M:%S")]) + + +# ================= ChatGPTRegister 核心类 ================= + +class ChatGPTRegister: + BASE = "https://chatgpt.com" + AUTH = "https://auth.openai.com" + + def __init__(self, proxy: str = None, tag: str = ""): + self.tag = tag + self.device_id = str(uuid.uuid4()) + self.auth_session_logging_id = str(uuid.uuid4()) + self.impersonate, self.chrome_major, self.chrome_full, self.ua, self.sec_ch_ua = _random_chrome_version() + + self.session = curl_requests.Session(impersonate=self.impersonate, verify=False) + self.proxy = proxy + if self.proxy: + self.session.proxies = {"http": self.proxy, "https": self.proxy} + + self.session.headers.update({ + "User-Agent": self.ua, + "Accept-Language": random.choice([ + "en-US,en;q=0.9", "en-US,en;q=0.9,zh-CN;q=0.8", + "en,en-US;q=0.9", "en-US,en;q=0.8", + ]), + "sec-ch-ua": self.sec_ch_ua, "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', "sec-ch-ua-arch": '"x86"', + "sec-ch-ua-bitness": '"64"', + "sec-ch-ua-full-version": f'"{self.chrome_full}"', + "sec-ch-ua-platform-version": f'"{random.randint(10, 15)}.0.0"', + }) + self.session.cookies.set("oai-did", self.device_id, domain="chatgpt.com") + self._callback_url = None + + def _print(self, msg): + prefix = f"[{self.tag}] " if self.tag else "" + with _print_lock: + print(f"{prefix}{msg}") + + def _log(self, step, method, url, status, body=None): + prefix = f"[{self.tag}] " if self.tag else "" + lines = [f"\n{'='*60}", f"{prefix}[Step] {step}", f"{prefix}[{method}] {url}", + f"{prefix}[Status] {status}"] + if body: + try: + lines.append(f"{prefix}[Response] {json.dumps(body, indent=2, ensure_ascii=False)[:1000]}") + except Exception: + lines.append(f"{prefix}[Response] {str(body)[:1000]}") + lines.append(f"{'='*60}") + with _print_lock: + print("\n".join(lines)) + + # ---- DuckMail (使用标准 requests,避免 curl_cffi TLS 超时) ---- + + def _create_duckmail_session(self): + """使用标准 requests + retry 策略(与 cpa.py 保持一致)""" + import requests as std_requests + from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry + session = std_requests.Session() + retry_strategy = Retry( + total=5, backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["HEAD", "GET", "POST", "OPTIONS"], + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("https://", adapter) + session.mount("http://", adapter) + session.headers.update({ + "User-Agent": self.ua, "Accept": "application/json", "Content-Type": "application/json", + }) + if self.proxy: + session.proxies = {"http": self.proxy, "https": self.proxy} + return session + + def create_temp_email(self): + """使用 GPTMail API 生成临时邮箱,返回 (email, password, email_address)""" + session = self._create_duckmail_session() + headers = {"X-API-Key": GPTMAIL_API_KEY} + max_retries = 5 + for attempt in range(max_retries): + try: + chars = string.ascii_lowercase + string.digits + ts = int(time.time()) % 100000 + prefix = f"t{ts}" + "".join(random.choices(chars, k=8)) + print(f" GPTMail 生成邮箱 (第{attempt+1}次), prefix={prefix}") + res = session.post( + f"{GPTMAIL_BASE}/api/generate-email", + json={"prefix": prefix}, + headers=headers, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + if data.get("success") and data.get("data", {}).get("email"): + email = data["data"]["email"] + password = _generate_password() + print(f" ✅ GPTMail 邮箱生成成功: {email}") + # mail_token 直接复用 email 地址(查收件箱用 email 参数) + return email, password, email + raise Exception(f"GPTMail 返回异常: {res.text[:200]}") + else: + raise Exception(f"GPTMail HTTP {res.status_code}: {res.text[:200]}") + except Exception as e: + print(f" ⚠️ GPTMail 重试 {attempt+1}/{max_retries}: {e}") + time.sleep(1) + raise Exception("GPTMail 创建邮箱失败: 超过最大重试次数") + + def _fetch_emails_duckmail(self, mail_token: str): + """mail_token 此处为 email 地址,通过 GPTMail API 获取邮件列表""" + try: + session = self._create_duckmail_session() + res = session.get( + f"{GPTMAIL_BASE}/api/emails", + params={"email": mail_token}, + headers={"X-API-Key": GPTMAIL_API_KEY}, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + if data.get("success"): + msgs = data.get("data", {}).get("emails", []) + if msgs: + print(f" [DEBUG] Fetched {len(msgs)} email(s)") + return msgs + else: + print(f" [DEBUG] Fetch emails failed: {res.status_code} {res.text[:100]}") + except Exception as e: + print(f" [DEBUG] _fetch_emails_duckmail error: {e}") + return [] + + def _REMOVED_fetch_emails_duckmail_OLD(self, mail_token: str): + # 已废弃,占位保留 + return [] + + def _fetch_email_detail_duckmail(self, mail_token: str, msg_id: str): + """通过 GPTMail API 读取单封邮件详情,mail_token 不使用(保留签名兼容)""" + try: + session = self._create_duckmail_session() + res = session.get( + f"{GPTMAIL_BASE}/api/email/{msg_id}", + headers={"X-API-Key": GPTMAIL_API_KEY}, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + if data.get("success"): + detail = data.get("data", {}) + # 统一 html/text 字段 + html_val = detail.get("html_content") or detail.get("content", "") + text_val = detail.get("content", "") + detail["html"] = html_val + detail["text"] = text_val + return detail + except Exception as e: + print(f" [DEBUG] _fetch_email_detail_duckmail error: {e}") + return None + + def _extract_verification_code(self, email_content: str): + if not email_content: + return None + patterns = [ + r"Verification code:?\s*(\d{6})", r"code is\s*(\d{6})", + r"代码为[::]?\s*(\d{6})", r"验证码[::]?\s*(\d{6})", + r">\s*(\d{6})\s*<", r"(? 0: + first_msg = messages[0] + print(f" [DEBUG] Message keys: {list(first_msg.keys())}") + # Try multiple possible id fields + msg_id = first_msg.get("id") or first_msg.get("@id") or first_msg.get("message_id") + + # First try: extract OTP directly from list item (worker may embed content) + inline_content = first_msg.get("text") or first_msg.get("html") or first_msg.get("raw") or first_msg.get("source") or "" + if inline_content: + code = self._extract_verification_code(inline_content) + if code: + self._print(f"[OTP] 验证码: {code}") + return code + + # Second try: fetch detail by msg_id + if msg_id: + detail = self._fetch_email_detail_duckmail(mail_token, str(msg_id)) + if detail: + content = detail.get("text") or detail.get("html") or detail.get("source") or "" + code = self._extract_verification_code(content) + if code: + self._print(f"[OTP] 验证码: {code}") + return code + elapsed = int(time.time() - start_time) + self._print(f"[OTP] 等待中... ({elapsed}s/{timeout}s)") + time.sleep(3) + self._print(f"[OTP] 超时 ({timeout}s)") + return None + + # ---- 注册流程 ---- + + def create_temp_email(self): + provider = _mail_provider_name() + if provider == "npcmail": + if not CMAIL_BASE or not CMAIL_API_KEY: + raise Exception("NPCmail 未配置 API 域名或 API Key,请在 config.json 中补充 npcmail_base,并确认前端已保存 npcmail_api_key") + session = self._create_duckmail_session() + payload = {"count": 1, "expiryDays": CMAIL_EXPIRY_DAYS} + if CMAIL_DOMAIN: + payload["domain"] = CMAIL_DOMAIN + res = session.post( + f"{CMAIL_BASE}/api/public/batch-create-emails", + json=payload, + headers={"X-API-Key": CMAIL_API_KEY}, + timeout=30, + ) + if res.status_code != 200: + raise Exception(f"NPCmail HTTP {res.status_code}: {res.text[:200]}") + data = res.json() + emails = data.get("emails") or [] + if not data.get("success") or not emails: + raise Exception(f"NPCmail response error: {res.text[:200]}") + created = emails[0] + address = (created.get("address") or "").strip() + if not address: + raise Exception("NPCmail 创建邮箱成功但未返回 address") + pin_code = (created.get("pin_code") or "").strip() + print(f" [OK] NPCmail email created: {address}") + return address, pin_code, address + + session = self._create_duckmail_session() + headers = {"X-API-Key": GPTMAIL_API_KEY} + max_retries = 5 + for attempt in range(max_retries): + try: + chars = string.ascii_lowercase + string.digits + ts = int(time.time()) % 100000 + prefix = f"t{ts}" + "".join(random.choices(chars, k=8)) + print(f" GPTMail creating email ({attempt+1}/{max_retries}), prefix={prefix}") + res = session.post( + f"{GPTMAIL_BASE}/api/generate-email", + json={"prefix": prefix}, + headers=headers, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + if data.get("success") and data.get("data", {}).get("email"): + email = data["data"]["email"] + password = _generate_password() + print(f" [OK] GPTMail email created: {email}") + return email, password, email + raise Exception(f"GPTMail response error: {res.text[:200]}") + raise Exception(f"GPTMail HTTP {res.status_code}: {res.text[:200]}") + except Exception as e: + print(f" [WARN] GPTMail retry {attempt+1}/{max_retries}: {e}") + time.sleep(1) + raise Exception("GPTMail create email failed: exceeded max retries") + + def _fetch_emails(self, mail_token: str): + if _mail_provider_name() == "npcmail": + try: + session = self._create_duckmail_session() + encoded_address = quote(mail_token, safe="") + res = session.get( + f"{CMAIL_BASE}/api/public/emails/{encoded_address}/messages", + headers={"X-API-Key": CMAIL_API_KEY}, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + if isinstance(data, list): + if data: + print(f" [DEBUG] Fetched {len(data)} email(s)") + return data + else: + print(f" [DEBUG] Fetch NPCmail emails failed: {res.status_code} {res.text[:100]}") + except Exception as e: + print(f" [DEBUG] _fetch_emails_npcmail error: {e}") + return [] + return self._fetch_emails_duckmail(mail_token) + + def _fetch_email_detail(self, mail_token: str, msg_id: str): + if _mail_provider_name() == "npcmail": + return None + return self._fetch_email_detail_duckmail(mail_token, msg_id) + + def _extract_codes_cmail(self, addresses: list[str]): + if not addresses: + return [] + try: + session = self._create_duckmail_session() + res = session.post( + f"{CMAIL_BASE}/api/public/extract-codes", + json={"addresses": addresses}, + headers={"X-API-Key": CMAIL_API_KEY}, + timeout=30, + ) + if res.status_code == 200: + data = res.json() + return data if isinstance(data, list) else [] + print(f" [DEBUG] Extract NPCmail codes failed: {res.status_code} {res.text[:100]}") + except Exception as e: + print(f" [DEBUG] _extract_codes_npcmail error: {e}") + return [] + + def wait_for_verification_email(self, mail_token: str, timeout: int = 120): + self._print(f"[OTP] 绛夊緟楠岃瘉鐮侀偖浠?(鏈€澶?{timeout}s)...") + start_time = time.time() + while time.time() - start_time < timeout: + if _mail_provider_name() == "npcmail": + extracted = self._extract_codes_cmail([mail_token]) + if extracted: + match = extracted[0] + code = match.get("code") + if code: + self._print(f"[OTP] 楠岃瘉鐮? {code}") + return str(code) + + messages = self._fetch_emails(mail_token) + if messages: + first_msg = messages[0] + print(f" [DEBUG] Message keys: {list(first_msg.keys())}") + msg_id = first_msg.get("id") or first_msg.get("@id") or first_msg.get("message_id") + inline_content = first_msg.get("text") or first_msg.get("body") or first_msg.get("html") or first_msg.get("raw") or first_msg.get("source") or "" + if inline_content: + code = self._extract_verification_code(inline_content) + if code: + self._print(f"[OTP] 楠岃瘉鐮? {code}") + return code + if msg_id: + detail = self._fetch_email_detail(mail_token, str(msg_id)) + if detail: + content = detail.get("text") or detail.get("body") or detail.get("html") or detail.get("source") or "" + code = self._extract_verification_code(content) + if code: + self._print(f"[OTP] 楠岃瘉鐮? {code}") + return code + elapsed = int(time.time() - start_time) + self._print(f"[OTP] 绛夊緟涓?.. ({elapsed}s/{timeout}s)") + time.sleep(3) + self._print(f"[OTP] 瓒呮椂 ({timeout}s)") + return None + + def visit_homepage(self): + url = f"{self.BASE}/" + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + self._log("0. Visit homepage", "GET", url, r.status_code, + {"cookies_count": len(self.session.cookies)}) + + def wait_for_verification_email(self, mail_token: str, timeout: int = 120): + self._print(f"[OTP] 等待验证码邮件 (最多 {timeout}s)...") + start_time = time.time() + while time.time() - start_time < timeout: + if _mail_provider_name() == "npcmail": + extracted = self._extract_codes_cmail([mail_token]) + if extracted: + match = extracted[0] + code = match.get("code") + if code: + self._print(f"[OTP] 验证码: {code}") + return str(code) + + messages = self._fetch_emails(mail_token) + if messages: + first_msg = messages[0] + print(f" [DEBUG] Message keys: {list(first_msg.keys())}") + msg_id = first_msg.get("id") or first_msg.get("@id") or first_msg.get("message_id") + inline_content = first_msg.get("text") or first_msg.get("body") or first_msg.get("html") or first_msg.get("raw") or first_msg.get("source") or "" + if inline_content: + code = self._extract_verification_code(inline_content) + if code: + self._print(f"[OTP] 验证码: {code}") + return code + if msg_id: + detail = self._fetch_email_detail(mail_token, str(msg_id)) + if detail: + content = detail.get("text") or detail.get("body") or detail.get("html") or detail.get("source") or "" + code = self._extract_verification_code(content) + if code: + self._print(f"[OTP] 验证码: {code}") + return code + elapsed = int(time.time() - start_time) + self._print(f"[OTP] 等待中... ({elapsed}s/{timeout}s)") + time.sleep(3) + self._print(f"[OTP] 超时 ({timeout}s)") + return None + + def get_csrf(self) -> str: + url = f"{self.BASE}/api/auth/csrf" + r = self.session.get(url, headers={"Accept": "application/json", "Referer": f"{self.BASE}/"}) + data = r.json() + token = data.get("csrfToken", "") + self._log("1. Get CSRF", "GET", url, r.status_code, data) + if not token: + raise Exception("Failed to get CSRF token") + return token + + def signin(self, email: str, csrf: str) -> str: + url = f"{self.BASE}/api/auth/signin/openai" + params = { + "prompt": "login", "ext-oai-did": self.device_id, + "auth_session_logging_id": self.auth_session_logging_id, + "screen_hint": "login_or_signup", "login_hint": email, + } + form_data = {"callbackUrl": f"{self.BASE}/", "csrfToken": csrf, "json": "true"} + r = self.session.post(url, params=params, data=form_data, headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", "Referer": f"{self.BASE}/", "Origin": self.BASE, + }) + data = r.json() + authorize_url = data.get("url", "") + self._log("2. Signin", "POST", url, r.status_code, data) + if not authorize_url: + raise Exception("Failed to get authorize URL") + return authorize_url + + def authorize(self, url: str) -> str: + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.BASE}/", "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + final_url = str(r.url) + self._log("3. Authorize", "GET", url, r.status_code, {"final_url": final_url}) + return final_url + + def register(self, email: str, password: str): + url = f"{self.AUTH}/api/accounts/user/register" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/create-account/password", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"username": email, "password": password}, headers=headers) + try: + data = r.json() + except Exception: + data = {"text": r.text[:500]} + self._log("4. Register", "POST", url, r.status_code, data) + return r.status_code, data + + def send_otp(self): + url = f"{self.AUTH}/api/accounts/email-otp/send" + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.AUTH}/create-account/password", "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + try: + data = r.json() + except Exception: + data = {"final_url": str(r.url), "status": r.status_code} + self._log("5. Send OTP", "GET", url, r.status_code, data) + return r.status_code, data + + def validate_otp(self, code: str): + url = f"{self.AUTH}/api/accounts/email-otp/validate" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/email-verification", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"code": code}, headers=headers) + try: + data = r.json() + except Exception: + data = {"text": r.text[:500]} + self._log("6. Validate OTP", "POST", url, r.status_code, data) + return r.status_code, data + + def create_account(self, name: str, birthdate: str): + url = f"{self.AUTH}/api/accounts/create_account" + headers = {"Content-Type": "application/json", "Accept": "application/json", + "Referer": f"{self.AUTH}/about-you", "Origin": self.AUTH} + headers.update(_make_trace_headers()) + r = self.session.post(url, json={"name": name, "birthdate": birthdate}, headers=headers) + try: + data = r.json() + except Exception: + data = {"text": r.text[:500]} + self._log("7. Create Account", "POST", url, r.status_code, data) + if isinstance(data, dict): + cb = data.get("continue_url") or data.get("url") or data.get("redirect_url") + if cb: + self._callback_url = cb + return r.status_code, data + + def callback(self, url: str = None): + if not url: + url = self._callback_url + if not url: + self._print("[!] No callback URL, skipping.") + return None, None + r = self.session.get(url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }, allow_redirects=True) + self._log("8. Callback", "GET", url, r.status_code, {"final_url": str(r.url)}) + return r.status_code, {"final_url": str(r.url)} + + # ---- 注册主流程 ---- + + def run_register(self, email, password, name, birthdate, mail_token): + self.visit_homepage() + _random_delay(0.3, 0.8) + csrf = self.get_csrf() + _random_delay(0.2, 0.5) + auth_url = self.signin(email, csrf) + _random_delay(0.3, 0.8) + final_url = self.authorize(auth_url) + final_path = urlparse(final_url).path + _random_delay(0.3, 0.8) + self._print(f"Authorize → {final_path}") + need_otp = False + + if "create-account/password" in final_path: + self._print("全新注册流程") + _random_delay(0.5, 1.0) + status, data = self.register(email, password) + if status != 200: + raise Exception(f"Register 失败 ({status}): {data}") + _random_delay(0.3, 0.8) + self.send_otp() + need_otp = True + elif "email-verification" in final_path or "email-otp" in final_path: + self._print("跳到 OTP 验证阶段") + need_otp = True + elif "about-you" in final_path: + self._print("跳到填写信息阶段") + _random_delay(0.5, 1.0) + self.create_account(name, birthdate) + _random_delay(0.3, 0.5) + self.callback() + return True + elif "callback" in final_path or "chatgpt.com" in final_url: + self._print("账号已完成注册") + return True + else: + self._print(f"未知跳转: {final_url}") + self.register(email, password) + self.send_otp() + need_otp = True + + if need_otp: + otp_code = self.wait_for_verification_email(mail_token) + if not otp_code: + raise Exception("未能获取验证码") + _random_delay(0.3, 0.8) + status, data = self.validate_otp(otp_code) + if status != 200: + self._print("验证码失败,重试...") + self.send_otp() + _random_delay(1.0, 2.0) + otp_code = self.wait_for_verification_email(mail_token, timeout=60) + if not otp_code: + raise Exception("重试后仍未获取验证码") + _random_delay(0.3, 0.8) + status, data = self.validate_otp(otp_code) + if status != 200: + raise Exception(f"验证码失败 ({status}): {data}") + + _random_delay(0.5, 1.5) + status, data = self.create_account(name, birthdate) + if status != 200: + raise Exception(f"Create account 失败 ({status}): {data}") + _random_delay(0.2, 0.5) + self.callback() + return True + + # ---- OAuth helpers ---- + + def _decode_oauth_session_cookie(self): + jar = getattr(self.session.cookies, "jar", None) + cookie_items = list(jar) if jar is not None else [] + for c in cookie_items: + name = getattr(c, "name", "") or "" + if "oai-client-auth-session" not in name: + continue + raw_val = (getattr(c, "value", "") or "").strip() + if not raw_val: + continue + candidates = [raw_val] + try: + from urllib.parse import unquote + decoded = unquote(raw_val) + if decoded != raw_val: + candidates.append(decoded) + except Exception: + pass + for val in candidates: + try: + if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")): + val = val[1:-1] + part = val.split(".")[0] if "." in val else val + pad = 4 - len(part) % 4 + if pad != 4: + part += "=" * pad + raw = base64.urlsafe_b64decode(part) + data = json.loads(raw.decode("utf-8")) + if isinstance(data, dict): + return data + except Exception: + continue + return None + + def _oauth_allow_redirect_extract_code(self, url: str, referer: str = None): + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", "User-Agent": self.ua, + } + if referer: + headers["Referer"] = referer + try: + resp = self.session.get(url, headers=headers, allow_redirects=True, + timeout=30, impersonate=self.impersonate) + final_url = str(resp.url) + code = _extract_code_from_url(final_url) + if code: + return code + for r in getattr(resp, "history", []) or []: + loc = r.headers.get("Location", "") + code = _extract_code_from_url(loc) or _extract_code_from_url(str(r.url)) + if code: + return code + except Exception as e: + maybe_localhost = re.search(r'(https?://localhost[^\s\'\"]+)', str(e)) + if maybe_localhost: + code = _extract_code_from_url(maybe_localhost.group(1)) + if code: + return code + return None + + def _oauth_follow_for_code(self, start_url: str, referer: str = None, max_hops: int = 16): + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", "User-Agent": self.ua, + } + if referer: + headers["Referer"] = referer + current_url = start_url + last_url = start_url + for hop in range(max_hops): + try: + resp = self.session.get(current_url, headers=headers, allow_redirects=False, + timeout=30, impersonate=self.impersonate) + except Exception as e: + maybe_localhost = re.search(r'(https?://localhost[^\s\'\"]+)', str(e)) + if maybe_localhost: + code = _extract_code_from_url(maybe_localhost.group(1)) + if code: + return code, maybe_localhost.group(1) + return None, last_url + last_url = str(resp.url) + code = _extract_code_from_url(last_url) + if code: + return code, last_url + if resp.status_code in (301, 302, 303, 307, 308): + loc = resp.headers.get("Location", "") + if not loc: + return None, last_url + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code, loc + current_url = loc + headers["Referer"] = last_url + continue + return None, last_url + return None, last_url + + def _oauth_submit_workspace_and_org(self, consent_url: str): + session_data = self._decode_oauth_session_cookie() + if not session_data: + self._print("[OAuth] 无法解码 oai-client-auth-session") + return None + workspaces = session_data.get("workspaces", []) + if not workspaces: + self._print("[OAuth] session 中没有 workspace 信息") + return None + workspace_id = (workspaces[0] or {}).get("id") + if not workspace_id: + return None + + h = {"Accept": "application/json", "Content-Type": "application/json", + "Origin": OAUTH_ISSUER, "Referer": consent_url, + "User-Agent": self.ua, "oai-device-id": self.device_id} + h.update(_make_trace_headers()) + + resp = self.session.post(f"{OAUTH_ISSUER}/api/accounts/workspace/select", + json={"workspace_id": workspace_id}, headers=h, + allow_redirects=False, timeout=30, impersonate=self.impersonate) + self._print(f"[OAuth] workspace/select -> {resp.status_code}") + + if resp.status_code in (301, 302, 303, 307, 308): + loc = resp.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code + code, _ = self._oauth_follow_for_code(loc, referer=consent_url) + if not code: + code = self._oauth_allow_redirect_extract_code(loc, referer=consent_url) + return code + + if resp.status_code != 200: + return None + + try: + ws_data = resp.json() + except Exception: + return None + + ws_next = ws_data.get("continue_url", "") + orgs = ws_data.get("data", {}).get("orgs", []) + + org_id = None + project_id = None + if orgs: + org_id = (orgs[0] or {}).get("id") + projects = (orgs[0] or {}).get("projects", []) + if projects: + project_id = (projects[0] or {}).get("id") + + if org_id: + org_body = {"org_id": org_id} + if project_id: + org_body["project_id"] = project_id + h_org = dict(h) + if ws_next: + h_org["Referer"] = ws_next if ws_next.startswith("http") else f"{OAUTH_ISSUER}{ws_next}" + resp_org = self.session.post(f"{OAUTH_ISSUER}/api/accounts/organization/select", + json=org_body, headers=h_org, allow_redirects=False, + timeout=30, impersonate=self.impersonate) + self._print(f"[OAuth] organization/select -> {resp_org.status_code}") + if resp_org.status_code in (301, 302, 303, 307, 308): + loc = resp_org.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{OAUTH_ISSUER}{loc}" + code = _extract_code_from_url(loc) + if code: + return code + code, _ = self._oauth_follow_for_code(loc, referer=h_org.get("Referer")) + if not code: + code = self._oauth_allow_redirect_extract_code(loc, referer=h_org.get("Referer")) + return code + if resp_org.status_code == 200: + try: + org_data = resp_org.json() + except Exception: + return None + org_next = org_data.get("continue_url", "") + if org_next: + if org_next.startswith("/"): + org_next = f"{OAUTH_ISSUER}{org_next}" + code, _ = self._oauth_follow_for_code(org_next, referer=h_org.get("Referer")) + if not code: + code = self._oauth_allow_redirect_extract_code(org_next, referer=h_org.get("Referer")) + return code + + if ws_next: + if ws_next.startswith("/"): + ws_next = f"{OAUTH_ISSUER}{ws_next}" + code, _ = self._oauth_follow_for_code(ws_next, referer=consent_url) + if not code: + code = self._oauth_allow_redirect_extract_code(ws_next, referer=consent_url) + return code + return None + + # ---- Codex OAuth 纯协议 ---- + + def perform_codex_oauth_login_http(self, email: str, password: str, mail_token: str = None): + self._print("[OAuth] 开始执行 Codex OAuth 纯协议流程...") + self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com") + self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com") + + code_verifier, code_challenge = _generate_pkce() + state = secrets.token_urlsafe(24) + authorize_params = { + "response_type": "code", "client_id": OAUTH_CLIENT_ID, + "redirect_uri": OAUTH_REDIRECT_URI, "scope": "openid profile email offline_access", + "code_challenge": code_challenge, "code_challenge_method": "S256", "state": state, + } + authorize_url = f"{OAUTH_ISSUER}/oauth/authorize?{urlencode(authorize_params)}" + + def _oauth_json_headers(referer: str): + h = {"Accept": "application/json", "Content-Type": "application/json", + "Origin": OAUTH_ISSUER, "Referer": referer, + "User-Agent": self.ua, "oai-device-id": self.device_id} + h.update(_make_trace_headers()) + return h + + def _bootstrap_oauth_session(): + self._print("[OAuth] 1/7 GET /oauth/authorize") + try: + r = self.session.get(authorize_url, headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{self.BASE}/", "Upgrade-Insecure-Requests": "1", "User-Agent": self.ua, + }, allow_redirects=True, timeout=30, impersonate=self.impersonate) + except Exception as e: + self._print(f"[OAuth] /oauth/authorize 异常: {e}") + return False, "" + final_url = str(r.url) + self._print(f"[OAuth] /oauth/authorize -> {r.status_code}, final={final_url[:140]}") + has_login = any(getattr(c, "name", "") == "login_session" for c in self.session.cookies) + if not has_login: + try: + r2 = self.session.get(f"{OAUTH_ISSUER}/api/oauth/oauth2/auth", + headers={"Accept": "text/html", "Referer": authorize_url, + "User-Agent": self.ua}, + params=authorize_params, allow_redirects=True, + timeout=30, impersonate=self.impersonate) + final_url = str(r2.url) + except Exception: + pass + has_login = any(getattr(c, "name", "") == "login_session" for c in self.session.cookies) + return has_login, final_url + + def _post_authorize_continue(referer_url: str): + sentinel = build_sentinel_token(self.session, self.device_id, flow="authorize_continue", + user_agent=self.ua, sec_ch_ua=self.sec_ch_ua, impersonate=self.impersonate) + if not sentinel: + self._print("[OAuth] authorize_continue sentinel 失败") + return None + headers = _oauth_json_headers(referer_url) + headers["openai-sentinel-token"] = sentinel + try: + return self.session.post(f"{OAUTH_ISSUER}/api/accounts/authorize/continue", + json={"username": {"kind": "email", "value": email}}, + headers=headers, timeout=30, allow_redirects=False, + impersonate=self.impersonate) + except Exception as e: + self._print(f"[OAuth] authorize/continue 异常: {e}") + return None + + has_login_session, authorize_final_url = _bootstrap_oauth_session() + if not authorize_final_url: + return None + + continue_referer = authorize_final_url if authorize_final_url.startswith(OAUTH_ISSUER) else f"{OAUTH_ISSUER}/log-in" + + self._print("[OAuth] 2/7 POST /api/accounts/authorize/continue") + resp_continue = _post_authorize_continue(continue_referer) + if resp_continue is None: + return None + + if resp_continue.status_code == 400 and "invalid_auth_step" in (resp_continue.text or ""): + self._print("[OAuth] invalid_auth_step, 重新 bootstrap") + has_login_session, authorize_final_url = _bootstrap_oauth_session() + if not authorize_final_url: + return None + continue_referer = authorize_final_url if authorize_final_url.startswith(OAUTH_ISSUER) else f"{OAUTH_ISSUER}/log-in" + resp_continue = _post_authorize_continue(continue_referer) + if resp_continue is None: + return None + + if resp_continue.status_code != 200: + self._print(f"[OAuth] 邮箱提交失败: {resp_continue.text[:180]}") + return None + + try: + continue_data = resp_continue.json() + except Exception: + return None + + continue_url = continue_data.get("continue_url", "") + page_type = (continue_data.get("page") or {}).get("type", "") + + self._print("[OAuth] 3/7 POST /api/accounts/password/verify") + sentinel_pwd = build_sentinel_token(self.session, self.device_id, flow="password_verify", + user_agent=self.ua, sec_ch_ua=self.sec_ch_ua, impersonate=self.impersonate) + if not sentinel_pwd: + return None + + headers_verify = _oauth_json_headers(f"{OAUTH_ISSUER}/log-in/password") + headers_verify["openai-sentinel-token"] = sentinel_pwd + + try: + resp_verify = self.session.post(f"{OAUTH_ISSUER}/api/accounts/password/verify", + json={"password": password}, headers=headers_verify, + timeout=30, allow_redirects=False, impersonate=self.impersonate) + except Exception as e: + self._print(f"[OAuth] password/verify 异常: {e}") + return None + + if resp_verify.status_code != 200: + self._print(f"[OAuth] 密码校验失败: {resp_verify.text[:180]}") + return None + + try: + verify_data = resp_verify.json() + except Exception: + return None + + continue_url = verify_data.get("continue_url", "") or continue_url + page_type = (verify_data.get("page") or {}).get("type", "") or page_type + + # OTP 阶段 + need_oauth_otp = (page_type == "email_otp_verification" + or "email-verification" in (continue_url or "") + or "email-otp" in (continue_url or "")) + + if need_oauth_otp: + self._print("[OAuth] 4/7 检测到邮箱 OTP 验证") + if not mail_token: + self._print("[OAuth] 需要 OTP 但未提供 mail_token") + return None + headers_otp = _oauth_json_headers(f"{OAUTH_ISSUER}/email-verification") + tried_codes = set() + otp_success = False + otp_deadline = time.time() + 120 + while time.time() < otp_deadline and not otp_success: + messages = self._fetch_emails(mail_token) or [] + candidate_codes = [] + for msg in messages[:12]: + code = None + # Try inline content first (like workers do) + inline_content = msg.get("text") or msg.get("html") or msg.get("raw") or msg.get("source") or "" + if inline_content: + code = self._extract_verification_code(inline_content) + + # Try fetching detail if inline extraction failed + if not code: + msg_id = msg.get("id") or msg.get("@id") or msg.get("message_id") + if msg_id: + detail = self._fetch_email_detail(mail_token, str(msg_id)) + if detail: + content = detail.get("text") or detail.get("html") or detail.get("source") or "" + code = self._extract_verification_code(content) + + if code and code not in tried_codes: + candidate_codes.append(code) + if not candidate_codes: + time.sleep(2) + continue + for otp_code in candidate_codes: + tried_codes.add(otp_code) + self._print(f"[OAuth] 尝试 OTP: {otp_code}") + try: + resp_otp = self.session.post(f"{OAUTH_ISSUER}/api/accounts/email-otp/validate", + json={"code": otp_code}, headers=headers_otp, + timeout=30, allow_redirects=False, impersonate=self.impersonate) + except Exception: + continue + if resp_otp.status_code != 200: + continue + try: + otp_data = resp_otp.json() + except Exception: + continue + continue_url = otp_data.get("continue_url", "") or continue_url + page_type = (otp_data.get("page") or {}).get("type", "") or page_type + otp_success = True + break + if not otp_success: + time.sleep(2) + if not otp_success: + self._print(f"[OAuth] OTP 验证失败") + return None + + # 提取 code + code = None + consent_url = continue_url + if consent_url and consent_url.startswith("/"): + consent_url = f"{OAUTH_ISSUER}{consent_url}" + if not consent_url and "consent" in page_type: + consent_url = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + if consent_url: + code = _extract_code_from_url(consent_url) + + if not code and consent_url: + self._print("[OAuth] 5/7 跟随 continue_url 提取 code") + code, _ = self._oauth_follow_for_code(consent_url, referer=f"{OAUTH_ISSUER}/log-in/password") + + consent_hint = (("consent" in (consent_url or "")) or ("sign-in-with-chatgpt" in (consent_url or "")) + or ("workspace" in (consent_url or "")) or ("organization" in (consent_url or "")) + or ("consent" in page_type) or ("organization" in page_type)) + + if not code and consent_hint: + if not consent_url: + consent_url = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + self._print("[OAuth] 6/7 执行 workspace/org 选择") + code = self._oauth_submit_workspace_and_org(consent_url) + + if not code: + fallback_consent = f"{OAUTH_ISSUER}/sign-in-with-chatgpt/codex/consent" + self._print("[OAuth] 6/7 回退 consent 路径重试") + code = self._oauth_submit_workspace_and_org(fallback_consent) + if not code: + code, _ = self._oauth_follow_for_code(fallback_consent, referer=f"{OAUTH_ISSUER}/log-in/password") + + if not code: + self._print("[OAuth] 未获取到 authorization code") + return None + + self._print("[OAuth] 7/7 POST /oauth/token") + token_resp = self.session.post(f"{OAUTH_ISSUER}/oauth/token", + headers={"Content-Type": "application/x-www-form-urlencoded", "User-Agent": self.ua}, + data={"grant_type": "authorization_code", "code": code, + "redirect_uri": OAUTH_REDIRECT_URI, "client_id": OAUTH_CLIENT_ID, + "code_verifier": code_verifier}, + timeout=60, impersonate=self.impersonate) + self._print(f"[OAuth] /oauth/token -> {token_resp.status_code}") + + if token_resp.status_code != 200: + self._print(f"[OAuth] token 交换失败: {token_resp.text[:200]}") + return None + + try: + data = token_resp.json() + except Exception: + return None + if not data.get("access_token"): + return None + + self._print("[OAuth] Codex Token 获取成功 ✅") + return data + + +# ================= Team 母号注册 ================= + +def register_team_master(proxy=None): + """注册 Team 母号:注册 → 获取 AccessToken 和 SessionToken(跳过 Codex OAuth)""" + tag = "master" + reg = ChatGPTRegister(proxy=proxy, tag=tag) + + # 1. 创建临时邮箱 + reg._print("[GPTMail] 创建临时邮箱...") + email, email_pwd, mail_token = reg.create_temp_email() + tag = email.split("@")[0] + reg.tag = tag + + chatgpt_password = _generate_password() + name = _random_name() + birthdate = _random_birthdate() + + with _print_lock: + print(f"\n{'='*60}") + print(f" [母号注册] {email}") + print(f" ChatGPT密码: {chatgpt_password}") + print(f" 邮箱密码: {email_pwd}") + print(f" 姓名: {name} | 生日: {birthdate}") + print(f"{'='*60}") + + # 2. 执行注册流程 + reg.run_register(email, chatgpt_password, name, birthdate, mail_token) + + # 3. 获取 SessionToken 和 AccessToken(跳过 Codex OAuth) + session_token_value = None + access_token = None + + # 从 cookies 中提取 __Secure-next-auth.session-token + jar = getattr(reg.session.cookies, "jar", None) + cookie_items = list(jar) if jar is not None else [] + for c in cookie_items: + cookie_name = getattr(c, "name", "") or "" + if "__Secure-next-auth.session-token" in cookie_name: + session_token_value = getattr(c, "value", "") + break + + if session_token_value: + reg._print("🔑 获取到 Session Token") + try: + r = reg.session.get("https://chatgpt.com/api/auth/session", timeout=30) + if r.status_code == 200: + data = r.json() + access_token = data.get("accessToken", "") + if access_token: + reg._print("🔑 获取 Access Token 成功 ✅") + else: + reg._print("⚠️ Session 响应中无 accessToken") + else: + reg._print(f"⚠️ 获取 session 失败: HTTP {r.status_code}") + except Exception as e: + reg._print(f"⚠️ 获取 session 异常: {e}") + else: + reg._print("⚠️ 未找到 session token cookie") + + # 4. 保存注册记录 + with _file_lock: + with open(DEFAULT_OUTPUT_FILE, "a", encoding="utf-8") as out: + out.write(f"{email}----{chatgpt_password}----{email_pwd}----master\n") + save_to_csv(email, chatgpt_password, email_pwd, oauth_status="master") + + reg._print(f"✅ 母号注册完成: {email}") + return { + "email": email, + "password": chatgpt_password, + "email_password": email_pwd, + "access_token": access_token, + "session_token": session_token_value, + } + + +def get_team_info_from_session(session_token, proxy=None): + """使用 session_token 获取 Team 信息(名称、ID、AccessToken、SessionToken)""" + s = curl_requests.Session(verify=False, impersonate="chrome120") + if proxy: + s.proxies = {"http": proxy, "https": proxy} + s.cookies.set("__Secure-next-auth.session-token", session_token, domain="chatgpt.com") + + # 1. 获取 fresh access token + r = s.get("https://chatgpt.com/api/auth/session", timeout=30) + if r.status_code != 200: + print(f"[TeamInfo] 获取 session 失败: HTTP {r.status_code}") + return None + + data = r.json() + access_token = data.get("accessToken", "") + if not access_token: + print("[TeamInfo] Session 响应中无 accessToken") + return None + + # 获取更新后的 session token + new_session_token = session_token + jar = getattr(s.cookies, "jar", None) + if jar: + for c in list(jar): + cname = getattr(c, "name", "") or "" + if "__Secure-next-auth.session-token" in cname: + val = getattr(c, "value", "") + if val: + new_session_token = val + break + + # 2. 获取 workspace/team 信息 + headers = { + "authorization": f"Bearer {access_token}", + "accept": "*/*", + "content-type": "application/json", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + } + + team_name = "" + account_id = "" + try: + r2 = s.get("https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", + headers=headers, timeout=30) + if r2.status_code == 200: + accounts_data = r2.json() + accounts = accounts_data.get("accounts", {}) + print(f"[TeamInfo] 找到 {len(accounts)} 个 workspace") + for acc_id, acc_info in accounts.items(): + account = acc_info.get("account", {}) + structure = account.get("structure", "") + plan_type = account.get("plan_type", "") + print(f"[TeamInfo] {acc_id}: structure={structure}, plan_type={plan_type}") + if structure == "workspace" or plan_type == "team": + account_id = acc_id + team_name = account.get("name", "") or account.get("display_name", "") + break + else: + print(f"[TeamInfo] accounts/check 失败: HTTP {r2.status_code}") + except Exception as e: + print(f"[TeamInfo] accounts/check 异常: {e}") + + # Fallback: 从 JWT 解码获取 account_id + if not account_id: + payload = _decode_jwt_payload(access_token) + auth_info = payload.get("https://api.openai.com/auth", {}) + account_id = auth_info.get("chatgpt_account_id", "") + print(f"[TeamInfo] 从 JWT 获取 account_id: {account_id}") + + return { + "name": team_name or f"Team-{account_id[:8]}", + "account_id": account_id, + "auth_token": access_token, + "session_token": new_session_token, + } + + +# ================= 并发批量注册 ================= + +def _register_one(idx, total, proxy, output_file): + """单个注册任务(线程内运行):DuckMail 创建 → 注册 → Team 邀请 → Codex OAuth""" + reg = None + try: + reg = ChatGPTRegister(proxy=proxy, tag=f"{idx}") + + # 1. 创建 DuckMail 临时邮箱 + reg._print("[DuckMail] 创建临时邮箱...") + email, email_pwd, mail_token = reg.create_temp_email() + tag = email.split("@")[0] + reg.tag = tag + + chatgpt_password = _generate_password() + name = _random_name() + birthdate = _random_birthdate() + + with _print_lock: + print(f"\n{'='*60}") + print(f" [{idx}/{total}] 注册: {email}") + print(f" ChatGPT密码: {chatgpt_password}") + print(f" 邮箱密码: {email_pwd}") + print(f" 姓名: {name} | 生日: {birthdate}") + print(f"{'='*60}") + + # 2. 执行注册流程 + reg.run_register(email, chatgpt_password, name, birthdate, mail_token) + + # 3. Team 邀请 + reg._print("📨 发送 Team 邀请...") + invite_ok = auto_invite_to_team(email, tag=tag) + if invite_ok: + reg._print("⏳ 等待邀请生效...") + time.sleep(5) + + # 4. Codex OAuth + oauth_ok = True + if ENABLE_OAUTH: + reg._print("[OAuth] 开始获取 Codex Token...") + tokens = reg.perform_codex_oauth_login_http(email, chatgpt_password, mail_token=mail_token) + oauth_ok = bool(tokens and tokens.get("access_token")) + if oauth_ok: + _save_codex_tokens(email, tokens) + reg._print("[OAuth] Token 已保存 ✅") + else: + if OAUTH_REQUIRED: + raise Exception("OAuth 获取失败(oauth_required=true)") + reg._print("[OAuth] 获取失败(按配置继续)") + + # 5. 保存结果 + with _file_lock: + with open(output_file, "a", encoding="utf-8") as out: + out.write(f"{email}----{chatgpt_password}----{email_pwd}----oauth={'ok' if oauth_ok else 'fail'}\n") + + save_to_csv(email, chatgpt_password, email_pwd, oauth_status="ok" if oauth_ok else "fail") + + with _print_lock: + print(f"\n[OK] [{tag}] {email} 注册成功! 🎉") + return True, email, None + + except Exception as e: + error_msg = str(e) + with _print_lock: + print(f"\n[FAIL] [{idx}] 注册失败: {error_msg}") + traceback.print_exc() + return False, None, error_msg + + +def run_batch(total_accounts: int = 4, output_file="registered_accounts.txt", + max_workers=1, proxy=None): + """并发批量注册 - DuckMail 临时邮箱 + Team 邀请 + Codex OAuth""" + actual_workers = min(max_workers, total_accounts) + print(f"\n{'#'*60}") + print(f" ChatGPT 批量自动注册 (纯协议版)") + print(f" 注册数量: {total_accounts} | 并发数: {actual_workers}") + print(f" GPTMail: {GPTMAIL_BASE}") + print(f" Teams: {len(TEAMS)} 个") + print(f" OAuth: {'开启' if ENABLE_OAUTH else '关闭'} | required: {'是' if OAUTH_REQUIRED else '否'}") + if ENABLE_OAUTH: + print(f" OAuth Issuer: {OAUTH_ISSUER}") + print(f" OAuth Client: {OAUTH_CLIENT_ID}") + print(f" Token输出: {TOKEN_JSON_DIR}/, {AK_FILE}, {RK_FILE}") + print(f" 输出文件: {output_file}") + print(f"{'#'*60}\n") + + success_count = 0 + fail_count = 0 + start_time = time.time() + + with ThreadPoolExecutor(max_workers=actual_workers) as executor: + futures = {} + for idx in range(1, total_accounts + 1): + future = executor.submit(_register_one, idx, total_accounts, proxy, output_file) + futures[future] = idx + + for future in as_completed(futures): + idx = futures[future] + try: + ok, email, err = future.result() + if ok: + success_count += 1 + else: + fail_count += 1 + print(f" [账号 {idx}] 失败: {err}") + except Exception as e: + fail_count += 1 + with _print_lock: + print(f"[FAIL] 账号 {idx} 线程异常: {e}") + + elapsed = time.time() - start_time + avg = elapsed / total_accounts if total_accounts else 0 + print(f"\n{'#'*60}") + print(f" 注册完成! 耗时 {elapsed:.1f} 秒") + print(f" 总数: {total_accounts} | 成功: {success_count} | 失败: {fail_count}") + print(f" 平均速度: {avg:.1f} 秒/个") + if success_count > 0: + print(f" 结果文件: {output_file}") + print(f"{'#'*60}") + + +def main(): + print("=" * 60) + print(" ChatGPT 批量自动注册工具 (纯协议版)") + print(" 注册 → Team 邀请 → Codex OAuth 全流程自动化") + print("=" * 60) + + proxy = DEFAULT_PROXY + if proxy: + print(f"[Info] 使用代理: {proxy}") + else: + env_proxy = (os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") + or os.environ.get("ALL_PROXY") or os.environ.get("all_proxy")) + if env_proxy: + print(f"[Info] 检测到环境变量代理: {env_proxy}") + proxy = env_proxy + else: + proxy_input = input("输入代理地址 (留空=不使用代理): ").strip() + proxy = proxy_input or None + + if proxy: + print(f"[Info] 使用代理: {proxy}") + + count_input = input(f"\n注册账号数量 (默认 {DEFAULT_TOTAL_ACCOUNTS}): ").strip() + total_accounts = int(count_input) if count_input.isdigit() and int(count_input) > 0 else DEFAULT_TOTAL_ACCOUNTS + + workers_input = input("并发数 (默认 1): ").strip() + max_workers = int(workers_input) if workers_input.isdigit() and int(workers_input) > 0 else 1 + + run_batch(total_accounts=total_accounts, output_file=DEFAULT_OUTPUT_FILE, + max_workers=max_workers, proxy=proxy) + + +if __name__ == "__main__": + main() diff --git a/team_all-in-one/static/mac_style.css b/team_all-in-one/static/mac_style.css new file mode 100644 index 0000000..d0ea371 --- /dev/null +++ b/team_all-in-one/static/mac_style.css @@ -0,0 +1,416 @@ +/* ═══════════════════════════════════════════════ + ChatGPT Batch Registration - Apple Glass Theme + v3.0 — Clean, tested, no layout issues +═══════════════════════════════════════════════ */ + +:root { + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", sans-serif; + --mono: "SF Mono", "Cascadia Code", "Fira Code", ui-monospace, monospace; + + --bg: #0d0d18; + --surface: rgba(22, 22, 40, 0.72); + --surface-hover: rgba(30, 30, 55, 0.85); + --border: rgba(255, 255, 255, 0.08); + --border-bright: rgba(255, 255, 255, 0.18); + + --accent: #0a84ff; + --accent-glow: rgba(10, 132, 255, 0.25); + --green: #32d74b; + --red: #ff453a; + --orange: #ff9f0a; + --purple: #bf5af2; + + --text: #f2f2f7; + --text-secondary: rgba(235, 235, 245, 0.6); + --text-placeholder: rgba(235, 235, 245, 0.28); + + --radius: 14px; + --radius-sm: 8px; + --radius-xs: 5px; + + --blur: saturate(180%) blur(28px); + --shadow: 0 8px 40px rgba(0, 0, 0, 0.4); + --shadow-sm: 0 3px 16px rgba(0, 0, 0, 0.25); +} + +/* ── Reset ── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 14px; scroll-behavior: smooth; } +body { + font-family: var(--font); + color: var(--text); + background: var(--bg); + min-height: 100vh; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* ── Animated Background ── */ +.bg { + position: fixed; inset: 0; z-index: -1; + background: + radial-gradient(ellipse 80% 60% at 10% 10%, rgba(90, 40, 160, 0.28), transparent), + radial-gradient(ellipse 70% 70% at 90% 20%, rgba(10, 80, 170, 0.22), transparent), + radial-gradient(ellipse 60% 80% at 50% 90%, rgba(10, 132, 255, 0.10), transparent), + #0d0d18; + animation: bgShift 18s ease-in-out infinite alternate; +} +@keyframes bgShift { + 0% { filter: hue-rotate(0deg); } + 100% { filter: hue-rotate(20deg); } +} + +/* ── Header ── */ +.header { + position: sticky; top: 0; z-index: 200; + display: flex; align-items: center; justify-content: space-between; + padding: 0 28px; height: 56px; + background: rgba(13, 13, 24, 0.8); + backdrop-filter: var(--blur); + border-bottom: 1px solid var(--border); +} +.header-brand { display: flex; align-items: center; gap: 10px; } +.header-brand h1 { + font-size: 1.05rem; font-weight: 600; + background: linear-gradient(120deg, #fff 30%, #a29bfe); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; +} +.status-pill { + display: flex; align-items: center; gap: 7px; + padding: 5px 14px; border-radius: 20px; + font-size: 0.8rem; font-weight: 500; + background: rgba(255,255,255,0.06); + border: 1px solid var(--border); + user-select: none; +} +.status-pill .dot { + width: 7px; height: 7px; border-radius: 50%; + background: var(--text-secondary); + transition: background 0.4s; +} +.status-pill.running .dot { + background: var(--green); + box-shadow: 0 0 7px var(--green); + animation: pulse 1.8s ease-in-out infinite; +} +@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} } + +/* ── Page Layout ── */ +.page { + display: grid; + grid-template-columns: 420px 1fr; + gap: 20px; + padding: 20px 24px; + max-width: 1600px; + margin: 0 auto; + align-items: start; +} + +@media (max-width: 1100px) { + .page { grid-template-columns: 1fr; } + .sidebar { position: static; max-height: none; } +} + +/* ── Sidebar ── */ +.sidebar { + display: flex; flex-direction: column; gap: 14px; + position: sticky; top: 76px; + max-height: calc(100vh - 90px); + overflow-y: auto; + padding-right: 2px; + padding-bottom: 8px; +} +.sidebar::-webkit-scrollbar { width: 4px; } +.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; } + +/* ── Glass Card ── */ +.card { + background: var(--surface); + backdrop-filter: var(--blur); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + overflow: visible; /* must NOT be hidden — team textareas must fully expand */ + transition: border-color 0.3s; + position: relative; +} +.card:focus-within { border-color: rgba(10,132,255,0.35); } +.card-inner-clip { + /* used only for rounding the card content — NOT .card itself */ + border-radius: var(--radius); + overflow: hidden; +} + +/* ── Card Header (collapsible) ── */ +.card-head { + display: flex; align-items: center; justify-content: space-between; + padding: 13px 18px; + font-weight: 600; font-size: 0.9rem; + cursor: pointer; + user-select: none; + background: rgba(0,0,0,0.12); + border-bottom: 1px solid var(--border); + transition: background 0.2s; +} +.card-head:hover { background: rgba(255,255,255,0.04); } +.card-head .title { display: flex; align-items: center; gap: 8px; } +.card-head .chevron { + font-size: 0.75rem; + color: var(--text-secondary); + transition: transform 0.3s cubic-bezier(.16,1,.3,1); +} +.card-head.collapsed .chevron { transform: rotate(-90deg); } + +.card-body { + display: flex; flex-direction: column; gap: 14px; + padding: 16px 18px; + transition: opacity 0.3s; + opacity: 1; + border-radius: 0 0 var(--radius) var(--radius); + overflow: visible; /* must be visible so team card textareas don't clip */ +} +.card-body.hidden { + display: none; + opacity: 0; + pointer-events: none; +} + +/* ── Form ── */ +.form-row { display: flex; flex-direction: column; gap: 6px; } +.form-row label { + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + padding-left: 2px; +} + +.inp { + width: 100%; + padding: 9px 13px; + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 0.88rem; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); +} +.inp::placeholder { color: var(--text-placeholder); } +.inp:focus { + background: rgba(0,0,0,0.3); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow), inset 0 1px 3px rgba(0,0,0,0.2); +} + +textarea.inp { + resize: vertical; min-height: 48px; line-height: 1.45; + font-family: var(--mono); font-size: 0.78rem; +} + +/* ── Buttons ── */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 7px; + padding: 9px 20px; + border: none; border-radius: var(--radius-sm); + font-family: var(--font); font-size: 0.88rem; font-weight: 500; + cursor: pointer; user-select: none; + transition: all 0.18s cubic-bezier(.16,1,.3,1); + white-space: nowrap; +} +.btn:active { transform: scale(0.96) !important; } +.btn:disabled { opacity: 0.45; cursor: not-allowed; } + +.btn-blue { background: var(--accent); color: #fff; box-shadow: 0 2px 8px rgba(10,132,255,.3); } +.btn-blue:hover:not(:disabled) { filter: brightness(1.1); box-shadow: 0 4px 14px rgba(10,132,255,.4); } + +.btn-green { background: var(--green); color: #000; box-shadow: 0 2px 8px rgba(50,215,75,.2); } +.btn-green:hover:not(:disabled) { filter: brightness(1.08); } + +.btn-red { background: var(--red); color: #fff; } +.btn-red:hover:not(:disabled) { filter: brightness(1.1); } + +.btn-ghost { + background: rgba(255,255,255,0.06); + border: 1px solid var(--border); + color: var(--text); +} +.btn-ghost:hover:not(:disabled) { background: rgba(255,255,255,0.1); border-color: var(--border-bright); } + +.btn-ghost-danger { + background: rgba(255,69,58,0.08); + border: 1px solid rgba(255,69,58,0.25); + color: var(--red); +} +.btn-ghost-danger:hover { background: rgba(255,69,58,0.18); } + +.icon-btn { + padding: 6px; + background: transparent; + border: none; border-radius: 6px; + color: var(--text-secondary); + cursor: pointer; + display: flex; align-items: center; + transition: all 0.18s; +} +.icon-btn:hover { background: rgba(255,255,255,0.1); color: var(--text); } +.icon-btn.danger:hover { background: rgba(255,69,58,0.18); color: var(--red); } + +/* ── Toggle ── */ +.toggle-wrap { display: flex; align-items: center; justify-content: space-between; } +.toggle-wrap label.label { font-size: 0.88rem; color: var(--text); } +.toggle { + position: relative; width: 44px; height: 24px; + cursor: pointer; +} +.toggle input { opacity: 0; width: 0; height: 0; } +.track { + position: absolute; inset: 0; + background: rgba(255,255,255,0.2); + border-radius: 12px; + transition: background 0.3s; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); +} +.toggle input:checked + .track { background: var(--green); } +.track::after { + content: ''; + position: absolute; + left: 2px; top: 2px; + width: 20px; height: 20px; + background: #fff; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0,0,0,0.25); + transition: transform 0.3s cubic-bezier(.16,1,.3,1); +} +.toggle input:checked + .track::after { transform: translateX(20px); } + +/* ── Checkbox ── */ +.chk { + appearance: none; + width: 16px; height: 16px; + border: 1px solid rgba(255,255,255,0.3); + border-radius: 4px; + background: rgba(0,0,0,0.25); + cursor: pointer; + position: relative; + vertical-align: middle; + transition: all 0.18s; +} +.chk:checked { + background: var(--accent); + border-color: var(--accent); +} +.chk:checked::after { + content: ''; + position: absolute; + left: 4px; top: 1px; + width: 5px; height: 9px; + border: 2px solid #fff; + border-top: none; border-left: none; + transform: rotate(45deg); +} + +/* ── Team Card ── */ +.team-card, .sub-card-item { + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.07); + border-radius: var(--radius-sm); + padding: 14px; + display: flex; flex-direction: column; gap: 12px; + transition: border-color 0.2s; +} +.team-card:focus-within, .sub-card-item:focus-within { border-color: rgba(10,132,255,0.3); } +.sub-card-item + .sub-card-item { margin-top: 10px; } +.team-card-top { + display: flex; align-items: center; justify-content: space-between; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255,255,255,0.06); +} +.team-card-top .team-lbl { + font-size: 0.82rem; font-weight: 600; + color: var(--text-secondary); + font-family: var(--mono); +} + +/* ── Main Content ── */ +.main { display: flex; flex-direction: column; gap: 20px; } + +/* ── Terminal ── */ +.terminal { + background: #000; + border-radius: 0 0 var(--radius) var(--radius); + padding: 14px 16px; + height: 340px; + overflow-y: auto; + font-family: var(--mono); + font-size: 0.8rem; + line-height: 1.6; +} +.terminal::-webkit-scrollbar { width: 6px; } +.terminal::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 4px; } +.log { color: #8b97b0; white-space: pre-wrap; word-break: break-all; } +.log.ok { color: #32d74b; } +.log.err { color: #ff453a; } +.log.wrn { color: #ff9f0a; } +.log.inf { color: #0a84ff; } +.log.hi { color: #fff; font-weight: 600; } + +/* ── Table ── */ +.tbl-wrap { overflow-x: auto; } +table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.84rem; } +thead th { + padding: 10px 16px; text-align: left; + font-size: 0.74rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; + color: var(--text-secondary); + background: rgba(0,0,0,0.12); + border-bottom: 1px solid var(--border); +} +tbody td { + padding: 11px 16px; + border-bottom: 1px solid rgba(255,255,255,0.04); + transition: background 0.15s; +} +tbody tr:hover td { background: rgba(255,255,255,0.03); } +tbody tr:last-child td { border-bottom: none; } + +.tag { + display: inline-flex; padding: 2px 9px; + border-radius: 12px; font-size: 0.74rem; font-weight: 600; +} +.tag.ok { background: rgba(50,215,75,0.14); color: var(--green); border: 1px solid rgba(50,215,75,.2); } +.tag.err { background: rgba(255,69,58,0.14); color: var(--red); border: 1px solid rgba(255,69,58,.2); } + +.empty { text-align: center; padding: 32px; color: var(--text-secondary); font-style: italic; } + +/* ── Toolbar ── */ +.toolbar { + display: flex; align-items: center; gap: 10px; + flex-wrap: wrap; padding: 16px 18px 0; +} +.toolbar-spacer { flex: 1; } + +/* ── Toast ── */ +.toast-wrap { + position: fixed; bottom: 22px; right: 22px; + display: flex; flex-direction: column; gap: 9px; + z-index: 9999; +} +.toast { + display: flex; align-items: center; gap: 10px; + padding: 12px 18px; + background: rgba(25,25,40,0.9); + backdrop-filter: var(--blur); + border: 1px solid var(--border-bright); + border-radius: 12px; + box-shadow: 0 12px 40px rgba(0,0,0,0.4); + font-size: 0.88rem; + animation: toastIn 0.35s cubic-bezier(.16,1,.3,1) forwards; + transform: translateY(20px); opacity: 0; +} +@keyframes toastIn { to { transform: none; opacity: 1; } } +.toast.out { animation: toastOut 0.25s ease-in forwards; } +@keyframes toastOut { to { transform: scale(.9); opacity: 0; } } +.toast-ico { font-size: 1rem; line-height: 1; } +.toast.ok .toast-ico { color: var(--green); } +.toast.err .toast-ico { color: var(--red); } diff --git a/team_all-in-one/static/style.css b/team_all-in-one/static/style.css new file mode 100644 index 0000000..c3827a4 --- /dev/null +++ b/team_all-in-one/static/style.css @@ -0,0 +1,611 @@ +/* macOS Glassmorphism Theme */ + +:root { + --bg-gradient-1: #1a1a2e; + --bg-gradient-2: #16213e; + --bg-gradient-3: #0f3460; + --bg-gradient-4: #4a1c40; + + --glass-bg: rgba(25, 25, 35, 0.45); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); + --glass-blur: saturate(180%) blur(25px); + + --accent: #0A84FF; + --accent-hover: #0070E0; + --success: #30D158; + --danger: #FF453A; + --danger-hover: #E0382D; + --warn: #FF9F0A; + + --text-main: #FFFFFF; + --text-muted: rgba(235, 235, 245, 0.6); + --text-disabled: rgba(235, 235, 245, 0.3); + + --radius-lg: 16px; + --radius-md: 10px; + --radius-sm: 6px; + + --font-apple: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: "SF Mono", "ui-monospace", "Cascadia Code", monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-apple); + color: var(--text-main); + background-color: #0b0b12; + min-height: 100vh; + overflow-x: hidden; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Dynamic Animated Background to make glass pop */ +.bg-mesh { + position: fixed; + top: 0; left: 0; width: 100vw; height: 100vh; + z-index: -1; + background: + radial-gradient(circle at 15% 50%, rgba(74, 28, 64, 0.5), transparent 50%), + radial-gradient(circle at 85% 30%, rgba(15, 52, 96, 0.5), transparent 50%), + radial-gradient(circle at 50% 80%, rgba(10, 132, 255, 0.15), transparent 50%); + background-size: cover; + filter: blur(60px); + animation: bgBreathing 15s ease-in-out infinite alternate; +} + +@keyframes bgBreathing { + 0% { transform: scale(1); } + 100% { transform: scale(1.1) translate(-2%, 2%); } +} + +/* ── Typography & Headers ── */ +h1, h2, h3, h4 { font-weight: 600; letter-spacing: -0.01em; } + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 32px; + background: rgba(15, 15, 20, 0.6); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border-bottom: 1px solid var(--glass-border); + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 4px 24px rgba(0,0,0,0.2); +} + +.header-title { + display: flex; + align-items: center; + gap: 12px; +} + +.header h1 { + font-size: 1.25rem; + background: linear-gradient(120deg, #fff, #a29bfe); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.status-badge { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + font-weight: 500; + padding: 6px 14px; + border-radius: 20px; + background: rgba(255,255,255,0.05); + border: 1px solid var(--glass-border); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.05); + min-width: 90px; + justify-content: center; + transition: all 0.3s ease; +} + +.status-badge .dot { + width: 8px; height: 8px; + border-radius: 50%; + background: var(--text-muted); +} +.status-badge.running .dot { + background: var(--success); + box-shadow: 0 0 8px var(--success); + animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} + +/* ── Layout ── */ +.container { + display: grid; + grid-template-columns: 360px 1fr; + gap: 24px; + padding: 24px 32px; + max-width: 1800px; + margin: 0 auto; + align-items: start; +} + +@media (max-width: 1024px) { + .container { + grid-template-columns: 1fr; + padding: 16px; + } +} + +/* ── Glass Cards ── */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + box-shadow: var(--glass-shadow); + position: relative; + overflow: hidden; + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease; +} +.glass-panel:hover { + box-shadow: 0 16px 40px rgba(0,0,0,0.4); +} +.glass-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; height: 1px; + background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.1) 50%, rgba(255,255,255,0)); +} + +.panel-header { + padding: 16px 20px; + font-weight: 600; + font-size: 0.95rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid rgba(255,255,255,0.04); + background: rgba(0,0,0,0.1); + user-select: none; +} +.panel-header .icon { + margin-right: 8px; + opacity: 0.8; +} + +.panel-body { + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ── Forms & Inputs ── */ +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 0.8rem; + color: var(--text-muted); + font-weight: 500; + margin-left: 2px; +} + +.mac-input { + width: 100%; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255,255,255,0.1); + border-radius: var(--radius-md); + color: var(--text-main); + font-family: var(--font-apple); + font-size: 0.9rem; + transition: all 0.2s ease; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); +} + +.mac-input:focus { + outline: none; + background: rgba(0, 0, 0, 0.3); + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3), inset 0 2px 4px rgba(0,0,0,0.2); +} +.mac-input::placeholder { color: rgba(255,255,255,0.2); } + +textarea.mac-input { + resize: vertical; + min-height: 44px; + font-family: var(--font-mono); + font-size: 0.85rem; + line-height: 1.4; +} + +/* ── Buttons ── */ +.mac-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 20px; + border: 1px solid transparent; + border-radius: var(--radius-md); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + user-select: none; + font-family: var(--font-apple); +} + +.mac-btn:active { + transform: scale(0.96); +} + +.mac-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +.btn-primary { + background: var(--accent); + color: #fff; + border-top: 1px solid rgba(255,255,255,0.2); + box-shadow: 0 2px 8px rgba(10, 132, 255, 0.3); +} +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); + box-shadow: 0 4px 12px rgba(10, 132, 255, 0.4); +} + +.btn-success { + background: var(--success); + color: #000; + border-top: 1px solid rgba(255,255,255,0.4); + box-shadow: 0 2px 8px rgba(48, 209, 88, 0.2); +} +.btn-success:hover:not(:disabled) { + filter: brightness(1.1); + box-shadow: 0 4px 12px rgba(48, 209, 88, 0.3); +} + +.btn-danger { + background: var(--danger); + color: #fff; + border-top: 1px solid rgba(255,255,255,0.2); +} +.btn-danger:hover:not(:disabled) { + background: var(--danger-hover); +} + +.btn-secondary { + background: rgba(255,255,255,0.1); + color: var(--text-main); + border: 1px solid rgba(255,255,255,0.1); +} +.btn-secondary:hover:not(:disabled) { + background: rgba(255,255,255,0.15); +} + +.btn-icon { + padding: 6px; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-muted); + border: none; + cursor: pointer; + transition: all 0.2s; + display: flex; align-items: center; justify-content: center; +} +.btn-icon:hover { + background: rgba(255,255,255,0.1); + color: var(--text-main); +} +.btn-icon.danger:hover { + background: rgba(255,69,58,0.2); + color: var(--danger); +} + +/* ── Sidebar Panels ── */ +.sidebar { + display: flex; + flex-direction: column; + gap: 16px; + position: sticky; + top: 90px; + max-height: calc(100vh - 110px); + overflow-y: auto; + padding-right: 4px; +} +.sidebar::-webkit-scrollbar { width: 4px; } +.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; } + +/* Collapsible sections */ +.collapse-trigger { + cursor: pointer; +} +.collapse-trigger .chevron { + transition: transform 0.3s ease; + font-size: 0.8rem; + color: var(--text-muted); +} +.collapse-trigger.collapsed .chevron { + transform: rotate(-90deg); +} +.collapse-content { + transition: max-height 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease, margin 0.3s ease; + max-height: 2000px; + opacity: 1; + overflow: hidden; +} +.collapse-content.collapsed { + max-height: 0; + opacity: 0; + margin: 0; + padding-top: 0; padding-bottom: 0; +} + +/* ── Team Management ── */ +.team-list { + display: flex; + flex-direction: column; + gap: 12px; +} +.team-card { + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.06); + border-radius: var(--radius-md); + padding: 12px; + position: relative; + display: flex; + flex-direction: column; + gap: 10px; + transition: border-color 0.2s; +} +.team-card:focus-within { + border-color: rgba(255,255,255,0.2); +} +.team-card-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: -4px; +} + +/* ── Right Panel ── */ +.main-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.control-bar { + display: flex; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; +} + +/* ── Log Terminal ── */ +.terminal-wrapper { + background: #000; + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + padding: 16px; + height: 360px; + overflow-y: auto; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); +} +.terminal-wrapper::-webkit-scrollbar { width: 6px; } +.terminal-wrapper::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; } +.terminal-wrapper::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); } + +.log-line { + font-family: var(--font-mono); + font-size: 0.82rem; + color: #A8B1C2; + line-height: 1.6; + word-break: break-all; + white-space: pre-wrap; +} +.log-line.info { color: #0A84FF; } +.log-line.success { color: #30D158; } +.log-line.warning { color: #FF9F0A; } +.log-line.error { color: #FF453A; } +.log-line.hilight { color: #FFF; font-weight: 600; } + +/* ── Table ── */ +.table-container { + overflow-x: auto; +} +.mac-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 0.85rem; +} +.mac-table th { + text-align: left; + padding: 12px 16px; + color: var(--text-muted); + font-weight: 500; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid rgba(255,255,255,0.08); + background: rgba(0,0,0,0.1); +} +.mac-table td { + padding: 12px 16px; + border-bottom: 1px solid rgba(255,255,255,0.04); + color: var(--text-main); + transition: background 0.2s; +} +.mac-table tbody tr:hover td { + background: rgba(255,255,255,0.03); +} +.mac-table tbody tr:last-child td { + border-bottom: none; +} + +.empty-state { + text-align: center; + padding: 32px; + color: var(--text-muted); + font-style: italic; +} + +/* Tags */ +.mac-tag { + display: inline-flex; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; +} +.mac-tag.success { + background: rgba(48, 209, 88, 0.15); + color: var(--success); + border: 1px solid rgba(48, 209, 88, 0.2); +} +.mac-tag.error { + background: rgba(255, 69, 58, 0.15); + color: var(--danger); + border: 1px solid rgba(255, 69, 58, 0.2); +} + +/* Checkbox macOS style */ +.mac-checkbox { + appearance: none; + width: 18px; + height: 18px; + border: 1px solid rgba(255,255,255,0.3); + border-radius: 4px; + background: rgba(0,0,0,0.2); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + vertical-align: middle; +} +.mac-checkbox:checked { + background: var(--accent); + border-color: var(--accent); +} +.mac-checkbox:checked::after { + content: ''; + position: absolute; + left: 5px; top: 1px; + width: 5px; height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* ── Toggle Switch ── */ +.mac-toggle { + position: relative; + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; +} +.mac-toggle input { + opacity: 0; + width: 0; height: 0; + position: absolute; +} +.toggle-track { + width: 44px; height: 24px; + background: rgba(255,255,255,0.2); + border-radius: 12px; + position: relative; + transition: background 0.3s ease; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); +} +.mac-toggle input:checked + .toggle-track { + background: var(--success); +} +.toggle-thumb { + width: 20px; height: 20px; + background: #fff; + border-radius: 50%; + position: absolute; + top: 2px; left: 2px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} +.mac-toggle input:checked + .toggle-track .toggle-thumb { + transform: translateX(20px); +} +.toggle-label { + font-size: 0.85rem; + color: var(--text-main); + user-select: none; +} + +/* ── Toolbars ── */ +.toolbar { + display: flex; + gap: 10px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +/* ── Toasts ── */ +.mac-toast-container { + position: fixed; + bottom: 24px; + right: 24px; + display: flex; + flex-direction: column; + gap: 10px; + z-index: 9999; +} + +.mac-toast { + background: rgba(30, 30, 30, 0.85); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: 1px solid rgba(255,255,255,0.1); + box-shadow: 0 10px 40px rgba(0,0,0,0.3); + border-radius: 12px; + padding: 14px 20px; + color: #fff; + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 12px; + transform: translateY(20px); + opacity: 0; + animation: toast-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +@keyframes toast-in { + to { transform: translateY(0); opacity: 1; } +} +.mac-toast.hiding { + animation: toast-out 0.3s ease-in forwards; +} +@keyframes toast-out { + to { transform: scale(0.9); opacity: 0; } +} + +.toast-icon { font-size: 1.1rem; } +.mac-toast.ok .toast-icon { color: var(--success); } +.mac-toast.err .toast-icon { color: var(--danger); } diff --git a/team_all-in-one/templates/index.html b/team_all-in-one/templates/index.html new file mode 100644 index 0000000..5a62fd6 --- /dev/null +++ b/team_all-in-one/templates/index.html @@ -0,0 +1,458 @@ + + + + + +ChatGPT 批量注册 + + + +
+
+

ChatGPT 批量注册

+
系统就绪
+
+ +
+ + +
+
+
+
+
+
+ + +
+
+ +
+
实时日志
+
System initialized. Waiting for task...
+
+ +
+
已注册账号库
+
+ + +
+ + +
+
+ + + +
#邮箱密码OAuth
正在加载...
+
+
+
+
+ +
+ + + +