""" 文件操作工具模块 提供文件内容读取和目录内容查看功能,支持跨平台(Windows/Linux)。 所有操作以项目根目录为边界,不允许访问根目录之外的文件。 """ from __future__ import annotations import os import platform import subprocess from pathlib import Path from typing import Optional, List, Union, Dict, Any from langchain_core.tools import tool from dataflow_agent.logger import get_logger import dataflow_agent.utils as utils log = get_logger(__name__) # 获取项目根目录 PROJECT_ROOT = utils.get_project_root() def _is_path_within_project(path: Path) -> bool: """ 检查路径是否在项目根目录内 Args: path: 要检查的路径 Returns: bool: 如果路径在项目根目录内返回 True,否则返回 False """ try: resolved_path = path.resolve() resolved_root = PROJECT_ROOT.resolve() # 检查路径是否以项目根目录开头 return str(resolved_path).startswith(str(resolved_root)) except Exception as e: log.warning(f"路径检查失败: {e}") return False def _resolve_path(path_str: str) -> Path: """ 解析路径,支持相对路径和绝对路径 Args: path_str: 路径字符串 Returns: Path: 解析后的路径对象 """ path = Path(path_str) if not path.is_absolute(): # 相对路径基于项目根目录 path = PROJECT_ROOT / path return path.resolve() def read_file_content( file_path: str, start_line: Optional[int] = None, end_line: Optional[int] = None, encoding: str = "utf-8" ) -> Dict[str, Any]: """ 读取文件内容 Args: file_path: 文件路径(相对于项目根目录或绝对路径) start_line: 起始行号(从1开始,可选) end_line: 结束行号(包含,可选) encoding: 文件编码,默认 utf-8 Returns: Dict 包含: - success: 是否成功 - content: 文件内容(成功时) - total_lines: 文件总行数(成功时) - read_lines: 实际读取的行范围 [start, end](成功时) - error: 错误信息(失败时) """ try: path = _resolve_path(file_path) # 安全检查:确保路径在项目根目录内 if not _is_path_within_project(path): return { "success": False, "error": f"安全限制:不允许访问项目根目录之外的文件。项目根目录: {PROJECT_ROOT}" } # 检查文件是否存在 if not path.exists(): return { "success": False, "error": f"文件不存在: {path}" } # 检查是否为文件 if not path.is_file(): return { "success": False, "error": f"路径不是文件: {path}" } # 读取文件内容 try: with open(path, 'r', encoding=encoding) as f: lines = f.readlines() except UnicodeDecodeError: return { "success": False, "error": f"文件编码错误,无法使用 {encoding} 编码读取。请尝试其他编码或确认文件为文本文件。" } total_lines = len(lines) # 处理行号范围 actual_start = 1 actual_end = total_lines if start_line is not None: if start_line < 1: start_line = 1 actual_start = min(start_line, total_lines) if end_line is not None: if end_line < 1: end_line = 1 actual_end = min(end_line, total_lines) # 确保 start <= end if actual_start > actual_end: actual_start, actual_end = actual_end, actual_start # 提取指定行范围的内容(行号从1开始,索引从0开始) selected_lines = lines[actual_start - 1:actual_end] content = ''.join(selected_lines) log.info(f"[read_file_content] 成功读取文件: {path}, 行范围: {actual_start}-{actual_end}/{total_lines}") return { "success": True, "content": content, "total_lines": total_lines, "read_lines": [actual_start, actual_end], "file_path": str(path) } except Exception as e: log.error(f"[read_file_content] 读取文件失败: {e}") return { "success": False, "error": str(e) } def list_directory_content( dir_path: str, show_hidden: bool = False, recursive: bool = False, max_depth: int = 1 ) -> Dict[str, Any]: """ 查看目录内容,支持 Windows 和 Linux Args: dir_path: 目录路径(相对于项目根目录或绝对路径) show_hidden: 是否显示隐藏文件,默认 False recursive: 是否递归显示子目录,默认 False max_depth: 递归最大深度(仅当 recursive=True 时有效),默认 1 Returns: Dict 包含: - success: 是否成功 - content: 目录内容(成功时) - path: 目录绝对路径(成功时) - error: 错误信息(失败时) """ try: path = _resolve_path(dir_path) # 安全检查:确保路径在项目根目录内 if not _is_path_within_project(path): return { "success": False, "error": f"安全限制:不允许访问项目根目录之外的目录。项目根目录: {PROJECT_ROOT}" } # 检查目录是否存在 if not path.exists(): return { "success": False, "error": f"目录不存在: {path}" } # 检查是否为目录 if not path.is_dir(): return { "success": False, "error": f"路径不是目录: {path}" } # 根据操作系统选择命令 system = platform.system().lower() if system == "windows": # Windows 使用 dir 命令 cmd = ["cmd", "/c", "dir"] if show_hidden: cmd.append("/a") # 显示所有文件包括隐藏文件 if recursive: cmd.append("/s") # 递归显示 cmd.append(str(path)) else: # Linux/macOS 使用 ls 命令 cmd = ["ls", "-l"] if show_hidden: cmd.append("-a") # 显示隐藏文件 if recursive: cmd.append("-R") # 递归显示 cmd.append(str(path)) # 执行命令 try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=30, cwd=str(path.parent) # 在父目录执行,避免路径问题 ) output = result.stdout if result.returncode != 0 and result.stderr: output += f"\n[stderr]: {result.stderr}" except subprocess.TimeoutExpired: return { "success": False, "error": "命令执行超时(30秒)" } except FileNotFoundError as e: # 如果命令不存在,使用 Python 原生方式 log.warning(f"系统命令不可用,使用 Python 原生方式: {e}") output = _list_directory_python(path, show_hidden, recursive, max_depth) log.info(f"[list_directory_content] 成功列出目录: {path}") return { "success": True, "content": output, "path": str(path), "system": system } except Exception as e: log.error(f"[list_directory_content] 列出目录失败: {e}") return { "success": False, "error": str(e) } def _list_directory_python( path: Path, show_hidden: bool = False, recursive: bool = False, max_depth: int = 1, current_depth: int = 0, prefix: str = "" ) -> str: """ 使用 Python 原生方式列出目录内容(备用方案) Args: path: 目录路径 show_hidden: 是否显示隐藏文件 recursive: 是否递归 max_depth: 最大递归深度 current_depth: 当前深度 prefix: 输出前缀(用于缩进) Returns: str: 格式化的目录内容 """ lines = [] if current_depth == 0: lines.append(f"目录: {path}") lines.append("-" * 60) try: entries = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())) for entry in entries: # 跳过隐藏文件(如果不显示) if not show_hidden and entry.name.startswith('.'): continue # 获取文件信息 try: stat = entry.stat() size = stat.st_size is_dir = entry.is_dir() # 格式化大小 if is_dir: size_str = "