Files
claude-code/spec/feature_20260510_F001_multi-encoding-file-tools/spec-plan-task-2.md
claude-code-best 0ce8f7a1cb feat: 添加 GBK 编码自动检测支持,文件读写工具透明处理非 UTF-8 文件
新增 encoding.ts 核心模块实现三层编码检测(BOM → UTF-8 fatal → GBK 回退),
改造同步/异步读取路径和写入路径,使 FileReadTool/FileEditTool/FileWriteTool
能正确处理 GBK 编码文件。包含完整单元测试和 spec 文档。

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
2026-05-10 20:50:12 +08:00

8.0 KiB
Raw Blame History

Task 2: 同步读取路径集成

背景: 当前同步读取路径(fileRead.tsfile.tsfileReadCache.ts)的编码检测仅通过 BOM 头识别 UTF-8 和 UTF-16LE非 BOM 编码文件一律按 UTF-8 读取导致乱码。本 Task 将 detectEncodingForResolvedPath 的内部实现从 BOM-only 升级为调用 Task 1 创建的 encoding.ts 三层检测,并将返回类型从 BufferEncoding 扩展为 FileEncoding。同时将所有 fs.readFileSync(path, { encoding }) 调用改为先读 Buffer 再用 decodeBuffer 解码,以支持 gbk 等非 BufferEncoding 编码。本 Task 依赖 Task 1src/utils/encoding.ts),输出被 Task 4写入路径适配依赖。

涉及文件:

  • 修改: src/utils/fileRead.ts
  • 修改: src/utils/file.ts
  • 修改: src/utils/fileReadCache.ts
  • 新建: src/utils/__tests__/fileRead.test.ts

执行步骤:

  • fileRead.ts 中导入 encoding.ts 的类型和函数

    • 位置: src/utils/fileRead.ts 文件顶部 import 区域,在 import { getFsImplementation, safeResolvePath } from './fsOperations.js' 之后
    • 添加导入:
      import { type FileEncoding, decodeBuffer, detectEncoding } from './encoding.js'
      
    • 原因: 后续步骤需要 FileEncoding 类型、detectEncoding 检测函数和 decodeBuffer 解码函数
  • 改造 detectEncodingForResolvedPath 函数,使用 encoding.ts 的三层检测

    • 位置: src/utils/fileRead.tsdetectEncodingForResolvedPath 函数
    • 将函数体替换为以下逻辑:
      export function detectEncodingForResolvedPath(
        resolvedPath: string,
      ): FileEncoding {
        const { buffer, bytesRead } = getFsImplementation().readSync(resolvedPath, {
          length: 4096,
        })
      
        // Empty files default to utf8 — nothing to detect
        if (bytesRead === 0) {
          return 'utf8'
        }
      
        return detectEncoding(buffer.subarray(0, bytesRead))
      }
      
    • 关键变更:
      • 返回类型从 BufferEncoding 改为 FileEncoding
      • 删除内联的 BOM 检测逻辑,改为调用 detectEncoding(buffer.subarray(0, bytesRead))
      • 使用 buffer.subarray(0, bytesRead) 截取实际读取的字节,避免尾部零字节干扰检测
    • 原因: 将检测逻辑委托给 encoding.ts 的三层算法,消除代码重复
  • 改造 readFileSyncWithMetadata 函数,支持非 BufferEncoding 解码

    • 位置: src/utils/fileRead.tsreadFileSyncWithMetadata 函数
    • 将函数签名和内部逻辑改为:
      export function readFileSyncWithMetadata(filePath: string): {
        content: string
        encoding: FileEncoding
        lineEndings: LineEndingType
      } {
        const fs = getFsImplementation()
        const { resolvedPath, isSymlink } = safeResolvePath(fs, filePath)
      
        if (isSymlink) {
          logForDebugging(`Reading through symlink: ${filePath} -> ${resolvedPath}`)
        }
      
        const encoding = detectEncodingForResolvedPath(resolvedPath)
        // Read raw Buffer first — fs.readFileSync encoding option only accepts
        // BufferEncoding, not gbk etc.
        const rawBuffer = fs.readFileBytesSync(resolvedPath)
        const raw = decodeBuffer(rawBuffer, encoding)
        const lineEndings = detectLineEndingsForString(raw.slice(0, 4096))
        return {
          content: raw.replaceAll('\r\n', '\n'),
          encoding,
          lineEndings,
        }
      }
      
    • 关键变更:
      • 返回类型中 encodingBufferEncoding 改为 FileEncoding
      • fs.readFileSync(resolvedPath, { encoding }) 改为 fs.readFileBytesSync(resolvedPath) 读取 Buffer
      • 新增 decodeBuffer(rawBuffer, encoding) 解码为字符串
    • 原因: fs.readFileSyncencoding 选项只接受 BufferEncodingutf8/utf16le/latin1 等),传入 'gbk' 会在运行时报错
  • 更新 file.tsdetectFileEncoding 的返回类型

    • 位置: src/utils/file.tsdetectFileEncoding 函数签名
    • ): BufferEncoding { 改为 ): FileEncoding {
    • 在文件顶部 import 区域添加:
      import { type FileEncoding, decodeBuffer, encodeString } from './encoding.js'
      
    • 原因: detectFileEncoding 调用 detectEncodingForResolvedPath,返回类型已改为 FileEncoding
  • 更新 file.tsdetectLineEndings 的 encoding 参数类型和解码逻辑

    • 位置: src/utils/file.tsdetectLineEndings 函数
    • 将函数签名改为:
      export function detectLineEndings(
        filePath: string,
        encoding: FileEncoding = 'utf8',
      ): LineEndingType {
      
    • 将内部 buffer.toString(encoding, 0, bytesRead) 改为:
      const content = decodeBuffer(buffer.subarray(0, bytesRead), encoding)
      
    • 原因: buffer.toString('gbk') 不可靠,统一使用 decodeBuffer 通过 TextDecoder 解码
  • 更新 fileReadCache.ts 的类型和解码逻辑

    • 位置: src/utils/fileReadCache.ts
    • 在文件顶部 import 区域添加:
      import { type FileEncoding, decodeBuffer } from './encoding.js'
      
    • CachedFileData 类型中 encoding: BufferEncoding 改为 encoding: FileEncoding
    • readFile 方法返回类型改为 { content: string; encoding: FileEncoding }
    • 将缓存未命中读取逻辑改为:
      const encoding = detectFileEncoding(filePath)
      const rawBuffer = fs.readFileBytesSync(filePath)
      const content = decodeBuffer(rawBuffer, encoding).replaceAll('\r\n', '\n')
      
    • 原因: 与 fileRead.ts 相同——必须改为 Buffer 读取 + decodeBuffer 解码
  • 为改造后的 detectEncodingForResolvedPathreadFileSyncWithMetadata 编写单元测试

    • 测试文件: src/utils/__tests__/fileRead.test.ts
    • 测试场景:
      • UTF-8 文件读取: 创建临时 UTF-8 文件 → 返回 encoding: 'utf-8'content 与写入内容一致
      • GBK 文件读取: 创建临时 GBK 编码文件 → 返回 encoding: 'gbk'content 包含正确的中文字符
      • 空文件读取: 创建空文件 → 返回 encoding: 'utf8'content 为空字符串
      • UTF-16LE BOM 文件读取: 创建带 BOM 的 UTF-16LE 文件 → 返回 encoding: 'utf-16le'
      • detectEncodingForResolvedPath 返回类型: 验证返回值为 FileEncoding 类型
    • Mock 策略: 使用 tests/mocks/debug.ts mock debug.ts,使用 tests/mocks/log.ts mock log.ts
    • 运行命令: bun test src/utils/__tests__/fileRead.test.ts
    • 预期: 所有测试通过

检查步骤:

  • 验证 fileRead.ts 的导入和返回类型已更新

    • grep -n "FileEncoding\|decodeBuffer\|detectEncoding" src/utils/fileRead.ts
    • 预期: 输出包含 import 行中的 FileEncodingdecodeBuffer,以及函数体中的 detectEncoding 调用
  • 验证 file.ts 的类型已更新

    • grep -n "FileEncoding\|decodeBuffer" src/utils/file.ts
    • 预期: detectFileEncoding 返回 FileEncodingdetectLineEndings 参数类型为 FileEncoding
  • 验证 fileReadCache.ts 的类型已更新

    • grep -n "FileEncoding\|decodeBuffer" src/utils/fileReadCache.ts
    • 预期: CachedFileDatareadFile 返回类型使用 FileEncoding
  • 验证 fileRead.ts 中不再有内联 BOM 检测逻辑

    • grep -c "0xff\|0xfe\|0xef\|0xbb\|0xbf" src/utils/fileRead.ts
    • 预期: 输出为 0
  • 运行 fileRead 单元测试

    • bun test src/utils/__tests__/fileRead.test.ts
    • 预期: 所有测试通过
  • 运行 precheck 确认无类型/lint/测试错误

    • bun run precheck
    • 预期: 零错误通过

认知变更:

  • [CLAUDE.md] fs.readFileSync(path, { encoding })encoding 选项只接受 BufferEncodingutf8/utf16le/latin1/ascii/binary/hex/base64/ucs2/utf16le不支持 gbk 等 ICU 编码名。读取非 UTF-8 文件时必须先 fs.readFileSync(path) 读 Buffer再用 TextDecoder 解码。项目中所有文件读取路径fileRead.ts、fileReadCache.ts、file.ts已统一使用 decodeBuffer 函数处理此逻辑。