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>
135 lines
3.7 KiB
Python
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),
|
|
)
|