fix(tm): 去除冗余的上传参数

This commit is contained in:
cnlimiter
2026-03-19 19:56:08 +08:00
parent 9d369bca63
commit 7f8e85b0aa
5 changed files with 15 additions and 252 deletions

View File

@@ -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`

View File

@@ -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
- 确认编译检查无语法错误

View File

@@ -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, "上传成功"

View File

@@ -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

View File

@@ -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}")