Skip to content

fix: 修复企业微信(WeCom,Aibot)完整实现多模态消息处理功能#1478

Closed
opcache wants to merge 1 commit intosipeed:mainfrom
opcache:fix/issue-1210-wecom-aibot
Closed

fix: 修复企业微信(WeCom,Aibot)完整实现多模态消息处理功能#1478
opcache wants to merge 1 commit intosipeed:mainfrom
opcache:fix/issue-1210-wecom-aibot

Conversation

@opcache
Copy link
Copy Markdown
Contributor

@opcache opcache commented Mar 13, 2026

📝 改动描述

为企业微信(WeCom)完整实现多模态消息处理功能,覆盖 App/Bot/AIBot 三个核心模块:

  1. AIBot 渠道:实现 AES-256-CBC 解密逻辑,支持图片消息的加密格式,使用消息级 aeskey 字段或配置 EncodingAESKey 进行解密;
  2. App 渠道:完善媒体文件下载逻辑(支持 MediaID 下载方式),适配 image、voice、video、file 等多模态消息类型;
  3. Bot 渠道:优化消息解密验证流程,确保企业微信应用收发消息的安全性;
  4. 双模式 Base64 解码:同时支持 URL-safe 和标准 Base64 编码的 AES 密钥,增强鲁棒性;
  5. 密钥回退机制:当消息中未提供 aeskey 时,自动使用配置文件中的 EncodingAESKey 作为回退;
  6. 混合消息支持:修复图片 + 文字混合消息的图片解密逻辑,确保所有场景都能正确处理;
  7. 清理调试代码:移除开发阶段的调试日志和临时文件保存逻辑。

🗣️ 改动类型

  • 🐞 Bug 修复(非破坏性改动,修复一个问题)
  • ✨ 新功能(非破坏性改动,新增功能)
  • 📖 文档更新
  • ⚡ 代码重构(无功能变更,接口变更)

🤖 AI 代码生成说明

  • 🤖 完全由 AI 生成(100% AI,0% 人工)
  • 🛠️ 主要由 AI 生成(AI 草稿,人工验证/修改)
  • 👨‍💻 主要由人工编写(人工主导,AI 辅助或无 AI 参与)

🔗 关联问题

无关联 issue

📚 技术背景

参考链接

改动原因

问题现象

  1. 用户向企业微信 AI Bot 发送图片时,Agent 回复 "I've completed processing but have no response to give"
  2. 日志显示图片下载后格式错误:image: unknown format
  3. 图片文件头不是标准的 PNG/JPG 格式,而是加密数据

根本原因

企业微信 AI Bot 返回的图片消息包含加密内容:

  • 每个图片消息有独立的 aeskey 字段(消息级别)
  • 图片数据使用 AES-256-CBC 加密,需要解密后才能获取原始图片
  • 原代码未实现解密逻辑,直接将加密数据当作图片处理

🧪 测试环境

  • 硬件: 台式机(Intel 酷睿系列,16GB 内存)
  • 操作系统: Ubuntu 22.04 LTS / Windows 10 22H2
  • 模型/服务商: 无(本次改动聚焦消息处理,未涉及 AI 模型)
  • 渠道: 企业微信 V4.1.2(App/Bot/AIBot 全场景)

Copilot AI review requested due to automatic review settings March 13, 2026 01:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

为企业微信(WeCom)渠道补齐多模态消息处理链路:在 Bot / App / AIBot 三种模式下,新增(或完善)图片/文件等媒体消息的下载、(AIBot 图片)解密、入站消息 mediaRefs 传递,从而让 Agent 能真正拿到可用的媒体内容。

Changes:

  • WeCom Bot:为 image/file/mixed 消息下载媒体并通过 mediaRefs 传递给 HandleMessage
  • WeCom App:对带 MediaId 的入站消息下载媒体并存入 MediaStore,同时在文本中追加媒体标签
  • WeCom AIBot:支持图片消息 aeskey 解密、mixed(text+image) 处理,并将媒体作为 mediaRefs 传入 Agent

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 20 comments.

File Description
pkg/channels/wecom/bot.go 为 Bot 渠道增加媒体下载与 mediaRefs 传递,并新增 URL 下载存储逻辑
pkg/channels/wecom/app.go 为 App 渠道增加基于 MediaId 的媒体下载/存储与内容标签追加
pkg/channels/wecom/aibot.go 为 AIBot 增加图片媒体下载、AES 解密、mixed 消息处理与媒体 refs 传递

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Generate temp file path
tempDir := os.TempDir()
mediaDir := filepath.Join(tempDir, "picoclaw_media", "wecom")
os.MkdirAll(mediaDir, 0o755)
Comment on lines +1334 to +1335
// Use default client - no custom redirects
resp, err := http.DefaultClient.Do(req)
Comment on lines +1756 to +1758
}
defer resp.Body.Close()

Comment on lines +654 to +669
// Handle media messages (download and store)
var mediaRefs []string
store := c.GetMediaStore()
if store != nil && msg.MediaId != "" {
mediaRefs = c.downloadInboundMedia(ctx, msg.MsgType, msg.MediaId, messageID, store)
}

// Append media tags to content
if len(mediaRefs) > 0 {
mediaTag := c.getMediaTag(msg.MsgType)
if content != "" {
content += "\n" + mediaTag
} else {
content = mediaTag
}
}
Comment on lines +681 to +683
// Create a unique filename to avoid collisions
uniqueFilename := fmt.Sprintf("%s-%d-%s", msgID, time.Now().Unix(), filename)
localPath := filepath.Join(mediaDir, uniqueFilename)
Comment on lines +684 to +714
// Download image and store in media store
var mediaRefs []string
store := c.GetMediaStore()
if store == nil {
logger.WarnCF("wecom_aibot", "MediaStore not available, image will not be processed", map[string]any{
"user_id": userID,
"chat_id": chatID,
"msg_id": msg.MsgID,
})
} else {
var ref string
scope := channels.BuildMediaScope("wecom_aibot", chatID, msg.MsgID)

// Try to download via media_id first (WeCom AI Bot callback directly provides media_id)
if mediaID != "" {
logger.InfoCF("wecom_aibot", "Trying to download image via media_id", map[string]any{
"media_id": mediaID,
"msg_id": msg.MsgID,
})
ref = c.downloadMediaFromMediaIDNoAuth(ctx, mediaID, "image", store, scope, msg.MsgID)
}

// Fallback to URL if media_id download failed or not available
if ref == "" && imageURL != "" {
logger.InfoCF("wecom_aibot", "Falling back to URL download", map[string]any{
"url": imageURL,
"msg_id": msg.MsgID,
"aes_key": aesKey,
})
ref = c.downloadMediaFromURL(ctx, imageURL, "image.jpg", aesKey, store, scope, msg.MsgID)
}
Comment on lines 361 to +413

// Extract content based on message type
var content string
var mediaRefs []string

scope := channels.BuildMediaScope("wecom", chatID, msg.MsgID)

switch msg.MsgType {
case "text":
content = msg.Text.Content
case "voice":
content = msg.Voice.Content // Voice to text content
case "mixed":
// For mixed messages, concatenate text items
// For mixed messages, process text and image items
for _, item := range msg.Mixed.MsgItem {
if item.MsgType == "text" {
content += item.Text.Content
}
}
case "image", "file":
// For image and file, we don't have text content
content = ""
// Download images from mixed messages
if store := c.GetMediaStore(); store != nil {
for _, item := range msg.Mixed.MsgItem {
if item.MsgType == "image" && item.Image.URL != "" {
ref := c.downloadMediaFromURL(ctx, item.Image.URL, "image.jpg", store, scope, msg.MsgID)
if ref != "" {
mediaRefs = append(mediaRefs, ref)
}
}
}
}
case "image":
// Download image from URL
if msg.Image.URL != "" {
if store := c.GetMediaStore(); store != nil {
ref := c.downloadMediaFromURL(ctx, msg.Image.URL, "image.jpg", store, scope, msg.MsgID)
if ref != "" {
mediaRefs = append(mediaRefs, ref)
}
}
}
content = "[image]"
case "file":
// Download file from URL
if msg.File.URL != "" {
if store := c.GetMediaStore(); store != nil {
ref := c.downloadMediaFromURL(ctx, msg.File.URL, "file", store, scope, msg.MsgID)
if ref != "" {
mediaRefs = append(mediaRefs, ref)
}
}
}
content = "[file]"
}
// Handle media messages (download and store)
var mediaRefs []string
store := c.GetMediaStore()
if store != nil && msg.MediaId != "" {
Comment on lines +786 to +794
// Determine file extension based on message type
var ext string
switch msgType {
case "image":
ext = ".jpg"
case "voice":
ext = ".amr"
case "video":
ext = ".mp4"
Comment on lines +817 to +821
// Store in media store
ref, err := store.Store(localPath, media.MediaMeta{
Filename: filename,
Source: "wecom_app",
}, scope)
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 13, 2026

CLA assistant check
All committers have signed the CLA.

xuwei-xy pushed a commit to xuwei-xy/picoclaw that referenced this pull request Mar 14, 2026
@xiajingjie
Copy link
Copy Markdown

xiajingjie commented Mar 17, 2026

@sipeed-bot
Copy link
Copy Markdown

sipeed-bot bot commented Apr 2, 2026

@opcache Hi! This PR has had no activity for over 2 weeks, so I'm closing it for now to keep things tidy. If it's still relevant, feel free to reopen it anytime and we'll pick it back up.

@sipeed-bot sipeed-bot bot closed this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: channel go Pull requests that update go code type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants