diff --git a/app/agent/tools/__init__.py b/app/agent/tools/__init__.py index 9e7a596b..62d4ff88 100644 --- a/app/agent/tools/__init__.py +++ b/app/agent/tools/__init__.py @@ -6,12 +6,15 @@ from app.agent.tools.impl.add_subscribe import AddSubscribeTool from app.agent.tools.impl.search_torrents import SearchTorrentsTool from app.agent.tools.impl.add_download import AddDownloadTool from app.agent.tools.impl.query_subscribes import QuerySubscribesTool +from app.agent.tools.impl.delete_subscribe import DeleteSubscribeTool from app.agent.tools.impl.query_downloads import QueryDownloadsTool from app.agent.tools.impl.query_downloaders import QueryDownloadersTool from app.agent.tools.impl.query_sites import QuerySitesTool from app.agent.tools.impl.get_recommendations import GetRecommendationsTool from app.agent.tools.impl.query_media_library import QueryMediaLibraryTool from app.agent.tools.impl.send_message import SendMessageTool +from app.agent.tools.impl.query_schedulers import QuerySchedulersTool +from app.agent.tools.impl.run_scheduler import RunSchedulerTool from .factory import MoviePilotToolFactory __all__ = [ @@ -21,11 +24,14 @@ __all__ = [ "SearchTorrentsTool", "AddDownloadTool", "QuerySubscribesTool", + "DeleteSubscribeTool", "QueryDownloadsTool", "QueryDownloadersTool", "QuerySitesTool", "GetRecommendationsTool", "QueryMediaLibraryTool", "SendMessageTool", + "QuerySchedulersTool", + "RunSchedulerTool", "MoviePilotToolFactory" ] diff --git a/app/agent/tools/factory.py b/app/agent/tools/factory.py index 3b777add..f4f9cf47 100644 --- a/app/agent/tools/factory.py +++ b/app/agent/tools/factory.py @@ -10,9 +10,12 @@ from app.agent.tools.impl.query_downloads import QueryDownloadsTool from app.agent.tools.impl.query_media_library import QueryMediaLibraryTool from app.agent.tools.impl.query_sites import QuerySitesTool from app.agent.tools.impl.query_subscribes import QuerySubscribesTool +from app.agent.tools.impl.delete_subscribe import DeleteSubscribeTool from app.agent.tools.impl.search_media import SearchMediaTool from app.agent.tools.impl.search_torrents import SearchTorrentsTool from app.agent.tools.impl.send_message import SendMessageTool +from app.agent.tools.impl.query_schedulers import QuerySchedulersTool +from app.agent.tools.impl.run_scheduler import RunSchedulerTool from app.core.plugin import PluginManager from app.log import logger from .base import MoviePilotTool @@ -33,12 +36,15 @@ class MoviePilotToolFactory: SearchTorrentsTool, AddDownloadTool, QuerySubscribesTool, + DeleteSubscribeTool, QueryDownloadsTool, QueryDownloadersTool, QuerySitesTool, GetRecommendationsTool, QueryMediaLibraryTool, - SendMessageTool + SendMessageTool, + QuerySchedulersTool, + RunSchedulerTool ] # 创建内置工具 for ToolClass in tool_definitions: diff --git a/app/agent/tools/impl/delete_subscribe.py b/app/agent/tools/impl/delete_subscribe.py new file mode 100644 index 00000000..719edfb5 --- /dev/null +++ b/app/agent/tools/impl/delete_subscribe.py @@ -0,0 +1,58 @@ +"""删除订阅工具""" + +from typing import Type + +from pydantic import BaseModel, Field + +from app.agent.tools.base import MoviePilotTool +from app.core.event import eventmanager +from app.db.subscribe_oper import SubscribeOper +from app.helper.subscribe import SubscribeHelper +from app.log import logger +from app.schemas.types import EventType + + +class DeleteSubscribeInput(BaseModel): + """删除订阅工具的输入参数模型""" + explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context") + subscribe_id: int = Field(..., description="The ID of the subscription to delete (can be obtained from query_subscribes tool)") + + +class DeleteSubscribeTool(MoviePilotTool): + name: str = "delete_subscribe" + description: str = "Delete a media subscription by its ID. This will remove the subscription and stop automatic downloads for that media." + args_schema: Type[BaseModel] = DeleteSubscribeInput + + async def run(self, subscribe_id: int, **kwargs) -> str: + logger.info(f"执行工具: {self.name}, 参数: subscribe_id={subscribe_id}") + + try: + subscribe_oper = SubscribeOper() + # 获取订阅信息 + subscribe = await subscribe_oper.async_get(subscribe_id) + if not subscribe: + return f"订阅 ID {subscribe_id} 不存在" + + # 在删除之前获取订阅信息(用于事件) + subscribe_info = subscribe.to_dict() + + # 删除订阅 + subscribe_oper.delete(subscribe_id) + + # 发送事件 + await eventmanager.async_send_event(EventType.SubscribeDeleted, { + "subscribe_id": subscribe_id, + "subscribe_info": subscribe_info + }) + + # 统计订阅 + SubscribeHelper().sub_done_async({ + "tmdbid": subscribe.tmdbid, + "doubanid": subscribe.doubanid + }) + + return f"成功删除订阅:{subscribe.name} ({subscribe.year})" + except Exception as e: + logger.error(f"删除订阅失败: {e}", exc_info=True) + return f"删除订阅时发生错误: {str(e)}" + diff --git a/app/agent/tools/impl/query_schedulers.py b/app/agent/tools/impl/query_schedulers.py new file mode 100644 index 00000000..634225d6 --- /dev/null +++ b/app/agent/tools/impl/query_schedulers.py @@ -0,0 +1,51 @@ +"""查询定时服务工具""" + +import json +from typing import Type + +from pydantic import BaseModel, Field + +from app.agent.tools.base import MoviePilotTool +from app.log import logger +from app.scheduler import Scheduler + + +class QuerySchedulersInput(BaseModel): + """查询定时服务工具的输入参数模型""" + explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context") + + +class QuerySchedulersTool(MoviePilotTool): + name: str = "query_schedulers" + description: str = "Query scheduled tasks and list all available scheduler jobs. Shows job status, next run time, and provider information." + args_schema: Type[BaseModel] = QuerySchedulersInput + + async def run(self, **kwargs) -> str: + logger.info(f"执行工具: {self.name}") + try: + scheduler = Scheduler() + schedulers = scheduler.list() + if schedulers: + # 转换为字典列表以便JSON序列化 + schedulers_list = [] + for s in schedulers: + schedulers_list.append({ + "id": s.id, + "name": s.name, + "provider": s.provider, + "status": s.status, + "next_run": s.next_run + }) + result_json = json.dumps(schedulers_list, ensure_ascii=False, indent=2) + # 限制最多30条结果 + total_count = len(schedulers_list) + if total_count > 30: + limited_schedulers = schedulers_list[:30] + limited_json = json.dumps(limited_schedulers, ensure_ascii=False, indent=2) + return f"注意:查询结果共找到 {total_count} 条,为节省上下文空间,仅显示前 30 条结果。\n\n{limited_json}" + return result_json + return "未找到定时服务" + except Exception as e: + logger.error(f"查询定时服务失败: {e}", exc_info=True) + return f"查询定时服务时发生错误: {str(e)}" + diff --git a/app/agent/tools/impl/run_scheduler.py b/app/agent/tools/impl/run_scheduler.py new file mode 100644 index 00000000..41c3d579 --- /dev/null +++ b/app/agent/tools/impl/run_scheduler.py @@ -0,0 +1,48 @@ +"""运行定时服务工具""" + +from typing import Type + +from pydantic import BaseModel, Field + +from app.agent.tools.base import MoviePilotTool +from app.log import logger +from app.scheduler import Scheduler + + +class RunSchedulerInput(BaseModel): + """运行定时服务工具的输入参数模型""" + explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context") + job_id: str = Field(..., description="The ID of the scheduled job to run (can be obtained from query_schedulers tool)") + + +class RunSchedulerTool(MoviePilotTool): + name: str = "run_scheduler" + description: str = "Manually trigger a scheduled task to run immediately. This will execute the specified scheduler job by its ID." + args_schema: Type[BaseModel] = RunSchedulerInput + + async def run(self, job_id: str, **kwargs) -> str: + logger.info(f"执行工具: {self.name}, 参数: job_id={job_id}") + + try: + scheduler = Scheduler() + # 检查定时服务是否存在 + schedulers = scheduler.list() + job_exists = False + job_name = None + for s in schedulers: + if s.id == job_id: + job_exists = True + job_name = s.name + break + + if not job_exists: + return f"定时服务 ID {job_id} 不存在,请使用 query_schedulers 工具查询可用的定时服务" + + # 运行定时服务 + scheduler.start(job_id) + + return f"成功触发定时服务:{job_name} (ID: {job_id})" + except Exception as e: + logger.error(f"运行定时服务失败: {e}", exc_info=True) + return f"运行定时服务时发生错误: {str(e)}" +