AgentEvalTool/backend/agenteval/web/routers/proxy.py
sinohqb a77cd83e6a v0.2.0-dev: 文件管理 + 页面布局统一 + 6 个 bug 修复
## 新增功能
- 文件管理模块:分类树 + 文件上传/下载/删除
- 文件上传支持拖拽(Dragger)+ 手动上传(customRequest 模式)

## 页面布局统一(参照评测执行页)
- 仪表盘/评测对象/评测场景/评测报告 全部改为全高 flex 布局
- 统一内联页头样式(h2 + 竖线分隔 + 描述)
- 表格撑满高度、overflow 处理
- 每页添加刷新按钮

## Bug 修复
- 分类树操作按钮 hover 不可见(CSS 规则缺失)
- 文件上传失败(multipart boundary 缺失)
- LLM API 响应 content blocks 数组格式支持(_extract_content_from_api_response)
- response_time_max_ms 被静默忽略(隐式规则传空 params)
- 空 messages 导致 IndexError 崩溃
- poll_reply 异常中止整个 run(缺 try/catch)
- engine finally 未关闭 session
- 3 个页面 UTC 时间戳解析偏差 8 小时

## 后端
- EvalEngine: poll_reply 异常保护、空 dialog 保护、session 关闭
- LLM API 响应解析支持 content-block-array 格式
- 隐式 response_time 规则正确传递 max_ms 参数

## 前端
- api.ts: 移除手动 Content-Type(让浏览器自动添加 boundary)
- Files.tsx: customRequest 替代 beforeUpload、布局优化
- index.css: 分类树 hover 规则
- Targets/Scenarios/Home/Reports: 全高布局改造
- 3 个页面时间戳改用 formatDateTime()(修复 UTC 偏差)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 15:25:22 +08:00

145 lines
4.7 KiB
Python

"""Reverse proxy for OpenClaw, rewriting headers for iframe embedding.
OpenClaw is configured with trusted-proxy auth mode, so the proxy sends
an x-forwarded-user header instead of a Bearer token. All connection
parameters come from :mod:`agenteval.config.settings` — no hardcoded hosts.
"""
import asyncio
import json
import httpx
import websockets
from fastapi import APIRouter, Request, WebSocket
from fastapi.responses import StreamingResponse
from agenteval.config import get_settings
def get_http_upstream() -> str:
return get_settings().openclaw_upstream
def get_ws_upstream() -> str:
return get_settings().openclaw_ws_upstream
def get_proxy_user() -> str:
return get_settings().openclaw_proxy_user
def get_auth_token() -> str:
return get_settings().openclaw_auth_token
_client = httpx.AsyncClient(follow_redirects=True, timeout=60.0)
router = APIRouter()
def _rewrite_headers(headers: httpx.Headers) -> dict[str, str]:
"""Strip iframe-blocking headers and rewrite CSP."""
result: dict[str, str] = {}
for key, value in headers.items():
lower = key.lower()
if lower in ("x-frame-options", "transfer-encoding", "content-encoding", "content-length"):
continue
if lower == "content-security-policy":
value = value.replace("frame-ancestors 'none'", "frame-ancestors 'self'")
value = value.replace("script-src 'self'", "script-src 'self' 'unsafe-inline'")
result[key] = value
return result
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def http_proxy(request: Request, path: str):
url = f"{get_http_upstream()}/{path}"
if request.query_params:
url += f"?{request.query_params}"
body = await request.body()
headers = dict(request.headers)
headers.pop("host", None)
headers.pop("referer", None)
headers.pop("origin", None)
headers["x-forwarded-user"] = get_proxy_user()
headers["authorization"] = f"Bearer {get_auth_token()}"
resp = await _client.request(
method=request.method,
url=url,
content=body,
headers=headers,
)
return StreamingResponse(
content=iter([resp.content]),
status_code=resp.status_code,
headers=_rewrite_headers(resp.headers),
)
async def ws_bridge(client_ws: WebSocket, upstream_url: str):
"""Bidirectional WebSocket bridge between client and upstream."""
await client_ws.accept()
settings = get_settings()
auth_token = settings.openclaw_auth_token
connect_kwargs: dict = {
"additional_headers": {
"x-forwarded-user": settings.openclaw_proxy_user,
"authorization": f"Bearer {auth_token}",
},
"max_size": 10 * 1024 * 1024,
}
if settings.openclaw_ws_origin:
connect_kwargs["origin"] = settings.openclaw_ws_origin
try:
async with websockets.connect(upstream_url, **connect_kwargs) as upstream_ws:
async def client_to_upstream():
try:
while True:
msg = await client_ws.receive_text()
try:
payload = json.loads(msg)
if (
isinstance(payload, dict)
and payload.get("method") == "connect.authenticate"
and isinstance(payload.get("payload"), dict)
):
params = payload["payload"].setdefault("params", {})
params.setdefault("auth", {})
params["auth"].setdefault("token", auth_token)
msg = json.dumps(payload)
except (ValueError, TypeError, AttributeError):
pass
await upstream_ws.send(msg)
except Exception:
pass
async def upstream_to_client():
try:
async for msg in upstream_ws:
if isinstance(msg, str):
await client_ws.send_text(msg)
else:
await client_ws.send_bytes(msg)
except Exception:
pass
done, pending = await asyncio.wait(
[asyncio.create_task(client_to_upstream()), asyncio.create_task(upstream_to_client())],
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
except Exception:
pass
finally:
try:
await client_ws.close()
except Exception:
pass