package channelbridge import ( "context" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" ) // 本文件掌管一轮入站 IM 对话以哪个角色说话(DevDocs/design/22 §2 // 裁决规则 + §7-3 + §7-4)。身份与权限仍是结构性的——actor // 元数据与会话的隔离边界决定谁可以做什么——而 // 角色只界定提示词、记忆命名空间、情绪机器与 // 思考预算的范围。 // // 优先顺序,自上而下(design/22 §2): // 1. 会话级绑定——对主人私聊而言即消息所镜像进的 Web 主 // 会话,因此 IM 镜像用 Web 客户端正在对话的同一 // 角色作答(design/22 §7-3); // 2. 渠道级绑定——账号在 channels.json 中的 personaId,用于 // 主会话自身未绑定角色时(design/22 §2 方案 b); // 3. 全局默认——以 "" 表示,循环会把它解析为 // 自己配置的默认角色。 // // 受限会话(群聊或陌生人,即每个隔离的 sess-xxx // 会话)被刻意排除在 1 和 2 之外:它被强制使用默认 // 角色(design/22 §7-4 的保守选择),因此渠道绑定绝不会 // 把第三方的会话放进某个角色的记忆命名空间。 // sessionPersonaReader 是历史存储的可选窄接口视图,用于 // 读取会话绑定的角色。*history.Store 实现了它;未装配它 // (或所用存储早于 design/22)的桥接器解析不出会话 // 绑定,即全局默认。 type sessionPersonaReader interface { SessionPersona(ctx context.Context, sessionID string) (string, error) } // resolvePersona 返回一轮入站对话所用的角色 id。target 指明 // 该轮对话所在之处(镜像的主会话还是隔离 // 会话),sess 携带结构化解析出的隔离边界,account // 是接收方适配器的注册名(platform:confId),用于 // 渠道级查询。 func (b *Bridge) resolvePersona(ctx context.Context, sess session.Session, target inboundTarget, account string) string { // design/22 §7-4:群聊/陌生人会话绝不扮演绑定角色。 if !target.private { return "" } // design/22 §7-3:主人私聊镜像进主会话,因此它 // 以该会话绑定的角色作答。 if pid := b.mainSessionPersona(ctx); pid != "" { return pid } // design/22 §2 方案 b:账号的渠道级角色,仅当主 // 会话自身未绑定角色时使用。 return b.personaOfAdapter(account) } // mainSessionPersona 返回 Web 主会话绑定的角色;会话继承 // 全局默认时返回 ""。读取器缺失或读取出错同样 // 返回 "":聊天绝不能因元数据查询而被阻塞,而回退 // 就是原有的默认角色。 func (b *Bridge) mainSessionPersona(ctx context.Context) string { if b.history == nil { return "" } reader, ok := b.history.(sessionPersonaReader) if !ok { return "" } pid, err := reader.SessionPersona(ctx, history.MainSessionID) if err != nil { return "" } return strings.TrimSpace(pid) } // personaOfAdapter 返回某个适配器注册名已缓存的渠道级角色, // 未绑定(或连接时未解析出)时 // 返回 ""。 func (b *Bridge) personaOfAdapter(account string) string { b.mu.RLock() defer b.mu.RUnlock() return b.adapterPersona[account] } // rememberAdapterPersona 解析并缓存刚连接的适配器的渠道级 // 角色。在连接/重载时解析(而不是按消息解析) // 让入站热路径无需读配置,同时面板保存仍能 // 立即生效:它写 channels.json 并调用 Reload,后者重建并 // 重连该平台的适配器,刷新此缓存。 func (b *Bridge) rememberAdapterPersona(account string) { if b.channelPersona == nil { return } platform, accountID, _ := strings.Cut(account, ":") pid := strings.TrimSpace(b.channelPersona(platform, accountID)) b.mu.Lock() defer b.mu.Unlock() if pid == "" { delete(b.adapterPersona, account) return } b.adapterPersona[account] = pid } // forgetAdapterPersona 在账号被重载移除(或更名)时丢弃该适配器 // 缓存的渠道角色,使已移除的账号绝不会继续 // 以不再配置的角色作答。 func (b *Bridge) forgetAdapterPersona(account string) { b.mu.Lock() defer b.mu.Unlock() delete(b.adapterPersona, account) }