Files
Cyrene_v2/internal/services/channelbridge/turnreply.go
T
AskaEth 5de708a560
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(gateway,channelbridge,turnreply): p 标签半包写入侧规整(修「整条塌成一段」)
裁定(已发安卓):**能修**,走**写入侧规整** —— 不指望模型输出、不动客户端。
- 新增 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)⇒ 两边共用同一份规整文本 ⇒ **历史一次性变好、两个客户端零改动**、渲染口径不分叉
(测试与变异详见切片报告)
2026-10-05 10:56:39 +08:00

50 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package channelbridge
import (
"context"
"errors"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/turnreply"
)
// 本文件承载 IM 回合的「可见回复」判定(切片 KB1 的渠道侧一半)。
//
// 缺口形状:网关侧已保证「归本代次、且不是被用户打断的回合,历史里一定有一条
// 可见的 assistant 消息」(gateway/session_turn_reply.go),但 IM 走的是**另一条
// 路** —— 渠道桥自己驱动 loop 并自行落库,过去的 deliverReply 遇到空文本直接
// return,于是模型给空回复(只有思维链、没有正文)时,IM 侧就是「她没声了」:
// 既没有出站消息,也没有任何说明。轮次用尽、安全网终止、回合内部报错这三条出口
// 同样静默。
//
// 文案与判定口径与网关**逐字一致**(同一份文案来自共享包
// internal/runtime/turnreply),但「该不该交付」的边界由渠道桥自己的编排状态决定:
// IM 没有网关那套会话代次(interrupt / 续跑队列是 Web 会话的概念),这里的取消
// 判据是**处理上下文被取消** —— 桥接器 Stop 会取消 b.ctx,在途回合随之取消,
// 用户意图就是「停下」,此时补一句「我出问题了」反而违背那条指令。
// turnVisibleReply 返回本回合应当交付给渠道的可见回复:reply 为要交付(并落库)
// 的文本,ok=false 表示这个回合**刻意什么都不交付**(被取消)。
//
// 与网关 persistTurnReply 同构:有真实文本就交付真实文本(连被取消的回合也照旧
// 交付 —— 内容是真的,与「不静默」无关);没有可见文本时,只有「未被取消」才补
// 一条按结束原因区分的诚实说明。
//
// 真实文本按渠道既有行为去掉首尾空白后交付(内容一字不改),与加入本保证之前
// 的行为完全一致 —— **唯一例外**是与网关同一次(同一份)的段落包裹规整
// (turnreply.NormalizeParagraphs):它给未被 `<p>` 包裹的段落补上包裹,
// 于是**落库文本**(主人私聊会镜像进 Web 主会话)与**出站气泡切分**
// (splitBubbles 按 `<p>` 切)吃的是同一份文本,客户端那侧「整条塌成一段」
// 的同一条判据在 IM 侧不会再出现第二个答案。
func turnVisibleReply(ctx context.Context, res *loop.Result, runErr error) (string, bool) {
text := turnreply.Text(res)
if res != nil && res.Text == text {
return strings.TrimSpace(turnreply.NormalizeParagraphs(res.Text)), true
}
if errors.Is(runErr, context.Canceled) || ctx.Err() != nil {
return "", false
}
return text, true
}