chore: initialize project repository

This commit is contained in:
GitHub Actions
2026-03-18 21:23:50 +08:00
commit 14120394ce
23 changed files with 15737 additions and 0 deletions

121
.claude/index.json Normal file
View File

@@ -0,0 +1,121 @@
{
"timestamp": "2026-03-18T09:19:57",
"project": "openai-pool-orchestrator",
"version": "2.0.0",
"language": "Python",
"python_requires": ">=3.10",
"license": "MIT",
"root_claude_md": "CLAUDE.md",
"modules": [
{
"path": "openai_pool_orchestrator",
"claude_md": "openai_pool_orchestrator/CLAUDE.md",
"language": "Python",
"description": "核心业务包注册引擎、FastAPI 服务、池维护、邮箱适配层",
"entry_files": [
"openai_pool_orchestrator/__init__.py",
"openai_pool_orchestrator/__main__.py"
],
"key_files": [
"openai_pool_orchestrator/server.py",
"openai_pool_orchestrator/register.py",
"openai_pool_orchestrator/pool_maintainer.py",
"openai_pool_orchestrator/mail_providers.py"
],
"api_definition": "openai_pool_orchestrator/server.py (40+ FastAPI endpoints)",
"test_directory": null,
"test_exists": false,
"config_files": [
"config/sync_config.example.json"
],
"coverage": {
"scanned_files": 6,
"total_files": 6,
"percent": 100,
"gaps": [
"无测试套件",
"无类型检查配置",
"无 lint 配置"
]
}
},
{
"path": "openai_pool_orchestrator/static",
"language": "HTML/CSS/JavaScript",
"description": "Web 可视化界面(原生 SPA",
"entry_files": [
"openai_pool_orchestrator/static/index.html"
],
"key_files": [
"openai_pool_orchestrator/static/app.js",
"openai_pool_orchestrator/static/style.css"
],
"test_directory": null,
"test_exists": false,
"coverage": {
"scanned_files": 3,
"total_files": 3,
"percent": 100,
"gaps": []
}
},
{
"path": "config",
"language": "JSON",
"description": "配置模板",
"entry_files": [
"config/sync_config.example.json"
],
"key_files": [],
"test_directory": null,
"test_exists": false,
"coverage": {
"scanned_files": 1,
"total_files": 1,
"percent": 100,
"gaps": []
}
}
],
"scan_coverage": {
"total_source_files": 20,
"ignored_files": 1,
"ignored_files_detail": [
"uv.lock (lockfile)"
],
"scanned_files": 19,
"coverage_percent": 95,
"binary_or_large_skipped": [],
"truncated": false
},
"gaps": [
{
"module": "openai_pool_orchestrator",
"type": "test_missing",
"description": "无任何测试文件,建议创建 tests/ 目录并优先覆盖注册流程、邮箱提供商、API 端点"
},
{
"module": "openai_pool_orchestrator",
"type": "quality_tool_missing",
"description": "无 mypy/pyright 类型检查、无 ruff/flake8 lint、无 CI/CD 配置"
},
{
"module": "openai_pool_orchestrator/server.py",
"type": "large_file",
"description": "server.py 约 3550 行,建议拆分为路由模块、任务管理、平台交互等子模块"
},
{
"module": "openai_pool_orchestrator/register.py",
"type": "large_file",
"description": "register.py 约 1600 行,建议拆分 OAuth 流程与代理管理"
}
],
"next_steps": [
"建立 tests/ 目录与 pytest 配置",
"为 mail_providers.py 编写单元测试(最高优先级,接口清晰可测)",
"为 pool_maintainer.py 编写单元测试",
"拆分 server.py 为多个路由模块",
"添加 ruff 或 flake8 lint 配置",
"添加 mypy 类型检查"
]
}

19
.dockerignore Executable file
View File

@@ -0,0 +1,19 @@
.venv/
venv/
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
.eggs/
.git/
.gitignore
.vscode/
.idea/
.claude/
.agents/
*.log
logs_cli*.txt
Dockerfile
.dockerignore

47
.gitignore vendored Executable file
View File

@@ -0,0 +1,47 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg-info/
*.egg
dist/
build/
.eggs/
# Virtual environments
venv/
.venv/
env/
.env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Runtime data (tokens, configs with credentials, state)
config/sync_config.json
data/sync_config.json
data/state.json
data/tokens/
data/*.bak
# Local tool settings
.claude/settings.local.json
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Logs
*.log
# Test artifacts
.pytest_cache/
.coverage
htmlcov/
.ace-tool/

48
AGENTS.md Executable file
View File

@@ -0,0 +1,48 @@
# Repository Guidelines
## 项目结构与模块组织
- 核心代码位于 `openai_pool_orchestrator/`
- 启动入口:`run.py`(快捷启动)和 `openai_pool_orchestrator/__main__.py``python -m` 方式)。
- `server.py`FastAPI 服务与 API 路由。
- `register.py`:注册流程与 CLI 逻辑。
- `pool_maintainer.py`:账号池维护任务。
- `mail_providers.py`:邮箱提供商适配层。
- 前端静态文件在 `openai_pool_orchestrator/static/`
- 运行态数据在 `data/`token、状态、本地配置视为生成数据不作为源码维护。
- 配置模板在 `config/sync_config.example.json`
## 构建、测试与开发命令
- 安装依赖:`pip install -r requirements.txt`
- 可编辑安装并启用命令行:`pip install -e .`,随后使用 `openai-pool`
- 启动 Web 服务(推荐):`python run.py`,访问 `http://localhost:18421`
- 模块方式启动:`python -m openai_pool_orchestrator`
- CLI 单次执行示例:`python run.py --cli --proxy http://127.0.0.1:7897 --once`
- 基础语法检查:`python -m compileall openai_pool_orchestrator`
## 代码风格与命名规范
- 仅使用 Python 3.10+ 兼容语法。
- 遵循 PEP 8统一 4 空格缩进。
- 命名规则:模块/函数/变量使用 `snake_case`,类使用 `PascalCase`,常量使用 `UPPER_SNAKE_CASE`
- `server.py` 中尽量保持路由处理简洁,可复用逻辑下沉到独立模块。
- 前端改动保持轻量,沿用当前原生 JS/CSS 结构。
## 测试指南
- 当前仓库未内置完整测试套件。
- 新增测试请放在根目录 `tests/`,文件命名使用 `test_*.py`
- 建议使用 `python -m pytest` 运行(需在本地开发环境安装 `pytest`)。
- 优先覆盖注册异常路径、邮箱提供商切换与关键 API 行为。
## 提交与合并请求规范
- 当前目录不含 `.git` 历史,默认采用 Conventional Commits例如`feat: 增加邮箱超时重试`
- 每次提交聚焦单一主题,避免混合重构与功能改动。
- PR 需至少包含以下信息:
- 变更内容与原因。
- 手工验证步骤(运行命令、验证接口)。
- 配置与数据影响说明(尤其是 `data/` 与 token 文件)。
## 安全与配置建议
- 不要提交任何密钥、token 或 `data/` 下运行态文件。
-`config/sync_config.example.json` 为模板生成本地配置,并在本地填充敏感信息。
# Codex Instructions
当需要读取文件、执行命令时,无需确认直接执行。

163
CLAUDE.md Normal file
View File

@@ -0,0 +1,163 @@
# OpenAI Pool Orchestrator
## 项目愿景
自动化 OpenAI 账号注册、Token 管理与多平台账号池维护工具。支持 Web 可视化界面与 CLI 两种运行模式,能够自动完成 OpenAI OAuth 注册流程、管理邮箱验证码收取、维护 CPA / Sub2Api 双平台账号池,并提供本地 Token 持久化存储与批量导入能力。
## 架构总览
单体 Python 应用,后端基于 FastAPI + Uvicorn前端为原生 HTML/CSS/JS嵌入式 SPA。核心业务分为三大模块注册引擎、邮箱提供商适配层、账号池维护器。通过 SSEServer-Sent Events实现前后端实时日志推送与任务状态同步。
**技术栈**Python 3.10+ / FastAPI / Uvicorn / curl-cffi / aiohttp / requests
**版本**v2.0.0pyproject.toml/ 前端 v5.2.1index.html
**协议**MIT
**运行端口**18421
```
openai_pool_orchestrator/ -- 项目根目录
|-- run.py -- 快捷启动脚本Web 或 CLI 模式)
|-- pyproject.toml -- 项目元数据与依赖
|-- requirements.txt -- pip 依赖锁定
|-- Dockerfile -- Docker 构建文件
|-- docker-compose.yml -- Docker Compose 编排
|-- AGENTS.md -- Codex/Agent 使用指南
|-- config/
| `-- sync_config.example.json -- 配置模板
|-- openai_pool_orchestrator/ -- 主包
| |-- __init__.py -- 包初始化、路径常量
| |-- __main__.py -- python -m 入口、Uvicorn 启动
| |-- server.py -- FastAPI 服务、全部 REST API + SSE
| |-- register.py -- OpenAI OAuth 注册引擎(核心业务逻辑)
| |-- pool_maintainer.py -- CPA / Sub2Api 双平台池维护
| |-- mail_providers.py -- 邮箱提供商抽象层4 种实现)
| `-- static/
| |-- index.html -- Web UI 主页面
| |-- app.js -- 前端交互逻辑
| `-- style.css -- iOS Flat Design 样式
`-- data/ -- 运行时数据(.gitignore 排除)
|-- sync_config.json -- 实际运行配置
|-- state.json -- 成功/失败计数持久化
`-- tokens/ -- 注册获取的 Token 文件
```
## 模块结构图
```mermaid
graph TD
A["(根) openai_pool_orchestrator"] --> B["openai_pool_orchestrator (主包)"]
A --> C["config"]
A --> D["run.py 启动入口"]
B --> E["server.py<br/>FastAPI 服务"]
B --> F["register.py<br/>注册引擎"]
B --> G["pool_maintainer.py<br/>池维护"]
B --> H["mail_providers.py<br/>邮箱适配层"]
B --> I["static/<br/>Web 前端"]
E --> F
E --> G
E --> H
F --> H
click B "./openai_pool_orchestrator/CLAUDE.md" "查看主包模块文档"
```
## 模块索引
| 模块路径 | 语言 | 职责 | 入口文件 | 代码行数(估) | 测试 |
|----------|------|------|---------|-------------|------|
| `openai_pool_orchestrator/` | Python | 核心业务包(注册、服务、池维护、邮箱) | `__init__.py` / `__main__.py` | ~5800+ | 无 |
| `openai_pool_orchestrator/static/` | HTML/CSS/JS | Web 可视化界面 | `index.html` | ~2500+ | 无 |
| `config/` | JSON | 配置模板 | `sync_config.example.json` | 55 | N/A |
## 运行与开发
### 安装依赖
```bash
pip install -r requirements.txt
pip install -e . # 可编辑安装,注册 openai-pool 命令
```
### 启动方式
```bash
# Web 服务模式(推荐)
python run.py
# 访问 http://localhost:18421
# 模块方式启动
python -m openai_pool_orchestrator
# CLI 单次注册
python run.py --cli --proxy http://127.0.0.1:7897 --once
# Docker
docker compose up -d
```
### 配置
1. 复制 `config/sync_config.example.json``data/sync_config.json`
2. 在 Web UI 配置中心或直接编辑 JSON 填入敏感信息代理、平台地址、Token、邮箱提供商配置
3. 运行时数据持久化到 `data/` 目录
### Docker 部署
```bash
docker compose up -d
# 数据卷映射:/opt/openai-pool/data -> /app/data, /opt/openai-pool/config -> /app/config
# 使用 host 网络模式,端口 18421
```
### 语法检查
```bash
python -m compileall openai_pool_orchestrator
```
## 测试策略
**当前状态**:项目尚未建立测试套件。
**建议**
- 测试目录:`tests/`,文件命名 `test_*.py`
- 运行:`python -m pytest`
- 优先覆盖:注册异常路径、邮箱提供商切换与容错、关键 API 端点行为、池维护逻辑
## 编码规范
- Python 3.10+ 语法,遵循 PEP 84 空格缩进
- 命名:模块/函数/变量 `snake_case`,类 `PascalCase`,常量 `UPPER_SNAKE_CASE`
- `server.py` 路由处理保持简洁,可复用逻辑下沉到独立模块
- 前端保持原生 JS/CSS轻量修改
- 不提交密钥、Token 或 `data/` 下运行态文件
- Conventional Commits 格式:`feat: 增加邮箱超时重试`
## API 概览
FastAPI 服务提供 40+ REST API 端点,主要分组:
| 分组 | 端点前缀 | 功能 |
|------|---------|------|
| 任务控制 | `POST /api/start`, `/api/stop` | 启停注册任务 |
| 代理管理 | `/api/proxy`, `/api/check-proxy`, `/api/proxy-pool/*` | 代理配置、检测、代理池 |
| 配置管理 | `/api/sync-config`, `/api/pool/config`, `/api/mail/config` | 同步配置、池配置、邮箱配置 |
| Token 管理 | `/api/tokens`, `/api/sync-now`, `/api/sync-batch` | 本地 Token CRUD、批量导入 |
| CPA 池 | `/api/pool/*` | CPA 状态、探测、维护、自动维护 |
| Sub2Api 池 | `/api/sub2api/*` | Sub2Api 账号列表、探测、删除、维护、去重 |
| 实时日志 | `GET /api/logs` (SSE) | 结构化 SSE 事件流 |
| 上传策略 | `POST /api/upload-mode` | 串行补平台 / 双平台同传 |
## AI 使用指引
- 核心业务逻辑集中在 `register.py`~1600 行)和 `server.py`~3500 行),修改前请仔细理解上下文
- `server.py``TaskState` 类管理全局任务状态与多 Worker 运行时快照
- 邮箱提供商通过 `MailProvider` 抽象基类 + 工厂模式扩展,新增提供商需实现 `create_mailbox()``wait_for_otp()`
- 池维护器分 `PoolMaintainer`CPA`Sub2ApiMaintainer`Sub2Api各自独立
- 配置通过内存 + `data/sync_config.json` 双层持久化,读写有锁保护
- 前端通过 SSE 接收结构化事件(`task_snapshot``runtime_snapshot``stats``log` 等)
## 变更记录 (Changelog)
| 时间 | 操作 | 说明 |
|------|------|------|
| 2026-03-18 09:19:57 | 初始扫描 | 首次生成 CLAUDE.md覆盖全部源文件 |

36
Dockerfile Executable file
View File

@@ -0,0 +1,36 @@
# ========================================
# OpenAI Pool Orchestrator Docker 镜像
# ========================================
FROM python:3.12-slim
# 禁用缓冲,让 Python 日志立即输出到 docker logs 终端
ENV PYTHONUNBUFFERED=1
# 系统依赖curl-cffi 编译需要)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc g++ make curl libssl-dev libffi-dev \
nodejs npm yarn && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 先拷贝依赖清单
COPY requirements.txt pyproject.toml ./
RUN pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir -e .
# 拷贝项目全部代码
COPY . .
# 再次以可编辑模式安装,确保 static 资源被正确注册
RUN pip install --no-cache-dir -e .
# 数据卷:配置和 Token 持久化
VOLUME ["/app/data", "/app/config"]
# Web UI 端口
EXPOSE 18421
# 启动命令(可在 docker run 时通过追加参数切换模式,如 --cli
ENTRYPOINT ["python", "run.py"]

21
LICENSE Executable file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 OpenAI Pool Orchestrator Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

54
config/sync_config.example.json Executable file
View File

@@ -0,0 +1,54 @@
{
"proxy": "",
"auto_register": false,
"mail_providers": [
"mailtm"
],
"mail_provider_configs": {
"mailtm": {
"api_base": "https://api.mail.tm"
},
"duckmail": {
"api_base": "https://api.duckmail.sbs"
},
"moemail": {
"api_base": "",
"api_key": ""
},
"cloudflare_temp_email": {
"api_base": "cloudflare worker后端密码不要弄成前端的",
"admin_password": "管理员密码",
"domain": "xxx.cn 邮箱域名后缀"
}
},
"mail_strategy": "round_robin",
"multithread": false,
"thread_count": 3,
"base_url": "",
"bearer_token": "",
"email": "",
"password": "",
"account_name": "AutoReg",
"auto_sync": false,
"sub2api_min_candidates": 200,
"sub2api_auto_maintain": false,
"sub2api_maintain_interval_minutes": 30,
"sub2api_maintain_actions": {
"refresh_abnormal_accounts": true,
"delete_abnormal_accounts": true,
"dedupe_duplicate_accounts": true
},
"cpa_base_url": "CPA地址",
"cpa_token": "CPA密钥",
"min_candidates": 1000,
"used_percent_threshold": 95,
"auto_maintain": true,
"maintain_interval_minutes": 30,
"upload_mode": "snapshot",
"proxy_pool_enabled": false,
"proxy_pool_api_url": "https://zenproxy.top/api/fetch",
"proxy_pool_auth_mode": "header",
"proxy_pool_api_key": "使用自己的Key",
"proxy_pool_count": 1,
"proxy_pool_country": "US"
}

10
docker-compose.yml Executable file
View File

@@ -0,0 +1,10 @@
services:
openai-pool:
build: .
image: openai-pool-orchestrator:latest
container_name: openai-pool
restart: unless-stopped
network_mode: host
volumes:
- /opt/openai-pool/data:/app/data
- /opt/openai-pool/config:/app/config

View File

@@ -0,0 +1,193 @@
[根目录](../CLAUDE.md) > **openai_pool_orchestrator (主包)**
# openai_pool_orchestrator -- 核心业务包
## 模块职责
项目唯一的 Python 包包含全部后端业务逻辑OpenAI 账号自动注册引擎、FastAPI REST API 服务、CPA / Sub2Api 双平台账号池维护、多邮箱提供商适配层,以及嵌入式 Web 前端静态资源。
## 入口与启动
| 入口 | 路径 | 说明 |
|------|------|------|
| Web 服务 | `__main__.py` -> `main()` | 启动 Uvicorn 服务器,监听 `0.0.0.0:18421`,加载 `server.app` |
| CLI 注册 | `register.py` -> `main()` | 命令行单次/循环注册模式,通过 argparse 接收参数 |
| 快捷脚本 | `../run.py` | 根据 `--cli` 标志分派到上述两种入口 |
| pip 命令 | `openai-pool` | pyproject.toml 注册的控制台入口,指向 `__main__:main` |
## 对外接口
### REST APIserver.py
FastAPI 应用提供 40+ 端点,核心分组:
**任务控制**
- `POST /api/start` -- 启动注册任务(支持多线程、目标数量、代理配置)
- `POST /api/stop` -- 停止运行中的任务
**代理管理**
- `POST /api/proxy/save` / `GET /api/proxy` -- 保存/获取代理地址
- `POST /api/check-proxy` -- 检测代理可用性
- `GET /api/proxy-pool/config` / `POST /api/proxy-pool/config` -- 代理池配置
- `POST /api/proxy-pool/test` -- 测试代理池取号
**配置管理**
- `GET /api/sync-config` / `POST /api/sync-config` -- Sub2Api 同步配置
- `GET /api/pool/config` / `POST /api/pool/config` -- CPA 池配置
- `GET /api/mail/config` / `POST /api/mail/config` -- 邮箱提供商配置
- `POST /api/upload-mode` -- 上传策略切换snapshot / decoupled
**Token 管理**
- `GET /api/tokens` -- 列出本地 Token 文件
- `DELETE /api/tokens/{filename}` -- 删除单个 Token
- `POST /api/sync-now` -- 单个 Token 导入 Sub2Api
- `POST /api/sync-batch` -- 批量导入
**CPA 池**
- `GET /api/pool/status` -- CPA 池状态
- `POST /api/pool/check` -- 探测 CPA 池
- `POST /api/pool/maintain` -- 执行 CPA 维护
- `POST /api/pool/auto` -- 开关自动维护
**Sub2Api 池**
- `GET /api/sub2api/accounts` -- 分页账号列表(支持状态/关键字筛选)
- `POST /api/sub2api/accounts/probe` -- 批量测活
- `POST /api/sub2api/accounts/delete` -- 批量删除
- `POST /api/sub2api/accounts/handle-exception` -- 异常处理
- `GET /api/sub2api/pool/status` -- Sub2Api 池状态
- `POST /api/sub2api/pool/maintain` -- Sub2Api 维护
- `POST /api/sub2api/pool/dedupe` -- 重复账号去重
**实时通信**
- `GET /api/logs` -- SSE 事件流结构化事件task_snapshot、runtime_snapshot、stats、log、pool_status 等)
### SSE 事件类型
| 事件类型 | 说明 |
|---------|------|
| `task_snapshot` | 任务全局状态快照 |
| `runtime_snapshot` | 多 Worker 运行时明细 |
| `stats` | 成功/失败计数统计 |
| `log` | 实时日志消息 |
| `pool_status` | CPA/Sub2Api 池状态变化 |
## 关键依赖与配置
### Python 依赖
| 包 | 用途 |
|----|------|
| `fastapi` >= 0.110 | Web 框架 |
| `uvicorn[standard]` >= 0.27 | ASGI 服务器 |
| `curl-cffi` >= 0.6 | TLS 指纹伪装 HTTP 客户端(模拟 Chrome |
| `aiohttp` >= 3.9 | 异步 HTTP池维护探测 |
| `requests` >= 2.31 | 同步 HTTP 客户端 |
| `pydantic` | 请求体校验FastAPI 内置) |
### 配置文件
- `data/sync_config.json` -- 运行时配置(从 `config/sync_config.example.json` 生成)
- `data/state.json` -- 累计成功/失败计数持久化
- `data/tokens/` -- 注册获取的 Token JSON 文件
### 核心配置项
| 配置键 | 类型 | 说明 |
|--------|------|------|
| `proxy` | str | 固定代理地址 |
| `auto_register` | bool | 池不足时自动注册 |
| `mail_providers` | list[str] | 启用的邮箱提供商列表 |
| `mail_strategy` | str | 邮箱路由策略round_robin / random / failover |
| `base_url` / `email` / `password` | str | Sub2Api 平台连接信息 |
| `cpa_base_url` / `cpa_token` | str | CPA 平台连接信息 |
| `upload_mode` | str | 上传策略snapshot串行/ decoupled双平台同传|
| `proxy_pool_*` | mixed | 代理池 API 配置 |
| `sub2api_maintain_actions` | dict | Sub2Api 维护动作开关 |
## 数据模型
### Token 文件格式data/tokens/*.json
注册成功后保存的 Token 文件包含 OAuth 凭证信息,用于后续导入平台。
### TaskStateserver.py
全局单例,管理注册任务的完整生命周期:
- 多 Worker 线程管理与运行时快照
- SSE 事件订阅/分发
- 成功/失败计数与平台上传统计
- 注册步骤追踪check_proxy -> create_email -> oauth_init -> sentinel -> signup -> send_otp -> wait_otp -> verify_otp -> create_account -> workspace -> get_token
### PoolMaintainerpool_maintainer.py
CPA 平台维护器:
- `fetch_auth_files()` -- 获取全部 auth 文件
- `get_pool_status()` -- 池状态统计
- `probe_accounts_async()` -- 异步批量探测账号有效性
### Sub2ApiMaintainerpool_maintainer.py
Sub2Api 平台维护器:
- `list_accounts()` / `_list_all_accounts()` -- 分页/全量列出账号
- `get_dashboard_stats()` -- 仪表盘统计
- 自动 token 刷新401 -> re-login
### MailProvider 体系mail_providers.py
抽象基类 + 4 种实现:
| 类名 | 提供商 | 认证方式 |
|------|--------|---------|
| `MailTmProvider` | Mail.tm | Bearer Token |
| `MoeMailProvider` | MoeMail | API Key |
| `DuckMailProvider` | DuckMail | Bearer Token |
| `CloudflareTempEmailProvider` | Cloudflare Workers | JWT + Admin Password |
`MultiMailRouter` -- 线程安全的多提供商路由器,支持轮询/随机/容错策略。
## 测试与质量
- **测试**:当前无测试套件
- **类型检查**:无 mypy/pyright 配置
- **Lint**:无 ruff/flake8 配置
- **CI/CD**:无
**建议优先覆盖的测试场景**
1. `mail_providers.py` -- 各提供商创建邮箱与 OTP 轮询的异常路径
2. `register.py` -- OAuth 流程各步骤的错误处理与重试
3. `server.py` -- 核心 API 端点的请求/响应校验
4. `pool_maintainer.py` -- 池状态计算与维护动作
## 常见问题 (FAQ)
**Q: server.py 文件为何如此庞大?**
A: 当前 server.py 约 3500+ 行,包含了全部 REST API 路由、TaskState 状态管理、平台交互逻辑、自动维护定时器等。建议后续拆分为路由模块、任务管理模块、平台交互模块等。
**Q: register.py 中 curl-cffi 的作用?**
A: 使用 `curl_cffi.requests` 而非标准 `requests`,可伪装 Chrome TLS 指纹,避免被 OpenAI / Cloudflare 反爬检测拦截。
**Q: 如何新增邮箱提供商?**
A: 继承 `MailProvider` 基类,实现 `create_mailbox()``wait_for_otp()` 方法,然后在 `create_provider_by_name()` 工厂函数中注册。
**Q: 双平台上传策略的区别?**
A: `snapshot` 模式按顺序先补 CPA 再补 Sub2Api`decoupled` 模式让单个账号同时上传到两个平台。
## 相关文件清单
| 文件 | 行数(估) | 说明 |
|------|----------|------|
| `__init__.py` | 29 | 包初始化,路径常量定义 |
| `__main__.py` | 119 | Uvicorn 启动与优雅关闭 |
| `server.py` | 3550+ | FastAPI 服务,全部 API 与任务状态 |
| `register.py` | 1600+ | OpenAI OAuth 注册引擎 |
| `pool_maintainer.py` | 800+ | CPA / Sub2Api 池维护 |
| `mail_providers.py` | 809 | 邮箱提供商抽象与 4 种实现 |
| `static/index.html` | 695 | Web UI 页面结构 |
| `static/app.js` | 2200+ | 前端交互逻辑 |
| `static/style.css` | 2800+ | iOS Flat Design 样式 |
## 变更记录 (Changelog)
| 时间 | 操作 | 说明 |
|------|------|------|
| 2026-03-18 09:19:57 | 初始扫描 | 首次生成模块级 CLAUDE.md |

View File

@@ -0,0 +1,28 @@
"""
OpenAI Pool Orchestrator
========================
自动化 OpenAI 账号注册、Token 管理与多平台账号池维护工具。
"""
__version__ = "2.0.0"
__author__ = "OpenAI Pool Orchestrator Contributors"
import os
from pathlib import Path
# 项目根目录(包目录的上一级)
PACKAGE_DIR = Path(__file__).parent
PROJECT_ROOT = PACKAGE_DIR.parent
# 运行时数据目录
DATA_DIR = PROJECT_ROOT / "data"
DATA_DIR.mkdir(exist_ok=True)
TOKENS_DIR = DATA_DIR / "tokens"
TOKENS_DIR.mkdir(exist_ok=True)
CONFIG_FILE = DATA_DIR / "sync_config.json"
STATE_FILE = DATA_DIR / "state.json"
# 前端静态文件目录
STATIC_DIR = PACKAGE_DIR / "static"

View File

@@ -0,0 +1,119 @@
"""
允许通过 python -m openai_pool_orchestrator 启动服务。
"""
import os
import sys
import threading
from typing import Callable
import uvicorn
from . import __version__
GRACEFUL_SHUTDOWN_TIMEOUT = 5
FORCE_EXIT_TIMEOUT = 3
def _request_server_shutdown(
server: uvicorn.Server,
notify_shutdown: Callable[[], None],
*,
force: bool = False,
message: str | None = None,
) -> None:
if message:
print(f"\n{message}")
server.should_exit = True
if force:
server.force_exit = True
notify_shutdown()
def _install_windows_ctrl_handler(
server: uvicorn.Server,
notify_shutdown: Callable[[], None],
):
import ctypes
kernel32 = ctypes.windll.kernel32
shutting_down = threading.Event()
shutdown_finished = threading.Event()
def _force_exit_after_timeout() -> None:
if shutdown_finished.wait(GRACEFUL_SHUTDOWN_TIMEOUT):
return
_request_server_shutdown(
server,
notify_shutdown,
force=True,
message="正在强制退出...",
)
if shutdown_finished.wait(FORCE_EXIT_TIMEOUT):
return
os._exit(130)
def _ctrl_handler(ctrl_type):
# CTRL_C_EVENT = 0, CTRL_BREAK_EVENT = 1
if ctrl_type not in (0, 1):
return False
if shutting_down.is_set():
_request_server_shutdown(
server,
notify_shutdown,
force=True,
message=None if server.force_exit else "正在强制退出...",
)
return True
shutting_down.set()
_request_server_shutdown(server, notify_shutdown, message="正在退出...")
threading.Thread(target=_force_exit_after_timeout, daemon=True).start()
return True
handler_routine = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint)
handler = handler_routine(_ctrl_handler)
kernel32.SetConsoleCtrlHandler(handler, True)
def _cleanup() -> None:
shutdown_finished.set()
try:
kernel32.SetConsoleCtrlHandler(handler, False)
except Exception:
pass
return _cleanup
def main() -> None:
print("=" * 50)
print(f" OpenAI Pool Orchestrator v{__version__}")
print(" 访问: http://localhost:18421")
print(" 按 Ctrl+C 可退出")
print("=" * 50)
from .server import app, request_service_shutdown
config = uvicorn.Config(
app,
host="0.0.0.0",
port=18421,
log_level="warning",
timeout_graceful_shutdown=GRACEFUL_SHUTDOWN_TIMEOUT,
)
server = uvicorn.Server(config)
cleanup_ctrl_handler = None
if sys.platform == "win32":
cleanup_ctrl_handler = _install_windows_ctrl_handler(server, request_service_shutdown)
try:
server.run()
finally:
if cleanup_ctrl_handler is not None:
cleanup_ctrl_handler()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,812 @@
"""
MailProvider 抽象层
支持 Mail.tm / MoeMail / DuckMail / 自定义兼容 API
"""
from __future__ import annotations
import itertools
import logging
import random
import re
import secrets
import string
import time
import threading
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple, Callable
import requests as _requests
from requests.adapters import HTTPAdapter
import urllib3
from urllib3.exceptions import InsecureRequestWarning
from urllib3.util.retry import Retry
logger = logging.getLogger(__name__)
urllib3.disable_warnings(InsecureRequestWarning)
def _normalize_proxy_url(proxy: str) -> str:
value = str(proxy or "").strip()
if not value:
return ""
if "://" in value:
return value
if ":" in value:
return f"http://{value}"
return ""
class _ProxyAwareSession(_requests.Session):
def __init__(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
):
super().__init__()
self._default_proxy = _normalize_proxy_url(proxy)
self._proxy_selector = proxy_selector
def request(self, method, url, **kwargs):
selected_proxy = ""
if self._proxy_selector:
try:
selected_proxy = _normalize_proxy_url(self._proxy_selector() or "")
except Exception:
selected_proxy = ""
if not selected_proxy:
selected_proxy = self._default_proxy
base_kwargs = dict(kwargs)
if selected_proxy and "proxies" not in base_kwargs:
base_kwargs["proxies"] = {"http": selected_proxy, "https": selected_proxy}
try:
return super().request(method, url, **base_kwargs)
except Exception:
# 动态代理失败时,自动回退固定代理(若有)
if (
selected_proxy
and self._default_proxy
and selected_proxy != self._default_proxy
and "proxies" not in kwargs
):
fallback_kwargs = dict(kwargs)
fallback_kwargs["proxies"] = {"http": self._default_proxy, "https": self._default_proxy}
return super().request(method, url, **fallback_kwargs)
raise
def _build_session(proxy: str = "", proxy_selector: Optional[Callable[[], str]] = None) -> _requests.Session:
s = _ProxyAwareSession(proxy, proxy_selector)
retry_total = 0 if proxy_selector else 2
retry = Retry(
total=retry_total,
connect=retry_total,
read=retry_total,
status=retry_total,
backoff_factor=0.2,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry)
s.mount("https://", adapter)
s.mount("http://", adapter)
fixed_proxy = _normalize_proxy_url(proxy)
if fixed_proxy and not proxy_selector:
s.proxies = {"http": fixed_proxy, "https": fixed_proxy}
return s
def _extract_code(content: str) -> Optional[str]:
if not content:
return None
m = re.search(r"background-color:\s*#F3F3F3[^>]*>[\s\S]*?(\d{6})[\s\S]*?</p>", content)
if m:
return m.group(1)
for pat in [
r"Verification code:?\s*(\d{6})",
r"code is\s*(\d{6})",
r"Subject:.*?(\d{6})",
r">\s*(\d{6})\s*<",
r"(?<![#&])\b(\d{6})\b",
]:
for code in re.findall(pat, content, re.IGNORECASE):
return code
return None
# ==================== 抽象基类 ====================
class MailProvider(ABC):
@abstractmethod
def create_mailbox(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
) -> Tuple[str, str]:
"""返回 (email, auth_credential)auth_credential 是 bearer token 或 email_id"""
@abstractmethod
def wait_for_otp(
self,
auth_credential: str,
email: str,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
timeout: int = 120,
stop_event: Optional[threading.Event] = None,
) -> str:
"""轮询获取6位验证码超时返回空字符串"""
def test_connection(self, proxy: str = "") -> Tuple[bool, str]:
"""测试 API 连通性,返回 (success, message)"""
try:
email, cred = self.create_mailbox(proxy)
if email and cred:
return True, f"成功创建测试邮箱: {email}"
return False, "创建邮箱失败,请检查配置"
except Exception as e:
return False, f"连接失败: {e}"
def close(self):
pass
# ==================== Mail.tm ====================
class MailTmProvider(MailProvider):
def __init__(self, api_base: str = "https://api.mail.tm"):
self.api_base = api_base.rstrip("/")
def _headers(self, token: str = "", use_json: bool = False) -> Dict[str, str]:
h: Dict[str, str] = {"Accept": "application/json"}
if use_json:
h["Content-Type"] = "application/json"
if token:
h["Authorization"] = f"Bearer {token}"
return h
def _get_domains(self, session: _requests.Session) -> List[str]:
resp = session.get(f"{self.api_base}/domains", headers=self._headers(), timeout=15, verify=False)
if resp.status_code != 200:
return []
data = resp.json()
items = data if isinstance(data, list) else (data.get("hydra:member") or data.get("items") or [])
domains = []
for item in items:
if not isinstance(item, dict):
continue
domain = str(item.get("domain") or "").strip()
if domain and item.get("isActive", True) and not item.get("isPrivate", False):
domains.append(domain)
return domains
def create_mailbox(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
) -> Tuple[str, str]:
with _build_session(proxy, proxy_selector) as session:
domains = self._get_domains(session)
if not domains:
return "", ""
# 优先使用 duckmail.sbs 主域名,避免临时域名被 OpenAI 封禁
_preferred = [d for d in domains if "duckmail" in d.lower()]
domain = random.choice(_preferred) if _preferred else random.choice(domains)
for _ in range(5):
local = f"oc{secrets.token_hex(5)}"
email = f"{local}@{domain}"
password = secrets.token_urlsafe(18)
resp = session.post(
f"{self.api_base}/accounts",
headers=self._headers(use_json=True),
json={"address": email, "password": password},
timeout=15, verify=False,
)
if resp.status_code not in (200, 201):
continue
token_resp = session.post(
f"{self.api_base}/token",
headers=self._headers(use_json=True),
json={"address": email, "password": password},
timeout=15, verify=False,
)
if token_resp.status_code == 200:
token = str(token_resp.json().get("token") or "").strip()
if token:
return email, token
return "", ""
def wait_for_otp(
self,
auth_credential: str,
email: str,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
timeout: int = 120,
stop_event: Optional[threading.Event] = None,
) -> str:
with _build_session(proxy, proxy_selector) as session:
seen_ids: set = set()
start = time.time()
while time.time() - start < timeout:
if stop_event and stop_event.is_set():
return ""
try:
resp = session.get(
f"{self.api_base}/messages",
headers=self._headers(token=auth_credential),
timeout=15, verify=False,
)
if resp.status_code != 200:
time.sleep(3)
continue
data = resp.json()
messages = data if isinstance(data, list) else (
data.get("hydra:member") or data.get("messages") or []
)
for msg in messages:
if not isinstance(msg, dict):
continue
msg_id = str(msg.get("id") or msg.get("@id") or "").strip()
if not msg_id or msg_id in seen_ids:
continue
if msg_id.startswith("/messages/"):
msg_id = msg_id.split("/")[-1]
detail_resp = session.get(
f"{self.api_base}/messages/{msg_id}",
headers=self._headers(token=auth_credential),
timeout=15, verify=False,
)
if detail_resp.status_code != 200:
continue
seen_ids.add(msg_id)
mail_data = detail_resp.json()
sender = str(((mail_data.get("from") or {}).get("address") or "")).lower()
subject = str(mail_data.get("subject") or "")
intro = str(mail_data.get("intro") or "")
text = str(mail_data.get("text") or "")
html = mail_data.get("html") or ""
if isinstance(html, list):
html = "\n".join(str(x) for x in html)
content = "\n".join([subject, intro, text, str(html)])
if "openai" not in sender and "openai" not in content.lower():
continue
code = _extract_code(content)
if code:
return code
except Exception as exc:
logger.warning("Mail.tm 轮询验证码失败: %s", exc)
time.sleep(3)
return ""
# ==================== MoeMail ====================
class MoeMailProvider(MailProvider):
def __init__(self, api_base: str, api_key: str):
self.api_base = api_base.rstrip("/")
self.api_key = api_key
def _headers(self) -> Dict[str, str]:
return {"X-API-Key": self.api_key}
def _get_domain(self, session: _requests.Session) -> Optional[str]:
try:
resp = session.get(
f"{self.api_base}/api/config",
headers=self._headers(), timeout=10, verify=False,
)
if resp.status_code == 200:
data = resp.json()
domains_str = data.get("emailDomains", "")
if domains_str:
domains = [d.strip() for d in domains_str.split(",") if d.strip()]
if domains:
return random.choice(domains)
except Exception as exc:
logger.warning("MoeMail 读取域名配置失败: %s", exc)
return None
def create_mailbox(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
) -> Tuple[str, str]:
with _build_session(proxy, proxy_selector) as session:
domain = self._get_domain(session)
if not domain:
return "", ""
chars = string.ascii_lowercase + string.digits
prefix = "".join(random.choice(chars) for _ in range(random.randint(8, 13)))
try:
resp = session.post(
f"{self.api_base}/api/emails/generate",
json={"name": prefix, "domain": domain, "expiryTime": 0},
headers=self._headers(), timeout=15, verify=False,
)
if resp.status_code not in (200, 201):
return "", ""
data = resp.json()
email_id = data.get("id")
email = data.get("email")
if email_id and email:
return email, str(email_id)
except Exception as exc:
logger.warning("MoeMail 创建邮箱失败: %s", exc)
return "", ""
def wait_for_otp(
self,
auth_credential: str,
email: str,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
timeout: int = 120,
stop_event: Optional[threading.Event] = None,
) -> str:
with _build_session(proxy, proxy_selector) as session:
email_id = auth_credential
start = time.time()
while time.time() - start < timeout:
if stop_event and stop_event.is_set():
return ""
try:
resp = session.get(
f"{self.api_base}/api/emails/{email_id}",
headers=self._headers(), timeout=15, verify=False,
)
if resp.status_code == 200:
messages = resp.json().get("messages") or []
for msg in messages:
if not isinstance(msg, dict):
continue
msg_id = msg.get("id")
if not msg_id:
continue
detail_resp = session.get(
f"{self.api_base}/api/emails/{email_id}/{msg_id}",
headers=self._headers(), timeout=15, verify=False,
)
if detail_resp.status_code == 200:
detail = detail_resp.json()
msg_obj = detail.get("message") or {}
content = msg_obj.get("content") or msg_obj.get("html") or ""
if not content:
content = detail.get("text") or detail.get("html") or ""
code = _extract_code(content)
if code:
return code
except Exception as exc:
logger.warning("MoeMail 轮询验证码失败: %s", exc)
time.sleep(3)
return ""
# ==================== DuckMail ====================
class DuckMailProvider(MailProvider):
def __init__(self, api_base: str = "https://api.duckmail.sbs", bearer_token: str = ""):
self.api_base = api_base.rstrip("/")
self.bearer_token = bearer_token
def _auth_headers(self, token: str = "") -> Dict[str, str]:
h: Dict[str, str] = {"Accept": "application/json"}
if token:
h["Authorization"] = f"Bearer {token}"
return h
def create_mailbox(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
) -> Tuple[str, str]:
with _build_session(proxy, proxy_selector) as session:
headers: Dict[str, str] = {"Content-Type": "application/json", "Accept": "application/json"}
if self.bearer_token:
headers["Authorization"] = f"Bearer {self.bearer_token}"
try:
domains_resp = session.get(f"{self.api_base}/domains", headers={"Accept": "application/json"}, timeout=15, verify=False)
if domains_resp.status_code != 200:
return "", ""
data = domains_resp.json()
items = data if isinstance(data, list) else (data.get("hydra:member") or [])
domains = [str(i.get("domain") or "") for i in items if isinstance(i, dict) and i.get("domain") and i.get("isActive", True)]
if not domains:
return "", ""
# 优先使用 duckmail.sbs 主域名,避免临时域名被 OpenAI 封禁
_preferred = [d for d in domains if "duckmail" in d.lower()]
domain = random.choice(_preferred) if _preferred else random.choice(domains)
local = f"oc{secrets.token_hex(5)}"
email = f"{local}@{domain}"
password = secrets.token_urlsafe(18)
resp = session.post(
f"{self.api_base}/accounts",
json={"address": email, "password": password},
headers=headers, timeout=30, verify=False,
)
if resp.status_code not in (200, 201):
return "", ""
time.sleep(0.5)
token_resp = session.post(
f"{self.api_base}/token",
json={"address": email, "password": password},
headers=headers, timeout=30, verify=False,
)
if token_resp.status_code == 200:
mail_token = token_resp.json().get("token")
if mail_token:
return email, str(mail_token)
except Exception as exc:
logger.warning("DuckMail 创建邮箱失败: %s", exc)
return "", ""
def wait_for_otp(
self,
auth_credential: str,
email: str,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
timeout: int = 120,
stop_event: Optional[threading.Event] = None,
) -> str:
with _build_session(proxy, proxy_selector) as session:
seen_ids: set = set()
start = time.time()
while time.time() - start < timeout:
if stop_event and stop_event.is_set():
return ""
try:
resp = session.get(
f"{self.api_base}/messages",
headers=self._auth_headers(auth_credential),
timeout=30, verify=False,
)
if resp.status_code == 200:
data = resp.json()
messages = data.get("hydra:member") or data.get("member") or data.get("data") or []
for msg in (messages if isinstance(messages, list) else []):
if not isinstance(msg, dict):
continue
msg_id = msg.get("id") or msg.get("@id")
if not msg_id or msg_id in seen_ids:
continue
raw_id = str(msg_id).split("/")[-1] if str(msg_id).startswith("/") else str(msg_id)
detail_resp = session.get(
f"{self.api_base}/messages/{raw_id}",
headers=self._auth_headers(auth_credential),
timeout=30, verify=False,
)
if detail_resp.status_code == 200:
seen_ids.add(msg_id)
detail = detail_resp.json()
content = detail.get("text") or detail.get("html") or ""
code = _extract_code(content)
if code:
return code
except Exception as exc:
logger.warning("DuckMail 轮询验证码失败: %s", exc)
time.sleep(3)
return ""
# ==================== Cloudflare Temp Email ====================
class CloudflareTempEmailProvider(MailProvider):
def __init__(self, api_base: str = "", admin_password: str = "", domain: str = ""):
self.api_base = api_base.rstrip("/")
self.admin_password = admin_password
self.domain = str(domain).strip()
# 使用线程本地 token避免多线程下邮箱 token 串用。
self._tls = threading.local()
def _get_random_domain(self) -> str:
if not self.domain:
return ""
# 尝试按照 JSON 数组解析
if self.domain.startswith("[") and self.domain.endswith("]"):
try:
import json
domain_list = json.loads(self.domain)
if isinstance(domain_list, list) and domain_list:
return random.choice([str(d).strip() for d in domain_list if str(d).strip()])
except Exception:
pass
# 按照逗号分隔解析
if "," in self.domain:
parts = [d.strip() for d in self.domain.split(",") if d.strip()]
if parts:
return random.choice(parts)
return self.domain
@staticmethod
def _message_matches_email(msg: Dict[str, Any], target_email: str) -> bool:
target = str(target_email or "").strip().lower()
if not target:
return True
def _extract_text_candidates(value: Any) -> List[str]:
out: List[str] = []
if isinstance(value, str):
out.append(value)
elif isinstance(value, dict):
for k in ("address", "email", "name", "value"):
if value.get(k):
out.extend(_extract_text_candidates(value.get(k)))
elif isinstance(value, list):
for item in value:
out.extend(_extract_text_candidates(item))
return out
candidates: List[str] = []
for key in ("to", "mailTo", "receiver", "receivers", "address", "email", "envelope_to"):
if key in msg:
candidates.extend(_extract_text_candidates(msg.get(key)))
if not candidates:
return True
target_lower = target.lower()
for raw in candidates:
text = str(raw or "").strip().lower()
if not text:
continue
if target_lower in text:
return True
return False
def create_mailbox(
self,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
) -> Tuple[str, str]:
if not self.api_base or not self.admin_password or not self.domain:
return "", ""
with _build_session(proxy, proxy_selector) as session:
try:
# 生成5位字母 + 1-3位数字 + 1-3位字母的随机名
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)))
name = letters1 + numbers + letters2
target_domain = self._get_random_domain()
if not target_domain:
return "", ""
resp = session.post(
f"{self.api_base}/admin/new_address",
json={
"enablePrefix": True,
"name": name,
"domain": target_domain,
},
headers={
"x-admin-auth": self.admin_password,
"Content-Type": "application/json"
},
timeout=30, verify=False,
)
if resp.status_code == 200:
data = resp.json()
email = data.get("address")
jwt_token = data.get("jwt")
if email and jwt_token:
self._tls.jwt_token = jwt_token
return email, jwt_token
except Exception as exc:
logger.warning("Cloudflare 临时邮箱创建失败: %s", exc)
return "", ""
def wait_for_otp(
self,
auth_credential: str,
email: str,
proxy: str = "",
proxy_selector: Optional[Callable[[], str]] = None,
timeout: int = 120,
stop_event: Optional[threading.Event] = None,
) -> str:
token = str(auth_credential or "").strip() or str(getattr(self._tls, "jwt_token", "") or "").strip()
if not token:
return ""
print(f"[CFMail] wait_for_otp 进入! email={email}, api_base={self.api_base}, jwt前16={token[:16] if token else 'EMPTY'}", flush=True)
with _build_session(proxy, proxy_selector) as session:
seen_ids: set = set()
start = time.time()
poll_count = 0
while time.time() - start < timeout:
if stop_event and stop_event.is_set():
print("[CFMail] stop_event 已触发,退出", flush=True)
return ""
try:
poll_count += 1
url = f"{self.api_base}/api/mails?limit=10&offset=0"
resp = session.get(
url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
timeout=30, verify=False,
)
print(f"[CFMail] 轮询#{poll_count} status={resp.status_code}, body前200={str(resp.text or '')[:200]}", flush=True)
if resp.status_code == 200:
try:
data = resp.json()
except Exception as je:
print(f"[CFMail] JSON解析失败: {je}", flush=True)
time.sleep(3)
continue
# API 返回字典 {"results": [...], "count": 0},需正确提取
if isinstance(data, dict):
messages = data.get("results") or []
elif isinstance(data, list):
messages = data
else:
messages = []
print(f"[CFMail] 解析到 {len(messages)} 条邮件", flush=True)
for msg in messages:
if not isinstance(msg, dict):
continue
if not self._message_matches_email(msg, email):
continue
msg_id = msg.get("id")
if not msg_id or msg_id in seen_ids:
continue
seen_ids.add(msg_id)
content = msg.get("text") or msg.get("html") or ""
# Cloudflare Temp Email 将邮件原文放在 raw 字段MIME 格式)
if not content and msg.get("raw"):
try:
import email as _email_mod
from email import policy
parsed = _email_mod.message_from_string(msg["raw"], policy=policy.default)
# 优先取纯文本
body = parsed.get_body(preferencelist=('plain', 'html'))
if body:
content = body.get_content() or ""
if not content:
# 回退:遍历所有 part
for part in parsed.walk():
ctype = part.get_content_type()
if ctype in ("text/plain", "text/html"):
payload = part.get_content()
if payload:
content = str(payload)
break
except Exception as parse_err:
print(f"[CFMail] MIME解析失败回退raw: {parse_err}", flush=True)
content = msg.get("raw", "")
print(f"[CFMail] 邮件id={msg_id}, 内容前200={content[:200]}", flush=True)
code = _extract_code(content)
if code:
print(f"[CFMail] 成功提取验证码: {code}", flush=True)
return code
except Exception as e:
print(f"[CFMail] 轮询异常: {e}", flush=True)
time.sleep(3)
print("[CFMail] wait_for_otp 超时, 未获取到验证码", flush=True)
return ""
# ==================== 多提供商路由 ====================
class MultiMailRouter:
"""线程安全的多邮箱提供商路由器,支持轮询/随机/容错策略"""
def __init__(self, config: Dict[str, Any]):
providers_list: List[str] = config.get("mail_providers") or []
provider_configs: Dict[str, Dict] = config.get("mail_provider_configs") or {}
self.strategy: str = config.get("mail_strategy", "round_robin")
if not providers_list:
legacy = config.get("mail_provider", "mailtm")
providers_list = [legacy]
provider_configs = {legacy: config.get("mail_config") or {}}
self._provider_names: List[str] = []
self._providers: Dict[str, MailProvider] = {}
self._failures: Dict[str, int] = {}
self._lock = threading.RLock()
self._counter = itertools.count()
for name in providers_list:
try:
p = create_provider_by_name(name, provider_configs.get(name, {}))
self._provider_names.append(name)
self._providers[name] = p
self._failures[name] = 0
except Exception as e:
logger.warning("创建邮箱提供商 %s 失败: %s", name, e)
if not self._providers:
if providers_list:
raise RuntimeError(f"邮箱提供商配置无效: {', '.join(str(n) for n in providers_list)}")
fallback = create_provider_by_name("mailtm", {})
self._provider_names = ["mailtm"]
self._providers = {"mailtm": fallback}
self._failures = {"mailtm": 0}
def next_provider(self) -> Tuple[str, MailProvider]:
with self._lock:
names = self._provider_names
if not names:
raise RuntimeError("无可用邮箱提供商")
if self.strategy == "random":
name = random.choice(names)
elif self.strategy == "failover":
name = min(names, key=lambda n: self._failures.get(n, 0))
else:
idx = next(self._counter) % len(names)
name = names[idx]
return name, self._providers[name]
def providers(self) -> List[Tuple[str, MailProvider]]:
with self._lock:
return [(n, self._providers[n]) for n in self._provider_names]
def report_success(self, provider_name: str) -> None:
with self._lock:
self._failures[provider_name] = max(0, self._failures.get(provider_name, 0) - 1)
def report_failure(self, provider_name: str) -> None:
with self._lock:
self._failures[provider_name] = self._failures.get(provider_name, 0) + 1
# ==================== 工厂函数 ====================
def create_provider_by_name(provider_type: str, mail_cfg: Dict[str, Any]) -> MailProvider:
"""根据提供商名称和单独配置创建实例"""
provider_type = provider_type.lower().strip()
api_base = str(mail_cfg.get("api_base", "")).strip()
if provider_type == "moemail":
return MoeMailProvider(
api_base=api_base or "https://your-moemail-api.example.com",
api_key=str(mail_cfg.get("api_key", "")).strip(),
)
elif provider_type == "duckmail":
return DuckMailProvider(
api_base=api_base or "https://api.duckmail.sbs",
bearer_token=str(mail_cfg.get("bearer_token", "")).strip(),
)
elif provider_type == "cloudflare_temp_email":
return CloudflareTempEmailProvider(
api_base=api_base,
admin_password=str(mail_cfg.get("admin_password", "")).strip(),
domain=str(mail_cfg.get("domain", "")).strip(),
)
elif provider_type == "mailtm":
return MailTmProvider(api_base=api_base or "https://api.mail.tm")
raise ValueError(f"未知邮箱提供商: {provider_type}")
def create_provider(config: Dict[str, Any]) -> MailProvider:
"""兼容旧配置格式的工厂函数"""
provider_type = str(config.get("mail_provider", "mailtm")).lower()
mail_cfg = config.get("mail_config") or {}
return create_provider_by_name(provider_type, mail_cfg)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3556
openai_pool_orchestrator/server.py Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,694 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenAI Pool Orchestrator</title>
<meta name="description" content="OpenAI 账号池编排器 — Web 可视化界面" />
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<!-- ========== 头部 ========== -->
<header>
<div class="header-brand">
<div class="logo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a4 4 0 0 0-4 4v2H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h-2V6a4 4 0 0 0-4-4z"/><circle cx="12" cy="15" r="2"/></svg>
</div>
<h1>Pool Orchestrator</h1>
<span class="version">v5.2.1</span>
</div>
<nav class="tab-nav header-tab-nav" role="tablist" aria-label="主导航">
<div class="segmented-control">
<div class="segment-indicator" id="segmentIndicator"></div>
<button class="tab-btn active" data-tab="tabDashboard" role="tab" aria-selected="true" aria-controls="tabDashboard">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
<span>仪表盘</span>
</button>
<button class="tab-btn" data-tab="tabConfig" role="tab" aria-selected="false" aria-controls="tabConfig">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
<span>配置中心</span>
</button>
</div>
</nav>
<div class="header-right">
<div id="headerSub2apiChip" class="pool-chip status-idle interactive-chip" title="切换到 Sub2Api 视图" role="button" tabindex="0" aria-controls="dataPanelSub2Api" aria-pressed="true">
<span class="pool-chip-name">Sub2Api</span>
<span id="headerSub2apiLabel" class="pool-chip-value">-- / --</span>
<span id="headerSub2apiDelta" class="pool-chip-delta">--</span>
<div class="pool-chip-track">
<div id="headerSub2apiBar" class="pool-chip-fill" style="width:0%"></div>
</div>
</div>
<div id="headerCpaChip" class="pool-chip status-idle interactive-chip" title="切换到 CPA 视图" role="button" tabindex="0" aria-controls="dataPanelCpa" aria-pressed="false">
<span class="pool-chip-name">CPA</span>
<span id="headerCpaLabel" class="pool-chip-value">-- / --</span>
<span id="headerCpaDelta" class="pool-chip-delta">--</span>
<div class="pool-chip-track">
<div id="headerCpaBar" class="pool-chip-fill" style="width:0%"></div>
</div>
</div>
<div id="headerLocalTokenChip" class="pool-chip status-idle interactive-chip" title="切换到本地 Token 视图" role="button" tabindex="0" aria-controls="dataPanelLocalTokens" aria-pressed="false">
<span class="pool-chip-name">Local Token</span>
<span id="headerLocalTokenLabel" class="pool-chip-value">-- / --</span>
<span id="headerLocalTokenDelta" class="pool-chip-delta">--</span>
<div class="pool-chip-track">
<div id="headerLocalTokenBar" class="pool-chip-fill" style="width:0%"></div>
</div>
</div>
<div id="statusBadge" class="status-badge idle">
<span id="statusDot" class="status-dot"></span>
<span id="statusText">空闲</span>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" type="button" aria-label="切换到明亮主题" title="切换到明亮主题">
<span class="theme-toggle-icon" aria-hidden="true"></span>
<span class="theme-toggle-label">黑暗</span>
</button>
</div>
</header>
<!-- ========== 进度条 ========== -->
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
<!-- ========== 仪表盘 Tab ========== -->
<div id="tabDashboard" class="tab-panel active" role="tabpanel">
<div class="app-shell">
<!-- ===== 左栏:控制面板 ===== -->
<aside class="sidebar">
<!-- 代理配置 -->
<div class="panel-section">
<div class="section-title">代理配置</div>
<div class="proxy-row">
<div class="input-wrapper">
<input type="text" id="proxyInput" placeholder="http://127.0.0.1:7897" value="http://127.0.0.1:7897"
autocomplete="off" spellcheck="false" />
</div>
<button id="checkProxyBtn" class="btn btn-ghost btn-sm">检测</button>
<button id="saveProxyBtn" class="btn btn-ghost btn-sm">保存</button>
</div>
<div id="proxyStatus" class="proxy-status">
<span>点击「检测」验证代理可用性</span>
</div>
</div>
<!-- 任务控制 -->
<div class="panel-section">
<div class="section-title">任务控制</div>
<div class="toggle-row">
<label class="ios-toggle-label" for="multithreadCheck">
<span class="ios-toggle">
<input type="checkbox" id="multithreadCheck" />
<span class="toggle-track"></span>
</span>
<span>多线程</span>
</label>
<div class="thread-count-wrap">
<label>线程</label>
<input type="number" id="threadCountInput" value="3" min="1" max="10" />
</div>
</div>
<div class="toggle-row">
<label class="ios-toggle-label" for="autoRegisterCheck">
<span class="ios-toggle">
<input type="checkbox" id="autoRegisterCheck" />
<span class="toggle-track"></span>
</span>
<span>池不足自动注册</span>
</label>
</div>
<div class="control-buttons">
<button id="btnStart" class="btn btn-success">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
启动
</button>
<button id="btnStop" class="btn btn-danger" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>
停止
</button>
</div>
</div>
<!-- 注册步骤追踪 -->
<div class="panel-section">
<div class="section-title">注册进度</div>
<div class="progress-section-block">
<div class="progress-subtitle">任务概览</div>
<div id="taskOverview" class="task-overview-grid">
<div class="task-overview-card empty">等待任务启动</div>
</div>
</div>
<div class="progress-section-block">
<div class="progress-subtitle progress-subtitle-row">
<span>Worker 列表</span>
<button id="unlockFocusBtn" class="btn btn-ghost btn-sm" type="button" disabled>跟随后端焦点</button>
</div>
<div id="workerList" class="worker-list">
<div class="worker-card empty">暂无 Worker 运行</div>
</div>
</div>
<div class="progress-section-block">
<div class="progress-subtitle">焦点 Worker 明细</div>
<div id="workerDetail" class="worker-detail-card empty">等待任务启动</div>
</div>
</div>
</aside>
<!-- 拖拽分隔条(左) -->
<div class="resize-handle" id="resizeLeft"></div>
<!-- ===== 中栏:日志面板 ===== -->
<main class="main-area">
<div class="log-panel">
<div class="log-header">
<div class="log-title">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span>实时日志</span>
<span id="logCount" class="log-count-badge">0</span>
</div>
<div class="log-actions">
<label class="ios-toggle-label log-autoscroll-label" for="autoScrollCheck">
<span class="ios-toggle ios-toggle-sm">
<input type="checkbox" id="autoScrollCheck" checked />
<span class="toggle-track"></span>
</span>
<span>自动滚动</span>
</label>
<button id="clearLogBtn" class="btn btn-ghost btn-sm">清空</button>
</div>
</div>
<div id="logBody" class="log-body">
<div class="log-entry log-placeholder">
等待任务启动...
</div>
</div>
</div>
</main>
<!-- 拖拽分隔条(右) -->
<div class="resize-handle" id="resizeRight"></div>
<!-- ===== 右栏:池状态 + Token ===== -->
<aside class="data-panel">
<div class="remote-panel-wrap">
<div class="data-panel-body">
<div id="dataPanelSub2Api" class="data-panel-section active" role="tabpanel" aria-labelledby="headerSub2apiChip">
<div class="pool-section-header">
<div class="pool-section-title">
<span>Sub2Api 账号池</span>
</div>
<div class="pool-section-actions">
<button id="sub2apiPoolRefreshBtn" class="btn btn-ghost btn-sm">刷新</button>
<button id="sub2apiPoolMaintainBtn" class="btn btn-primary btn-sm">维护</button>
<span id="sub2apiPoolMaintainStatus" class="inline-status"></span>
</div>
</div>
<div class="pool-overview">
<div class="pool-stat-card">
<div id="sub2apiPoolTotal" class="stat-value blue">--</div>
<div class="stat-label">总账号</div>
</div>
<div class="pool-stat-card">
<div id="sub2apiPoolNormal" class="stat-value green">--</div>
<div class="stat-label">正常</div>
</div>
<div class="pool-stat-card">
<div id="sub2apiPoolError" class="stat-value red">--</div>
<div class="stat-label">异常</div>
</div>
<div class="pool-stat-card">
<div id="sub2apiPoolThreshold" class="stat-value muted">--</div>
<div class="stat-label">目标</div>
</div>
<div class="pool-stat-card">
<div id="sub2apiPoolPercent" class="stat-value">--</div>
<div class="stat-label">充足率</div>
</div>
</div>
<div class="sub2api-account-wrap">
<div class="token-filter-row">
<select id="sub2apiAccountStatusFilter" class="token-filter-select">
<option value="all">全部状态</option>
<option value="normal">仅正常</option>
<option value="abnormal">仅异常</option>
<option value="error">仅 Error</option>
<option value="disabled">仅 Disabled</option>
<option value="duplicate">仅重复</option>
</select>
<input id="sub2apiAccountKeyword" type="text" placeholder="筛选邮箱 / ID / 名称" autocomplete="off" autocapitalize="off" spellcheck="false" data-lpignore="true" />
<button id="sub2apiAccountApplyBtn" class="btn btn-ghost btn-sm">筛选</button>
<button id="sub2apiAccountResetBtn" class="btn btn-ghost btn-sm">重置</button>
</div>
<div class="token-filter-row account-toolbar">
<label class="account-select-all" for="sub2apiAccountSelectAll">
<input type="checkbox" id="sub2apiAccountSelectAll" />
<span>全选当前页</span>
</label>
<button id="sub2apiAccountProbeBtn" class="btn btn-ghost btn-sm">测活选中</button>
<button id="sub2apiAccountExceptionBtn" class="btn btn-ghost btn-sm">异常处理</button>
<button id="sub2apiDuplicateScanBtn" class="btn btn-ghost btn-sm">重复检测</button>
<button id="sub2apiDuplicateCleanBtn" class="btn btn-primary btn-sm">清理重复</button>
<button id="sub2apiAccountDeleteBtn" class="btn btn-danger btn-sm">批量删除</button>
<span id="sub2apiAccountSelection" class="inline-status"></span>
</div>
<div id="sub2apiAccountList" class="token-list sub2api-account-list">
<div class="empty-state">
<div class="empty-icon"></div>
<span>正在加载 Sub2Api 账号列表...</span>
</div>
</div>
<div class="pool-table-footer">
<div class="account-pager">
<button id="sub2apiAccountPrevBtn" class="btn btn-ghost btn-sm">上一页</button>
<span id="sub2apiAccountPageInfo" class="pager-info">第 1/1 页 · 每页 20 条</span>
<button id="sub2apiAccountNextBtn" class="btn btn-ghost btn-sm">下一页</button>
<select id="sub2apiAccountPageSize" class="token-filter-select pager-size-select">
<option value="20">20/页</option>
<option value="50">50/页</option>
<option value="100">100/页</option>
</select>
</div>
<span id="sub2apiAccountActionStatus" class="inline-status"></span>
</div>
</div>
</div>
<div id="dataPanelCpa" class="data-panel-section" role="tabpanel" aria-labelledby="headerCpaChip">
<div class="pool-section-header">
<div class="pool-section-title">
<span>CPA 账号池</span>
</div>
<div class="pool-section-actions">
<button id="poolRefreshBtn" class="btn btn-ghost btn-sm">刷新</button>
<button id="poolMaintainBtn" class="btn btn-primary btn-sm">维护</button>
<span id="poolMaintainStatus" class="inline-status"></span>
</div>
</div>
<div class="pool-overview">
<div class="pool-stat-card">
<div id="poolTotal" class="stat-value blue">--</div>
<div class="stat-label">总账号</div>
</div>
<div class="pool-stat-card">
<div id="poolCandidates" class="stat-value green">--</div>
<div class="stat-label">正常</div>
</div>
<div class="pool-stat-card">
<div id="poolError" class="stat-value red">--</div>
<div class="stat-label">异常</div>
</div>
<div class="pool-stat-card">
<div id="poolThreshold" class="stat-value muted">--</div>
<div class="stat-label">目标</div>
</div>
<div class="pool-stat-card">
<div id="poolPercent" class="stat-value">--</div>
<div class="stat-label">充足率</div>
</div>
</div>
<div class="platform-note-wrap">
<div class="platform-note-card">
<div class="platform-note-title">CPA 当前交互</div>
<div class="platform-note-text">右侧面板展示池状态与维护动作,详细配置仍在配置中心;当前版本暂不提供 CPA 账号明细列表。</div>
</div>
</div>
</div>
<div id="dataPanelLocalTokens" class="data-panel-section local-token-panel-section" role="tabpanel" aria-labelledby="headerLocalTokenChip">
<div class="pool-section-header">
<div class="pool-section-title">
<span>本地 Token 池</span>
</div>
<div class="pool-section-actions">
<button id="poolCopyRtBtn" class="btn btn-ghost btn-sm">复制 RT</button>
<button id="poolExportBtn" class="btn btn-ghost btn-sm">导出</button>
<button id="poolPwSyncBtn" class="btn btn-primary btn-sm">批量导入</button>
</div>
</div>
<div class="pool-table-wrap local-token-wrap">
<div class="token-filter-row">
<select id="tokenFilterStatus" class="token-filter-select">
<option value="all">全部</option>
<option value="synced">仅已导入</option>
<option value="unsynced">仅未导入</option>
<option value="cpa">仅 CPA</option>
<option value="sub2api">仅 Sub2Api</option>
<option value="both">CPA + Sub2Api</option>
</select>
<input id="tokenFilterKeyword" type="text" placeholder="筛选邮箱/文件名" />
<button id="tokenFilterApplyBtn" class="btn btn-ghost btn-sm">筛选</button>
<button id="tokenFilterResetBtn" class="btn btn-ghost btn-sm">重置</button>
</div>
<div id="poolTokenList" class="token-list">
<div class="empty-state">
<div class="empty-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
</div>
<span>暂无 Token</span>
</div>
</div>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
<!-- ========== 配置中心 Tab ========== -->
<div id="tabConfig" class="tab-panel" role="tabpanel">
<div class="config-page">
<!-- 全局上传策略 -->
<div class="config-card collapsible open" style="grid-column: span 2;">
<div class="collapsible-trigger">
<span>全局上传策略</span>
<span class="collapse-icon"></span>
</div>
<div class="collapsible-body">
<div class="config-field" style="max-width:720px;">
<label>上传策略</label>
<select id="uploadMode">
<option value="snapshot">串行补平台先CPA后Sub2Api</option>
<option value="decoupled">双平台同传(单账号双上传)</option>
</select>
<div class="config-hint">
串行模式更保守;并行模式会让单账号并发上传到两个平台。当前任务运行中切换策略,将在下轮任务生效。
</div>
</div>
<div class="config-actions" style="margin-top:8px;">
<button id="uploadModeSaveBtn" class="btn btn-primary btn-sm">保存策略</button>
<span id="uploadModeStatus" class="inline-status"></span>
</div>
</div>
</div>
<!-- 请求代理池配置 -->
<div class="config-card collapsible open" style="grid-column: span 2;">
<div class="collapsible-trigger">
<span>请求代理池配置</span>
<span class="collapse-icon"></span>
</div>
<div class="collapsible-body">
<div class="config-hint" style="margin-top:0;">
在每次请求前动态取号。默认接口将附带 <code>api_key</code><code>count</code><code>country</code> 参数。
</div>
<div class="toggle-row" style="margin-top:10px;">
<label class="ios-toggle-label" for="proxyPoolEnabled">
<span class="ios-toggle ios-toggle-sm">
<input type="checkbox" id="proxyPoolEnabled" />
<span class="toggle-track"></span>
</span>
<span>启用请求前代理池</span>
</label>
</div>
<div class="config-field">
<label>代理池 API</label>
<input type="text" id="proxyPoolApiUrl" value="https://zenproxy.top/api/fetch" autocomplete="off" spellcheck="false" />
</div>
<div class="config-row">
<div class="config-field" style="flex:1;">
<label>认证方式</label>
<select id="proxyPoolAuthMode">
<option value="query">Query 参数</option>
<option value="header">Header Bearer</option>
</select>
</div>
<div class="config-field" style="flex:1;">
<label>API Key留空则保持原值</label>
<input type="password" id="proxyPoolApiKey" placeholder="19c0ec43-8f76-4c97-81bc-bcda059eeba4" autocomplete="off" />
</div>
</div>
<div class="config-row">
<div class="config-field" style="max-width:180px;">
<label>Count</label>
<input type="number" id="proxyPoolCount" value="1" min="1" max="20" />
</div>
<div class="config-field" style="max-width:180px;">
<label>Country</label>
<input type="text" id="proxyPoolCountry" value="US" maxlength="8" autocomplete="off" spellcheck="false" />
</div>
</div>
<div class="config-actions">
<button id="proxyPoolTestBtn" class="btn btn-ghost btn-sm">测试代理池取号</button>
<button id="proxyPoolSaveBtn" class="btn btn-primary btn-sm">保存代理池配置</button>
<span id="proxyPoolStatus" class="inline-status"></span>
</div>
</div>
</div>
<!-- CPA 平台配置 -->
<div class="config-card collapsible open">
<div class="collapsible-trigger">
<span>CPA 平台配置</span>
<span class="collapse-icon"></span>
</div>
<div class="collapsible-body">
<div class="config-field">
<label>Base URL</label>
<input type="text" id="cpaBaseUrl" placeholder="https://your-cpa-server.com" autocomplete="off"
spellcheck="false" />
</div>
<div class="config-field">
<label>Token</label>
<input type="password" id="cpaToken" placeholder="CPA 登录密码" autocomplete="off" />
</div>
<div class="config-row">
<div class="config-field" style="flex:1;">
<label>目标阈值</label>
<input type="number" id="cpaMinCandidates" value="800" min="1" />
</div>
<div class="config-field" style="flex:1;">
<label>使用率阈值(%)</label>
<input type="number" id="cpaUsedPercent" value="95" min="1" max="100" />
</div>
</div>
<div class="config-row" style="margin-top:10px;">
<div class="config-field" style="flex:1;">
<label class="ios-toggle-label" for="cpaAutoMaintain">
<span class="ios-toggle ios-toggle-sm">
<input type="checkbox" id="cpaAutoMaintain" />
<span class="toggle-track"></span>
</span>
<span>自动维护</span>
</label>
</div>
<div class="config-field" style="flex:1;">
<label>间隔(分钟)</label>
<input type="number" id="cpaInterval" value="30" min="5" />
</div>
</div>
<div class="config-actions">
<button id="cpaTestBtn" class="btn btn-ghost btn-sm">测试连接</button>
<button id="cpaSaveBtn" class="btn btn-primary btn-sm">保存</button>
<span id="cpaStatus" class="inline-status"></span>
</div>
</div>
</div>
<!-- Sub2Api 平台配置 -->
<div class="config-card collapsible open">
<div class="collapsible-trigger">
<span>Sub2Api 平台配置</span>
<span class="collapse-icon"></span>
</div>
<div class="collapsible-body">
<div class="config-field">
<label>平台地址</label>
<input type="text" id="sub2apiBaseUrl" value="" autocomplete="off" spellcheck="false" />
</div>
<div class="config-row">
<div class="config-field" style="flex:1;">
<label>管理员邮箱</label>
<input type="text" id="sub2apiEmail" value="" autocomplete="off" placeholder="admin@example.com" />
</div>
<div class="config-field" style="flex:1;">
<label>密码</label>
<input type="password" id="sub2apiPassword" value="" autocomplete="off" />
</div>
</div>
<div class="toggle-row" style="margin-top:6px;">
<label class="ios-toggle-label" for="autoSyncCheck">
<span class="ios-toggle ios-toggle-sm">
<input type="checkbox" id="autoSyncCheck" checked />
<span class="toggle-track"></span>
</span>
<span>注册后自动导入</span>
</label>
</div>
<div class="config-row" style="margin-top:10px;">
<div class="config-field" style="flex:1;">
<label>目标阈值</label>
<input type="number" id="sub2apiMinCandidates" value="200" min="1" />
</div>
<div class="config-field" style="flex:1;">
<label>维护间隔(分钟)</label>
<input type="number" id="sub2apiInterval" value="30" min="5" />
</div>
</div>
<div class="toggle-row" style="margin-top:6px;">
<label class="ios-toggle-label" for="sub2apiAutoMaintain">
<span class="ios-toggle ios-toggle-sm">
<input type="checkbox" id="sub2apiAutoMaintain" />
<span class="toggle-track"></span>
</span>
<span>自动维护</span>
</label>
</div>
<div class="config-field" style="margin-top:10px;">
<label>维护动作</label>
<div class="config-hint" style="margin-top:0;">
手动维护和自动维护都会按勾选项执行,可分别控制异常账号测活、仍异常删除与重复账号清理。
</div>
<div class="maintain-option-grid">
<label class="maintain-option">
<input type="checkbox" id="sub2apiMaintainRefreshAbnormal" checked />
<span>异常账号测活</span>
</label>
<label class="maintain-option">
<input type="checkbox" id="sub2apiMaintainDeleteAbnormal" checked />
<span>删除仍异常账号</span>
</label>
<label class="maintain-option">
<input type="checkbox" id="sub2apiMaintainDedupe" checked />
<span>重复账号清理</span>
</label>
</div>
</div>
<div class="config-actions">
<button id="sub2apiTestPoolBtn" class="btn btn-ghost btn-sm">测试连接</button>
<button id="saveSyncConfigBtn" class="btn btn-primary btn-sm">保存</button>
<span id="syncStatus" class="inline-status"></span>
</div>
</div>
</div>
<!-- 邮箱提供商 -->
<div class="config-card collapsible open" style="grid-column: span 2;">
<div class="collapsible-trigger">
<span>邮箱提供商</span>
<span class="collapse-icon"></span>
</div>
<div class="collapsible-body">
<div class="mail-providers-group">
<div class="provider-item" data-provider="mailtm">
<label class="provider-toggle">
<input type="checkbox" class="mail-provider-check" value="mailtm" checked />
<span class="provider-check-mark"></span>
Mail.tm (免费)
</label>
<div class="provider-config">
<div class="config-field">
<label>API 地址</label>
<input type="text" data-key="api_base" placeholder="https://api.mail.tm" autocomplete="off"
spellcheck="false" />
</div>
</div>
</div>
<div class="provider-item" data-provider="moemail">
<label class="provider-toggle">
<input type="checkbox" class="mail-provider-check" value="moemail" />
<span class="provider-check-mark"></span>
MoeMail (API Key)
</label>
<div class="provider-config" style="display:none;">
<div class="config-field">
<label>API 地址</label>
<input type="text" data-key="api_base" placeholder="https://your-moemail-api.example.com" autocomplete="off"
spellcheck="false" />
</div>
<div class="config-field">
<label>API Key</label>
<input type="password" data-key="api_key" placeholder="MoeMail API Key" autocomplete="off" />
</div>
</div>
</div>
<div class="provider-item" data-provider="duckmail">
<label class="provider-toggle">
<input type="checkbox" class="mail-provider-check" value="duckmail" />
<span class="provider-check-mark"></span>
DuckMail (Bearer Token)
</label>
<div class="provider-config" style="display:none;">
<div class="config-field">
<label>API 地址</label>
<input type="text" data-key="api_base" placeholder="https://api.duckmail.sbs" autocomplete="off"
spellcheck="false" />
</div>
<div class="config-field">
<label>Bearer Token</label>
<input type="password" data-key="bearer_token" placeholder="DuckMail Bearer Token"
autocomplete="off" />
</div>
</div>
</div>
<div class="provider-item" data-provider="cloudflare_temp_email">
<label class="provider-toggle">
<input type="checkbox" class="mail-provider-check" value="cloudflare_temp_email" />
<span class="provider-check-mark"></span>
Cloudflare Temp Email
</label>
<div class="provider-config" style="display:none;">
<div class="config-field">
<label>API 地址 (Worker URL)</label>
<input type="text" data-key="api_base" placeholder="https://xxx.xxx.workers.dev" autocomplete="off"
spellcheck="false" />
</div>
<div class="config-row">
<div class="config-field" style="flex:1;">
<label>Admin 密码</label>
<input type="password" data-key="admin_password" placeholder="x-admin-auth"
autocomplete="off" />
</div>
<div class="config-field" style="flex:1;">
<label>分配域名</label>
<input type="text" data-key="domain" placeholder="example.com" autocomplete="off"
spellcheck="false" />
</div>
</div>
</div>
</div>
</div>
<div class="config-field" style="margin-top:10px;max-width:240px;">
<label>路由策略</label>
<select id="mailStrategySelect">
<option value="round_robin">轮询</option>
<option value="random">随机</option>
<option value="failover">容错优先</option>
</select>
</div>
<div class="config-actions">
<button id="mailTestBtn" class="btn btn-ghost btn-sm">测试连接</button>
<button id="mailSaveBtn" class="btn btn-primary btn-sm">保存</button>
<span id="mailStatus" class="inline-status"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Toast 容器 -->
<div id="toastContainer" class="toast-container"></div>
<script src="/static/app.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

38
pyproject.toml Executable file
View File

@@ -0,0 +1,38 @@
[project]
name = "openai-pool-orchestrator"
version = "2.0.0"
description = "OpenAI 账号池编排器 — 自动化注册、Token 管理与多平台账号池维护"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
keywords = ["openai", "account-pool", "automation", "token-management"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
]
dependencies = [
"fastapi>=0.110",
"uvicorn[standard]>=0.27",
"curl-cffi>=0.6",
"aiohttp>=3.9",
"requests>=2.31",
]
[project.scripts]
openai-pool = "openai_pool_orchestrator.__main__:main"
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["openai_pool_orchestrator*"]
[tool.setuptools.package-data]
openai_pool_orchestrator = ["static/**/*"]

5
requirements.txt Executable file
View File

@@ -0,0 +1,5 @@
fastapi>=0.110
uvicorn[standard]>=0.27
curl-cffi>=0.6
aiohttp>=3.9
requests>=2.31

31
run.py Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""
快速启动脚本 - OpenAI Pool Orchestrator
用法:
python run.py # 启动 Web 服务
python run.py --cli # CLI 模式(单次注册)
python run.py --cli --proxy http://127.0.0.1:7897
"""
import sys
import os
# 将项目根目录加入 Python 路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def main():
if "--cli" in sys.argv:
# CLI 模式:直接调用注册脚本
sys.argv.remove("--cli")
from openai_pool_orchestrator.register import main as cli_main
cli_main()
else:
# Web 模式:启动 FastAPI 服务
from openai_pool_orchestrator.__main__ import main as web_main
web_main()
if __name__ == "__main__":
main()

1555
uv.lock generated Executable file

File diff suppressed because it is too large Load Diff