feat: 企业微信应用 支持主动消息推送,并优化企微应用、微信公众号、微信客服音频相关的处理 (#4998)

This commit is contained in:
Soulter
2026-02-09 22:15:11 +08:00
committed by GitHub
parent 67b81c279b
commit 04faf26140
5 changed files with 206 additions and 87 deletions

View File

@@ -26,6 +26,7 @@ from astrbot.api.platform import (
from astrbot.core import logger
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.utils.media_utils import convert_audio_to_wav
from astrbot.core.utils.webhook_utils import log_webhook_info
from .wecom_event import WecomPlatformEvent
@@ -165,6 +166,7 @@ class WecomPlatformAdapter(Platform):
self.api_base_url += "/"
self.server = WecomServer(self._event_queue, self.config)
self.agent_id: str | None = None
self.client = WeChatClient(
self.config["corpid"].strip(),
@@ -215,6 +217,36 @@ class WecomPlatformAdapter(Platform):
session: MessageSesion,
message_chain: MessageChain,
) -> None:
# 企业微信客服不支持主动发送
if hasattr(self.client, "kf_message"):
logger.warning("企业微信客服模式不支持 send_by_session 主动发送。")
await super().send_by_session(session, message_chain)
return
if not self.agent_id:
logger.warning(
f"send_by_session 失败:无法为会话 {session.session_id} 推断 agent_id。",
)
await super().send_by_session(session, message_chain)
return
message_obj = AstrBotMessage()
message_obj.self_id = self.agent_id
message_obj.session_id = session.session_id
message_obj.type = session.message_type
message_obj.sender = MessageMember(session.session_id, session.session_id)
message_obj.message = []
message_obj.message_str = ""
message_obj.message_id = uuid.uuid4().hex
message_obj.raw_message = {"_proactive_send": True}
event = WecomPlatformEvent(
message_str=message_obj.message_str,
message_obj=message_obj,
platform_meta=self.meta(),
session_id=message_obj.session_id,
client=self.client,
)
await event.send(message_chain)
await super().send_by_session(session, message_chain)
@override
@@ -318,11 +350,8 @@ class WecomPlatformAdapter(Platform):
f.write(resp.content)
try:
from pydub import AudioSegment
path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav")
audio = AudioSegment.from_file(path)
audio.export(path_wav, format="wav")
path_wav = await convert_audio_to_wav(path, path_wav)
except Exception as e:
logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。")
path_wav = path
@@ -344,6 +373,7 @@ class WecomPlatformAdapter(Platform):
logger.warning(f"暂未实现的事件: {msg.type}")
return
self.agent_id = abm.self_id
logger.info(f"abm: {abm}")
await self.handle_msg(abm)
@@ -388,11 +418,8 @@ class WecomPlatformAdapter(Platform):
f.write(resp.content)
try:
from pydub import AudioSegment
path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav")
audio = AudioSegment.from_file(path)
audio.export(path_wav, format="wav")
path_wav = await convert_audio_to_wav(path, path_wav)
except Exception as e:
logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。")
path_wav = path

View File

@@ -1,24 +1,16 @@
import asyncio
import os
import uuid
from wechatpy.enterprise import WeChatClient
from astrbot.api import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import Image, Plain, Record
from astrbot.api.message_components import File, Image, Plain, Record, Video
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.utils.media_utils import convert_audio_to_amr
from .wecom_kf_message import WeChatKFMessage
try:
import pydub
except Exception:
logger.warning(
"检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 -> 平台日志 -> 安装 Pip 库安装 pydub。",
)
class WecomPlatformEvent(AstrMessageEvent):
def __init__(
@@ -125,25 +117,66 @@ class WecomPlatformEvent(AstrMessageEvent):
)
elif isinstance(comp, Record):
record_path = await comp.convert_to_file_path()
# 转成amr
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
record_path_amr = os.path.join(temp_dir, f"{uuid.uuid4()}.amr")
pydub.AudioSegment.from_wav(record_path).export(
record_path_amr,
format="amr",
)
record_path_amr = await convert_audio_to_amr(record_path)
with open(record_path_amr, "rb") as f:
try:
with open(record_path_amr, "rb") as f:
try:
response = self.client.media.upload("voice", f)
except Exception as e:
logger.error(f"微信客服上传语音失败: {e}")
await self.send(
MessageChain().message(
f"微信客服上传语音失败: {e}"
),
)
return
logger.info(f"微信客服上传语音返回: {response}")
kf_message_api.send_voice(
user_id,
self.get_self_id(),
response["media_id"],
)
finally:
if record_path_amr != record_path and os.path.exists(
record_path_amr,
):
try:
os.remove(record_path_amr)
except OSError as e:
logger.warning(f"删除临时音频文件失败: {e}")
elif isinstance(comp, File):
file_path = await comp.get_file()
with open(file_path, "rb") as f:
try:
response = self.client.media.upload("voice", f)
response = self.client.media.upload("file", f)
except Exception as e:
logger.error(f"微信客服上传语音失败: {e}")
logger.error(f"微信客服上传文件失败: {e}")
await self.send(
MessageChain().message(f"微信客服上传语音失败: {e}"),
MessageChain().message(f"微信客服上传文件失败: {e}"),
)
return
logger.info(f"微信客服上传语音返回: {response}")
kf_message_api.send_voice(
logger.debug(f"微信客服上传文件返回: {response}")
kf_message_api.send_file(
user_id,
self.get_self_id(),
response["media_id"],
)
elif isinstance(comp, Video):
video_path = await comp.convert_to_file_path()
with open(video_path, "rb") as f:
try:
response = self.client.media.upload("video", f)
except Exception as e:
logger.error(f"微信客服上传视频失败: {e}")
await self.send(
MessageChain().message(f"微信客服上传视频失败: {e}"),
)
return
logger.debug(f"微信客服上传视频返回: {response}")
kf_message_api.send_video(
user_id,
self.get_self_id(),
response["media_id"],
@@ -183,25 +216,66 @@ class WecomPlatformEvent(AstrMessageEvent):
)
elif isinstance(comp, Record):
record_path = await comp.convert_to_file_path()
# 转成amr
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
record_path_amr = os.path.join(temp_dir, f"{uuid.uuid4()}.amr")
pydub.AudioSegment.from_wav(record_path).export(
record_path_amr,
format="amr",
)
record_path_amr = await convert_audio_to_amr(record_path)
with open(record_path_amr, "rb") as f:
try:
with open(record_path_amr, "rb") as f:
try:
response = self.client.media.upload("voice", f)
except Exception as e:
logger.error(f"企业微信上传语音失败: {e}")
await self.send(
MessageChain().message(
f"企业微信上传语音失败: {e}"
),
)
return
logger.info(f"企业微信上传语音返回: {response}")
self.client.message.send_voice(
message_obj.self_id,
message_obj.session_id,
response["media_id"],
)
finally:
if record_path_amr != record_path and os.path.exists(
record_path_amr,
):
try:
os.remove(record_path_amr)
except OSError as e:
logger.warning(f"删除临时音频文件失败: {e}")
elif isinstance(comp, File):
file_path = await comp.get_file()
with open(file_path, "rb") as f:
try:
response = self.client.media.upload("voice", f)
response = self.client.media.upload("file", f)
except Exception as e:
logger.error(f"企业微信上传语音失败: {e}")
logger.error(f"企业微信上传文件失败: {e}")
await self.send(
MessageChain().message(f"企业微信上传语音失败: {e}"),
MessageChain().message(f"企业微信上传文件失败: {e}"),
)
return
logger.info(f"企业微信上传语音返回: {response}")
self.client.message.send_voice(
logger.debug(f"企业微信上传文件返回: {response}")
self.client.message.send_file(
message_obj.self_id,
message_obj.session_id,
response["media_id"],
)
elif isinstance(comp, Video):
video_path = await comp.convert_to_file_path()
with open(video_path, "rb") as f:
try:
response = self.client.media.upload("video", f)
except Exception as e:
logger.error(f"企业微信上传视频失败: {e}")
await self.send(
MessageChain().message(f"企业微信上传视频失败: {e}"),
)
return
logger.debug(f"企业微信上传视频返回: {response}")
self.client.message.send_video(
message_obj.self_id,
message_obj.session_id,
response["media_id"],

View File

@@ -24,6 +24,7 @@ from astrbot.api.platform import (
)
from astrbot.core import logger
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.utils.media_utils import convert_audio_to_wav
from astrbot.core.utils.webhook_utils import log_webhook_info
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
@@ -294,14 +295,11 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
f.write(resp.content)
try:
from pydub import AudioSegment
path_wav = f"data/temp/wecom_{msg.media_id}.wav"
audio = AudioSegment.from_file(path)
audio.export(path_wav, format="wav")
path_wav = await convert_audio_to_wav(path, path_wav)
except Exception as e:
logger.error(
f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。",
f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。",
)
path_wav = path
return

View File

@@ -1,5 +1,5 @@
import asyncio
import uuid
import os
from typing import cast
from wechatpy import WeChatClient
@@ -9,13 +9,7 @@ from astrbot.api import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import Image, Plain, Record
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
try:
import pydub
except Exception:
logger.warning(
"检测到 pydub 库未安装,微信公众平台将无法语音收发。如需使用语音,请前往管理面板 -> 平台日志 -> 安装 Pip 库安装 pydub。",
)
from astrbot.core.utils.media_utils import convert_audio_to_amr
class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
@@ -137,38 +131,46 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
elif isinstance(comp, Record):
record_path = await comp.convert_to_file_path()
# 转成amr
record_path_amr = f"data/temp/{uuid.uuid4()}.amr"
pydub.AudioSegment.from_wav(record_path).export(
record_path_amr,
format="amr",
)
record_path_amr = await convert_audio_to_amr(record_path)
with open(record_path_amr, "rb") as f:
try:
response = self.client.media.upload("voice", f)
except Exception as e:
logger.error(f"微信公众平台上传语音失败: {e}")
await self.send(
MessageChain().message(f"微信公众平台上传语音失败: {e}"),
)
return
logger.info(f"微信公众平台上传语音返回: {response}")
try:
with open(record_path_amr, "rb") as f:
try:
response = self.client.media.upload("voice", f)
except Exception as e:
logger.error(f"微信公众平台上传语音失败: {e}")
await self.send(
MessageChain().message(
f"微信公众平台上传语音失败: {e}"
),
)
return
logger.info(f"微信公众平台上传语音返回: {response}")
if active_send_mode:
self.client.message.send_voice(
message_obj.sender.user_id,
response["media_id"],
)
else:
reply = VoiceReply(
media_id=response["media_id"],
message=cast(dict, self.message_obj.raw_message)["message"],
)
xml = reply.render()
future = cast(dict, self.message_obj.raw_message)["future"]
assert isinstance(future, asyncio.Future)
future.set_result(xml)
if active_send_mode:
self.client.message.send_voice(
message_obj.sender.user_id,
response["media_id"],
)
else:
reply = VoiceReply(
media_id=response["media_id"],
message=cast(dict, self.message_obj.raw_message)[
"message"
],
)
xml = reply.render()
future = cast(dict, self.message_obj.raw_message)["future"]
assert isinstance(future, asyncio.Future)
future.set_result(xml)
finally:
if record_path_amr != record_path and os.path.exists(
record_path_amr
):
try:
os.remove(record_path_amr)
except OSError as e:
logger.warning(f"删除临时音频文件失败: {e}")
else:
logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}")

View File

@@ -259,6 +259,24 @@ async def convert_audio_format(
raise Exception("ffmpeg not found")
async def convert_audio_to_amr(audio_path: str, output_path: str | None = None) -> str:
"""将音频转换为amr格式。"""
return await convert_audio_format(
audio_path=audio_path,
output_format="amr",
output_path=output_path,
)
async def convert_audio_to_wav(audio_path: str, output_path: str | None = None) -> str:
"""将音频转换为wav格式。"""
return await convert_audio_format(
audio_path=audio_path,
output_format="wav",
output_path=output_path,
)
async def extract_video_cover(
video_path: str,
output_path: str | None = None,