主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
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
|
||
}
|