P2-1(行为缺陷):窗口准入此前只在 prepareInbound 里算 `solo`,而 prepare 发生在 **窗口等待之后** —— 平台帧(action / 系统通知 / 我们自己的 message_sent 回执)照样 进窗、照样把滑动计时重置:一条回执就能把这一批的静默期一直续住(群友说了三句话, 她因为「撤回事件」迟迟不开口)。这与任务书「窗口只收 chat/image/voice/file」相反。 - `laneWindowAdmits(msg)` 成为**唯一准入判据**,只看结构化事实(ContentType + isSystemNoticeMessage,绝不解析正文); - `splitWindowItems` 在窗口等待阶段把到达的条目分成「进窗口」与「不进窗口」两半; - 非准入帧**不建窗口、不重置计时**(首条即到达即发车),窗口中途到达的只收进 solos, 这一批发车后**原样回插队首**各自单跑(保序、不合并、不丢弃); - prepareInbound 里的 `solo` 保留为第二道保险(适配器将来把平台事件标成 text 时, 仍会在组批前被切出去)。 P2-2(硬约束):lane.go 682 行 → 按职责纯搬移成 5 个文件(最大 246 行): lane.go(车道类型与注册表)/ lane_queue.go(入队与取队列)/ lane_worker.go(单消费者与回合槽)/ lane_drop.go(丢弃与停机诚实)/ lane_owner.go(主人私聊判据与车道键)。语义零改动:去注释后的声明集合 (34 个顶层声明,含函数体/字段/常量值)逐字节全等;既有用例一条未改。 顺带:把两处「IsOwner() && !IsGroupChat()」的注释改成代码真正用的判据 (ownerPrivateLane = !msg.IsGroup() && actor.IsOwner()),并记下这处 P3 分叉。 验证:全树 build/vet/test 全绿(73 包 ok);三包 -race 全绿; 新增 4 条准入用例在「准入失效」变异下三条必红、对照组保持绿。
69 lines
4.4 KiB
Go
69 lines
4.4 KiB
Go
package channelbridge
|
||
|
||
// 本文件承载「**主人私聊**」这条车道的判据与车道键派生(M22 切片 LANEQUEUE;
|
||
// M23 的红线「主人私聊不要应用静默窗口」建立在这里的判据上)。
|
||
//
|
||
// 判据只用 actor 元数据 + 白名单:**绝不读正文**、绝不看提示词(项目红线)。
|
||
//
|
||
// 注意判据是 M22 既有的 `!msg.IsGroup() && actor.IsOwner()`,**不是**任务书里写的
|
||
// `actor.IsGroupChat()`:两者在「适配器把群事件标成 Sender.IsGroup 但 ChannelType 仍是
|
||
// Direct」这类帧上可能分叉(那条帧会走 direct,更保守,红线方向不受影响)。
|
||
// 本片不改这个判据(改它会动 M22 的车道键与丢弃策略,属另一片)。
|
||
// 它是 `neverDrop`(永不拒收)与 `direct`(无窗口、拿不到定时器工厂)两件事的
|
||
// **同一个**来源,因此不存在「键说是主人、丢弃策略说不是」的两套口径。
|
||
|
||
import (
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||
)
|
||
|
||
// ownerPrivateLane 报告这条消息是否属于「主人私聊」车道 —— 也就是**永不丢弃**的那条。
|
||
// 判据与 laneKey 完全同源(私聊 + actor 元数据说他是主人),因此不会出现「键说是主人、
|
||
// 丢弃策略说不是」的两套口径。这里绝不读正文:身份只来自 actor 元数据 + 白名单。
|
||
func ownerPrivateLane(msg adapter.UnifiedMessage, actor adapter.Actor) bool {
|
||
return !msg.IsGroup() && actor.IsOwner()
|
||
}
|
||
|
||
// laneStrangerPrivate 报告一条入站是否属于**陌生人私聊**(非群 + 非主人)。
|
||
//
|
||
// 它与 `isStrangerPrivate(sess)` 同源(同一判据的两面):会话层从 actor 派生
|
||
// `sess.Owner`,这里直接用 actor 元数据,因此不需要先查会话就能判定「这条车道
|
||
// 要不要受 applyToStrangers 管」。主人私聊走不到这里(那是 direct 车道)。
|
||
func laneStrangerPrivate(item laneItem) bool {
|
||
return !item.msg.IsGroup() && !item.actor.IsOwner()
|
||
}
|
||
|
||
// laneKey 决定「哪些消息共用一条车道」,也就是「什么算同一个会话」。它必须与
|
||
// **会话键同源**(M22 复审 P1-1):两套各算各的判据会让同一个 session 出现两条车道、
|
||
// 两个回合并发,而两条 run 共享 sessionID ⇒ checkpoint 行 id `{sessionID}:{step}` 互相
|
||
// 覆盖、`loadCheckpoint` 可能续到另一条在途 run 的快照而重放工具调用,flow 桶
|
||
// `p.graphs[sessionID]` 被先收尾者删掉(loop 没有会话级互斥,IM 侧也没有网关那层
|
||
// `session_turn_fence`)。
|
||
//
|
||
// - **主人私聊**:无论来自哪个平台,都被 inboundTargetFor 路由进同一个 Web 主会话
|
||
// (history.MainSessionID),因此共用一条 `main` 车道。这里的键与会话键**故意不同**
|
||
// ——`main` 是那条路由的目的地,比渠道键更强(把各平台的主人私聊并成一条)。
|
||
// - 其余:**逐字就是** `session.Key(msg)`(`platform \x00 channelID`,与会话层缓存键
|
||
// 同一个出处),所以「会话层认为是一个会话」与「车道认为是一个会话」不可能分叉。
|
||
//
|
||
// 🔴 **取舍(比旧实现少了一个维度,是有意的)**:键里**没有账号名**。同一平台的两个账号
|
||
// 收到同一对端/同一个群的消息时,它们是**同一个 session**(会话键只含 platform 与
|
||
// channel),因此必须共用一条车道 —— 分开就会让同一会话两个回合并发。副作用是同一对端
|
||
// 的多个账号的入站会串行排队;回复出口不受影响:出站路由取的是**每条消息自己的**
|
||
// `msg.Platform`/`ChannelID`/`GroupID` 与它自己的适配器(`laneItem.a`),车道本身从不
|
||
// 持有任何账号级状态。
|
||
//
|
||
// 同理也**不要**在这里重新引入「频道」维度:kook 同一 guild 的两个 text channel 在会话层
|
||
// 是同一个群会话(键按 `group:<GroupID>`),按 `ChannelID` 分车道同样会造成双车道。
|
||
func laneKey(msg adapter.UnifiedMessage, actor adapter.Actor) string {
|
||
if ownerPrivateLane(msg, actor) {
|
||
return ownerPrivateLaneKey
|
||
}
|
||
if session.ChannelKey(msg) == "" {
|
||
// 没有任何渠道可用(私聊既无 ChannelID 也无 SenderID):会话层会以
|
||
// ErrInvalidChannel 拒掉这条消息,这里只需要一个不会与真实会话撞键的稳定键。
|
||
return "unbound" + laneKeySep + adapter.ConversationKey(msg)
|
||
}
|
||
return session.Key(msg)
|
||
}
|