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>
This commit is contained in:
claude-code-best
2026-05-10 20:50:12 +08:00
parent 6e1d3d8f47
commit 0ce8f7a1cb
22 changed files with 1728 additions and 121 deletions

View File

@@ -0,0 +1,179 @@
# 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`(单字节编码,永远成功,作为最终兜底)
```typescript
// 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.ts` — `detectEncodingForResolvedPath`
将现有的 BOM-only 检测替换为调用 `encoding.ts``detectEncoding` 函数。返回值从 `BufferEncoding` 改为 `FileEncoding``BufferEncoding | '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.ts` — `writeTextContent`
现有函数签名 `writeTextContent(filePath, content, encoding, lineEndings)` 已接受 encoding 参数。需要:
- 扩展类型,接受 `FileEncoding` 而非仅 `BufferEncoding`
- 对于 UTF-8 和 UTF-16LE行为不变
- 对于 GBK使用 `encodeString` 函数尝试编码,失败则回退为 UTF-8 写入
#### `FileWriteTool` 和 `FileEditTool`
这两个工具的 `call` 方法中,`writeTextContent` 调用已传递 `encoding`(来自 `readFileSyncWithMetadata` 的返回值)。改动很小——只需确保类型系统接受新编码名。
### 类型扩展
```typescript
// 扩展编码类型 — 仅添加 GBK
export type FileEncoding = BufferEncoding | 'gbk'
```
`readFileSyncWithMetadata` 返回类型中将 `encoding``BufferEncoding` 改为 `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. **混合编码文件**:极少见,不在本次覆盖范围内
### 依赖
- 零外部依赖,仅使用 `TextDecoder`Node.js 13+ / Bun 内置 full-icu
- Bun 运行时对 GBK 的 TextDecoder 支持已验证可用Bun 1.3.13
## 验收标准
- [x] FileReadTool 能正确读取 GBK 编码的中文文本文件,显示正确的中文内容
- [x] FileReadTool 能正确读取 UTF-8 文件(行为不变,回归测试通过)
- [x] FileReadTool 能正确读取 UTF-16LE 文件(行为不变)
- [x] FileEditTool 能编辑 GBK 文件并写回,内容不乱码
- [x] FileWriteTool 编辑 GBK 文件后写回,编码保持或合理转换
- [x] readFileInRange 的 fast path 路径支持非 UTF-8 编码
- [x] readFileInRange 的 streaming path 支持非 UTF-8 编码
- [x] 编码检测性能4KB 数据检测耗时 < 1ms
- [x] `bun run precheck` typecheck + lint + 相关测试零错误
- [x] 新增编码相关单元测试覆盖检测和解码逻辑