fix(turnreply): 产出侧不再产出未配对的段落标签(安卓报障的回执)
安卓客户端登记(他们文档 02 末节):一条助手消息的 content 是
`<p><p>正文</p>`,按他们的解析契约(非贪婪)必然把 `<p>正文` 当内容 ⇒ 用户
直接看到字面 `<p>`。任何按该契约定实现的消费方都会踩。
根因(两半都证实,都在同一处):
- (a) 规整器**原样放过**模型吐出的 `<p><p>X</p>`:段块正则非贪婪,把它当成
一个"已完整包裹"的块逐字保留(旧的覆盖检查用同一个正则 ⇒ 护栏本身是盲的);
- (b) 规整器**亲手制造**该形状:末段漏 `</p>` 时无条件套一层 ⇒ `<p><p>X</p>`。
实证:模型吐 `<p>那现在…?`(无闭标签)经旧代码规整后与上报串**逐字节相同**;
- (c) 顺带修一个幂等漏洞:落单 `</p>` 旧行为会越跑越多(`<p></p></p>`)。
修法(新增 paragraph_repair.go:配对修复 + 接线):
`<p><p>X</p>`⇒`<p>X</p>`;`<p>X`⇒补 `</p>`;`<p>正文<p>`⇒合并;`<p>A</p><p>`⇒
丢空开标签;`<p></p>`⇒整对丢;落单 `</p>`⇒丢标签正文不动。不吞正文护栏:
`<p`(无 `>`)/`<p2>`/`1 < 2`/`a<b>`/代码围栏/`<action>` 内 `<p>`/自闭合
`<p/>` 一律原样(每条都有用例)。`normalizePiece` 改为"补闭不补开"——在造出
畸形的那一点上根治。
验收(主会话复跑 + 亲自变异):go build ./... + vet 绿;turnreply /
gateway 全量 / channelbridge 三包绿;我用 -overlay 摘掉配对修复接线 ⇒
TestNormalizeParagraphs_* 变红。生产库只读核查:177 行 assistant 里**仅 1 行**
畸形(各消费方已容错)⇒ 不做一次性修复脚本。
⚠️ 未部署:跑着的 :10942 仍是旧产物 ⇒ 畸形仍会产出,安卓端的容错请继续保留。
This commit is contained in:
1 parent
378faf212e
commit
8051906afa
4 files changed
+449
-3
No files matched your search
@@ -30,6 +30,9 @@ import (
|
||||
// - `<action>` / `<narration>` 块:**永不跨空行拆开**(被拆开的标签对会让 Web 与
|
||||
// IM 都渲染出野生标签),它们随所在段落一起被包进**同一个** `<p>`。
|
||||
// - `<p>` 块内部是原子的:块内的空行不参与分段,块本身不被二次包裹。
|
||||
// - 收尾必过**标签配对修复**(paragraph_repair.go):落库/对外文本里不允许出现未配对
|
||||
// 的段落标签(`<p><p>X</p>`、落单的开或闭),它们会被按「一个 `<p>` = 一段」合并
|
||||
// 或补齐。该步同样幂等,且只增删标签本身,不吞正文。
|
||||
var (
|
||||
// atomRe 匹配文本里**不可再拆**的原子块:完整的 <p>…</p>(含自闭合 <p/>)与
|
||||
// 完整的 <action>/<narration> 块。它们内部的空行不算段落分隔。
|
||||
@@ -48,7 +51,9 @@ var (
|
||||
)
|
||||
|
||||
// NormalizeParagraphs 返回规整后的助手文本:未被 `<p>` 包裹的段落补上 `<p>…</p>`,
|
||||
// 已完整包裹的段落逐字保留,`<action>`/`<narration>` 段也包进它所在的 `<p>`。
|
||||
// 已完整包裹的段落逐字保留,`<action>`/`<narration>` 段也包进它所在的 `<p>`;最后再
|
||||
// 做一遍**标签配对修复**(未配对的段落标签按「一个 `<p>` = 一段」收干净,见
|
||||
// paragraph_repair.go)。
|
||||
//
|
||||
// 它是纯函数且**幂等**(`NormalizeParagraphs(NormalizeParagraphs(s)) ==
|
||||
// NormalizeParagraphs(s)`),空文本、纯空白、含代码围栏的文本原样返回。
|
||||
@@ -58,7 +63,7 @@ func NormalizeParagraphs(text string) string {
|
||||
}
|
||||
cuts := paragraphCuts(text)
|
||||
if len(cuts) == 0 {
|
||||
return normalizePiece(text)
|
||||
return repairParagraphTags(normalizePiece(text))
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(text) + 3*len(cuts))
|
||||
@@ -68,7 +73,7 @@ func NormalizeParagraphs(text string) string {
|
||||
prev = cut
|
||||
}
|
||||
b.WriteString(normalizePiece(text[prev:]))
|
||||
return b.String()
|
||||
return repairParagraphTags(b.String())
|
||||
}
|
||||
|
||||
// paragraphCuts 返回文本里全部的**分段边界**(切点,升序去重):
|
||||
@@ -128,6 +133,11 @@ func isParagraphBlock(s string) bool {
|
||||
// `<action>`/`<narration>` 块里,必须原样保留)。已是 `<p>` 块或纯空白时逐字返回;
|
||||
// 否则整段包上 `<p>`,这一段自己的首尾空白(它同时是相邻 `<p>` 块之间的分隔)
|
||||
// 原样保留。
|
||||
//
|
||||
// **例外(补闭不补开)**:段首已经是一个段落开标签(模型漏了闭标签,如 `<p>X`)时
|
||||
// **只补 `</p>`、不再套外层 `<p>`** —— 再套一层就造出 `<p><p>X</p>`:客户端的非贪婪
|
||||
// 契约会把 `<p>X` 当成气泡内容,用户直接看到字面的 `<p>`。这一处与 paragraph_repair.go
|
||||
// 的配对修复是同一条根因的两半(详见该文件头)。
|
||||
func normalizePiece(piece string) string {
|
||||
if isParagraphBlock(piece) || strings.TrimSpace(piece) == "" {
|
||||
return piece
|
||||
@@ -138,5 +148,8 @@ func normalizePiece(piece string) string {
|
||||
if strings.TrimSpace(core) == "" {
|
||||
return piece
|
||||
}
|
||||
if startsWithParagraphOpen(core) {
|
||||
return piece[:lead] + core + "</p>" + piece[len(piece)-trail:]
|
||||
}
|
||||
return piece[:lead] + "<p>" + core + "</p>" + piece[len(piece)-trail:]
|
||||
}
|
||||
Reference in new issue
Block a user