Files
MoviePilot/app/agent/tools/impl/update_agent_task.py
jxxghp 7e851dbfa7 refactor(chain): 处理链功能域 mixin 化,清理未使用导入并根治兼容层循环导入
- ChainBase 拆分为 RecognitionMixin/MessageProcessingMixin/NotificationMixin
- TransferChain 拆分为 7 个功能 mixin(_mixins.py),SubscribeChain 音乐订阅域拆出 _music.py
- 斜杠命令交互四件套收敛为 InteractionChainMixin 委托,会话管理器移至 application 层,chain 层不再 re-export
- 模块基础类收敛到 app/modules/_base(notification/mediaserver 语义重命名)
- 清理 app/chain/__init__.py 24 个未使用导入,修正 49 处测试 patch 目标到实际命名空间
- 兼容层 legacy 符号不再并入 __all__,根治 schemas 初始化反向拉起 application.transfer 的循环导入
- 修复 bangumi 集数为字符串时 set_bangumi_info 抛 TypeError
- 新增重复代码等架构门禁测试;capability 清单校验排除下划线内部目录
2026-08-16 16:30:16 +08:00

212 lines
8.3 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.
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.runtime.config import settings
from app.db.oper.agenttask import AgentTaskOper
from app.runtime.scheduling 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.application.scheduling import update_agent_task_job
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} 正在执行,请稍后再修改"}
has_schedule_update = payload.trigger_type is not None
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
)
normalized_type = trigger_type
normalized_trigger = trigger_value
validates_existing_date_schedule = bool(
payload.enabled
and task.last_status != "interrupted"
and trigger_type == "date"
)
if has_schedule_update or validates_existing_date_schedule:
normalized_type, normalized_trigger = TimerUtils.normalize_schedule_trigger(
trigger_type=trigger_type,
trigger_value=trigger_value,
timezone_name=settings.TZ,
require_future=bool(
trigger_type == "date"
and (
task.last_status == "interrupted"
or (task.enabled if payload.enabled is None else payload.enabled)
)
),
)
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 has_schedule_update:
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 and task.last_status != "interrupted":
update_payload["last_status"] = "waiting"
updated = oper.update(
task_id=payload.task_id,
payload=update_payload,
user_id=str(self._user_id),
)
if not updated:
current = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
if current and current.last_status == "running":
return {"error": f"Agent 定时任务 {payload.task_id} 正在执行,请稍后再修改"}
return None
next_run_at = 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)