mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
fix(tm): 去除冗余的上传参数
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
# DuckMail 邮箱服务设计说明
|
||||
|
||||
**目标:** 为系统新增独立的 `duck_mail` 邮箱服务类型,支持在邮箱服务管理页配置 DuckMail,并在注册页选择该服务进行注册。
|
||||
|
||||
## 背景
|
||||
|
||||
当前项目已有三类邮箱服务:
|
||||
|
||||
- `tempmail`:公共临时邮箱
|
||||
- `custom_domain`:MoeMail 风格 REST API
|
||||
- `temp_mail`:自部署 Cloudflare Worker 临时邮箱
|
||||
|
||||
DuckMail 的接口模型与 `custom_domain` 不兼容。它采用 `/accounts`、`/token`、`/messages` 资源模型,并通过 Bearer Token 或 API Key 访问。因此需要新增独立服务类型,而不是复用现有 `custom_domain` 实现。
|
||||
|
||||
## 设计决策
|
||||
|
||||
### 1. 独立服务类型
|
||||
|
||||
新增 `duck_mail` 枚举值、服务类、前后端配置项与注册页可见性逻辑,避免与现有 MoeMail 接口混淆。
|
||||
|
||||
### 2. 配置模型
|
||||
|
||||
DuckMail 仅支持手动填写默认域名,不预拉取 `/domains`,配置项如下:
|
||||
|
||||
- `base_url`:DuckMail API 地址
|
||||
- `default_domain`:默认域名,创建邮箱时直接拼接
|
||||
- `password_length`:自动创建 DuckMail 账户时使用的随机密码长度
|
||||
- `api_key`:可选,私有域名时通过 `Authorization: Bearer dk_xxx` 调用
|
||||
- `timeout`
|
||||
- `max_retries`
|
||||
- `proxy_url`
|
||||
|
||||
### 3. 创建邮箱流程
|
||||
|
||||
DuckMail 服务在 `create_email()` 中:
|
||||
|
||||
1. 生成合法用户名(至少 3 个字符)
|
||||
2. 用 `default_domain` 拼接邮箱地址
|
||||
3. 生成随机密码
|
||||
4. 调用 `POST /accounts`
|
||||
5. 调用 `POST /token` 获取 Bearer Token
|
||||
6. 返回 `email`、`service_id`、`account_id`、`token`、内部随机密码等信息
|
||||
|
||||
### 4. 取验证码流程
|
||||
|
||||
DuckMail 的列表接口不返回正文,需:
|
||||
|
||||
1. 轮询 `GET /messages`
|
||||
2. 筛选 OpenAI 邮件
|
||||
3. 对新消息调用 `GET /messages/{id}`
|
||||
4. 从 `text/html` 中提取验证码
|
||||
|
||||
### 5. 删除与健康检查
|
||||
|
||||
- `delete_email()`:使用账户 Bearer Token 调 `DELETE /accounts/{id}`
|
||||
- `check_health()`:优先调 `GET /domains`;如果配置了私有域名 API Key,也沿用相同入口
|
||||
|
||||
### 6. 前端接入
|
||||
|
||||
邮箱服务管理页新增 DuckMail 子类型:
|
||||
|
||||
- 列表中单独展示为 DuckMail
|
||||
- 新建/编辑表单展示 DuckMail 专属字段
|
||||
|
||||
注册页:
|
||||
|
||||
- 从 `/registration/available-services` 中显示 DuckMail 服务
|
||||
- 选择后与其他数据库邮箱服务一样传递 `email_service_type=duck_mail` 与 `email_service_id`
|
||||
|
||||
## 测试范围
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- DuckMail `create_email()` 会调用账户创建与 token 获取
|
||||
- DuckMail `get_verification_code()` 会按“列表 -> 详情”提取验证码
|
||||
- 注册接口的可用服务列表包含 `duck_mail`
|
||||
- 邮箱服务类型接口与敏感字段过滤支持 `duck_mail`
|
||||
@@ -1,151 +0,0 @@
|
||||
# DuckMail 邮箱服务实现计划
|
||||
|
||||
> **给 Codex:** 必须按 TDD 执行本计划,先写失败测试,再写最小实现。
|
||||
|
||||
**目标:** 新增独立 `duck_mail` 邮箱服务类型,支持 DuckMail 配置、测试、注册可见性与实际验证码拉取。
|
||||
|
||||
**架构:** 在现有 `BaseEmailService` 体系中新增 `DuckMailService`,并把它接入服务工厂、邮箱服务管理 API、注册可用服务 API 与前端页面。DuckMail 不复用 `custom_domain` 逻辑,单独维护请求头、创建邮箱、登录取 token、拉取消息与删除账户逻辑。
|
||||
|
||||
**技术栈:** Python、FastAPI、SQLAlchemy、原生 JavaScript、pytest
|
||||
|
||||
---
|
||||
|
||||
### 任务 1:补 DuckMail 服务层测试
|
||||
|
||||
**文件:**
|
||||
- 新建:`tests/test_duck_mail_service.py`
|
||||
- 参考:`src/services/temp_mail.py`
|
||||
- 参考:`src/services/moe_mail.py`
|
||||
|
||||
**步骤 1:写失败测试**
|
||||
|
||||
- 测试 `create_email()`:
|
||||
- 会先调用 `POST /accounts`
|
||||
- 再调用 `POST /token`
|
||||
- 返回值包含 `email`、`service_id`、`account_id`、`token`
|
||||
- 测试 `get_verification_code()`:
|
||||
- 先调 `GET /messages`
|
||||
- 再调 `GET /messages/{id}`
|
||||
- 能从正文提取 6 位验证码
|
||||
|
||||
**步骤 2:运行失败测试**
|
||||
|
||||
运行:`pytest tests/test_duck_mail_service.py -q`
|
||||
|
||||
预期:因为 `DuckMailService` 尚不存在而失败。
|
||||
|
||||
**步骤 3:写最小实现**
|
||||
|
||||
- 新增 `src/services/duck_mail.py`
|
||||
- 只实现本轮测试所需的最小方法:
|
||||
- 初始化配置
|
||||
- 请求封装
|
||||
- 创建邮箱
|
||||
- 获取 token
|
||||
- 拉取消息与验证码
|
||||
- 删除账户
|
||||
- 健康检查
|
||||
|
||||
**步骤 4:运行测试确认通过**
|
||||
|
||||
运行:`pytest tests/test_duck_mail_service.py -q`
|
||||
|
||||
### 任务 2:补服务枚举与工厂接入测试
|
||||
|
||||
**文件:**
|
||||
- 新建:`tests/test_email_service_duckmail_routes.py`
|
||||
- 修改:`src/config/constants.py`
|
||||
- 修改:`src/services/__init__.py`
|
||||
|
||||
**步骤 1:写失败测试**
|
||||
|
||||
- 断言 `EmailServiceType("duck_mail")` 可用
|
||||
- 断言 `EmailServiceFactory.get_service_class(EmailServiceType.DUCK_MAIL)` 已注册
|
||||
|
||||
**步骤 2:运行失败测试**
|
||||
|
||||
运行:`pytest tests/test_email_service_duckmail_routes.py::test_duck_mail_service_registered -q`
|
||||
|
||||
**步骤 3:写最小实现**
|
||||
|
||||
- 在枚举中加入 `DUCK_MAIL`
|
||||
- 在服务工厂注册 `DuckMailService`
|
||||
|
||||
**步骤 4:运行测试确认通过**
|
||||
|
||||
运行:`pytest tests/test_email_service_duckmail_routes.py::test_duck_mail_service_registered -q`
|
||||
|
||||
### 任务 3:补邮箱服务 API 与注册可见性测试
|
||||
|
||||
**文件:**
|
||||
- 修改:`src/web/routes/email.py`
|
||||
- 修改:`src/web/routes/registration.py`
|
||||
- 参考:`src/database/models.py`
|
||||
|
||||
**步骤 1:写失败测试**
|
||||
|
||||
- 测试邮箱服务类型接口包含 `duck_mail`
|
||||
- 测试敏感配置过滤支持 `api_key`
|
||||
- 测试 `/registration/available-services` 会返回 `duck_mail`
|
||||
|
||||
**步骤 2:运行失败测试**
|
||||
|
||||
运行:`pytest tests/test_email_service_duckmail_routes.py -q`
|
||||
|
||||
**步骤 3:写最小实现**
|
||||
|
||||
- `email.py`
|
||||
- 统计增加 `duck_mail_count`
|
||||
- `get_service_types()` 增加 DuckMail 配置项
|
||||
- `registration.py`
|
||||
- `available-services` 增加 `duck_mail`
|
||||
- `_normalize_email_service_config()` 支持 DuckMail 字段
|
||||
- `_run_sync_registration_task()` 支持 DuckMail 默认选择逻辑
|
||||
|
||||
**步骤 4:运行测试确认通过**
|
||||
|
||||
运行:`pytest tests/test_email_service_duckmail_routes.py -q`
|
||||
|
||||
### 任务 4:补前端 DuckMail 配置与注册入口
|
||||
|
||||
**文件:**
|
||||
- 修改:`templates/email_services.html`
|
||||
- 修改:`static/js/email_services.js`
|
||||
- 修改:`static/js/app.js`
|
||||
- 可选修改:`templates/index.html`
|
||||
|
||||
**步骤 1:先补最小前端逻辑**
|
||||
|
||||
- 邮箱服务页新增 DuckMail 类型展示与新建/编辑字段
|
||||
- 注册页邮箱服务下拉新增 DuckMail 分组
|
||||
- 选择 DuckMail 时传 `duck_mail:<id>`
|
||||
|
||||
**步骤 2:人工自检**
|
||||
|
||||
- 检查新增字段是否与后端字段名一致:
|
||||
- `base_url`
|
||||
- `api_key`
|
||||
- `default_domain`
|
||||
- `password_length`
|
||||
|
||||
### 任务 5:完整验证
|
||||
|
||||
**文件:**
|
||||
- 修改:`README.md`
|
||||
|
||||
**步骤 1:补文档**
|
||||
|
||||
- 在功能列表与邮箱服务说明中加入 DuckMail
|
||||
|
||||
**步骤 2:运行完整验证**
|
||||
|
||||
运行:`pytest tests/test_duck_mail_service.py tests/test_email_service_duckmail_routes.py -q`
|
||||
|
||||
如环境允许,再运行:
|
||||
|
||||
`python -m compileall src`
|
||||
|
||||
**步骤 3:检查结果**
|
||||
|
||||
- 确认 pytest 退出码为 0
|
||||
- 确认编译检查无语法错误
|
||||
@@ -53,8 +53,7 @@ def upload_to_team_manager(
|
||||
headers=headers,
|
||||
json=payload,
|
||||
proxies=None,
|
||||
timeout=30,
|
||||
impersonate="chrome110",
|
||||
timeout=30
|
||||
)
|
||||
if resp.status_code in (200, 201):
|
||||
return True, "上传成功"
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
"""
|
||||
账号管理 API 路由
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks, Body
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...database import crud
|
||||
from ...database.session import get_db
|
||||
from ...database.models import Account
|
||||
from ...config.constants import AccountStatus
|
||||
from ...config.settings import get_settings
|
||||
from ...core.openai.token_refresh import refresh_account_token as do_refresh
|
||||
from ...core.openai.token_refresh import validate_account_token as do_validate
|
||||
from ...core.upload.cpa_upload import generate_token_json, batch_upload_to_cpa, upload_to_cpa
|
||||
from ...core.upload.team_manager_upload import upload_to_team_manager, batch_upload_to_team_manager
|
||||
from ...core.upload.sub2api_upload import batch_upload_to_sub2api, upload_to_sub2api
|
||||
|
||||
from ...database import crud
|
||||
from ...database.models import Account
|
||||
from ...database.session import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -477,10 +484,6 @@ async def export_accounts_sub2api(request: BatchExportRequest):
|
||||
@router.post("/export/cpa")
|
||||
async def export_accounts_cpa(request: BatchExportRequest):
|
||||
"""导出账号为 CPA Token JSON 格式(每个账号单独一个 JSON 文件,打包为 ZIP)"""
|
||||
import io
|
||||
import zipfile
|
||||
from ...core.upload.cpa_upload import generate_token_json
|
||||
|
||||
with get_db() as db:
|
||||
ids = resolve_account_ids(
|
||||
db, request.ids, request.select_all,
|
||||
@@ -582,8 +585,6 @@ class BatchValidateRequest(BaseModel):
|
||||
@router.post("/batch-refresh")
|
||||
async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: BackgroundTasks):
|
||||
"""批量刷新账号 Token"""
|
||||
from ...core.openai.token_refresh import refresh_account_token as do_refresh
|
||||
|
||||
# 使用传入的代理或全局代理配置
|
||||
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
||||
|
||||
@@ -617,7 +618,6 @@ async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: B
|
||||
@router.post("/{account_id}/refresh")
|
||||
async def refresh_account_token(account_id: int, request: Optional[TokenRefreshRequest] = Body(default=None)):
|
||||
"""刷新单个账号的 Token"""
|
||||
from ...core.openai.token_refresh import refresh_account_token as do_refresh
|
||||
|
||||
# 使用传入的代理或全局代理配置
|
||||
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
|
||||
@@ -639,7 +639,6 @@ async def refresh_account_token(account_id: int, request: Optional[TokenRefreshR
|
||||
@router.post("/batch-validate")
|
||||
async def batch_validate_tokens(request: BatchValidateRequest):
|
||||
"""批量验证账号 Token 有效性"""
|
||||
from ...core.openai.token_refresh import validate_account_token as do_validate
|
||||
|
||||
# 使用传入的代理或全局代理配置
|
||||
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
||||
@@ -682,7 +681,6 @@ async def batch_validate_tokens(request: BatchValidateRequest):
|
||||
@router.post("/{account_id}/validate")
|
||||
async def validate_account_token(account_id: int, request: Optional[TokenValidateRequest] = Body(default=None)):
|
||||
"""验证单个账号的 Token 有效性"""
|
||||
from ...core.openai.token_refresh import validate_account_token as do_validate
|
||||
|
||||
# 使用传入的代理或全局代理配置
|
||||
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
|
||||
@@ -717,7 +715,6 @@ class BatchCPAUploadRequest(BaseModel):
|
||||
@router.post("/batch-upload-cpa")
|
||||
async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
"""批量上传账号到 CPA"""
|
||||
from ...core.upload.cpa_upload import batch_upload_to_cpa
|
||||
|
||||
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
||||
|
||||
@@ -745,7 +742,6 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
@router.post("/{account_id}/upload-cpa")
|
||||
async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequest] = Body(default=None)):
|
||||
"""上传单个账号到 CPA"""
|
||||
from ...core.upload.cpa_upload import upload_to_cpa, generate_token_json
|
||||
|
||||
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
|
||||
cpa_service_id = request.cpa_service_id if request else None
|
||||
@@ -809,7 +805,6 @@ class BatchSub2ApiUploadRequest(BaseModel):
|
||||
@router.post("/batch-upload-sub2api")
|
||||
async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest):
|
||||
"""批量上传账号到 Sub2API"""
|
||||
from ...core.upload.sub2api_upload import batch_upload_to_sub2api
|
||||
|
||||
# 解析指定的 Sub2API 服务
|
||||
api_url = None
|
||||
@@ -848,7 +843,6 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest):
|
||||
@router.post("/{account_id}/upload-sub2api")
|
||||
async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUploadRequest] = Body(default=None)):
|
||||
"""上传单个账号到 Sub2API"""
|
||||
from ...core.upload.sub2api_upload import upload_to_sub2api
|
||||
|
||||
service_id = request.service_id if request else None
|
||||
concurrency = request.concurrency if request else 3
|
||||
@@ -908,7 +902,6 @@ class BatchUploadTMRequest(BaseModel):
|
||||
@router.post("/batch-upload-tm")
|
||||
async def batch_upload_accounts_to_tm(request: BatchUploadTMRequest):
|
||||
"""批量上传账号到 Team Manager"""
|
||||
from ...core.upload.team_manager_upload import batch_upload_to_team_manager
|
||||
|
||||
with get_db() as db:
|
||||
if request.service_id:
|
||||
@@ -935,7 +928,6 @@ async def batch_upload_accounts_to_tm(request: BatchUploadTMRequest):
|
||||
@router.post("/{account_id}/upload-tm")
|
||||
async def upload_account_to_tm(account_id: int, request: Optional[UploadTMRequest] = Body(default=None)):
|
||||
"""上传单账号到 Team Manager"""
|
||||
from ...core.upload.team_manager_upload import upload_to_team_manager
|
||||
|
||||
service_id = request.service_id if request else None
|
||||
|
||||
|
||||
@@ -444,7 +444,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
# 自动上传到 Team Manager(可多服务)
|
||||
if auto_upload_tm:
|
||||
try:
|
||||
from ...core.upload.team_manager_upload import upload_account_to_tm
|
||||
from ...core.upload.team_manager_upload import upload_to_team_manager
|
||||
from ...database.models import Account as AccountModel
|
||||
saved_account = db.query(AccountModel).filter_by(email=result.email).first()
|
||||
if saved_account and saved_account.access_token:
|
||||
@@ -459,7 +459,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
if not _svc:
|
||||
continue
|
||||
log_callback(f"[TM] 上传到服务: {_svc.name}")
|
||||
_ok, _msg = upload_account_to_tm(saved_account, _svc.api_url, _svc.api_key)
|
||||
_ok, _msg = upload_to_team_manager(saved_account, _svc.api_url, _svc.api_key)
|
||||
log_callback(f"[TM] {'成功' if _ok else '失败'}({_svc.name}): {_msg}")
|
||||
except Exception as _e:
|
||||
log_callback(f"[TM] 异常({_sid}): {_e}")
|
||||
|
||||
Reference in New Issue
Block a user