- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
109 lines
4.9 KiB
Go
109 lines
4.9 KiB
Go
package channelbridge
|
||
|
||
import (
|
||
"context"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||
)
|
||
|
||
// 本文件承载 channelbridge 的聊天历史持久化钩子(design/16
|
||
// §4.4):主人的私聊被路由进唯一的 Web 主会话并
|
||
// 打上渠道平台标签,随后广播出去,使 Web 客户端无需刷新
|
||
// 即可镜像。它与 channelbridge.go 分开,好让桥接器的入站
|
||
// 流水线文件保持在模块体积上限之内。
|
||
|
||
// modeChat 是每条 IM 持久化消息写入的行为模式:IM 只承载
|
||
// 聊天文本(design/02 §1 单向回流),因此那里永远不会出现其他模式。
|
||
const modeChat = "chat"
|
||
|
||
// SetOnPersisted 装配(或替换)主人私聊消息持久化进 Web 主会话
|
||
// 之后所调用的传输钩子。组合根中 gateway 在桥接器
|
||
// 之后构建,因此该钩子在 New 之后通过此 setter 挂上;
|
||
// 它可以安全空调用,也可与在途处理
|
||
// 安全并发。
|
||
func (b *Bridge) SetOnPersisted(fn func(history.Message)) {
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
b.onPersisted = fn
|
||
}
|
||
|
||
// SetOnRejected 装配(或替换)消息被允许/拒绝黑白名单
|
||
// 丢弃后所调用的钩子(design/16 §4.6 §6)。组合根
|
||
// 把它接进 gateway 审计轨迹。它可以安全空调用,也可与
|
||
// 在途处理安全并发。
|
||
func (b *Bridge) SetOnRejected(fn func(adapter.UnifiedMessage, string)) {
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
b.onRejected = fn
|
||
}
|
||
|
||
// notifyPersisted 在装配了传输钩子时,把刚持久化的消息交给它,
|
||
// 使 gateway 能广播 `message_persisted`(Web 聊天)或
|
||
// `channel_persisted`(IM 适配器日志)事件,实现多端实时同步。
|
||
func (b *Bridge) notifyPersisted(msg history.Message) {
|
||
b.mu.RLock()
|
||
fn := b.onPersisted
|
||
b.mu.RUnlock()
|
||
if fn != nil {
|
||
fn(msg)
|
||
}
|
||
}
|
||
|
||
// persistToMain 把一条消息写入 Web 主会话,并显式带上来源平台
|
||
// 与客户端 deviceID,随后通知传输钩子,让 Web 客户端无需刷新即可收到
|
||
// `message_persisted` 事件(design/16 §4.4)。deviceID 是
|
||
// 发送该消息的具体客户端(design/16 §4.5)——例如 IM 的 "weixin-ilink:conf0:wx_uid"。
|
||
// adapterID 是接收该消息的账号适配器注册名
|
||
// (如 "kook:3bef..."),便于管理日志按适配器查询收/发流;
|
||
// Web 来源的消息该字段为空。mood 是该轮角色的心情
|
||
// (design/01 §2 情绪状态机),只挂在助手回复上(design/16 §3.3:
|
||
// 助手消息承载 mode/mood);用户消息与未跟踪的心情传 ""。
|
||
// 未装配历史存储时为空操作,因此桥接器在没有
|
||
// 存储的情况下行为不变。
|
||
func (b *Bridge) persistToMain(ctx context.Context, role, content, mood, platform, deviceID, adapterID string) {
|
||
if b.history == nil {
|
||
return
|
||
}
|
||
full, err := b.history.AppendFull(ctx, history.Message{
|
||
SessionID: history.MainSessionID, Role: role, Content: content, Mood: mood, Platform: platform, DeviceID: deviceID, AdapterID: adapterID, Mode: modeChat,
|
||
})
|
||
if err != nil {
|
||
b.log.Warn("persist main session", "err", err, "role", role, "platform", platform, "device_id", deviceID, "adapter", adapterID)
|
||
return
|
||
}
|
||
b.notifyPersisted(full)
|
||
}
|
||
|
||
// persistIsolated 把一条消息写入受限(群聊/陌生人)会话
|
||
// 自有的平台日志(design/16 §4.6 §4):消息落在隔离的 sess-xxx
|
||
// 会话(不是 Web 主会话)中,并带上渠道平台、客户端
|
||
// deviceID、发送者显示名与账号适配器注册名,便于
|
||
// IM 适配器日志页按适配器配置渲染非主人消息流。
|
||
// 该消息仍不会镜像进 Web 主会话(只有主人私聊
|
||
// 消息才镜像到 Web);它只交给传输钩子,让
|
||
// gateway 广播 `channel_persisted` 事件、IM 适配器日志页无需刷新
|
||
// 即可追加。未装配历史存储时为空操作,因此桥接器在
|
||
// 没有存储的情况下行为不变。
|
||
func (b *Bridge) persistIsolated(ctx context.Context, role, content, deviceID string, sess session.Session, platform, senderName, adapterID string) {
|
||
if b.history == nil {
|
||
return
|
||
}
|
||
full, err := b.history.AppendFull(ctx, history.Message{
|
||
SessionID: sess.SessionID, Role: role, Content: content, Platform: platform,
|
||
DeviceID: deviceID, SenderName: senderName, AdapterID: adapterID, Mode: modeChat,
|
||
})
|
||
if err != nil {
|
||
b.log.Warn("persist isolated session", "err", err, "role", role,
|
||
"session", sess.SessionID, "platform", platform, "sender_name", senderName, "adapter", adapterID)
|
||
return
|
||
}
|
||
// 把隔离写入作为 `channel_persisted` 事件广播,让 IM 适配器
|
||
// 日志页实时追加。gateway 按会话区分事件类型:
|
||
// 这不是 Web 主会话,因此 Web 聊天没有它的处理器,
|
||
// 消息本身仍只落在隔离的 sess-xxx 会话(绝不在
|
||
// 主/`web` 会话,design/16 §4.6 §3)。
|
||
b.notifyPersisted(full)
|
||
}
|