复审 r1 的 P1-1 被证实:车道键(a.Name()+ConversationKey,含频道与账号维度)与会话键 (platform \x00 channelID)是两套没有共同来源的判据,于是 kook 同一 guild 的两个 text channel、以及同平台两个账号对同一对端,都会得到「一个 session、两条车道、两个回合并发」 —— 两条 run 共享 sessionID,checkpoint 行 id 互相覆盖 / flow 桶被先收尾者删掉。 - P1-1 车道键与会话键同源:`internal/channel/session` 导出 ChannelKey / Key(channelID 与原缓存键派生**原样**导出,manager 内部改为走同一个 sessionKey,语义一字未改); `laneKey` 现在逐字是 `session.Key(msg)`(主人私聊仍是 `main|web` 这个目的地键), **去掉 a.Name() 维度**。同平台多账号对同一对端因此共用一条车道(它们是同一个 session), 出站路由不受影响:回复取每条消息自己的 platform/ChannelID 与它自己的适配器。 - P2-1 溢出区保序:`spill` 非空后新条目一律追加 spill、不再回写 `ch`,修掉复审实测的 `[m1 m2 m3 m5 m4]` 超车(现为到达顺序)。 - P2-2 口径修正:lane_test 文件头改指 `TestLaneRejectsNewestWhenFull`(语义是「门前拒收 最新、队列不动」);laneKey 注释改写「多账号必须共用一条车道」并写明取舍。 - P2-3 生产可见性:丢弃 WARN 带上 `dropped_stale_total` / `dropped_overflow_total` 累计 计数(不新开端点);主人私聊积压改成按倍率 16/32/64… 告警,不再只响一次。 断言:TestLaneKeyNeverCollidesAcrossSessions(表驱动,含 kook c1/c2@同 guild、 telegram conf0/conf1@同对端两行 + 不同群仍并行的控制行)、 TestLaneOwnerPrivateSpillKeepsArrivalOrder(逐条放行,钉住到达顺序)、 internal/channel/session/keys_test.go(键派生与会话缓存键形状)。
55 lines
3.5 KiB
Go
55 lines
3.5 KiB
Go
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:<GroupID>` 判成**同一个** 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:<GroupID>`(加前缀使群不会与同 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
|
||
}
|