# 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,不做代码页探测。