Files
Cyrene_v2/internal/services/channelbridge/transcript.go
T
AskaEth 89715d1813
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels): 受限车道回合补自身会话历史(群聊/陌生人能连贯理解一段对话;复用共享 transcript,拒绝主会话 id)
2026-10-05 12:58:01 +08:00

76 lines
4.6 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"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/transcript"
)
// 本文件让 IM 回合带上**该会话自己**最近的对话历史,使她看得懂一段连贯的对话,而不是只
// 听得到当次那一条消息(主人 2026-10-05 反馈:「只有 @ 才有反应、不 @ 就不理解意图」——
// 此前群里的回合只有「来源系统行 + 当次消息」,前几句她根本没看见)。
//
// 会话环境在 loop.SessionEnv 上早已按车道解析(ActorEnv/MemWritable/PersonaID,
// design/22 §7-3/§7-4),缺的一直是**历史转录**:Web 主会话回合由编排器播种该会话最近 N 条
// 转录(gateway/session_transcript.go),而 IM 回合此前没有。
//
// - **主人私聊**:IM 回合镜像进主会话,因此播种**主会话**(`web`)的转录,与 Web 主会话
// 看到的历史逐条一致 —— 她不会像第一次见到主人(主人 2026-10-05 要求两条车道环境一致)。
// - **群聊 / 陌生人(受限车道)**:播种**该隔离会话自己**(`sess-*`)的最近转录,因此群里
// 前几句、以及被最小打扰静默掉而只登记不回的那几条(light.go 的 registerSilenced 已把它们
// 落进同一会话)都在她的上下文里,她据此才能理解一段连贯对话的意图。
//
// 两条车道读的会话不同,重建逻辑与条数上限却是同一份:internal/runtime/transcript
// (ContextCount / Read / Message)—— 与网关同一份实现、同一个常量,绝不另写第二套
// (两份映射必然漂移)。**受限车道结构上够不到主会话**:目标会话由 inboundTargetFor 结构化
// 决定,受限车道拿到的是 `sess-*` 隔离会话,主人的主会话历史一个字都不外泄
// (design/16 §4.6 §3、design/02 §3)。
// turnEnv 返回一条入站消息本轮运行所用的 loop 会话环境。
//
// - 主人私聊(inboundTarget.private)与 Web 主会话是**同一条车道**,因此经主人车道的
// 唯一构造入口 loop.OwnerEnvFor 派生:两条车道逐字段同源,SessionEnv 将来新增字段时
// 会一起拿到,而不是各自漂移出「同一个她、两套环境」。
// - 受限会话(群聊/陌生人)由结构化解析出的隔离边界决定:记忆只读(MemWritable=false)、
// 并强制默认角色(design/22 §7-4 —— resolvePersona 对受限会话恒返回 "")。身份与权限
// 始终来自 actor 元数据,绝不来自这里的任何字符串(项目红线)。
func turnEnv(sess session.Session, target inboundTarget, personaID string) loop.SessionEnv {
if target.private {
return loop.OwnerEnvFor(personaID)
}
return loop.SessionEnv{ActorEnv: sess.Env(), MemWritable: sess.MemWritable, PersonaID: personaID}
}
// seedTurnTranscript 在本回合模型可见消息之前前置**该会话自己**最近的转录
// (与网关编排器的 startTurnMessages 同序:转录在前、本次消息在后):
//
// - 主人私聊 → 主会话(`web`)的转录,与 Web 主会话逐条一致;
// - 群聊 / 陌生人 → 该隔离会话(`sess-*`)的转录,含被静默登记的群成员发言。
//
// 时刻与网关一致:调用点在本回合用户消息落库**之前**(runInboundTurn 里装配先于
// persistToMain / persistIsolated),因此当次消息绝不会在上下文里出现两次。
//
// 转录读不出来(未接历史存储、读失败、空会话)时原样返回,这正是既有行为:回合退化为
// 「来源行 + 当次消息」,而不是让回合失败。
//
// 受限车道另设一道显式守卫:目标会话是主会话 id 时宁可不播种。这在生产里结构上不可能
// (inboundTargetFor 只在主人私聊时给出 MainSessionID),但上下文隔离是安全红线 —— 万一
// 将来多出别的构造点写错,也绝不能把主人的主会话历史灌进群聊/陌生人上下文
// (design/02 §3、design/16 §4.6 §3)。
func (b *Bridge) seedTurnTranscript(ctx context.Context, target inboundTarget, msgs []model.Message) []model.Message {
if !target.private && target.sessionID == history.MainSessionID {
return msgs
}
seed := transcript.Read(ctx, b.history, target.sessionID)
if len(seed) == 0 {
return msgs
}
out := make([]model.Message, 0, len(seed)+len(msgs))
out = append(out, seed...)
return append(out, msgs...)
}