mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-08 22:53:43 +08:00
194 lines
7.2 KiB
Python
194 lines
7.2 KiB
Python
"""查询文件系统目录内容工具"""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional, Type
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.agent.tools.base import MoviePilotTool
|
|
from app.agent.tools.tags import ToolTag
|
|
from app.chain.storage import StorageChain
|
|
from app.log import logger
|
|
from app.schemas.file import FileItem
|
|
from app.utils.string import StringUtils
|
|
|
|
|
|
DEFAULT_DIRECTORY_PAGE_SIZE = 50
|
|
MAX_DIRECTORY_PAGE_SIZE = 200
|
|
|
|
|
|
class ListDirectoryInput(BaseModel):
|
|
"""查询文件系统目录内容工具的输入参数模型"""
|
|
path: str = Field(..., description="Directory path to list contents (e.g., '/home/user/downloads' or 'C:/Downloads')")
|
|
storage: Optional[str] = Field("local", description="Storage type (default: 'local' for local file system, can be 'smb', 'alist', etc.)")
|
|
sort_by: Optional[str] = Field("name", description="Sort order: 'name' for alphabetical sorting, 'time' for modification time sorting (default: 'name')")
|
|
limit: Optional[int] = Field(
|
|
DEFAULT_DIRECTORY_PAGE_SIZE,
|
|
ge=1,
|
|
le=MAX_DIRECTORY_PAGE_SIZE,
|
|
description=(
|
|
f"Maximum items to return in this page (default: {DEFAULT_DIRECTORY_PAGE_SIZE}, "
|
|
f"maximum: {MAX_DIRECTORY_PAGE_SIZE})"
|
|
),
|
|
)
|
|
offset: Optional[int] = Field(
|
|
0,
|
|
ge=0,
|
|
description="Number of sorted directory items to skip before this page",
|
|
)
|
|
|
|
|
|
class ListDirectoryTool(MoviePilotTool):
|
|
"""分页查询本地或远程存储目录中的文件和子目录。"""
|
|
|
|
name: str = "list_directory"
|
|
tags: list[str] = [
|
|
ToolTag.Read,
|
|
ToolTag.Directory,
|
|
ToolTag.File,
|
|
]
|
|
description: str = (
|
|
"List actual files and folders in a file system directory (NOT configuration). "
|
|
"Shows files and subdirectories with their names, types, sizes, and modification "
|
|
f"times. Returns a page of up to {DEFAULT_DIRECTORY_PAGE_SIZE} items with total "
|
|
f"count and next offset; limit is capped at {MAX_DIRECTORY_PAGE_SIZE}. "
|
|
"Use 'query_directory_settings' to query directory configuration settings."
|
|
)
|
|
args_schema: Type[BaseModel] = ListDirectoryInput
|
|
|
|
def get_tool_message(self, **kwargs) -> Optional[str]:
|
|
"""根据目录参数生成友好的提示消息"""
|
|
path = kwargs.get("path", "")
|
|
storage = kwargs.get("storage", "local")
|
|
|
|
message = f"查询目录: {path}"
|
|
if storage != "local":
|
|
message += f" [存储: {storage}]"
|
|
|
|
return message
|
|
|
|
@staticmethod
|
|
def _list_directory_sync(
|
|
path: str,
|
|
storage: Optional[str] = "local",
|
|
sort_by: Optional[str] = "name",
|
|
limit: Optional[int] = DEFAULT_DIRECTORY_PAGE_SIZE,
|
|
offset: Optional[int] = 0,
|
|
) -> str:
|
|
"""
|
|
目录遍历可能触发本地磁盘或远程存储请求,统一放到线程池中执行并分页返回。
|
|
"""
|
|
if not path:
|
|
return "错误:路径不能为空"
|
|
|
|
if storage == "local":
|
|
if not path.startswith("/") and not (len(path) > 1 and path[1] == ":"):
|
|
path = str(Path(path).resolve())
|
|
elif not path.startswith("/"):
|
|
path = "/" + path
|
|
|
|
fileitem = FileItem(storage=storage or "local", path=path, type="dir")
|
|
file_list = StorageChain().list_files(fileitem, recursion=False)
|
|
|
|
if file_list is None:
|
|
return f"无法访问目录:{path},请检查路径是否正确或存储是否可用"
|
|
if sort_by == "time":
|
|
file_list.sort(key=lambda x: x.modify_time or 0, reverse=True)
|
|
else:
|
|
file_list.sort(
|
|
key=lambda x: (
|
|
0 if x.type == "dir" else 1,
|
|
StringUtils.natural_sort_key(x.name or ""),
|
|
)
|
|
)
|
|
|
|
total_count = len(file_list)
|
|
normalized_limit = max(
|
|
1,
|
|
min(int(limit or DEFAULT_DIRECTORY_PAGE_SIZE), MAX_DIRECTORY_PAGE_SIZE),
|
|
)
|
|
normalized_offset = max(0, int(offset or 0))
|
|
limited_list = file_list[
|
|
normalized_offset : normalized_offset + normalized_limit
|
|
]
|
|
simplified_items = []
|
|
for item in limited_list:
|
|
size_str = StringUtils.str_filesize(item.size) if item.size else None
|
|
modify_time_str = None
|
|
if item.modify_time:
|
|
try:
|
|
modify_time_str = datetime.fromtimestamp(item.modify_time).strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
)
|
|
except (ValueError, OSError):
|
|
modify_time_str = str(item.modify_time)
|
|
|
|
simplified = {
|
|
"name": item.name,
|
|
"type": item.type,
|
|
"path": item.path,
|
|
"size": size_str,
|
|
"modify_time": modify_time_str,
|
|
}
|
|
if item.type == "file" and item.extension:
|
|
simplified["extension"] = item.extension
|
|
simplified_items.append(simplified)
|
|
|
|
returned_count = len(simplified_items)
|
|
has_more = normalized_offset + returned_count < total_count
|
|
return json.dumps(
|
|
{
|
|
"items": simplified_items,
|
|
"total_count": total_count,
|
|
"returned_count": returned_count,
|
|
"limit": normalized_limit,
|
|
"offset": normalized_offset,
|
|
"has_more": has_more,
|
|
"next_offset": (
|
|
normalized_offset + returned_count if has_more else None
|
|
),
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
|
|
async def run(self, path: str, storage: Optional[str] = "local",
|
|
sort_by: Optional[str] = "name",
|
|
limit: Optional[int] = DEFAULT_DIRECTORY_PAGE_SIZE,
|
|
offset: Optional[int] = 0,
|
|
**kwargs) -> str:
|
|
"""
|
|
分页查询指定目录的文件和子目录。
|
|
|
|
:param path: 要查询的目录路径
|
|
:param storage: 存储类型,默认为本地存储
|
|
:param sort_by: 排序方式,支持名称或修改时间
|
|
:param limit: 当前页最大条数,最高不超过工具上限
|
|
:param offset: 当前页起始偏移量
|
|
:return: 包含项目列表和分页元数据的 JSON 字符串
|
|
"""
|
|
logger.info(f"执行工具: {self.name}, 参数: path={path}, storage={storage}, sort_by={sort_by}")
|
|
|
|
try:
|
|
resolved_path, access_error = await self._check_local_storage_access(
|
|
path=path, storage=storage, operation="列出"
|
|
)
|
|
if access_error:
|
|
return access_error
|
|
if resolved_path:
|
|
path = str(resolved_path)
|
|
return await self.run_blocking(
|
|
"storage",
|
|
self._list_directory_sync,
|
|
path,
|
|
storage,
|
|
sort_by,
|
|
limit,
|
|
offset,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"查询目录内容失败: {e}", exc_info=True)
|
|
return f"查询目录内容时发生错误: {str(e)}"
|