frist
This commit is contained in:
541
config.py
Normal file
541
config.py
Normal file
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
配置管理模块
|
||||
|
||||
使用 Pydantic Settings 从环境变量和 .env 文件加载配置
|
||||
|
||||
支持的配置项:
|
||||
- 代理设置(代理池、轮换策略)
|
||||
- 并发控制(最大并发数、重试次数)
|
||||
- 日志级别
|
||||
- 邮箱配置(可选)
|
||||
- Sentinel 配置(可选)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import List, Optional, Literal, Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
import random
|
||||
|
||||
# 加载 .env 文件
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ProxyConfig(BaseModel):
|
||||
"""代理配置"""
|
||||
|
||||
enabled: bool = Field(default=False, description="是否启用代理")
|
||||
pool: List[str] = Field(default_factory=list, description="代理池列表")
|
||||
rotation: Literal["random", "round_robin"] = Field(
|
||||
default="random",
|
||||
description="代理轮换策略"
|
||||
)
|
||||
|
||||
_current_index: int = 0 # 用于 round_robin 策略
|
||||
|
||||
def get_next_proxy(self) -> Optional[str]:
|
||||
"""
|
||||
获取下一个代理地址
|
||||
|
||||
根据配置的轮换策略返回代理
|
||||
- random: 随机选择
|
||||
- round_robin: 轮流使用
|
||||
|
||||
返回:
|
||||
代理地址,如果代理池为空则返回 None
|
||||
"""
|
||||
if not self.enabled or not self.pool:
|
||||
return None
|
||||
|
||||
if self.rotation == "random":
|
||||
return random.choice(self.pool)
|
||||
else: # round_robin
|
||||
proxy = self.pool[self._current_index % len(self.pool)]
|
||||
self._current_index += 1
|
||||
return proxy
|
||||
|
||||
def validate_proxy_format(self, proxy: str) -> bool:
|
||||
"""
|
||||
验证代理格式是否正确
|
||||
|
||||
支持格式:
|
||||
- http://ip:port
|
||||
- http://user:pass@ip:port
|
||||
- https://ip:port
|
||||
- socks5://ip:port
|
||||
|
||||
参数:
|
||||
proxy: 代理地址
|
||||
|
||||
返回:
|
||||
True 如果格式正确,否则 False
|
||||
"""
|
||||
import re
|
||||
pattern = r'^(http|https|socks5)://([^:]+:[^@]+@)?[\w.-]+:\d+$'
|
||||
return bool(re.match(pattern, proxy))
|
||||
|
||||
|
||||
class MailConfig(BaseModel):
|
||||
"""邮箱配置"""
|
||||
|
||||
enabled: bool = Field(default=False, description="是否启用邮箱功能")
|
||||
type: Literal["imap", "tempmail", "api", "manual", "cloudmail"] = Field(
|
||||
default="manual",
|
||||
description="邮箱类型"
|
||||
)
|
||||
|
||||
# IMAP 配置
|
||||
imap_host: Optional[str] = Field(default=None, description="IMAP 服务器地址")
|
||||
imap_port: int = Field(default=993, description="IMAP 端口")
|
||||
imap_username: Optional[str] = Field(default=None, description="邮箱用户名")
|
||||
imap_password: Optional[str] = Field(default=None, description="邮箱密码")
|
||||
|
||||
# 临时邮箱 API 配置
|
||||
api_key: Optional[str] = Field(default=None, description="临时邮箱 API Key")
|
||||
api_endpoint: Optional[str] = Field(default=None, description="临时邮箱 API 端点")
|
||||
|
||||
# CloudMail 配置
|
||||
cloudmail_api_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail API 基础 URL"
|
||||
)
|
||||
cloudmail_token: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail 身份令牌(预先生成)"
|
||||
)
|
||||
cloudmail_domain: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail 邮箱域名(例如 mygoband.com)"
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典格式(供 MailHandler 使用)"""
|
||||
config = {"type": self.type}
|
||||
|
||||
if self.type == "imap":
|
||||
config.update({
|
||||
"host": self.imap_host,
|
||||
"port": self.imap_port,
|
||||
"username": self.imap_username,
|
||||
"password": self.imap_password,
|
||||
})
|
||||
elif self.type in ["tempmail", "api"]:
|
||||
config.update({
|
||||
"api_key": self.api_key,
|
||||
"api_endpoint": self.api_endpoint,
|
||||
})
|
||||
elif self.type == "cloudmail":
|
||||
config.update({
|
||||
"api_base_url": self.cloudmail_api_url,
|
||||
"token": self.cloudmail_token,
|
||||
"domain": self.cloudmail_domain,
|
||||
})
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class SentinelConfig(BaseModel):
|
||||
"""Sentinel 配置"""
|
||||
|
||||
enabled: bool = Field(default=False, description="是否启用 Sentinel 解决器")
|
||||
solver_type: Literal["external_script", "api", "module"] = Field(
|
||||
default="external_script",
|
||||
description="解决器类型"
|
||||
)
|
||||
|
||||
# 外部脚本配置
|
||||
script_path: Optional[str] = Field(
|
||||
default=None,
|
||||
description="外部脚本路径(Node.js 或其他)"
|
||||
)
|
||||
|
||||
# API 配置
|
||||
api_endpoint: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Sentinel solver API 端点"
|
||||
)
|
||||
api_key: Optional[str] = Field(default=None, description="API Key")
|
||||
|
||||
# Python 模块配置
|
||||
module_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Python 模块名称(例如 'my_sentinel_solver')"
|
||||
)
|
||||
|
||||
|
||||
class AppConfig(BaseSettings):
|
||||
"""
|
||||
应用配置(从环境变量加载)
|
||||
|
||||
环境变量优先级:
|
||||
1. 系统环境变量
|
||||
2. .env 文件
|
||||
3. 默认值
|
||||
"""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore" # 忽略未定义的环境变量
|
||||
)
|
||||
|
||||
# ========== 代理配置 ==========
|
||||
proxy_enabled: bool = Field(
|
||||
default=False,
|
||||
description="是否启用代理"
|
||||
)
|
||||
proxy_pool: str = Field(
|
||||
default="",
|
||||
description="代理池(逗号分隔),例如: http://ip1:port,socks5://ip2:port"
|
||||
)
|
||||
proxy_rotation: Literal["random", "round_robin"] = Field(
|
||||
default="random",
|
||||
description="代理轮换策略"
|
||||
)
|
||||
|
||||
# ========== 并发配置 ==========
|
||||
max_workers: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
le=50,
|
||||
description="最大并发任务数"
|
||||
)
|
||||
retry_limit: int = Field(
|
||||
default=3,
|
||||
ge=0,
|
||||
le=10,
|
||||
description="失败重试次数"
|
||||
)
|
||||
retry_delay: int = Field(
|
||||
default=5,
|
||||
ge=0,
|
||||
description="重试延迟(秒)"
|
||||
)
|
||||
|
||||
# ========== 日志配置 ==========
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field(
|
||||
default="INFO",
|
||||
description="日志级别"
|
||||
)
|
||||
log_to_file: bool = Field(
|
||||
default=True,
|
||||
description="是否记录日志到文件"
|
||||
)
|
||||
|
||||
# ========== 邮箱配置 ==========
|
||||
mail_enabled: bool = Field(default=False, description="是否启用邮箱功能")
|
||||
mail_type: Literal["imap", "tempmail", "api", "manual", "cloudmail"] = Field(
|
||||
default="manual",
|
||||
description="邮箱类型"
|
||||
)
|
||||
mail_imap_host: Optional[str] = Field(default=None, description="IMAP 服务器")
|
||||
mail_imap_port: int = Field(default=993, description="IMAP 端口")
|
||||
mail_imap_username: Optional[str] = Field(default=None, description="邮箱用户名")
|
||||
mail_imap_password: Optional[str] = Field(default=None, description="邮箱密码")
|
||||
mail_api_key: Optional[str] = Field(default=None, description="临时邮箱 API Key")
|
||||
mail_api_endpoint: Optional[str] = Field(default=None, description="API 端点")
|
||||
|
||||
# CloudMail 配置
|
||||
mail_cloudmail_api_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail API URL"
|
||||
)
|
||||
mail_cloudmail_token: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail 身份令牌"
|
||||
)
|
||||
mail_cloudmail_domain: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Cloud Mail 邮箱域名(例如 mygoband.com)"
|
||||
)
|
||||
|
||||
# ========== Sentinel 配置 ==========
|
||||
sentinel_enabled: bool = Field(default=False, description="是否启用 Sentinel")
|
||||
sentinel_solver_type: Literal["external_script", "api", "module"] = Field(
|
||||
default="external_script",
|
||||
description="Sentinel 解决器类型"
|
||||
)
|
||||
sentinel_script_path: Optional[str] = Field(default=None, description="脚本路径")
|
||||
sentinel_api_endpoint: Optional[str] = Field(default=None, description="API 端点")
|
||||
sentinel_api_key: Optional[str] = Field(default=None, description="API Key")
|
||||
sentinel_module_name: Optional[str] = Field(default=None, description="模块名称")
|
||||
|
||||
# ========== TLS 指纹配置 ==========
|
||||
tls_impersonate: Literal["chrome110", "chrome120", "chrome124"] = Field(
|
||||
default="chrome124",
|
||||
description="模拟的浏览器版本"
|
||||
)
|
||||
|
||||
# ========== 其他配置 ==========
|
||||
accounts_output_file: str = Field(
|
||||
default="accounts.txt",
|
||||
description="成功账号保存文件路径"
|
||||
)
|
||||
request_timeout: int = Field(
|
||||
default=30,
|
||||
ge=5,
|
||||
le=300,
|
||||
description="HTTP 请求超时时间(秒)"
|
||||
)
|
||||
|
||||
@field_validator("proxy_pool")
|
||||
@classmethod
|
||||
def validate_proxy_pool(cls, v: str) -> str:
|
||||
"""验证代理池格式"""
|
||||
if not v:
|
||||
return v
|
||||
|
||||
proxies = [p.strip() for p in v.split(",") if p.strip()]
|
||||
for proxy in proxies:
|
||||
# 基本格式检查
|
||||
if not any(proxy.startswith(prefix) for prefix in ["http://", "https://", "socks5://"]):
|
||||
raise ValueError(
|
||||
f"Invalid proxy format: {proxy}. "
|
||||
"Must start with http://, https://, or socks5://"
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
@property
|
||||
def proxy(self) -> ProxyConfig:
|
||||
"""获取代理配置对象"""
|
||||
pool = [p.strip() for p in self.proxy_pool.split(",") if p.strip()]
|
||||
return ProxyConfig(
|
||||
enabled=self.proxy_enabled,
|
||||
pool=pool,
|
||||
rotation=self.proxy_rotation
|
||||
)
|
||||
|
||||
@property
|
||||
def mail(self) -> MailConfig:
|
||||
"""获取邮箱配置对象"""
|
||||
return MailConfig(
|
||||
enabled=self.mail_enabled,
|
||||
type=self.mail_type,
|
||||
imap_host=self.mail_imap_host,
|
||||
imap_port=self.mail_imap_port,
|
||||
imap_username=self.mail_imap_username,
|
||||
imap_password=self.mail_imap_password,
|
||||
api_key=self.mail_api_key,
|
||||
api_endpoint=self.mail_api_endpoint,
|
||||
cloudmail_api_url=self.mail_cloudmail_api_url,
|
||||
cloudmail_token=self.mail_cloudmail_token,
|
||||
cloudmail_domain=self.mail_cloudmail_domain,
|
||||
)
|
||||
|
||||
@property
|
||||
def sentinel(self) -> SentinelConfig:
|
||||
"""获取 Sentinel 配置对象"""
|
||||
return SentinelConfig(
|
||||
enabled=self.sentinel_enabled,
|
||||
solver_type=self.sentinel_solver_type,
|
||||
script_path=self.sentinel_script_path,
|
||||
api_endpoint=self.sentinel_api_endpoint,
|
||||
api_key=self.sentinel_api_key,
|
||||
module_name=self.sentinel_module_name,
|
||||
)
|
||||
|
||||
def validate_config(self) -> List[str]:
|
||||
"""
|
||||
验证配置完整性,返回警告列表
|
||||
|
||||
返回:
|
||||
警告信息列表,空列表表示无警告
|
||||
"""
|
||||
warnings = []
|
||||
|
||||
# 检查代理配置
|
||||
if self.proxy_enabled and not self.proxy_pool:
|
||||
warnings.append("Proxy enabled but proxy_pool is empty")
|
||||
|
||||
# 检查邮箱配置
|
||||
if self.mail_enabled:
|
||||
if self.mail_type == "imap":
|
||||
if not all([self.mail_imap_host, self.mail_imap_username, self.mail_imap_password]):
|
||||
warnings.append("IMAP mail enabled but credentials incomplete")
|
||||
elif self.mail_type in ["tempmail", "api"]:
|
||||
if not self.mail_api_key:
|
||||
warnings.append(f"{self.mail_type} enabled but api_key not configured")
|
||||
elif self.mail_type == "cloudmail":
|
||||
if not all([self.mail_cloudmail_api_url, self.mail_cloudmail_token, self.mail_cloudmail_domain]):
|
||||
warnings.append(
|
||||
"CloudMail enabled but config incomplete. "
|
||||
"Required: MAIL_CLOUDMAIL_API_URL, MAIL_CLOUDMAIL_TOKEN, MAIL_CLOUDMAIL_DOMAIN"
|
||||
)
|
||||
|
||||
# 检查 Sentinel 配置
|
||||
if self.sentinel_enabled:
|
||||
if self.sentinel_solver_type == "external_script" and not self.sentinel_script_path:
|
||||
warnings.append("Sentinel external_script enabled but script_path not configured")
|
||||
elif self.sentinel_solver_type == "api" and not self.sentinel_api_endpoint:
|
||||
warnings.append("Sentinel API enabled but api_endpoint not configured")
|
||||
elif self.sentinel_solver_type == "module" and not self.sentinel_module_name:
|
||||
warnings.append("Sentinel module enabled but module_name not configured")
|
||||
|
||||
# 检查并发设置
|
||||
if self.max_workers > 10 and not self.proxy_enabled:
|
||||
warnings.append(
|
||||
f"High concurrency ({self.max_workers} workers) without proxy may trigger rate limits"
|
||||
)
|
||||
|
||||
return warnings
|
||||
|
||||
def print_summary(self):
|
||||
"""打印配置摘要"""
|
||||
from utils.logger import logger
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("Configuration Summary")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"Proxy: {'Enabled' if self.proxy_enabled else 'Disabled'}")
|
||||
if self.proxy_enabled:
|
||||
logger.info(f" - Pool size: {len(self.proxy.pool)}")
|
||||
logger.info(f" - Rotation: {self.proxy_rotation}")
|
||||
|
||||
logger.info(f"Mail: {'Enabled' if self.mail_enabled else 'Disabled'}")
|
||||
if self.mail_enabled:
|
||||
logger.info(f" - Type: {self.mail_type}")
|
||||
|
||||
logger.info(f"Sentinel: {'Enabled' if self.sentinel_enabled else 'Disabled'}")
|
||||
if self.sentinel_enabled:
|
||||
logger.info(f" - Solver: {self.sentinel_solver_type}")
|
||||
|
||||
logger.info(f"Concurrency: {self.max_workers} workers")
|
||||
logger.info(f"Retry limit: {self.retry_limit}")
|
||||
logger.info(f"Log level: {self.log_level}")
|
||||
logger.info(f"TLS impersonate: {self.tls_impersonate}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 打印警告
|
||||
warnings = self.validate_config()
|
||||
if warnings:
|
||||
logger.warning("Configuration warnings:")
|
||||
for warning in warnings:
|
||||
logger.warning(f" ⚠️ {warning}")
|
||||
|
||||
|
||||
def load_config() -> AppConfig:
|
||||
"""
|
||||
加载配置
|
||||
|
||||
返回:
|
||||
AppConfig 实例
|
||||
|
||||
示例:
|
||||
config = load_config()
|
||||
print(config.proxy.enabled)
|
||||
print(config.mail.type)
|
||||
"""
|
||||
config = AppConfig()
|
||||
return config
|
||||
|
||||
|
||||
def create_default_env_file(path: str = ".env.example"):
|
||||
"""
|
||||
创建默认的 .env 示例文件
|
||||
|
||||
参数:
|
||||
path: 文件保存路径
|
||||
"""
|
||||
content = """# OpenAI 账号自动注册系统 - 配置文件示例
|
||||
# 复制此文件为 .env 并根据实际情况修改
|
||||
|
||||
# ========== 代理配置 ==========
|
||||
# 是否启用代理(true/false)
|
||||
PROXY_ENABLED=false
|
||||
|
||||
# 代理池(逗号分隔,支持 http/https/socks5)
|
||||
# 格式: protocol://[user:pass@]ip:port
|
||||
# 示例: http://user:pass@1.2.3.4:8080,socks5://5.6.7.8:1080
|
||||
PROXY_POOL=
|
||||
|
||||
# 代理轮换策略(random: 随机选择, round_robin: 轮流使用)
|
||||
PROXY_ROTATION=random
|
||||
|
||||
# ========== 并发配置 ==========
|
||||
# 最大并发任务数(建议 1-5,过高可能触发风控)
|
||||
MAX_WORKERS=1
|
||||
|
||||
# 失败重试次数
|
||||
RETRY_LIMIT=3
|
||||
|
||||
# 重试延迟(秒)
|
||||
RETRY_DELAY=5
|
||||
|
||||
# ========== 日志配置 ==========
|
||||
# 日志级别(DEBUG, INFO, WARNING, ERROR)
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 是否记录日志到文件
|
||||
LOG_TO_FILE=true
|
||||
|
||||
# ========== 邮箱配置 ==========
|
||||
# 是否启用邮箱功能(true/false)
|
||||
MAIL_ENABLED=false
|
||||
|
||||
# 邮箱类型(imap, tempmail, api, cloudmail, manual)
|
||||
MAIL_TYPE=manual
|
||||
|
||||
# IMAP 配置(如果使用 IMAP)
|
||||
MAIL_IMAP_HOST=imap.gmail.com
|
||||
MAIL_IMAP_PORT=993
|
||||
MAIL_IMAP_USERNAME=your@email.com
|
||||
MAIL_IMAP_PASSWORD=your_app_password
|
||||
|
||||
# 临时邮箱 API 配置(如果使用临时邮箱)
|
||||
MAIL_API_KEY=
|
||||
MAIL_API_ENDPOINT=
|
||||
|
||||
# CloudMail 配置(如果使用 CloudMail)
|
||||
# 1. 先通过 Cloud Mail 管理界面或 API 生成 Token
|
||||
# 2. 将 Token 填入 MAIL_CLOUDMAIL_TOKEN
|
||||
# 3. 填写你的邮箱域名(不带 @)
|
||||
# 4. Token 失效时需要手动更新
|
||||
MAIL_CLOUDMAIL_API_URL=https://your-cloudmail-domain.com
|
||||
MAIL_CLOUDMAIL_TOKEN=9f4e298e-7431-4c76-bc15-4931c3a73984
|
||||
MAIL_CLOUDMAIL_DOMAIN=mygoband.com
|
||||
|
||||
# ========== Sentinel 配置 ==========
|
||||
# 是否启用 Sentinel 解决器(true/false)
|
||||
SENTINEL_ENABLED=false
|
||||
|
||||
# Sentinel 解决器类型(external_script, api, module)
|
||||
SENTINEL_SOLVER_TYPE=external_script
|
||||
|
||||
# 外部脚本路径(如果使用 external_script)
|
||||
SENTINEL_SCRIPT_PATH=./sentinel_solver.js
|
||||
|
||||
# API 配置(如果使用 api)
|
||||
SENTINEL_API_ENDPOINT=http://localhost:8000/solve
|
||||
SENTINEL_API_KEY=
|
||||
|
||||
# Python 模块名称(如果使用 module)
|
||||
SENTINEL_MODULE_NAME=
|
||||
|
||||
# ========== TLS 指纹配置 ==========
|
||||
# 模拟的浏览器版本(chrome110, chrome120, chrome124)
|
||||
TLS_IMPERSONATE=chrome124
|
||||
|
||||
# ========== 其他配置 ==========
|
||||
# 成功账号保存文件路径
|
||||
ACCOUNTS_OUTPUT_FILE=accounts.txt
|
||||
|
||||
# HTTP 请求超时时间(秒)
|
||||
REQUEST_TIMEOUT=30
|
||||
"""
|
||||
|
||||
Path(path).write_text(content, encoding="utf-8")
|
||||
print(f"✅ Default .env.example created at: {path}")
|
||||
|
||||
|
||||
# 导出主要接口
|
||||
__all__ = [
|
||||
"AppConfig",
|
||||
"ProxyConfig",
|
||||
"MailConfig",
|
||||
"SentinelConfig",
|
||||
"load_config",
|
||||
"create_default_env_file",
|
||||
]
|
||||
Reference in New Issue
Block a user