From e4e4bb33fbcb2f2155d436cb64d648dc63194ce8 Mon Sep 17 00:00:00 2001 From: ntduc Date: Mon, 20 Jul 2026 15:05:54 +0700 Subject: [PATCH] fix(feishu): preserve group context without sender name --- internal/channels/feishu/bot.go | 13 +- internal/channels/feishu/bot_policy_test.go | 135 ++++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/internal/channels/feishu/bot.go b/internal/channels/feishu/bot.go index 4bfeb537..5e93feee 100644 --- a/internal/channels/feishu/bot.go +++ b/internal/channels/feishu/bot.go @@ -101,6 +101,10 @@ func (c *Channel) handleMessageEventFrom(ctx context.Context, event *MessageEven // 3. Resolve sender name (cached) senderName := c.resolveSenderName(ctx, mc.SenderID) + senderLabel := senderName + if senderLabel == "" { + senderLabel = mc.SenderID + } // 4. Resolve media BEFORE mention gate so non-mentioned messages // also have their files downloaded and stored in pending history. @@ -166,7 +170,7 @@ func (c *Channel) handleMessageEventFrom(ctx context.Context, event *MessageEven historyKey = fmt.Sprintf("%s:topic:%s", mc.ChatID, mc.RootID) } c.GroupHistory().Record(historyKey, channels.HistoryEntry{ - Sender: senderName, + Sender: senderLabel, SenderID: mc.SenderID, Body: mc.Content, Media: earlyMediaPaths, @@ -307,9 +311,12 @@ func (c *Channel) handleMessageEventFrom(ctx context.Context, event *MessageEven } // Annotate content with sender identity so the agent knows who is messaging. - if senderName != "" { + if mc.ChatType == "group" || senderName != "" { if mc.ChatType == "group" { - annotated := fmt.Sprintf("[From: %s]\n%s", senderName, content) + annotated := content + if senderLabel != "" { + annotated = fmt.Sprintf("[From: %s]\n%s", senderLabel, content) + } if c.HistoryLimit() > 0 { content = c.GroupHistory().BuildContext(chatID, annotated, c.HistoryLimit()) } else { diff --git a/internal/channels/feishu/bot_policy_test.go b/internal/channels/feishu/bot_policy_test.go index 2c8ecdba..2b157920 100644 --- a/internal/channels/feishu/bot_policy_test.go +++ b/internal/channels/feishu/bot_policy_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "strings" "testing" "time" @@ -80,6 +81,140 @@ func TestCheckGroupPolicy_Pairing_GroupAllowListBypasses(t *testing.T) { } } +func TestHandleMessageEvent_GroupContextFallsBackToSenderIDWhenNameLookupFails(t *testing.T) { + requireMention := true + msgBus := bus.New() + defer msgBus.Close() + + ch, err := New(config.FeishuConfig{ + AppID: "app", + AppSecret: "secret", + GroupPolicy: "open", + RequireMention: &requireMention, + }, msgBus, nil, nil, nil) + if err != nil { + t.Fatalf("New: %v", err) + } + ch.SetName("feishu-test") + ch.botOpenID = "ou_target_bot" + srv := newSimpleMockServer(t, `{"code":50000,"msg":"name lookup failed"}`) + ch.client = NewLarkClient("app", "secret", srv.URL) + + first := feishuGroupTextEvent(t, "om_context", "ou_duc", "oc_group", "Send AGENTS.md again", nil) + ch.handleMessageEvent(context.Background(), first) + assertNoFeishuInbound(t, msgBus) + + entries := ch.GroupHistory().GetEntries("oc_group") + if len(entries) != 1 { + t.Fatalf("history entries = %d, want 1", len(entries)) + } + if entries[0].Sender != "ou_duc" { + t.Fatalf("history sender = %q, want sender ID fallback", entries[0].Sender) + } + + second := feishuGroupTextEvent(t, "om_mention", "ou_duc", "oc_group", "@_user_1", []EventMention{ + feishuMention("@_user_1", "ou_target_bot", "Techlead"), + }) + ch.handleMessageEvent(context.Background(), second) + + msg := assertFeishuInbound(t, msgBus) + for _, want := range []string{ + "[From: ou_duc]", + "[empty message]", + "Send AGENTS.md again", + } { + if !strings.Contains(msg.Content, want) { + t.Errorf("content missing %q:\n%s", want, msg.Content) + } + } + if got := ch.GroupHistory().GetEntries("oc_group"); len(got) != 0 { + t.Fatalf("history entries after publish = %d, want 0", len(got)) + } +} + +func TestHandleMessageEvent_GroupContextPreservesResolvedSenderName(t *testing.T) { + requireMention := true + msgBus := bus.New() + defer msgBus.Close() + + ch, err := New(config.FeishuConfig{ + AppID: "app", + AppSecret: "secret", + GroupPolicy: "open", + RequireMention: &requireMention, + }, msgBus, nil, nil, nil) + if err != nil { + t.Fatalf("New: %v", err) + } + ch.SetName("feishu-test") + ch.botOpenID = "ou_target_bot" + srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{"user":{"name":"Duc Nguyen"}}}`) + ch.client = NewLarkClient("app", "secret", srv.URL) + + event := feishuGroupTextEvent(t, "om_named", "ou_duc", "oc_group", "@_user_1 hello", []EventMention{ + feishuMention("@_user_1", "ou_target_bot", "Techlead"), + }) + ch.handleMessageEvent(context.Background(), event) + + msg := assertFeishuInbound(t, msgBus) + if msg.Content != "[From: Duc Nguyen]\nhello" { + t.Fatalf("content = %q, want resolved sender name", msg.Content) + } + if msg.Metadata["sender_name"] != "Duc Nguyen" { + t.Fatalf("sender_name metadata = %q", msg.Metadata["sender_name"]) + } +} + +func TestHandleMessageEvent_P2PContextRemainsNameBased(t *testing.T) { + msgBus := bus.New() + defer msgBus.Close() + + ch, err := New(config.FeishuConfig{AppID: "app", AppSecret: "secret", DMPolicy: "open"}, msgBus, nil, nil, nil) + if err != nil { + t.Fatalf("New: %v", err) + } + ch.SetName("feishu-test") + srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{"user":{"name":"Duc Nguyen"}}}`) + ch.client = NewLarkClient("app", "secret", srv.URL) + + event := feishuGroupTextEvent(t, "om_p2p", "ou_duc", "oc_p2p", "hello", nil) + event.Event.Message.ChatType = "p2p" + ch.handleMessageEvent(context.Background(), event) + + msg := assertFeishuInbound(t, msgBus) + if msg.Content != "[From: Duc Nguyen]\nhello" { + t.Fatalf("content = %q, want unchanged p2p name context", msg.Content) + } +} + +func TestHandleMessageEvent_GroupContextDoesNotEmitEmptySenderLabel(t *testing.T) { + requireMention := true + msgBus := bus.New() + defer msgBus.Close() + + ch, err := New(config.FeishuConfig{ + AppID: "app", + AppSecret: "secret", + GroupPolicy: "open", + RequireMention: &requireMention, + }, msgBus, nil, nil, nil) + if err != nil { + t.Fatalf("New: %v", err) + } + ch.SetName("feishu-test") + ch.botOpenID = "ou_target_bot" + + event := feishuGroupTextEvent(t, "om_no_sender", "", "oc_group", "@_user_1", []EventMention{ + feishuMention("@_user_1", "ou_target_bot", "Techlead"), + }) + ch.handleMessageEvent(context.Background(), event) + + msg := assertFeishuInbound(t, msgBus) + if msg.Content != "[empty message]" { + t.Fatalf("content = %q, want no empty sender annotation", msg.Content) + } +} + type feishuPolicyPairingStore struct { requests int lastSenderID string