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 @@
+
+
+
+
+
+
+
+
+
+
+
关键词(Email / Account ID)
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+
+
+ 检测存活
+
+
+
+
+
+
+
+
+ 检测后清理失效账号
+
+ 检测选中 {{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
+
+
+ 检测 {{ checkForm.limit > 0 ? checkForm.limit : '全部' }} ({{ checkTotal }})
+
+
+
+
+
+
+ 存活 {{ checkProgress.alive }}
+ 已死 {{ checkProgress.dead }}
+ 异常 {{ checkProgress.error }}
+ 共 {{ checkProgress.total }}
+
+
+
+
+
+
+
+
+
+
+ 共 {{ total }} 条记录
+
+ 已选 {{ selectedIds.length }} 条
+
+
+
+
+
+
+
+
+
+
+
+ {{ aliveLabel(row.alive) }}
+
+
+
+
+ {{ row.plan_type }}
+ —
+
+
+
+
+ {{ usageSummary(row.usage_json) }}
+ —
+
+
+
+ {{ formatTime(row.checked_at) }}
+
+
+
+
+
+ {{ row.error ? '失败' : '成功' }}
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ detail.id }}
+ {{ detail.session_id }}
+
+
+ {{ detail.error ? '失败' : '成功' }}
+
+
+
+ {{ aliveLabel(detail.alive) }}
+
+ {{ formatTime(detail.last_auto_refresh) }}
+ {{ formatTime(detail.checked_at) }}
+ {{ detail.email || '—' }}
+ {{ detail.account_id || '—' }}
+ {{ detail.expired || '—' }}
+
+ {{ detail.plan_type }}
+ —
+
+
+
+ {{ parsedUsage.rate_limit?.allowed ? '可用' : '不可用' }}
+
+
+ 已用 {{ parsedUsage.rate_limit.primary_window.used_percent }}%
+
+
+
+ {{ parsedUsage.promo.message || parsedUsage.promo.campaign_id }}
+
+ {{ detail.proxy_used || '—' }}
+ {{ formatTime(detail.created_at) }}
+
+ {{ detail.error }}
+
+
+
+
+ Token 信息
+
+
+
{{ detail[field.key] || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+ 选择 CSV / TXT 文件
+
+
+
+
+
+
+ 有效 {{ importProgress.alive }}
+ 失效 {{ importProgress.dead + importProgress.error }}
+ 共 {{ importProgress.total }}
+
+
+
+
+
+ 取消
+ 开始导入
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ 批量注册
+
+
+
+
+
+
+ 注册时随机选取代理,多代理可提高成功率
+ 已输入 {{ proxyCount }} 个代理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ status === 'running' ? '注册中...' : '开始注册' }}
+
+
+
+
+
+
+
+ 暂停
+
+
+
+ 继续
+
+
+
+ 导出本次 CSV
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ progress.success }}
+ /
+ {{ targetCount }}
+
+
+ 失败 {{ progress.failed }}
+
+ · 成功率 {{ successRate }}%
+
+
+
+
+
+
+
+
+
+ {{ line.time }}
+ {{ line.type === 'success' ? 'SUCCESS' : 'FAILED ' }}
+ {{ line.text }}
+
+
等待任务开始...
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ row.name || '未命名' }}
+
+
+
+
+ {{ row.schedule_type === 'daily' ? '每天' : '单次' }}
+
+
+
+
+
+
+ {{ taskTypeLabel(row.task_type || 'register') }}
+
+
+
+
+
+ 每天 {{ row.run_time }}
+ {{ formatTime(row.run_time) }}
+
+
+
+
+ {{ row.target }}
+ 自动
+
+
+ {{ {all:'全部', alive:'存活', unchecked:'未检测'}[row.check_filter] || '全部' }}
+
+
+ ×{{ row.check_limit }}
+
+
+
+
+
+
+
+ {{ formatTime(row.next_run) }}
+ --
+
+
+
+
+ {{ formatTime(row.last_run_at) }}
+ --
+
+
+
+
+ #{{ row.last_session_id }}
+ --
+
+
+
+
+
+ {{ row.enabled ? '启用' : '停用' }}
+
+
+
+
+
+
+ {{ row.enabled ? '停用' : '启用' }}
+
+ 编辑
+ 删除
+
+
+
+
+
+ 创建第一个
+
+
+
+
+
+
+
+
+
+ 执行记录
+ 刷新
+
+
+
+
+
+
+ {{ row.schedule_name || `#${row.schedule_id}` }}
+
+
+
+
+ {{ taskTypeLabel(row.task_type) }}
+
+
+
+ {{ formatTime(row.started_at) }}
+
+
+
+ {{ formatTime(row.finished_at) }}
+ 运行中...
+
+
+
+
+ {{ runStatusLabel(row.status) }}
+
+
+
+
+
+
+
+
+
{{ row.detail }}
+
+ {{ row.detail || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注册账号
+ 检测存活
+ 刷新Token
+ 清理失效
+
+
+
+
+ 单次
+ 每天
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 = 全部
+
+
+ 检测后删除失效账号
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(acct.expired) }}
+
+
+
+
+
+ {{ aliveLabel(acct.alive) }}
+
+
+
+
+
+ {{ acct.error ? '失败' : '成功' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(row.created_at) }}
+
+
+
+
+
+
+
+ {{ row.unique_ips }} 个
+ (重复 {{ row.reused_ips }})
+
+ —
+
+
+
+
+ {{ row.success }}
+
+
+
+
+ {{ row.failed }}
+
+
+
+
+ {{ row.success }} / {{ row.requested }}
+ —
+
+
+
+
+
+ {{ Math.round((row.success / (row.success + row.failed)) * 100) }}%
+
+ —
+
+
+
+
+
+ {{ statusLabel(row.status) }}
+
+
+
+
+
+
+
+ 暂停
+
+
+ 继续
+
+
+ 导出
+
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
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 = ``;
+ } 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)}
+ ${escapeHtml(statusLabel)}
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 批量注册
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
实时日志
+
System initialized. Waiting for task...
+
+
+
+
+
+
+
+
+
+
+