Files
Cyrene_v2/internal/services/gateway/session_transcript.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

130 lines
5.1 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 gateway
import (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件拥有从持久化转录重建一个回合对话上下文的工作
// (design/15 §2.2/§4「上下文有界:只保留最近 N 条上下文」、design/02
// §1 主会话承载完整上下文、design/22 §7-5)。一个回合此前只从刚
// 发送的消息开始,使模型完全看不到它正在延续的
// 对话;更早回合的助手话语也没有记录
// 是谁说的,因此切换进会话的角色会把另一个
// 角色的话读成自己的。
//
// 播种修好了这两半:新回合携带该会话最近的
// 持久化消息,且每条助手消息都被说话的角色标注
// (model.Message.SenderName),使 loop 的上下文组装能归属它
// (loop/attribution.go)。消息上只走标签 —— 没有任何消息体
// 被改写。
//
// 范围说明:被播种的窗口就是该对话,因此 loop 的逐步
// 后台记忆抽取现在读的是这些近期消息,而不是
// 单条刚发送的消息。抽取出的条目仍归到该回合的角色
// 名下(loop.persistMemories,design/22 §4.2),且同一内容的重复抽取
// 会被抽取器去重,但一个在另一角色活跃时首次说出的事实,
// 若当时从未被抽取,现在就可能被当前角色拾取。
// 把抽取输入收窄为当前回合的消息是一个
// 独立的决定,留作后续跟进。
// transcriptContextCount 限定一个新回合带入模型上下文的持久化
// 消息数。它与编排器的续跑尾部上限
// (defaultMaxTail)一致,使播种回合与续跑回合保持同样大小;
// 超出之后仍由 loop 自身的上下文预算治理压缩。
const transcriptContextCount = 20
// transcriptReader 是播种所需的窄历史视图。*history.Store
// 满足它;未满足的存储(测试替身,或未接持久化
// 构建的网关)只是得不到播种上下文,因此编排器
// 完全照旧继续运行。
type transcriptReader interface {
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
}
// startTurnMessages 返回新回合运行其上的对话:该会话
// 已播种的转录,后跟客户端刚发送的消息。转录
// 不可用时,调用方的消息原样返回,这正是
// 既有行为。
func (o *Orchestrator) startTurnMessages(ctx context.Context, sessionID string, messages []model.Message) []model.Message {
seed := o.sessionTranscript(ctx, sessionID)
if len(seed) == 0 {
return messages
}
return concatMessages(seed, messages...)
}
// sessionTranscript 读取该会话最近的持久化消息并把它们
// 映射为模型消息(最旧优先)。nil/不可读的存储或读错误
// 都得到 nil,因此聊天绝不被元数据查找阻塞;没有
// 历史的会话也什么都不产出 —— 二者都退化为既有的
// 「只有新消息」上下文。
func (o *Orchestrator) sessionTranscript(ctx context.Context, sessionID string) []model.Message {
if o.history == nil || sessionID == "" {
return nil
}
reader, ok := o.history.(transcriptReader)
if !ok {
return nil
}
recent, err := reader.Recent(ctx, sessionID, transcriptContextCount)
if err != nil {
o.log.Debug("turn transcript read", "err", err, "session", sessionID)
return nil
}
out := make([]model.Message, 0, len(recent))
for _, h := range recent {
out = append(out, transcriptMessage(h))
}
return out
}
// transcriptMessage 把一条持久化聊天消息映射为回合延续所依据的
// 模型消息。助手消息携带说话的角色(该回合已存的
// persona_id)作为其说话者标签,因此即使会话被切换过,
// 上下文也能把它归属到那个角色;非助手消息保持其
// 已存发送者标签(design/16 §4.6 的第三方显示名),绝不
// 被重新标为说话角色。
func transcriptMessage(h history.Message) model.Message {
role := historyRoleToModel(h.Role)
msg := model.Message{
Role: role,
Content: []model.Content{{Type: model.ContentTypeText, Text: h.Content}},
}
if h.Image != "" {
msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image})
}
if role == model.RoleAssistant {
msg.SenderName = h.PersonaID
return msg
}
msg.SenderName = h.SenderName
return msg
}
// historyRoleToModel 把已存聊天角色映射为模型的角色词汇,
// 未知角色默认归到用户通道。
func historyRoleToModel(role string) model.Role {
switch role {
case history.RoleAssistant:
return model.RoleAssistant
case history.RoleSystem:
return model.RoleSystem
default:
return model.RoleUser
}
}
// assistantTailMessage 为一个已结算回合的回复构建续跑尾部
// 条目,并用说话的角色标注它(design/22 §7-5):该尾部是
// 播种转录的内存孪生,因此之后切换进来的角色必须能
// 在紧接着的那个排队回合上,把更早角色的回复与自己的话
// 区分开。
func assistantTailMessage(text, personaID string) model.Message {
msg := model.TextMessage(model.RoleAssistant, text)
msg.SenderName = personaID
return msg
}