Skip to content

Commit 66cb97f

Browse files
authored
[0969] 修复 LLM 插件输出 emoji 乱码:herk_to_utf8 补 4 字节 UTF-8 分支 (#4536)
1 parent 9313d00 commit 66cb97f

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

devel/0969.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# 任务 0969:修复 LLM 插件输出 emoji 乱码
2+
3+
## 问题
4+
5+
llm 商业版中发现 LLM 回复中的 emoji(如 😊)显示为乱码;在 mogan 自带的
6+
llm 假插件(`TeXmacs/plugins/llm`,Insert → Session → LLM)可稳定复现。
7+
用户提供的 `你是谁.tmu` 中,原应为 😊 的位置存储的是 `C3 BF C2 98 C2 8A`
8+
(即 U+00FF U+0098 U+008A),屏幕上显示为 `ÿ` 加两个缺字形方块。
9+
10+
## 根因
11+
12+
Mogan 会话输出链路:插件子进程 stdout → `texmacs_input_rep::utf8_flush`
13+
`src/Data/Convert/Generic/input.cpp`)→ `tree_utf8_to_herk` → 文档内部
14+
herk 编码。UTF-8 → herk 方向(`lolly::data::utf8_to_herk`)把映射不到单字节的
15+
码点正确逃逸为 `<#XXXX>`(如 😊 U+1F60A → `<#1F60A>`)。
16+
17+
但反向 `lolly::data::herk_to_utf8` 的辅助函数 `append_utf8_code`
18+
`lolly/lolly/data/herk.cpp`)只有 1/2/3 字节 UTF-8 分支,缺 4 字节分支。
19+
对 ≥ U+10000 的码点按 3 字节形式截断编码:
20+
21+
- U+1F60A:`0xE0|(code>>12)`=0xFF、`0x80|((code>>6)&0x3F)`=0x98、
22+
`0x80|(code&0x3F)`=0x8A → 输出非法 UTF-8 字节 `FF 98 8A`
23+
- 下游(聊天 tab 显示、.tmu 保存)把这三个坏字节按 Latin-1 逐字节再编码为
24+
`C3 BF C2 98 C2 8A`,与 .tmu 文件中观察到的乱码逐字节吻合。
25+
26+
对比:cork 侧同功能的 `cork_to_utf8``encode_as_utf8`
27+
`lolly/lolly/data/unicode.cpp`),有完整 4 字节分支,不受影响;
28+
全库仅 `herk.cpp``append_utf8_code` 一处缺该分支。
29+
30+
受影响路径(均经 `herk_to_utf8`/`tree_herk_to_utf8`):会话/聊天输出还原、
31+
`connection.cpp:266` 插件输入序列化、`edit_complete.cpp:359`
32+
`QTMMenuhelper.cpp:93``smart_font.cpp:865`,以及 Scheme glue
33+
`herk->utf8` / `tree-herk->utf8-tree`
34+
35+
## 修复
36+
37+
- `lolly/lolly/data/herk.cpp``append_utf8_code` 补上
38+
`code < 0x10000` 的 3 字节分支与 `code <= 0x1FFFFF` 的 4 字节分支,
39+
超出 Unicode 上限的输入不再编码(与 `encode_as_utf8` 行为对齐)。
40+
41+
## 测试
42+
43+
- `lolly/tests/lolly/data/herk_test.cpp` 新增 `herk_4byte_codepoints` 用例:
44+
U+1F60A/U+1F642/U+20000/U+10FFFF 的 `<#XXXX>` → UTF-8 往返,及混合文本
45+
`A<#1F60A>B` 的还原。
46+
- 运行 `xmake test lolly_tests/herk_test`(仓库根目录):修复前新用例
47+
失败(复现 bug),修复后 38 用例 635 断言全部通过。
48+
- GUI 手工验证(Insert → Session → LLM,假插件回显含 😊 的消息)由用户执行。
49+
50+
## 涉及文件
51+
52+
- `lolly/lolly/data/herk.cpp`(修复)
53+
- `lolly/tests/lolly/data/herk_test.cpp`(回归测试)
54+
- `devel/0969.md`(本文档)

lolly/lolly/data/herk.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,19 @@ append_utf8_code (string& r, int code) {
118118
r << (char) (0xC0 | (code >> 6));
119119
r << (char) (0x80 | (code & 0x3F));
120120
}
121-
else {
121+
else if (code < 0x10000) {
122122
r << (char) (0xE0 | (code >> 12));
123123
r << (char) (0x80 | ((code >> 6) & 0x3F));
124124
r << (char) (0x80 | (code & 0x3F));
125125
}
126+
else if (code <= 0x1FFFFF) {
127+
// 4 字节形式(U+10000 以上,含 emoji);缺失此分支会把 17+ 位码点
128+
// 截成 3 字节非法 UTF-8(如 U+1F60A 曾输出 FF 98 8A 即 LLM emoji 乱码)
129+
r << (char) (0xF0 | (code >> 18));
130+
r << (char) (0x80 | ((code >> 12) & 0x3F));
131+
r << (char) (0x80 | ((code >> 6) & 0x3F));
132+
r << (char) (0x80 | (code & 0x3F));
133+
}
126134
}
127135

128136
string

lolly/tests/lolly/data/herk_test.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,23 @@ TEST_CASE ("herk_escapes") {
746746
string_eq (utf8_to_herk ("<#00FF>"), "<#00FF>");
747747
}
748748

749+
TEST_CASE ("herk_4byte_codepoints") {
750+
// 辅助平面码点(emoji 等)往返:herk 侧逃逸为 <#XXXX>,还原须是 4 字节
751+
// UTF-8 而非截断的 3 字节(U+1F60A 曾被截成 FF 98 8A)
752+
string_eq (herk_to_utf8 ("<#1F60A>"), "\xF0\x9F\x98\x8A"); // U+1F60A 😊
753+
string_eq (utf8_to_herk ("\xF0\x9F\x98\x8A"), "<#1F60A>"); // U+1F60A
754+
string_eq (herk_to_utf8 ("<#1F642>"), "\xF0\x9F\x99\x82"); // U+1F642 🙂
755+
string_eq (utf8_to_herk ("\xF0\x9F\x99\x82"), "<#1F642>"); // U+1F642
756+
string_eq (herk_to_utf8 ("<#20000>"),
757+
"\xF0\xA0\x80\x80"); // U+20000 𠀀
758+
string_eq (utf8_to_herk ("\xF0\xA0\x80\x80"), "<#20000>"); // U+20000
759+
string_eq (herk_to_utf8 ("<#10FFFF>"),
760+
"\xF4\x8F\xBF\xBF"); // U+10FFFF
761+
string_eq (utf8_to_herk ("\xF4\x8F\xBF\xBF"), "<#10FFFF>"); // U+10FFFF
762+
string_eq (herk_to_utf8 ("A<#1F60A>B"), "A\xF0\x9F\x98\x8A"
763+
"B");
764+
}
765+
749766
TEST_CASE ("herk_named_escapes") {
750767
string_eq (utf8_to_herk ("<less>"), "<less>");
751768
string_eq (herk_to_utf8 ("<less>"), "<less>");

0 commit comments

Comments
 (0)