mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(channel): add quoted message context injection for Discord and Feishu
Prepend replied-to message text and attachments into the user query so the LLM can see what is being replied to, matching the existing Telegram behavior. Also set is_reply_to_bot metadata for Feishu reply-to-bot detection in group chats.
This commit is contained in:
@@ -428,6 +428,7 @@ func (a *FeishuAdapter) Connect(ctx context.Context, cfg channel.ChannelConfig,
|
||||
return nil
|
||||
}
|
||||
a.enrichSenderProfile(connCtx, cfg, event, &msg)
|
||||
a.enrichQuotedMessage(connCtx, cfg, &msg, botOpenID)
|
||||
msg.BotID = cfg.BotID
|
||||
if a.logger != nil {
|
||||
isMentioned := false
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package feishu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
||||
|
||||
"github.com/memohai/memoh/internal/channel"
|
||||
)
|
||||
|
||||
const feishuQuotedTextMaxLength = 200
|
||||
|
||||
// enrichQuotedMessage fetches the parent message via API and prepends a
|
||||
// quoted-text summary to the inbound message so the AI can see what is
|
||||
// being replied to. It also sets the "is_reply_to_bot" metadata flag.
|
||||
func (a *FeishuAdapter) enrichQuotedMessage(ctx context.Context, cfg channel.ChannelConfig, msg *channel.InboundMessage, botOpenID string) {
|
||||
if msg == nil || msg.Message.Reply == nil {
|
||||
return
|
||||
}
|
||||
parentID := strings.TrimSpace(msg.Message.Reply.MessageID)
|
||||
if parentID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
feishuCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := lark.NewClient(feishuCfg.AppID, feishuCfg.AppSecret, lark.WithOpenBaseUrl(feishuCfg.openBaseURL()))
|
||||
resp, err := client.Im.Message.Get(lookupCtx, larkim.NewGetMessageReqBuilder().MessageId(parentID).Build())
|
||||
if err != nil {
|
||||
if a.logger != nil {
|
||||
a.logger.Debug("feishu quoted message fetch failed",
|
||||
slog.String("parent_id", parentID),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if resp == nil || !resp.Success() || resp.Data == nil || len(resp.Data.Items) == 0 {
|
||||
if a.logger != nil {
|
||||
code, respMsg := 0, ""
|
||||
if resp != nil {
|
||||
code = resp.Code
|
||||
respMsg = resp.Msg
|
||||
}
|
||||
a.logger.Debug("feishu quoted message fetch empty",
|
||||
slog.String("parent_id", parentID),
|
||||
slog.Int("code", code),
|
||||
slog.String("msg", respMsg),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parent := resp.Data.Items[0]
|
||||
|
||||
// Determine if the parent message is from the bot itself.
|
||||
isReplyToBot := false
|
||||
senderName := ""
|
||||
if parent.Sender != nil {
|
||||
senderType := ptrStr(parent.Sender.SenderType)
|
||||
senderID := ptrStr(parent.Sender.Id)
|
||||
if senderType == "app" {
|
||||
// When botOpenID is known, match precisely; otherwise any app sender counts.
|
||||
isReplyToBot = strings.TrimSpace(botOpenID) == "" || senderID == strings.TrimSpace(botOpenID)
|
||||
}
|
||||
}
|
||||
if msg.Metadata == nil {
|
||||
msg.Metadata = map[string]any{}
|
||||
}
|
||||
msg.Metadata["is_reply_to_bot"] = isReplyToBot
|
||||
|
||||
// Extract text content from the parent message.
|
||||
text := extractFeishuMessageText(parent)
|
||||
if text == "" {
|
||||
msgType := ptrStr(parent.MsgType)
|
||||
if msgType != "" && msgType != larkim.MsgTypeText {
|
||||
text = "[" + msgType + "]"
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
if len([]rune(text)) > feishuQuotedTextMaxLength {
|
||||
text = string([]rune(text)[:feishuQuotedTextMaxLength]) + "..."
|
||||
}
|
||||
|
||||
var quotedText string
|
||||
if senderName != "" {
|
||||
quotedText = fmt.Sprintf("[Reply to %s: %s]", senderName, text)
|
||||
} else {
|
||||
quotedText = fmt.Sprintf("[Reply to: %s]", text)
|
||||
}
|
||||
|
||||
current := strings.TrimSpace(msg.Message.Text)
|
||||
if current != "" {
|
||||
msg.Message.Text = quotedText + "\n" + current
|
||||
} else {
|
||||
msg.Message.Text = quotedText
|
||||
}
|
||||
}
|
||||
|
||||
// extractFeishuMessageText extracts plain text from a Feishu message object
|
||||
// returned by the Get Message API.
|
||||
func extractFeishuMessageText(msg *larkim.Message) string {
|
||||
if msg == nil || msg.Body == nil || msg.Body.Content == nil {
|
||||
return ""
|
||||
}
|
||||
content := strings.TrimSpace(*msg.Body.Content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var contentMap map[string]any
|
||||
if err := json.Unmarshal([]byte(content), &contentMap); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
msgType := ptrStr(msg.MsgType)
|
||||
switch msgType {
|
||||
case larkim.MsgTypeText:
|
||||
if txt, ok := contentMap["text"].(string); ok {
|
||||
return strings.TrimSpace(txt)
|
||||
}
|
||||
case larkim.MsgTypePost:
|
||||
return extractFeishuPostText(contentMap)
|
||||
}
|
||||
|
||||
// Fallback: try "text" key for unknown types.
|
||||
if txt, ok := contentMap["text"].(string); ok {
|
||||
return strings.TrimSpace(txt)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -110,6 +110,7 @@ func (h *WebhookHandler) Handle(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
h.adapter.enrichSenderProfile(reqCtx, cfg, event, &msg)
|
||||
h.adapter.enrichQuotedMessage(reqCtx, cfg, &msg, botOpenID)
|
||||
msg.BotID = cfg.BotID
|
||||
return h.manager.HandleInbound(reqCtx, cfg, msg)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user