mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
194 lines
7.5 KiB
Python
194 lines
7.5 KiB
Python
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Literal, Optional, Type
|
|
|
|
import pytz
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
from app.agent.tools.base import MoviePilotTool
|
|
from app.agent.tools.tags import ToolTag
|
|
from app.core.config import settings
|
|
from app.db.agenttask_oper import AgentTaskOper
|
|
from app.utils.timer import TimerUtils
|
|
|
|
|
|
class UpdateAgentTaskInput(BaseModel):
|
|
"""更新 Agent 自主定时任务的输入参数。"""
|
|
|
|
task_id: int = Field(..., ge=1, description="ID of the task to update.")
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
content: Optional[str] = Field(None, min_length=1, max_length=10000)
|
|
trigger_type: Optional[Literal["date", "cron"]] = Field(
|
|
None,
|
|
description="New trigger type. Must be provided together with trigger.",
|
|
)
|
|
trigger: Optional[str] = Field(
|
|
None,
|
|
min_length=1,
|
|
max_length=200,
|
|
description="New ISO 8601 date or five-field cron expression.",
|
|
)
|
|
delay_minutes: Optional[int] = Field(
|
|
None,
|
|
ge=1,
|
|
le=525600,
|
|
description=(
|
|
"For a one-time date task expressed as 'in N minutes', provide this instead "
|
|
"of trigger together with trigger_type='date'."
|
|
),
|
|
)
|
|
enabled: Optional[bool] = Field(
|
|
None,
|
|
description="Set false to pause the task or true to resume it.",
|
|
)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_update(self) -> "UpdateAgentTaskInput":
|
|
"""校验更新内容和触发参数组合。"""
|
|
if self.name is not None:
|
|
self.name = self.name.strip()
|
|
if not self.name:
|
|
raise ValueError("name 不能只包含空白字符")
|
|
if self.content is not None:
|
|
self.content = self.content.strip()
|
|
if not self.content:
|
|
raise ValueError("content 不能只包含空白字符")
|
|
has_schedule_update = any(
|
|
value is not None
|
|
for value in (self.trigger_type, self.trigger, self.delay_minutes)
|
|
)
|
|
if has_schedule_update:
|
|
if self.trigger_type is None:
|
|
raise ValueError("修改触发配置时必须提供 trigger_type")
|
|
if self.trigger_type == "date":
|
|
if self.delay_minutes is not None:
|
|
# 保持校验幂等,具体绝对时间在更新调度前只计算一次。
|
|
self.trigger = None
|
|
elif self.trigger is None:
|
|
raise ValueError("date 任务必须提供 trigger 或 delay_minutes")
|
|
elif self.trigger is None or self.delay_minutes is not None:
|
|
raise ValueError("cron 任务必须提供 trigger,且不能提供 delay_minutes")
|
|
if all(
|
|
value is None
|
|
for value in (
|
|
self.name,
|
|
self.content,
|
|
self.trigger_type,
|
|
self.enabled,
|
|
)
|
|
):
|
|
raise ValueError("至少需要提供一个要更新的字段")
|
|
return self
|
|
|
|
|
|
class UpdateAgentTaskTool(MoviePilotTool):
|
|
"""修改、暂停或恢复 Agent 自主定时任务。"""
|
|
|
|
name: str = "update_agent_task"
|
|
tags: list[str] = [ToolTag.Write, ToolTag.AgentTask, ToolTag.Admin]
|
|
description: str = (
|
|
"Update an autonomous agent task's name, instructions, exact date or cron "
|
|
"trigger, relative delay_minutes, or enabled state. Use enabled=false to pause "
|
|
"and enabled=true to resume."
|
|
)
|
|
args_schema: Type[BaseModel] = UpdateAgentTaskInput
|
|
require_admin: bool = True
|
|
|
|
def get_tool_message(self, **kwargs: object) -> Optional[str]:
|
|
"""生成更新定时任务的提示消息。"""
|
|
return f"更新自主定时任务:{kwargs.get('task_id', '')}"
|
|
|
|
def _update_task(self, payload: UpdateAgentTaskInput) -> Optional[dict]:
|
|
"""更新当前用户的任务并刷新运行时调度。"""
|
|
from app.scheduler import Scheduler
|
|
|
|
oper = AgentTaskOper()
|
|
task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
|
|
if not task:
|
|
return None
|
|
if task.last_status == "running":
|
|
return {"error": f"Agent 定时任务 {payload.task_id} 正在执行,请稍后再修改"}
|
|
|
|
trigger_type = payload.trigger_type or task.trigger_type
|
|
trigger_value = payload.trigger
|
|
if trigger_type == "date" and payload.delay_minutes is not None:
|
|
timezone = pytz.timezone(settings.TZ)
|
|
trigger_value = (
|
|
datetime.now(timezone) + timedelta(minutes=payload.delay_minutes)
|
|
).isoformat(timespec="seconds")
|
|
if trigger_value is None:
|
|
trigger_value = (
|
|
task.cron_expression if trigger_type == "cron" else task.run_at
|
|
)
|
|
enabled = task.enabled if payload.enabled is None else payload.enabled
|
|
normalized_type, normalized_trigger = TimerUtils.normalize_schedule_trigger(
|
|
trigger_type=trigger_type,
|
|
trigger_value=trigger_value,
|
|
timezone_name=settings.TZ,
|
|
require_future=bool(enabled and trigger_type == "date"),
|
|
)
|
|
|
|
update_payload = {}
|
|
if payload.name is not None:
|
|
update_payload["name"] = payload.name.strip()
|
|
if payload.content is not None:
|
|
update_payload["content"] = payload.content.strip()
|
|
if payload.trigger_type is not None:
|
|
update_payload.update(
|
|
{
|
|
"trigger_type": normalized_type,
|
|
"cron_expression": (
|
|
normalized_trigger if normalized_type == "cron" else None
|
|
),
|
|
"run_at": normalized_trigger if normalized_type == "date" else None,
|
|
"last_status": "waiting",
|
|
"last_result": None,
|
|
}
|
|
)
|
|
if payload.enabled is not None:
|
|
update_payload["enabled"] = payload.enabled
|
|
if payload.enabled:
|
|
update_payload["last_status"] = "waiting"
|
|
|
|
oper.update(
|
|
task_id=payload.task_id,
|
|
payload=update_payload,
|
|
user_id=str(self._user_id),
|
|
)
|
|
scheduler = Scheduler()
|
|
next_run_at = scheduler.update_agent_task_job(payload.task_id)
|
|
updated_task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
|
|
return oper.to_dict(
|
|
updated_task,
|
|
next_run_at=next_run_at,
|
|
timezone=settings.TZ,
|
|
)
|
|
|
|
async def run(
|
|
self,
|
|
task_id: int,
|
|
name: Optional[str] = None,
|
|
content: Optional[str] = None,
|
|
trigger_type: Optional[str] = None,
|
|
trigger: Optional[str] = None,
|
|
delay_minutes: Optional[int] = None,
|
|
enabled: Optional[bool] = None,
|
|
**kwargs: object,
|
|
) -> str:
|
|
"""更新 Agent 自主定时任务。"""
|
|
payload = UpdateAgentTaskInput(
|
|
task_id=task_id,
|
|
name=name,
|
|
content=content,
|
|
trigger_type=trigger_type,
|
|
trigger=trigger,
|
|
delay_minutes=delay_minutes,
|
|
enabled=enabled,
|
|
)
|
|
task = await self.run_blocking("db", self._update_task, payload)
|
|
if not task:
|
|
return f"Agent 定时任务 {task_id} 不存在或不属于当前用户"
|
|
if task.get("error"):
|
|
return task["error"]
|
|
return json.dumps(task, ensure_ascii=False, indent=2)
|