AgentEvalTool/backend/agenteval/web/app.py
sinohqb 739d586aec feat(backend): v0.4 triggered_by tracking, login gate, compare guard, dashboard stats
- EvalRun.triggered_by 全链路(manual/ai_assistant/cli)+ 迁移 b7d4e6f81c22
- 标准 agenteval-run SKILL.md 纳入版本管理,deploy 脚本同步 + API Key 注入
- 简单登录:AGENTEVAL_ADMIN_PASSWORD + HMAC 会话 token,require_auth 双凭据
- 对比报告限同场景(400)+ 空 results 误判修复
- /api/stats/dashboard 扩展聚合;/api/runs 返回场景/对象名
- 测试 218 → 232
2026-07-28 17:40:54 +08:00

107 lines
3.5 KiB
Python

"""FastAPI web backend for AgentEvalTool."""
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from agenteval.config import get_settings
from agenteval.storage.db import init_db
from agenteval.version import get_build_info, get_version
from agenteval.web.deps import require_api_key
from agenteval.web.routers import auth, files, model_configs, proxy, reports, runs, scenarios, stats, targets
from agenteval.web.websocket import ws_manager
@asynccontextmanager
async def lifespan(_: FastAPI):
init_db()
yield
app = FastAPI(
title="AgentEvalTool",
description="智能体质量评估工具集平台 Web API",
version=get_version(),
lifespan=lifespan,
)
settings = get_settings()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_api_deps = [Depends(require_api_key)]
# Login endpoints must stay open — they are how the client obtains credentials.
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(targets.router, prefix="/api/targets", tags=["targets"], dependencies=_api_deps)
app.include_router(scenarios.router, prefix="/api/scenarios", tags=["scenarios"], dependencies=_api_deps)
app.include_router(runs.router, prefix="/api/runs", tags=["runs"], dependencies=_api_deps)
app.include_router(reports.router, prefix="/api/reports", tags=["reports"], dependencies=_api_deps)
app.include_router(stats.router, prefix="/api/stats", tags=["stats"], dependencies=_api_deps)
app.include_router(files.router, prefix="/api/files", tags=["files"], dependencies=_api_deps)
app.include_router(
model_configs.router,
prefix="/api/model-configs",
tags=["model-configs"],
dependencies=_api_deps,
)
@app.websocket("/openclaw")
async def openclaw_ws_root(websocket: WebSocket):
url = proxy.get_ws_upstream()
if websocket.query_params:
url += f"?{websocket.query_params}"
await proxy.ws_bridge(websocket, url)
@app.websocket("/openclaw/{path:path}")
async def openclaw_ws_proxy(websocket: WebSocket, path: str):
url = f"{proxy.get_ws_upstream()}/{path}"
if websocket.query_params:
url += f"?{websocket.query_params}"
await proxy.ws_bridge(websocket, url)
app.include_router(proxy.router, prefix="/openclaw", tags=["proxy"])
_default_dist = Path(__file__).resolve().parent.parent.parent.parent / "frontend" / "web" / "dist"
WEB_DIST = Path(settings.frontend_dist_path) if settings.frontend_dist_path else _default_dist
if WEB_DIST.exists():
from fastapi.staticfiles import StaticFiles
app.mount("/assets", StaticFiles(directory=WEB_DIST / "assets"), name="assets")
@app.get("/api/health", tags=["health"])
def health() -> dict:
return {"status": "ok", **get_build_info()}
@app.websocket("/ws/runs/{run_id}")
async def websocket_run_progress(websocket: WebSocket, run_id: str):
await ws_manager.connect(run_id, websocket)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
ws_manager.disconnect(run_id, websocket)
@app.get("/{full_path:path}")
def serve_spa(full_path: str):
"""Serve the React SPA for all non-API routes."""
index_file = WEB_DIST / "index.html"
if index_file.exists():
return FileResponse(index_file)
return JSONResponse({"detail": "frontend not built"}, status_code=404)