From db8465f68beb12d62771fe1e9988dd2d88c96f6a Mon Sep 17 00:00:00 2001 From: Aska Date: Mon, 7 Sep 2026 18:38:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(kook):=20=E4=B8=A2=E5=BC=83=E8=87=AA?= =?UTF-8?q?=E6=88=91=E5=9B=9E=E5=A3=B0=E6=B6=88=E6=81=AF=EF=BC=8C=E9=98=B2?= =?UTF-8?q?=20bot=20=E6=8A=8A=E8=87=AA=E5=B7=B1=E7=9A=84=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=BD=93=E5=85=A5=E7=AB=99=E7=94=A8=E6=88=B7=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KOOK 网关会把 bot 自己发的消息又推回给 bot(author_id=bot uid),导致 bot 把自己的 回复当成入站用户消息处理(循环/浪费token)。toUnifiedMessage 计算 senderID 后调用 isSelf(senderID) 比对 a.botID(user/me 解析出的 bot 账号 id),命中即返回 ok=false 丢弃。 - identity.go: 新增 isSelf(并发安全,botMu) - message.go: self-echo 丢弃 + debug 日志 - message_test.go: TestToUnified_DropsSelfEcho 验收: go build/vet/test ./... 全绿 --- plugins/channels/kook/identity.go | 13 +++++++++++++ plugins/channels/kook/message.go | 6 ++++++ plugins/channels/kook/message_test.go | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/plugins/channels/kook/identity.go b/plugins/channels/kook/identity.go index 6093caf..98ce34f 100644 --- a/plugins/channels/kook/identity.go +++ b/plugins/channels/kook/identity.go @@ -81,6 +81,19 @@ func (a *Adapter) botIdentitySnapshot() (id, username, nickname string) { return a.botID, a.botUsername, a.botNickname } +// isSelf reports whether senderID is the bot's own account id. KOOK delivers the +// bot's own messages back over the gateway, so an inbound event whose author_id equals +// the bot's user id is a self-echo and must be dropped — otherwise the bridge would +// process the bot's replies as inbound user messages and loop (or burn tokens). +func (a *Adapter) isSelf(senderID string) bool { + if senderID == "" { + return false + } + a.botMu.RLock() + defer a.botMu.RUnlock() + return a.botID != "" && senderID == a.botID +} + // BotIdentity returns the resolved bot account (id/username/nickname). The values // are non-sensitive (the bot's public handle and display name) so the admin panel // may show them. diff --git a/plugins/channels/kook/message.go b/plugins/channels/kook/message.go index b95229e..2a148b5 100644 --- a/plugins/channels/kook/message.go +++ b/plugins/channels/kook/message.go @@ -82,6 +82,12 @@ func (a *Adapter) toUnifiedMessage(data json.RawMessage, frame []byte) (adapter. _ = json.Unmarshal(ev.Extra, &ex) senderID, senderName := senderOf(&ev, &ex) + // Self-echo: KOOK pushes the bot's own channel messages back to it via the gateway. + // Drop them so the bridge never treats the bot's replies as inbound user messages. + if a.isSelf(senderID) { + a.log.Debug("self message dropped (echo)", "sender_id", senderID, "channel", ev.TargetID) + return adapter.UnifiedMessage{}, false + } ct, media := messageKind(ev.Type, content) group := channelIsGroup(ev.ChannelType) diff --git a/plugins/channels/kook/message_test.go b/plugins/channels/kook/message_test.go index 57be1c4..4f84881 100644 --- a/plugins/channels/kook/message_test.go +++ b/plugins/channels/kook/message_test.go @@ -7,6 +7,21 @@ import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) +func TestToUnified_DropsSelfEcho(t *testing.T) { + t.Parallel() + a := New(Config{BotToken: "x"}) + a.setBotIdentity(botIdentity{ID: "bot-1", Username: "Cyrene", Nickname: "Cyrene"}) + // KOOK pushes the bot's own channel message back to it; author_id == bot id. + data := json.RawMessage(`{ + "channel_type":"GROUP","type":1,"target_id":"chan-1","author_id":"bot-1", + "content":"咦,亲爱的这是想戳人家哪里呀♪","msg_id":"m-self","msg_timestamp":1700000000000,"nonce":"", + "extra":{"type":1,"guild_id":"g-1","mention":[],"author":{"id":"bot-1","username":"Cyrene","nickname":"Cyrene"}} + }`) + if _, ok := a.toUnifiedMessage(data, nil); ok { + t.Fatal("self-echo message should be dropped (ok=false)") + } +} + func TestToUnified_GroupText(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"})