Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Read 工具

Read 工具

Read 工具用于读取文件系统中的文件内容。

概述

Read 工具允许 AI 助手读取项目中的任何文件。它支持:

  • 绝对路径和相对路径
  • 行号偏移和限制
  • 二进制文件检测
  • 图像和 PDF 文件支持(作为附件)
  • 自动建议相似文件名(文件不存在时)

定义位置

  • packages/opencode/src/tool/read.ts:16-45
  • packages/opencode/src/tool/read.txt

参数说明

参数名类型必填说明
filePathstring是要读取的文件路径
offsetnumber否开始读取的行号(从 0 开始,0-based)
limitnumber否要读取的行数(默认 2000)

TypeScript 类型定义

export type ReadInput = {
  filePath: string
  offset?: number // 默认 0
  limit?: number // 默认 2000
}

export type ReadOutput = {
  title: string // 相对路径
  output: string // 文件内容(带行号)
  metadata: {
    preview: string // 前 20 行的预览
    truncated: boolean // 是否被截断
  }
  attachments?: MessageV2.FilePart[] // 图像/PDF 附件
}

常量

定义位置:packages/opencode/src/tool/read.ts:12-14

const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
const MAX_BYTES = 50 * 1024 // 50 KB

说明:

  • DEFAULT_READ_LIMIT: 默认读取行数
  • MAX_LINE_LENGTH: 单行最大长度
  • MAX_BYTES: 最大字节数(50 KB),用于截断

工作流程

1. 路径解析

let filepath = params.filePath
if (!path.isAbsolute(filepath)) {
  filepath = path.join(process.cwd(), filepath)
}
const title = path.relative(Instance.worktree, filepath)

2. 权限检查

await assertExternalDirectory(ctx, filepath, {
  bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
})

3. 权限请求

await ctx.ask({
  permission: "read",
  patterns: [filepath],
  always: ["*"],
  metadata: {},
})

4. 文件存在性检查

const file = Bun.file(filepath)
if (!(await file.exists())) {
  const dir = path.dirname(filepath)
  const base = path.basename(filepath)

  // 查找相似的文件名
  const dirEntries = fs.readdirSync(dir)
  const suggestions = dirEntries
    .filter(
      (entry) => entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
    )
    .map((entry) => path.join(dir, entry))
    .slice(0, 3)

  if (suggestions.length > 0) {
    throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
  }

  throw new Error(`File not found: ${filepath}`)
}

5. 图像和 PDF 处理

const isImage =
  file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet"
const isPdf = file.type === "application/pdf"

if (isImage || isPdf) {
  const mime = file.type
  const msg = `${isImage ? "Image" : "PDF"} read successfully`

  return {
    title,
    output: msg,
    metadata: {
      preview: msg,
      truncated: false,
    },
    attachments: [
      {
        id: Identifier.ascending("part"),
        sessionID: ctx.sessionID,
        messageID: ctx.messageID,
        type: "file",
        mime,
        url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,
      },
    ],
  }
}

6. 二进制文件检测

async function isBinaryFile(filepath: string, file: Bun.BunFile): Promise<boolean> {
  const ext = path.extname(filepath).toLowerCase()

  // 检查常见的二进制扩展名
  switch (ext) {
    case ".zip":
    case ".tar":
    case ".gz":
    case ".exe":
    case ".dll":
    case ".so":
    case ".class":
    case ".jar":
    case ".war":
    case ".7z":
      return true
    default:
      break
  }

  // 检查字节内容
  const stat = await file.stat()
  const fileSize = stat.size
  if (fileSize === 0) return false

  const bufferSize = Math.min(4096, fileSize)
  const buffer = await file.arrayBuffer()
  const bytes = new Uint8Array(buffer.slice(0, bufferSize))

  // 计算不可打印字符比例
  let nonPrintableCount = 0
  for (let i = 0; i < bytes.length; i++) {
    if (bytes[i] === 0) return true
    if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
      nonPrintableCount++
    }
  }

  // 如果 >30% 不可打印,认为是二进制
  return nonPrintableCount / bytes.length > 0.3
}

const isBinary = await isBinaryFile(filepath, file)
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)

7. 文本读取和截断

const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const lines = await file.text().then((text) => text.split("\n"))

const raw: string[] = []
let bytes = 0
let truncatedByBytes = false

// 逐行读取,直到达到限制
for (let i = offset; i < Math.min(lines.length, offset + limit); i++) {
  const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]

  const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)

  if (bytes + size > MAX_BYTES) {
    truncatedByBytes = true
    break
  }

  raw.push(line)
  bytes += size
}

// 添加行号
const content = raw.map((line, index) => {
  return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
})

const preview = raw.slice(0, 20).join("\n")

8. 输出格式化

let output = "<file>\n"
output += content.join("\n")

const totalLines = lines.length
const lastReadLine = offset + raw.length
const hasMoreLines = totalLines > lastReadLine
const truncated = hasMoreLines || truncatedByBytes

if (truncatedByBytes) {
  output += `\n\n(Output truncated at ${MAX_BYTES} bytes. Use 'offset' parameter to read beyond line ${lastReadLine})`
} else if (hasMoreLines) {
  output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})`
} else {
  output += `\n\n(End of file - total ${totalLines} lines)`
}
output += "\n</file>"

9. LSP 和文件时间追踪

// 通知 LSP 客户端文件被读取
LSP.touchFile(filepath, false)

// 记录文件读取时间
FileTime.read(ctx.sessionID, filepath)

典型使用场景

场景 1:读取整个文件

// LLM 调用
await ReadTool.execute({
  filePath: "/home/user/project/src/app.ts",
}, ctx)

// 返回:
<file>
00001| import express from 'express'
00002| import bodyParser from 'body-parser'
...
00150| export default app
</file>

(End of file - total 150 lines)

场景 2:读取部分内容(使用 offset 和 limit)

await ReadTool.execute(
  {
    filePath: "src/app.ts",
    offset: 100, // 从第 100 行开始
    limit: 50, // 读取 50 行
  },
  ctx,
)

// 返回从行 101 到 150 的内容

场景 3:读取图像文件

await ReadTool.execute({
  filePath: "assets/logo.png",
}, ctx)

// 返回包含附件的结果:
{
  title: "assets/logo.png",
  output: "Image read successfully",
  metadata: {
    preview: "Image read successfully",
    truncated: false,
  },
  attachments: [{
    id: "part_abc123",
    type: "file",
    mime: "image/png",
    url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
  }],
}

场景 4:相对路径处理

// 相对路径会被解析为绝对路径
await ReadTool.execute(
  {
    filePath: "src/config.ts", // 相对路径
  },
  ctx,
)

// 实际读取:/home/user/project/src/config.ts

错误处理

文件不存在

Error: File not found: /path/to/file.ts

Did you mean one of these?
/path/to/file_backup.ts
/path/to/file.test.ts
/path/to/file_old.ts

解决:

  • 检查文件路径是否正确
  • 查看建议的相似文件名
  • 检查文件是否已删除或重命名

二进制文件

Error: Cannot read binary file: /path/to/file.exe

解决:

  • 不要尝试读取二进制可执行文件
  • 对于二进制数据,使用其他工具(如 Bash)处理

外部目录权限

如果尝试读取项目外的文件,会触发权限检查:

// 需要用户确认
await ctx.ask({
  permission: "read",
  patterns: ["/external/path/file.txt"],
  always: ["*"],
})

限制和注意事项

读取限制

  1. 最大行数:默认 2000 行,可通过 limit 参数调整
  2. 最大行长度:单行超过 2000 字符会被截断并添加 "..."
  3. 最大字节数:总输出超过 50 KB 会被截断

特殊文件处理

二进制文件(自动检测):

  • .zip, .tar, .gz
  • .exe, .dll, .so
  • .class, .jar, .war
  • .doc, .docx, .xls, .xlsx
  • .ppt, .pptx
  • 等等

支持的特殊格式:

  • 图像文件(JPG, PNG, GIF 等,不包括 SVG)
  • PDF 文件
  • 这些文件会作为 attachments 返回

文件名建议

当文件不存在时,会查找:

  • 包含目标文件名片段的文件
  • 目标文件名包含其片段的文件
  • 最多返回 3 个建议

权限要求

权限类型说明
read需要读取文件的权限
external_directory如果文件在项目外,需要此权限

最佳实践

1. 使用绝对路径

// ✅ 好
await ReadTool.execute(
  {
    filePath: "/home/user/project/src/app.ts",
  },
  ctx,
)

// ❌ 差(相对路径在非标准工作目录下可能出错)
await ReadTool.execute(
  {
    filePath: "../../other-project/src/app.ts",
  },
  ctx,
)

2. 处理大文件

对于大文件,使用 offset 和 limit 参数:

// 第一次读取
const result1 = await ReadTool.execute(
  {
    filePath: "large-file.txt",
    limit: 1000,
  },
  ctx,
)

// 后续读取(如果被截断)
if (result1.metadata.truncated) {
  const lastLine = 1000 // 基于返回信息
  const result2 = await ReadTool.execute(
    {
      filePath: "large-file.txt",
      offset: lastLine,
      limit: 1000,
    },
    ctx,
  )
}

3. 批量读取

可以并行读取多个独立文件:

const [result1, result2, result3] = await Promise.all([
  ReadTool.execute({ filePath: "src/config.ts" }, ctx),
  ReadTool.execute({ filePath: "src/utils.ts" }, ctx),
  ReadTool.execute({ filePath: "package.json" }, ctx),
])

4. 检查截断标记

总是检查 metadata.truncated 标记:

const result = await ReadTool.execute(
  {
    filePath: "large-file.txt",
  },
  ctx,
)

if (result.metadata.truncated) {
  console.log("File was truncated, need to read more")
}

相关文档

  • Tool System - Tool 系统实现机制
  • Write - 写入文件
  • Edit - 编辑文件
  • Glob - 查找文件
  • Grep - 搜索文件内容

变更历史

版本变更内容日期
v1初始 Read 工具-