Repository navigation
缺陷:PAIRED 正则吞掉最多 64 个字符的助手正文 #1720
Copy link
Copy link
Closed
Labels
Description
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)
Reactions are currently unavailable
Activity
ranxianglei commentedon Sep 30, 2026
已修复,PR: #1721
验证(修复前后对比)
PAIRED的 g-replace(issue 里三个例子:30 字符 Z、含换行段落、CJK 文本全部被吞);流式路径(createTagEchoFilter)经 LONE_OPEN→swallow-until-close 状态机同样吞掉。src/loop/**,CI e2e 会自动跑。修复方案(比 issue 建议略广一层)
PAIRED捕获组收窄为\s*m\d{4,}\s*(内核引用是 m+≥5 位零填充数字,REF_WIDTH = 5,\d{4,}是安全下界);未加g标志,理由同 issue 注意事项。PAIRED不够:流式路径对「带属性的开标签」不走PAIRED,而是进入 swallow-until-close,在 ~80 字符预算内把闭标签前的一切(含正文)丢弃。因此把闭标签到达时的判定改为按内容形状:引用形 → 整段丢弃(与旧行为一致);非引用形 → 保留正文、只删两个标签片段。仿冒吞咽路径(odd-quote/wrapped-turn,releases=false)行为不变。m1/m2/ref/m001这类短非引用占位符夹在配对标签间,过去只靠过宽的组才被剥掉——改为真实的 ≥5 位引用,语义不变。行为变更披露(old → new)
m1/m001/ref这类短非引用文本现在会以纯文本留下而不是被吞——预期行为,内核从不签发 <4 位引用。一句话中文摘要:修复了 PAIRED 正则(及流式路径对应的吞咽状态机)把最多 64 字符助手正文当标签引用随标签删除的问题——PAIRED 收窄为真实引用形状、流式路径在闭标签时按内容形状判定,两条路径均已回归测试钉住,可以合并。
Merge pull request #1721 from ranxianglei/2026-09-30_paired-prose-swa…
fix: keep prose between paired render tags in tag-echo filter (ranxia…
verif: tag-echo-filter replay judge — finds real bugs ranxianglei#4106 …
fix(loop): tag-echo bare-open swallow re-adjudicates on a nested open…
fix: close the remaining #4107 chunk-split shapes and flip its pins (#…
fix(loop): tag-echo bare-open swallow re-adjudicates on a nested open…
fix: close the remaining ranxianglei#4107 chunk-split shapes and flip…