mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 14:17:40 +00:00
fix: Implement Lark bot identity probe and group history for mention detection, fix Discord placeholder chunking.
- Implement `probeBotInfo()` to fetch bot's `open_id` via `/open-apis/bot/v3/info`, fixing group mention detection that was always false due to empty `botOpenID`. - Add group pending history support to Feishu channel (record/build context/clear) matching Discord pattern. - Fix Discord placeholder edit to chunk long responses instead of truncating at 2000 chars.
This commit is contained in:
1 parent
2ee39c8483
commit
2e5cf4a9c9
4 files changed
+92
-19
No files matched your search
@@ -146,18 +146,31 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to edit the placeholder "Thinking..." message
|
||||
// Try to edit the placeholder "Thinking..." message with the first chunk,
|
||||
// then send the rest as follow-up messages.
|
||||
if pID, ok := c.placeholders.Load(placeholderKey); ok {
|
||||
c.placeholders.Delete(placeholderKey)
|
||||
msgID := pID.(string)
|
||||
|
||||
// Discord has a 2000-char message limit
|
||||
const maxLen = 2000
|
||||
editContent := content
|
||||
if len(editContent) > 2000 {
|
||||
editContent = editContent[:1997] + "..."
|
||||
remaining := ""
|
||||
|
||||
if len(editContent) > maxLen {
|
||||
// Break at a newline if possible
|
||||
cutAt := maxLen
|
||||
if idx := lastIndexByte(content[:maxLen], '\n'); idx > maxLen/2 {
|
||||
cutAt = idx + 1
|
||||
}
|
||||
editContent = content[:cutAt]
|
||||
remaining = content[cutAt:]
|
||||
}
|
||||
|
||||
if _, editErr := c.session.ChannelMessageEdit(channelID, msgID, editContent); editErr == nil {
|
||||
// Send remaining content as follow-up messages
|
||||
if remaining != "" {
|
||||
return c.sendChunked(channelID, remaining)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
slog.Warn("discord: placeholder edit failed, sending new message",
|
||||
|
||||
@@ -67,13 +67,26 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. RequireMention check
|
||||
// 5. RequireMention check — record to history if not mentioned
|
||||
requireMention := true
|
||||
if c.cfg.RequireMention != nil {
|
||||
requireMention = *c.cfg.RequireMention
|
||||
}
|
||||
if requireMention && !mc.MentionedBot {
|
||||
slog.Debug("feishu group message skipped: bot not mentioned", "chat_id", mc.ChatID)
|
||||
historyKey := mc.ChatID
|
||||
if mc.RootID != "" && c.cfg.TopicSessionMode == "enabled" {
|
||||
historyKey = fmt.Sprintf("%s:topic:%s", mc.ChatID, mc.RootID)
|
||||
}
|
||||
c.groupHistory.Record(historyKey, channels.HistoryEntry{
|
||||
Sender: senderName,
|
||||
Body: mc.Content,
|
||||
Timestamp: time.Now(),
|
||||
MessageID: messageID,
|
||||
}, c.historyLimit)
|
||||
|
||||
slog.Debug("feishu group message recorded (no mention)",
|
||||
"chat_id", mc.ChatID, "sender", senderName,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -124,13 +137,23 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) {
|
||||
metadata["sender_open_id"] = sender.SenderID.OpenID
|
||||
}
|
||||
|
||||
// Annotate current message with sender name so LLM knows who is talking in groups.
|
||||
// Build final content with group context (pending history + sender annotation).
|
||||
if mc.ChatType == "group" && senderName != "" {
|
||||
content = fmt.Sprintf("[From: %s]\n%s", senderName, content)
|
||||
annotated := fmt.Sprintf("[From: %s]\n%s", senderName, content)
|
||||
if c.historyLimit > 0 {
|
||||
content = c.groupHistory.BuildContext(chatID, annotated, c.historyLimit)
|
||||
} else {
|
||||
content = annotated
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Publish to bus
|
||||
c.HandleMessage(mc.SenderID, chatID, content, nil, metadata, peerKind)
|
||||
|
||||
// Clear pending history after sending to agent.
|
||||
if mc.ChatType == "group" {
|
||||
c.groupHistory.Clear(chatID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parse ---
|
||||
|
||||
@@ -31,17 +31,19 @@ const (
|
||||
// Channel connects to Feishu/Lark via native HTTP + WebSocket.
|
||||
type Channel struct {
|
||||
*channels.BaseChannel
|
||||
cfg config.FeishuConfig
|
||||
client *LarkClient
|
||||
botOpenID string
|
||||
pairingService store.PairingStore
|
||||
senderCache sync.Map // open_id → *senderCacheEntry
|
||||
dedup sync.Map // message_id → struct{}
|
||||
cfg config.FeishuConfig
|
||||
client *LarkClient
|
||||
botOpenID string
|
||||
pairingService store.PairingStore
|
||||
senderCache sync.Map // open_id → *senderCacheEntry
|
||||
dedup sync.Map // message_id → struct{}
|
||||
pairingDebounce sync.Map // senderID → time.Time
|
||||
groupAllowList []string
|
||||
stopCh chan struct{}
|
||||
httpServer *http.Server
|
||||
wsClient *WSClient
|
||||
groupAllowList []string
|
||||
groupHistory *channels.PendingHistory
|
||||
historyLimit int
|
||||
stopCh chan struct{}
|
||||
httpServer *http.Server
|
||||
wsClient *WSClient
|
||||
}
|
||||
|
||||
type senderCacheEntry struct {
|
||||
@@ -62,12 +64,19 @@ func New(cfg config.FeishuConfig, msgBus *bus.MessageBus, pairingSvc store.Pairi
|
||||
|
||||
base := channels.NewBaseChannel("feishu", msgBus, cfg.AllowFrom)
|
||||
|
||||
historyLimit := cfg.HistoryLimit
|
||||
if historyLimit == 0 {
|
||||
historyLimit = channels.DefaultGroupHistoryLimit
|
||||
}
|
||||
|
||||
return &Channel{
|
||||
BaseChannel: base,
|
||||
cfg: cfg,
|
||||
client: client,
|
||||
pairingService: pairingSvc,
|
||||
groupAllowList: cfg.GroupAllowFrom,
|
||||
groupHistory: channels.NewPendingHistory(),
|
||||
historyLimit: historyLimit,
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
@@ -233,7 +242,14 @@ func (c *Channel) startWebhook(ctx context.Context) error {
|
||||
// --- Bot probe ---
|
||||
|
||||
func (c *Channel) probeBotInfo(ctx context.Context) error {
|
||||
// Bot open_id will be resolved from first message event if needed
|
||||
openID, err := c.client.GetBotInfo(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch bot info: %w", err)
|
||||
}
|
||||
if openID == "" {
|
||||
return fmt.Errorf("bot open_id is empty")
|
||||
}
|
||||
c.botOpenID = openID
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -387,6 +387,27 @@ func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, c
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Bot API ---
|
||||
|
||||
// GetBotInfo fetches the bot's identity from /open-apis/bot/v3/info.
|
||||
// Returns the bot's open_id which is needed for mention detection in groups.
|
||||
func (c *LarkClient) GetBotInfo(ctx context.Context) (string, error) {
|
||||
resp, err := c.doJSON(ctx, "GET", "/open-apis/bot/v3/info", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return "", fmt.Errorf("get bot info: code=%d msg=%s", resp.Code, resp.Msg)
|
||||
}
|
||||
var result struct {
|
||||
Bot struct {
|
||||
OpenID string `json:"open_id"`
|
||||
} `json:"bot"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
return result.Bot.OpenID, nil
|
||||
}
|
||||
|
||||
// --- Contact API ---
|
||||
|
||||
func (c *LarkClient) GetUser(ctx context.Context, userID, userIDType string) (string, error) {
|
||||
|
||||
Reference in new issue
Block a user