fix(pico): preserve image media across pico attachments and client

This commit is contained in:
lxowalle 2026-05-15 11:42:14 +08:00
parent 89631b8671
commit 18050872fc
3 changed files with 123 additions and 5 deletions

View file

@ -235,6 +235,8 @@ func (c *PicoClientChannel) handleInbound(pc *picoConn, msg PicoMessage) {
case TypeMessageCreate:
// Server sent us a message — treat as inbound
c.handleServerMessage(pc, msg)
case TypeMediaCreate:
c.handleServerMessage(pc, msg)
default:
logger.DebugCF("pico_client", "Ignoring message type", map[string]any{
"type": msg.Type,
@ -248,7 +250,14 @@ func (c *PicoClientChannel) handleServerMessage(pc *picoConn, msg PicoMessage) {
}
content, _ := msg.Payload[PayloadKeyContent].(string)
if strings.TrimSpace(content) == "" {
media, err := parseInlineImageMedia(msg.Payload)
if err != nil {
logger.WarnCF("pico_client", "Ignoring invalid media payload", map[string]any{
"error": err.Error(),
})
return
}
if strings.TrimSpace(content) == "" && len(media) == 0 {
return
}
@ -281,7 +290,7 @@ func (c *PicoClientChannel) handleServerMessage(pc *picoConn, msg PicoMessage) {
},
}
c.HandleInboundContext(c.ctx, chatID, content, nil, inboundCtx, sender)
c.HandleInboundContext(c.ctx, chatID, content, media, inboundCtx, sender)
}
// Send sends a message to the remote server.

View file

@ -285,6 +285,24 @@ func TestParseInlineImageMedia_Valid(t *testing.T) {
}
}
func TestParseInlineImageMedia_Attachments(t *testing.T) {
imageURL := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X2ioAAAAASUVORK5CYII="
media, err := parseInlineImageMedia(map[string]any{
"attachments": []any{
map[string]any{
"type": "image",
"url": imageURL,
},
},
})
if err != nil {
t.Fatalf("parseInlineImageMedia() error = %v", err)
}
if len(media) != 1 || media[0] != imageURL {
t.Fatalf("media = %#v, want attachment image payload", media)
}
}
func TestPicoChannel_HandleMessageSend_AllowsMediaOnly(t *testing.T) {
mb := bus.NewMessageBus()
bc := &config.Channel{Type: "pico", Enabled: true}
@ -326,6 +344,46 @@ func TestPicoChannel_HandleMessageSend_AllowsMediaOnly(t *testing.T) {
}
}
func TestPicoClientChannel_HandleServerMessage_ForwardsMedia(t *testing.T) {
mb := bus.NewMessageBus()
bc := &config.Channel{Type: config.ChannelPicoClient, Enabled: true}
ch, err := NewPicoClientChannel(bc, &config.PicoClientSettings{
URL: "ws://localhost:8080/ws",
}, mb)
if err != nil {
t.Fatalf("NewPicoClientChannel() error = %v", err)
}
ch.ctx = context.Background()
pc := &picoConn{sessionID: "sess-media"}
imageURL := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X2ioAAAAASUVORK5CYII="
ch.handleServerMessage(pc, PicoMessage{
Type: TypeMessageCreate,
Payload: map[string]any{
PayloadKeyContent: "describe this",
"attachments": []any{
map[string]any{
"type": "image",
"url": imageURL,
},
},
},
})
select {
case msg := <-mb.InboundChan():
if msg.Content != "describe this" {
t.Fatalf("msg.Content = %q, want describe this", msg.Content)
}
if len(msg.Media) != 1 || msg.Media[0] != imageURL {
t.Fatalf("msg.Media = %#v, want forwarded image payload", msg.Media)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for forwarded media message")
}
}
func TestIsThoughtPayload(t *testing.T) {
tests := []struct {
name string

View file

@ -990,11 +990,24 @@ func parseInlineImageMedia(payload map[string]any) ([]string, error) {
return nil, nil
}
raw, ok := payload["media"]
if !ok || raw == nil {
return nil, nil
media, err := parseInlineImageValues(payload["media"])
if err != nil {
return nil, err
}
attachments, err := parseInlineImageAttachments(payload["attachments"])
if err != nil {
return nil, err
}
media = append(media, attachments...)
return media, nil
}
func parseInlineImageValues(raw any) ([]string, error) {
if raw == nil {
return nil, nil
}
switch values := raw.(type) {
case []any:
media := make([]string, 0, len(values))
@ -1030,6 +1043,44 @@ func parseInlineImageMedia(payload map[string]any) ([]string, error) {
}
}
func parseInlineImageAttachments(raw any) ([]string, error) {
if raw == nil {
return nil, nil
}
values, ok := raw.([]any)
if !ok {
return nil, fmt.Errorf("attachments must be an array")
}
media := make([]string, 0, len(values))
for i, item := range values {
attachment, ok := item.(map[string]any)
if !ok {
return nil, fmt.Errorf("attachments[%d]: attachment must be an object", i)
}
attachmentType, _ := attachment["type"].(string)
attachmentType = strings.ToLower(strings.TrimSpace(attachmentType))
if attachmentType != "" && attachmentType != "image" {
continue
}
value, err := inlineImageValue(attachment)
if err != nil {
if attachmentType == "image" {
return nil, fmt.Errorf("attachments[%d]: %w", i, err)
}
continue
}
if err := validateInlineImageDataURL(value); err != nil {
return nil, fmt.Errorf("attachments[%d]: %w", i, err)
}
media = append(media, value)
}
return media, nil
}
func inlineImageValue(item any) (string, error) {
switch value := item.(type) {
case string: