feat: 新增修改下载任务Agent工具,查询下载任务支持返回标签

- 新增 modify_download Agent工具,支持通过hash修改下载任务的标签、开始和暂停下载
- 在 ChainBase 及三个下载器模块中新增 set_torrents_tag 方法
- DownloadingTorrent schema 新增 tags 字段
- 各下载器模块构建 DownloadingTorrent 时填充 tags
- query_download_tasks 工具输出中新增 tags 字段
This commit is contained in:
jxxghp
2026-03-24 18:32:07 +08:00
parent 4fbd2a7612
commit aae50004b1
8 changed files with 190 additions and 0 deletions

View File

@@ -36,6 +36,7 @@ from app.agent.tools.impl.query_workflows import QueryWorkflowsTool
from app.agent.tools.impl.run_workflow import RunWorkflowTool
from app.agent.tools.impl.update_site_cookie import UpdateSiteCookieTool
from app.agent.tools.impl.delete_download import DeleteDownloadTool
from app.agent.tools.impl.modify_download import ModifyDownloadTool
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
@@ -85,6 +86,7 @@ class MoviePilotToolFactory:
DeleteSubscribeTool,
QueryDownloadTasksTool,
DeleteDownloadTool,
ModifyDownloadTool,
QueryDownloadersTool,
QuerySitesTool,
UpdateSiteTool,

View File

@@ -0,0 +1,123 @@
"""修改下载任务工具"""
from typing import Optional, Type, List
from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.chain.download import DownloadChain
from app.log import logger
class ModifyDownloadInput(BaseModel):
"""修改下载任务工具的输入参数模型"""
explanation: str = Field(
...,
description="Clear explanation of why this tool is being used in the current context",
)
hash: str = Field(
..., description="Task hash (can be obtained from query_download_tasks tool)"
)
action: Optional[str] = Field(
None,
description="Action to perform on the task: 'start' to resume downloading, 'stop' to pause downloading. "
"If not provided, no start/stop action will be performed.",
)
tags: Optional[List[str]] = Field(
None,
description="List of tags to set on the download task. If provided, these tags will be added to the task. "
"Example: ['movie', 'hd']",
)
downloader: Optional[str] = Field(
None,
description="Name of specific downloader (optional, if not provided will search all downloaders)",
)
class ModifyDownloadTool(MoviePilotTool):
"""修改下载任务工具"""
name: str = "modify_download"
description: str = (
"Modify a download task in the downloader by task hash. "
"Supports: 1) Setting tags on a download task, "
"2) Starting (resuming) a paused download task, "
"3) Stopping (pausing) a downloading task. "
"Multiple operations can be performed in a single call."
)
args_schema: Type[BaseModel] = ModifyDownloadInput
def get_tool_message(self, **kwargs) -> Optional[str]:
hash_value = kwargs.get("hash", "")
action = kwargs.get("action")
tags = kwargs.get("tags")
downloader = kwargs.get("downloader")
parts = [f"正在修改下载任务: {hash_value}"]
if action == "start":
parts.append("操作: 开始下载")
elif action == "stop":
parts.append("操作: 暂停下载")
if tags:
parts.append(f"标签: {', '.join(tags)}")
if downloader:
parts.append(f"下载器: {downloader}")
return " | ".join(parts)
async def run(
self,
hash: str,
action: Optional[str] = None,
tags: Optional[List[str]] = None,
downloader: Optional[str] = None,
**kwargs,
) -> str:
logger.info(
f"执行工具: {self.name}, 参数: hash={hash}, action={action}, tags={tags}, downloader={downloader}"
)
try:
# 校验 hash 格式
if len(hash) != 40 or not all(c in "0123456789abcdefABCDEF" for c in hash):
return "参数错误hash 格式无效,请先使用 query_download_tasks 工具获取正确的 hash。"
# 校验参数:至少需要一个操作
if not action and not tags:
return "参数错误:至少需要指定 actionstart/stop或 tags 中的一个。"
# 校验 action 参数
if action and action not in ("start", "stop"):
return f"参数错误action 只支持 'start'(开始下载)或 'stop'(暂停下载),收到: '{action}'"
download_chain = DownloadChain()
results = []
# 设置标签
if tags:
tag_result = download_chain.set_torrents_tag(
hashs=[hash], tags=tags, downloader=downloader
)
if tag_result:
results.append(f"成功设置标签:{', '.join(tags)}")
else:
results.append(f"设置标签失败,请检查任务是否存在或下载器是否可用")
# 执行开始/暂停操作
if action:
action_result = download_chain.set_downloading(
hash_str=hash, oper=action, name=downloader
)
action_desc = "开始" if action == "start" else "暂停"
if action_result:
results.append(f"成功{action_desc}下载任务")
else:
results.append(
f"{action_desc}下载任务失败,请检查任务是否存在或下载器是否可用"
)
return f"下载任务 {hash}" + "".join(results)
except Exception as e:
logger.error(f"修改下载任务失败: {e}", exc_info=True)
return f"修改下载任务时发生错误: {str(e)}"

View File

@@ -214,6 +214,7 @@ class QueryDownloadTasksTool(MoviePilotTool):
"state": d.state,
"upspeed": d.upspeed,
"dlspeed": d.dlspeed,
"tags": d.tags,
"left_time": d.left_time
}
# 精简 media 字段

View File

@@ -1034,6 +1034,18 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("stop_torrents", hashs=hashs, downloader=downloader)
def set_torrents_tag(
self, hashs: Union[list, str], tags: list, downloader: Optional[str] = None
) -> bool:
"""
设置种子标签
:param hashs: 种子Hash
:param tags: 标签列表
:param downloader: 下载器
:return: bool
"""
return self.run_module("set_torrents_tag", hashs=hashs, tags=tags, downloader=downloader)
def torrent_files(
self, tid: str, downloader: Optional[str] = None
) -> Optional[Union[TorrentFilesList, List[File]]]:

View File

@@ -318,6 +318,7 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
state="paused" if torrent.get('state') in ("paused", "pausedDL") else "downloading",
dlspeed=StringUtils.str_filesize(torrent.get('dlspeed')),
upspeed=StringUtils.str_filesize(torrent.get('upspeed')),
tags=torrent.get('tags'),
left_time=StringUtils.str_secends(
(torrent.get('total_size') - torrent.get('completed')) / torrent.get(
'dlspeed')) if torrent.get(
@@ -356,6 +357,21 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
return None
return server.delete_torrents(delete_file=delete_file, ids=hashs)
def set_torrents_tag(self, hashs: Union[str, list], tags: list,
downloader: Optional[str] = None) -> Optional[bool]:
"""
设置种子标签
:param hashs: 种子Hash
:param tags: 标签列表
:param downloader: 下载器
:return: bool
"""
server: Qbittorrent = self.get_instance(downloader)
if not server:
return None
server.set_torrents_tag(ids=hashs, tags=tags)
return True
def start_torrents(self, hashs: Union[list, str],
downloader: Optional[str] = None) -> Optional[bool]:
"""

View File

@@ -391,6 +391,7 @@ class RtorrentModule(_ModuleBase, _DownloaderBase[Rtorrent]):
else "downloading",
dlspeed=StringUtils.str_filesize(dlspeed),
upspeed=StringUtils.str_filesize(upspeed),
tags=torrent.get("tags"),
left_time=StringUtils.str_secends(
(total_size - completed) / dlspeed
)
@@ -445,6 +446,22 @@ class RtorrentModule(_ModuleBase, _DownloaderBase[Rtorrent]):
return None
return server.delete_torrents(delete_file=delete_file, ids=hashs)
def set_torrents_tag(
self, hashs: Union[str, list], tags: list,
downloader: Optional[str] = None,
) -> Optional[bool]:
"""
设置种子标签
:param hashs: 种子Hash
:param tags: 标签列表
:param downloader: 下载器
:return: bool
"""
server: Rtorrent = self.get_instance(downloader)
if not server:
return None
return server.set_torrents_tag(ids=hashs, tags=tags)
def start_torrents(
self, hashs: Union[list, str], downloader: Optional[str] = None
) -> Optional[bool]:

View File

@@ -309,6 +309,7 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
state="paused" if torrent.status == "stopped" else "downloading",
dlspeed=StringUtils.str_filesize(dlspeed),
upspeed=StringUtils.str_filesize(upspeed),
tags=",".join(torrent.labels or []),
left_time=StringUtils.str_secends(torrent.left_until_done / dlspeed) if dlspeed > 0 else ''
))
finally:
@@ -353,6 +354,23 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
return None
return server.delete_torrents(delete_file=delete_file, ids=hashs)
def set_torrents_tag(self, hashs: Union[str, list], tags: list,
downloader: Optional[str] = None) -> Optional[bool]:
"""
设置种子标签
:param hashs: 种子Hash
:param tags: 标签列表
:param downloader: 下载器
:return: bool
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
if not server:
return None
# 获取原标签TR默认会覆盖需追加
org_tags = server.get_torrent_tags(ids=hashs)
return server.set_torrent_tag(ids=hashs, tags=tags, org_tags=org_tags)
def start_torrents(self, hashs: Union[list, str],
downloader: Optional[str] = None) -> Optional[bool]:
"""

View File

@@ -40,6 +40,7 @@ class DownloadingTorrent(BaseModel):
state: Optional[str] = 'downloading'
upspeed: Optional[str] = None
dlspeed: Optional[str] = None
tags: Optional[str] = None
media: Optional[dict] = Field(default_factory=dict)
userid: Optional[str] = None
username: Optional[str] = None