Merge branch 'master' of https://github.com/AstrBotDevs/AstrBot into feat-add-volcengine-support

This commit is contained in:
YOO_koishi
2025-05-22 08:09:59 +08:00
8 changed files with 160 additions and 55 deletions
+7 -1
View File
@@ -5,7 +5,7 @@
import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "3.5.10"
VERSION = "3.5.11"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
# 默认配置
@@ -176,6 +176,7 @@ CONFIG_METADATA_2 = {
"api_base_url": "https://api.weixin.qq.com/cgi-bin/",
"callback_server_host": "0.0.0.0",
"port": 6194,
"active_send_mode": False
},
"wecom(企业微信)": {
"id": "wecom",
@@ -220,6 +221,11 @@ CONFIG_METADATA_2 = {
},
},
"items": {
"active_send_mode": {
"description": "是否换用主动发送接口",
"type": "bool",
"desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。"
},
"wpp_active_message_poll": {
"description": "是否启用主动消息轮询",
"type": "bool",
@@ -69,39 +69,42 @@ class WeChatPadProAdapter(Platform):
self.auth_key = loaded_credentials.get("auth_key")
self.wxid = loaded_credentials.get("wxid")
isLoginIn = await self.check_online_status()
# 检查在线状态
if self.auth_key and await self.check_online_status():
logger.info("WeChatPadPro 设备已在线,跳过扫码登录。")
if self.auth_key and isLoginIn:
logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。")
# 如果在线,连接 WebSocket 接收消息
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
else:
logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。")
# 1. 生成授权码
await self.generate_auth_key()
if not self.auth_key:
logger.error("无法获取授权码,WeChatPadPro 适配器启动失败")
return
logger.info("WeChatPadPro 无可用凭据,将生成新的授权码")
await self.generate_auth_key()
# 2. 获取登录二维码
qr_code_url = await self.get_login_qr_code()
if not isLoginIn:
logger.info("WeChatPadPro 设备已离线,开始扫码登录。")
qr_code_url = await self.get_login_qr_code()
if qr_code_url:
logger.info(f"请扫描以下二维码登录: {qr_code_url}")
else:
logger.error("无法获取登录二维码。")
return
if qr_code_url:
logger.info(f"请扫描以下二维码登录: {qr_code_url}")
else:
logger.error("无法获取登录二维码。")
return
# 3. 检测扫码状态
login_successful = await self.check_login_status()
# 3. 检测扫码状态
login_successful = await self.check_login_status()
if login_successful:
# 登录成功后,连接 WebSocket 接收消息
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
else:
logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
await self.terminate()
return
if login_successful:
logger.info("登录成功,WeChatPadPro适配器已连接。")
else:
logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
await self.terminate()
return
# 登录成功后,连接 WebSocket 接收消息
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
self._shutdown_event = asyncio.Event()
await self._shutdown_event.wait()
@@ -156,16 +159,29 @@ class WeChatPadProAdapter(Platform):
if login_state == 1:
logger.info("WeChatPadPro 设备当前在线。")
return True
else:
# login_state == 3 为离线状态
elif login_state == 3:
logger.info(
f"WeChatPadPro 设备不在线,登录状态: {login_state}"
"WeChatPadPro 设备不在线"
)
return False
else:
logger.error(
f"未知的在线状态: {login_state:}"
)
return False
# Code == 300 为微信退出状态。
elif response.status == 200 and response_data.get("Code") == 300:
logger.info(
"WeChatPadPro 设备已退出。"
)
return False
else:
logger.error(
f"检查在线状态失败: {response.status}, {response_data}"
)
return False
except aiohttp.ClientConnectorError as e:
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
return False
@@ -179,7 +195,7 @@ class WeChatPadProAdapter(Platform):
"""
url = f"{self.base_url}/admin/GenAuthKey1"
params = {"key": self.admin_key}
payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码
payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
async with aiohttp.ClientSession() as session:
try:
@@ -336,7 +352,7 @@ class WeChatPadProAdapter(Platform):
message = await asyncio.wait_for(
websocket.recv(), timeout=wait_time
)
logger.info(message)
# logger.debug(message) # 不显示原始消息内容
asyncio.create_task(self.handle_websocket_message(message))
except asyncio.TimeoutError:
logger.warning(
@@ -350,7 +366,7 @@ class WeChatPadProAdapter(Platform):
logger.error(f"处理 WebSocket 消息时发生错误: {e}")
break
except Exception as e:
logger.error(f"WebSocket 连接失败: {e}")
logger.error(f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。")
await asyncio.sleep(5)
async def handle_websocket_message(self, message: str):
@@ -425,7 +441,7 @@ class WeChatPadProAdapter(Platform):
):
# 再根据消息类型处理消息内容
await self._process_message_content(abm, raw_message, msg_type, content)
return abm
return None
@@ -20,7 +20,7 @@ from requests import Response
from wechatpy.utils import check_signature
from wechatpy.crypto import WeChatCrypto
from wechatpy import WeChatClient
from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage
from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage, BaseMessage
from wechatpy.exceptions import InvalidSignatureException
from wechatpy import parse_message
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
@@ -87,7 +87,11 @@ class WecomServer:
logger.info(f"解析成功: {msg}")
if self.callback:
await self.callback(msg)
result_xml = await self.callback(msg)
if not result_xml:
return "success"
if isinstance(result_xml, str):
return result_xml
return "success"
@@ -117,6 +121,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
self.api_base_url = platform_config.get(
"api_base_url", "https://api.weixin.qq.com/cgi-bin/"
)
self.active_send_mode = self.config.get("active_send_mode", False)
if not self.api_base_url:
self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
@@ -138,9 +143,29 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
self.client.API_BASE_URL = self.api_base_url
async def callback(msg):
# 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重
# msgid -> Future
self.wexin_event_workers: dict[str, asyncio.Future] = {}
async def callback(msg: BaseMessage):
try:
await self.convert_message(msg)
if self.active_send_mode:
await self.convert_message(msg, None)
else:
if msg.id in self.wexin_event_workers:
future = self.wexin_event_workers[msg.id]
logger.debug(f"duplicate message id checked: {msg.id}")
else:
future = asyncio.get_event_loop().create_future()
self.wexin_event_workers[msg.id] = future
await self.convert_message(msg, future)
# I love shield so much!
result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s
logger.debug(f"Got future result: {result}")
self.wexin_event_workers.pop(msg.id, None)
return result # xml. see weixin_offacc_event.py
except asyncio.TimeoutError:
pass
except Exception as e:
logger.error(f"转换消息时出现异常: {e}")
@@ -163,7 +188,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
async def run(self):
await self.server.start_polling()
async def convert_message(self, msg) -> AstrBotMessage | None:
async def convert_message(
self, msg, future: asyncio.Future = None
) -> AstrBotMessage | None:
abm = AstrBotMessage()
if isinstance(msg, TextMessage):
abm.message_str = msg.content
@@ -177,7 +204,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
abm.message_id = msg.id
abm.timestamp = msg.time
abm.session_id = abm.sender.user_id
abm.raw_message = msg
elif msg.type == "image":
assert isinstance(msg, ImageMessage)
abm.message_str = "[图片]"
@@ -191,7 +217,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
abm.message_id = msg.id
abm.timestamp = msg.time
abm.session_id = abm.sender.user_id
abm.raw_message = msg
elif msg.type == "voice":
assert isinstance(msg, VoiceMessage)
@@ -209,7 +234,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
audio = AudioSegment.from_file(path)
audio.export(path_wav, format="wav")
except Exception as e:
logger.error(f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。")
logger.error(
f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。"
)
path_wav = path
return
@@ -224,11 +251,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
abm.message_id = msg.id
abm.timestamp = msg.time
abm.session_id = abm.sender.user_id
abm.raw_message = msg
else:
logger.warning(f"暂未实现的事件: {msg.type}")
future.set_result(None)
return
# 很不优雅 :(
abm.raw_message = {
"message": msg,
"future": future,
"active_send_mode": self.active_send_mode,
}
logger.info(f"abm: {abm}")
await self.handle_msg(abm)
@@ -4,6 +4,8 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
from astrbot.api.message_components import Plain, Image, Record
from wechatpy import WeChatClient
from wechatpy.replies import TextReply, ImageReply, VoiceReply
from astrbot.api import logger
@@ -82,12 +84,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
async def send(self, message: MessageChain):
message_obj = self.message_obj
active_send_mode = message_obj.raw_message.get("active_send_mode", False)
for comp in message.chain:
if isinstance(comp, Plain):
# Split long text messages if needed
plain_chunks = await self.split_plain(comp.text)
for chunk in plain_chunks:
self.client.message.send_text(message_obj.sender.user_id, chunk)
if active_send_mode:
self.client.message.send_text(message_obj.sender.user_id, chunk)
else:
reply = TextReply(
content=chunk,
message=self.message_obj.raw_message["message"],
)
xml = reply.render()
future = self.message_obj.raw_message["future"]
assert isinstance(future, asyncio.Future)
future.set_result(xml)
await asyncio.sleep(0.5) # Avoid sending too fast
elif isinstance(comp, Image):
img_path = await comp.convert_to_file_path()
@@ -102,10 +115,22 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
)
return
logger.debug(f"微信公众平台上传图片返回: {response}")
self.client.message.send_image(
message_obj.sender.user_id,
response["media_id"],
)
if active_send_mode:
self.client.message.send_image(
message_obj.sender.user_id,
response["media_id"],
)
else:
reply = ImageReply(
media_id=response["media_id"],
message=self.message_obj.raw_message["message"],
)
xml = reply.render()
future = self.message_obj.raw_message["future"]
assert isinstance(future, asyncio.Future)
future.set_result(xml)
elif isinstance(comp, Record):
record_path = await comp.convert_to_file_path()
# 转成amr
@@ -124,10 +149,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
)
return
logger.info(f"微信公众平台上传语音返回: {response}")
self.client.message.send_voice(
message_obj.sender.user_id,
response["media_id"],
)
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=self.message_obj.raw_message["message"],
)
xml = reply.render()
future = self.message_obj.raw_message["future"]
assert isinstance(future, asyncio.Future)
future.set_result(xml)
else:
logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}")
@@ -61,9 +61,9 @@ class ProviderVolcengineTTS(TTSProvider):
payload = self._build_request_payload(text)
logger.info(f"请求头: {headers}")
logger.info(f"请求 URL: {self.api_base}")
logger.info(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...")
logger.debug(f"请求头: {headers}")
logger.debug(f"请求 URL: {self.api_base}")
logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...")
try:
async with aiohttp.ClientSession() as session:
@@ -73,10 +73,10 @@ class ProviderVolcengineTTS(TTSProvider):
headers=headers,
timeout=self.timeout
) as response:
logger.info(f"响应状态码: {response.status}")
logger.debug(f"响应状态码: {response.status}")
response_text = await response.text()
logger.info(f"响应内容: {response_text[:200]}...")
logger.debug(f"响应内容: {response_text[:200]}...")
if response.status == 200:
resp_data = json.loads(response_text)
@@ -103,5 +103,5 @@ class ProviderVolcengineTTS(TTSProvider):
except Exception as e:
error_details = traceback.format_exc()
logger.info(f"火山引擎 TTS 异常详情: {error_details}")
logger.debug(f"火山引擎 TTS 异常详情: {error_details}")
raise Exception(f"火山引擎 TTS 异常: {str(e)}")
+7
View File
@@ -0,0 +1,7 @@
# What's Changed
1. 新增:火山引擎 TTS
2. 修复:修复了 WeChatPadPro 在重新登录时为新设备的问题
2. ‼️修复:微信公众号(个人认证或者未认证)的情况下能接收但无法回复消息的问题
3. 修复:Minimax TTS 相关问题
4. 优化:登录界面侧边栏、关于页面样式,修复如果此前已经登录但未自行跳转的问题
+6
View File
@@ -1462,3 +1462,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
plugin_cfg["reset"] = reset_cfg
alter_cmd_cfg["astrbot"] = plugin_cfg
sp.put("alter_cmd", alter_cmd_cfg)
@filter.command("test")
async def test_to(self, event: AstrMessageEvent):
import asyncio
await asyncio.sleep(10)
yield event.plain_result("OK")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "3.5.10"
version = "3.5.11"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"