Files
claude-code/spec/feature_20260510_F001_multi-encoding-file-tools/spec-design.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.1 KiB
Raw Blame History

Feature: 20260510_F001 - multi-encoding-file-tools

需求背景

当前文件读写工具FileReadTool、FileWriteTool、FileEditTool的编码检测非常简单——仅通过 BOM 头识别 UTF-8 和 UTF-16LE其他所有情况默认按 UTF-8 处理。对于 GBK/GB2312 等非 BOM 编码文件,读取时会产生乱码,导致 AI 模型无法正确理解和编辑这些文件。

这在中文 Windows 用户场景中尤其常见:许多旧项目、日志文件、配置文件使用 GBK 编码,当前工具链无法处理。

目标

  • 文件读取时自动检测编码并正确解码,对 AI 模型完全透明(不增加 encoding 参数)
  • 文件写入时保持原文件编码,不改变用户的编码习惯
  • 覆盖 GBK 编码(最常见非 UTF-8 CJK 编码latin1 作为最终兜底
  • 零外部依赖,仅使用 Node.js/Bun 内置的 TextDecoder/TextEncoder

范围变更

仅保留 GBK 编码支持。Shift_JIS、EUC-JP、EUC-KR、Big5、GB18030、ISO-8859-1 已移出范围。原因:多编码回退链存在字节序列歧义(如 GBK 和 Shift_JIS 共享大量有效字节范围导致误检测。GBK 覆盖了最核心的中文 Windows 用户场景。

方案设计

架构概述

新增一个独立的编码工具模块 src/utils/encoding.ts,提供编码检测和解码/编码函数。现有文件读写路径通过调用此模块实现对非 UTF-8 编码的支持。

                    ┌─────────────────────────┐
                    │   src/utils/encoding.ts  │
                    │  detectEncoding(buffer)  │
                    │  decodeBuffer(buf, enc)  │
                    │  encodeString(str, enc)  │
                    └─────────┬───────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
     fileRead.ts      readFileInRange.ts    file.ts
   (readFileSync     (异步读取路径)      (writeTextContent)
   WithMetadata)

编码检测算法(三层检测)

检测基于文件头部 4KB 数据,分三层依次判断:

第一层BOM 检测(现有逻辑保留)

  • FF FE → UTF-16LE
  • EF BB BF → UTF-8带 BOM

第二层UTF-8 验证

  • new TextDecoder('utf-8', { fatal: true }) 对头部 4KB 做解码
  • 成功 → 文件为 UTF-8覆盖绝大多数现代源码文件
  • 失败(抛出 TypeError→ 进入第三层

第三层GBK 回退

  • new TextDecoder('gbk', { fatal: true }) 尝试解码头部 4KB
  • 成功 → 文件为 GBK覆盖中文 Windows 用户最常见的非 UTF-8 编码)
  • 失败 → latin1(单字节编码,永远成功,作为最终兜底)
// src/utils/encoding.ts 核心逻辑

export type FileEncoding = BufferEncoding | 'gbk'
export type DetectedEncoding = string

export function detectEncoding(buffer: Buffer): FileEncoding {
  // Layer 1: BOM
  if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
    return 'utf-16le'
  }
  if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
    return 'utf-8'
  }

  // Layer 2: UTF-8 validation
  try {
    new TextDecoder('utf-8', { fatal: true }).decode(buffer)
    return 'utf-8'
  } catch {}

  // Layer 3: GBK fallback
  try {
    new TextDecoder('gbk', { fatal: true }).decode(buffer)
    return 'gbk'
  } catch {}

  return 'latin1'
}

读取路径改造

src/utils/fileRead.tsdetectEncodingForResolvedPath

将现有的 BOM-only 检测替换为调用 encoding.tsdetectEncoding 函数。返回值从 BufferEncoding 改为 FileEncodingBufferEncoding | 'gbk')。

readFileSyncWithMetadata 函数先读 raw Buffer再用 decodeBuffer 解码,而非使用 fs.readFileSync 的 encoding 选项(该选项只接受 BufferEncoding,不支持 gbk)。

src/utils/readFileInRange.ts — 异步读取

当前两个路径fast path 和 streaming path都硬编码 encoding: 'utf8'

Fast path 改造

  • readFile 改为读取 Buffer去掉 encoding 参数)
  • 读取后调用 detectEncoding(buffer) 检测编码
  • decodeBuffer 解码为字符串
  • 后续行处理逻辑不变

Streaming path 改造

  • createReadStream 去掉 encoding: 'utf8',改为 Buffer 模式
  • 第一个 chunk 做编码检测(同时保留 BOM 剥离逻辑)
  • 后续 chunk 拼接后用 TextDecoder 解码
  • 注意streaming 路径需要特殊处理——先收集足够字节做检测,再逐行扫描

Streaming 编码处理策略 streaming 路径改为两阶段:

  1. 检测阶段:前 4KB 数据到达后立即检测编码
  2. 解码阶段:用检测到的编码创建一个 TextDecoder{ stream: true } 模式),逐 chunk 解码

写入路径改造

编码回写策略

写入时需要将内部 UTF-8 字符串编码回原文件编码。由于 TextEncoder 只支持 UTF-8 输出,需要使用 TextDecoder 的反向操作。

最终决定:对于非 UTF-8 文件的写回,尝试使用 Buffer.from(content, encoding) 编码,失败则自动转换为 UTF-8 并在结果消息中注明。这样既满足了零依赖约束,也避免了数据损坏。

src/utils/file.tswriteTextContent

现有函数签名 writeTextContent(filePath, content, encoding, lineEndings) 已接受 encoding 参数。需要:

  • 扩展类型,接受 FileEncoding 而非仅 BufferEncoding
  • 对于 UTF-8 和 UTF-16LE行为不变
  • 对于 GBK使用 encodeString 函数尝试编码,失败则回退为 UTF-8 写入

FileWriteToolFileEditTool

这两个工具的 call 方法中,writeTextContent 调用已传递 encoding(来自 readFileSyncWithMetadata 的返回值)。改动很小——只需确保类型系统接受新编码名。

类型扩展

// 扩展编码类型 — 仅添加 GBK
export type FileEncoding = BufferEncoding | 'gbk'

readFileSyncWithMetadata 返回类型中将 encodingBufferEncoding 改为 FileEncoding

实现要点

关键技术决策

  1. 检测只用头部 4KB:避免全文件扫描,性能开销极小(多几次 TextDecoder 调用,每次 ~1μs
  2. GBK 作为唯一回退:中文 Windows 用户最多,且避免了多编码回退链的字节序列歧义问题
  3. TextDecoder fatal 模式{ fatal: true } 是检测的关键——如果字节序列不符合编码规范会抛异常,借此区分不同编码
  4. streaming 路径的两阶段设计:先攒够检测数据再开始行扫描,避免半字符解码问题
  5. latin1 最终兜底:单字节编码永远成功,确保任何文件都能被读取

难点

  1. Streaming 编码解码TextDecoder 支持 { stream: true } 模式处理多字节字符的 chunk 边界,但需要在检测完成前缓冲数据
  2. 编码回写的零依赖方案TextEncoder 只输出 UTF-8非 UTF-8 编码回写需要额外处理。务实方案是 UTF-8 写入 + 消息提示
  3. 混合编码文件:极少见,不在本次覆盖范围内

依赖

  • 零外部依赖,仅使用 TextDecoderNode.js 13+ / Bun 内置 full-icu
  • Bun 运行时对 GBK 的 TextDecoder 支持已验证可用Bun 1.3.13

验收标准

  • FileReadTool 能正确读取 GBK 编码的中文文本文件,显示正确的中文内容
  • FileReadTool 能正确读取 UTF-8 文件(行为不变,回归测试通过)
  • FileReadTool 能正确读取 UTF-16LE 文件(行为不变)
  • FileEditTool 能编辑 GBK 文件并写回,内容不乱码
  • FileWriteTool 编辑 GBK 文件后写回,编码保持或合理转换
  • readFileInRange 的 fast path 路径支持非 UTF-8 编码
  • readFileInRange 的 streaming path 支持非 UTF-8 编码
  • 编码检测性能4KB 数据检测耗时 < 1ms
  • bun run precheck typecheck + lint + 相关测试零错误
  • 新增编码相关单元测试覆盖检测和解码逻辑