fix(gateway,channelbridge,turnreply): p 标签半包写入侧规整(修「整条塌成一段」)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

裁定(已发安卓):**能修**,走**写入侧规整** —— 不指望模型输出、不动客户端。
- 新增 internal/runtime/turnreply/paragraph.go 的 NormalizeParagraphs:
  把未被 <p> 包裹的段落补上 <p>(按空行分段;<action>/<narration> 段也包进 <p>,
  与正确形态 <p><action>…</action>…</p> 一致)
- **幂等**守门断言:已完全包裹的文本逐字不变(防二次加工)
- 落点覆盖 Web 与 IM 两条落库路径(gateway/session_turn_reply.go、channelbridge/turnreply.go、
  proactive.go)⇒ 两边共用同一份规整文本 ⇒ **历史一次性变好、两个客户端零改动**、渲染口径不分叉
(测试与变异详见切片报告)
This commit is contained in:
AskaEth committed 2026-10-05 10:56:39 +08:00
1 parent d532eb3fcd
commit 5de708a560
17 files changed
+721 -46

No files matched your search

+10 -4
View File
@@ -7,6 +7,7 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/turnreply"
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
)
@@ -54,6 +55,10 @@ func (b *Bridge) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
out := msg
out.Platform = target.platform
out.ChannelType = adapter.ChannelDirect
// 主动推送的文本与回合回复走**同一份**段落包裹规整(同一函数、同一口径):
// 半包 <p> 的推送在 Web 历史里同样会塌成一整段,在 IM 侧同样只会得到
// 一个气泡(splitBubbles 只认完整 <p> 块)。
out.Content = turnreply.NormalizeParagraphs(msg.Content)
// 主人的账号 id 在每个适配器上都作私聊对端:telegram/qq 读
// ChannelID,kook/weixin 读 SenderID(ChannelID 作回退)。两者
// 都设置,无论适配器偏好哪个字段,私聊消息都可投达,
@@ -63,17 +68,18 @@ func (b *Bridge) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
// 推送经共享的 IM 气泡派发器出口,因此多段主动消息
// 会被拆成气泡、角色扮演/旁白被剥离,并采用
// 与回复相同的 0.5-2s 间隔——WeChat 主动推送不再作为
// 一整条未拆分消息发送。
// 一整条未拆分消息发送。切分吃的是**规整后**的同一份文本。
if err := b.sendIMBubbles(ctx, out, func(ctx context.Context, msg adapter.UnifiedMessage) error {
return ad.SendProactive(ctx, msg)
}, msg.Content); err != nil {
}, out.Content); err != nil {
return fmt.Errorf("channelbridge: send proactive via %q: %w", target.platform, err)
}
// 把主动消息镜像进主对话上下文(design/16
// §4.4 + §4.6.1)。否则模型看不到自己主动说了什么,
// 主人后续回复就会显得不连贯(该消息「丢失」了历史)。
// 它与主人私聊 IM 回复走同一条镜像路径,作用域为 MainSessionID。
b.persistToMain(ctx, history.RoleAssistant, msg.Content, "", "", target.platform,
// 它与主人私聊 IM 回复走同一条镜像路径,作用域为 MainSessionID,
// 落库的同样是规整后的那一份(Web 与 IM 看到的必须是同一份)。
b.persistToMain(ctx, history.RoleAssistant, out.Content, "", "", target.platform,
fmt.Sprintf("%s:%s:%s", target.platform, target.accountID, target.accountID),
target.platform)
return nil