feat(tools): Phase T.1-T.4 complete - manifest system, registry, implementations, runtime, collaboration, scheduler

This commit is contained in:
2026-04-05 11:54:57 +08:00
parent fca7a7cf3d
commit 10d9340c53
30 changed files with 2891 additions and 4 deletions

View File

@@ -0,0 +1,242 @@
"""
File Operator Tool
File system operations tool with path safety checks.
"""
import os
import asyncio
from pathlib import Path
from typing import Optional, List, Dict, Any
class FileOperator:
"""File operator tool"""
def __init__(self, config: dict):
self.allowed_dirs = self._parse_allowed_dirs(config.get("allowed_directories", ""))
self.max_file_size = config.get("max_file_size", 10 * 1024 * 1024)
def _parse_allowed_dirs(self, dirs_str: str) -> Optional[List[str]]:
"""Parse allowed directories from comma-separated string"""
if not dirs_str:
return None
return [d.strip() for d in dirs_str.split(",") if d.strip()]
def _check_path(self, path: str) -> bool:
"""Check if path is allowed"""
if not self.allowed_dirs:
return True
resolved = Path(path).resolve()
return any(str(resolved).startswith(allowed) for allowed in self.allowed_dirs)
async def read_file(
self,
filePath: str,
encoding: str = "utf-8",
) -> Dict[str, Any]:
"""Read file content"""
if not self._check_path(filePath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(filePath)
if not path.exists():
return {"status": "error", "error": "File does not exist"}
if not path.is_file():
return {"status": "error", "error": "Path is not a file"}
try:
stat = path.stat()
if stat.st_size > self.max_file_size:
return {
"status": "error",
"error": f"File too large (> {self.max_file_size} bytes)",
}
suffix = path.suffix.lower()
if suffix in [".pdf", ".docx", ".xlsx", ".xls", ".csv"]:
return await self._read_binary_file(path)
content = path.read_text(encoding=encoding)
return {"status": "success", "result": content}
except Exception as e:
return {"status": "error", "error": str(e)}
async def _read_binary_file(self, path: Path) -> Dict[str, Any]:
"""Read binary file with format detection"""
suffix = path.suffix.lower()
if suffix == ".pdf":
return await self._read_pdf(path)
elif suffix in [".docx", ".doc"]:
return await self._read_docx(path)
elif suffix in [".xlsx", ".xls"]:
return await self._read_xlsx(path)
elif suffix == ".csv":
return await self._read_csv(path)
return {"status": "error", "error": f"Unsupported file format: {suffix}"}
async def _read_pdf(self, path: Path) -> Dict[str, Any]:
"""Read PDF file (placeholder - requires PyPDF2)"""
return {"status": "error", "error": "PDF reading requires PyPDF2 dependency"}
async def _read_docx(self, path: Path) -> Dict[str, Any]:
"""Read DOCX file (placeholder - requires python-docx)"""
return {"status": "error", "error": "DOCX reading requires python-docx dependency"}
async def _read_xlsx(self, path: Path) -> Dict[str, Any]:
"""Read XLSX file (placeholder - requires openpyxl)"""
return {"status": "error", "error": "XLSX reading requires openpyxl dependency"}
async def _read_csv(self, path: Path) -> Dict[str, Any]:
"""Read CSV file"""
try:
import csv
rows = []
with open(path, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
rows.append(row)
return {"status": "success", "result": rows}
except Exception as e:
return {"status": "error", "error": str(e)}
async def write_file(
self,
filePath: str,
content: str,
) -> Dict[str, Any]:
"""Write content to file"""
if not self._check_path(filePath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(filePath)
if path.exists():
path = self._get_unique_path(path)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
return {
"status": "success",
"result": f"File saved: {path.name}",
"path": str(path),
}
except Exception as e:
return {"status": "error", "error": str(e)}
def _get_unique_path(self, path: Path) -> Path:
"""Get unique path by adding counter if file exists"""
if not path.exists():
return path
stem = path.stem
suffix = path.suffix
parent = path.parent
counter = 1
while True:
new_path = parent / f"{stem}({counter}){suffix}"
if not new_path.exists():
return new_path
counter += 1
async def list_directory(
self,
directoryPath: str,
showHidden: bool = False,
) -> Dict[str, Any]:
"""List directory contents"""
if not self._check_path(directoryPath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(directoryPath)
if not path.exists():
return {"status": "error", "error": "Directory does not exist"}
if not path.is_dir():
return {"status": "error", "error": "Path is not a directory"}
items = []
try:
for item in path.iterdir():
if not showHidden and item.name.startswith("."):
continue
items.append(
{
"name": item.name,
"type": "directory" if item.is_dir() else "file",
"size": item.stat().st_size if item.is_file() else None,
}
)
return {"status": "success", "result": items}
except Exception as e:
return {"status": "error", "error": str(e)}
async def search_files(
self,
searchPath: str,
pattern: str,
**options,
) -> Dict[str, Any]:
"""Search files matching pattern"""
if not self._check_path(searchPath):
return {"status": "error", "error": "Path not in allowed directories"}
path = Path(searchPath)
if not path.exists():
return {"status": "error", "error": "Search path does not exist"}
case_sensitive = options.get("caseSensitive", False)
file_type = options.get("fileType", "all")
include_hidden = options.get("includeHidden", False)
import fnmatch
results = []
try:
for item in path.rglob("*"):
if not include_hidden and item.name.startswith("."):
continue
name = item.name if case_sensitive else item.name.lower()
pat = pattern if case_sensitive else pattern.lower()
if not fnmatch.fnmatch(name, pat):
continue
if file_type == "file" and item.is_dir():
continue
if file_type == "directory" and item.is_file():
continue
results.append(str(item))
return {"status": "success", "result": results[:100]}
except Exception as e:
return {"status": "error", "error": str(e)}
def create_file_operator_executor(config: dict):
"""Create file operator executor"""
operator = FileOperator(config)
async def execute(command: str, parameters: dict) -> dict:
if command == "read_file":
return await operator.read_file(**parameters)
elif command == "write_file":
return await operator.write_file(**parameters)
elif command == "list_directory":
return await operator.list_directory(**parameters)
elif command == "search_files":
return await operator.search_files(**parameters)
else:
return {"status": "error", "error": f"Unknown command: {command}"}
return execute