Files
claude-code/spec/feature_20260510_F001_multi-encoding-file-tools/spec-plan-task-4.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

7.6 KiB
Raw Blame History

Task 4: 写入路径和工具层适配

背景: [业务语境] — 当用户通过 FileEditTool 或 FileWriteTool 编辑非 UTF-8 编码文件(如 GBK写入操作需要将内部 UTF-8 字符串编码回原文件编码,否则写入的内容会乱码。当前 writeTextContent 只接受 BufferEncoding 类型,无法处理 gbk 等编码。 [修改原因] — writeTextContentencoding 参数类型为 BufferEncodingwriteFileSyncAndFlush_DEPRECATED 内部直接将 encoding 传给 fs.writeFileSync(只接受标准 BufferEncodingFileEditTool.validateInput 中硬编码了 BOM-only 编码检测,无法识别 GBK 文件。 [上下游影响] — 本 Task 依赖 Task 1 创建的 encodeString 函数和 FileEncoding 类型。FileEditToolFileWriteTool 通过 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 语句
  • writeTextContentencoding 参数类型从 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 中导入 FileEncodingdetectEncoding / decodeBuffer

    • 位置: packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts 导入区域
    • 添加: import { detectEncoding, decodeBuffer, type FileEncoding } from 'src/utils/encoding.js'
    • 原因: validateInput 编码检测和 readFileForEdit 返回类型需要 FileEncoding 类型
  • readFileForEdit 返回类型中的 encodingBufferEncoding 改为 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(使用共享 mock tests/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 使用 detectEncoding

    • grep -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 零错误通过