import hashlib from typing import Callable, List, Optional, Type from app.agent.tools.impl.add_download_tasks import AddDownloadTasksTool from app.agent.tools.impl.add_subscribe import AddSubscribeTool from app.agent.tools.impl.update_subscribe import UpdateSubscribeTool from app.agent.tools.impl.search_subscribe import SearchSubscribeTool from app.agent.tools.impl.get_recommendations import GetRecommendationsTool from app.agent.tools.impl.query_downloaders import QueryDownloadersTool from app.agent.tools.impl.query_download_tasks import QueryDownloadTasksTool from app.agent.tools.impl.query_library_exists import QueryLibraryExistsTool from app.agent.tools.impl.query_library_latest import QueryLibraryLatestTool from app.agent.tools.impl.query_sites import QuerySitesTool from app.agent.tools.impl.update_site import UpdateSiteTool from app.agent.tools.impl.query_site_userdata import QuerySiteUserdataTool from app.agent.tools.impl.test_site import TestSiteTool from app.agent.tools.impl.query_subscribes import QuerySubscribesTool from app.agent.tools.impl.query_subscribe_shares import QuerySubscribeSharesTool from app.agent.tools.impl.query_rule_groups import QueryRuleGroupsTool from app.agent.tools.impl.query_builtin_filter_rules import QueryBuiltinFilterRulesTool from app.agent.tools.impl.query_custom_filter_rules import QueryCustomFilterRulesTool from app.agent.tools.impl.add_custom_filter_rule import AddCustomFilterRuleTool from app.agent.tools.impl.update_custom_filter_rule import UpdateCustomFilterRuleTool from app.agent.tools.impl.delete_custom_filter_rule import DeleteCustomFilterRuleTool from app.agent.tools.impl.add_rule_group import AddRuleGroupTool from app.agent.tools.impl.update_rule_group import UpdateRuleGroupTool from app.agent.tools.impl.delete_rule_group import DeleteRuleGroupTool from app.agent.tools.impl.query_popular_subscribes import QueryPopularSubscribesTool from app.agent.tools.impl.query_subscribe_history import QuerySubscribeHistoryTool from app.agent.tools.impl.delete_subscribe import DeleteSubscribeTool from app.agent.tools.impl.search_media import SearchMediaTool from app.agent.tools.impl.search_person import SearchPersonTool from app.agent.tools.impl.search_person_credits import SearchPersonCreditsTool from app.agent.tools.impl.recognize_media import RecognizeMediaTool from app.agent.tools.impl.scrape_metadata import ScrapeMetadataTool from app.agent.tools.impl.query_episode_schedule import QueryEpisodeScheduleTool from app.agent.tools.impl.query_media_detail import QueryMediaDetailTool from app.agent.tools.impl.search_torrents import SearchTorrentsTool from app.agent.tools.impl.get_search_results import GetSearchResultsTool from app.agent.tools.impl.search_web import SearchWebTool from app.agent.tools.impl.recognize_captcha import RecognizeCaptchaTool from app.agent.tools.impl.send_message import SendMessageTool from app.agent.tools.impl.ask_user_choice import AskUserChoiceTool from app.agent.tools.impl.send_local_file import SendLocalFileTool from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool from app.agent.tools.impl.create_agent_task import CreateAgentTaskTool from app.agent.tools.impl.delete_agent_task import DeleteAgentTaskTool from app.agent.tools.impl.query_agent_tasks import QueryAgentTasksTool from app.agent.tools.impl.query_schedulers import QuerySchedulersTool from app.agent.tools.impl.run_agent_task import RunAgentTaskTool from app.agent.tools.impl.run_scheduler import RunSchedulerTool from app.agent.tools.impl.update_agent_task import UpdateAgentTaskTool from app.agent.tools.impl.query_workflows import QueryWorkflowsTool from app.agent.tools.impl.run_workflow import RunWorkflowTool from app.agent.tools.impl.query_personas import QueryPersonasTool from app.agent.tools.impl.switch_persona import SwitchPersonaTool from app.agent.tools.impl.update_persona_definition import UpdatePersonaDefinitionTool from app.agent.tools.impl.update_site_cookie import UpdateSiteCookieTool from app.agent.tools.impl.delete_download_tasks import DeleteDownloadTasksTool from app.agent.tools.impl.delete_download_history import DeleteDownloadHistoryTool from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool from app.agent.tools.impl.update_download_tasks import UpdateDownloadTasksTool from app.agent.tools.impl.query_directory_settings import QueryDirectorySettingsTool from app.agent.tools.impl.list_directory import ListDirectoryTool from app.agent.tools.impl.query_transfer_history import QueryTransferHistoryTool from app.agent.tools.impl.transfer_file import TransferFileTool from app.agent.tools.impl.execute_command import ExecuteCommandTool from app.agent.tools.impl.edit_file import EditFileTool from app.agent.tools.impl.write_file import WriteFileTool from app.agent.tools.impl.read_file import ReadFileTool from app.agent.tools.impl.browse_webpage import BrowseWebpageTool from app.agent.tools.impl.query_installed_plugins import QueryInstalledPluginsTool from app.agent.tools.impl.query_market_plugins import QueryMarketPluginsTool from app.agent.tools.impl.query_plugin_capabilities import QueryPluginCapabilitiesTool from app.agent.tools.impl.query_plugin_config import QueryPluginConfigTool from app.agent.tools.impl.update_plugin_config import UpdatePluginConfigTool from app.agent.tools.impl.reload_plugin import ReloadPluginTool from app.agent.tools.impl.query_plugin_data import QueryPluginDataTool from app.agent.tools.impl.install_plugin import InstallPluginTool from app.agent.tools.impl.uninstall_plugin import UninstallPluginTool from app.agent.tools.impl.run_slash_command import RunSlashCommandTool from app.agent.tools.impl.list_slash_commands import ListSlashCommandsTool from app.agent.tools.impl.query_custom_identifiers import QueryCustomIdentifiersTool from app.agent.tools.impl.query_doctor_report import QueryDoctorReportTool from app.agent.tools.impl.update_custom_identifiers import UpdateCustomIdentifiersTool from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool from app.agent.tools.impl.update_system_settings import UpdateSystemSettingsTool from app.agent.llm.capability import AgentCapabilityManager from app.core.plugin import PluginManager from app.log import logger from app.schemas.message import ChannelCapabilityManager from app.schemas.types import MessageChannel from .base import MoviePilotTool from .catalog import ToolCatalogError, ToolCatalogSnapshot class MoviePilotToolFactory: """ MoviePilot工具工厂 """ BUILTIN_TOOL_CLASSES: tuple[Type[MoviePilotTool], ...] = ( SearchMediaTool, SearchPersonTool, SearchPersonCreditsTool, RecognizeMediaTool, ScrapeMetadataTool, QueryEpisodeScheduleTool, QueryMediaDetailTool, AddSubscribeTool, UpdateSubscribeTool, SearchSubscribeTool, SearchTorrentsTool, GetSearchResultsTool, SearchWebTool, RecognizeCaptchaTool, AddDownloadTasksTool, QuerySubscribesTool, QuerySubscribeSharesTool, QueryPopularSubscribesTool, QueryBuiltinFilterRulesTool, QueryCustomFilterRulesTool, QueryRuleGroupsTool, AddCustomFilterRuleTool, UpdateCustomFilterRuleTool, DeleteCustomFilterRuleTool, AddRuleGroupTool, UpdateRuleGroupTool, DeleteRuleGroupTool, QuerySubscribeHistoryTool, DeleteSubscribeTool, QueryDownloadTasksTool, DeleteDownloadTasksTool, DeleteDownloadHistoryTool, DeleteTransferHistoryTool, UpdateDownloadTasksTool, QueryDownloadersTool, QuerySitesTool, UpdateSiteTool, QuerySiteUserdataTool, TestSiteTool, UpdateSiteCookieTool, GetRecommendationsTool, QueryLibraryExistsTool, QueryLibraryLatestTool, QueryDirectorySettingsTool, ListDirectoryTool, QueryTransferHistoryTool, TransferFileTool, SendMessageTool, CreateAgentTaskTool, QueryAgentTasksTool, UpdateAgentTaskTool, RunAgentTaskTool, DeleteAgentTaskTool, QuerySchedulersTool, RunSchedulerTool, QueryWorkflowsTool, RunWorkflowTool, QueryPersonasTool, SwitchPersonaTool, UpdatePersonaDefinitionTool, ExecuteCommandTool, EditFileTool, WriteFileTool, ReadFileTool, BrowseWebpageTool, QueryInstalledPluginsTool, QueryMarketPluginsTool, QueryPluginCapabilitiesTool, QueryPluginConfigTool, UpdatePluginConfigTool, ReloadPluginTool, QueryPluginDataTool, InstallPluginTool, UninstallPluginTool, RunSlashCommandTool, ListSlashCommandsTool, QueryDoctorReportTool, QueryCustomIdentifiersTool, UpdateCustomIdentifiersTool, QuerySystemSettingsTool, UpdateSystemSettingsTool, ) # 这些通用工具需要始终保留,避免大工具集裁剪后让 Agent 丢失基础的 # 文件系统、命令执行、历史检索或交互确认能力。AskUserChoiceTool 仅在支持按钮 # 的渠道中才会实际注入,因此后续会再按已加载工具做一次求交集。 TOOL_SELECTOR_ALWAYS_INCLUDE_NAMES = ( "list_directory", "write_file", "read_file", "edit_file", "execute_command", "ask_user_choice", "create_agent_task", "query_agent_tasks", ) CATALOG_BUILD_MAX_ATTEMPTS = 3 @classmethod def catalog_factory_revision(cls) -> str: """返回当前内置工具工厂定义的稳定摘要。""" identities = ( f"{tool_class.__module__}.{tool_class.__qualname__}" for tool_class in cls.BUILTIN_TOOL_CLASSES ) return hashlib.sha256("\n".join(identities).encode("utf-8")).hexdigest() @staticmethod def _should_enable_choice_tool(channel: Optional[str] = None) -> bool: if not channel: return False try: message_channel = MessageChannel(channel) except ValueError: return False return ChannelCapabilityManager.supports_buttons( message_channel ) and ChannelCapabilityManager.supports_callbacks(message_channel) @classmethod def get_tool_selector_always_include_names( cls, tools: List[MoviePilotTool] ) -> List[str]: """ 返回当前实际已加载且需要绕过工具筛选的工具名。 `LLMToolSelectorMiddleware` 会校验 `always_include` 中的工具名是否 存在于当前请求里,因此这里必须根据运行时工具列表做交集过滤。 """ available_tool_names = { tool.name for tool in tools if getattr(tool, "name", None) } return [ tool_name for tool_name in cls.TOOL_SELECTOR_ALWAYS_INCLUDE_NAMES if tool_name in available_tool_names ] @classmethod def _get_builtin_tool_classes( cls, channel: Optional[str] = None ) -> list[Type[MoviePilotTool]]: """ 返回当前渠道可用的内置工具类清单。 """ tool_definitions = list(cls.BUILTIN_TOOL_CLASSES) if cls._should_enable_choice_tool(channel): tool_definitions.append(AskUserChoiceTool) tool_definitions.append(SendLocalFileTool) if AgentCapabilityManager.supports_audio_output(): tool_definitions.append(SendVoiceMessageTool) return tool_definitions @classmethod def create_tools( cls, session_id: str, user_id: str, channel: str = None, source: str = None, username: str = None, stream_handler: Callable = None, agent_context: dict = None, allow_message_tools: bool = True, ) -> List[MoviePilotTool]: """ 创建MoviePilot工具列表 """ tools = [] tool_definitions = cls._get_builtin_tool_classes(channel) # 创建内置工具 for ToolClass in tool_definitions: tool = ToolClass(session_id=session_id, user_id=user_id) if not allow_message_tools and getattr(tool, "sends_message", False): continue tool.set_message_attr(channel=channel, source=source, username=username) tool.set_stream_handler(stream_handler=stream_handler) tool.set_agent_context(agent_context=agent_context) object.__setattr__(tool, "_agent_tool_source", "builtin") tools.append(tool) # 加载插件提供的工具 plugin_tools_count = 0 plugin_tools_info = PluginManager().get_plugin_agent_tools() for plugin_info in plugin_tools_info: plugin_id = plugin_info.get("plugin_id") plugin_name = plugin_info.get("plugin_name") tool_classes = plugin_info.get("tools", []) for ToolClass in tool_classes: try: # 验证工具类是否继承自 MoviePilotTool if not issubclass(ToolClass, MoviePilotTool): logger.warning( f"插件 {plugin_name}({plugin_id}) 提供的工具类 {ToolClass.__name__} 未继承自 MoviePilotTool,已跳过" ) continue # 创建工具实例 tool = ToolClass(session_id=session_id, user_id=user_id) if not allow_message_tools and getattr(tool, "sends_message", False): continue tool.set_message_attr( channel=channel, source=source, username=username ) tool.set_stream_handler(stream_handler=stream_handler) tool.set_agent_context(agent_context=agent_context) object.__setattr__( tool, "_agent_tool_source", f"plugin:{plugin_id or 'unknown'}", ) tools.append(tool) plugin_tools_count += 1 logger.debug( f"成功加载插件 {plugin_name}({plugin_id}) 的工具: {ToolClass.__name__}" ) except Exception as e: logger.error( f"加载插件 {plugin_name}({plugin_id}) 的工具 {ToolClass.__name__} 失败: {str(e)}" ) builtin_tools_count = len(tool_definitions) if plugin_tools_count > 0: logger.debug( f"成功创建 {len(tools)} 个MoviePilot工具(内置工具: {builtin_tools_count} 个,插件工具: {plugin_tools_count} 个)" ) else: logger.debug(f"成功创建 {len(tools)} 个MoviePilot工具") return tools @classmethod def create_catalog(cls, **tool_kwargs) -> ToolCatalogSnapshot: """在插件目录稳定窗口内构造一份完整本地工具快照。""" plugin_manager = PluginManager() for _attempt in range(cls.CATALOG_BUILD_MAX_ATTEMPTS): before_revision = plugin_manager.get_plugin_agent_tools_revision() tools = cls.create_tools(**tool_kwargs) after_revision = plugin_manager.get_plugin_agent_tools_revision() if before_revision == after_revision: return ToolCatalogSnapshot.from_tools( tools, plugin_revision=after_revision, factory_revision=cls.catalog_factory_revision(), ) raise ToolCatalogError("插件工具目录持续变化,无法建立当前快照")