裁定(已发安卓):**能修**,走**写入侧规整** —— 不指望模型输出、不动客户端。 - 新增 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)⇒ 两边共用同一份规整文本 ⇒ **历史一次性变好、两个客户端零改动**、渲染口径不分叉 (测试与变异详见切片报告)
50 lines
2.7 KiB
Go
50 lines
2.7 KiB
Go
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
|
||
}
|