diff --git a/internal/channels/feishu/bot.go b/internal/channels/feishu/bot.go index 9222e066..87ce4b0a 100644 --- a/internal/channels/feishu/bot.go +++ b/internal/channels/feishu/bot.go @@ -110,6 +110,13 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) { content = "[empty message]" } + // 7b. Fetch reply context if this is a reply to another message + if mc.ParentID != "" { + if replyCtx := c.fetchReplyContext(ctx, mc.ParentID); replyCtx != "" { + content += "\n\n" + replyCtx + } + } + // 8. Topic session chatID := mc.ChatID if mc.RootID != "" && c.cfg.TopicSessionMode == "enabled" { @@ -258,3 +265,35 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) { c.groupHistory.Clear(chatID) } } + +const replyContextMaxLen = 500 + +// fetchReplyContext fetches the parent message content and returns a formatted +// reply context string, similar to Telegram's [Replying to ...] format. +func (c *Channel) fetchReplyContext(ctx context.Context, parentID string) string { + resp, err := c.client.GetMessage(ctx, parentID) + if err != nil { + slog.Debug("feishu: failed to fetch parent message", "parent_id", parentID, "error", err) + return "" + } + if len(resp.Items) == 0 { + return "" + } + + item := &resp.Items[0] + body := parseMessageContent(item.Body.Content, item.MsgType) + if body == "" { + return "" + } + + // Resolve sender name + senderName := "unknown" + if item.Sender.ID != "" { + if name := c.resolveSenderName(ctx, item.Sender.ID); name != "" { + senderName = name + } + } + + body = channels.Truncate(body, replyContextMaxLen) + return fmt.Sprintf("[Replying to %s]\n%s\n[/Replying]", senderName, body) +} diff --git a/internal/channels/feishu/larkclient_messaging.go b/internal/channels/feishu/larkclient_messaging.go index ce568958..f5ecf300 100644 --- a/internal/channels/feishu/larkclient_messaging.go +++ b/internal/channels/feishu/larkclient_messaging.go @@ -89,6 +89,42 @@ func (c *LarkClient) UploadFile(ctx context.Context, data io.Reader, fileName, f return result.FileKey, nil } +// --- IM API: Get Message --- + +// GetMessageResp holds the response from GET /open-apis/im/v1/messages/{message_id}. +type GetMessageResp struct { + Items []struct { + MessageID string `json:"message_id"` + MsgType string `json:"msg_type"` + Body struct { + Content string `json:"content"` + } `json:"body"` + Sender struct { + ID string `json:"id"` + IDType string `json:"id_type"` + SenderType string `json:"sender_type"` + } `json:"sender"` + } `json:"items"` +} + +// GetMessage retrieves a message by ID. +// Lark API: GET /open-apis/im/v1/messages/{message_id} +func (c *LarkClient) GetMessage(ctx context.Context, messageID string) (*GetMessageResp, error) { + path := fmt.Sprintf("/open-apis/im/v1/messages/%s", messageID) + resp, err := c.doJSON(ctx, "GET", path, nil) + if err != nil { + return nil, err + } + if resp.Code != 0 { + return nil, fmt.Errorf("get message: code=%d msg=%s", resp.Code, resp.Msg) + } + var data GetMessageResp + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, fmt.Errorf("unmarshal get message: %w", err) + } + return &data, nil +} + // --- IM API: Message Resources --- func (c *LarkClient) DownloadMessageResource(ctx context.Context, messageID, fileKey, resourceType string) ([]byte, string, error) {