Files
Cyrene_v2/internal/services/gateway/session_persist.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

114 lines
4.9 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/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件拥有编排器的聊天历史持久化钩子(design/16
// §3.3):用户消息流在提交时写入,助手回复在
// 回合结算时写入。未接线 history 存储时二者都是静默空操作,
// 因此编排器无存储也能原样运行(测试 / 未接线的网关)。
// modeChat 是回合未携带行为模式时所持久化的模式(design/01 §3,
// design/16 §5.1)。它也是 IM 路径唯一会写入的模式,因为 IM
// 只携带聊天文本。
const modeChat = "chat"
// modeOrDefault 返回 mode,调用方未提供时返回 modeChat,使
// 持久化消息绝不携带空的行为模式(design/16 §5.1)。
func modeOrDefault(mode string) string {
if mode == "" {
return modeChat
}
return mode
}
// messageModeContextKey 是携带回合行为模式的 context 键
// (design/01 §3、design/16 §5.1),从 HTTP/WS 边界引入编排器的
// 持久化,使每条持久化消息被它发送时的模式标注。
type messageModeContextKey struct{}
// withMode 返回携带 mode 的 context,使编排器能标注
// 持久化的用户消息与助手回复。空 mode 是空操作
// (持久化层回退到 modeChat)。
func withMode(ctx context.Context, mode string) context.Context {
return context.WithValue(ctx, messageModeContextKey{}, mode)
}
// modeFrom 返回 ctx 携带的行为模式,未设置时返回 ""。
func modeFrom(ctx context.Context) string {
v, _ := ctx.Value(messageModeContextKey{}).(string)
return v
}
// persistUserMessages 把一次已提交回合中每条 RoleUser 消息写入
// 历史存储,使原始用户消息流在回合结算前就持久,随后
// 通知传输层,使同一账号的每个浏览器都能追加它
// 而无需手动刷新(design/16 §3.3、§4.2)。deviceID 用发送客户端标注
// 消息(design/16 §4.5),mode 是该回合发送时的行为模式
// (design/16 §5.1),为空时回退到 modeChat,而 personaID 是
// 该回合说话时的角色(design/22 §7-5)—— 即当时的角色,因此
// 之后切换角色的会话仍保有准确的「谁说了这个」流。未接线
// history 存储时它是空操作。被拒绝的回合(空、待处理已满)
// 绝不触达它,因此不会持久化任何空内容。
func (o *Orchestrator) persistUserMessages(ctx context.Context, sessionID string, messages []model.Message, deviceID, mode, personaID string) {
if o.history == nil {
return
}
for _, m := range messages {
if m.Role != model.RoleUser {
continue
}
text, image := messageTextImage(m)
if text == "" && image == "" {
continue
}
full, err := o.history.AppendFull(ctx, history.Message{SessionID: sessionID, Role: history.RoleUser, Content: text, Image: image, DeviceID: deviceID, Mode: modeOrDefault(mode), PersonaID: personaID})
if err != nil {
o.log.Error("orchestrator persist user", "err", err, "session", sessionID, "device_id", deviceID)
continue
}
o.notifyPersisted(full)
}
}
// persistAssistantReply 把已结算回合的最终助手文本写入
// 历史存储,然后通知传输层(design/16 §3.3、§4.2)。deviceID 用
// 发送该回合的客户端标注回复(design/16 §4.5),mode 是
// 该回合的行为模式(design/16 §5.1),为空时回退到 modeChat,
// 而 personaID 是产出该回复的角色(design/22 §7-5)。Mood
// 是该回合中人格的 mood(design/01 §2 情绪状态机),取自 loop
// 结果,因此已存/DTO/`message_persisted` 的 mood 是真实数据,而不是
// 永远为空的列;不跟踪 mood 的回合存一个空 mood。它
// 在未接线 history 存储,或该回合没有产出文本
// 时是空操作(被中断/出错的回合保持 res 为 nil)。该写入在一个全新的
// 后台 context 下运行,因此被取消的回合 context 绝不可能中断它。
func (o *Orchestrator) persistAssistantReply(sessionID string, res *loop.Result, deviceID, mode, personaID string) {
if o.history == nil || res == nil || res.Text == "" {
return
}
full, err := o.history.AppendFull(context.Background(), history.Message{SessionID: sessionID, Role: history.RoleAssistant, Content: res.Text, Mood: res.Mood, DeviceID: deviceID, Mode: modeOrDefault(mode), PersonaID: personaID})
if err != nil {
o.log.Error("orchestrator persist assistant", "err", err, "session", sessionID, "device_id", deviceID)
return
}
o.notifyPersisted(full)
}
// messageTextImage 返回消息内容部分拼接后的文本与首个图片
// URL,供持久化进 chat_messages 各列。
func messageTextImage(m model.Message) (text, image string) {
for _, c := range m.Content {
if c.Type == model.ContentTypeText {
text += c.Text
} else if c.Type == model.ContentTypeImage && image == "" {
image = c.ImageURL
}
}
return text, image
}