"""Agent 定时任务 CRUD API""" from __future__ import annotations import logging from datetime import datetime from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.auth import get_current_user from app.core.database import get_db from app.models.agent import Agent from app.models.agent_schedule import AgentSchedule from app.models.user import User from app.services.agent_schedule_service import ( compute_next_run, create_execution_for_schedule, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/agent-schedules", tags=["agent-schedules"]) # ─── Pydantic Schemas ────────────────────────────────────────────── class ScheduleCreate(BaseModel): agent_id: str name: str = Field(..., max_length=100) cron_expression: str = Field(..., description="标准 5 位 cron,如 0 9 * * *") input_message: str = Field(..., description="每次触发时发给 Agent 的消息") timezone: str = "Asia/Shanghai" class ScheduleUpdate(BaseModel): name: Optional[str] = None cron_expression: Optional[str] = None input_message: Optional[str] = None timezone: Optional[str] = None enabled: Optional[bool] = None class ScheduleResponse(BaseModel): id: str agent_id: str name: str cron_expression: str input_message: str timezone: str enabled: bool last_run_at: Optional[datetime] = None last_run_status: Optional[str] = None next_run_at: datetime created_at: datetime updated_at: datetime class Config: from_attributes = True # ─── API Endpoints ───────────────────────────────────────────────── @router.get("", response_model=List[ScheduleResponse]) async def list_schedules( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """获取当前用户的所有定时任务。""" schedules = ( db.query(AgentSchedule) .filter(AgentSchedule.user_id == current_user.id) .order_by(AgentSchedule.created_at.desc()) .all() ) return schedules @router.post("", response_model=ScheduleResponse, status_code=201) async def create_schedule( data: ScheduleCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """创建定时任务。""" # 验证 Agent 存在 agent = db.query(Agent).filter(Agent.id == data.agent_id).first() if not agent: raise HTTPException(status_code=404, detail="Agent 不存在") if agent.user_id and agent.user_id != current_user.id and current_user.role != "admin": raise HTTPException(status_code=403, detail="无权使用该 Agent") # 验证 cron 表达式 try: next_run = compute_next_run(data.cron_expression) except (ValueError, KeyError) as e: raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}") schedule = AgentSchedule( agent_id=data.agent_id, name=data.name, cron_expression=data.cron_expression, input_message=data.input_message, timezone=data.timezone or "Asia/Shanghai", enabled=True, next_run_at=next_run, user_id=current_user.id, ) db.add(schedule) db.commit() db.refresh(schedule) logger.info( "定时任务创建: user=%s agent=%s name=%s cron=%s next_run=%s", current_user.id, data.agent_id, data.name, data.cron_expression, next_run, ) return schedule @router.put("/{schedule_id}", response_model=ScheduleResponse) async def update_schedule( schedule_id: str, data: ScheduleUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """更新定时任务配置。""" schedule = db.query(AgentSchedule).filter(AgentSchedule.id == schedule_id).first() if not schedule: raise HTTPException(status_code=404, detail="定时任务不存在") if schedule.user_id != current_user.id and current_user.role != "admin": raise HTTPException(status_code=403, detail="无权修改该定时任务") if data.name is not None: schedule.name = data.name if data.cron_expression is not None: try: schedule.next_run_at = compute_next_run(data.cron_expression, after=datetime.utcnow()) schedule.cron_expression = data.cron_expression except (ValueError, KeyError) as e: raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}") if data.input_message is not None: schedule.input_message = data.input_message if data.timezone is not None: schedule.timezone = data.timezone if data.enabled is not None: schedule.enabled = data.enabled schedule.updated_at = datetime.utcnow() db.commit() db.refresh(schedule) return schedule @router.delete("/{schedule_id}") async def delete_schedule( schedule_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """删除定时任务。""" schedule = db.query(AgentSchedule).filter(AgentSchedule.id == schedule_id).first() if not schedule: raise HTTPException(status_code=404, detail="定时任务不存在") if schedule.user_id != current_user.id and current_user.role != "admin": raise HTTPException(status_code=403, detail="无权删除该定时任务") db.delete(schedule) db.commit() return {"message": "定时任务已删除"} @router.post("/{schedule_id}/trigger") async def trigger_schedule( schedule_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """手动触发一次定时任务。""" schedule = db.query(AgentSchedule).filter(AgentSchedule.id == schedule_id).first() if not schedule: raise HTTPException(status_code=404, detail="定时任务不存在") if schedule.user_id != current_user.id and current_user.role != "admin": raise HTTPException(status_code=403, detail="无权触发该定时任务") execution_id = create_execution_for_schedule(db, schedule) if not execution_id: raise HTTPException(status_code=500, detail="触发执行失败") return { "message": "定时任务已触发", "execution_id": execution_id, }