mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-21 00:00:52 +08:00
4
This commit is contained in:
@@ -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
332
src/core/token_refresh.py
Normal 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)
|
||||
@@ -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()
|
||||
@@ -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}"
|
||||
@@ -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):
|
||||
"""设置页面"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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": "代理已禁用"}
|
||||
|
||||
Reference in New Issue
Block a user