This commit is contained in:
cnlimiter
2026-03-14 20:36:03 +08:00
parent 0688f4ca7e
commit 6891b9f11d
22 changed files with 3882 additions and 299 deletions

View File

@@ -40,11 +40,13 @@ class RegistrationResult:
"""注册结果"""
success: bool
email: str = ""
password: str = "" # 注册密码
account_id: str = ""
workspace_id: str = ""
access_token: str = ""
refresh_token: str = ""
id_token: str = ""
session_token: str = "" # 会话令牌
error_message: str = ""
logs: list = None
metadata: dict = None
@@ -54,11 +56,13 @@ class RegistrationResult:
return {
"success": self.success,
"email": self.email,
"password": self.password,
"account_id": self.account_id,
"workspace_id": self.workspace_id,
"access_token": self.access_token[:20] + "..." if self.access_token else "",
"refresh_token": self.refresh_token[:20] + "..." if self.refresh_token else "",
"id_token": self.id_token[:20] + "..." if self.id_token else "",
"session_token": self.session_token[:20] + "..." if self.session_token else "",
"error_message": self.error_message,
"logs": self.logs or [],
"metadata": self.metadata or {},
@@ -107,9 +111,11 @@ class RegistrationEngine:
# 状态变量
self.email: Optional[str] = None
self.password: Optional[str] = None # 注册密码
self.email_info: Optional[Dict[str, Any]] = None
self.oauth_start: Optional[OAuthStart] = None
self.session: Optional[cffi_requests.Session] = None
self.session_token: Optional[str] = None # 会话令牌
self.logs: list = []
def _log(self, message: str, level: str = "info"):
@@ -268,6 +274,7 @@ class RegistrationEngine:
try:
# 生成密码
password = self._generate_password()
self.password = password # 保存密码到实例变量
self._log(f"生成密码: {password}")
# 提交密码注册
@@ -662,6 +669,14 @@ class RegistrationEngine:
result.access_token = token_info.get("access_token", "")
result.refresh_token = token_info.get("refresh_token", "")
result.id_token = token_info.get("id_token", "")
result.password = self.password or "" # 保存密码
# 尝试获取 session_token 从 cookie
session_cookie = self.session.cookies.get("__Secure-next-auth.session-token")
if session_cookie:
self.session_token = session_cookie
result.session_token = session_cookie
self._log(f"获取到 Session Token")
# 17. 完成
self._log("=" * 60)
@@ -699,11 +714,17 @@ class RegistrationEngine:
return False
try:
# 获取默认 client_id
settings = get_settings()
with get_db() as db:
# 保存账户信息
account = crud.create_account(
db,
email=result.email,
password=result.password,
client_id=settings.openai_client_id,
session_token=result.session_token,
email_service=self.email_service.service_type.value,
email_service_id=self.email_info.get("service_id") if self.email_info else None,
account_id=result.account_id,
@@ -712,7 +733,7 @@ class RegistrationEngine:
refresh_token=result.refresh_token,
id_token=result.id_token,
proxy_used=self.proxy_url,
metadata=result.metadata
extra_data=result.metadata
)
self._log(f"账户已保存到数据库ID: {account.id}")

332
src/core/token_refresh.py Normal file
View File

@@ -0,0 +1,332 @@
"""
Token 刷新模块
支持 Session Token 和 OAuth Refresh Token 两种刷新方式
"""
import logging
import json
import time
from typing import Optional, Dict, Any, Tuple
from dataclasses import dataclass
from datetime import datetime, timedelta
from curl_cffi import requests as cffi_requests
from ..config.settings import get_settings
from ..database.session import get_db
from ..database import crud
from ..database.models import Account
logger = logging.getLogger(__name__)
@dataclass
class TokenRefreshResult:
"""Token 刷新结果"""
success: bool
access_token: str = ""
refresh_token: str = ""
expires_at: Optional[datetime] = None
error_message: str = ""
class TokenRefreshManager:
"""
Token 刷新管理器
支持两种刷新方式:
1. Session Token 刷新(优先)
2. OAuth Refresh Token 刷新
"""
# OpenAI OAuth 端点
SESSION_URL = "https://chatgpt.com/api/auth/session"
TOKEN_URL = "https://auth.openai.com/oauth/token"
def __init__(self, proxy_url: Optional[str] = None):
"""
初始化 Token 刷新管理器
Args:
proxy_url: 代理 URL
"""
self.proxy_url = proxy_url
self.settings = get_settings()
def _create_session(self) -> cffi_requests.Session:
"""创建 HTTP 会话"""
session = cffi_requests.Session(impersonate="chrome120", proxy=self.proxy_url)
return session
def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult:
"""
使用 Session Token 刷新
Args:
session_token: 会话令牌
Returns:
TokenRefreshResult: 刷新结果
"""
result = TokenRefreshResult(success=False)
try:
session = self._create_session()
# 设置会话 Cookie
session.cookies.set(
"__Secure-next-auth.session-token",
session_token,
domain=".chatgpt.com",
path="/"
)
# 请求会话端点
response = session.get(
self.SESSION_URL,
headers={
"accept": "application/json",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
timeout=30
)
if response.status_code != 200:
result.error_message = f"Session token 刷新失败: HTTP {response.status_code}"
logger.warning(result.error_message)
return result
data = response.json()
# 提取 access_token
access_token = data.get("accessToken")
if not access_token:
result.error_message = "Session token 刷新失败: 未找到 accessToken"
logger.warning(result.error_message)
return result
# 提取过期时间
expires_at = None
expires_str = data.get("expires")
if expires_str:
try:
expires_at = datetime.fromisoformat(expires_str.replace("Z", "+00:00"))
except:
pass
result.success = True
result.access_token = access_token
result.expires_at = expires_at
logger.info(f"Session token 刷新成功,过期时间: {expires_at}")
return result
except Exception as e:
result.error_message = f"Session token 刷新异常: {str(e)}"
logger.error(result.error_message)
return result
def refresh_by_oauth_token(
self,
refresh_token: str,
client_id: Optional[str] = None
) -> TokenRefreshResult:
"""
使用 OAuth Refresh Token 刷新
Args:
refresh_token: OAuth 刷新令牌
client_id: OAuth Client ID
Returns:
TokenRefreshResult: 刷新结果
"""
result = TokenRefreshResult(success=False)
try:
session = self._create_session()
# 使用配置的 client_id 或默认值
client_id = client_id or self.settings.openai_client_id
# 构建请求体
token_data = {
"client_id": client_id,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"redirect_uri": self.settings.openai_redirect_uri
}
response = session.post(
self.TOKEN_URL,
headers={
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json"
},
data=token_data,
timeout=30
)
if response.status_code != 200:
result.error_message = f"OAuth token 刷新失败: HTTP {response.status_code}"
logger.warning(f"{result.error_message}, 响应: {response.text[:200]}")
return result
data = response.json()
# 提取令牌
access_token = data.get("access_token")
new_refresh_token = data.get("refresh_token", refresh_token)
expires_in = data.get("expires_in", 3600)
if not access_token:
result.error_message = "OAuth token 刷新失败: 未找到 access_token"
logger.warning(result.error_message)
return result
# 计算过期时间
expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
result.success = True
result.access_token = access_token
result.refresh_token = new_refresh_token
result.expires_at = expires_at
logger.info(f"OAuth token 刷新成功,过期时间: {expires_at}")
return result
except Exception as e:
result.error_message = f"OAuth token 刷新异常: {str(e)}"
logger.error(result.error_message)
return result
def refresh_account(self, account: Account) -> TokenRefreshResult:
"""
刷新账号的 Token
优先级:
1. Session Token 刷新
2. OAuth Refresh Token 刷新
Args:
account: 账号对象
Returns:
TokenRefreshResult: 刷新结果
"""
# 优先尝试 Session Token
if account.session_token:
logger.info(f"尝试使用 Session Token 刷新账号 {account.email}")
result = self.refresh_by_session_token(account.session_token)
if result.success:
return result
logger.warning(f"Session Token 刷新失败,尝试 OAuth 刷新")
# 尝试 OAuth Refresh Token
if account.refresh_token:
logger.info(f"尝试使用 OAuth Refresh Token 刷新账号 {account.email}")
result = self.refresh_by_oauth_token(
refresh_token=account.refresh_token,
client_id=account.client_id
)
return result
# 无可用刷新方式
return TokenRefreshResult(
success=False,
error_message="账号没有可用的刷新方式(缺少 session_token 和 refresh_token"
)
def validate_token(self, access_token: str) -> Tuple[bool, Optional[str]]:
"""
验证 Access Token 是否有效
Args:
access_token: 访问令牌
Returns:
Tuple[bool, Optional[str]]: (是否有效, 错误信息)
"""
try:
session = self._create_session()
# 调用 OpenAI API 验证 token
response = session.get(
"https://chatgpt.com/backend-api/me",
headers={
"authorization": f"Bearer {access_token}",
"accept": "application/json"
},
timeout=30
)
if response.status_code == 200:
return True, None
elif response.status_code == 401:
return False, "Token 无效或已过期"
elif response.status_code == 403:
return False, "账号可能被封禁"
else:
return False, f"验证失败: HTTP {response.status_code}"
except Exception as e:
return False, f"验证异常: {str(e)}"
def refresh_account_token(account_id: int, proxy_url: Optional[str] = None) -> TokenRefreshResult:
"""
刷新指定账号的 Token 并更新数据库
Args:
account_id: 账号 ID
proxy_url: 代理 URL
Returns:
TokenRefreshResult: 刷新结果
"""
with get_db() as db:
account = crud.get_account_by_id(db, account_id)
if not account:
return TokenRefreshResult(success=False, error_message="账号不存在")
manager = TokenRefreshManager(proxy_url=proxy_url)
result = manager.refresh_account(account)
if result.success:
# 更新数据库
update_data = {
"access_token": result.access_token,
"last_refresh": datetime.utcnow()
}
if result.refresh_token:
update_data["refresh_token"] = result.refresh_token
if result.expires_at:
update_data["expires_at"] = result.expires_at
crud.update_account(db, account_id, **update_data)
return result
def validate_account_token(account_id: int, proxy_url: Optional[str] = None) -> Tuple[bool, Optional[str]]:
"""
验证指定账号的 Token 是否有效
Args:
account_id: 账号 ID
proxy_url: 代理 URL
Returns:
Tuple[bool, Optional[str]]: (是否有效, 错误信息)
"""
with get_db() as db:
account = crud.get_account_by_id(db, account_id)
if not account:
return False, "账号不存在"
if not account.access_token:
return False, "账号没有 access_token"
manager = TokenRefreshManager(proxy_url=proxy_url)
return manager.validate_token(account.access_token)

View File

@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, desc, asc, func
from .models import Account, EmailService, RegistrationTask, Setting
from .models import Account, EmailService, RegistrationTask, Setting, Proxy
# ============================================================================
@@ -18,6 +18,9 @@ def create_account(
db: Session,
email: str,
email_service: str,
password: Optional[str] = None,
client_id: Optional[str] = None,
session_token: Optional[str] = None,
email_service_id: Optional[str] = None,
account_id: Optional[str] = None,
workspace_id: Optional[str] = None,
@@ -25,11 +28,15 @@ def create_account(
refresh_token: Optional[str] = None,
id_token: Optional[str] = None,
proxy_used: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
expires_at: Optional['datetime'] = None,
extra_data: Optional[Dict[str, Any]] = None
) -> Account:
"""创建新账户"""
db_account = Account(
email=email,
password=password,
client_id=client_id,
session_token=session_token,
email_service=email_service,
email_service_id=email_service_id,
account_id=account_id,
@@ -38,7 +45,8 @@ def create_account(
refresh_token=refresh_token,
id_token=id_token,
proxy_used=proxy_used,
metadata=metadata or {},
expires_at=expires_at,
extra_data=extra_data or {},
registered_at=datetime.utcnow()
)
db.add(db_account)
@@ -369,4 +377,120 @@ def delete_setting(db: Session, key: str) -> bool:
db.delete(db_setting)
db.commit()
return True
return True
# ============================================================================
# 代理 CRUD
# ============================================================================
def create_proxy(
db: Session,
name: str,
type: str,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
enabled: bool = True,
priority: int = 0
) -> Proxy:
"""创建代理配置"""
db_proxy = Proxy(
name=name,
type=type,
host=host,
port=port,
username=username,
password=password,
enabled=enabled,
priority=priority
)
db.add(db_proxy)
db.commit()
db.refresh(db_proxy)
return db_proxy
def get_proxy_by_id(db: Session, proxy_id: int) -> Optional[Proxy]:
"""根据 ID 获取代理"""
return db.query(Proxy).filter(Proxy.id == proxy_id).first()
def get_proxies(
db: Session,
enabled: Optional[bool] = None,
skip: int = 0,
limit: int = 100
) -> List[Proxy]:
"""获取代理列表"""
query = db.query(Proxy)
if enabled is not None:
query = query.filter(Proxy.enabled == enabled)
query = query.order_by(desc(Proxy.created_at)).offset(skip).limit(limit)
return query.all()
def get_enabled_proxies(db: Session) -> List[Proxy]:
"""获取所有启用的代理"""
return db.query(Proxy).filter(Proxy.enabled == True).all()
def update_proxy(
db: Session,
proxy_id: int,
**kwargs
) -> Optional[Proxy]:
"""更新代理配置"""
db_proxy = get_proxy_by_id(db, proxy_id)
if not db_proxy:
return None
for key, value in kwargs.items():
if hasattr(db_proxy, key):
setattr(db_proxy, key, value)
db.commit()
db.refresh(db_proxy)
return db_proxy
def delete_proxy(db: Session, proxy_id: int) -> bool:
"""删除代理配置"""
db_proxy = get_proxy_by_id(db, proxy_id)
if not db_proxy:
return False
db.delete(db_proxy)
db.commit()
return True
def update_proxy_last_used(db: Session, proxy_id: int) -> bool:
"""更新代理最后使用时间"""
db_proxy = get_proxy_by_id(db, proxy_id)
if not db_proxy:
return False
db_proxy.last_used = datetime.utcnow()
db.commit()
return True
def get_random_proxy(db: Session) -> Optional[Proxy]:
"""随机获取一个启用的代理"""
import random
proxies = get_enabled_proxies(db)
if not proxies:
return None
return random.choice(proxies)
def get_proxies_count(db: Session, enabled: Optional[bool] = None) -> int:
"""获取代理数量"""
query = db.query(func.count(Proxy.id))
if enabled is not None:
query = query.filter(Proxy.enabled == enabled)
return query.scalar()

View File

@@ -34,18 +34,20 @@ class Account(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String(255), nullable=False, unique=True, index=True)
password_hash = Column(String(255))
password = Column(String(255)) # 注册密码(明文存储)
access_token = Column(Text)
refresh_token = Column(Text)
id_token = Column(Text)
session_token = Column(Text) # 会话令牌(优先刷新方式)
client_id = Column(String(255)) # OAuth Client ID
account_id = Column(String(255))
workspace_id = Column(String(255))
email_service = Column(String(50), nullable=False) # 'tempmail', 'outlook', 'custom_domain'
email_service_id = Column(String(255)) # 邮箱服务中的ID
proxy_used = Column(String(255))
registered_at = Column(DateTime, default=datetime.utcnow)
last_refresh = Column(DateTime)
expires_at = Column(DateTime)
last_refresh = Column(DateTime) # 最后刷新时间
expires_at = Column(DateTime) # Token 过期时间
status = Column(String(20), default='active') # 'active', 'expired', 'banned', 'failed'
extra_data = Column(JSONEncodedDict) # 额外信息存储
created_at = Column(DateTime, default=datetime.utcnow)
@@ -56,10 +58,14 @@ class Account(Base):
return {
'id': self.id,
'email': self.email,
'password': self.password,
'client_id': self.client_id,
'email_service': self.email_service,
'account_id': self.account_id,
'workspace_id': self.workspace_id,
'registered_at': self.registered_at.isoformat() if self.registered_at else None,
'last_refresh': self.last_refresh.isoformat() if self.last_refresh else None,
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
'status': self.status,
'proxy_used': self.proxy_used,
'created_at': self.created_at.isoformat() if self.created_at else None,
@@ -110,4 +116,59 @@ class Setting(Base):
value = Column(Text)
description = Column(Text)
category = Column(String(50), default='general') # 'general', 'email', 'proxy', 'openai'
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Proxy(Base):
"""代理列表表"""
__tablename__ = 'proxies'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False) # 代理名称
type = Column(String(20), nullable=False, default='http') # http, socks5
host = Column(String(255), nullable=False)
port = Column(Integer, nullable=False)
username = Column(String(100))
password = Column(String(255))
enabled = Column(Boolean, default=True)
priority = Column(Integer, default=0) # 优先级(保留字段)
last_used = Column(DateTime) # 最后使用时间
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self, include_password: bool = False) -> Dict[str, Any]:
"""转换为字典"""
result = {
'id': self.id,
'name': self.name,
'type': self.type,
'host': self.host,
'port': self.port,
'username': self.username,
'enabled': self.enabled,
'priority': self.priority,
'last_used': self.last_used.isoformat() if self.last_used else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
if include_password:
result['password'] = self.password
else:
result['has_password'] = bool(self.password)
return result
@property
def proxy_url(self) -> str:
"""获取完整的代理 URL"""
if self.type == "http":
scheme = "http"
elif self.type == "socks5":
scheme = "socks5"
else:
scheme = self.type
auth = ""
if self.username and self.password:
auth = f"{self.username}:{self.password}@"
return f"{scheme}://{auth}{self.host}:{self.port}"

View File

@@ -78,6 +78,11 @@ def create_app() -> FastAPI:
"""账号管理页面"""
return templates.TemplateResponse("accounts.html", {"request": request})
@app.get("/email-services", response_class=HTMLResponse)
async def email_services_page(request: Request):
"""邮箱服务管理页面"""
return templates.TemplateResponse("email_services.html", {"request": request})
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
"""设置页面"""

View File

@@ -26,10 +26,14 @@ class AccountResponse(BaseModel):
"""账号响应模型"""
id: int
email: str
password: Optional[str] = None
client_id: Optional[str] = None
email_service: str
account_id: Optional[str] = None
workspace_id: Optional[str] = None
registered_at: Optional[str] = None
last_refresh: Optional[str] = None
expires_at: Optional[str] = None
status: str
proxy_used: Optional[str] = None
created_at: Optional[str] = None
@@ -69,10 +73,14 @@ def account_to_response(account: Account) -> AccountResponse:
return AccountResponse(
id=account.id,
email=account.email,
password=account.password,
client_id=account.client_id,
email_service=account.email_service,
account_id=account.account_id,
workspace_id=account.workspace_id,
registered_at=account.registered_at.isoformat() if account.registered_at else None,
last_refresh=account.last_refresh.isoformat() if account.last_refresh else None,
expires_at=account.expires_at.isoformat() if account.expires_at else None,
status=account.status,
proxy_used=account.proxy_used,
created_at=account.created_at.isoformat() if account.created_at else None,
@@ -260,13 +268,18 @@ async def export_accounts_json(
for acc in accounts:
export_data.append({
"email": acc.email,
"password": acc.password,
"client_id": acc.client_id,
"account_id": acc.account_id,
"workspace_id": acc.workspace_id,
"access_token": acc.access_token,
"refresh_token": acc.refresh_token,
"id_token": acc.id_token,
"session_token": acc.session_token,
"email_service": acc.email_service,
"registered_at": acc.registered_at.isoformat() if acc.registered_at else None,
"last_refresh": acc.last_refresh.isoformat() if acc.last_refresh else None,
"expires_at": acc.expires_at.isoformat() if acc.expires_at else None,
"status": acc.status,
})
@@ -310,9 +323,10 @@ async def export_accounts_csv(
# 写入表头
writer.writerow([
"ID", "Email", "Account ID", "Workspace ID",
"Access Token", "Refresh Token", "ID Token",
"Email Service", "Status", "Registered At"
"ID", "Email", "Password", "Client ID",
"Account ID", "Workspace ID",
"Access Token", "Refresh Token", "ID Token", "Session Token",
"Email Service", "Status", "Registered At", "Last Refresh", "Expires At"
])
# 写入数据
@@ -320,14 +334,19 @@ async def export_accounts_csv(
writer.writerow([
acc.id,
acc.email,
acc.password or "",
acc.client_id or "",
acc.account_id or "",
acc.workspace_id or "",
acc.access_token or "",
acc.refresh_token or "",
acc.id_token or "",
acc.session_token or "",
acc.email_service,
acc.status,
acc.registered_at.isoformat() if acc.registered_at else ""
acc.registered_at.isoformat() if acc.registered_at else "",
acc.last_refresh.isoformat() if acc.last_refresh else "",
acc.expires_at.isoformat() if acc.expires_at else ""
])
# 生成文件名
@@ -367,3 +386,123 @@ async def get_accounts_stats():
"by_status": {status: count for status, count in status_stats},
"by_email_service": {service: count for service, count in service_stats}
}
# ============== Token 刷新相关 ==============
class TokenRefreshRequest(BaseModel):
"""Token 刷新请求"""
proxy: Optional[str] = None
class BatchRefreshRequest(BaseModel):
"""批量刷新请求"""
ids: List[int]
proxy: Optional[str] = None
class TokenValidateRequest(BaseModel):
"""Token 验证请求"""
proxy: Optional[str] = None
class BatchValidateRequest(BaseModel):
"""批量验证请求"""
ids: List[int]
proxy: Optional[str] = None
@router.post("/{account_id}/refresh")
async def refresh_account_token(account_id: int, request: TokenRefreshRequest = None):
"""刷新单个账号的 Token"""
from ...core.token_refresh import refresh_account_token as do_refresh
proxy = request.proxy if request else None
result = do_refresh(account_id, proxy)
if result.success:
return {
"success": True,
"message": "Token 刷新成功",
"expires_at": result.expires_at.isoformat() if result.expires_at else None
}
else:
return {
"success": False,
"error": result.error_message
}
@router.post("/batch-refresh")
async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: BackgroundTasks):
"""批量刷新账号 Token"""
from ...core.token_refresh import refresh_account_token as do_refresh
results = {
"success_count": 0,
"failed_count": 0,
"errors": []
}
for account_id in request.ids:
try:
result = do_refresh(account_id, request.proxy)
if result.success:
results["success_count"] += 1
else:
results["failed_count"] += 1
results["errors"].append({"id": account_id, "error": result.error_message})
except Exception as e:
results["failed_count"] += 1
results["errors"].append({"id": account_id, "error": str(e)})
return results
@router.post("/{account_id}/validate")
async def validate_account_token(account_id: int, request: TokenValidateRequest = None):
"""验证单个账号的 Token 有效性"""
from ...core.token_refresh import validate_account_token as do_validate
proxy = request.proxy if request else None
is_valid, error = do_validate(account_id, proxy)
return {
"id": account_id,
"valid": is_valid,
"error": error
}
@router.post("/batch-validate")
async def batch_validate_tokens(request: BatchValidateRequest):
"""批量验证账号 Token 有效性"""
from ...core.token_refresh import validate_account_token as do_validate
results = {
"valid_count": 0,
"invalid_count": 0,
"details": []
}
for account_id in request.ids:
try:
is_valid, error = do_validate(account_id, request.proxy)
results["details"].append({
"id": account_id,
"valid": is_valid,
"error": error
})
if is_valid:
results["valid_count"] += 1
else:
results["invalid_count"] += 1
except Exception as e:
results["invalid_count"] += 1
results["details"].append({
"id": account_id,
"valid": False,
"error": str(e)
})
return results

View File

@@ -43,6 +43,7 @@ class EmailServiceResponse(BaseModel):
name: str
enabled: bool
priority: int
config: Optional[Dict[str, Any]] = None # 过滤敏感信息后的配置
last_used: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
@@ -82,6 +83,29 @@ class OutlookBatchImportResponse(BaseModel):
# ============== Helper Functions ==============
# 敏感字段列表,返回响应时需要过滤
SENSITIVE_FIELDS = {'password', 'api_key', 'refresh_token', 'access_token'}
def filter_sensitive_config(config: Dict[str, Any]) -> Dict[str, Any]:
"""过滤敏感配置信息"""
if not config:
return {}
filtered = {}
for key, value in config.items():
if key in SENSITIVE_FIELDS:
# 敏感字段不返回,但标记是否存在
filtered[f"has_{key}"] = bool(value)
else:
filtered[key] = value
# 为 Outlook 计算是否有 OAuth
if config.get('client_id') and config.get('refresh_token'):
filtered['has_oauth'] = True
return filtered
def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
"""转换服务模型为响应"""
return EmailServiceResponse(
@@ -90,6 +114,7 @@ def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
name=service.name,
enabled=service.enabled,
priority=service.priority,
config=filter_sensitive_config(service.config),
last_used=service.last_used.isoformat() if service.last_used else None,
created_at=service.created_at.isoformat() if service.created_at else None,
updated_at=service.updated_at.isoformat() if service.updated_at else None,
@@ -98,6 +123,39 @@ def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
# ============== API Endpoints ==============
@router.get("/stats")
async def get_email_services_stats():
"""获取邮箱服务统计信息"""
with get_db() as db:
from sqlalchemy import func
# 按类型统计
type_stats = db.query(
EmailServiceModel.service_type,
func.count(EmailServiceModel.id)
).group_by(EmailServiceModel.service_type).all()
# 启用数量
enabled_count = db.query(func.count(EmailServiceModel.id)).filter(
EmailServiceModel.enabled == True
).scalar()
stats = {
'outlook_count': 0,
'custom_count': 0,
'tempmail_available': True, # 临时邮箱始终可用
'enabled_count': enabled_count
}
for service_type, count in type_stats:
if service_type == 'outlook':
stats['outlook_count'] = count
elif service_type == 'custom_domain':
stats['custom_count'] = count
return stats
@router.get("/types")
async def get_service_types():
"""获取支持的邮箱服务类型"""
@@ -435,3 +493,36 @@ async def batch_delete_outlook(service_ids: List[int]):
db.commit()
return {"success": True, "deleted": deleted, "message": f"已删除 {deleted} 个服务"}
# ============== 临时邮箱测试 ==============
class TempmailTestRequest(BaseModel):
"""临时邮箱测试请求"""
api_url: Optional[str] = None
@router.post("/test-tempmail")
async def test_tempmail_service(request: TempmailTestRequest):
"""测试临时邮箱服务是否可用"""
try:
from ...services import EmailServiceFactory, EmailServiceType
from ...config.settings import get_settings
settings = get_settings()
base_url = request.api_url or settings.tempmail_base_url
config = {"base_url": base_url}
tempmail = EmailServiceFactory.create(EmailServiceType.TEMPMAIL, config)
# 检查服务健康状态
health = tempmail.check_health()
if health:
return {"success": True, "message": "临时邮箱连接正常"}
else:
return {"success": False, "message": "临时邮箱连接失败"}
except Exception as e:
logger.error(f"测试临时邮箱失败: {e}")
return {"success": False, "message": f"测试失败: {str(e)}"}

View File

@@ -7,14 +7,14 @@ import logging
import uuid
import random
from datetime import datetime
from typing import List, Optional, Dict
from typing import List, Optional, Dict, Tuple
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel, Field
from ...database import crud
from ...database.session import get_db
from ...database.models import RegistrationTask
from ...database.models import RegistrationTask, Proxy
from ...core.register import RegistrationEngine, RegistrationResult
from ...services import EmailServiceFactory, EmailServiceType
from ...config.settings import get_settings
@@ -28,6 +28,38 @@ running_tasks: dict = {}
batch_tasks: Dict[str, dict] = {}
# ============== Proxy Helper Functions ==============
def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]:
"""
获取用于注册的代理
策略:
1. 优先从代理列表中随机选择一个启用的代理
2. 如果代理列表为空,使用系统设置中的默认代理
Returns:
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID如果来自代理列表
"""
# 先尝试从代理列表中获取
proxy = crud.get_random_proxy(db)
if proxy:
return proxy.proxy_url, proxy.id
# 代理列表为空,使用系统设置中的默认代理
settings = get_settings()
if settings.proxy_enabled and settings.proxy_url:
return settings.proxy_url, None
return None, None
def update_proxy_usage(db, proxy_id: Optional[int]):
"""更新代理的使用时间"""
if proxy_id:
crud.update_proxy_last_used(db, proxy_id)
# ============== Pydantic Models ==============
class RegistrationTaskCreate(BaseModel):
@@ -114,6 +146,20 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
logger.error(f"任务不存在: {task_uuid}")
return
# 确定使用的代理
# 如果前端传入了代理参数,使用传入的
# 否则从代理列表或系统设置中获取
actual_proxy_url = proxy
proxy_id = None
if not actual_proxy_url:
actual_proxy_url, proxy_id = get_proxy_for_registration(db)
if actual_proxy_url:
logger.info(f"任务 {task_uuid} 使用代理: {actual_proxy_url[:50]}...")
# 更新任务的代理记录
crud.update_registration_task(db, task_uuid, proxy=actual_proxy_url)
# 创建邮箱服务
service_type = EmailServiceType(email_service_type)
settings = get_settings()
@@ -140,7 +186,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
"base_url": settings.tempmail_base_url,
"timeout": settings.tempmail_timeout,
"max_retries": settings.tempmail_max_retries,
"proxy_url": proxy,
"proxy_url": actual_proxy_url,
}
elif service_type == EmailServiceType.CUSTOM_DOMAIN:
# 检查数据库中是否有可用的自定义域名服务
@@ -158,7 +204,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
config = {
"base_url": settings.custom_domain_base_url,
"api_key": settings.custom_domain_api_key.get_secret_value() if settings.custom_domain_api_key else "",
"proxy_url": proxy,
"proxy_url": actual_proxy_url,
}
else:
raise ValueError("没有可用的自定义域名邮箱服务,请先在设置中配置")
@@ -188,7 +234,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
engine = RegistrationEngine(
email_service=email_service,
proxy_url=proxy,
proxy_url=actual_proxy_url,
callback_logger=log_callback,
task_uuid=task_uuid
)
@@ -197,6 +243,9 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
result = engine.run()
if result.success:
# 更新代理使用时间
update_proxy_usage(db, proxy_id)
# 保存到数据库
engine.save_to_database(result)

View File

@@ -135,6 +135,66 @@ async def update_proxy_settings(request: ProxySettings):
return {"success": True, "message": "代理设置已更新"}
@router.post("/proxy/test")
async def test_proxy_settings(request: ProxySettings):
"""测试代理连接"""
import time
from curl_cffi import requests as cffi_requests
# 构建代理 URL
if request.type == "http":
scheme = "http"
elif request.type == "socks5":
scheme = "socks5"
else:
raise HTTPException(status_code=400, detail="不支持的代理类型")
auth = ""
if request.username and request.password:
auth = f"{request.username}:{request.password}@"
proxy_url = f"{scheme}://{auth}{request.host}:{request.port}"
# 测试连接
test_url = "https://api.ipify.org?format=json"
start_time = time.time()
try:
proxies = {
"http": proxy_url,
"https": proxy_url
}
response = cffi_requests.get(
test_url,
proxies=proxies,
timeout=3,
impersonate="chrome110"
)
elapsed_time = time.time() - start_time
if response.status_code == 200:
ip_info = response.json()
return {
"success": True,
"ip": ip_info.get("ip", ""),
"response_time": round(elapsed_time * 1000), # 毫秒
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
}
else:
return {
"success": False,
"message": f"代理返回错误状态码: {response.status_code}"
}
except Exception as e:
return {
"success": False,
"message": f"代理连接失败: {str(e)}"
}
@router.get("/registration")
async def get_registration_settings():
"""获取注册设置"""
@@ -292,3 +352,275 @@ async def get_recent_logs(
}
except Exception as e:
return {"logs": [], "error": str(e)}
# ============== 临时邮箱设置 ==============
class TempmailSettings(BaseModel):
"""临时邮箱设置"""
api_url: Optional[str] = None
enabled: bool = True
@router.get("/tempmail")
async def get_tempmail_settings():
"""获取临时邮箱设置"""
settings = get_settings()
return {
"api_url": settings.tempmail_base_url,
"timeout": settings.tempmail_timeout,
"max_retries": settings.tempmail_max_retries,
"enabled": True # 临时邮箱默认可用
}
@router.post("/tempmail")
async def update_tempmail_settings(request: TempmailSettings):
"""更新临时邮箱设置"""
update_dict = {}
if request.api_url:
update_dict["tempmail_base_url"] = request.api_url
update_settings(**update_dict)
return {"success": True, "message": "临时邮箱设置已更新"}
# ============== 代理列表 CRUD ==============
class ProxyCreateRequest(BaseModel):
"""创建代理请求"""
name: str
type: str = "http" # http, socks5
host: str
port: int
username: Optional[str] = None
password: Optional[str] = None
enabled: bool = True
priority: int = 0
class ProxyUpdateRequest(BaseModel):
"""更新代理请求"""
name: Optional[str] = None
type: Optional[str] = None
host: Optional[str] = None
port: Optional[int] = None
username: Optional[str] = None
password: Optional[str] = None
enabled: Optional[bool] = None
priority: Optional[int] = None
@router.get("/proxies")
async def get_proxies_list(enabled: Optional[bool] = None):
"""获取代理列表"""
with get_db() as db:
proxies = crud.get_proxies(db, enabled=enabled)
return {
"proxies": [p.to_dict() for p in proxies],
"total": len(proxies)
}
@router.post("/proxies")
async def create_proxy_item(request: ProxyCreateRequest):
"""创建代理"""
with get_db() as db:
proxy = crud.create_proxy(
db,
name=request.name,
type=request.type,
host=request.host,
port=request.port,
username=request.username,
password=request.password,
enabled=request.enabled,
priority=request.priority
)
return {"success": True, "proxy": proxy.to_dict()}
@router.get("/proxies/{proxy_id}")
async def get_proxy_item(proxy_id: int):
"""获取单个代理"""
with get_db() as db:
proxy = crud.get_proxy_by_id(db, proxy_id)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
return proxy.to_dict(include_password=True)
@router.patch("/proxies/{proxy_id}")
async def update_proxy_item(proxy_id: int, request: ProxyUpdateRequest):
"""更新代理"""
with get_db() as db:
update_data = {}
if request.name is not None:
update_data["name"] = request.name
if request.type is not None:
update_data["type"] = request.type
if request.host is not None:
update_data["host"] = request.host
if request.port is not None:
update_data["port"] = request.port
if request.username is not None:
update_data["username"] = request.username
if request.password is not None:
update_data["password"] = request.password
if request.enabled is not None:
update_data["enabled"] = request.enabled
if request.priority is not None:
update_data["priority"] = request.priority
proxy = crud.update_proxy(db, proxy_id, **update_data)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
return {"success": True, "proxy": proxy.to_dict()}
@router.delete("/proxies/{proxy_id}")
async def delete_proxy_item(proxy_id: int):
"""删除代理"""
with get_db() as db:
success = crud.delete_proxy(db, proxy_id)
if not success:
raise HTTPException(status_code=404, detail="代理不存在")
return {"success": True, "message": "代理已删除"}
@router.post("/proxies/{proxy_id}/test")
async def test_proxy_item(proxy_id: int):
"""测试单个代理"""
import time
from curl_cffi import requests as cffi_requests
with get_db() as db:
proxy = crud.get_proxy_by_id(db, proxy_id)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
proxy_url = proxy.proxy_url
test_url = "https://api.ipify.org?format=json"
start_time = time.time()
try:
proxies = {
"http": proxy_url,
"https": proxy_url
}
response = cffi_requests.get(
test_url,
proxies=proxies,
timeout=3,
impersonate="chrome110"
)
elapsed_time = time.time() - start_time
if response.status_code == 200:
ip_info = response.json()
return {
"success": True,
"ip": ip_info.get("ip", ""),
"response_time": round(elapsed_time * 1000),
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
}
else:
return {
"success": False,
"message": f"代理返回错误状态码: {response.status_code}"
}
except Exception as e:
return {
"success": False,
"message": f"代理连接失败: {str(e)}"
}
@router.post("/proxies/test-all")
async def test_all_proxies():
"""测试所有启用的代理"""
import time
from curl_cffi import requests as cffi_requests
with get_db() as db:
proxies = crud.get_enabled_proxies(db)
results = []
for proxy in proxies:
proxy_url = proxy.proxy_url
test_url = "https://api.ipify.org?format=json"
start_time = time.time()
try:
proxies_dict = {
"http": proxy_url,
"https": proxy_url
}
response = cffi_requests.get(
test_url,
proxies=proxies_dict,
timeout=3,
impersonate="chrome110"
)
elapsed_time = time.time() - start_time
if response.status_code == 200:
ip_info = response.json()
results.append({
"id": proxy.id,
"name": proxy.name,
"success": True,
"ip": ip_info.get("ip", ""),
"response_time": round(elapsed_time * 1000)
})
else:
results.append({
"id": proxy.id,
"name": proxy.name,
"success": False,
"message": f"状态码: {response.status_code}"
})
except Exception as e:
results.append({
"id": proxy.id,
"name": proxy.name,
"success": False,
"message": str(e)
})
success_count = sum(1 for r in results if r["success"])
return {
"total": len(proxies),
"success": success_count,
"failed": len(proxies) - success_count,
"results": results
}
@router.post("/proxies/{proxy_id}/enable")
async def enable_proxy(proxy_id: int):
"""启用代理"""
with get_db() as db:
proxy = crud.update_proxy(db, proxy_id, enabled=True)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
return {"success": True, "message": "代理已启用"}
@router.post("/proxies/{proxy_id}/disable")
async def disable_proxy(proxy_id: int):
"""禁用代理"""
with get_db() as db:
proxy = crud.update_proxy(db, proxy_id, enabled=False)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
return {"success": True, "message": "代理已禁用"}