package session // 本文件把「消息 → 会话」的键派生**导出**给别的层复用(M22 复审 P1-1)。 // // 为什么必须导出:会话的键一直是会话层自己算的(`channelID` + `platform \x00 channelID`), // 而渠道桥接器的**每会话回合车道**需要「什么算同一个会话」这个判据与它**同源**。 // 各算各的会漂移,而且漂移的后果不是排序不好看,是**同一个 session 两条车道、两个回合 // 并发**——两条 run 共享 sessionID,checkpoint 行 id `{sessionID}:{step}` 互相覆盖、 // `loadCheckpoint` 可能续到另一条在途 run 的快照而重放工具调用,flow 桶 // `p.graphs[sessionID]` 被先收尾者删掉(loop 没有会话级互斥,IM 侧也没有网关那层 // `session_turn_fence` 兜底)。 // // 两处真实反例(都来自复审实测): // - kook:同一 guild 的两个 text channel —— `ChannelID` 不同(c1/c2)但 `GroupID` 相同, // 会话层按 `group:` 判成**同一个** session;任何把 `ChannelID` 当会话判据的 // 派生都会把它们拆成两条车道。 // - 多账号:同一平台两个账号对同一对端 —— 账号名不同(`telegram:conf0`/`:conf1`)、 // `platform` 常量相同、`ChannelID` 相同 ⇒ 也是**同一个** session;任何把「账号」当 // 会话判据的派生都会把它们拆开。 // // 因此本文件只做一件事:把既有的派生**原样**导出,语义一个字不改(`channelID` 仍是唯一 // 的实现,见 manager.go)。 import "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" // sessionKeySep 分隔 (platform, channel_id) 两个维度。用 NUL 是因为平台名与渠道 id 都 // 不可能包含它,拼接因此不会产生歧义(`a` + "b" 与 `ab` + "" 不会撞键)。 const sessionKeySep = "\x00" // ChannelKey 返回一条入站消息所代表的**渠道**键(会话派生的第二个维度): // 群消息按 `group:`(加前缀使群不会与同 id 的私聊渠道撞键),私聊按 // `ChannelID`,没有显式渠道 id 时退回 `SenderID`。 // // 它就是 `SessionManager.GetOrCreate` 内部用的那一个函数(manager.go 的 `channelID`), // 导出只为让别的层(渠道桥接器的回合车道)能与会话键同源,**不得**在别处另写一份。 // 返回空串表示这条消息没有可用的渠道(`GetOrCreate` 会以 `ErrInvalidChannel` 拒掉它)。 func ChannelKey(msg adapter.UnifiedMessage) string { return channelID(msg) } // Key 返回一条入站消息的**会话键** —— 与 `SessionManager` 的缓存键和 // `channel_sessions` 表的唯一键**逐字相同**(`platform \x00 channelID`)。 // // 它是本包对外唯一的「什么算同一个会话」判据:任何需要按会话串行/分桶的调用方都用它, // 不要自己拼 `platform|channel` 之类的近似键(那正是 M22 复审里「车道键与会话键不同源」 // 的成因)。返回的键在渠道不可用时仍然非空(`platform\x00`),调用方若需要区分, // 用 ChannelKey 判空。 func Key(msg adapter.UnifiedMessage) string { return sessionKey(msg.Platform, channelID(msg)) } // sessionKey 拼接 (platform, channel_id) 两维度。本包内所有「会话键」的**唯一**出处 // (GetOrCreate / List / 导出的 Key 都经这里),因此不可能各自漂移。 func sessionKey(platform, cid string) string { return platform + sessionKeySep + cid }