Skip to content

缺陷:PAIRED 正则吞掉最多 64 个字符的助手正文 #1720

Closed

Description

@MZXmzx382

Description *

概述

PAIRED 的定义是:

var PAIRED=new RegExp("<"+NAME+"\\s[^<>]{0,256}>([^<>]{0,64})<\\/"+NAME+">");

捕获组 ([^<>]{0,64}) 在上下文代码中被当作标签引用(m00123)来对待,但 [^<>] 是一个能匹配换行的否定字符类。因此它实际匹配的是「最多 64 个非尖括号字符」——包括普通的助手正文——而 stripAcpTags() 会把整个匹配结果删除:

out = out.replace(new RegExp(PAIRED.source,"g"),"");

于是,一个开标签、其后最多 64 个字符的真实回答正文、再跟一个闭标签,会导致正文随标签一起被删除。

这也使 PAIRED 与 wrappedSpan() 产生语义冲突:后者的长度检查(first.length > 64 -> return null)显然是基于「标签跨度很短」这一前提写的。

复现

加载该模块的 stripAcpTags(除 wrappedSpan、LONE_OPEN、LONE_CLOSE、TRUNC_OPEN、TRUNC_CLOSE、MARKER_LINE 和 stripBiliArtifacts 需要打桩外,该函数是自包含的):

// 开闭标签之间夹着 30 字符的正文
stripAcpTags('<acp tokens="9">' + 'Z'.repeat(30) + '</acp>')
// 期望:'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ'
// 实际:''

另外两例,在 0.1.172 / 0.1.173 / 0.1.174 上均可复现:

stripAcpTags('<acp tokens="9" type="text">\n正文段落\n</acp>')
// 期望:'\n正文段落\n'
// 实际:''

// (同一类输入:任何不超过 64 字符的正文体,换行也算在内)

一个真实场景——用户从屏幕上把泄漏的标签粘贴到输入框,紧随其后的文字在回答中消失了:

stripAcpTags('<acp tokens="60" type="text">m00239</acp>这是什么东西')
// 期望:'这是什么东西'
// 实际:''   (当正文恰好位于一对开闭标签之间时,
//              标签和其后的文字会被一并删除)

建议的修复

把该组收窄为真实的标签引用。引用的形式是 m 加至少 4 位数字:

var PAIRED=new RegExp("<"+NAME+"\\s[^<>]{0,256}>(\\s*m\\d{4,}\\s*)<\\/"+NAME+">");

这样仍会剥除所有真实的标签引用,同时保留正文。应用该修复后,上述用例会保留正文,而标签依旧被删除:

输入 修复前 修复后
<acp tokens="9"> + 'Z'*30 + </acp> '' 'ZZZ…Z'(30 字符)
<acp tokens="9" type="text">\n正文段落\n</acp> '' '\n正文段落\n'
<acp tokens="60" type="text">m00239</acp>这是什么东西 '' '这是什么东西'
前面的话 <acp tokens="20" type="text">m00098</acp> 后面的话 标签被删除 标签被删除(行为不变)
这是一段普通正文。 不变 不变

注意事项

  • PAIRED.source 被 createTagEchoFilter() 复用,后者会在滑动缓冲区上调用 PAIRED.exec(buf)。如果采纳该修复,请勿给 PAIRED 加上 g 标志——否则 lastIndex 会跨流式分块累积,导致漏匹配。现有代码正是正确地依赖了 PAIRED 不带全局标志这一点。
  • 与上述修复无关的另一情况:位于消息开头的不成对泄漏标签(<acp tokens="110" string="true"> m00433 然后这里是正文内容)不会被 PAIRED 删除,而是依赖 LONE_OPEN;测试中该路径行为正常。

bili version *

0.1.174

Usage mode *

Other / not sure

Client and version *

dsh 0.2.0 -r2

Operating system *

Windows 11

Upstream and model *

deepseek v4.1

Compression-related config (optional)

Log excerpt (strongly recommended)


/acp or acp_status output (optional)


Activity

  1. ranxianglei commented on Sep 30, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    已修复,PR: #1721

    验证(修复前后对比)

    • 在 0.1.174 上复现确认:两条路径都会吞正文——整文路径经 PAIRED 的 g-replace(issue 里三个例子:30 字符 Z、含换行段落、CJK 文本全部被吞);流式路径(createTagEchoFilter)经 LONE_OPEN→swallow-until-close 状态机同样吞掉。
    • 修复后上述例子全部保留正文、标签照删;真实标签引用(m+≥5 位数字,含前后空白形式)在两条路径上依旧被完整剥除。
    • typecheck / 全量测试(3283 pass / 0 fail)/ build 均绿;diff 触及 src/loop/**,CI e2e 会自动跑。

    修复方案(比 issue 建议略广一层)

    1. 按 issue 建议把 PAIRED 捕获组收窄为 \s*m\d{4,}\s*(内核引用是 m+≥5 位零填充数字,REF_WIDTH = 5,\d{4,} 是安全下界);未加 g 标志,理由同 issue 注意事项。
    2. 但只收窄 PAIRED 不够:流式路径对「带属性的开标签」不走 PAIRED,而是进入 swallow-until-close,在 ~80 字符预算内把闭标签前的一切(含正文)丢弃。因此把闭标签到达时的判定改为按内容形状:引用形 → 整段丢弃(与旧行为一致);非引用形 → 保留正文、只删两个标签片段。仿冒吞咽路径(odd-quote/wrapped-turn,releases=false)行为不变。
    3. 顺手修正三个测试夹具:它们用 m1/m2/ref/m001 这类短非引用占位符夹在配对标签间,过去只靠过宽的组才被剥掉——改为真实的 ≥5 位引用,语义不变。

    行为变更披露(old → new)

    • 旧:配对标签之间任何 ≤64 个非尖括号字符(含换行/CJK/普通正文)会随标签一起删除(整文路径;流式路径 ≤~80 字符)。
    • 新:仅引用形内容(m+≥4 位数字,可带前后空白)随标签删除;其余内容保留,只删两个标签片段。严格减少删除量——没有任何原本保留的输入现在会被删。副作用:m1/m001/ref 这类短非引用文本现在会以纯文本留下而不是被吞——预期行为,内核从不签发 <4 位引用。
    • 无配置面变更(未新增字段/env/CLI 参数);tool-call 参数按设计不受影响(文件顶部不变量块)。

    一句话中文摘要:修复了 PAIRED 正则(及流式路径对应的吞咽状态机)把最多 64 字符助手正文当标签引用随标签删除的问题——PAIRED 收窄为真实引用形状、流式路径在闭标签时按内容形状判定,两条路径均已回归测试钉住,可以合并。

  2. added a commit that references this issue on Sep 30, 2026
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

    bugSomething isn't workingp1

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions