Conversation
审查者指南(小型 PR 中折叠显示)审查者指南该 PR 通过将混合消息中的动态超级表情编码为行内表情,避免消息被截断,同时保留独立表情的全屏渲染效果;此外,它还修复了接收端的解析逻辑,使服务类型 37 不再停止处理后续元素。 服务类型 37 后继续解析的时序图sequenceDiagram
participant P as parseElements
participant R as ParsedMessage
P->>P: Decode serviceType 37
P->>R: Append parsed face element
P->>P: Continue processing next message element
P->>R: Append subsequent message segment
混合消息中超级表情编码的流程图flowchart TD
A[Face element] --> B{faceType === 3}
B -->|yes| C{inputElems.length === 1}
C -->|yes| D[Encode LargeFaceExtra serviceType 37]
C -->|no| E[Encode QSmallFaceExtra serviceType 33]
B -->|no| F{faceType === 2}
F -->|yes| E
文件级变更
可能相关的问题
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板以:
获取帮助Original review guide in EnglishReviewer's guide (collapsed on small PRs)Reviewer's GuideThe PR prevents mixed messages containing animated super stickers from being truncated by encoding them as inline faces, while preserving full-screen rendering for standalone stickers; it also fixes receive-side parsing so service type 37 no longer stops processing subsequent elements. Sequence diagram for continued parsing after service type 37sequenceDiagram
participant P as parseElements
participant R as ParsedMessage
P->>P: Decode serviceType 37
P->>R: Append parsed face element
P->>P: Continue processing next message element
P->>R: Append subsequent message segment
Flow diagram for super sticker encoding in mixed messagesflowchart TD
A[Face element] --> B{faceType === 3}
B -->|yes| C{inputElems.length === 1}
C -->|yes| D[Encode LargeFaceExtra serviceType 37]
C -->|no| E[Encode QSmallFaceExtra serviceType 33]
B -->|no| F{faceType === 2}
F -->|yes| E
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
嘿——我发现了 1 个问题
AI 代理提示词
请处理这次代码审查中的评论:
## 个别评论
### 评论 1
<location path="src/ntqqapi/helper/messageBuilding.ts" line_range="83" />
<code_context>
},
})
- } else if (faceElement.faceType === 2) {
+ } else if (faceElement.faceType === 2 || faceElement.faceType === 3) {
const f = faceElement
const pbElem = Msg.QSmallFaceExtra.encode({
</code_context>
<issue_to_address>
**issue (broader_impact):** 包含 `faceType === 3` 骰子或剪刀石头布元素的混合消息会被编码为 `serviceType: 33`。该类型使用 `QSmallFaceExtra`,且不携带 `resultId`。因此,接收解析器会将其还原为没有结果的普通 `faceType: 2` 表情,而 OneBot 转换器会生成结果未定义的骰子/剪刀石头布消息段。
**触发条件:** 当 `SendElement.dice()` 或 `SendElement.rps()` 与其他任意消息元素一起包含在消息中时。
**建议修复:** 将降级范围限制为 AniStickerType 超级表情,或将骰子/剪刀石头布保留为 `serviceType: 37`,以便继续编码其 `resultId`。
</issue_to_address>Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/ntqqapi/helper/messageBuilding.ts" line_range="83" />
<code_context>
},
})
- } else if (faceElement.faceType === 2) {
+ } else if (faceElement.faceType === 2 || faceElement.faceType === 3) {
const f = faceElement
const pbElem = Msg.QSmallFaceExtra.encode({
</code_context>
<issue_to_address>
**issue (broader_impact):** Mixed messages containing `faceType === 3` dice or rock-paper-scissors elements are encoded as `serviceType: 33`, which uses `QSmallFaceExtra` and does not carry `resultId`. The receive parser therefore reconstructs them as ordinary `faceType: 2` faces with no result, and the OneBot transformer emits dice/RPS segments with an undefined result.
**Triggers:** When `SendElement.dice()` or `SendElement.rps()` is included alongside any other message element.
**Suggested fix:** Restrict the downgrade to AniStickerType super faces, or preserve dice/RPS as `serviceType: 37` so their `resultId` remains encoded.
</issue_to_address>- 发送端:多元素图文混排时将超级表情降级为小表情形态(serviceType: 33),仅在单体唯一表情时构造 serviceType: 37 大表情 - 接收端:移除 messageParsing 遍历 elements 循环中 svcType===37 分支内错误的 break 语句,防止后续消息元素被丢弃 Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
3a496c7 to
a42c169
Compare
- 替换原有的 break 语句为针对 fallback 文本的跳过逻辑(skipIndex) - 自动剔除紧随其后的 [表情描述] 或 [动画表情] 等降级文本,同时确保后续正常消息内容不被截断 Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
为什么 NapCat 没有遇到这些问题?(兼谈与 Hook 框架的架构差异)在研究对齐 NapCat(以 1. 发送端(混排大表情截断)
2. 接收端(降级文本与 break 隐患)
|
- 补充对腾讯服务端在 serviceType: 37 大表情后附带 fallback 纯文本段的机制说明 - 记录实测确认的典型案例(/吃糖 附带 [吃糖],/菜汪 附带 [菜汪] 等) Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
- 注释说明腾讯 QQ 服务端禁止在混排中携带 serviceType: 37(会触发 retcode 1200 拦截) - 阐明多元素混排时必须降级为 serviceType: 33 的官方客户端行为依据 Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
- 新增 docs/super-face-protocol.md 总结 serviceType 33 与 37 协议陷阱与防呆规则 - 在 CLAUDE.md 文档索引表中补充对应条目 Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
01d7a85 to
34869a3
Compare
- 明确 LargeFaceExtra 单体发送约束:仅在消息段长度为 1 时构造 serviceType: 37 大表情 - 阐明混排降级分支原理:具备大表情潜质(faceType: 3)的元素在混排时自动委屈降级为 serviceType: 33 小表情形态,避免腾讯服务端报错与客户端截断 Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
|
ok啊,我似乎理解更多东西了, 如果我没理解错的话: 在这个commit里面新增了注释, 有关为什么 分别对于两个parse消息的分支情况的判断 增加了 一个and 和 一个or: } else if (faceElement.faceType === 3 && this.inputElems.length === 1) {
// 腾讯协议约束:超级大表情 LargeFaceExtra(dice / rps / 超级表情等)必须作为单体消息独立发送(消息段数组长度为 1),
// 此时才视为合法的大表情调用,构造 serviceType: 37;若在图文混排等情况(消息段数组的长度 > 1)中强传 37,腾讯服务端会拒收并报错 retcode: 1200。
const f = faceElement
const pbElem = Msg.LargeFaceExtra.encode({ ... })
this.outputElems.push({
commonElem: {
serviceType: 37,
pbElem,
businessType: f.stickerType ?? 1,
},
})
} else if (faceElement.faceType === 2 || faceElement.faceType === 3) {
// 1. faceType === 2: 原生小黄豆扩展表情。
// 2. faceType === 3: 虽然具有成为大表情的潜质,但由于与其他文字/图片等消息段混排(未通过上方 length === 1 的独立发送检验),
// 因此对齐官方 QQ 客户端行为,委屈其降级为小表情形态(serviceType: 33 / QSmallFaceExtra)打包进消息段数组,确保混排消息完整送达且不截断。
const f = faceElement |
- 完善图文混排描述:细化为“图文混排等情况(消息段数组的长度 > 1)” - 明确混排对象范围:补充“与其他文字/图片等消息段混排” Co-authored-by: gemini-code-assist <200291788+gemini-code-assist@users.noreply.github.com>
|
然后就是有关于 剔除 所谓 // 剔除紧随其后的降级文本段(腾讯服务端为兼容老客户端,在下发 serviceType: 37 大表情时会附带 fallback 纯文本段)
// 实测典型案例:/吃糖 会附带 "[吃糖]",/菜汪 会附带 "[菜汪]",骰子附带 "[骰子]" 等
// 采用 skipIndex 精准跳过此冗余段,避免直接 break 导致后续正常消息内容被截断
const nextElem = elems[index + 1]
if (nextElem?.text?.str) {
const nextStr = nextElem.text.str
const faceName = face?.QDes ? face.QDes.replace(/^\//, '') : ''
const isFallbackText =
nextStr === '[动画表情]' ||
(faceName && (nextStr === `[${faceName}]` || nextStr === face.QDes)) ||
(faceIndex === 358 && nextStr === '[骰子]') ||
(faceIndex === 359 && (nextStr === '[包剪锤]' || nextStr === '[剪刀石头布]'))
if (isFallbackText) {
skipIndex = index + 1
}
} |
当然我暂时还没想到 or 测到其他的边界情况 (本pr 可能会引发的更多新问题捏) 所以请review, thanks。 (¦3[▓▓] 晚安 , 心脏有点不好喵 |
我的思考是这样的,如果llbot遇到了一个serviceType37的 大表情消息段,那么这个一定是单独的一个超大表情, 这个结论建立在这个假设之上: 腾讯既然能把这种消息广播出来,那么腾讯服务端肯定校验过了 这个是一个合法的 单独超大表情消息。 而我觉得这个假设成立的概率很大喵。 |

















问题
AniStickerType的超级表情在图文混排时被强制构造为全屏大表情(serviceType: 37),导致 QQ 客户端富文本渲染截断,丢失后续所有内容。messageParsing.ts在处理svcType === 37时误用了break,导致跳出遍历消息元素的for循环,丢弃后续消息段。修复
inputElems.length > 1)将超级表情降级为行内小表情形态(serviceType: 33),仅单体唯一表情构造全屏大表情。messageParsing循环内的break。复现代码
效果预览
Sourcery 总结
修复超级贴纸渲染和消息解析问题,确保混合内容消息保留完整内容。
Bug 修复:
Original summary in English
Sourcery 总结
修复超级贴纸渲染和消息解析问题,确保混合内容消息保留完整内容。
错误修复:
Original summary in English
Sourcery 总结
在正确处理超级贴纸渲染和备用文本的同时,保留完整的混合内容消息。
错误修复:
Original summary in English
Sourcery 摘要
修复超级表情的处理逻辑,确保混合消息保持完整,同时让独立动画继续保留全屏显示行为。
Bug 修复:
文档:
Original summary in English
Sourcery 摘要
修复超级表情处理逻辑,确保混合消息内容完整,同时保留独立动画表情的全屏渲染效果。
错误修复:
文档:
Original summary in English
Sourcery 总结
在保留独立超大表情全屏渲染效果的同时,完整保留混合消息内容。
错误修复:
文档:
Original summary in English
Sourcery 总结
修复超级表情符号的处理逻辑,确保混合消息保持完整,同时独立的动画表情符号仍可全屏显示。
错误修复:
文档:
Original summary in English
Summary by Sourcery
Fix super emoji handling so mixed messages remain intact while standalone animated emojis retain full-screen rendering.
Bug Fixes:
Documentation: