package weixin import ( "context" "encoding/base64" "encoding/hex" "fmt" "log/slog" "mime" "path/filepath" "strings" "time" "github.com/chenhg5/cc-connect/core" ) func imageDecryptMaterial(img *imageItem) (encParam, aesKeyBase64 string, ok bool) { if img == nil || img.Media == nil { return "", "", false } encParam = strings.TrimSpace(img.Media.EncryptQueryParam) if encParam == "" { return "", "", false } if hx := strings.TrimSpace(img.AESKeyHex); hx != "" { raw, err := hex.DecodeString(hx) if err == nil && len(raw) == 16 { return encParam, base64.StdEncoding.EncodeToString(raw), true } } if k := strings.TrimSpace(img.Media.AESKey); k != "" { return encParam, k, true } return encParam, "", false } func (p *Platform) collectInboundMedia(ctx context.Context, items []messageItem) (images []core.ImageAttachment, files []core.FileAttachment, audio *core.AudioAttachment) { if p == nil || len(items) == 0 || strings.TrimSpace(p.cdnBaseURL) == "" { return nil, nil, nil } dlCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() client := p.cdnHttpClient if client == nil { client = p.httpClient } base := p.cdnBaseURL // Deduplicate identical CDN references within one message (duplicate items / retries). seenEnc := make(map[string]struct{}) tryEnc := func(enc string) bool { if enc == "" { return false } if _, ok := seenEnc[enc]; ok { return false } seenEnc[enc] = struct{}{} return true } var extraVoiceN int for _, it := range items { switch it.Type { case messageItemImage: img := it.ImageItem enc, keyB64, hasKey := imageDecryptMaterial(img) if enc == "" || !tryEnc(enc) { continue } var buf []byte var err error if hasKey && keyB64 != "" { buf, err = downloadAndDecryptCDN(dlCtx, client, base, enc, keyB64, "weixin inbound image") } else { buf, err = downloadPlainCDN(dlCtx, client, base, enc, "weixin inbound image-plain") } if err != nil { slog.Warn("weixin: inbound image CDN failed", "error", err) continue } mt := detectImageMime(buf) images = append(images, core.ImageAttachment{MimeType: mt, Data: buf}) case messageItemFile: f := it.FileItem if f == nil || f.Media == nil { continue } enc := strings.TrimSpace(f.Media.EncryptQueryParam) keyB64 := strings.TrimSpace(f.Media.AESKey) if enc == "" || keyB64 == "" || !tryEnc(enc) { continue } buf, err := downloadAndDecryptCDN(dlCtx, client, base, enc, keyB64, "weixin inbound file") if err != nil { slog.Warn("weixin: inbound file CDN failed", "error", err) continue } name := strings.TrimSpace(f.FileName) if name == "" { name = "attachment.bin" } mt := mime.TypeByExtension(filepath.Ext(name)) if mt == "" { mt = "application/octet-stream" } files = append(files, core.FileAttachment{MimeType: mt, Data: buf, FileName: name}) case messageItemVideo: v := it.VideoItem if v == nil || v.Media == nil { continue } enc := strings.TrimSpace(v.Media.EncryptQueryParam) keyB64 := strings.TrimSpace(v.Media.AESKey) if enc == "" || keyB64 == "" || !tryEnc(enc) { continue } buf, err := downloadAndDecryptCDN(dlCtx, client, base, enc, keyB64, "weixin inbound video") if err != nil { slog.Warn("weixin: inbound video CDN failed", "error", err) continue } files = append(files, core.FileAttachment{MimeType: "video/mp4", Data: buf, FileName: "video.mp4"}) case messageItemVoice: v := it.VoiceItem if v == nil || v.Media == nil { continue } if strings.TrimSpace(v.Text) != "" { // WeChat ASR text is enough when present; avoid STT path / duplicate handling. continue } enc := strings.TrimSpace(v.Media.EncryptQueryParam) keyB64 := strings.TrimSpace(v.Media.AESKey) if enc == "" || keyB64 == "" || !tryEnc(enc) { continue } buf, err := downloadAndDecryptCDN(dlCtx, client, base, enc, keyB64, "weixin inbound voice") if err != nil { slog.Warn("weixin: inbound voice CDN failed", "error", err) continue } a := &core.AudioAttachment{ MimeType: "audio/silk", Data: buf, Format: "silk", } if audio == nil { audio = a } else { // core.Message carries one Audio; extra raw voice segments go as file attachments for the agent. extraVoiceN++ files = append(files, core.FileAttachment{ MimeType: "audio/silk", Data: buf, FileName: fmt.Sprintf("voice_%d.silk", extraVoiceN), }) } } } return images, files, audio }