Skip to content

[Bug] 行内三连反引号误触发代码围栏,吞掉后续内容(v1.12.2 已反编译定位根因) #1333

Open

Description

@CharlesWu0810

环境

  • Operit AI 1.12.2(versionCode 51,targetSdk 34)
  • 设备:vivo V2520A(iQOO Neo11),Android 16

复现步骤

  1. 发送一行文本,句中包含 3 个连续半角反引号(U+0060 ×3),且不做闭合。示例(代码块内为示例句的字面内容):
停止条件比如 ``` 收尾、工具调用的结束标记。
  1. 观察渲染结果。

实际现象

  • 代码块从该行中间开启;同一行剩余文字被当作语言标识,显示为代码块标题。
  • 从该处起直到出现下一个三连反引号或消息结束,全部内容被吞进代码块。
  • 一条消息内出现两个未配对的三连反引号时,围栏角色整体错位:本应开启块的位置被当成闭合,末尾留下未闭合块,吞掉消息尾部。

期望行为(对齐 CommonMark)

  • 只有位于行首(最多 3 个空格缩进)的连续 3 个及以上反引号才能开启围栏。
  • 行内出现的三连反引号应作为普通文本。

代码定位(反编译自 v1.12.2 dex)

1. com.ai.assistance.operit.util.WaifuMessageProcessor

静态正则(smali .line 27 / .line 28):

FENCED_CODE_BLOCK_REGEX          = ```[^\r\n`]*[\r\n]?[\s\S]*?```
UNCLOSED_FENCED_CODE_BLOCK_REGEX = ```[^\r\n`]*[\r\n]?[\s\S]*$

两条正则均以裸的三连反引号开头,无行首锚点、无多行模式,因此任意位置的三连反引号都会被匹配为围栏起始。

另有一条用于剥离围栏标记的正则(.line 800):三连反引号 + [a-zA-Z]* + 可选换行,或可选换行 + 三连反引号。

2. com.ai.assistance.operit.util.stream.plugins.StreamMarkdownFencedCodeBlockPlugin

流式渲染的围栏检测器,基于自研 KMP 扫描器(StreamKmpGraph,字段 startMatcher / endMatcher,状态 hasStartedMatchingFence / isMatchingEndFence)。模式由 repeat(0x60)(3 个反引号)与 greedyStar 组合构造,与上述正则同构,同样缺少行首/换行约束。

3. 渲染侧(本身逻辑正确,但会放大上游错误)

  • com.ai.assistance.operit.ui.common.markdown.CanvasMarkdownNodeRendererKt(约 590 行):对块首行 trim 后 startsWith 三连反引号,判定语言标识。
  • com.ai.assistance.operit.ui.common.markdown.MarkdownPlainTextRendererKt(约 101 行):同一逻辑。
    由于传入的块文本从行内三连反引号处开始,其首行即「行内剩余文字」,被 trim 后成了语言标识并显示为标题。

建议修复

  1. 正则加行首约束(多行模式 + 行首最多 3 个空白);未闭合分支同样加 (?m)^ 前缀。
  2. 流式插件 startMatcher 要求三连反引号之前处于行首(或前一字符是换行)。
  3. 围栏未闭合时按普通文本降级,不吞剩余消息(可复用 UNCLOSED_FENCED_CODE_BLOCK_REGEX)。
  4. 附带问题:当前实现不解码 HTML 数字字符实体(如 ` 原样显示五个字符),与 CommonMark 不同。
  5. 附带观察:波浪线围栏(3 个 ~)在整个 APK 的 dex 中无实现,不会误触发,但也不支持该语法。

附加信息

  • 该 APK 未使用 Markwon 或 commonmark-java(dex 中不存在 io.noties.markwon / org.commonmark 相关类),Markdown 解析为自研实现。
  • 该现象在本机 100% 稳定复现(每次发送含未闭合三连反引号的句子)。

备注

本 issue 由 AI 辅助分析生成(现象为用户实测复现,代码定位由 AI 反编译 v1.12.2 得出),如有错误,敬请谅解。

Activity

  1. 3316891527 commented on Oct 4, 2026

    @3316891527
    Contributor

    感谢反馈,以及详细的复现步骤和代码定位。我们核对了当前 dev 的源码,确认这是代码围栏识别边界的问题:Kotlin 和 C++ 流式解析器在匹配开始围栏时缺少行首约束,Waifu 文本清理正则也存在同样的问题,因此行内三连反引号会被误判为代码块,影响后续正文显示或清理结果。

    我们已在 #1334 提交修复,统一这三处的围栏边界:开始围栏仅允许位于行首或最多三个前导空格之后;结束围栏需要满足独立行、反引号长度及尾部空白要求。同时保留合法未闭合代码块持续到消息末尾的行为,这符合 CommonMark,也适用于流式输出过程中结束围栏尚未到达的情况。

    本次新增了 11 个 JVM 回归用例,Android Tests 和 Android Build 已派发,结果尚待验证,修复目前还未合并发布。数字字符实体和波浪线围栏属于另外的 Markdown 兼容性问题,本次修复集中处理行内反引号误触发。

    感谢你的细致反馈,也感谢理解。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions