mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 14:15:00 +00:00
* fix: resolve Zalo group name to real chat ID, notify origin on failed forward Two related root causes behind "forward message to group by name" silently failing while the agent reports success: 1. No tool let the agent resolve a group's display name (e.g. "Ban Dieu Hanh") to the chat ID the message tool actually requires. sessions_list only exposes session keys (numeric group IDs), never human-readable names, so the agent had no reliable way to turn a name into a real target and ended up passing the display name itself as `target`. Adds zalo_list_groups, wrapping the already-used (dashboard picker) protocol.FetchGroups behind the same optional-interface pattern as list_group_members/GroupMemberProvider (GroupListProvider on channels.Manager, gated to zalo_personal via RequiredChannelTypes). 2. When the resulting send fails downstream (e.g. Zalo rejects a bad chat_id), dispatchOutbound only ever retried/notified media failures on the same (already-broken) destination, and dropped text-only failures entirely — even though message.go's own postCrossTargetNotice comment states forwards must never announce a fake delivery. Because the bus publish is fire-and-forget, the tool had already returned "sent" and announced success to the origin chat before the real send was even attempted. message.go now tags cross-target forwards with origin channel/chat in OutboundMessage.Metadata; dispatchOutbound uses it to notify the ORIGIN chat with the real failure instead of silently dropping it or retrying against the same invalid target. * test: cover forward-origin metadata tagging and dispatch failure notice Extracts dispatchOutbound's error branch into handleSendFailure so it can be unit tested without driving the consumer loop/goroutine, and adds coverage for: forward failures notifying the origin chat (not the broken destination), pre-existing non-forward media/text-only behavior staying unchanged, message.go tagging cross-target group forwards with origin metadata alongside group_id, and the new zalo_list_groups tool/Manager delegator.
79 lines
2.5 KiB
Go
79 lines
2.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/channels"
|
|
)
|
|
|
|
// GroupLister abstracts listing the groups a channel account belongs to.
|
|
// Implemented by channels.Manager.ListGroups.
|
|
type GroupLister func(ctx context.Context, channel string) ([]channels.GroupInfo, error)
|
|
|
|
// GroupListerAware tools can receive a group lister function.
|
|
type GroupListerAware interface {
|
|
SetGroupLister(GroupLister)
|
|
}
|
|
|
|
// ListGroupsTool lets the agent resolve a group's real chat ID from its
|
|
// display name before sending/forwarding a message to it. Without this,
|
|
// the only signal available is sessions_list (which exposes session keys,
|
|
// not human-readable names), so the agent has no reliable way to turn
|
|
// "the Ban Điều Hành group" into the ID the message tool actually requires.
|
|
type ListGroupsTool struct {
|
|
lister GroupLister
|
|
}
|
|
|
|
func NewListGroupsTool() *ListGroupsTool { return &ListGroupsTool{} }
|
|
|
|
func (t *ListGroupsTool) SetGroupLister(l GroupLister) { t.lister = l }
|
|
|
|
func (t *ListGroupsTool) RequiredChannelTypes() []string { return []string{"zalo_personal"} }
|
|
|
|
func (t *ListGroupsTool) Name() string { return "zalo_list_groups" }
|
|
func (t *ListGroupsTool) Description() string {
|
|
return "List all Zalo groups the connected account belongs to (group_id + display name + member count). Use this BEFORE sending/forwarding a message to a group by name — the message tool's `target` requires a real group_id, not a display name; passing the name directly will fail or silently go nowhere."
|
|
}
|
|
|
|
func (t *ListGroupsTool) Parameters() map[string]any {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{},
|
|
}
|
|
}
|
|
|
|
func (t *ListGroupsTool) Execute(ctx context.Context, args map[string]any) *Result {
|
|
if t.lister == nil {
|
|
return ErrorResult("zalo_list_groups: no group lister available")
|
|
}
|
|
|
|
channel := ToolChannelFromCtx(ctx)
|
|
if channel == "" {
|
|
return ErrorResult("channel is required (no current channel in context)")
|
|
}
|
|
|
|
groups, err := t.lister(ctx, channel)
|
|
if err != nil {
|
|
return ErrorResult(fmt.Sprintf("failed to list groups: %v", err))
|
|
}
|
|
|
|
type groupOut struct {
|
|
GroupID string `json:"group_id"`
|
|
Name string `json:"name"`
|
|
TotalMember int `json:"total_member,omitempty"`
|
|
}
|
|
out := make([]groupOut, len(groups))
|
|
for i, g := range groups {
|
|
out[i] = groupOut{GroupID: g.GroupID, Name: g.Name, TotalMember: g.TotalMember}
|
|
}
|
|
|
|
data, _ := json.Marshal(map[string]any{
|
|
"channel": channel,
|
|
"count": len(out),
|
|
"groups": out,
|
|
})
|
|
return NewResult(string(data))
|
|
}
|