mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 16:22:27 +08:00
Initial commit
This commit is contained in:
3
src/config/__init__.py
Normal file
3
src/config/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .settings import Config, config
|
||||
|
||||
__all__ = ["Config", "config"]
|
||||
235
src/config/constants.py
Normal file
235
src/config/constants.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# Constants for better maintainability
|
||||
# ==============================================================================
|
||||
# 缓存相关常量
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
# 缓存 TTL(秒)
|
||||
class CacheTTL:
|
||||
"""缓存过期时间配置(秒)"""
|
||||
|
||||
# 用户缓存 - 用户信息变更较频繁
|
||||
USER = 60 # 1分钟
|
||||
|
||||
# Provider/Model 缓存 - 配置变更不频繁
|
||||
PROVIDER = 300 # 5分钟
|
||||
MODEL = 300 # 5分钟
|
||||
MODEL_MAPPING = 300 # 5分钟
|
||||
|
||||
# 缓存亲和性 - 对应 provider_api_key.cache_ttl_minutes 默认值
|
||||
CACHE_AFFINITY = 300 # 5分钟
|
||||
|
||||
# L1 本地缓存(用于减少 Redis 访问)
|
||||
L1_LOCAL = 3 # 3秒
|
||||
|
||||
# 并发锁 TTL - 防止死锁
|
||||
CONCURRENCY_LOCK = 600 # 10分钟
|
||||
|
||||
|
||||
# 缓存容量限制
|
||||
class CacheSize:
|
||||
"""缓存容量配置"""
|
||||
|
||||
# 默认 LRU 缓存大小
|
||||
DEFAULT = 1000
|
||||
|
||||
# ModelMapping 缓存(可能有较多别名)
|
||||
MODEL_MAPPING = 2000
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 并发和限流常量
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class ConcurrencyDefaults:
|
||||
"""并发控制默认值"""
|
||||
|
||||
# 自适应并发初始限制(保守值)
|
||||
INITIAL_LIMIT = 3
|
||||
|
||||
# 429错误后的冷却时间(分钟)- 在此期间不会增加并发限制
|
||||
COOLDOWN_AFTER_429_MINUTES = 5
|
||||
|
||||
# 探测间隔上限(分钟)- 用于长期探测策略
|
||||
MAX_PROBE_INTERVAL_MINUTES = 60
|
||||
|
||||
# === 基于滑动窗口的扩容参数 ===
|
||||
# 滑动窗口大小(采样点数量)
|
||||
UTILIZATION_WINDOW_SIZE = 20
|
||||
|
||||
# 滑动窗口时间范围(秒)- 只保留最近这段时间内的采样
|
||||
UTILIZATION_WINDOW_SECONDS = 120 # 2分钟
|
||||
|
||||
# 利用率阈值 - 窗口内平均利用率 >= 此值时考虑扩容
|
||||
UTILIZATION_THRESHOLD = 0.7 # 70%
|
||||
|
||||
# 高利用率采样比例 - 窗口内超过阈值的采样点比例 >= 此值时触发扩容
|
||||
HIGH_UTILIZATION_RATIO = 0.6 # 60% 的采样点高于阈值
|
||||
|
||||
# 最小采样数 - 窗口内至少需要这么多采样才能做出扩容决策
|
||||
MIN_SAMPLES_FOR_DECISION = 5
|
||||
|
||||
# 扩容步长 - 每次扩容增加的并发数
|
||||
INCREASE_STEP = 1
|
||||
|
||||
# 缩容乘数 - 遇到 429 时的缩容比例
|
||||
DECREASE_MULTIPLIER = 0.7
|
||||
|
||||
# 最大并发限制上限
|
||||
MAX_CONCURRENT_LIMIT = 100
|
||||
|
||||
# 最小并发限制下限
|
||||
MIN_CONCURRENT_LIMIT = 1
|
||||
|
||||
# === 探测性扩容参数 ===
|
||||
# 探测性扩容间隔(分钟)- 长时间无 429 且有流量时尝试扩容
|
||||
PROBE_INCREASE_INTERVAL_MINUTES = 30
|
||||
|
||||
# 探测性扩容最小请求数 - 在探测间隔内至少需要这么多请求
|
||||
PROBE_INCREASE_MIN_REQUESTS = 10
|
||||
|
||||
|
||||
class CircuitBreakerDefaults:
|
||||
"""熔断器配置默认值(滑动窗口 + 半开状态模式)
|
||||
|
||||
新的熔断器基于滑动窗口错误率,而不是累计健康度。
|
||||
支持半开状态,允许少量请求验证服务是否恢复。
|
||||
"""
|
||||
|
||||
# === 滑动窗口配置 ===
|
||||
# 滑动窗口大小(最近 N 次请求)
|
||||
WINDOW_SIZE = 20
|
||||
|
||||
# 滑动窗口时间范围(秒)- 只保留最近这段时间内的请求记录
|
||||
WINDOW_SECONDS = 300 # 5分钟
|
||||
|
||||
# 最小请求数 - 窗口内至少需要这么多请求才能做出熔断决策
|
||||
MIN_REQUESTS_FOR_DECISION = 5
|
||||
|
||||
# 错误率阈值 - 窗口内错误率超过此值时触发熔断
|
||||
ERROR_RATE_THRESHOLD = 0.5 # 50%
|
||||
|
||||
# === 半开状态配置 ===
|
||||
# 半开状态持续时间(秒)- 在此期间允许少量请求通过
|
||||
HALF_OPEN_DURATION_SECONDS = 30
|
||||
|
||||
# 半开状态成功阈值 - 达到此成功次数则关闭熔断器
|
||||
HALF_OPEN_SUCCESS_THRESHOLD = 3
|
||||
|
||||
# 半开状态失败阈值 - 达到此失败次数则重新打开熔断器
|
||||
HALF_OPEN_FAILURE_THRESHOLD = 2
|
||||
|
||||
# === 熔断恢复配置 ===
|
||||
# 初始探测间隔(秒)- 熔断后多久进入半开状态
|
||||
INITIAL_RECOVERY_SECONDS = 30
|
||||
|
||||
# 探测间隔退避倍数
|
||||
RECOVERY_BACKOFF_MULTIPLIER = 2
|
||||
|
||||
# 最大探测间隔(秒)
|
||||
MAX_RECOVERY_SECONDS = 300 # 5分钟
|
||||
|
||||
# === 旧参数(向后兼容,仍用于展示健康度)===
|
||||
# 成功时健康度增量
|
||||
SUCCESS_INCREMENT = 0.15
|
||||
|
||||
# 失败时健康度减量
|
||||
FAILURE_DECREMENT = 0.03
|
||||
|
||||
# 探测成功后的快速恢复健康度
|
||||
PROBE_RECOVERY_SCORE = 0.5
|
||||
|
||||
|
||||
class AdaptiveReservationDefaults:
|
||||
"""动态预留比例配置默认值
|
||||
|
||||
动态预留机制根据学习置信度和负载自动调整缓存用户预留比例,
|
||||
解决固定 30% 预留在学习初期和负载变化时的不适应问题。
|
||||
"""
|
||||
|
||||
# 探测阶段配置
|
||||
PROBE_PHASE_REQUESTS = 100 # 探测阶段请求数阈值
|
||||
PROBE_RESERVATION = 0.1 # 探测阶段预留比例(10%)
|
||||
|
||||
# 稳定阶段配置
|
||||
STABLE_MIN_RESERVATION = 0.1 # 稳定阶段最小预留(10%)
|
||||
STABLE_MAX_RESERVATION = 0.35 # 稳定阶段最大预留(35%)
|
||||
|
||||
# 置信度计算参数
|
||||
SUCCESS_COUNT_FOR_FULL_CONFIDENCE = 50 # 连续成功多少次达到满置信
|
||||
COOLDOWN_HOURS_FOR_FULL_CONFIDENCE = 24 # 429后多少小时达到满置信
|
||||
|
||||
# 负载阈值
|
||||
LOW_LOAD_THRESHOLD = 0.5 # 低负载阈值(50%)
|
||||
HIGH_LOAD_THRESHOLD = 0.8 # 高负载阈值(80%)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 超时和重试常量
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class TimeoutDefaults:
|
||||
"""超时配置默认值(秒)"""
|
||||
|
||||
# HTTP 请求默认超时
|
||||
HTTP_REQUEST = 300 # 5分钟
|
||||
|
||||
# 数据库连接池获取超时
|
||||
DB_POOL = 30
|
||||
|
||||
# Redis 操作超时
|
||||
REDIS_OPERATION = 5
|
||||
|
||||
|
||||
class RetryDefaults:
|
||||
"""重试配置默认值"""
|
||||
|
||||
# 最大重试次数
|
||||
MAX_RETRIES = 3
|
||||
|
||||
# 重试基础延迟(秒)
|
||||
BASE_DELAY = 1.0
|
||||
|
||||
# 重试延迟倍数(指数退避)
|
||||
DELAY_MULTIPLIER = 2.0
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 消息格式常量
|
||||
# ==============================================================================
|
||||
|
||||
# 角色常量
|
||||
ROLE_USER = "user"
|
||||
ROLE_ASSISTANT = "assistant"
|
||||
ROLE_SYSTEM = "system"
|
||||
ROLE_TOOL = "tool"
|
||||
|
||||
# 内容类型常量
|
||||
CONTENT_TEXT = "text"
|
||||
CONTENT_IMAGE = "image"
|
||||
CONTENT_TOOL_USE = "tool_use"
|
||||
CONTENT_TOOL_RESULT = "tool_result"
|
||||
|
||||
# 工具常量
|
||||
TOOL_FUNCTION = "function"
|
||||
|
||||
# 停止原因常量
|
||||
STOP_END_TURN = "end_turn"
|
||||
STOP_MAX_TOKENS = "max_tokens"
|
||||
STOP_TOOL_USE = "tool_use"
|
||||
STOP_ERROR = "error"
|
||||
|
||||
# 事件类型常量
|
||||
EVENT_MESSAGE_START = "message_start"
|
||||
EVENT_MESSAGE_STOP = "message_stop"
|
||||
EVENT_MESSAGE_DELTA = "message_delta"
|
||||
EVENT_CONTENT_BLOCK_START = "content_block_start"
|
||||
EVENT_CONTENT_BLOCK_STOP = "content_block_stop"
|
||||
EVENT_CONTENT_BLOCK_DELTA = "content_block_delta"
|
||||
EVENT_PING = "ping"
|
||||
|
||||
# Delta类型常量
|
||||
DELTA_TEXT = "text_delta"
|
||||
DELTA_INPUT_JSON = "input_json_delta"
|
||||
259
src/config/settings.py
Normal file
259
src/config/settings.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
服务器配置
|
||||
从环境变量或 .env 文件加载配置
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 尝试加载 .env 文件
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = Path(".env")
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
except ImportError:
|
||||
# 如果没有安装 python-dotenv,仍然可以从环境变量读取
|
||||
pass
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self) -> None:
|
||||
# 服务器配置
|
||||
self.host = os.getenv("HOST", "0.0.0.0")
|
||||
self.port = int(os.getenv("PORT", "8084"))
|
||||
self.log_level = os.getenv("LOG_LEVEL", "INFO")
|
||||
self.worker_processes = int(
|
||||
os.getenv("WEB_CONCURRENCY", os.getenv("GUNICORN_WORKERS", "4"))
|
||||
)
|
||||
|
||||
# PostgreSQL 连接池计算相关配置
|
||||
# PG_MAX_CONNECTIONS: PostgreSQL 的 max_connections 设置(默认 100)
|
||||
# PG_RESERVED_CONNECTIONS: 为其他应用/管理工具预留的连接数(默认 10)
|
||||
self.pg_max_connections = int(os.getenv("PG_MAX_CONNECTIONS", "100"))
|
||||
self.pg_reserved_connections = int(os.getenv("PG_RESERVED_CONNECTIONS", "10"))
|
||||
|
||||
# 数据库配置 - 延迟验证,支持测试环境覆盖
|
||||
self._database_url = os.getenv("DATABASE_URL")
|
||||
|
||||
# JWT配置
|
||||
self.jwt_secret_key = os.getenv("JWT_SECRET_KEY", None)
|
||||
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
self.jwt_expiration_hours = int(os.getenv("JWT_EXPIRATION_HOURS", "24"))
|
||||
|
||||
# 加密密钥配置(独立于JWT密钥,用于敏感数据加密)
|
||||
self.encryption_key = os.getenv("ENCRYPTION_KEY", None)
|
||||
|
||||
# 环境配置 - 智能检测
|
||||
# Docker 部署默认为生产环境,本地开发默认为开发环境
|
||||
is_docker = (
|
||||
os.path.exists("/.dockerenv")
|
||||
or os.environ.get("DOCKER_CONTAINER", "false").lower() == "true"
|
||||
)
|
||||
default_env = "production" if is_docker else "development"
|
||||
self.environment = os.getenv("ENVIRONMENT", default_env)
|
||||
|
||||
# Redis 依赖策略(生产默认必需,开发默认可选,可通过 REDIS_REQUIRED 覆盖)
|
||||
redis_required_env = os.getenv("REDIS_REQUIRED")
|
||||
if redis_required_env is None:
|
||||
self.require_redis = self.environment not in {"development", "test", "testing"}
|
||||
else:
|
||||
self.require_redis = redis_required_env.lower() == "true"
|
||||
|
||||
# CORS配置 - 使用环境变量配置允许的源
|
||||
# 格式: 逗号分隔的域名列表,如 "http://localhost:3000,https://example.com"
|
||||
cors_origins = os.getenv("CORS_ORIGINS", "")
|
||||
if cors_origins:
|
||||
self.cors_origins = [
|
||||
origin.strip() for origin in cors_origins.split(",") if origin.strip()
|
||||
]
|
||||
else:
|
||||
# 默认: 开发环境允许本地前端,生产环境不允许任何跨域
|
||||
if self.environment == "development":
|
||||
self.cors_origins = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:5173", # Vite 默认端口
|
||||
"http://127.0.0.1:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
]
|
||||
else:
|
||||
# 生产环境默认不允许跨域,必须显式配置
|
||||
self.cors_origins = []
|
||||
|
||||
# CORS是否允许凭证(Cookie/Authorization header)
|
||||
# 注意: allow_credentials=True 时不能使用 allow_origins=["*"]
|
||||
self.cors_allow_credentials = os.getenv("CORS_ALLOW_CREDENTIALS", "true").lower() == "true"
|
||||
|
||||
# 管理员账户配置(用于初始化)
|
||||
self.admin_email = os.getenv("ADMIN_EMAIL", "admin@localhost")
|
||||
self.admin_username = os.getenv("ADMIN_USERNAME", "admin")
|
||||
|
||||
# 管理员密码 - 必须在环境变量中设置
|
||||
admin_password_env = os.getenv("ADMIN_PASSWORD")
|
||||
if admin_password_env:
|
||||
self.admin_password = admin_password_env
|
||||
else:
|
||||
# 未设置密码,启动时会报错
|
||||
self.admin_password = ""
|
||||
self._missing_admin_password = True
|
||||
|
||||
# API Key 配置
|
||||
self.api_key_prefix = os.getenv("API_KEY_PREFIX", "sk")
|
||||
|
||||
# LLM API 速率限制配置(每分钟请求数)
|
||||
self.llm_api_rate_limit = int(os.getenv("LLM_API_RATE_LIMIT", "100"))
|
||||
self.public_api_rate_limit = int(os.getenv("PUBLIC_API_RATE_LIMIT", "60"))
|
||||
|
||||
# 异常处理配置
|
||||
# 设置为 True 时,ProxyException 会传播到路由层以便记录 provider_request_headers
|
||||
# 设置为 False 时,使用全局异常处理器统一处理
|
||||
self.propagate_provider_exceptions = os.getenv(
|
||||
"PROPAGATE_PROVIDER_EXCEPTIONS", "true"
|
||||
).lower() == "true"
|
||||
|
||||
# 数据库连接池配置 - 智能自动调整
|
||||
# 系统会根据 Worker 数量和 PostgreSQL 限制自动计算安全值
|
||||
self.db_pool_size = int(os.getenv("DB_POOL_SIZE") or self._auto_pool_size())
|
||||
self.db_max_overflow = int(os.getenv("DB_MAX_OVERFLOW") or self._auto_max_overflow())
|
||||
self.db_pool_timeout = int(os.getenv("DB_POOL_TIMEOUT", "60"))
|
||||
self.db_pool_recycle = int(os.getenv("DB_POOL_RECYCLE", "3600"))
|
||||
self.db_pool_warn_threshold = int(os.getenv("DB_POOL_WARN_THRESHOLD", "70"))
|
||||
|
||||
# 验证连接池配置
|
||||
self._validate_pool_config()
|
||||
|
||||
def _auto_pool_size(self) -> int:
|
||||
"""
|
||||
智能计算连接池大小 - 根据 Worker 数量和 PostgreSQL 限制计算
|
||||
|
||||
公式: (pg_max_connections - reserved) / workers / 2
|
||||
除以 2 是因为还要预留 max_overflow 的空间
|
||||
"""
|
||||
available_connections = self.pg_max_connections - self.pg_reserved_connections
|
||||
# 每个 Worker 可用的连接数(pool_size + max_overflow)
|
||||
per_worker_total = available_connections // max(self.worker_processes, 1)
|
||||
# pool_size 取总数的一半,另一半留给 overflow
|
||||
pool_size = max(per_worker_total // 2, 5) # 最小 5 个连接
|
||||
return min(pool_size, 30) # 最大 30 个连接
|
||||
|
||||
def _auto_max_overflow(self) -> int:
|
||||
"""智能计算最大溢出连接数 - 与 pool_size 相同"""
|
||||
return self.db_pool_size
|
||||
|
||||
def _validate_pool_config(self) -> None:
|
||||
"""验证连接池配置是否安全"""
|
||||
total_per_worker = self.db_pool_size + self.db_max_overflow
|
||||
total_all_workers = total_per_worker * self.worker_processes
|
||||
safe_limit = self.pg_max_connections - self.pg_reserved_connections
|
||||
|
||||
if total_all_workers > safe_limit:
|
||||
# 记录警告(不抛出异常,避免阻止启动)
|
||||
self._pool_config_warning = (
|
||||
f"[WARN] 数据库连接池配置可能超过 PostgreSQL 限制: "
|
||||
f"{self.worker_processes} workers x {total_per_worker} connections = "
|
||||
f"{total_all_workers} > {safe_limit} (pg_max_connections - reserved). "
|
||||
f"建议调整 DB_POOL_SIZE 或 PG_MAX_CONNECTIONS 环境变量。"
|
||||
)
|
||||
else:
|
||||
self._pool_config_warning = None
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""
|
||||
数据库 URL(延迟验证)
|
||||
|
||||
在测试环境中可以通过依赖注入覆盖,而不会在导入时崩溃
|
||||
"""
|
||||
if not self._database_url:
|
||||
raise ValueError(
|
||||
"DATABASE_URL environment variable is required. "
|
||||
"Example: postgresql://username:password@localhost:5432/dbname"
|
||||
)
|
||||
return self._database_url
|
||||
|
||||
@database_url.setter
|
||||
def database_url(self, value: str):
|
||||
"""允许在测试中设置数据库 URL"""
|
||||
self._database_url = value
|
||||
|
||||
def log_startup_warnings(self) -> None:
|
||||
"""
|
||||
记录启动时的安全警告
|
||||
这个方法应该在 logger 初始化后调用
|
||||
"""
|
||||
from src.core.logger import logger
|
||||
|
||||
# 连接池配置警告
|
||||
if hasattr(self, "_pool_config_warning") and self._pool_config_warning:
|
||||
logger.warning(self._pool_config_warning)
|
||||
|
||||
# 管理员密码检查(必须在环境变量中设置)
|
||||
if hasattr(self, "_missing_admin_password") and self._missing_admin_password:
|
||||
logger.error("必须设置 ADMIN_PASSWORD 环境变量!")
|
||||
raise ValueError("ADMIN_PASSWORD environment variable must be set!")
|
||||
|
||||
# JWT 密钥警告
|
||||
if not self.jwt_secret_key:
|
||||
if self.environment == "production":
|
||||
logger.error(
|
||||
"生产环境未设置 JWT_SECRET_KEY! 这是严重的安全漏洞。"
|
||||
"使用 'python generate_keys.py' 生成安全密钥。"
|
||||
)
|
||||
else:
|
||||
logger.warning("JWT_SECRET_KEY 未设置,将使用默认密钥(仅限开发环境)")
|
||||
|
||||
# 加密密钥警告
|
||||
if not self.encryption_key and self.environment != "production":
|
||||
logger.warning(
|
||||
"ENCRYPTION_KEY 未设置,使用开发环境默认密钥。生产环境必须设置。"
|
||||
)
|
||||
|
||||
# CORS 配置警告(生产环境)
|
||||
if self.environment == "production" and not self.cors_origins:
|
||||
logger.warning("生产环境 CORS 未配置,前端将无法访问 API。请设置 CORS_ORIGINS。")
|
||||
|
||||
def validate_security_config(self) -> list[str]:
|
||||
"""
|
||||
验证安全配置,返回错误列表
|
||||
生产环境会阻止启动,开发环境仅警告
|
||||
|
||||
Returns:
|
||||
错误消息列表(空列表表示验证通过)
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
if self.environment == "production":
|
||||
# 生产环境必须设置 JWT 密钥
|
||||
if not self.jwt_secret_key:
|
||||
errors.append(
|
||||
"JWT_SECRET_KEY must be set in production. "
|
||||
"Use 'python generate_keys.py' to generate a secure key."
|
||||
)
|
||||
elif len(self.jwt_secret_key) < 32:
|
||||
errors.append("JWT_SECRET_KEY must be at least 32 characters in production.")
|
||||
|
||||
# 生产环境必须设置加密密钥
|
||||
if not self.encryption_key:
|
||||
errors.append(
|
||||
"ENCRYPTION_KEY must be set in production. "
|
||||
"Use 'python generate_keys.py' to generate a secure key."
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
def __repr__(self):
|
||||
"""配置信息字符串表示"""
|
||||
return f"""
|
||||
Configuration:
|
||||
Server: {self.host}:{self.port}
|
||||
Log Level: {self.log_level}
|
||||
Environment: {self.environment}
|
||||
"""
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
config = Config()
|
||||
|
||||
# 在调试模式下记录配置(延迟到日志系统初始化后)
|
||||
# 这个配置信息会在应用启动时通过日志系统输出
|
||||
Reference in New Issue
Block a user