diff --git a/packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts b/packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts index 81bf2d67a9..2ceb44f6c2 100644 --- a/packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts +++ b/packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts @@ -178,14 +178,26 @@ export const FileEditTool = buildTool({ const fs = getFsImplementation() - // Prevent OOM on multi-GB files. + // 预检:防止 OOM(超大文件)+ 防止编辑目录路径 + // 目录路径预检避免 AI 模型传入目录时触发原始 EISDIR 错误, + // 确保模型拿到清晰、可操作的指引消息。 try { - const { size } = await fs.stat(fullFilePath) - if (size > MAX_EDIT_FILE_SIZE) { + const fileStat = await fs.stat(fullFilePath) + // 预检:如果路径已是一个存在的目录,及时拒绝并给出指引 + // 避免 AI 模型传入目录路径后 Edit 工具读取文件时抛 EISDIR。 + if (fileStat.isDirectory()) { return { result: false, behavior: 'ask', - message: `File is too large to edit (${formatFileSize(size)}). Maximum editable file size is ${formatFileSize(MAX_EDIT_FILE_SIZE)}.`, + message: `Cannot edit '${file_path}': the specified path is an existing directory. Use Bash ls to list files in this directory, or specify a filename path to edit a specific file.`, + errorCode: 10, + } + } + if (fileStat.size > MAX_EDIT_FILE_SIZE) { + return { + result: false, + behavior: 'ask', + message: `File is too large to edit (${formatFileSize(fileStat.size)}). Maximum editable file size is ${formatFileSize(MAX_EDIT_FILE_SIZE)}.`, errorCode: 10, } } diff --git a/packages/builtin-tools/src/tools/FileEditTool/prompt.ts b/packages/builtin-tools/src/tools/FileEditTool/prompt.ts index 5b6031fc9c..e399897cc3 100644 --- a/packages/builtin-tools/src/tools/FileEditTool/prompt.ts +++ b/packages/builtin-tools/src/tools/FileEditTool/prompt.ts @@ -24,5 +24,6 @@ Usage:${getPreReadInstruction()} - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.${minimalUniquenessHint} -- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.` +- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. +- The file_path must be a file path, not a directory path. If the path resolves to an existing directory, the tool will reject it. Use a path that points to an existing file.` } diff --git a/packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts b/packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts index 910c1e9beb..ef101d422c 100644 --- a/packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts +++ b/packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts @@ -464,6 +464,23 @@ export const FileReadTool = buildTool({ return { result: true } } + // 预检:如果路径已是一个存在的目录,及时拒绝并给出指引 + // 避免 AI 模型用 Read 工具读取目录路径时抛出原始 EISDIR 错误, + // 模型拿到错误后不知该用 ls 列目录,可能做出错误恢复决策。 + try { + const fileStat = await getFsImplementation().stat(fullFilePath) + if (fileStat.isDirectory()) { + return { + result: false, + message: `Cannot read '${file_path}': the specified path is an existing directory. Use Bash ls to list files in this directory, or specify a filename path to read a specific file.`, + errorCode: 10, + } + } + } catch (e) { + if (!isENOENT(e)) throw e + // ENOENT = 文件还不存在,放行,call() 会通过 findSimilarFile 给出友好建议 + } + // Binary extension check (string check on extension only, no I/O). // PDF, images, and SVG are excluded - this tool renders them natively. const ext = path.extname(fullFilePath).toLowerCase() diff --git a/packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts b/packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts index 1b339911c8..203795753f 100644 --- a/packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts +++ b/packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts @@ -184,6 +184,17 @@ export const FileWriteTool = buildTool({ let fileMtimeMs: number try { const fileStat = await fs.stat(fullFilePath) + // 预检:如果路径已是一个存在的目录,及时拒绝并给出指引 + // 避免 AI 模型传入目录路径(如 my-docs/analysis/api/)后, + // Write 工具先 mkdir 再写文件时抛出原始 EISDIR 错误, + // 导致模型不知如何处理、可能误删目录内容。 + if (fileStat.isDirectory()) { + return { + result: false, + message: `Cannot write to '${file_path}': the specified path is an existing directory. To write a file inside this directory, use a path that includes a filename with extension, e.g. '${file_path}/.md'. Use Bash ls to list existing files in this directory.`, + errorCode: 5, + } + } fileMtimeMs = fileStat.mtimeMs } catch (e) { if (isENOENT(e)) { diff --git a/packages/builtin-tools/src/tools/FileWriteTool/prompt.ts b/packages/builtin-tools/src/tools/FileWriteTool/prompt.ts index aa18606fe0..bd0be9ac75 100644 --- a/packages/builtin-tools/src/tools/FileWriteTool/prompt.ts +++ b/packages/builtin-tools/src/tools/FileWriteTool/prompt.ts @@ -14,5 +14,6 @@ Usage: - This tool will overwrite the existing file if there is one at the provided path.${getPreReadInstruction()} - Prefer the Edit tool for modifying existing files \u2014 it only sends the diff. Only use this tool to create new files or for complete rewrites. - NEVER create documentation files (*.md) or README files unless explicitly requested by the User. -- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.` +- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked. +- The file_path must be a distinct file path, not a directory path. If the path resolves to an existing directory, the tool will reject it with a clear error message. Use a path that includes a filename with an appropriate extension (e.g., \`my-docs/analysis/api/report.md\`).` }