mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: 企业微信应用 支持主动消息推送,并优化企微应用、微信公众号、微信客服音频相关的处理 (#4998)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}。")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user