AgentEvalTool/backend/agenteval/channels/openclaw.py
sinohqb 12c1732617 v0.4-t3t4: OpenClaw 通道 + 前端 bundle 优化
## T3: OpenClaw 直连通道
- channels/openclaw.py: OpenClawChannel
  - send: POST /api/v1/chat/completions
  - poll_reply: GET /api/v1/chat/completions/{msg_id}
  - health_check: GET /api/health
  - 默认从 settings 读取 upstream/auth_token,channel_config 可覆盖
- channels/factory.py: 注册 ChannelType.OPENCLAW → OpenClawChannel
- Targets.tsx: 通道类型下拉新增「HTTP 通用」和「OpenClaw」选项
- 8 个 OpenClawChannel 单元测试(发送/轮询/超时/健康检查/默认配置)

## T4: 前端 Bundle 优化
- App.tsx: 7 个页面改为 React.lazy 懒加载 + Suspense fallback(Spin)
- vite.config.ts: 精细化 manualChunks
  - vendor-antd / vendor-monaco / vendor-charts 独立拆分
  - 主 index 68KB → 7KB,页面按需加载
  - 无循环依赖警告

## 测试
- 178/178 全绿,覆盖率维持 77%

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 15:37:14 +08:00

109 lines
4.1 KiB
Python

"""OpenClaw message channel.
Connects directly to an OpenClaw instance as an evaluation target.
Uses the OpenClaw chat API to send messages and poll for replies.
Configuration keys (in channel_config, all optional — defaults come from settings):
base_url Override OpenClaw upstream URL (defaults to AGENTEVAL_OPENCLAW_UPSTREAM)
auth_token Override auth token (defaults to AGENTEVAL_OPENCLAW_AUTH_TOKEN)
model Model name for chat completions (default: "doubao-seed-2.0")
poll_interval Seconds between polls (default 1.0)
timeout Seconds before poll gives up (default 30.0)
"""
import asyncio
import uuid
from typing import Any, Optional
import httpx
from agenteval.channels.base import ChannelHealth, EvalChannel, Reply, SendResult
from agenteval.config import get_settings
class OpenClawChannel(EvalChannel):
"""Message channel backed by an OpenClaw chat API."""
def __init__(self, config: dict[str, Any]):
settings = get_settings()
self.base_url: str = config.get("base_url", settings.openclaw_upstream).rstrip("/")
self.auth_token: str = config.get("auth_token", settings.openclaw_auth_token)
self.model: str = config.get("model", "doubao-seed-2.0")
self._poll_interval: float = float(config.get("poll_interval", 1.0))
self._client = httpx.AsyncClient(
headers={
"Authorization": f"Bearer {self.auth_token}",
"Content-Type": "application/json",
},
timeout=30,
)
async def close(self) -> None:
await self._client.aclose()
async def health_check(self) -> ChannelHealth:
try:
resp = await self._client.get(f"{self.base_url}/api/health")
resp.raise_for_status()
return ChannelHealth(ok=True, message=f"OpenClaw {resp.status_code}")
except Exception as exc:
return ChannelHealth(ok=False, message=str(exc))
async def send(self, content: str, **kwargs: Any) -> SendResult:
"""Send a chat message to OpenClaw."""
payload = {
"model": self.model,
"messages": [{"role": "user", "content": content}],
"stream": False,
}
try:
resp = await self._client.post(
f"{self.base_url}/api/v1/chat/completions",
json=payload,
)
resp.raise_for_status()
data = resp.json()
# Extract the assistant message ID from the response
msg_id = data.get("id") or str(uuid.uuid4())
return SendResult(ok=True, question_msg_id=msg_id, raw_response=data)
except Exception as exc:
return SendResult(ok=False, error=str(exc))
async def poll_reply(
self,
question_msg_id: str,
timeout: float = 30.0,
poll_interval: float = 1.0,
) -> Optional[Reply]:
"""Poll the chat completions endpoint until a reply is available.
Uses the conversation ID from the send response to track the thread.
"""
interval = poll_interval or self._poll_interval
deadline = asyncio.get_event_loop().time() + timeout
# Re-send with the same conversation to get the latest reply
while asyncio.get_event_loop().time() < deadline:
try:
# Get the thread/messages from the conversation
resp = await self._client.get(
f"{self.base_url}/api/v1/chat/completions/{question_msg_id}",
)
if resp.status_code == 200:
data = resp.json()
choice = (data.get("choices") or [{}])[0]
reply_content = choice.get("message", {}).get("content", "")
if reply_content:
return Reply(
question_msg_id=question_msg_id,
content=reply_content,
raw_message={"text": reply_content, "_raw": data},
)
except Exception:
pass
await asyncio.sleep(interval)
return None