From 0b2178a88d5560cdc8a1e546e8be208e77fbe8c0 Mon Sep 17 00:00:00 2001 From: zhangzhenfei Date: Tue, 30 Jun 2026 16:47:23 +0800 Subject: [PATCH] fix(core): return clear error when file read tool receives a directory path When astrbot_file_read_tool is called with a directory path, Windows reports Permission denied and other platforms report opaque errno messages. Check the path type before probing so the LLM gets actionable guidance instead. Fixes AstrBotDevs/AstrBot#9087 Co-authored-by: Cursor --- astrbot/core/computer/file_read_utils.py | 34 ++++++++++++++++++++---- tests/test_computer_fs_tools.py | 20 ++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/astrbot/core/computer/file_read_utils.py b/astrbot/core/computer/file_read_utils.py index 5b5fd9fc8e..1fffdde342 100644 --- a/astrbot/core/computer/file_read_utils.py +++ b/astrbot/core/computer/file_read_utils.py @@ -78,13 +78,26 @@ class ParsedDocument: text: str +def _directory_read_error(path: str) -> str: + return ( + f"Error: `{path}` is a directory, not a file. " + "Use a file path instead, or use `astrbot_execute_shell` " + "to list directory contents." + ) + + def _build_probe_script(path: str) -> str: + directory_error = _directory_read_error(path) return f""" import base64 import json from pathlib import Path path = Path({path!r}) +if path.is_dir(): + raise ValueError({directory_error!r}) +if not path.is_file(): + raise ValueError("Error: File does not exist: `{path}`") with path.open("rb") as file_obj: sample = file_obj.read({_FILE_SNIFF_BYTES}) print( @@ -652,13 +665,24 @@ async def read_file_tool_result( workspace_dir: str | None = None, ) -> ToolExecResult: if local_mode: + file_path = Path(path) + if file_path.is_dir(): + return _directory_read_error(path) + if not file_path.is_file(): + return f"Error: File does not exist: `{path}`" probe_payload = await _probe_local_file(path) else: - probe_payload = await _exec_python_json( - booter, - _build_probe_script(path), - action="file probe", - ) + try: + probe_payload = await _exec_python_json( + booter, + _build_probe_script(path), + action="file probe", + ) + except RuntimeError as exc: + probe_error = str(exc).removeprefix("file probe failed: ").strip() + if probe_error.startswith("Error:"): + return probe_error + raise sample_b64 = str(probe_payload.get("sample_b64", "") or "") sample = base64.b64decode(sample_b64) if sample_b64 else b"" size_bytes = int(probe_payload.get("size_bytes", 0) or 0) diff --git a/tests/test_computer_fs_tools.py b/tests/test_computer_fs_tools.py index 11571665a5..c5f7e32fff 100644 --- a/tests/test_computer_fs_tools.py +++ b/tests/test_computer_fs_tools.py @@ -407,6 +407,26 @@ def test_detect_text_encoding_allows_utf8_probe_cut_mid_character(): assert file_read_utils.detect_text_encoding(sample) in {"utf-8", "utf-8-sig"} +@pytest.mark.asyncio +async def test_file_read_tool_rejects_directory_path( + monkeypatch: pytest.MonkeyPatch, + tmp_path, +): + workspace = _setup_local_fs_tools(monkeypatch, tmp_path) + directory = workspace / "api" + directory.mkdir() + + result = await fs_tools.FileReadTool().call( + _make_context(), + path="api", + ) + + assert "is a directory, not a file" in result + assert "astrbot_execute_shell" in result + assert "Permission denied" not in result + assert "Is a directory" not in result + + @pytest.mark.asyncio async def test_file_read_tool_rejects_large_full_text_read_before_local_stream_read( monkeypatch: pytest.MonkeyPatch,