{"record":{"id":"70e166d0b8ea5780","repo":"sipeed/picoclaw","slug":"weixin-send-media-w","errorCode":null,"errorMessage":"weixin send media: %w","messagePattern":"weixin send media: %w","errorType":"exception","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/weixin/media.go","lineNumber":1128,"sourceCode":"\t\tcontextToken, _ = v.(string)\n\t}\n\tif contextToken == \"\" {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"weixin send media: missing context token for chat %s: %w\",\n\t\t\tmsg.ChatID,\n\t\t\tbasechannels.ErrSendFailed,\n\t\t)\n\t}\n\n\tfor _, part := range msg.Parts {\n\t\tlocalPath, filename, contentType, cleanup, err := c.resolveOutboundPart(ctx, part)\n\t\tif err != nil {\n\t\t\tlogger.ErrorCF(\"weixin\", \"Failed to resolve outbound media\", map[string]any{\n\t\t\t\t\"chat_id\": msg.ChatID,\n\t\t\t\t\"ref\":     part.Ref,\n\t\t\t\t\"error\":   err.Error(),\n\t\t\t})\n\t\t\treturn nil, fmt.Errorf(\"weixin send media: %w\", basechannels.ErrSendFailed)\n\t\t}\n\t\tfunc() {\n\t\t\tif cleanup != nil {\n\t\t\t\tdefer cleanup()\n\t\t\t}\n\n\t\t\tkind := outboundMediaKind(part.Type, filename, contentType)\n\t\t\tuploaded, uploadErr := c.uploadLocalFile(ctx, localPath, filename, msg.ChatID, kind)\n\t\t\tif uploadErr != nil {\n\t\t\t\terr = uploadErr\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = c.sendUploadedMedia(ctx, msg.ChatID, contextToken, part.Caption, kind, uploaded)\n\t\t}()\n\t\tif err != nil {\n\t\t\tlogger.ErrorCF(\"weixin\", \"Failed to send outbound media\", map[string]any{\n\t\t\t\t\"chat_id\": msg.ChatID,\n\t\t\t\t\"ref\":     part.Ref,","sourceCodeStart":1110,"sourceCodeEnd":1146,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/weixin/media.go#L1110-L1146","documentation":"While iterating media parts of a Weixin outbound message, resolveOutboundPart failed to materialize one part's Ref into a local file, so the whole media send aborts, wrapping basechannels.ErrSendFailed. The failing ref is logged (chat_id, ref, error) before returning. Unlike the WeCom channel (error 665's sibling), this path does not fall back to a placeholder — the first unresolvable part stops the entire send.","triggerScenarios":"Any part in msg.Parts whose Ref cannot be resolved: attachment store miss, expired/signed URL, unreachable storage, deleted cache file. The loop at media.go:1118 returns immediately on the first failure.","commonSituations":"Agent attaches a stale ref from an earlier turn whose backing file was evicted; media URL expired between generation and send; object store briefly down; local tmp cleaned by an aggressive janitor between sessions.","solutions":["Pre-validate each part's ref (store lookup or URL HEAD) before calling SendMedia\nRe-fetch or re-upload the referenced asset and retry the send\nDrop unresolvable parts and send the remainder plus caption text\nFix storage retention: keep resolved files until the send completes (the cleanup func pattern implies files may be temp)"],"exampleFix":"// before\nch.SendMedia(ctx, msg) // contains a dead ref -> whole send aborts (error 678)\n\n// after\nparts := filterParts(msg.Parts, func(p MediaPart) bool {\n    _, err := store.Stat(p.Ref)\n    return err == nil\n})\nmsg.Parts = parts\nch.SendMedia(ctx, msg)","handlingStrategy":"try-catch","validationCode":"// pre-resolve every part before invoking SendMedia\nfor _, part := range msg.Parts {\n    if strings.TrimSpace(part.Ref) == \"\" {\n        continue\n    }\n    if _, _, _, cleanup, err := ch.ResolveOutboundPart(ctx, part); err != nil {\n        msg.Parts = dropPart(msg.Parts, part.Ref) // drop instead of failing the whole send\n    } else if cleanup != nil {\n        cleanup()\n    }\n}","typeGuard":"func isWeixinMediaResolveFail(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"weixin send media:\") && errors.Is(err, basechannels.ErrSendFailed) && strings.Contains(err.Error(), \"resolve\")\n}","tryCatchPattern":"if _, err := ch.SendMedia(ctx, msg); err != nil {\n    if isWeixinMediaResolveFail(err) {\n        // deterministic: filter bad refs, resend remaining parts (captions still go out)\n        msg.Parts = filterParts(msg.Parts, alive)\n        _, err = ch.SendMedia(ctx, msg)\n    }\n}","preventionTips":["Stat/HEAD each ref before building the outbound message","Expire-tune signed media URLs so they outlive send latency by a wide margin","Keep resolved temp files until the send finishes; clean only via the provided cleanup func","Send the first dead part alone shouldn't kill the batch — filter at the application layer"],"tags":["weixin","media","resolution","attachment","send-failed"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}