Files
Cyrene_v2/internal/services/channelbridge/turnreply.go
T
AskaEth fbbbb4ea64
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(channelbridge): IM 渠道也走「回合必落可见回复」(与网关同口径)
根因(agent-194,文件:行):
- inbound.go 的 deliverReply 对空回复直接 return(静默)⇒ 只剩 user 消息;
- runInboundTurn 报错直接 return ⇒ 失败回合连说明都没有。
修法:抽共享**纯函数**包 internal/runtime/turnreply(从网关逐字搬运、零语义改动)+
channelbridge/turnreply.go 的 turnVisibleReply(与网关 persistTurnReply 同构);
取消边界在 IM 用 context.Canceled / ctx 取消表达(渠道桥无代次概念,如实登记差异)。
测试 6 条:空回复/纯空白/轮次用尽/安全网/回合报错/取消不落库/正常逐字不变。
变异:还原旧静默行为 ⇒ 4 条红(含「出站 0 条,want 1」与英文内部诊断外泄)。
2026-10-04 22:29:54 +08:00

46 lines
2.2 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 同构:有真实文本就交付真实文本(连被取消的回合也照旧
// 交付 —— 内容是真的,与「不静默」无关);没有可见文本时,只有「未被取消」才补
// 一条按结束原因区分的诚实说明。
//
// 真实文本按渠道既有行为去掉首尾空白后交付(内容一字不改),与加入本保证之前
// 的行为完全一致。
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(text), true
}
if errors.Is(runErr, context.Canceled) || ctx.Err() != nil {
return "", false
}
return text, true
}