From eb7d23de3d79470620020678ad703fe29e2dd795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Mon, 3 Aug 2026 11:21:50 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=20UTF-8=20=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B9=E6=A1=88=E4=B8=8E=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PDD 方案(编号 32),覆盖 6 处逐字节强转修复、测试设计与版本升级计划 - 新增 PRD 需求(编号 2),定位模板编译阶段中文乱码根因并明确验收标准 - 添加 .codegraph 目录忽略规则,避免本地数据文件入库 --- .codegraph/.gitignore | 5 + design/pdd/32-模板编译器UTF-8编码修复方案.md | 222 +++++++++++++++++++ design/prd/2-模板编译器UTF-8编码修复需求.md | 172 ++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 .codegraph/.gitignore create mode 100644 design/pdd/32-模板编译器UTF-8编码修复方案.md create mode 100644 design/prd/2-模板编译器UTF-8编码修复需求.md diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..d20c0fe --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/design/pdd/32-模板编译器UTF-8编码修复方案.md b/design/pdd/32-模板编译器UTF-8编码修复方案.md new file mode 100644 index 0000000..f1e90a2 --- /dev/null +++ b/design/pdd/32-模板编译器UTF-8编码修复方案.md @@ -0,0 +1,222 @@ +# 模板编译器 UTF-8 编码修复方案 + +- 状态:Approved(第 1 轮审查通过,已按审查结论修正) +- 作者:think +- 日期:2026-08-03 +- 关联 PRD:`design/prd/2-模板编译器UTF-8编码修复需求.md` + +## 1. 背景与目标 + +agcore 的 `src/prompt/template.rs` 模板编译器有 6 处 `bytes[i] as char`(将 UTF-8 字节逐字节强转为 Unicode 码点,等价 Latin-1 解码),导致含中文等多字节字符的模板经 `compile → render` 后产生 mojibake 乱码,且乱码会原样发送给 LLM。下游 dc-management 的 system 提示词(采集策略 validator / collector)已全部乱码,属静默失败——模型在容忍乱码的情况下继续工作,但提示词中的规则约束已被破坏。 + +目标(对齐 PRD §1、§4 v1): + +- 统一修复 6 处逐字节强转,非 ASCII 文本逐字符正确保留 +- 补充回归测试(模板编译器首次补测试) +- 版本 0.3.6 → 0.3.7,patch 发布 +- 公开 API 不变、ASCII 模板行为零变化 + +## 2. 需求推演概要 + +### 2.1 需求拆解 + +v1 必做三项:6 处修复(含 parse_tag 内标签内容)、回归测试、版本号更新。v2 无。 + +非目标:不重构语法、不加新功能、不改公开 API、不修改其他模块(全库已扫描确认无同类风险点)、不涉及下游回切决策。裸闭合标签(顶层出现 `{{/xxx}}`)的静默截断行为(template.rs L248-249 既有 `tag.starts_with("/") => break`)不在本次修复范围,后续单独评估。 + +### 2.2 边界识别 + +- 模板语法标签(`{{`、`}}`、`#if`、`#each`、`#raw`)均为 ASCII,字节比较判断语法安全,保持不变 +- 非 ASCII 文本必须逐字符保留(修复目标是编译输出与模板原文一致) +- ASCII 模板行为零变化是硬约束(向后兼容,需回归测试保障) + +### 2.3 关键假设 + +1. **UTF-8 结构性保证**:多字节字符的 continuation bytes 恒在 0x80–0xBF,首字节 ≥ 0xC0,而 `{`=0x7B、`}`=0x7D、`#`=0x23——任何多字节字符的任何字节都不可能等于语法字符,故 `bytes[i]` 字节比较永远不会在多字节字符内部误命中 +2. **字符边界不变量**:循环中 i 起始为 0(边界);`{{`/`}}` 检测命中后 `end = i + 2`(`{`/`}` 各 1 字节,保持边界);字符推进按 `len_utf8()`(保持边界)。因此 `template[i..]` 切片不会 panic +3. 6 处 `as char` 是全库唯一编码风险点(grep `as char|char::from` 确认仅 template.rs 6 处) +4. 模板文件经 `include_str!` 加载有编译期 UTF-8 校验,编码问题不可能存在;日志/发送链路无转码——问题仅存在于模板编译器的内存字符串处理 + +## 3. 当前问题分析 + +### 3.1 根因 + +6 处 `bytes[i] as char` 明细: + +| 行号 | 函数 | 破坏内容 | +|------|------|---------| +| 255 | `compile_fragments` literal 分支 | 模板纯文本(主要破坏点) | +| 275 | `parse_tag` | `{{ 标签 }}` 内部内容(当前 ASCII 变量名未触发,中文变量名同样损坏) | +| 324 | `parse_block` else_body | `#if` else 分支块体 | +| 326 | `parse_block` body | `#if` 分支块体 | +| 364 | `parse_each_block` body | `#each` 块体 | +| 389 | `parse_raw_block` content | `#raw` 块内容 | + +中文每字 3 字节(emoji 4 字节)被拆成多个 Latin-1 字符(0xE4→ä、0xBD→½),产生 mojibake。 + +### 3.2 关键代码结构观察 + +- `parse_block` / `parse_each_block` / `parse_raw_block` 遇到 `{{` 标签时用 `template[i..end]` 原样字符串切片推回(该路径天然保留 UTF-8),只有逐字节累积路径被破坏 +- `parse_tag` 当前签名 `fn parse_tag(bytes: &[u8], start: usize)` 只收字节切片,无法按字符边界推进,需要改签名 +- 4 个调用点:223(compile_fragments)/ 296(parse_block)/ 344(parse_each_block)/ 380(parse_raw_block),且宿主函数均已持有 `template: &str` 参数,调用点改换为纯机械替换 +- `bytes` 局部变量在各函数中仍被 `{{` 判断使用,不可删除 + +## 4. 架构决策记录 + +### ADR-1:采用「字节索引 + 字符边界推进」(方案 A) + +| 方案 | 描述 | 结论 | +|------|------|------| +| A. 字节索引 + 字符推进(采纳) | 保持 `bytes[i]` ASCII 判断,字符累积改 `template[i..].chars().next()` + `len_utf8()` 推进 | ✅ 改动最小,标签检测、`template[i..end]` 切片、递归编译逻辑零变动,正确性有结构性论证 | +| B. 整体重构 chars 迭代器(否决) | 编译器是「索引 + 原样子串回填」混合模型,迭代器消费性导致 4 个函数的位置换算全部重写、嵌套 depth 管理重做 | ❌ 回归风险远高于收益 | +| C. 手写 UTF-8 长度表(否决) | 避免 chars() 解码 | ❌ 引入手写 0xC0/0xE0/0xF0 分支,标准库更可靠,性能差异可忽略 | + +### ADR-2:`parse_tag` 签名 `&[u8]` → `&str` + +- 理由:类型系统强制 UTF-8 保证,未来维护者想再写 `bytes[i] as char` 必须显式 `as_bytes()`,从类型层面降低复发概率;改动成本几乎为零(1 处签名 + 4 处调用点机械替换) +- 否决替代:内部 `from_utf8(bytes)` 转换以保持 `&[u8]` 签名——引入不可能触发的错误分支和 O(n) 校验,语义绕 + +### ADR-3:加 `debug_assert!(template.is_char_boundary(i))` + +- 零成本(仅 debug 构建生效)故障信号,防未来索引推进逻辑回归 + +## 5. 设计方案 + +### 5.1 修复模式(6 处统一) + +保持 `bytes[i]` 对 ASCII 语法字符判断不变;字符累积分支统一改为: + +```rust +} else { + debug_assert!(template.is_char_boundary(i)); + match template[i..].chars().next() { + Some(ch) => { + // 推入对应 String(literal / content / body / else_body) + i += ch.len_utf8(); + } + None => return Err(PromptError::Parse("模板包含非法字符序列".to_string())), + } +} +``` + +要点: + +- **`None` 分支语义统一为显式失败**:6 处全部返回 `Err(PromptError::Parse("模板包含非法字符序列"))`。特别地,`compile_fragments` 的 `None` 分支**绝不能**写 `break`——顶层循环 break 后会以 `Ok(fragments)` 返回,静默丢弃模板剩余部分;其余 5 处 break 虽会落到函数末尾的 `Err`(非静默),但错误消息为「未闭合」不准确。统一显式 `Err` 使防御失效时可观测,且消息一致准确 +- **None 分支为纯防御,结构性保证下不可达**:`while i < len` + 字符边界不变量保证 `template[i..]` 非空;且 `template[i..]` 在非字符边界处切片会先 panic(str 切片要求字符边界),`chars().next()` 返回 None 实际不会发生。该分支的价值在于:未来维护者若将索引逻辑改写为宽容 API(如 `get(i..)`),防御分支仍能保证显式失败而非静默错误 +- 每字符常数开销约 2–3 倍于原逐字节路径,但模板编译是低频一次性操作(register 时编译 / register_lazy 首次 render),渲染热路径走 Fragment AST 与此无关,无需优化;纯 ASCII 快速路径属于过度设计,明确不做 + +### 5.2 `parse_tag` 签名变更 + +```rust +fn parse_tag(template: &str, start: usize) -> Result<(String, usize), PromptError> { + let bytes = template.as_bytes(); // {{ 判断仍用字节比较 + // ...内容累积同样按 5.1 模式按字符边界推进 +} +``` + +- 4 个调用点(223 / 296 / 344 / 380 行):`parse_tag(bytes, i)` → `parse_tag(template, i)` +- 改动后 grep `parse_tag(` 复核 4 处调用点无遗漏 + +### 5.3 测试设计(`#[cfg(test)] mod tests` 新建于 template.rs) + +测试清单(对应 PRD §4 清单 + 评审增量,共 16 个用例): + +| # | 测试 | 覆盖点 | +|---|------|--------| +| 1 | 纯中文模板 compile+render 与原文逐字符一致 | 主破坏路径 literal 恒等 | +| 2 | 真实故障素材:PRD §3.1 原文「你是采集策略专家,负责审查已采集的产品编码结果,决策下一轮搜索方向。」及含 ★、→、中文引号「」、全角标点的样本 | 故障现场固化回归(增量 B) | +| 3 | 中文 + `{{ var }}` 变量插值混合 | 混合渲染 | +| 4 | 中文位于 `#if` body | 块内中文 | +| 5 | 中文位于 `#if` else 分支 | 块内中文 | +| 6 | 中文位于 `#each` body | 块内中文(循环变量固定 `{{item}}`,Array 用 `TemplateContext::from_json(&json!(...))` 构造) | +| 7 | 中文位于 `#raw` 内容 | 块内中文 | +| 8 | emoji 等 4 字节字符 | 4 字节字符保留 | +| 9 | 多字节字符紧邻 `{{` / `}}` 边界 | 边界解析 | +| 10 | 模板以中文结尾(EOF 边界) | 尾部边界 | +| 11 | 空模板 | 边界 | +| 12 | 中文 + 未闭合 `{{` → 返回 Err 且不 panic | 错误路径(增量 A),断言 `matches!(err, PromptError::Parse(_))`;None 防御分支结构性不可达、不单独设用例,本用例确保索引推进边界改动不引入 panic | +| 13 | 纯 ASCII 模板渲染与修复前一致 | 向后兼容回归:渲染结果等于预定义期望输出(PRD §7 要求,勿做快照对比);以 composer.rs 既有 4 个 ASCII 测试为硬基线 | +| 14 | 中文变量名 `{{ 问候 }}` | 标签内非 ASCII 内容(增量,PRD 已有) | +| 15 | 中文 `#if` 条件 | 标签内非 ASCII 内容(增量,PRD 已有) | +| 16 | 嵌套块:外层 `#if` 分支内含 `#each` + 中文文本 | 嵌套块中多字节字符正确保留(审查观察补充) | + +测试写法约定(增量 D): + +- 错误断言必须用 `matches!`(`PromptError` 只 derive 了 `Error, Debug`,无 `PartialEq`,`assert_eq!` 不可用——本模块首个测试文件最易踩的坑) +- `#each` 循环变量硬编码为 `item`(渲染器 `child_ctx.vars.insert("item", ...)` 固定) +- Array 构造沿用 composer.rs 既有模式:`TemplateContext::from_json(&serde_json::json!({...}))` +- 避免对含 `TemplateValue::Object` 的渲染输出做全等断言(HashMap 无序迭代,Display 输出不稳定) +- 测试函数签名可用 `-> Result<(), PromptError>` + `?` 或与 composer.rs 一致的 unwrap 风格 +- 测试 12 的未闭合标签错误断言:`tpl.unwrap_err()` 对 compile 返回值合法可调用(要求 `PromptTemplate: Debug`,Ok 时 panic),但 `PromptError` 无 `PartialEq`,拿 err 后无法 `assert_eq!` 比对变体;建议直接 `assert!(PromptTemplate::compile("中文{{未闭合").is_err())` 或 `matches!(tpl.unwrap_err(), PromptError::Parse(_))` + +### 5.4 版本号 + +- `Cargo.toml` version 0.3.6 → 0.3.7 +- 若仓库提交 Cargo.lock,确认 lock 中 agcore 条目随 `cargo build` 更新并一并提交 + +## 6. 实施步骤 + +| 步骤 | 操作 | 验证 | +|------|------|------| +| 1 | 跑基线:`cargo test` | composer.rs 4 个 ASCII 模板测试通过(硬基线) | +| 2 | 改 `parse_tag` 签名 + 4 调用点 | `cargo build` 通过 | +| 3 | 6 处字符推进修复 + debug_assert | `grep -n "as char\|char::from" src/` 无结果 | +| 4 | 新增 `#[cfg(test)] mod tests`(16 个用例) | `cargo test` 全绿 | +| 5 | Cargo.toml 0.3.6 → 0.3.7 + Cargo.lock 同步 | 版本号确认 | +| 6 | `cargo clippy` | 无新增警告 | +| 7 | 步骤 2–6 的改动合并为一次提交(避免「可编译但含缺陷」的中间态单独提交),打 tag v0.3.7(发布动作由维护者执行) | tag 描述含验证指引 | + +## 7. 验证标准 + +对齐 PRD §7 验收标准,并补充增量 A: + +- [ ] 中文模板经 compile + render 后与原文逐字符一致(含纯文本、变量插值混合、真实故障素材) +- [ ] 中文位于 #if / #each / #raw 块体内时正确保留 +- [ ] emoji 等 4 字节字符正确保留 +- [ ] 多字节字符紧邻 {{ / }} 边界时解析正确 +- [ ] 模板以中文结尾、空模板等边界场景不 panic +- [ ] 中文 + 未闭合标签 → 返回 Err 且不 panic(增量 A) +- [ ] 纯 ASCII 模板渲染结果与修复前完全一致(composer.rs 4 个测试为基线全部通过,新测试断言等于预定义期望输出) +- [ ] 6 处逐字节强转全部消除(grep `as char|char::from` 于 `src/` 无结果) +- [ ] cargo test 全部通过(含新增 16 个测试) +- [ ] cargo clippy 无新增警告 +- [ ] Cargo.toml 版本为 0.3.7 + +## 8. 发布计划 + +| 阶段 | 范围 | 说明 | +|------|------|------| +| v1 | 6 处修复 + 16 个回归测试 + 版本号 0.3.7 | 本方案范围 | +| 发布 | 打 tag v0.3.7,推送 origin | 实际发布动作由维护者执行 | +| 发布说明 | `git tag -a v0.3.7` tag 描述(增量 E): | 不更新 CHANGELOG | +| 下游升级 | dc-management 升级 agcore 引用至 v0.3.7 | 下游自行评估是否回切模板链路 | + +tag 描述建议内容(增量 E,写入发布说明): + +1. 修复说明:模板编译器 UTF-8 编码修复(6 处逐字节强转改为按字符边界推进),中文模板经 compile+render 后与原文一致 +2. 最小验证代码(可复制): + +```rust +use agcore::prompt::{PromptTemplate, TemplateContext}; +let tpl = PromptTemplate::compile("你是助手:{{msg}}").unwrap(); +let mut ctx = TemplateContext::new(); +ctx.insert("msg", "你好"); +assert_eq!(tpl.render(&ctx).unwrap(), "你是助手:你好"); +``` + +3. 行为变更提示:修复后 LLM 将从「读乱码提示词」变为「读正确提示词」,此前被破坏的规则约束(编码格式、品牌限定等)恢复生效,模型输出可能明显变化——升级后建议跑一轮真实采集对比验证后再正式切换 + +## 9. 回滚方案 + +- 代码回滚:`git revert` 该修复 commit,回到 0.3.6 行为 +- 下游回退:dc-management 将 agcore 依赖回退至 0.3.6 即可恢复原行为(无 API 变更,回退无迁移成本) +- 注意:回滚即恢复乱码行为,仅作为应急手段;正确路径是验证后继续使用 0.3.7 +- 版本号冲突:`git revert` 后 Cargo.toml 回到 0.3.6,若应急后需重新发布,将撞上已发布的 v0.3.6/v0.3.7 tag,应升级至 0.3.8 + +## 10. 历史版本 + +| 版本 | 日期 | 变更说明 | +|------|------|---------| +| v1 | 2026-08-03 | 首版方案(基于 PRD v1 + 双顾问评审增量) | +| v2 | 2026-08-03 | 第 1 轮审查结论修正:None 分支统一显式 Err(消除静默截断)、测试 13 断言措辞对齐 PRD、新增嵌套块测试 16、回滚补版本号冲突说明、grep 范围标注 src/ | +| v3 | 2026-08-03 | 第 2 轮复审修正:§6 步骤 4 用例数 15→16 全文统一、unwrap_err 断言表述订正、§7 第 7 条补「预定义期望输出」对齐 PRD、§2.1 非目标补充裸闭合标签静默截断不在范围 | diff --git a/design/prd/2-模板编译器UTF-8编码修复需求.md b/design/prd/2-模板编译器UTF-8编码修复需求.md new file mode 100644 index 0000000..8d652bf --- /dev/null +++ b/design/prd/2-模板编译器UTF-8编码修复需求.md @@ -0,0 +1,172 @@ +# PRD:模板编译器 UTF-8 编码修复 + +**状态**:Draft +**作者**:proposal +**日期**:2026-08-03 + +## 1. 核心目标 + +修复 `agcore::prompt::PromptTemplate` 模板编译器(`src/prompt/template.rs`)对非 ASCII(UTF-8)文本的编码破坏问题:当前 6 处 `bytes[i] as char` 将 UTF-8 字节逐字节强转为 Unicode 码点(等价 Latin-1 解码),导致所有含中文等多字节字符的模板经 `compile → render` 后输出乱码(mojibake),且该乱码会原样发送给 LLM。本次修复使模板编译器对非 ASCII 文本逐字符正确保留,同时保持 ASCII 模板行为完全不变。 + +## 2. 目标用户与场景 + +| 用户角色 | 使用场景 | 核心诉求 | +|---------|---------|---------| +| agcore 下游 crate(如 dc-management) | 使用 `PromptTemplate` 编译中文 system prompt 模板(validator / collector 采集策略) | 模板文本经编译渲染后与原文一致,不产生乱码 | +| agcore 下游 crate | 使用 `PromptTemplate` 的 `#if` / `#each` / `#raw` 块语法,块体内含中文 | 块内容逐字符正确保留 | +| agcore 维护者 | 修复后发布 patch 版本,下游可升级 | 公开 API 不变、ASCII 行为不变、有回归测试保障 | + +## 3. 问题描述 + +### 3.1 现象 + +dc-management 项目的探索调试日志(`log/explore-20260803-013158915066.txt`)显示:发送给 LLM 的 system 提示词全部乱码: + +``` +ä½ æ¯éä¾ç¥ç¥ä¸å®¶ï¼è´è´£å®¡æ¥å·²é产åç¼ç ç»æï¼å³ç­ä¸ä¸è½®æç´¢æ¹åã +``` + +(原文为「你是采集策略专家,负责审查已采集的产品编码结果,决策下一轮搜索方向。」) + +日志中的 user 消息与 LLM 响应均为正常中文,证明:乱码发生在**内存字符串**中(模板编译阶段),实际发送给 LLM 的请求体就是损坏文本;日志文件本身(纯 UTF-8 直写)只是如实记录了已损坏的内容。该缺陷为静默失败——模型在容忍乱码的情况下继续工作,但提示词中的规则约束(编码格式、品牌限定等)已被破坏,直接影响下游采集质量。 + +### 3.2 根因 + +`src/prompt/template.rs` 共 6 处 `bytes[i] as char`(将 UTF-8 字节逐字节强转为 Unicode 码点,等价 Latin-1 解码): + +| 行号 | 位置 | 破坏内容 | +|------|------|---------| +| 255 | `compile_fragments` literal 分支 | 模板纯文本(**主要破坏点**) | +| 275 | `parse_tag` | `{{ 标签 }}` 内部内容(当前为 ASCII 变量名,未触发;若使用中文变量名同样损坏) | +| 324 | `parse_block` else_body | `#if` 分支的 else 块体 | +| 326 | `parse_block` body | `#if` 分支块体 | +| 364 | `parse_each_block` body | `#each` 块体 | +| 389 | `parse_raw_block` content | `#raw` 块内容 | + +中文 UTF-8 编码每字 3 字节(emoji 等 4 字节),被拆成多个 Latin-1 字符(如 `0xE4 → ä`、`0xBD → ½`),产生 mojibake。所有经 `PromptTemplate::compile → render` 链路的非 ASCII 模板文本均被破坏。 + +### 3.3 影响面 + +- 所有使用 `PromptTemplate` 且模板含非 ASCII 文本的 agcore 下游项目 +- dc-management 的采集策略(validator)与产品信息采集(collector)两条 system prompt 链路全部受影响 +- 已确认模板文件本身为合法 UTF-8、日志写出链路无转码、发送链路无转码——问题仅存在于模板编译器 + +### 3.4 已知线索 + +- 下游使用 `include_str!` 宏加载模板文件(如 dc-management 的 `src-tauri/src/llm/prompts.rs`),该宏具有编译期 UTF-8 校验:非法编码的模板文件在编译期即报错,故模板文件编码问题不可能存在 +- agcore 仓库 `src/prompt/template.rs` 当前没有任何测试(无 `#[cfg(test)]` 模块),本次为模板编译器首次补测试 +- 模板语法标签(`{{ }}`、`#if`、`#each`、`#raw`)均为 ASCII,字节比较判断语法是安全的,无需改动 + +## 4. 功能清单 + +### v1 必做 + +- **统一修复 6 处 `bytes[i] as char`**(`src/prompt/template.rs` 255 / 275 / 324 / 326 / 364 / 389 行): + - 保持现有 `bytes[i]` 对 ASCII 语法字符(`b'{'` / `b'}'` 等)的字节判断不变 + - 字符累积改为按 UTF-8 字符边界推进:从当前位置取完整字符(如 `template[i..].chars().next()`),再按该字符的 UTF-8 长度推进索引(`i += ch.len_utf8()`) + - 效果:非 ASCII 文本(中文 / emoji / 任意多字节字符)逐字符正确保留;ASCII 文本行为与修复前完全一致 + +- **补充回归测试**(`src/prompt/template.rs` 新建 `#[cfg(test)] mod tests`): + - 纯中文模板 compile + render 后与原文逐字符一致(主破坏路径 literal) + - 中文文本 + `{{ var }}` 变量插值混合渲染正确 + - 中文文本位于 `#if` / `#each` / `#raw` 块体内时正确保留 + - emoji 等 4 字节字符正确保留 + - 多字节字符紧邻 `{{` / `}}` 标签边界的解析正确性 + - 纯 ASCII 模板渲染结果与修复前一致(向后兼容回归) + - 标签内部含非 ASCII 内容(中文变量名如 `{{ 问候 }}`、中文 `#if` 条件)的解析与渲染正确 ← PM Advisor + +- **版本号更新**:`Cargo.toml` version `0.3.6` → `0.3.7` + +### v1 可选 + +- 无(上述三项即可完整修复) + +### v2 考虑 + +- 无。CHANGELOG 记录暂不做(本次需求明确暂不记录),后续版本若需要可单独补充 + +### 非目标 + +- 不重构模板语法(标签体系、块结构保持现状) +- 不新增模板功能(新标签、新渲染特性) +- 不改动公开 API(`PromptTemplate::compile` / `render` 签名与语义不变) +- 不修改其他模块(已扫描 `as char` / `from_utf8_lossy` 等模式,项目中无同类编码风险点) +- 不涉及下游 dc-management 是否回切模板链路的决策(由下游另行评估) + +## 5. 边界与假设 + +| 边界 / 假设 | 来源 | 说明 | +|-------------|------|------| +| ASCII 语法判断可保留 | 语法分析 | 模板标签均为 ASCII(`{`、`}`、`#`、`/`、`>`),`bytes[i]` 字节比较对 ASCII 安全 | +| 非 ASCII 文本必须逐字符保留 | 需求确认 | 修复目标是编译输出与模板原文一致 | +| ASCII 模板行为零变化 | 需求确认 | 向后兼容是硬约束,需回归测试保障 | +| 公开 API 不变 | 需求确认 | `compile` / `render` 签名不变,下游无需改代码 | +| 修复范围 6 处统一 | 用户确认 | 「尽可能全面的修复」,包括 parse_tag 内的变量名内容 | +| 测试纳入本次范围 | 用户确认 | 用户明确要求补充测试 | +| 版本为 patch(0.3.7) | 用户确认 | bug 修复走 patch 版本,尽快让下游升级 | +| CHANGELOG 暂不记录 | 用户确认 | 本次发布不更新 CHANGELOG | + +## 6. 术语表 + +| 术语 | 定义 | 说明 | +|------|------|------| +| mojibake | 乱码 | 文本因编码解码不匹配产生的字符错乱,此处为 UTF-8 字节被按 Latin-1 逐字节解码 | +| Latin-1 | ISO-8859-1 单字节编码 | 0x00–0xFF 每个字节对应一个 Unicode 码点;`u8 as char` 等价于该解码 | +| `len_utf8()` | Rust 标准库方法 | 返回 `char` 的 UTF-8 编码长度(1–4 字节),用于按字符边界推进索引 | +| `PromptTemplate` | agcore 提示词模板引擎 | 支持变量插值 `{{ var }}`、条件渲染 `#if`、循环 `#each`、原始块 `#raw` | + +## 7. 验收标准 + +- [ ] 中文模板经 `compile` + `render` 后与原文逐字符一致(含纯文本、变量插值混合) +- [ ] 中文位于 `#if` / `#each` / `#raw` 块体内时正确保留 +- [ ] emoji 等 4 字节字符正确保留 +- [ ] 多字节字符紧邻 `{{` / `}}` 边界时解析正确 +- [ ] 纯 ASCII 模板渲染结果与修复前完全一致(以 `src/prompt/composer.rs` 既有 4 个 ASCII 编译测试为基线全部通过,新测试断言等于预定义期望输出)← PM Advisor +- [ ] 6 处逐字节强转全部消除(grep 模式 `as char|char::from` 验证)← PM Advisor +- [ ] `cargo test` 全部通过(含新增测试) +- [ ] `cargo clippy` 无新增警告 +- [ ] `Cargo.toml` 版本为 0.3.7 + +## 8. 风险评估 + +| 风险 | 影响 | 可能性 | 应对方向 | +|------|------|--------|---------| +| 索引推进改动引入边界回归(模板尾部、空模板、`{{` 未闭合) | 解析错误或 panic | 中 | 新增测试覆盖边界;保持 ASCII 判断逻辑不变 | +| parse_tag 内标签内容的解码方式与 literal 不一致 | 中文变量名场景损坏残留 | 低 | 6 处统一采用同一修复模式 | +| 下游对 0.3.7 的升级未验证中文模板 | 下游继续受影响 | 中 | tag 描述中明确修复内容与最小验证步骤(编译含中文的模板对比输出)← PM Advisor | +| 未来新增模板代码时再次引入逐字节强转 | 同类 bug 复发 | 低 | 回归测试(中文断言)可捕获此类问题 | + +## 9. 发布计划 + +| 阶段 | 范围 | 说明 | +|------|------|------| +| v1 | 6 处修复 + 回归测试 + 版本号 0.3.7 | 本 PRD 范围 | +| 发布 | 打 tag `v0.3.7`,推送 origin | 实际发布动作由维护者执行 | +| 发布说明 | 以 `git tag -a v0.3.7` 的 tag 描述作为发布说明载体(附最小验证:编译含中文的模板并对比输出)← PM Advisor | 不更新 CHANGELOG,tag 描述随仓库分发 | +| 下游升级 | dc-management 升级 agcore 引用至 v0.3.7 | 下游自行评估是否回切模板链路 | + +## 10. 历史版本 + +| 版本 | 日期 | 变更说明 | +|------|------|---------| +| v1 | 2026-08-03 | 人工种子(原始) | + +### 种子内容 + +发起方:dc-management 项目排查探索调试日志 `log/explore-20260803-013158915066.txt`,发现发送给 LLM 的 system 提示词乱码。 + +原始需求描述(一字不改): + +「检查一下当前日志 log 目录中保存的日志文件,似乎现在发送给LLM的提示词存在乱码?」 + +根因: + +- `src/prompt/template.rs` 中 6 处 `bytes[i] as char`(255 / 275 / 324 / 326 / 364 / 389 行)将 UTF-8 字节逐字节强转为 Unicode 码点(等价 Latin-1 解码),非 ASCII 模板文本被破坏 +- 日志文件、发送链路、模板文件编码均无问题,乱码发生在内存字符串(模板编译阶段) + +修复方向(用户确认): + +- 6 处统一修复:保持 ASCII 语法判断不变,字符累积按 UTF-8 字符边界推进 +- 补充回归测试(中文模板恒等、混合插值、块内中文、emoji、边界、ASCII 回归) +- `Cargo.toml` 0.3.6 → 0.3.7,打 tag `v0.3.7` +- CHANGELOG 暂不记录