Files
aiagent/backend/app/api/agent_schedules.py
renjianbo 68fbadae76 feat: add 8 builtin tools, AgentSchedules management page, Celery Beat integration
- Add 3 schedule tools (create/list/delete) and 5 utility tools (crypto, random, email, URL, regex)
- Add frontend AgentSchedules.vue page with full CRUD, cron presets, manual trigger
- Integrate Celery Beat for automatic schedule execution
- Update startup scripts with Celery Beat launch
- Fix schedule list API to show all schedules for admin users
- Add celrybeat-schedule.* to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-02 19:14:25 +08:00

199 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"
webhook_url: Optional[str] = Field(None, description="飞书机器人 Webhook URL可选")
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
webhook_url: Optional[str] = None
class ScheduleResponse(BaseModel):
id: str
agent_id: str
name: str
cron_expression: str
input_message: str
timezone: str
enabled: bool
webhook_url: Optional[str] = None
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),
):
"""获取当前用户的所有定时任务。"""
query = db.query(AgentSchedule)
if current_user.role != "admin":
query = query.filter(AgentSchedule.user_id == current_user.id)
schedules = query.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",
webhook_url=data.webhook_url,
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
if data.webhook_url is not None:
schedule.webhook_url = data.webhook_url
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,
}