33 lines
2.3 KiB
Markdown
33 lines
2.3 KiB
Markdown
# ADR-0004:运行日志的输出编码
|
||
|
||
- 状态:已接受
|
||
- 日期:2026-10-24
|
||
|
||
## 背景
|
||
|
||
任务日志原先按 UTF-8 无损方式(`String::from_utf8_lossy`)解码子进程输出。中文 Windows 上多数程序并不输出 UTF-8:Python 默认使用系统 ANSI 代码页(简体中文为 GBK/936),`cmd.exe`、部分 CLI 使用控制台代码页。结果日志里出现成片的 `����`,例如 `release/TaskPilot/logs/copyparty/1.log` 中真实的 `copyparty ������ (PID: 22056)`。
|
||
|
||
同一处还有两个次要问题:读取日志按 64 KiB 切块时可能切开多字节字符;终态任务只要日志超过 64 KiB,前端拿到首块就停止轮询,导致尾部丢失。
|
||
|
||
## 决策
|
||
|
||
1. 在写入侧解码:子进程每读完一行就解码为 UTF-8 再落盘,日志文件因此始终是合法 UTF-8,字节 offset 语义不变。
|
||
2. 默认 `auto`:先按 UTF-8 严格解码,失败时回退到系统 ANSI 代码页(`GetACP`),并按代码页映射到 `encoding_rs` 编码(936→GBK、950→Big5、932→Shift_JIS、949→EUC-KR、125x→windows-125x)。
|
||
3. 允许用 `TASK_PILOT_LOG_ENCODING` 指定编码标签(如 `gbk`、`utf-8`、`windows-1251`);显式指定时不再嗅探,行为完全确定。
|
||
4. HTTP 任务响应体沿用同一解码器。
|
||
5. 读取侧只做 UTF-8 边界对齐:块尾落在多字节字符中间时,把不完整字节留给下一次读取;只有真正非法的字节才替换为 `U+FFFD`。
|
||
6. `complete` 改为"运行已结束且已读到文件末尾",修复长日志被截断的问题。
|
||
|
||
## 原因
|
||
|
||
- 解码放在写入侧,只需处理一次,SSE、增量读取、下载、`grep` 得到的内容完全一致。
|
||
- 先 UTF-8 再回退,是因为 UTF-8 合法序列有强约束,误判概率低;反过来(先 GBK)会把大量 UTF-8 日志解坏。
|
||
- 使用系统代码页而不是硬编码 GBK,保证非中文 Windows 上的行为同样正确。
|
||
- 逐行解码避免多字节字符被缓冲区边界切开,且不受行尾差异影响。
|
||
|
||
## 边界
|
||
|
||
- 逐行启发式无法覆盖"一行内混用多种编码",也无法识别恰好构成合法 UTF-8 的 ANSI 字节序列;这两种情况需要显式设置 `TASK_PILOT_LOG_ENCODING`。
|
||
- 修复前写入的历史日志已经是替换字符,不会恢复。
|
||
- 非 Windows 平台回退编码为 UTF-8,不做代码页探测。
|