mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
This reverts commit e556a816e4.
This commit is contained in:
@@ -49,8 +49,6 @@ type FeishuChannel struct {
|
||||
|
||||
mu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
|
||||
progress *channels.ToolFeedbackAnimator
|
||||
}
|
||||
|
||||
type cachedMessage struct {
|
||||
@@ -76,7 +74,6 @@ func NewFeishuChannel(bc *config.Channel, cfg *config.FeishuSettings, bus *bus.M
|
||||
tokenCache: tc,
|
||||
client: lark.NewClient(cfg.AppID, cfg.AppSecret.String(), opts...),
|
||||
}
|
||||
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
||||
ch.SetOwner(ch)
|
||||
return ch, nil
|
||||
}
|
||||
@@ -135,9 +132,6 @@ func (c *FeishuChannel) Stop(ctx context.Context) error {
|
||||
}
|
||||
c.wsClient = nil
|
||||
c.mu.Unlock()
|
||||
if c.progress != nil {
|
||||
c.progress.StopAll()
|
||||
}
|
||||
|
||||
c.SetRunning(false)
|
||||
logger.InfoC("feishu", "Feishu channel stopped")
|
||||
@@ -155,50 +149,17 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]st
|
||||
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||
}
|
||||
|
||||
isToolFeedback := outboundMessageIsToolFeedback(msg)
|
||||
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
|
||||
if isToolFeedback {
|
||||
if msgID, handled, err := c.progress.Update(ctx, msg.ChatID, msg.Content); handled {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{msgID}, nil
|
||||
}
|
||||
} else {
|
||||
if msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {
|
||||
return msgIDs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Build interactive card with markdown content
|
||||
sendContent := msg.Content
|
||||
if isToolFeedback {
|
||||
sendContent = channels.InitialAnimatedToolFeedbackContent(msg.Content)
|
||||
}
|
||||
cardContent, err := buildMarkdownCard(sendContent)
|
||||
cardContent, err := buildMarkdownCard(msg.Content)
|
||||
if err != nil {
|
||||
// If card build fails, fall back to plain text
|
||||
msgID, sendErr := c.sendText(ctx, msg.ChatID, sendContent)
|
||||
if sendErr != nil {
|
||||
return nil, sendErr
|
||||
}
|
||||
if isToolFeedback {
|
||||
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
||||
} else if hasTrackedMsg {
|
||||
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
||||
}
|
||||
return []string{msgID}, nil
|
||||
return nil, c.sendText(ctx, msg.ChatID, msg.Content)
|
||||
}
|
||||
|
||||
// First attempt: try sending as interactive card
|
||||
msgID, err := c.sendCard(ctx, msg.ChatID, cardContent)
|
||||
err = c.sendCard(ctx, msg.ChatID, cardContent)
|
||||
if err == nil {
|
||||
if isToolFeedback {
|
||||
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
||||
} else if hasTrackedMsg {
|
||||
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
||||
}
|
||||
return []string{msgID}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if error is due to card table limit (error code 11310)
|
||||
@@ -213,14 +174,9 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]st
|
||||
})
|
||||
|
||||
// Second attempt: fall back to plain text message
|
||||
msgID, textErr := c.sendText(ctx, msg.ChatID, sendContent)
|
||||
textErr := c.sendText(ctx, msg.ChatID, msg.Content)
|
||||
if textErr == nil {
|
||||
if isToolFeedback {
|
||||
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
||||
} else if hasTrackedMsg {
|
||||
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
||||
}
|
||||
return []string{msgID}, nil
|
||||
return nil, nil
|
||||
}
|
||||
// If text also fails, return the text error
|
||||
return nil, textErr
|
||||
@@ -254,23 +210,6 @@ func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messageID, cont
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMessage implements channels.MessageDeleter.
|
||||
func (c *FeishuChannel) DeleteMessage(ctx context.Context, chatID, messageID string) error {
|
||||
req := larkim.NewDeleteMessageReqBuilder().
|
||||
MessageId(messageID).
|
||||
Build()
|
||||
|
||||
resp, err := c.client.Im.V1.Message.Delete(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("feishu delete: %w", err)
|
||||
}
|
||||
if !resp.Success() {
|
||||
c.invalidateTokenOnAuthError(resp.Code)
|
||||
return fmt.Errorf("feishu delete api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendPlaceholder implements channels.PlaceholderCapable.
|
||||
// Sends an interactive card with placeholder text and returns its message ID.
|
||||
func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
|
||||
@@ -312,81 +251,6 @@ func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID string) (str
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func outboundMessageIsToolFeedback(msg bus.OutboundMessage) bool {
|
||||
if len(msg.Context.Raw) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(msg.Context.Raw["message_kind"]), "tool_feedback")
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) currentToolFeedbackMessage(chatID string) (string, bool) {
|
||||
if c.progress == nil {
|
||||
return "", false
|
||||
}
|
||||
return c.progress.Current(chatID)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) takeToolFeedbackMessage(chatID string) (string, string, bool) {
|
||||
if c.progress == nil {
|
||||
return "", "", false
|
||||
}
|
||||
return c.progress.Take(chatID)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) RecordToolFeedbackMessage(chatID, messageID, content string) {
|
||||
if c.progress == nil {
|
||||
return
|
||||
}
|
||||
c.progress.Record(chatID, messageID, content)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) ClearToolFeedbackMessage(chatID string) {
|
||||
if c.progress == nil {
|
||||
return
|
||||
}
|
||||
c.progress.Clear(chatID)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) DismissToolFeedbackMessage(ctx context.Context, chatID string) {
|
||||
msgID, ok := c.currentToolFeedbackMessage(chatID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.dismissTrackedToolFeedbackMessage(ctx, chatID, msgID)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) dismissTrackedToolFeedbackMessage(ctx context.Context, chatID, messageID string) {
|
||||
if strings.TrimSpace(chatID) == "" || strings.TrimSpace(messageID) == "" {
|
||||
return
|
||||
}
|
||||
c.ClearToolFeedbackMessage(chatID)
|
||||
_ = c.DeleteMessage(ctx, chatID, messageID)
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) finalizeTrackedToolFeedbackMessage(
|
||||
ctx context.Context,
|
||||
chatID string,
|
||||
content string,
|
||||
editFn func(context.Context, string, string, string) error,
|
||||
) ([]string, bool) {
|
||||
msgID, baseContent, ok := c.takeToolFeedbackMessage(chatID)
|
||||
if !ok || editFn == nil {
|
||||
return nil, false
|
||||
}
|
||||
if err := editFn(ctx, chatID, msgID, content); err != nil {
|
||||
c.RecordToolFeedbackMessage(chatID, msgID, baseContent)
|
||||
return nil, false
|
||||
}
|
||||
return []string{msgID}, true
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) FinalizeToolFeedbackMessage(ctx context.Context, msg bus.OutboundMessage) ([]string, bool) {
|
||||
if outboundMessageIsToolFeedback(msg) {
|
||||
return nil, false
|
||||
}
|
||||
return c.finalizeTrackedToolFeedbackMessage(ctx, msg.ChatID, msg.Content, c.EditMessage)
|
||||
}
|
||||
|
||||
// ReactToMessage implements channels.ReactionCapable.
|
||||
// Adds a reaction (randomly chosen from config) and returns an undo function to remove it.
|
||||
func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
|
||||
@@ -459,7 +323,6 @@ func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMess
|
||||
if !c.IsRunning() {
|
||||
return nil, channels.ErrNotRunning
|
||||
}
|
||||
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
|
||||
|
||||
if msg.ChatID == "" {
|
||||
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||
@@ -476,10 +339,6 @@ func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMess
|
||||
}
|
||||
}
|
||||
|
||||
if hasTrackedMsg {
|
||||
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -942,7 +801,7 @@ func appendMediaTags(content, messageType string, mediaRefs []string) string {
|
||||
}
|
||||
|
||||
// sendCard sends an interactive card message to a chat.
|
||||
func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string) (string, error) {
|
||||
func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string) error {
|
||||
req := larkim.NewCreateMessageReqBuilder().
|
||||
ReceiveIdType(larkim.ReceiveIdTypeChatId).
|
||||
Body(larkim.NewCreateMessageReqBodyBuilder().
|
||||
@@ -954,26 +813,23 @@ func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string
|
||||
|
||||
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("feishu send card: %w", channels.ErrTemporary)
|
||||
return fmt.Errorf("feishu send card: %w", channels.ErrTemporary)
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
c.invalidateTokenOnAuthError(resp.Code)
|
||||
return "", fmt.Errorf("feishu api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
||||
return fmt.Errorf("feishu api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
||||
}
|
||||
|
||||
logger.DebugCF("feishu", "Feishu card message sent", map[string]any{
|
||||
"chat_id": chatID,
|
||||
})
|
||||
|
||||
if resp.Data != nil && resp.Data.MessageId != nil {
|
||||
return *resp.Data.MessageId, nil
|
||||
}
|
||||
return "", nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendText sends a plain text message to a chat (fallback when card fails).
|
||||
func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) (string, error) {
|
||||
func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) error {
|
||||
content, _ := json.Marshal(map[string]string{"text": text})
|
||||
|
||||
req := larkim.NewCreateMessageReqBuilder().
|
||||
@@ -987,21 +843,18 @@ func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) (stri
|
||||
|
||||
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("feishu send text: %w", channels.ErrTemporary)
|
||||
return fmt.Errorf("feishu send text: %w", channels.ErrTemporary)
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
||||
return fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
||||
}
|
||||
|
||||
logger.DebugCF("feishu", "Feishu text message sent (fallback)", map[string]any{
|
||||
"chat_id": chatID,
|
||||
})
|
||||
|
||||
if resp.Data != nil && resp.Data.MessageId != nil {
|
||||
return *resp.Data.MessageId, nil
|
||||
}
|
||||
return "", nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendImage uploads an image and sends it as a message.
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
package feishu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
)
|
||||
|
||||
func TestExtractContent(t *testing.T) {
|
||||
@@ -283,84 +279,3 @@ func TestExtractFeishuSenderID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeTrackedToolFeedbackMessage_ClearAfterSuccessfulEdit(t *testing.T) {
|
||||
ch := &FeishuChannel{
|
||||
progress: channels.NewToolFeedbackAnimator(nil),
|
||||
}
|
||||
ch.RecordToolFeedbackMessage("chat-1", "msg-1", "🔧 `read_file`")
|
||||
|
||||
msgIDs, handled := ch.finalizeTrackedToolFeedbackMessage(
|
||||
context.Background(),
|
||||
"chat-1",
|
||||
"final reply",
|
||||
func(_ context.Context, chatID, messageID, content string) error {
|
||||
if chatID != "chat-1" || messageID != "msg-1" || content != "final reply" {
|
||||
t.Fatalf("unexpected edit args: %s %s %s", chatID, messageID, content)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if !handled {
|
||||
t.Fatal("expected finalizeTrackedToolFeedbackMessage to handle tracked message")
|
||||
}
|
||||
if len(msgIDs) != 1 || msgIDs[0] != "msg-1" {
|
||||
t.Fatalf("unexpected msgIDs: %v", msgIDs)
|
||||
}
|
||||
if _, ok := ch.currentToolFeedbackMessage("chat-1"); ok {
|
||||
t.Fatal("expected tracked tool feedback to be cleared after successful edit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
|
||||
ch := &FeishuChannel{
|
||||
progress: channels.NewToolFeedbackAnimator(nil),
|
||||
}
|
||||
ch.RecordToolFeedbackMessage("chat-1", "msg-1", "🔧 `read_file`")
|
||||
|
||||
msgIDs, handled := ch.finalizeTrackedToolFeedbackMessage(
|
||||
context.Background(),
|
||||
"chat-1",
|
||||
"final reply",
|
||||
func(_ context.Context, chatID, messageID, content string) error {
|
||||
if _, ok := ch.currentToolFeedbackMessage(chatID); ok {
|
||||
t.Fatal("expected tracked tool feedback to be stopped before edit")
|
||||
}
|
||||
if chatID != "chat-1" || messageID != "msg-1" || content != "final reply" {
|
||||
t.Fatalf("unexpected edit args: %s %s %s", chatID, messageID, content)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if !handled {
|
||||
t.Fatal("expected finalizeTrackedToolFeedbackMessage to handle tracked message")
|
||||
}
|
||||
if len(msgIDs) != 1 || msgIDs[0] != "msg-1" {
|
||||
t.Fatalf("unexpected msgIDs: %v", msgIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeTrackedToolFeedbackMessage_EditFailureKeepsTrackedMessage(t *testing.T) {
|
||||
ch := &FeishuChannel{
|
||||
progress: channels.NewToolFeedbackAnimator(nil),
|
||||
}
|
||||
ch.RecordToolFeedbackMessage("chat-1", "msg-1", "🔧 `read_file`")
|
||||
|
||||
msgIDs, handled := ch.finalizeTrackedToolFeedbackMessage(
|
||||
context.Background(),
|
||||
"chat-1",
|
||||
"final reply",
|
||||
func(context.Context, string, string, string) error {
|
||||
return errors.New("edit failed")
|
||||
},
|
||||
)
|
||||
if handled {
|
||||
t.Fatal("expected finalizeTrackedToolFeedbackMessage to report unhandled on edit failure")
|
||||
}
|
||||
if len(msgIDs) != 0 {
|
||||
t.Fatalf("unexpected msgIDs: %v", msgIDs)
|
||||
}
|
||||
if msgID, ok := ch.currentToolFeedbackMessage("chat-1"); !ok || msgID != "msg-1" {
|
||||
t.Fatalf("expected tracked tool feedback to remain after failed edit, got (%q, %v)", msgID, ok)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user