新增 encoding.ts 核心模块实现三层编码检测(BOM → UTF-8 fatal → GBK 回退), 改造同步/异步读取路径和写入路径,使 FileReadTool/FileEditTool/FileWriteTool 能正确处理 GBK 编码文件。包含完整单元测试和 spec 文档。 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
7.6 KiB
Task 4: 写入路径和工具层适配
背景:
[业务语境] — 当用户通过 FileEditTool 或 FileWriteTool 编辑非 UTF-8 编码文件(如 GBK)时,写入操作需要将内部 UTF-8 字符串编码回原文件编码,否则写入的内容会乱码。当前 writeTextContent 只接受 BufferEncoding 类型,无法处理 gbk 等编码。
[修改原因] — writeTextContent 的 encoding 参数类型为 BufferEncoding,writeFileSyncAndFlush_DEPRECATED 内部直接将 encoding 传给 fs.writeFileSync(只接受标准 BufferEncoding)。FileEditTool.validateInput 中硬编码了 BOM-only 编码检测,无法识别 GBK 文件。
[上下游影响] — 本 Task 依赖 Task 1 创建的 encodeString 函数和 FileEncoding 类型。FileEditTool 和 FileWriteTool 通过 writeTextContent 间接依赖本 Task 的改造。BashTool 和 NotebookEditTool 也调用 writeTextContent,签名变更后它们无需额外改动(encoding 参数类型由上游传入,自动兼容)。
涉及文件:
- 修改:
src/utils/file.ts - 修改:
packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts
执行步骤:
-
在
src/utils/file.ts中合并encodeString到 Task 2 已创建的encoding.js导入- 位置: 文件导入区域,Task 2 已添加的
import { type FileEncoding, decodeBuffer } from './encoding.js'行 - 将该行改为:
import { type FileEncoding, decodeBuffer, encodeString } from './encoding.js' - 原因: 避免对同一模块创建两个 import 语句
- 位置: 文件导入区域,Task 2 已添加的
-
将
writeTextContent的encoding参数类型从BufferEncoding改为FileEncoding- 位置:
src/utils/file.ts:writeTextContent() - 修改函数签名:
export function writeTextContent( filePath: string, content: string, encoding: FileEncoding, endings: LineEndingType, ): void - 修改函数体,在行尾处理之后、调用
writeFileSyncAndFlush_DEPRECATED之前,增加编码判断逻辑:const BUFFER_ENCODINGS = new Set<string>([ 'utf8', 'utf-8', 'utf16le', 'ucs2', 'ucs-2', 'ascii', 'latin1', 'binary', 'base64', 'hex', ]) if (BUFFER_ENCODINGS.has(encoding)) { writeFileSyncAndFlush_DEPRECATED(filePath, toWrite, { encoding: encoding as BufferEncoding }) } else { // 非 BufferEncoding(如 gbk),使用 encodeString 获取 Buffer const { buffer, converted } = encodeString(toWrite, encoding) writeFileSyncAndFlush_DEPRECATED(filePath, buffer, { buffer }) if (converted) { logForDebugging( `writeTextContent: encoding '${encoding}' unsupported for write, fell back to UTF-8 for ${filePath}`, { level: 'warn' }, ) } } - 原因:
fs.writeFileSync只接受标准 BufferEncoding,对于 gbk 等编码必须先转为 Buffer 再写入
- 位置:
-
扩展
writeFileSyncAndFlush_DEPRECATED支持 Buffer 写入- 位置:
src/utils/file.ts:writeFileSyncAndFlush_DEPRECATED() - 修改函数签名中
content参数类型和options类型:export function writeFileSyncAndFlush_DEPRECATED( filePath: string, content: string | Buffer, options: { encoding?: BufferEncoding; mode?: number; buffer?: Buffer } = {}, ): void - 修改原子写入路径的
writeOptions构建逻辑:const isBufferWrite = Buffer.isBuffer(content) || options.buffer !== undefined const writeData = options.buffer ?? content const writeOptions: { encoding?: BufferEncoding flush: boolean mode?: number } = { flush: true, ...(isBufferWrite ? {} : { encoding: options.encoding ?? 'utf-8' }), } - 修改非原子回退路径,使用相同的
isBufferWrite/writeData/writeOptions模式 - 原因:
fs.writeFileSync(path, buffer)可以直接写入 Buffer,不需要 encoding 参数
- 位置:
-
在
FileEditTool.ts中导入FileEncoding和detectEncoding/decodeBuffer- 位置:
packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts导入区域 - 添加:
import { detectEncoding, decodeBuffer, type FileEncoding } from 'src/utils/encoding.js' - 原因:
validateInput编码检测和readFileForEdit返回类型需要FileEncoding类型
- 位置:
-
将
readFileForEdit返回类型中的encoding从BufferEncoding改为FileEncoding- 位置:
packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts:readFileForEdit() - 修改返回类型声明:
function readFileForEdit(absoluteFilePath: string): { content: string fileExists: boolean encoding: FileEncoding lineEndings: LineEndingType } - 原因:
readFileSyncWithMetadata返回的encoding类型已由 Task 2 改为FileEncoding
- 位置:
-
改造
FileEditTool.validateInput中的编码检测逻辑- 位置:
packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts:validateInput() - 将现有的 BOM-only 编码检测:
const encoding: BufferEncoding = fileBuffer.length >= 2 && fileBuffer[0] === 0xff && fileBuffer[1] === 0xfe ? 'utf16le' : 'utf8' fileContent = fileBuffer.toString(encoding).replaceAll('\r\n', '\n') - 替换为:
const encoding: FileEncoding = detectEncoding(fileBuffer) fileContent = decodeBuffer(fileBuffer, encoding).replaceAll('\r\n', '\n') - 原因: 使 validateInput 也能正确识别 GBK 文件,避免编辑时因编码检测不一致导致 old_string 匹配失败
- 位置:
-
为
writeTextContent的多编码写入能力编写单元测试- 测试文件:
src/utils/__tests__/file.test.ts - 在现有测试 describe 块之后追加新的 describe('writeTextContent with multi-encoding') 块
- 测试场景:
- UTF-8 写入: 写入 UTF-8 内容 → 文件内容正确,无回退警告
- UTF-16LE 写入: 写入 UTF-16LE 内容(含 BOM) → 文件二进制内容与预期一致
- GBK 写入回退: 对 gbk 编码调用
writeTextContent→ 文件以 UTF-8 写入(encodeString回退行为),内容不损坏 - CRLF 行尾 + GBK:
endings: 'CRLF'+ gbk 编码 → 行尾正确转换为\r\n,编码回退为 UTF-8
- 注意: 需要 mock
src/utils/debug.ts(使用共享 mocktests/mocks/debug.ts) - 运行命令:
bun test src/utils/__tests__/file.test.ts - 预期: 所有测试通过
- 测试文件:
检查步骤:
-
验证
writeTextContent签名使用FileEncoding类型grep -n 'encoding: FileEncoding' src/utils/file.ts- 预期: 输出包含
writeTextContent函数定义行
-
验证
writeFileSyncAndFlush_DEPRECATED支持 Buffer 写入grep -n 'content: string | Buffer' src/utils/file.ts- 预期: 输出包含
writeFileSyncAndFlush_DEPRECATED函数定义行
-
验证
FileEditTool.readFileForEdit返回类型已更新grep -n 'encoding: FileEncoding' packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts- 预期: 输出包含
readFileForEdit函数的返回类型声明
-
验证
FileEditTool.validateInput使用detectEncodinggrep -n 'detectEncoding' packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts- 预期: 输出包含 validateInput 内部的调用
-
运行 file.ts 单元测试
bun test src/utils/__tests__/file.test.ts- 预期: 所有测试通过,无新增失败
-
运行 FileEditTool 工具函数测试
bun test packages/builtin-tools/src/tools/FileEditTool/__tests__/utils.test.ts- 预期: 所有现有测试通过
-
运行完整 precheck
bun run precheck- 预期: typecheck + lint + test 零错误通过