Read 工具
Read 工具用于读取文件系统中的文件内容。
概述
Read 工具允许 AI 助手读取项目中的任何文件。它支持:
- 绝对路径和相对路径
- 行号偏移和限制
- 二进制文件检测
- 图像和 PDF 文件支持(作为附件)
- 自动建议相似文件名(文件不存在时)
定义位置
packages/opencode/src/tool/read.ts:16-45packages/opencode/src/tool/read.txt
参数说明
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
filePath | string | 是 | 要读取的文件路径 |
offset | number | 否 | 开始读取的行号(从 0 开始,0-based) |
limit | number | 否 | 要读取的行数(默认 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: ["*"],
})
限制和注意事项
读取限制
- 最大行数:默认 2000 行,可通过
limit参数调整 - 最大行长度:单行超过 2000 字符会被截断并添加 "..."
- 最大字节数:总输出超过 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 工具 | - |