feat(feishu): include reply context when user replies to a message (#272)

When a Feishu/Lark user replies to an existing message, the agent now
sees the quoted message content via the GetMessage API, formatted as
[Replying to SenderName]...[/Replying] — matching the Telegram channel
behavior. Previously parent_id was parsed but the content was silently
discarded, so the agent had no idea what the user was replying to.

Requires im:message:readonly scope on the Lark app.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-19 14:03:09 +07:00
1 parent 0dd4ebd6e6
commit 44197011f8
2 files changed
+75

No files matched your search

+39
View File
@@ -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)
}
@@ -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) {