{"record":{"id":"7ed13c6b9c36f3cd","repo":"sipeed/picoclaw","slug":"wecom-resolve-media-q-v-w","errorCode":null,"errorMessage":"wecom resolve media %q: %v: %w","messagePattern":"wecom resolve media %q: (.+?): %w","errorType":"exception","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/wecom/wecom.go","lineNumber":245,"sourceCode":"\troute, chatType, hasTurn := c.resolveMediaRoute(msg.ChatID)\n\tchatID := route.ChatID\n\tif chatID == \"\" {\n\t\tchatID = msg.ChatID\n\t}\n\n\tfor _, part := range msg.Parts {\n\t\tif strings.TrimSpace(part.Ref) == \"\" {\n\t\t\tif caption := strings.TrimSpace(part.Caption); caption != \"\" {\n\t\t\t\tif err := c.sendActivePush(chatID, chatType, caption); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tlocalPath, filename, contentType, cleanup, err := c.resolveOutboundPart(ctx, part)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"wecom resolve media %q: %v: %w\", part.Ref, err, channels.ErrSendFailed)\n\t\t}\n\n\t\tfunc() {\n\t\t\tif cleanup != nil {\n\t\t\t\tdefer cleanup()\n\t\t\t}\n\n\t\t\tuploaded, uploadErr := c.uploadOutboundMedia(ctx, localPath, filename, contentType, part)\n\t\t\tif uploadErr != nil {\n\t\t\t\tlogger.WarnCF(\"wecom\", \"Falling back to placeholder after media upload failure\", map[string]any{\n\t\t\t\t\t\"chat_id\":      chatID,\n\t\t\t\t\t\"ref\":          part.Ref,\n\t\t\t\t\t\"filename\":     filename,\n\t\t\t\t\t\"content_type\": contentType,\n\t\t\t\t\t\"error\":        uploadErr.Error(),\n\t\t\t\t})\n\t\t\t\tif hasTurn {\n\t\t\t\t\tif finishErr := c.sendStreamChunk(route, true, \"\"); finishErr != nil {","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/wecom/wecom.go#L227-L263","documentation":"Raised while preparing a WeCom outbound message: resolveOutboundPart failed to turn a message part's Ref into a (localPath, filename, contentType, cleanup) tuple — the referenced attachment could not be fetched/materialized locally. The error chains the underlying cause plus channels.ErrSendFailed, so upstream retry logic treats it as a permanent send failure. Only the ref-resolution step aborts; later upload failures fall back to a placeholder instead.","triggerScenarios":"A message part with a non-empty Ref whose backing asset cannot be resolved: unknown/expired reference in the attachment store, download of the referenced URL failing, storage backend unreachable, or the ref pointing at a file that no longer exists. The send loop at wecom.go:245 wraps any such failure with this message and stops processing remaining parts.","commonSituations":"The agent replies with an attachment ref produced in an earlier session that has since been evicted; the media URL is behind auth or has expired (signed URL TTL passed); local cache/tmp directory was cleaned; object store outage; a ref typo in a manually constructed message.","solutions":["Verify the ref exists in the attachment store before queueing the send (lookup/HEAD the ref)","If the ref is a URL, check that it is still valid and reachable from the host (curl it); re-upload or re-sign if expired","Ensure the media cache/storage directory the resolver uses is present and writable","Drop the broken part and resend — text parts without Refs still deliver (captions are sent as active pushes)","If refs routinely expire, shorten the gap between generation and send or persist the binary instead of the ref"],"exampleFix":"// before\nparts := []channels.MediaPart{{Ref: oldRef}} // oldRef no longer resolvable -> error 665\n\n// after\nif _, err := store.Stat(oldRef); err != nil {\n    parts = nil // drop dead ref, send text only\n}\nsend(parts...)","handlingStrategy":"try-catch","validationCode":"// resolve refs before building the outbound message\nfor _, part := range msg.Parts {\n    if strings.TrimSpace(part.Ref) == \"\" {\n        continue\n    }\n    if _, err := store.Stat(part.Ref); err != nil {\n        // for URL refs: HEAD the URL and check 200\n        return fmt.Errorf(\"part ref %q unresolvable: %w\", part.Ref, err)\n    }\n}","typeGuard":"func isWecomResolveMedia(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"wecom resolve media\")\n}","tryCatchPattern":"if _, err := ch.Send(ctx, msg); err != nil {\n    if isWecomResolveMedia(err) && errors.Is(err, channels.ErrSendFailed) {\n        // permanent: strip bad parts and resend text-only instead of retrying\n    }\n}","preventionTips":["Verify refs exist in the attachment store before queueing sends","Set adequate TTLs on signed media URLs relative to send latency","Retain resolved temp files until the send completes (cleanup runs post-upload)","Drop dead refs and still deliver caption/text parts rather than failing the whole message"],"tags":["wecom","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"}