Files
evotraders/backend/config/env_config.py
cillin 3926a6bd07 feat: 架构修复 - P0/P1 问题全面修复
P0 修复:
- runtimeStore: 添加缺失的 lastDayHistory 字段
- Gateway/RuntimeService: 状态同步改为内存优先,消除 glob 竞态
- App.jsx: 从 3075 行重构到 ~500 行,提取 8 个独立文件

P1 修复:
- CORS: 4 个服务改为从环境变量读取允许 origins
- MarketStore: 改为模块级单例模式
- Domain 层: 删除 trading thin wrapper,保留 news 真实逻辑
- 测试: 补齐 77 个 gateway/runtime 测试

新增文件:
- backend/tests/test_gateway.py (43 tests)
- frontend/src/hooks/useWebSocketHandler.js
- frontend/src/hooks/useStockRequestCallbacks.js
- frontend/src/hooks/useAgentCallbacks.js
- frontend/src/hooks/useRuntimeCallbacks.js
- frontend/src/hooks/useWatchlistCallbacks.js
- frontend/src/components/TickerBar.jsx
- frontend/src/components/HeaderRight.jsx
- frontend/src/components/ChartTabs.jsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 18:45:57 +08:00

135 lines
3.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Environment config helpers with light validation and normalization."""
import os
import warnings
from dataclasses import dataclass
from typing import Optional
FALSEY_ENV_VALUES = {"0", "false", "no", "off"}
PROVIDER_ALIASES = {
"openai_compatible": "OPENAI",
"openai_compat": "OPENAI",
"claude": "ANTHROPIC",
"google": "GEMINI",
"vertex": "GEMINI",
"vertexai": "GEMINI",
}
# Default dev CORS origins (localhost variants used by common dev servers)
_LOCALHOST_ORIGINS = [
"http://localhost:5173",
"http://localhost:3000",
"http://localhost:8000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:8000",
]
def get_cors_origins() -> list[str]:
"""Get CORS allowed origins from environment.
Reads CORS_ALLOWED_ORIGINS env var (comma-separated).
Falls back to localhost dev origins if not set.
Warns if "*" is configured (only acceptable for local dev).
"""
origins = get_env_list("CORS_ALLOWED_ORIGINS", default=[])
if origins:
if "*" in origins:
warnings.warn(
"CORS_ALLOWED_ORIGINS contains '*' — this allows any origin. "
"Only use in local development, never in production.",
UserWarning,
)
return origins
# Fallback: local dev only
return _LOCALHOST_ORIGINS
@dataclass(frozen=True)
class AgentModelConfig:
"""Resolved model config for one agent."""
model_name: str
provider: str
def _get_env_raw(key: str) -> Optional[str]:
value = os.getenv(key)
if value is None:
return None
value = value.strip()
return value or None
def get_env_str(key: str, default: str = "") -> str:
"""Get trimmed string from env."""
value = _get_env_raw(key)
return value if value is not None else default
def get_env_list(key: str, default: list = None) -> list:
"""Get comma-separated list from env."""
value = _get_env_raw(key)
if not value:
return default or []
return [item.strip() for item in value.split(",") if item.strip()]
def get_env_float(key: str, default: float = 0.0) -> float:
"""Get float from env."""
value = _get_env_raw(key)
if value is None:
return default
try:
return float(value)
except ValueError:
return default
def get_env_int(key: str, default: int = 0) -> int:
"""Get int from env."""
value = _get_env_raw(key)
if value is None:
return default
try:
return int(value)
except ValueError:
return default
def get_env_bool(key: str, default: bool = False) -> bool:
"""Parse common truthy/falsey env values."""
value = _get_env_raw(key)
if value is None:
return default
return value.lower() not in FALSEY_ENV_VALUES
def canonicalize_model_provider(provider: Optional[str]) -> str:
"""Normalize provider labels to stable uppercase names."""
if not provider:
return "OPENAI"
normalized = provider.strip().lower().replace("-", "_")
normalized = PROVIDER_ALIASES.get(normalized, normalized)
return normalized.upper()
def get_agent_model_config(agent_id: str) -> AgentModelConfig:
"""Resolve model config with agent-specific override and global fallback."""
agent_key = agent_id.upper().replace("-", "_")
model_name = get_env_str(f"AGENT_{agent_key}_MODEL_NAME")
provider = get_env_str(f"AGENT_{agent_key}_MODEL_PROVIDER")
if not model_name:
model_name = get_env_str("MODEL_NAME", "gpt-4o")
if not provider:
provider = get_env_str("MODEL_PROVIDER", "OPENAI")
return AgentModelConfig(
model_name=model_name,
provider=canonicalize_model_provider(provider),
)