Skip to content

命令与文件读取工具:ExecuteCommand / ReadFile / SearchFiles / ListFiles

源码版本v4.0.10

职责

这一页讲 Cline 的四个「能力类」只读/执行工具,都注册在 IFullyManagedTool 接口下、由 ToolExecutor 统一调度:

  • ExecuteCommandToolHandler(ClineDefaultTool.BASH):在终端跑一条 shell 命令,带超时、多工作区、权限校验(class declaration:58)。
  • ReadFileToolHandler(ClineDefaultTool.FILE_READ):读单个文件,加 1-based 行号、支持 start_line/end_line 切片、对图片文件返回 image block(class declaration:142)。
  • SearchFilesToolHandler(ClineDefaultTool.SEARCH):基于 ripgrep 的正则搜索,支持跨多工作区并行搜、file_pattern 过滤(class declaration:21)。
  • ListFilesToolHandler(ClineDefaultTool.LIST_FILES):列目录,支持递归,200 条上限后截断(class declaration:17)。

这四个工具共享一套交互模式:handlePartialBlock 提前推送 UI 让用户看到「模型正在读 X」,execute 做校验(clineignore + 路径 + 参数)→ 审批 → PreToolUse hook → 真正执行 → 遥测。它们都尊重 config.isSubagentExecution:子 agent 执行时跳过部分 UI 推送和 ask,直接干活。

它们和 WriteToFileToolHandler 的根本差异是「只读或外部副作用」——读、列、搜不改变文件系统状态;execute_command 改变状态但通过 terminal 走,不进 diff 视图,所以这套工具没有 diff / userEdits 回流路径。

设计动机

  • mistake 计数统一规则:每个工具都在缺参数或 clineignore 命中时 consecutiveMistakeCount++,在核心操作成功后 = 0,让 YOLO 模式的连错限额能跨工具累计(reset counter:377)。
  • 命令分类超时:execute_command 把常见长跑命令(npm install、cargo build、pytest 等)匹配出 300 秒超时,其他 30 秒,避免短命令被长超时拖住(LONG_RUNNING patterns:22-34)。
  • 命令权限双闸:CLINE_COMMAND_PERMISSIONS 环境变量做的 segment 级白名单 + clineignore 路径校验,两层独立拒绝(permission checks:162-190)。
  • 读文件去重缓存:fileReadCache 只存元数据(readCount + mtime + imageBlock),不存内容,命中且 mtime 没变就直接返回「已读过」提示,防止模型烧 token 反复读同一文件(dedup cache:298-314)。
  • 行号化展示:formatFileContentWithLineNumbers 给每行加 N | 前缀并附 continuation 提示,让模型用 start_line 精确续读(formatFileContentWithLineNumbers:79)。
  • 多工作区并行搜:SearchFilesToolHandler 在多工作区模式下把 searchPaths 全部并行 Promise.all 跑,再按 workspace 名分桶汇总(parallel search:279-285)。
  • ListFiles 200 上限:listFiles 第三参 limit=200,超出后返回 didHitLimit=true,formatFilesList 末尾附提示让模型用更具体的 path 缩小范围(listFiles call:100)。

关键文件

数据流

ExecuteCommand 是这四个里最复杂的,核心是审批闸门与超时解析。模型给的 requires_approval + command 字符串决定走哪条审批路径:

typescript
// apps/vscode/src/core/task/tools/handlers/ExecuteCommandToolHandler.ts
timeoutSeconds = resolveCommandTimeoutSeconds(
    command,
    timeoutParam,
    config.yoloModeToggled || config.vscodeTerminalExecutionMode === "backgroundExec",
)
// ...
if (
    config.isSubagentExecution ||
    (!requiresApprovalPerLLM && autoApproveSafe) ||
    (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)
) {
    // Auto-approve flow
    await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
    await config.callbacks.say("command", actualCommand, undefined, undefined, false)
    didAutoApprove = true
} else {
    // Manual approval flow
    const didApprove = await ToolResultUtils.askApprovalAndPushFeedback(
        "command",
        actualCommand + `${autoApproveSafe && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`,
        config,
    )
    if (!didApprove) {
        return formatResponse.toolDenied()
    }
}

autoApprover.shouldAutoApproveTool 返回 [autoApproveSafe, autoApproveAll] 二元组:前者是「safe 命令自动批」,后者是「all 命令自动批」。模型说 risky(requires_approval=true)且只配了 safe 自动批时,走手动审批且在命令后追加强审批提示串(auto-approve logic:223-227)。审批通过后 PreToolUse hook、然后 30 秒通知定时器(只在自动批 + 通知开时设),最后 executeCommandTool(finalCommand, timeoutSeconds) 在 terminal 跑。完成后整个 fileReadCache 清空。

ReadFileToolHandler 的核心是缓存去重:

typescript
// apps/vscode/src/core/task/tools/handlers/ReadFileToolHandler.ts
const cacheKey = absolutePath.toLowerCase()
const cached = config.taskState.fileReadCache.get(cacheKey)
if (cached) {
    try {
        const stat = await import("node:fs/promises").then((fs) => fs.stat(absolutePath))
        if (stat.mtimeMs !== cached.mtime) {
            config.taskState.fileReadCache.delete(cacheKey)
        }
    } catch {
        config.taskState.fileReadCache.delete(cacheKey)
    }
}

mtime 变了(用户在编辑器里手改了)就 evict,否则复用缓存。缓存里不存内容省内存,命中后仍然从盘上重读。readCount 到 3 次以上返回 DUPLICATE READ 强提示。新读成功后 extractFileContent 拿内容(图片文件返回 imageBlock 推到 userMessageContent),把 mtime + readCount=1 存进缓存。

SearchFiles 和 ListFiles 都做路径解析 + clineignore + 审批 + 执行 + 格式化,代码结构相似。SearchFiles 多了多工作区并行:

typescript
// apps/vscode/src/core/task/tools/handlers/SearchFilesToolHandler.ts
const searchPromises = searchPaths.map(({ absolutePath, workspaceName, workspaceRoot }) =>
    this.executeSearch(config, absolutePath, workspaceName, workspaceRoot, regex, filePattern),
)
const searchResults = await Promise.all(searchPromises)
const results = this.formatSearchResults(config, searchResults, searchPaths)

regexSearchFiles 调底层 ripgrep,结果按 workspace 名分桶汇总。

边界与失败

  • 缺参数:execute_command 缺 command / requires_approval,read/search/list 缺 path,search 缺 regex 都 consecutiveMistakeCount++ + 走 sayAndCreateMissingParamError(missing params:100-112)。
  • 命令被拒:CLINE_COMMAND_PERMISSIONS 拒时返回 permissionDeniedError,带 failedSegment 或 matchedPattern 给模型可修正信息(permission denied:162-181)。
  • 长跑命令通知:auto-approve + 通知开时,30 秒后弹系统通知「Command is still running」,避免用户以为卡死(timeout notification:294-303)。
  • 读文件不存在:extractFileContent 抛错被 catch 成 toolError("Error reading file: ..."),不向 Task 上层抛,让模型看错误自己改路径(read error:361-373)。
  • 缓存 mtime 不一致:stat 失败或 mtime 变了都 evict 缓存,保证下次读到最新内容(mtime check:304-313)。
  • search path 解析失败:parseWorkspaceInlinePath 抛错时返回 Error resolving search path: ...,mistake++(path resolution error:233-242)。
  • list files 超限:listFiles 抛或路径不对返回 Error listing files: ...,成功后 didHitLimit 由 formatFilesList 转成提示(list error:101-105)。
  • 子 agent 跳过 UI:四个 handler 都在 isSubagentExecution 时跳过 partial say 和 ask 流程,直接干完返回(subagent skip:155-157)。

小结

这四个 handler 是 Cline 的「眼睛和手」:ExecuteCommand 跑 shell,ReadFile 读盘,SearchFiles 跑 ripgrep,ListFiles 列目录。它们共享「校验 → 审批 → hook → 执行 → 遥测」的骨架,差异在执行细节:execute_command 有命令分类超时和双闸审批,read_file 有 mtime 去重缓存,search 支持多工作区并行,list 有 200 上限。所有工具的 consecutiveMistakeCount 在成功后归零,失败累计到 YOLO 上限就停。

想往里钻可以接着看:

  • 写盘工具:/edit-tools/write-to-file
  • 多文件补丁:/edit-tools/apply-patch
  • 浏览器工具:/cap-tools/browser
  • Web fetch/search:/cap-tools/web

对照官方资料:Cline 文档 · README