Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions devel/0969.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 任务 0969:修复 LLM 插件输出 emoji 乱码

## 问题

llm 商业版中发现 LLM 回复中的 emoji(如 😊)显示为乱码;在 mogan 自带的
llm 假插件(`TeXmacs/plugins/llm`,Insert → Session → LLM)可稳定复现。
用户提供的 `你是谁.tmu` 中,原应为 😊 的位置存储的是 `C3 BF C2 98 C2 8A`
(即 U+00FF U+0098 U+008A),屏幕上显示为 `ÿ` 加两个缺字形方块。

## 根因

Mogan 会话输出链路:插件子进程 stdout → `texmacs_input_rep::utf8_flush`
(`src/Data/Convert/Generic/input.cpp`)→ `tree_utf8_to_herk` → 文档内部
herk 编码。UTF-8 → herk 方向(`lolly::data::utf8_to_herk`)把映射不到单字节的
码点正确逃逸为 `<#XXXX>`(如 😊 U+1F60A → `<#1F60A>`)。

但反向 `lolly::data::herk_to_utf8` 的辅助函数 `append_utf8_code`
(`lolly/lolly/data/herk.cpp`)只有 1/2/3 字节 UTF-8 分支,缺 4 字节分支。
对 ≥ U+10000 的码点按 3 字节形式截断编码:

- U+1F60A:`0xE0|(code>>12)`=0xFF、`0x80|((code>>6)&0x3F)`=0x98、
`0x80|(code&0x3F)`=0x8A → 输出非法 UTF-8 字节 `FF 98 8A`;
- 下游(聊天 tab 显示、.tmu 保存)把这三个坏字节按 Latin-1 逐字节再编码为
`C3 BF C2 98 C2 8A`,与 .tmu 文件中观察到的乱码逐字节吻合。

对比:cork 侧同功能的 `cork_to_utf8` 走 `encode_as_utf8`
(`lolly/lolly/data/unicode.cpp`),有完整 4 字节分支,不受影响;
全库仅 `herk.cpp` 的 `append_utf8_code` 一处缺该分支。

受影响路径(均经 `herk_to_utf8`/`tree_herk_to_utf8`):会话/聊天输出还原、
`connection.cpp:266` 插件输入序列化、`edit_complete.cpp:359`、
`QTMMenuhelper.cpp:93`、`smart_font.cpp:865`,以及 Scheme glue
`herk->utf8` / `tree-herk->utf8-tree`。

## 修复

- `lolly/lolly/data/herk.cpp`:`append_utf8_code` 补上
`code < 0x10000` 的 3 字节分支与 `code <= 0x1FFFFF` 的 4 字节分支,
超出 Unicode 上限的输入不再编码(与 `encode_as_utf8` 行为对齐)。

## 测试

- `lolly/tests/lolly/data/herk_test.cpp` 新增 `herk_4byte_codepoints` 用例:
U+1F60A/U+1F642/U+20000/U+10FFFF 的 `<#XXXX>` → UTF-8 往返,及混合文本
`A<#1F60A>B` 的还原。
- 运行 `xmake test lolly_tests/herk_test`(仓库根目录):修复前新用例
失败(复现 bug),修复后 38 用例 635 断言全部通过。
- GUI 手工验证(Insert → Session → LLM,假插件回显含 😊 的消息)由用户执行。

## 涉及文件

- `lolly/lolly/data/herk.cpp`(修复)
- `lolly/tests/lolly/data/herk_test.cpp`(回归测试)
- `devel/0969.md`(本文档)
10 changes: 9 additions & 1 deletion lolly/lolly/data/herk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,19 @@ append_utf8_code (string& r, int code) {
r << (char) (0xC0 | (code >> 6));
r << (char) (0x80 | (code & 0x3F));
}
else {
else if (code < 0x10000) {
r << (char) (0xE0 | (code >> 12));
r << (char) (0x80 | ((code >> 6) & 0x3F));
r << (char) (0x80 | (code & 0x3F));
}
else if (code <= 0x1FFFFF) {
// 4 字节形式(U+10000 以上,含 emoji);缺失此分支会把 17+ 位码点
// 截成 3 字节非法 UTF-8(如 U+1F60A 曾输出 FF 98 8A 即 LLM emoji 乱码)
r << (char) (0xF0 | (code >> 18));
r << (char) (0x80 | ((code >> 12) & 0x3F));
r << (char) (0x80 | ((code >> 6) & 0x3F));
r << (char) (0x80 | (code & 0x3F));
}
}

string
Expand Down
17 changes: 17 additions & 0 deletions lolly/tests/lolly/data/herk_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,23 @@ TEST_CASE ("herk_escapes") {
string_eq (utf8_to_herk ("<#00FF>"), "<#00FF>");
}

TEST_CASE ("herk_4byte_codepoints") {
// 辅助平面码点(emoji 等)往返:herk 侧逃逸为 <#XXXX>,还原须是 4 字节
// UTF-8 而非截断的 3 字节(U+1F60A 曾被截成 FF 98 8A)
string_eq (herk_to_utf8 ("<#1F60A>"), "\xF0\x9F\x98\x8A"); // U+1F60A 😊
string_eq (utf8_to_herk ("\xF0\x9F\x98\x8A"), "<#1F60A>"); // U+1F60A
string_eq (herk_to_utf8 ("<#1F642>"), "\xF0\x9F\x99\x82"); // U+1F642 🙂
string_eq (utf8_to_herk ("\xF0\x9F\x99\x82"), "<#1F642>"); // U+1F642
string_eq (herk_to_utf8 ("<#20000>"),
"\xF0\xA0\x80\x80"); // U+20000 𠀀
string_eq (utf8_to_herk ("\xF0\xA0\x80\x80"), "<#20000>"); // U+20000
string_eq (herk_to_utf8 ("<#10FFFF>"),
"\xF4\x8F\xBF\xBF"); // U+10FFFF
string_eq (utf8_to_herk ("\xF4\x8F\xBF\xBF"), "<#10FFFF>"); // U+10FFFF
string_eq (herk_to_utf8 ("A<#1F60A>B"), "A\xF0\x9F\x98\x8A"
"B");
}

TEST_CASE ("herk_named_escapes") {
string_eq (utf8_to_herk ("<less>"), "<less>");
string_eq (herk_to_utf8 ("<less>"), "<less>");
Expand Down
Loading