All checks were successful
CI / test (push) Successful in 4m2s
方案③落地后,智能评估执行机制从'常驻 cron 每分钟自唤醒'改为'平台每 60s 扫描入队 + 按需触发无状态 headless agent'(触发式执行)。对齐领域语言: - CONTEXT.md:Cron 池/工作单元(Worker)标 deprecated;新增触发式执行词条; 修正时间窗口(cron 自唤醒→平台扫描时段到期)、任务队列(消费端)、决策日志 - ADR-0009 新增:记录触发式执行取代 cron 池的决策(原因:cron 需外部 channel, OpenClaw webchat 非 channel 账号无法 delivery);ADR-0007 标 superseded - 代码标 deprecated:cron_pool / fault_tolerance / openclaw_cron_pool 路由 / CronPoolMonitor 页(导航入口已从 App.tsx 移除,监控由 TaskQueueMonitor 承担) 895 passed, vitest 19 passed
161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
"""API routes for OpenClaw cron pool management.
|
||
|
||
DEPRECATED (ADR-0009): 智能评估已改为触发式执行,cron 池不再使用(worker cron 已禁用)。
|
||
端点遗留保留仅供回溯;监控职责由前端任务队列页(TaskQueueMonitor)承担。
|
||
"""
|
||
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
from sqlmodel import Session
|
||
|
||
from agenteval.intelligent_eval import cron_pool
|
||
from agenteval.intelligent_eval.openclaw_client import OpenClawClient
|
||
from agenteval.web.deps import get_db
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class ScaleRequest(BaseModel):
|
||
target_size: int = Field(ge=1, le=50)
|
||
|
||
|
||
class HeartbeatRequest(BaseModel):
|
||
status: str # idle / busy
|
||
current_eval_id: str | None = None
|
||
|
||
|
||
@router.get("/cron-pool")
|
||
async def get_cron_pool_status(session: Session = Depends(get_db)) -> dict:
|
||
"""Get cron pool status."""
|
||
status = cron_pool.get_pool_status(session)
|
||
return {"pool": status}
|
||
|
||
|
||
@router.post("/cron-pool/scale")
|
||
async def scale_cron_pool(request: ScaleRequest, session: Session = Depends(get_db)) -> dict:
|
||
"""Manually scale cron pool to target size."""
|
||
client = OpenClawClient()
|
||
return await cron_pool.scale_to(request.target_size, session, client)
|
||
|
||
|
||
@router.post("/cron-pool/sync")
|
||
async def sync_cron_states(session: Session = Depends(get_db)) -> dict:
|
||
"""Sync cron states from OpenClaw to platform DB."""
|
||
client = OpenClawClient()
|
||
synced = await cron_pool.sync_cron_states(session, client)
|
||
return {"success": True, "synced": synced}
|
||
|
||
|
||
@router.post("/cron-pool/auto-scale")
|
||
async def auto_scale_pool(session: Session = Depends(get_db)) -> dict:
|
||
"""Trigger auto-scaling based on current load."""
|
||
client = OpenClawClient()
|
||
scaled_up, scaled_down = await cron_pool.auto_scale(session, client)
|
||
return {
|
||
"success": True,
|
||
"scaled_up": scaled_up,
|
||
"scaled_down": scaled_down,
|
||
}
|
||
|
||
|
||
@router.post("/crons/{cron_id}/heartbeat")
|
||
async def report_heartbeat(
|
||
cron_id: str,
|
||
request: HeartbeatRequest,
|
||
session: Session = Depends(get_db),
|
||
) -> dict:
|
||
"""Report cron heartbeat. Updates last_active_at and current status."""
|
||
try:
|
||
cron_pool.heartbeat(cron_id, request.status, request.current_eval_id, session)
|
||
except LookupError as e:
|
||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||
return {"success": True}
|
||
|
||
|
||
@router.get("/cron-pool/metrics")
|
||
async def get_cron_pool_metrics(session: Session = Depends(get_db)) -> dict:
|
||
"""Get cron pool metrics."""
|
||
from agenteval.intelligent_eval.metrics import get_all_metrics
|
||
|
||
metrics = get_all_metrics(session)
|
||
return {"metrics": metrics}
|
||
|
||
|
||
@router.post("/cron-pool/check-alerts")
|
||
async def check_alerts(session: Session = Depends(get_db)) -> dict:
|
||
"""Check alert rules and create alerts if triggered."""
|
||
from agenteval.intelligent_eval.alerts import AlertManager
|
||
|
||
client = OpenClawClient()
|
||
manager = AlertManager(session, openclaw_client=client)
|
||
alerts = manager.check_rules()
|
||
# T7: link each newly-created alert to auto-scale (best-effort).
|
||
for alert in alerts:
|
||
await manager.maybe_autoscale(alert)
|
||
|
||
return {
|
||
"success": True,
|
||
"alerts_triggered": len(alerts),
|
||
"alerts": [
|
||
{
|
||
"id": alert.id,
|
||
"alert_type": alert.alert_type,
|
||
"severity": alert.severity,
|
||
"message": alert.message,
|
||
"metric_value": alert.metric_value,
|
||
"threshold": alert.threshold,
|
||
"created_at": alert.created_at.isoformat(),
|
||
}
|
||
for alert in alerts
|
||
],
|
||
}
|
||
|
||
|
||
@router.get("/cron-pool/alerts")
|
||
async def get_alert_history(
|
||
limit: int = 100,
|
||
unresolved_only: bool = False,
|
||
session: Session = Depends(get_db),
|
||
) -> dict:
|
||
"""Get alert history."""
|
||
from agenteval.intelligent_eval.alerts import AlertManager
|
||
|
||
manager = AlertManager(session)
|
||
|
||
if unresolved_only:
|
||
alerts = manager.get_unresolved_alerts()
|
||
else:
|
||
alerts = manager.get_alert_history(limit=limit)
|
||
|
||
return {
|
||
"alerts": [
|
||
{
|
||
"id": alert.id,
|
||
"alert_type": alert.alert_type,
|
||
"severity": alert.severity,
|
||
"message": alert.message,
|
||
"metric_value": alert.metric_value,
|
||
"threshold": alert.threshold,
|
||
"created_at": alert.created_at.isoformat(),
|
||
"resolved_at": alert.resolved_at.isoformat() if alert.resolved_at else None,
|
||
"webhook_sent": alert.webhook_sent,
|
||
}
|
||
for alert in alerts
|
||
]
|
||
}
|
||
|
||
|
||
@router.post("/cron-pool/alerts/{alert_id}/resolve")
|
||
async def resolve_alert(alert_id: str, session: Session = Depends(get_db)) -> dict:
|
||
"""Resolve an alert."""
|
||
from agenteval.intelligent_eval.alerts import AlertManager
|
||
|
||
manager = AlertManager(session)
|
||
resolved = manager.resolve_alert(alert_id)
|
||
|
||
if not resolved:
|
||
raise HTTPException(status_code=404, detail=f"alert {alert_id} not found")
|
||
|
||
return {"success": True}
|