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>
This commit is contained in:
2026-03-23 18:45:57 +08:00
parent 80256a4079
commit 3926a6bd07
21 changed files with 4280 additions and 2790 deletions

View File

@@ -302,36 +302,28 @@ def _start_gateway_process(
@router.get("/context", response_model=RunContextResponse)
async def get_run_context() -> RunContextResponse:
"""Return the most recent run context."""
snapshot_path = PROJECT_ROOT.glob("runs/*/state/runtime_state.json")
snapshots = sorted(snapshot_path, key=lambda p: p.stat().st_mtime, reverse=True)
if not snapshots:
"""Return the current run context from in-memory state (avoids glob race condition)."""
manager = _runtime_state.runtime_manager
if manager is None or manager.context is None:
raise HTTPException(status_code=404, detail="No run context available")
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
context = latest.get("context")
if context is None:
raise HTTPException(status_code=404, detail="Run context is not ready")
context = manager.context
return RunContextResponse(
config_name=context["config_name"],
run_dir=context["run_dir"],
bootstrap_values=context["bootstrap_values"],
config_name=context.config_name,
run_dir=str(context.run_dir),
bootstrap_values=context.bootstrap_values,
)
@router.get("/agents", response_model=RuntimeAgentsResponse)
async def get_runtime_agents() -> RuntimeAgentsResponse:
"""Return agent states from the most recent run."""
snapshot_path = PROJECT_ROOT.glob("runs/*/state/runtime_state.json")
snapshots = sorted(snapshot_path, key=lambda p: p.stat().st_mtime, reverse=True)
if not snapshots:
"""Return agent states from the in-memory runtime manager (avoids glob race condition)."""
manager = _runtime_state.runtime_manager
if manager is None:
raise HTTPException(status_code=404, detail="No runtime state available")
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
agents = latest.get("agents", [])
snapshot = manager.build_snapshot()
agents = snapshot.get("agents", [])
return RuntimeAgentsResponse(
agents=[RuntimeAgentState(**a) for a in agents]
@@ -340,15 +332,13 @@ async def get_runtime_agents() -> RuntimeAgentsResponse:
@router.get("/events", response_model=RuntimeEventsResponse)
async def get_runtime_events() -> RuntimeEventsResponse:
"""Return events from the most recent run."""
snapshot_path = PROJECT_ROOT.glob("runs/*/state/runtime_state.json")
snapshots = sorted(snapshot_path, key=lambda p: p.stat().st_mtime, reverse=True)
if not snapshots:
"""Return events from the in-memory runtime manager (avoids glob race condition)."""
manager = _runtime_state.runtime_manager
if manager is None:
raise HTTPException(status_code=404, detail="No runtime state available")
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
events = latest.get("events", [])
snapshot = manager.build_snapshot()
events = snapshot.get("events", [])
return RuntimeEventsResponse(
events=[RuntimeEvent(**e) for e in events]
@@ -362,15 +352,10 @@ async def get_gateway_status() -> GatewayStatusResponse:
run_id = None
if is_running:
# Try to find run_id from runtime state
snapshot_path = PROJECT_ROOT.glob("runs/*/state/runtime_state.json")
snapshots = sorted(snapshot_path, key=lambda p: p.stat().st_mtime, reverse=True)
if snapshots:
try:
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
run_id = latest.get("context", {}).get("config_name")
except Exception as e:
logger.warning(f"Failed to parse latest snapshot: {e}")
# Get run_id from in-memory runtime manager (avoids glob race condition)
manager = _runtime_state.runtime_manager
if manager is not None and manager.context is not None:
run_id = manager.context.config_name
return GatewayStatusResponse(
is_running=is_running,
@@ -404,8 +389,28 @@ def _build_gateway_ws_url(request: Request, port: int) -> str:
return f"{ws_scheme}://{host}:{port}"
def _load_latest_runtime_snapshot() -> Dict[str, Any]:
"""Load the latest persisted runtime snapshot."""
def _get_current_runtime_context() -> Dict[str, Any]:
"""Return the active runtime context from the in-memory manager (avoids glob race condition).
Falls back to file-based lookup only when the in-memory manager is not available
(e.g., after a service restart). File-based lookup is deprecated and exists
only for backward compatibility.
"""
if not _is_gateway_running():
raise HTTPException(status_code=404, detail="No runtime is currently running")
# Primary: use in-memory manager (always correct for current process)
manager = _runtime_state.runtime_manager
if manager is not None and manager.context is not None:
ctx = manager.context
return {
"config_name": ctx.config_name,
"run_dir": str(ctx.run_dir),
"bootstrap_values": ctx.bootstrap_values,
}
# Deprecated fallback: scan filesystem (only for backward compatibility
# after service restart without a restart of the runtime itself)
snapshots = sorted(
PROJECT_ROOT.glob("runs/*/state/runtime_state.json"),
key=lambda p: p.stat().st_mtime,
@@ -413,14 +418,7 @@ def _load_latest_runtime_snapshot() -> Dict[str, Any]:
)
if not snapshots:
raise HTTPException(status_code=404, detail="No runtime information available")
return json.loads(snapshots[0].read_text(encoding="utf-8"))
def _get_current_runtime_context() -> Dict[str, Any]:
"""Return the active runtime context from the latest snapshot."""
if not _is_gateway_running():
raise HTTPException(status_code=404, detail="No runtime is currently running")
latest = _load_latest_runtime_snapshot()
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
context = latest.get("context") or {}
if not context.get("config_name"):
raise HTTPException(status_code=404, detail="No runtime context available")
@@ -663,15 +661,8 @@ async def get_current_runtime():
if not _is_gateway_running():
raise HTTPException(status_code=404, detail="No runtime is currently running")
# Find latest runtime state
snapshot_path = PROJECT_ROOT.glob("runs/*/state/runtime_state.json")
snapshots = sorted(snapshot_path, key=lambda p: p.stat().st_mtime, reverse=True)
if not snapshots:
raise HTTPException(status_code=404, detail="No runtime information available")
latest = json.loads(snapshots[0].read_text(encoding="utf-8"))
context = latest.get("context", {})
# Get context from in-memory manager (avoids glob race condition)
context = _get_current_runtime_context()
return {
"run_id": context.get("config_name"),