真因(agent-221 用三条独立证据推翻了「SenderID 为空」的假设):ev.UserID 有值; owner.yaml 的 qq 条目是 10:37:57 才加的(此前那几条判陌生人属**正确的 fail-closed**); 而热重载成功后仍不对 —— 因为 internal/channel/session/manager.go 的 GetOrCreate 对**已绑定的渠道直接返回缓存会话**:判主门槛用新 actor 会过(所以她会回话), 但**车道用的是缓存的旧会话**(Owner=false ⇒ 不镜像进 Web 主会话、MemWritable=false、 落盘 actor_role 永不更新)。 修:缓存命中但 ActorRole 与本次解析不一致 ⇒ rebindLocked 刷新车道 + 同步落盘 (updateActorRoleSQL);persist 的 INSERT OR IGNORE 后读回落盘车道、不一致就地纠正 (覆盖上一轮进程留下的旧行);落盘失败只 WARN(不因记账失败丢主人消息); **身份变化不搬家、不换 session id**;车道只来自 ResolveActor 的结构化白名单判定。 QQ 适配器(防御加固,非本次真机 bug 的修复):senderIDOf 兜底链 顶层 user_id → sender.user_id;并用完整论证钉住「**群聊绝不能用 ChannelID 兜底**」(否则群里任何人 可顶群号冒充主人);顺手体检:自己发的消息(post_type=message_sent)与 meta_event 已被 handlePayload 过滤。为守 ≤500 行把 id/字符串小工具拆到 ids.go。 测试:session/ownerlane_test + qq/ownerlane_test 新增用例(含「群号等于主人号时 陌生人不得借群号冒充」「群聊发送者 id 缺失时 SenderID 必须为空而非群号」「白名单 热重载后车道立刻刷新且 session id 不变」「白名单移除后立即退回,不是单向提权」 「跨进程继承的旧行被就地纠正」)。 变异 5 条(去 sender.user_id 兜底 / 群聊用群号当 SenderID / 命中却判 unknown / 还原缓存直返 / 去掉旧行纠正),每条先确认可编译、贴红行、还原复绿(md5 逐字节校验)。
56 lines
1.7 KiB
Go
56 lines
1.7 KiB
Go
package qq_obv11
|
||
|
||
// 本文件承载 OneBot 事件里 id 的解析与几个轻量字符串工具,从 message.go 拆出
|
||
// (AGENTS §10 文件 ≤500 行)。判主用的 SenderID 解析规则就在这里:见 senderIDOf。
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
)
|
||
|
||
// senderIDOf 解析本条消息的发送者 id(判主用的那个字段)。OneBot v11 规定消息事件
|
||
// 在**顶层**带 user_id(obEvent.UserID),但实现之间有出入:有的只在 sender.user_id
|
||
// 里给。过去的实现只读顶层,顶层缺失/为 0 时 SenderID 就成了空串,而
|
||
// owner.IsOwner 对空 accountID 一律返回 false(fail-closed)——号码明明在白名单里
|
||
// 也永远判不成主人。这里给出兜底链:顶层 user_id → sender.user_id,两者都缺才是空。
|
||
//
|
||
// 群聊同样走这条链(发送者就是 user_id):群号**不是**发送者,绝不能作为它的兜底,
|
||
// 否则群里任何人都能顶着群号去撞白名单。见 toUnified 的 group/private 两个分支。
|
||
func senderIDOf(ev *obEvent) string {
|
||
if ev == nil {
|
||
return ""
|
||
}
|
||
if id := formatID(ev.UserID); id != "" {
|
||
return id
|
||
}
|
||
if ev.Sender != nil {
|
||
return formatID(ev.Sender.UserID)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// formatID 把 OneBot 的数字 id 归一成字符串;0(字段缺失)归一成空串,让
|
||
// 「没给」与「给了 0」在下游是同一种情况。
|
||
func formatID(id int64) string {
|
||
if id == 0 {
|
||
return ""
|
||
}
|
||
return strconv.FormatInt(id, 10)
|
||
}
|
||
|
||
func parseInt64(s string) (int64, error) {
|
||
if s == "" {
|
||
return 0, fmt.Errorf("empty id")
|
||
}
|
||
return strconv.ParseInt(s, 10, 64)
|
||
}
|
||
|
||
func firstNonEmpty(ss ...string) string {
|
||
for _, s := range ss {
|
||
if s != "" {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|