Merge remote-tracking branch 'origin/develop' into fix/develop-ruff-f401

This commit is contained in:
2026-07-24 00:16:27 +08:00
23 changed files with 966 additions and 569 deletions
+22 -301
View File
@@ -1,307 +1,28 @@
"""API 服务配置 — 继承 SharedSettings,只追加 API 特有字段
"""API 服务配置(向后兼容层)
通用配置(DB/Redis/OSS/Celery/CosyVoice/Doubao 等)统一在
packages/shared/config.py 的 SharedSettings 中定义,这里不重复。
统一配置已迁移到 packages.config.api_settings。
新代码请使用:
from packages.config import APISettings, get_api_settings
历史上 API 端使用 UPPER_CASE 命名风格的字段,目前通过 property
别名向后兼容。新代码统一使用 snake_case(继承自 SharedSettings
本文件保留 Settings 类名、get_settings() 函数、settings 模块级单例,
确保所有旧的 import 路径仍然有效
"""
import os
from typing import Optional
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import SettingsConfigDict
from packages.shared.config import SharedSettings
class Settings(SharedSettings):
"""API 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 API 独有字段。
"""
# ── 应用基本信息 ────────────────────────────────────────────────────
app_name: str = "xiaoxia-saas"
app_version: str = "0.1.61"
# 应用基础 URL,用于生成认证邮件中的链接
# 开发环境默认 http://localhost:3000
# 生产环境应通过环境变量 APP_BASE_URL 设置
app_base_url: str = "http://localhost:3000"
# Container bind address; external expose is controlled by Docker/Nginx.
api_host: str = "0.0.0.0" # nosec: B104
api_port: int = 8000
# ── 数据库特有 ──────────────────────────────────────────────────────
use_in_memory_db: bool = False
# ── Redis 特有 ──────────────────────────────────────────────────────
enable_redis_sessions: bool = False
# ── JWT ────────────────────────────────────────────────────────────
# JWT secret key - MUST be set via environment variable, no default allowed
jwt_secret_key: Optional[str] = None
# JWT 算法与过期时间
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = 30
jwt_refresh_token_expire_days: int = 30
@field_validator("jwt_secret_key", mode="before")
@classmethod
def validate_jwt_secret_key(cls, v):
if v is None or v == "":
raise ValueError(
"JWT_SECRET_KEY must be set via environment variable. " "Do not use default value in production!"
)
# Block known insecure default values
insecure_defaults = [
"your-secret-key-change-in-production",
"your-secret-key",
"secret",
"changeme",
"password",
]
if v.lower() in [d.lower() for d in insecure_defaults]:
raise ValueError(
f"JWT_SECRET_KEY '{v}' is insecure. " "Please set a strong random secret via environment variable."
)
return v
# ── 邮件 ────────────────────────────────────────────────────────────
enable_email_delivery: bool = False
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
smtp_from_email: str = ""
smtp_from_name: str = "小虾 SaaS"
smtp_use_tls: bool = True
# ── OSS 特有校验 ────────────────────────────────────────────────────
@field_validator("oss_access_key_id", mode="before")
@classmethod
def validate_oss_access_key_id(cls, v):
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
raise ValueError(
"OSS_ACCESS_KEY_ID must be set via environment variable in non-development environments. "
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
)
return v or ""
@field_validator("oss_access_key_secret", mode="before")
@classmethod
def validate_oss_access_key_secret(cls, v):
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
raise ValueError(
"OSS_ACCESS_KEY_SECRET must be set via environment variable in non-development environments. "
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
)
return v or ""
oss_direct_upload_max_mb: int = Field(
default=2000,
validation_alias=AliasChoices("oss_direct_upload_max_mb", "max_upload_size_mb"),
)
# ── CORS ────────────────────────────────────────────────────────────
cors_origins_raw: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
# ── 渲染引擎 ────────────────────────────────────────────────────────
# 渲染引擎选择:legacy=旧VideoComposeServiceunified=新UnifiedRenderService
render_engine: str = "legacy"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
@property
def cors_origins(self) -> list[str]:
return [origin.strip() for origin in self.cors_origins_raw.split(",") if origin.strip()]
# ── 向后兼容:UPPER_CASE property 别名 ──────────────────────────────
# 新代码请使用 snake_case(继承的字段名),以下别名仅用于兼容旧代码
@property
def APP_NAME(self) -> str:
return self.app_name
@property
def APP_VERSION(self) -> str:
return self.app_version
@property
def ENVIRONMENT(self) -> str:
return self.environment
@property
def DEBUG(self) -> bool:
return self.debug
@property
def APP_BASE_URL(self) -> str:
return self.app_base_url
@property
def API_HOST(self) -> str:
return self.api_host
@property
def API_PORT(self) -> int:
return self.api_port
@property
def DATABASE_URL(self) -> str:
return self.database_url
@property
def DATABASE_POOL_SIZE(self) -> int:
return self.database_pool_size
@property
def DATABASE_MAX_OVERFLOW(self) -> int:
return self.database_max_overflow
@property
def DATABASE_POOL_TIMEOUT(self) -> int:
return self.database_pool_timeout
@property
def DATABASE_POOL_RECYCLE(self) -> int:
return self.database_pool_recycle
@property
def USE_IN_MEMORY_DB(self) -> bool:
return self.use_in_memory_db
@property
def AUTO_CREATE_SCHEMA(self) -> bool:
return self.auto_create_schema
@property
def REDIS_URL(self) -> str:
return self.redis_url
@property
def ENABLE_REDIS_SESSIONS(self) -> bool:
return self.enable_redis_sessions
@property
def JWT_SECRET_KEY(self) -> Optional[str]:
return self.jwt_secret_key
@property
def JWT_ALGORITHM(self) -> str:
return self.jwt_algorithm
@property
def JWT_ACCESS_TOKEN_EXPIRE_MINUTES(self) -> int:
return self.jwt_access_token_expire_minutes
@property
def JWT_REFRESH_TOKEN_EXPIRE_DAYS(self) -> int:
return self.jwt_refresh_token_expire_days
@property
def ENABLE_EMAIL_DELIVERY(self) -> bool:
return self.enable_email_delivery
@property
def SMTP_HOST(self) -> str:
return self.smtp_host
@property
def SMTP_PORT(self) -> int:
return self.smtp_port
@property
def SMTP_USER(self) -> str:
return self.smtp_user
@property
def SMTP_PASSWORD(self) -> str:
return self.smtp_password
@property
def SMTP_FROM_EMAIL(self) -> str:
return self.smtp_from_email
@property
def SMTP_FROM_NAME(self) -> str:
return self.smtp_from_name
@property
def SMTP_USE_TLS(self) -> bool:
return self.smtp_use_tls
@property
def CELERY_BROKER_URL(self) -> str:
return self.celery_broker_url
@property
def CELERY_RESULT_BACKEND(self) -> str:
return self.celery_result_backend
@property
def OSS_ENDPOINT(self) -> str:
return self.oss_endpoint
@property
def OSS_ACCESS_KEY_ID(self) -> str:
return self.oss_access_key_id
@property
def OSS_ACCESS_KEY_SECRET(self) -> str:
return self.oss_access_key_secret
@property
def OSS_BUCKET_NAME(self) -> str:
return self.oss_bucket_name
@property
def OSS_DIRECT_UPLOAD_MAX_MB(self) -> int:
return self.oss_direct_upload_max_mb
@property
def OSS_DIRECT_UPLOAD_EXPIRE_SECONDS(self) -> int:
return self.oss_direct_upload_expire_seconds
@property
def CORS_ORIGINS_RAW(self) -> str:
return self.cors_origins_raw
@property
def CORS_ORIGINS(self) -> list[str]:
return self.cors_origins
@property
def RENDER_ENGINE(self) -> str:
return self.render_engine
_settings: Optional["Settings"] = None
def get_settings() -> "Settings":
"""获取 API 配置单例。
优先读取 APP_ENV 指定的环境文件(.env.{env}),不存在则读 .env。
"""
global _settings
if _settings is None:
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
if os.path.exists(env_file):
_settings = Settings(_env_file=env_file)
else:
_settings = Settings()
return _settings
from packages.config import APISettings as Settings
from packages.config import get_api_settings as get_settings
from packages.config import reload_settings_cache
# 模块级单例(向后兼容)
settings = get_settings()
# 暴露旧的 reload_settings 函数名
def reload_settings():
"""重新加载配置(测试用)。"""
reload_settings_cache()
global settings
settings = get_settings()
return settings
__all__ = ["Settings", "get_settings", "settings", "reload_settings"]
Regular → Executable
+10 -9
View File
@@ -1,19 +1,20 @@
"""Compatibility layer for the canonical API settings module.
"""向后兼容层 — 配置已统一到 packages.config。
Use `app.config` as the single source of truth for API configuration.
This module remains only for older imports during migration.
新代码请使用:
from packages.config import get_api_settings, APISettings
"""
from app.config import Settings as AppSettings
from app.config import get_settings, settings
from packages.config import APISettings as AppSettings
from packages.config import get_api_settings as get_settings
from packages.config import reload_settings_cache
def reload_settings() -> AppSettings:
"""Reload settings for tests and legacy callers."""
import app.config as canonical_config
"""重新加载配置(测试用)。"""
reload_settings_cache()
return get_settings()
canonical_config.settings = canonical_config.get_settings()
return canonical_config.settings
settings = get_settings()
__all__ = ["AppSettings", "get_settings", "reload_settings", "settings"]
+13 -52
View File
@@ -1,61 +1,22 @@
"""Worker 服务配置 — 继承 SharedSettings,只追加 Worker 特有字段
"""Worker 服务配置(向后兼容层)
通用配置(DB/Redis/Celery/OSS/CosyVoice/Doubao 等)统一在
packages/shared/config.py 的 SharedSettings 中定义,这里不重复。
统一配置已迁移到 packages.config.worker_settings。
新代码请使用:
from packages.config import WorkerSettings, get_worker_settings
"""
import os
from typing import Optional
from pydantic_settings import SettingsConfigDict
from packages.shared.config import SharedSettings
from packages.config import WorkerSettings, get_worker_settings, reload_settings_cache
class WorkerSettings(SharedSettings):
"""Worker 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 Worker 独有字段。
Celery broker/backend 使用继承的 celery_broker_url / celery_result_backend
历史上 Worker 使用 broker_url / result_backend 字段名,通过 property 别名兼容。
"""
# ── Worker 特有 ────────────────────────────────────────────────────
worker_name: str = "xiaoxia-saas-worker"
worker_concurrency: int = 4
worker_max_tasks_per_child: int = 1000
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── 向后兼容:Celery 字段名别名 ──────────────────────────────────
@property
def broker_url(self) -> str:
return self.celery_broker_url
@property
def result_backend(self) -> str:
return self.celery_result_backend
def get_settings() -> WorkerSettings:
"""获取 Worker 配置单例(向后兼容)。"""
return get_worker_settings()
_settings: Optional["WorkerSettings"] = None
def reload_settings() -> WorkerSettings:
"""重新加载配置(测试用)。"""
reload_settings_cache()
return get_worker_settings()
def get_settings() -> "WorkerSettings":
"""获取 Worker 配置单例。
优先读取 APP_ENV 指定的环境文件(.env.{env}),不存在则读 .env。
"""
global _settings
if _settings is None:
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
if os.path.exists(env_file):
_settings = WorkerSettings(_env_file=env_file)
else:
_settings = WorkerSettings()
return _settings
__all__ = ["WorkerSettings", "get_settings", "reload_settings"]