feat(agent): add command-execute skill for intelligent command dispatch

- Enhance run_plugin_command tool to support all registered commands (system preset + plugin + other), not just plugin commands
- Add list_all_commands tool to discover all available commands with descriptions and categories
- Add command-execute skill that guides the agent to recognize user intent from natural language and match it to available system/plugin commands
This commit is contained in:
jxxghp
2026-04-06 23:45:31 +08:00
parent eb5e17a115
commit 7806267e92
4 changed files with 178 additions and 23 deletions

View File

@@ -51,6 +51,7 @@ 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_plugin_capabilities import QueryPluginCapabilitiesTool
from app.agent.tools.impl.run_plugin_command import RunPluginCommandTool
from app.agent.tools.impl.list_all_commands import ListAllCommandsTool
from app.core.plugin import PluginManager
from app.log import logger
from .base import MoviePilotTool
@@ -126,6 +127,7 @@ class MoviePilotToolFactory:
QueryInstalledPluginsTool,
QueryPluginCapabilitiesTool,
RunPluginCommandTool,
ListAllCommandsTool,
]
# 创建内置工具
for ToolClass in tool_definitions:

View File

@@ -0,0 +1,78 @@
"""查询所有可用命令工具(系统命令 + 插件命令)"""
import json
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.command import Command
from app.log import logger
class ListAllCommandsInput(BaseModel):
"""查询所有可用命令工具的输入参数模型"""
explanation: str = Field(
...,
description="Clear explanation of why this tool is being used in the current context",
)
class ListAllCommandsTool(MoviePilotTool):
name: str = "list_all_commands"
description: str = (
"List all available commands in the system, including system preset commands "
"(e.g. /cookiecloud, /sites, /subscribes, /downloading, /transfer, /restart, etc.) "
"and plugin-registered commands. "
"Use this tool to discover what commands are available before executing them with run_plugin_command. "
"This is especially useful when the user describes an action in natural language and you need to "
"find the matching command to fulfill their request."
)
args_schema: Type[BaseModel] = ListAllCommandsInput
require_admin: bool = True
def get_tool_message(self, **kwargs) -> Optional[str]:
"""生成友好的提示消息"""
return "正在查询所有可用命令"
async def run(self, **kwargs) -> str:
logger.info(f"执行工具: {self.name}")
try:
command_obj = Command()
all_commands = command_obj.get_commands()
if not all_commands:
return "当前没有可用的命令"
commands_list = []
for cmd, info in all_commands.items():
cmd_info = {
"command": cmd,
"description": info.get("description", ""),
}
if info.get("category"):
cmd_info["category"] = info["category"]
# 标识命令类型
if info.get("type") == "scheduler":
cmd_info["type"] = "scheduler"
elif info.get("pid"):
cmd_info["type"] = "plugin"
cmd_info["plugin_id"] = info["pid"]
else:
cmd_info["type"] = "system"
commands_list.append(cmd_info)
result = {
"total": len(commands_list),
"commands": commands_list,
}
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"查询可用命令失败: {e}", exc_info=True)
return json.dumps(
{"success": False, "message": f"查询可用命令时发生错误: {str(e)}"},
ensure_ascii=False,
)

View File

@@ -1,4 +1,4 @@
"""运行插件命令工具"""
"""运行插件/系统命令工具"""
import json
from typing import Optional, Type
@@ -6,14 +6,14 @@ from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.command import Command
from app.core.event import eventmanager
from app.core.plugin import PluginManager
from app.log import logger
from app.schemas.types import EventType, MessageChannel
class RunPluginCommandInput(BaseModel):
"""运行插件命令工具的输入参数模型"""
"""运行插件/系统命令工具的输入参数模型"""
explanation: str = Field(
...,
@@ -23,16 +23,20 @@ class RunPluginCommandInput(BaseModel):
...,
description="The slash command to execute, e.g. '/cookiecloud'. "
"Must start with '/'. Can include arguments after the command, e.g. '/command arg1 arg2'. "
"Use query_plugin_capabilities tool to discover available commands first.",
"Use query_plugin_capabilities tool to discover available plugin commands, "
"or list_all_commands tool to discover all available commands (including system commands).",
)
class RunPluginCommandTool(MoviePilotTool):
name: str = "run_plugin_command"
description: str = (
"Execute a plugin command by sending a CommandExcute event. "
"Plugin commands are slash-commands (starting with '/') registered by plugins. "
"Use the query_plugin_capabilities tool first to discover available commands and their descriptions. "
"Execute a system or plugin command by sending a CommandExcute event. "
"This tool supports ALL registered commands, including: "
"1) System preset commands (e.g. /cookiecloud, /sites, /subscribes, /downloading, /transfer, /restart, etc.) "
"2) Plugin commands registered by installed plugins. "
"Use the query_plugin_capabilities tool to discover plugin commands, "
"or the list_all_commands tool to discover all available commands. "
"The command will be executed asynchronously. "
"Note: This tool triggers the command execution but the actual processing happens in the background."
)
@@ -42,7 +46,7 @@ class RunPluginCommandTool(MoviePilotTool):
def get_tool_message(self, **kwargs) -> Optional[str]:
"""生成友好的提示消息"""
command = kwargs.get("command", "")
return f"正在执行插件命令: {command}"
return f"正在执行命令: {command}"
async def run(self, command: str, **kwargs) -> str:
logger.info(f"执行工具: {self.name}, 参数: command={command}")
@@ -52,21 +56,17 @@ class RunPluginCommandTool(MoviePilotTool):
if not command.startswith("/"):
command = f"/{command}"
# 验证命令是否存在
plugin_manager = PluginManager()
registered_commands = plugin_manager.get_plugin_commands()
# 从全局 Command 单例中验证命令是否存在(包含系统预设命令 + 插件命令 + 其他命令)
cmd_name = command.split()[0]
matched_command = None
for cmd in registered_commands:
if cmd.get("cmd") == cmd_name:
matched_command = cmd
break
command_obj = Command()
matched_command = command_obj.get(cmd_name)
if not matched_command:
# 列出可用命令帮助用户
# 列出所有可用命令帮助用户
all_commands = command_obj.get_commands()
available_cmds = [
f"{cmd.get('cmd')} - {cmd.get('desc', '无描述')}"
for cmd in registered_commands
f"{cmd} - {info.get('description', '无描述')}"
for cmd, info in all_commands.items()
]
result = {
"success": False,
@@ -99,14 +99,16 @@ class RunPluginCommandTool(MoviePilotTool):
"success": True,
"message": f"命令 {cmd_name} 已触发执行",
"command": command,
"command_desc": matched_command.get("desc", ""),
"plugin_id": matched_command.get("pid", ""),
"command_desc": matched_command.get("description", ""),
}
# 如果是插件命令附加插件ID
if matched_command.get("pid"):
result["plugin_id"] = matched_command["pid"]
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"执行插件命令失败: {e}", exc_info=True)
logger.error(f"执行命令失败: {e}", exc_info=True)
return json.dumps(
{"success": False, "message": f"执行插件命令时发生错误: {str(e)}"},
{"success": False, "message": f"执行命令时发生错误: {str(e)}"},
ensure_ascii=False,
)

View File

@@ -0,0 +1,73 @@
---
name: command-execute
description: >-
Use this skill when the user's intent is to execute a system or plugin function. Applicable scenarios include:
1) The user sends a slash command starting with / (e.g. /cookiecloud, /sites, /subscribes, etc.);
2) The user describes an action in natural language that can be fulfilled by a system or plugin command
(e.g. "sync sites", "show subscriptions", "refresh subscriptions", "check downloads", etc.).
This skill helps you identify the user's intent, find the matching command, extract necessary parameters,
and execute the corresponding command.
allowed-tools: list_all_commands query_plugin_capabilities run_plugin_command
---
# Command Execute
Use this skill to identify user intent and invoke the corresponding system or plugin command.
## When to Use
- The user sends a `/xxx` slash command (execute directly)
- The user describes an action in natural language, for example:
- "Sync sites" → `/cookiecloud`
- "Show my subscriptions" → `/subscribes`
- "Refresh subscriptions" → `/subscribe_refresh`
- "What's downloading?" → `/downloading`
- "Organize downloaded files" → `/transfer`
- "Clear cache" → `/clear_cache`
- "Restart the system" → `/restart`
- "Pause all QB tasks" → `/pause_torrents` (plugin command)
## Tools
- `list_all_commands` — List all available commands (system + plugin), returns command name, description, and category
- `query_plugin_capabilities` — Query detailed plugin capabilities (commands, actions, scheduled services)
- `run_plugin_command` — Execute a specified command (works for both system and plugin commands)
## Workflow
### Step 1: Identify User Intent
Determine whether the user's message is requesting the execution of a command:
- **Direct command**: Message starts with `/`, e.g. `/sites`, `/subscribes` → skip to Step 3
- **Natural language**: The user describes an actionable request → continue to Step 2
### Step 2: Find Matching Command
Use `list_all_commands` to retrieve all available commands. Match the user's described intent against the `description` and `category` fields of each command.
If the user's description involves a specific plugin's functionality, additionally use `query_plugin_capabilities` to query that plugin's detailed capabilities.
**Matching strategy**:
- Prefer exact matches on command description
- Then narrow down by category and match
- If no matching command is found, inform the user that no corresponding function is available
### Step 3: Extract Parameters and Execute
Some commands support additional arguments (space-separated after the command), for example:
- `/redo <history_id>` — Manually re-organize a specific record
- `/subscribe_delete <name>` — Delete a specific subscription
Use `run_plugin_command` to execute the command in the format `/command_name arg1 arg2`.
### Step 4: Report Result
Command execution is asynchronous. After triggering, inform the user that the command has started. If the command does not exist, list available commands for reference.
## Important Notes
- Command execution requires admin privileges; the tool will automatically check permissions
- Both system and plugin commands are executed via the `run_plugin_command` tool — no need to distinguish between them
- If you are unsure which command matches the user's intent, use `list_all_commands` first to look up before deciding
- Never guess non-existent commands; always select from the available command list