feat(webui): 添加WebSocket支持实现实时任务状态更新

- 在注册任务和批量任务中集成WebSocket连接
- 添加TaskManager管理任务状态和日志推送
- 前端app.js重构支持WebSocket与轮询降级机制
- 配置模块重构为完全基于数据库存储
This commit is contained in:
cnlimiter
2026-03-15 03:52:24 +08:00
parent 76efc047b3
commit 3d8a90cda9
11 changed files with 1028 additions and 248 deletions

View File

@@ -2,7 +2,18 @@
配置模块
"""
from .settings import Settings, get_settings, update_settings, get_database_url
from .settings import (
Settings,
get_settings,
update_settings,
get_database_url,
init_default_settings,
get_setting_definition,
get_all_setting_definitions,
SETTING_DEFINITIONS,
SettingCategory,
SettingDefinition,
)
from .constants import (
AccountStatus,
TaskStatus,
@@ -22,6 +33,12 @@ __all__ = [
'get_settings',
'update_settings',
'get_database_url',
'init_default_settings',
'get_setting_definition',
'get_all_setting_definitions',
'SETTING_DEFINITIONS',
'SettingCategory',
'SettingDefinition',
'AccountStatus',
'TaskStatus',
'EmailServiceType',

View File

@@ -114,8 +114,6 @@ EMAIL_SERVICE_DEFAULTS = {
# 验证码相关
OTP_CODE_PATTERN = r"(?<!\d)(\d{6})(?!\d)"
OTP_WAIT_TIMEOUT = 120 # 秒
OTP_POLL_INTERVAL = 3 # 秒
OTP_MAX_ATTEMPTS = 40 # 最大轮询次数
# 验证码提取正则(增强版)

View File

@@ -1,40 +1,451 @@
"""
配置管理 - Pydantic 设置模型
配置管理 - 完全基于数据库存储
所有配置都从数据库读取,不再使用环境变量或 .env 文件
"""
import os
from typing import Optional, Dict, Any
from pydantic import Field, field_validator
from typing import Optional, Dict, Any, Type
from enum import Enum
from pydantic import BaseModel, field_validator
from pydantic.types import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
from dataclasses import dataclass
class Settings(BaseSettings):
"""
应用配置
优先级:环境变量 > .env 文件 > 默认值
"""
class SettingCategory(str, Enum):
"""设置分类"""
GENERAL = "general"
DATABASE = "database"
WEBUI = "webui"
LOG = "log"
OPENAI = "openai"
PROXY = "proxy"
REGISTRATION = "registration"
EMAIL = "email"
TEMPMAIL = "tempmail"
CUSTOM_DOMAIN = "custom_domain"
SECURITY = "security"
CPA = "cpa"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
@dataclass
class SettingDefinition:
"""设置定义"""
db_key: str
default_value: Any
category: SettingCategory
description: str = ""
is_secret: bool = False
# 所有配置项定义(包含数据库键名、默认值、分类、描述)
SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
# 应用信息
app_name: str = Field(default="OpenAI/Codex CLI 自动注册系统")
app_version: str = Field(default="2.0.0")
debug: bool = Field(default=False)
"app_name": SettingDefinition(
db_key="app.name",
default_value="OpenAI/Codex CLI 自动注册系统",
category=SettingCategory.GENERAL,
description="应用名称"
),
"app_version": SettingDefinition(
db_key="app.version",
default_value="2.0.0",
category=SettingCategory.GENERAL,
description="应用版本"
),
"debug": SettingDefinition(
db_key="app.debug",
default_value=False,
category=SettingCategory.GENERAL,
description="调试模式"
),
# 数据库配置
database_url: str = Field(
default=os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
'data',
'database.db'
)
)
"database_url": SettingDefinition(
db_key="database.url",
default_value="data/database.db",
category=SettingCategory.DATABASE,
description="数据库路径或连接字符串"
),
# Web UI 配置
"webui_host": SettingDefinition(
db_key="webui.host",
default_value="0.0.0.0",
category=SettingCategory.WEBUI,
description="Web UI 监听地址"
),
"webui_port": SettingDefinition(
db_key="webui.port",
default_value=8000,
category=SettingCategory.WEBUI,
description="Web UI 监听端口"
),
"webui_secret_key": SettingDefinition(
db_key="webui.secret_key",
default_value="your-secret-key-change-in-production",
category=SettingCategory.WEBUI,
description="Web UI 密钥",
is_secret=True
),
# 日志配置
"log_level": SettingDefinition(
db_key="log.level",
default_value="INFO",
category=SettingCategory.LOG,
description="日志级别"
),
"log_file": SettingDefinition(
db_key="log.file",
default_value="logs/app.log",
category=SettingCategory.LOG,
description="日志文件路径"
),
"log_retention_days": SettingDefinition(
db_key="log.retention_days",
default_value=30,
category=SettingCategory.LOG,
description="日志保留天数"
),
# OpenAI 配置
"openai_client_id": SettingDefinition(
db_key="openai.client_id",
default_value="app_EMoamEEZ73f0CkXaXp7hrann",
category=SettingCategory.OPENAI,
description="OpenAI OAuth 客户端 ID"
),
"openai_auth_url": SettingDefinition(
db_key="openai.auth_url",
default_value="https://auth.openai.com/oauth/authorize",
category=SettingCategory.OPENAI,
description="OpenAI OAuth 授权 URL"
),
"openai_token_url": SettingDefinition(
db_key="openai.token_url",
default_value="https://auth.openai.com/oauth/token",
category=SettingCategory.OPENAI,
description="OpenAI OAuth Token URL"
),
"openai_redirect_uri": SettingDefinition(
db_key="openai.redirect_uri",
default_value="http://localhost:1455/auth/callback",
category=SettingCategory.OPENAI,
description="OpenAI OAuth 回调 URI"
),
"openai_scope": SettingDefinition(
db_key="openai.scope",
default_value="openid email profile offline_access",
category=SettingCategory.OPENAI,
description="OpenAI OAuth 权限范围"
),
# 代理配置
"proxy_enabled": SettingDefinition(
db_key="proxy.enabled",
default_value=False,
category=SettingCategory.PROXY,
description="是否启用代理"
),
"proxy_type": SettingDefinition(
db_key="proxy.type",
default_value="http",
category=SettingCategory.PROXY,
description="代理类型 (http/socks5)"
),
"proxy_host": SettingDefinition(
db_key="proxy.host",
default_value="127.0.0.1",
category=SettingCategory.PROXY,
description="代理服务器地址"
),
"proxy_port": SettingDefinition(
db_key="proxy.port",
default_value=7890,
category=SettingCategory.PROXY,
description="代理服务器端口"
),
"proxy_username": SettingDefinition(
db_key="proxy.username",
default_value="",
category=SettingCategory.PROXY,
description="代理用户名"
),
"proxy_password": SettingDefinition(
db_key="proxy.password",
default_value="",
category=SettingCategory.PROXY,
description="代理密码",
is_secret=True
),
# 注册配置
"registration_max_retries": SettingDefinition(
db_key="registration.max_retries",
default_value=3,
category=SettingCategory.REGISTRATION,
description="注册最大重试次数"
),
"registration_timeout": SettingDefinition(
db_key="registration.timeout",
default_value=120,
category=SettingCategory.REGISTRATION,
description="注册超时时间(秒)"
),
"registration_default_password_length": SettingDefinition(
db_key="registration.default_password_length",
default_value=12,
category=SettingCategory.REGISTRATION,
description="默认密码长度"
),
"registration_sleep_min": SettingDefinition(
db_key="registration.sleep_min",
default_value=5,
category=SettingCategory.REGISTRATION,
description="注册间隔最小值(秒)"
),
"registration_sleep_max": SettingDefinition(
db_key="registration.sleep_max",
default_value=30,
category=SettingCategory.REGISTRATION,
description="注册间隔最大值(秒)"
),
# 邮箱服务配置
"email_service_priority": SettingDefinition(
db_key="email.service_priority",
default_value={"tempmail": 0, "outlook": 1, "custom_domain": 2},
category=SettingCategory.EMAIL,
description="邮箱服务优先级"
),
# Tempmail.lol 配置
"tempmail_base_url": SettingDefinition(
db_key="tempmail.base_url",
default_value="https://api.tempmail.lol/v2",
category=SettingCategory.TEMPMAIL,
description="Tempmail API 地址"
),
"tempmail_timeout": SettingDefinition(
db_key="tempmail.timeout",
default_value=30,
category=SettingCategory.TEMPMAIL,
description="Tempmail 超时时间(秒)"
),
"tempmail_max_retries": SettingDefinition(
db_key="tempmail.max_retries",
default_value=3,
category=SettingCategory.TEMPMAIL,
description="Tempmail 最大重试次数"
),
# 自定义域名邮箱配置
"custom_domain_base_url": SettingDefinition(
db_key="custom_domain.base_url",
default_value="",
category=SettingCategory.CUSTOM_DOMAIN,
description="自定义域名 API 地址"
),
"custom_domain_api_key": SettingDefinition(
db_key="custom_domain.api_key",
default_value="",
category=SettingCategory.CUSTOM_DOMAIN,
description="自定义域名 API 密钥",
is_secret=True
),
# 安全配置
"encryption_key": SettingDefinition(
db_key="security.encryption_key",
default_value="your-encryption-key-change-in-production",
category=SettingCategory.SECURITY,
description="加密密钥",
is_secret=True
),
# CPA 上传配置
"cpa_enabled": SettingDefinition(
db_key="cpa.enabled",
default_value=False,
category=SettingCategory.CPA,
description="是否启用 CPA 上传"
),
"cpa_api_url": SettingDefinition(
db_key="cpa.api_url",
default_value="",
category=SettingCategory.CPA,
description="CPA API 地址"
),
"cpa_api_token": SettingDefinition(
db_key="cpa.api_token",
default_value="",
category=SettingCategory.CPA,
description="CPA API Token",
is_secret=True
),
# 验证码配置
"email_code_timeout": SettingDefinition(
db_key="email_code.timeout",
default_value=120,
category=SettingCategory.EMAIL,
description="验证码等待超时时间(秒)"
),
"email_code_poll_interval": SettingDefinition(
db_key="email_code.poll_interval",
default_value=3,
category=SettingCategory.EMAIL,
description="验证码轮询间隔(秒)"
),
}
# 属性名到数据库键名的映射(用于向后兼容)
DB_SETTING_KEYS = {name: defn.db_key for name, defn in SETTING_DEFINITIONS.items()}
# 类型定义映射
SETTING_TYPES: Dict[str, Type] = {
"debug": bool,
"webui_port": int,
"log_retention_days": int,
"proxy_enabled": bool,
"proxy_port": int,
"registration_max_retries": int,
"registration_timeout": int,
"registration_default_password_length": int,
"registration_sleep_min": int,
"registration_sleep_max": int,
"email_service_priority": dict,
"tempmail_timeout": int,
"tempmail_max_retries": int,
"cpa_enabled": bool,
"email_code_timeout": int,
"email_code_poll_interval": int,
}
# 需要作为 SecretStr 处理的字段
SECRET_FIELDS = {name for name, defn in SETTING_DEFINITIONS.items() if defn.is_secret}
def _convert_value(attr_name: str, value: str) -> Any:
"""将数据库字符串值转换为正确的类型"""
if attr_name in SECRET_FIELDS:
return SecretStr(value) if value else SecretStr("")
target_type = SETTING_TYPES.get(attr_name, str)
if target_type == bool:
if isinstance(value, bool):
return value
return str(value).lower() in ("true", "1", "yes", "on")
elif target_type == int:
if isinstance(value, int):
return value
return int(value) if value else 0
elif target_type == dict:
if isinstance(value, dict):
return value
import json
return json.loads(value) if value else {}
else:
return value
def _value_to_string(value: Any) -> str:
"""将值转换为数据库存储的字符串"""
if isinstance(value, SecretStr):
return value.get_secret_value()
elif isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, dict):
import json
return json.dumps(value)
elif value is None:
return ""
else:
return str(value)
def init_default_settings() -> None:
"""
初始化数据库中的默认设置
如果设置项不存在,则创建并设置默认值
"""
try:
from ..database.session import get_db
from ..database.crud import get_setting, set_setting
with get_db() as db:
for attr_name, defn in SETTING_DEFINITIONS.items():
existing = get_setting(db, defn.db_key)
if not existing:
default_value = _value_to_string(defn.default_value)
set_setting(
db,
defn.db_key,
default_value,
category=defn.category.value,
description=defn.description
)
print(f"[Settings] 初始化默认设置: {defn.db_key} = {default_value if not defn.is_secret else '***'}")
except Exception as e:
print(f"[Settings] 初始化默认设置失败: {e}")
def _load_settings_from_db() -> Dict[str, Any]:
"""从数据库加载所有设置"""
try:
from ..database.session import get_db
from ..database.crud import get_setting
settings_dict = {}
with get_db() as db:
for attr_name, defn in SETTING_DEFINITIONS.items():
db_setting = get_setting(db, defn.db_key)
if db_setting:
settings_dict[attr_name] = _convert_value(attr_name, db_setting.value)
else:
# 数据库中没有此设置,使用默认值
settings_dict[attr_name] = _convert_value(attr_name, _value_to_string(defn.default_value))
return settings_dict
except Exception as e:
print(f"[Settings] 从数据库加载设置失败: {e},使用默认值")
return {name: defn.default_value for name, defn in SETTING_DEFINITIONS.items()}
def _save_settings_to_db(**kwargs) -> None:
"""保存设置到数据库"""
try:
from ..database.session import get_db
from ..database.crud import set_setting
with get_db() as db:
for attr_name, value in kwargs.items():
if attr_name in SETTING_DEFINITIONS:
defn = SETTING_DEFINITIONS[attr_name]
str_value = _value_to_string(value)
set_setting(
db,
defn.db_key,
str_value,
category=defn.category.value,
description=defn.description
)
except Exception as e:
print(f"[Settings] 保存设置到数据库失败: {e}")
class Settings(BaseModel):
"""
应用配置 - 完全基于数据库存储
"""
# 应用信息
app_name: str = "OpenAI/Codex CLI 自动注册系统"
app_version: str = "2.0.0"
debug: bool = False
# 数据库配置
database_url: str = "data/database.db"
@field_validator('database_url', mode='before')
@classmethod
@@ -48,31 +459,29 @@ class Settings(BaseSettings):
return v
# Web UI 配置
webui_host: str = Field(default="0.0.0.0")
webui_port: int = Field(default=8000)
webui_secret_key: SecretStr = Field(
default=SecretStr("your-secret-key-change-in-production")
)
webui_host: str = "0.0.0.0"
webui_port: int = 8000
webui_secret_key: SecretStr = SecretStr("your-secret-key-change-in-production")
# 日志配置
log_level: str = Field(default="INFO")
log_file: str = Field(default="logs/app.log")
log_retention_days: int = Field(default=30)
log_level: str = "INFO"
log_file: str = "logs/app.log"
log_retention_days: int = 30
# OpenAI 配置
openai_client_id: str = Field(default="app_EMoamEEZ73f0CkXaXp7hrann")
openai_auth_url: str = Field(default="https://auth.openai.com/oauth/authorize")
openai_token_url: str = Field(default="https://auth.openai.com/oauth/token")
openai_redirect_uri: str = Field(default="http://localhost:1455/auth/callback")
openai_scope: str = Field(default="openid email profile offline_access")
openai_client_id: str = "app_EMoamEEZ73f0CkXaXp7hrann"
openai_auth_url: str = "https://auth.openai.com/oauth/authorize"
openai_token_url: str = "https://auth.openai.com/oauth/token"
openai_redirect_uri: str = "http://localhost:1455/auth/callback"
openai_scope: str = "openid email profile offline_access"
# 代理配置
proxy_enabled: bool = Field(default=False)
proxy_type: str = Field(default="http") # http, socks5
proxy_host: str = Field(default="127.0.0.1")
proxy_port: int = Field(default=7890)
proxy_username: Optional[str] = Field(default=None)
proxy_password: Optional[SecretStr] = Field(default=None)
proxy_enabled: bool = False
proxy_type: str = "http"
proxy_host: str = "127.0.0.1"
proxy_port: int = 7890
proxy_username: Optional[str] = None
proxy_password: Optional[SecretStr] = None
@property
def proxy_url(self) -> Optional[str]:
@@ -94,35 +503,35 @@ class Settings(BaseSettings):
return f"{scheme}://{auth}{self.proxy_host}:{self.proxy_port}"
# 注册配置
registration_max_retries: int = Field(default=3)
registration_timeout: int = Field(default=120) # 秒
registration_default_password_length: int = Field(default=12)
registration_sleep_min: int = Field(default=5)
registration_sleep_max: int = Field(default=30)
registration_max_retries: int = 3
registration_timeout: int = 120
registration_default_password_length: int = 12
registration_sleep_min: int = 5
registration_sleep_max: int = 30
# 邮箱服务配置
email_service_priority: Dict[str, int] = Field(
default={"tempmail": 0, "outlook": 1, "custom_domain": 2}
)
email_service_priority: Dict[str, int] = {"tempmail": 0, "outlook": 1, "custom_domain": 2}
# Tempmail.lol 配置
tempmail_base_url: str = Field(default="https://api.tempmail.lol/v2")
tempmail_timeout: int = Field(default=30)
tempmail_max_retries: int = Field(default=3)
tempmail_base_url: str = "https://api.tempmail.lol/v2"
tempmail_timeout: int = 30
tempmail_max_retries: int = 3
# 自定义域名邮箱配置
custom_domain_base_url: str = Field(default="")
custom_domain_api_key: Optional[SecretStr] = Field(default=None)
custom_domain_base_url: str = ""
custom_domain_api_key: Optional[SecretStr] = None
# 安全配置
encryption_key: SecretStr = Field(
default=SecretStr("your-encryption-key-change-in-production")
)
encryption_key: SecretStr = SecretStr("your-encryption-key-change-in-production")
# CPA 上传配置
cpa_enabled: bool = Field(default=False)
cpa_api_url: str = Field(default="") # 例如: https://cpa.example.com
cpa_api_token: SecretStr = Field(default=SecretStr(""))
cpa_enabled: bool = False
cpa_api_url: str = ""
cpa_api_token: SecretStr = SecretStr("")
# 验证码配置
email_code_timeout: int = 120
email_code_poll_interval: int = 3
# 全局配置实例
@@ -132,25 +541,34 @@ _settings: Optional[Settings] = None
def get_settings() -> Settings:
"""
获取全局配置实例(单例模式)
完全从数据库加载配置
"""
global _settings
if _settings is None:
_settings = Settings()
# 先初始化默认设置(如果数据库中没有的话)
init_default_settings()
# 从数据库加载所有设置
settings_dict = _load_settings_from_db()
_settings = Settings(**settings_dict)
return _settings
def update_settings(**kwargs) -> Settings:
"""
更新配置(用于测试或运行时配置更改)
更新配置并保存到数据库
"""
global _settings
if _settings is None:
_settings = Settings()
_settings = get_settings()
# 创建新的配置实例
updated_data = _settings.model_dump()
updated_data.update(kwargs)
_settings = Settings(**updated_data)
# 保存到数据库
_save_settings_to_db(**kwargs)
return _settings
@@ -171,3 +589,13 @@ def get_database_url() -> str:
return f"sqlite:///{abs_path}"
return url
def get_setting_definition(attr_name: str) -> Optional[SettingDefinition]:
"""获取设置项的定义信息"""
return SETTING_DEFINITIONS.get(attr_name)
def get_all_setting_definitions() -> Dict[str, SettingDefinition]:
"""获取所有设置项的定义"""
return SETTING_DEFINITIONS.copy()