Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
Production frontend service.
Serves the built frontend static files on port 80 (configurable via
FRONTEND_PORT) and proxies API / WebSocket requests to backend services.
"""
import asyncio
import logging
import os
from pathlib import Path
import httpx
import websockets
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, Response
logger = logging.getLogger(__name__)
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
AGENT_SERVICE_URL = os.getenv("AGENT_SERVICE_URL", "http://localhost:8000")
RUNTIME_SERVICE_URL = os.getenv("RUNTIME_SERVICE_URL", "http://localhost:8003")
GATEWAY_WS_URL = os.getenv("GATEWAY_WS_URL", "")
app = FastAPI(title="EvoTraders Frontend")
async def _resolve_gateway_ws_url() -> str:
"""Resolve the Gateway WebSocket URL dynamically from runtime API."""
if GATEWAY_WS_URL:
return GATEWAY_WS_URL
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{RUNTIME_SERVICE_URL}/api/runtime/gateway/port")
data = resp.json()
if data.get("is_running") and data.get("port"):
url = f"ws://localhost:{data['port']}"
logger.info(f"[Frontend] Resolved gateway URL: {url}")
return url
except Exception as e:
logger.warning(f"[Frontend] Failed to resolve gateway port: {e}")
fallback = f"ws://localhost:{os.getenv('GATEWAY_PORT', '8765')}"
logger.info(f"[Frontend] Using fallback gateway URL: {fallback}")
return fallback
# ── API reverse proxy ────────────────────────────────────────────────
@app.api_route(
"/api/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
async def proxy_api(request: Request, path: str):
"""Forward /api/* requests to the appropriate backend service."""
target = RUNTIME_SERVICE_URL if path.startswith("runtime/") else AGENT_SERVICE_URL
body = await request.body()
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.request(
method=request.method,
url=f"{target}/api/{path}",
content=body,
headers={
k: v
for k, v in request.headers.items()
if k.lower() not in ("host", "transfer-encoding")
},
)
return Response(
content=resp.content,
status_code=resp.status_code,
headers=dict(resp.headers),
)
# ── WebSocket proxy ──────────────────────────────────────────────────
@app.websocket("/ws")
async def proxy_ws(ws: WebSocket):
"""Proxy WebSocket connections to the Gateway (port resolved dynamically)."""
gateway_url = await _resolve_gateway_ws_url()
await ws.accept()
upstream = None
try:
upstream = await websockets.asyncio.client.connect(gateway_url)
async def client_to_upstream():
try:
while True:
data = await ws.receive_text()
await upstream.send(data)
except WebSocketDisconnect:
pass
async def upstream_to_client():
try:
async for msg in upstream:
if isinstance(msg, str):
await ws.send_text(msg)
else:
await ws.send_bytes(msg)
except websockets.exceptions.ConnectionClosed:
pass
await asyncio.gather(client_to_upstream(), upstream_to_client())
except Exception as e:
logger.warning(f"[Frontend] WebSocket proxy error: {e}")
finally:
if upstream:
await upstream.close()
# ── Static files (SPA fallback) ─────────────────────────────────────
if FRONTEND_DIST.is_dir():
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve static files; fall back to index.html for SPA routing."""
file_path = FRONTEND_DIST / full_path
if full_path and file_path.is_file():
return FileResponse(file_path)
return FileResponse(FRONTEND_DIST / "index.html")