From 5aeca9662b30a246ed637062bd4acff10a042c1a Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Sun, 11 May 2025 22:57:50 +0800 Subject: [PATCH 01/54] =?UTF-8?q?feat:=20=E5=AF=B9aiocqhttp=E4=B8=AD,=20At?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E6=96=B0=E5=A2=9E=E5=A4=84=E7=90=86:=20?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8At=E5=AD=97=E6=AE=B5=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E4=B9=9F=E4=BC=9A=E8=A2=AB=E8=A7=A3=E6=9E=90=E4=B8=BA=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E4=BF=A1=E6=81=AF(=E4=BD=86=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=93=BE=E5=B9=B6=E6=B2=A1=E6=9C=89=E4=BF=AE=E6=94=B9,=20?= =?UTF-8?q?=E5=8F=AA=E6=98=AF=E5=9C=A8=E7=94=A8=E4=BA=8Ellm=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=9A=84=E6=96=87=E6=9C=AC=E4=B8=AD=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86At=E4=BF=A1=E6=81=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiocqhttp/aiocqhttp_platform_adapter.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 97754f2c9..4786b996c 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -287,6 +287,31 @@ class AiocqhttpAdapter(Platform): logger.error(f"获取引用消息失败: {e}。") a = ComponentTypes[t](**m["data"]) # noqa: F405 abm.message.append(a) + elif t == "at": + for m in m_group: + try: + at_info = await self.bot.call_action( + action="get_stranger_info", + user_id=int(m["data"]["qq"]), + ) + if at_info: + nickname = at_info.get("nick", "") + abm.message.append( + At( + qq=m["data"]["qq"], + name=nickname, + ) + ) + # 兼容文本消息 + message_str += f"@{nickname} " + else: + abm.message.append( + At(qq=m["data"]["qq"], name="") + ) # noqa: F405 + except ActionFailed as e: + logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") + except BaseException as e: + logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") else: for m in m_group: a = ComponentTypes[t](**m["data"]) # noqa: F405 From 4bef5e8313444dbcb0a84cf6fad69fb3e791ff66 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 12 May 2025 00:21:48 +0800 Subject: [PATCH 02/54] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8Dmessage=5Fstr?= =?UTF-8?q?=E8=A2=AB=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/aiocqhttp/aiocqhttp_platform_adapter.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 4786b996c..0896e6ccb 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -165,9 +165,7 @@ class AiocqhttpAdapter(Platform): if "sub_type" in event: if event["sub_type"] == "poke" and "target_id" in event: - abm.message.append( - Poke(qq=str(event["target_id"]), type="poke") - ) # noqa: F405 + abm.message.append(Poke(qq=str(event["target_id"]), type="poke")) # noqa: F405 return abm @@ -218,7 +216,7 @@ class AiocqhttpAdapter(Platform): a = None if t == "text": # 合并相邻文本段 - message_str = "".join(m["data"]["text"] for m in m_group).strip() + message_str += "".join(m["data"]["text"] for m in m_group).strip() a = ComponentTypes[t](text=message_str) # noqa: F405 abm.message.append(a) @@ -305,9 +303,7 @@ class AiocqhttpAdapter(Platform): # 兼容文本消息 message_str += f"@{nickname} " else: - abm.message.append( - At(qq=m["data"]["qq"], name="") - ) # noqa: F405 + abm.message.append(At(qq=m["data"]["qq"], name="")) # noqa: F405 except ActionFailed as e: logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") except BaseException as e: From e46cf20dd30f7b0e832e62151f1c31fe3c036c4e Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 12 May 2025 11:22:46 +0800 Subject: [PATCH 03/54] =?UTF-8?q?fix:=20=E4=B8=8D=E5=86=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=94=A4=E9=86=92=E7=9A=84@=E5=88=B0message=5Fstr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiocqhttp/aiocqhttp_platform_adapter.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 4786b996c..e6ed9205f 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -288,6 +288,8 @@ class AiocqhttpAdapter(Platform): a = ComponentTypes[t](**m["data"]) # noqa: F405 abm.message.append(a) elif t == "at": + first_at_self_processed = False + for m in m_group: try: at_info = await self.bot.call_action( @@ -296,18 +298,23 @@ class AiocqhttpAdapter(Platform): ) if at_info: nickname = at_info.get("nick", "") + is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"} + abm.message.append( At( qq=m["data"]["qq"], name=nickname, ) ) - # 兼容文本消息 - message_str += f"@{nickname} " + + if is_at_self and not first_at_self_processed: + # 第一个@是机器人,不添加到message_str + first_at_self_processed = True + else: + # 非第一个@机器人或@其他用户,添加到message_str + message_str += f"@{nickname} " else: - abm.message.append( - At(qq=m["data"]["qq"], name="") - ) # noqa: F405 + abm.message.append(At(qq=str(m["data"]["qq"]), name="")) except ActionFailed as e: logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") except BaseException as e: From 77c823c100c89518f295a8f412cec8a868c853d2 Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 12 May 2025 11:32:40 +0800 Subject: [PATCH 04/54] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=85=A8=E4=BD=93=E6=88=90=E5=91=98=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/aiocqhttp/aiocqhttp_platform_adapter.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index eb50ad679..cbf40edaa 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -217,9 +217,9 @@ class AiocqhttpAdapter(Platform): for t, m_group in itertools.groupby(event.message, key=lambda x: x["type"]): a = None if t == "text": - # 合并相邻文本段 - message_str += "".join(m["data"]["text"] for m in m_group).strip() - a = ComponentTypes[t](text=message_str) # noqa: F405 + current_text = "".join(m["data"]["text"] for m in m_group).strip() + message_str += current_text + a = ComponentTypes[t](text=current_text) # noqa: F405 abm.message.append(a) elif t == "file": @@ -292,6 +292,11 @@ class AiocqhttpAdapter(Platform): for m in m_group: try: + if m["data"]["qq"] == "all": + abm.message.append(At(qq="all", name="全体成员")) + message_str += "@全体成员 " + continue + at_info = await self.bot.call_action( action="get_stranger_info", user_id=int(m["data"]["qq"]), From a730cee7fd64daead7e8d5fd59ab8d135bc34ce6 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 12 May 2025 14:48:31 +0800 Subject: [PATCH 05/54] =?UTF-8?q?fix:=20at=E5=85=A8=E4=BD=93=E4=B8=8D?= =?UTF-8?q?=E5=8A=A0=E5=85=A5message=5Fstr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index cbf40edaa..9d882741c 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -294,7 +294,6 @@ class AiocqhttpAdapter(Platform): try: if m["data"]["qq"] == "all": abm.message.append(At(qq="all", name="全体成员")) - message_str += "@全体成员 " continue at_info = await self.bot.call_action( From 3ad2c46f3fcb48136692b131b1bfa97a235d09b6 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 12 May 2025 15:04:23 +0800 Subject: [PATCH 06/54] =?UTF-8?q?perf:=20tg=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5aiocqhttp=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/platform/sources/telegram/tg_adapter.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index a2ce88736..b13b60d7d 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -282,10 +282,12 @@ class TelegramPlatformAdapter(Platform): entity.offset + 1 : entity.offset + entity.length ] message.message.append(Comp.At(qq=name, name=name)) - plain_text = ( - plain_text[: entity.offset] - + plain_text[entity.offset + entity.length :] - ) + # 如果mention是当前bot则移除;否则保留 + if name.lower() == context.bot.username.lower(): + plain_text = ( + plain_text[: entity.offset] + + plain_text[entity.offset + entity.length :] + ) if plain_text: message.message.append(Comp.Plain(plain_text)) From 3923b87f0847b9f875171b826a4cdb76381f8c7a Mon Sep 17 00:00:00 2001 From: Li Haoyuan <1513624626@qq.com> Date: Wed, 14 May 2025 11:01:28 +0800 Subject: [PATCH 07/54] feat: Add MiniMax TTS API provider --- astrbot/core/config/default.py | 66 ++++++++++ astrbot/core/provider/manager.py | 4 + .../sources/minimax_tts_api_source.py | 120 ++++++++++++++++++ dashboard/src/views/ProviderPage.vue | 59 ++++----- 4 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 astrbot/core/provider/sources/minimax_tts_api_source.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7a8985242..65163a16a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -788,6 +788,25 @@ CONFIG_METADATA_2 = { "azure_tts_subscription_key": "", "azure_tts_region": "eastus" }, + "MiniMax TTS(API)": { + "id": "minimax_tts", + "type": "minimax_tts_api", + "provider_type": "text_to_speech", + "enable": False, + "api_key": "", + "api_base": "https://api.minimax.chat/v1/t2a_v2", + "minimax-group-id": "", + "model": "speech-02-turbo", + "minimax-langboost": "auto", + "minimax-voice-speed": 1.0, + "minimax-voice-vol": 1.0, + "minimax-voice-pitch": 0, + "minimax-voice-id": "female-shaonv", + "minimax-voice-emotion": "neutral", + "minimax-voice-latex": False, + "minimax-voice-english-normalization": False, + "timeout": "20", + }, }, "items": { "azure_tts_voice": { @@ -911,6 +930,53 @@ CONFIG_METADATA_2 = { }, }, }, + "minimax-group-id": { + "type": "string", + "description": "用户所属的组", + "hint": "于账户管理->基本信息中可见", + }, + "minimax-langboost": { + "type": "string", + "description": "指定语言/方言识别能力", + "hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现", + "options": [ "Chinese","Chinese,Yue","English","Arabic","Russian","Spanish","French","Portuguese","German","Turkish","Dutch","Ukrainian","Vietnamese","Indonesian","Japanese","Italian","Korean","Thai","Polish","Romanian","Greek","Czech","Finnish","Hindi","auto",], + }, + "minimax-voice-speed": { + "type": "float", + "description": "语速取值越大,语速越快", + "hint": "生成声音的语速, 取值[0.5, 2], 默认为1.0, 取值越大,语速越快", + }, + "minimax-voice-vol": { + "type": "float", + "description": "音量", + "hint": "生成声音的音量, 取值(0, 10], 默认为1.0, 取值越大,音量越高", + }, + "minimax-voice-pitch": { + "type": "int", + "description": "语调", + "hint": "生成声音的语调, 取值[-12, 12], 默认为0", + }, + "minimax-voice-id": { + "type": "string", + "description": "音色编号", + "hint": "请求的音色编号, 请见官网文档", + }, + "minimax-voice-emotion": { + "type": "string", + "description": "语音情绪", + "hint": "控制合成语音的情绪", + "options": ["happy","sad","angry","fearful","disgusted","surprised","neutral",], + }, + "minimax-voice-latex": { + "type": "bool", + "description": "是否支持朗读latex公式", + "hint": "", + }, + "minimax-voice-english-normalization": { + "type": "bool", + "description": "是否支持英语文本规范化", + "hint": "可提升数字阅读场景的性能,但会略微增加延迟", + }, "rag_options": { "description": "RAG 选项", "type": "object", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index e61fbf925..596293ac2 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -206,6 +206,10 @@ class ProviderManager: from .sources.azure_tts_source import ( AzureTTSProvider as AzureTTSProvider, ) + case "minimax_tts_api": + from .sources.minimax_tts_api_source import ( + ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py new file mode 100644 index 000000000..52e8ccc46 --- /dev/null +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -0,0 +1,120 @@ +import json +import os +import uuid +from typing import Iterator + +import requests + +from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +from ..entities import ProviderType +from ..provider import TTSProvider +from ..register import register_provider_adapter + + +@register_provider_adapter( + "minimax_tts_api", "MiniMax TTS API", provider_type=ProviderType.TEXT_TO_SPEECH +) +class ProviderMiniMaxTTSAPI(TTSProvider): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + ) -> None: + super().__init__(provider_config, provider_settings) + self.chosen_api_key: str = provider_config.get("api_key", "") + self.api_base: str = provider_config.get( + "api_base", "https://api.minimax.chat/v1/t2a_v2" + ) + self.group_id: str = provider_config.get("minimax-group-id", "") + self.set_model(provider_config.get("model", "")) + self.lang_boost: str = provider_config.get("minimax-langboost", "auto") + + self.voice_setting: dict = { + "speed": provider_config.get("minimax-voice-speed", 1.0), + "vol": provider_config.get("minimax-voice-vol", 1.0), + "pitch": provider_config.get("minimax-voice-pitch", 0), + "voice_id": provider_config.get("minimax-voice-id", ""), + "emotion": provider_config.get("minimax-voice-emotion", "neutral"), + "latex_read": provider_config.get("minimax-voice-latex", False), + "english_normalization": provider_config.get( + "minimax-voice-english-normalization", False + ), + } + + self.audio_setting: dict = { + "sample_rate": 32000, + "bitrate": 128000, + "format": "mp3", + } + + self.concat_base_url: str = self.api_base + "?GroupId=" + self.group_id + self.headers = { + "Authorization": f"Bearer {self.chosen_api_key}", + "accept": "application/json, text/plain, */*", + "content-type": "application/json", + } + + def _build_tts_stream_body(self, text: str): + """构建流式请求体""" + body = json.dumps( + { + "model": self.model_name, + "text": text, + "stream": True, + "language_boost": self.lang_boost, + "voice_setting": self.voice_setting, + "audio_setting": self.audio_setting, + } + ) + return body + + def _call_tts_stream(self, text: str) -> Iterator[bytes]: + """进行流式请求""" + tts_body = self._build_tts_stream_body(text) + try: + response = requests.request( + "POST", + self.concat_base_url, + stream=True, + headers=self.headers, + data=tts_body, + ) + response.raise_for_status() + for chunk in response.raw: + if chunk: + if chunk[:5] == b"data:": + data = json.loads(chunk[5:]) + if "data" in data and "extra_info" not in data: + if "audio" in data["data"]: + audio = data["data"]["audio"] + yield audio + except requests.exceptions.RequestException as e: + raise Exception(f"MiniMax TTS API请求失败: {str(e)}") + + def _audio_play(self, audio_stream: Iterator[bytes]) -> bytes: + """解码数据流到audio比特流""" + audio = b"" + for chunk in audio_stream: + if chunk is not None and chunk != "\n": + decoded_hex = bytes.fromhex(chunk) + audio += decoded_hex + + return audio + + async def get_audio(self, text: str) -> str: + temp_dir = os.path.join(get_astrbot_data_path(), "temp") + path = os.path.join(temp_dir, f"minimax_tts_api_{uuid.uuid4()}.mp3") + + try: + audio_chunk_iterator = self._call_tts_stream(text) + audio = self._audio_play(audio_chunk_iterator) + + # 结果保存至文件 + with open(path, "wb") as file: + file.write(audio) + + return path + + except requests.exceptions.RequestException as e: + raise e diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index 9f27854d6..151aff56b 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -30,7 +30,7 @@ mdi-tag - 提供商类型: + 提供商类型: {{ item.type }} @@ -94,7 +94,7 @@ mdi-close - + @@ -110,14 +110,14 @@ 文字转语音 - + - - @@ -155,17 +155,17 @@ {{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }} {{ updatingMode ? '编辑' : '新增' }} {{ newSelectedProviderName }} 服务提供商 - + - - + - + @@ -183,7 +183,7 @@ location="top"> {{ save_message }} - + @@ -221,7 +221,7 @@ export default { save_message_success: "success", showConsole: false, - + // 新增提供商对话框相关 showAddProviderDialog: false, activeProviderTab: 'chat_completion', @@ -247,16 +247,16 @@ export default { getTemplatesByType(type) { const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {}; const filtered = {}; - + for (const [name, template] of Object.entries(templates)) { if (template.provider_type === type) { filtered[name] = template; } } - + return filtered; }, - + // 获取提供商类型对应的图标 getProviderIcon(type) { const icons = { @@ -278,6 +278,7 @@ export default { 'LM Studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg', 'FishAudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg', 'Azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg', + 'MiniMax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg', }; for (const key in icons) { if (type.startsWith(key)) { @@ -296,7 +297,7 @@ export default { }; return names[tabType] || tabType; }, - + // 获取提供商简介 getProviderDescription(template, name) { if (name == 'OpenAI') { @@ -304,7 +305,7 @@ export default { } return `${template.type} 服务提供商`; }, - + // 选择提供商模板 selectProviderTemplate(name) { this.newSelectedProviderName = name; @@ -334,7 +335,7 @@ export default { break; } } - + const mergeConfigWithOrder = (target, source, reference) => { // 首先复制所有source中的属性到target if (source && typeof source === 'object' && !Array.isArray(source)) { @@ -348,7 +349,7 @@ export default { } } } - + // 然后根据reference的结构添加或覆盖属性 for (let key in reference) { if (typeof reference[key] === 'object' && reference[key] !== null) { @@ -356,8 +357,8 @@ export default { target[key] = Array.isArray(reference[key]) ? [] : {}; } mergeConfigWithOrder( - target[key], - source && source[key] ? source[key] : {}, + target[key], + source && source[key] ? source[key] : {}, reference[key] ); } else if (!(key in target)) { @@ -366,7 +367,7 @@ export default { } } }; - + if (defaultConfig) { mergeConfigWithOrder(this.newSelectedProviderConfig, provider, defaultConfig); } @@ -417,7 +418,7 @@ export default { providerStatusChange(provider) { provider.enable = !provider.enable; // 切换状态 - + axios.post('/api/config/provider/update', { id: provider.id, config: provider @@ -429,13 +430,13 @@ export default { this.showError(err.response?.data?.message || err.message); }); }, - + showSuccess(message) { this.save_message = message; this.save_message_success = "success"; this.save_message_snack = true; }, - + showError(message) { this.save_message = message; this.save_message_success = "error"; @@ -475,4 +476,4 @@ export default { .v-window { border-radius: 4px; } - \ No newline at end of file + From a7823b352f2e45d713e615396fb70ca6f37f51ba Mon Sep 17 00:00:00 2001 From: Li Haoyuan <1513624626@qq.com> Date: Wed, 14 May 2025 13:09:09 +0800 Subject: [PATCH 08/54] docs: Adjust MiniMax TTS configuration info --- astrbot/core/config/default.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 8470a0c29..0bbbdf944 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -944,18 +944,18 @@ CONFIG_METADATA_2 = { }, "minimax-group-id": { "type": "string", - "description": "用户所属的组", + "description": "用户组", "hint": "于账户管理->基本信息中可见", }, "minimax-langboost": { "type": "string", - "description": "指定语言/方言识别能力", + "description": "指定语言/方言", "hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现", "options": [ "Chinese","Chinese,Yue","English","Arabic","Russian","Spanish","French","Portuguese","German","Turkish","Dutch","Ukrainian","Vietnamese","Indonesian","Japanese","Italian","Korean","Thai","Polish","Romanian","Greek","Czech","Finnish","Hindi","auto",], }, "minimax-voice-speed": { "type": "float", - "description": "语速取值越大,语速越快", + "description": "语速", "hint": "生成声音的语速, 取值[0.5, 2], 默认为1.0, 取值越大,语速越快", }, "minimax-voice-vol": { @@ -970,23 +970,23 @@ CONFIG_METADATA_2 = { }, "minimax-voice-id": { "type": "string", - "description": "音色编号", - "hint": "请求的音色编号, 请见官网文档", + "description": "音色", + "hint": "音色编号, 详见官网文档", }, "minimax-voice-emotion": { "type": "string", - "description": "语音情绪", + "description": "情绪", "hint": "控制合成语音的情绪", "options": ["happy","sad","angry","fearful","disgusted","surprised","neutral",], }, "minimax-voice-latex": { "type": "bool", - "description": "是否支持朗读latex公式", - "hint": "", + "description": "支持朗读latex公式", + "hint": "朗读latex公式, 但是需要确保输入文本按官网要求格式化", }, "minimax-voice-english-normalization": { "type": "bool", - "description": "是否支持英语文本规范化", + "description": "支持英语文本规范化", "hint": "可提升数字阅读场景的性能,但会略微增加延迟", }, "rag_options": { From 2117b65487a27929e7554a58429f66f754d77b12 Mon Sep 17 00:00:00 2001 From: Li Haoyuan <1513624626@qq.com> Date: Wed, 14 May 2025 14:21:23 +0800 Subject: [PATCH 09/54] feat: Support timber_weights for MiniMax TTS --- astrbot/core/config/default.py | 16 +++++++- .../sources/minimax_tts_api_source.py | 38 +++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 0bbbdf944..6cc6dab6c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -813,7 +813,9 @@ CONFIG_METADATA_2 = { "minimax-voice-speed": 1.0, "minimax-voice-vol": 1.0, "minimax-voice-pitch": 0, + "minimax-is-timber-weight": False, "minimax-voice-id": "female-shaonv", + "minimax-timber-weight": '[{"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 1}]', "minimax-voice-emotion": "neutral", "minimax-voice-latex": False, "minimax-voice-english-normalization": False, @@ -968,10 +970,20 @@ CONFIG_METADATA_2 = { "description": "语调", "hint": "生成声音的语调, 取值[-12, 12], 默认为0", }, + "minimax-is-timber-weight": { + "type": "bool", + "description": "启用混合音色", + "hint": "启用混合音色, 支持以自定义权重混合最多四种音色, 启用后自动忽略单一音色设置", + }, + "minimax-timber-weight": { + "type": "string", + "description": "混合音色", + "hint": "混合音色及其权重, 最多支持四种音色, 权重为整数, 取值[1, 100]. 可在官网体验页面查看代码获得预设以及编写模板", + }, "minimax-voice-id": { "type": "string", - "description": "音色", - "hint": "音色编号, 详见官网文档", + "description": "单一音色", + "hint": "单一音色编号, 详见官网文档", }, "minimax-voice-emotion": { "type": "string", diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index 52e8ccc46..8bc61bfee 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -1,7 +1,7 @@ import json import os import uuid -from typing import Iterator +from typing import Dict, Iterator, List, Union import requests @@ -29,12 +29,23 @@ class ProviderMiniMaxTTSAPI(TTSProvider): self.group_id: str = provider_config.get("minimax-group-id", "") self.set_model(provider_config.get("model", "")) self.lang_boost: str = provider_config.get("minimax-langboost", "auto") + self.is_timber_weight: bool = provider_config.get( + "minimax-is-timber-weight", False + ) + self.timber_weight: List[Dict[str, Union[str, int]]] = json.loads( + provider_config.get( + "minimax-timber-weight", + '[{"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 1}]', + ) + ) self.voice_setting: dict = { "speed": provider_config.get("minimax-voice-speed", 1.0), "vol": provider_config.get("minimax-voice-vol", 1.0), "pitch": provider_config.get("minimax-voice-pitch", 0), - "voice_id": provider_config.get("minimax-voice-id", ""), + "voice_id": provider_config.get("minimax-voice-id", "") + if not self.is_timber_weight + else "", "emotion": provider_config.get("minimax-voice-emotion", "neutral"), "latex_read": provider_config.get("minimax-voice-latex", False), "english_normalization": provider_config.get( @@ -57,16 +68,19 @@ class ProviderMiniMaxTTSAPI(TTSProvider): def _build_tts_stream_body(self, text: str): """构建流式请求体""" - body = json.dumps( - { - "model": self.model_name, - "text": text, - "stream": True, - "language_boost": self.lang_boost, - "voice_setting": self.voice_setting, - "audio_setting": self.audio_setting, - } - ) + dict_body: Dict[str, object] = { + "model": self.model_name, + "text": text, + "stream": True, + "language_boost": self.lang_boost, + "voice_setting": self.voice_setting, + "audio_setting": self.audio_setting, + } + if self.is_timber_weight: + dict_body["timber_weights"] = self.timber_weight + + body = json.dumps(dict_body) + return body def _call_tts_stream(self, text: str) -> Iterator[bytes]: From e01d4264e3fa3d97d73f064ef3c4835dbd8d3f94 Mon Sep 17 00:00:00 2001 From: Li Haoyuan <1513624626@qq.com> Date: Wed, 14 May 2025 14:40:25 +0800 Subject: [PATCH 10/54] docs: Adjust MiniMax TTS timber_weights description --- astrbot/core/config/default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 6cc6dab6c..ef2a69609 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -815,7 +815,7 @@ CONFIG_METADATA_2 = { "minimax-voice-pitch": 0, "minimax-is-timber-weight": False, "minimax-voice-id": "female-shaonv", - "minimax-timber-weight": '[{"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 1}]', + "minimax-timber-weight": '[{"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 25}, {"voice_id": "Chinese (Mandarin)_BashfulGirl", "weight": 50}]', "minimax-voice-emotion": "neutral", "minimax-voice-latex": False, "minimax-voice-english-normalization": False, @@ -978,7 +978,7 @@ CONFIG_METADATA_2 = { "minimax-timber-weight": { "type": "string", "description": "混合音色", - "hint": "混合音色及其权重, 最多支持四种音色, 权重为整数, 取值[1, 100]. 可在官网体验页面查看代码获得预设以及编写模板", + "hint": "混合音色及其权重, 最多支持四种音色, 权重为整数, 取值[1, 100]. 可在官网API语音调试台预览代码获得预设以及编写模板, 需要严格按照json字符串格式编写, 可以查看控制台判断是否解析成功. 具体结构可参照默认值以及官网代码预览.", }, "minimax-voice-id": { "type": "string", From 25ef0039e42ae87fa9e15a20a03c99e2fd89e5cc Mon Sep 17 00:00:00 2001 From: Li Haoyuan <1513624626@qq.com> Date: Wed, 14 May 2025 20:59:45 +0800 Subject: [PATCH 11/54] refactor: Optimize MiniMax TTS API Provider --- astrbot/core/config/default.py | 2 +- .../sources/minimax_tts_api_source.py | 45 +++++++++---------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ef2a69609..2b802b595 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -819,7 +819,7 @@ CONFIG_METADATA_2 = { "minimax-voice-emotion": "neutral", "minimax-voice-latex": False, "minimax-voice-english-normalization": False, - "timeout": "20", + "timeout": 20, }, }, "items": { diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index 8bc61bfee..26d08ac75 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -43,9 +43,9 @@ class ProviderMiniMaxTTSAPI(TTSProvider): "speed": provider_config.get("minimax-voice-speed", 1.0), "vol": provider_config.get("minimax-voice-vol", 1.0), "pitch": provider_config.get("minimax-voice-pitch", 0), - "voice_id": provider_config.get("minimax-voice-id", "") - if not self.is_timber_weight - else "", + "voice_id": "" + if self.is_timber_weight + else provider_config.get("minimax-voice-id", ""), "emotion": provider_config.get("minimax-voice-emotion", "neutral"), "latex_read": provider_config.get("minimax-voice-latex", False), "english_normalization": provider_config.get( @@ -59,7 +59,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider): "format": "mp3", } - self.concat_base_url: str = self.api_base + "?GroupId=" + self.group_id + self.concat_base_url: str = f"{self.api_base}?GroupId={self.group_id}" self.headers = { "Authorization": f"Bearer {self.chosen_api_key}", "accept": "application/json, text/plain, */*", @@ -79,42 +79,37 @@ class ProviderMiniMaxTTSAPI(TTSProvider): if self.is_timber_weight: dict_body["timber_weights"] = self.timber_weight - body = json.dumps(dict_body) - - return body + return json.dumps(dict_body) def _call_tts_stream(self, text: str) -> Iterator[bytes]: """进行流式请求""" - tts_body = self._build_tts_stream_body(text) try: - response = requests.request( - "POST", + response = requests.post( self.concat_base_url, stream=True, headers=self.headers, - data=tts_body, + data=self._build_tts_stream_body(text), ) response.raise_for_status() + for chunk in response.raw: - if chunk: - if chunk[:5] == b"data:": - data = json.loads(chunk[5:]) - if "data" in data and "extra_info" not in data: - if "audio" in data["data"]: - audio = data["data"]["audio"] - yield audio + if not chunk or not chunk.startswith(b"data:"): + continue + data = json.loads(chunk[5:]) + if "extra_info" in data: + continue + audio = data.get("data", {}).get("audio") + if audio is not None: + yield audio + except requests.exceptions.RequestException as e: raise Exception(f"MiniMax TTS API请求失败: {str(e)}") def _audio_play(self, audio_stream: Iterator[bytes]) -> bytes: """解码数据流到audio比特流""" - audio = b"" - for chunk in audio_stream: - if chunk is not None and chunk != "\n": - decoded_hex = bytes.fromhex(chunk) - audio += decoded_hex - - return audio + return b"".join( + bytes.fromhex(chunk) for chunk in audio_stream if chunk and chunk != b"\n" + ) async def get_audio(self, text: str) -> str: temp_dir = os.path.join(get_astrbot_data_path(), "temp") From 143702b92b95c8fb0aa8ae9d3b9e473d8cc9d930 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Thu, 15 May 2025 10:18:05 +0800 Subject: [PATCH 12/54] =?UTF-8?q?fix(tts):=20record=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=8D=95=E7=8B=AC=E5=8F=91=E9=80=81=E4=BB=A5=E4=BF=9D=E8=AF=81?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/respond/stage.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index bff94a64d..1bd70cd14 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -154,6 +154,11 @@ class RespondStage(Stage): except Exception as e: logger.warning(f"空内容检查异常: {e}") + record_comps = [c for c in result.chain if isinstance(c, Comp.Record)] + non_record_comps = [ + c for c in result.chain if not isinstance(c, Comp.Record) + ] + if self.enable_seg and ( (self.only_llm_result and result.is_llm_result()) or not self.only_llm_result @@ -171,8 +176,18 @@ class RespondStage(Stage): decorated_comps.append(comp) result.chain.remove(comp) break + + for rcomp in record_comps: + i = await self._calc_comp_interval(rcomp) + await asyncio.sleep(i) + try: + await event.send(MessageChain([rcomp])) + except Exception as e: + logger.error(f"发送消息失败: {e} chain: {result.chain}") + break + # 分段回复 - for comp in result.chain: + for comp in non_record_comps: i = await self._calc_comp_interval(comp) await asyncio.sleep(i) try: @@ -181,11 +196,18 @@ class RespondStage(Stage): logger.error(f"发送消息失败: {e} chain: {result.chain}") break else: + for rcomp in record_comps: + try: + await event.send(MessageChain([rcomp])) + except Exception as e: + logger.error(f"发送消息失败: {e} chain: {result.chain}") + try: - await event.send(result) + await event.send(MessageChain(non_record_comps)) except Exception as e: logger.error(traceback.format_exc()) logger.error(f"发送消息失败: {e} chain: {result.chain}") + await event._post_send() logger.info( f"AstrBot -> {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}" From bf7fc02c8dc8977095ee5235542b30fe45083442 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Thu, 15 May 2025 17:26:31 +0800 Subject: [PATCH 13/54] =?UTF-8?q?=E9=80=82=E9=85=8D=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E5=BE=AE=E4=BF=A1=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E2=80=94=E2=80=94wechatpadpro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 8 + astrbot/core/platform/manager.py | 4 + .../wechatpadpro/wechatpadpro_adapter.py | 513 ++++++++++++++++++ .../wechatpadpro_message_event.py | 183 +++++++ requirements.txt | 3 +- 5 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py create mode 100644 astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 56bdd5bac..9a8ba66eb 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -155,6 +155,14 @@ CONFIG_METADATA_2 = { "host": "这里填写你的局域网IP或者公网服务器IP", "port": 11451, }, + "wechatpadpro(微信)": { + "id": "wechatpadpro", + "type": "wechatpadpro", + "enable": False, + "admin_key": "stay33", + "host": "这里填写你的局域网IP或者公网服务器IP", + "port": 8059, + }, "weixin_official_account(微信公众平台)": { "id": "weixin_official_account", "type": "weixin_official_account", diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index 4ac575446..9473e9199 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -62,6 +62,10 @@ class PlatformManager: from .sources.gewechat.gewechat_platform_adapter import ( GewechatPlatformAdapter, # noqa: F401 ) + case "wechatpadpro": + from .sources.wechatpadpro.wechatpadpro_adapter import ( + WeChatPadProAdapter, + ) case "lark": from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401 case "dingtalk": diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py new file mode 100644 index 000000000..ad0a5fe16 --- /dev/null +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -0,0 +1,513 @@ +import asyncio +import aiohttp +import json +import os +import websockets +from typing import Awaitable, Any, Optional, Coroutine +from astrbot.api.message_components import Plain, Image, At, Record, Video +from astrbot.api.platform import Platform, PlatformMetadata +from astrbot.api.event import MessageChain +from astrbot.core.platform.astr_message_event import MessageSesion +from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageMember, MessageType +from ...register import register_platform_adapter +from astrbot import logger +from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from .wechatpadpro_message_event import WeChatPadProMessageEvent + +@register_platform_adapter("wechatpadpro", "WeChatPadPro 消息平台适配器") +class WeChatPadProAdapter(Platform): + def __init__( + self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue + ) -> None: + super().__init__(event_queue) + self.config = platform_config + self.settings = platform_settings + self.unique_session = platform_settings.get("unique_session", False) + + self.metadata = PlatformMetadata( + name="wechatpadpro", + description="WeChatPadPro 消息平台适配器", + id=self.config.get("id", "wechatpadpro"), + ) + + # 保存配置信息 + self.admin_key = self.config.get("admin_key") + self.host = self.config.get("host") + self.port = self.config.get("port") + self.base_url = f"http://{self.host}:{self.port}" + self.auth_key = None # 用于保存生成的授权码 + self.wxid = None # 用于保存登录成功后的 wxid + self.credentials_file = os.path.join(get_astrbot_data_path(), "wechatpadpro_credentials.json") # 持久化文件路径 + self._websocket = None # 用于保存 WebSocket 连接 + + async def run(self) -> None: + """ + 启动平台适配器的运行实例。 + """ + logger.info("WeChatPadPro 适配器正在启动...") + + # 尝试从文件中加载凭据 + loaded_credentials = self.load_credentials() + if loaded_credentials: + self.auth_key = loaded_credentials.get("auth_key") + self.wxid = loaded_credentials.get("wxid") + + # 检查在线状态 + if self.auth_key and await self.check_online_status(): + logger.info("WeChatPadPro 设备已在线,跳过扫码登录。") + # 如果在线,连接 WebSocket 接收消息 + 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 + + # 2. 获取登录二维码 + qr_code_url = await self.get_login_qr_code() + + if qr_code_url: + logger.info(f"请扫描以下二维码登录: {qr_code_url}") + else: + logger.error("无法获取登录二维码。") + return + + # 3. 检测扫码状态 + login_successful = await self.check_login_status() + + if login_successful: + # 登录成功后,连接 WebSocket 接收消息 + asyncio.create_task(self.connect_websocket()) + else: + logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") + await self.terminate() + return + + + # 示例:保持运行直到终止事件被设置 + self._shutdown_event = asyncio.Event() + await self._shutdown_event.wait() + logger.info("WeChatPadPro 适配器已停止。") + + def load_credentials(self): + """ + 从文件中加载 auth_key 和 wxid。 + """ + if os.path.exists(self.credentials_file): + try: + with open(self.credentials_file, "r") as f: + credentials = json.load(f) + logger.info("成功加载 WeChatPadPro 凭据。") + return credentials + except Exception as e: + logger.error(f"加载 WeChatPadPro 凭据失败: {e}") + return None + + def save_credentials(self): + """ + 将 auth_key 和 wxid 保存到文件。 + """ + credentials = { + "auth_key": self.auth_key, + "wxid": self.wxid, + } + try: + # 确保数据目录存在 + data_dir = os.path.dirname(self.credentials_file) + os.makedirs(data_dir, exist_ok=True) + with open(self.credentials_file, "w") as f: + json.dump(credentials, f) + logger.info("成功保存 WeChatPadPro 凭据。") + except Exception as e: + logger.error(f"保存 WeChatPadPro 凭据失败: {e}") + + async def check_online_status(self): + """ + 检查 WeChatPadPro 设备是否在线。 + """ + url = f"{self.base_url}/login/GetLoginStatus" + params = {"key": self.auth_key} + + async with aiohttp.ClientSession() as session: + try: + async with session.get(url, params=params) as response: + response_data = await response.json() + # 根据提供的在线接口返回示例,成功状态码是 200,loginState 为 1 表示在线 + if response.status == 200 and response_data.get("Code") == 200: + login_state = response_data.get("Data", {}).get("loginState") + if login_state == 1: + logger.info("WeChatPadPro 设备当前在线。") + return True + else: + logger.info(f"WeChatPadPro 设备不在线,登录状态: {login_state}") + return False + else: + logger.error(f"检查在线状态失败: {response.status}, {response_data}") + return False + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + return False + except Exception as e: + logger.error(f"检查在线状态时发生错误: {e}") + return False + + + async def generate_auth_key(self): + """ + 生成授权码。 + """ + url = f"{self.base_url}/admin/GenAuthKey1" + params = {"key": self.admin_key} + payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码 + + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + response_data = await response.json() + # 修正成功判断条件和授权码提取路径 + if response.status == 200 and response_data.get("Code") == 200: + # 授权码在 Data 字段的列表中 + if response_data.get("Data") and isinstance(response_data["Data"], list) and len(response_data["Data"]) > 0: + self.auth_key = response_data["Data"][0] + logger.info(f"成功获取授权码: {self.auth_key}") + else: + logger.error(f"生成授权码成功但未找到授权码: {response_data}") + else: + logger.error(f"生成授权码失败: {response.status}, {response_data}") + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + except Exception as e: + logger.error(f"生成授权码时发生错误: {e}") + + async def get_login_qr_code(self): + """ + 获取登录二维码地址。 + """ + url = f"{self.base_url}/login/GetLoginQrCodeNew" + params = {"key": self.auth_key} + payload = {} # 根据文档,这个接口的 body 可以为空 + + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + response_data = await response.json() + # 修正成功判断条件和数据提取路径 + if response.status == 200 and response_data.get("Code") == 200: + # 二维码地址在 Data.QrCodeUrl 字段中 + if response_data.get("Data") and response_data["Data"].get("QrCodeUrl"): + return response_data["Data"]["QrCodeUrl"] + else: + logger.error(f"获取登录二维码成功但未找到二维码地址: {response_data}") + return None + else: + logger.error(f"获取登录二维码失败: {response.status}, {response_data}") + return None + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + return None + except Exception as e: + logger.error(f"获取登录二维码时发生错误: {e}") + return None + + async def check_login_status(self): + """ + 循环检测扫码状态。 + 尝试 6 次后跳出循环,添加倒计时。 + 返回 True 如果登录成功,否则返回 False。 + """ + url = f"{self.base_url}/login/CheckLoginStatus" + params = {"key": self.auth_key} + + attempts = 0 # 初始化尝试次数 + max_attempts = 6 # 最大尝试次数 + countdown = 30 # 倒计时时长 + logger.info(f"请在 {countdown} 秒内扫码登录!!!") + while attempts < max_attempts: + async with aiohttp.ClientSession() as session: + try: + async with session.get(url, params=params) as response: + response_data = await response.json() + # 成功判断条件和数据提取路径 + if response.status == 200 and response_data.get("Code") == 200: + if response_data.get("Data") and response_data["Data"].get("state") is not None: + status = response_data["Data"]["state"] + logger.info(f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown-attempts*5}秒") + if status == 2: # 状态 2 表示登录成功 + logger.info("登录成功!") + self.wxid = response_data["Data"].get("wxid") + self.wxnewpass = response_data["Data"].get("wxnewpass") + logger.info(f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}") + self.save_credentials() # 登录成功后保存凭据 + return True + elif status == -2: # 二维码过期 + logger.error("二维码已过期,请重新获取。") + return False + else: + logger.error(f"检测登录状态成功但未找到登录状态: {response_data}") + else: + logger.info(f"检测登录状态失败: {response.status}, {response_data}") + + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + await asyncio.sleep(5) + attempts += 1 + continue + except Exception as e: + logger.error(f"检测登录状态时发生错误: {e}") + attempts += 1 + continue + + attempts += 1 + await asyncio.sleep(5) # 每隔5秒检测一次 + logger.warning("登录检测超过最大尝试次数,退出检测。") + return False + + async def connect_websocket(self): + """ + 建立 WebSocket 连接并处理接收到的消息。 + """ + os.environ["no_proxy"] = f"localhost,127.0.0.1,{self.host}" + ws_url = f"ws://{self.host}:{self.port}/ws/GetSyncMsg?key={self.auth_key}" + logger.info(f"正在连接 WebSocket: {ws_url}") + while True: + try: + async with websockets.connect(ws_url) as websocket: + self._websocket = websocket + logger.info("WebSocket 连接成功。") + while True: + try: + message = await websocket.recv() + asyncio.create_task(self.handle_websocket_message(message)) + except websockets.exceptions.ConnectionClosedOK: + logger.info("WebSocket 连接正常关闭。") + break + except Exception as e: + logger.error(f"处理 WebSocket 消息时发生错误: {e}") + # 在这里可以添加重连逻辑 + break + except Exception as e: + logger.error(f"WebSocket 连接失败: {e}") + # 在这里可以添加重连逻辑 + await asyncio.sleep(5) # 等待一段时间后重试 + + async def handle_websocket_message(self, message: str): + """ + 处理从 WebSocket 接收到的消息。 + """ + logger.info(f"收到 WebSocket 消息: {message}") + try: + message_data = json.loads(message) + # 检查消息结构,确保是有效的消息推送 + if message_data.get("msg_id") is not None and message_data.get("from_user_name") is not None: + abm = await self.convert_message(message_data) + if abm: + # 创建 WeChatPadProMessageEvent 实例 + message_event = WeChatPadProMessageEvent( + message_str=abm.message_str, + message_obj=abm, + platform_meta=self.meta(), + session_id=abm.session_id, + # 传递适配器实例,以便在事件中调用 send 方法 + adapter=self, + ) + # 提交事件到事件队列 + self.commit_event(message_event) + else: + logger.warning(f"收到未知结构的 WebSocket 消息: {message_data}") + + except json.JSONDecodeError: + logger.error(f"无法解析 WebSocket 消息为 JSON: {message}") + except Exception as e: + logger.error(f"处理 WebSocket 消息时发生错误: {e}") + + + async def convert_message(self, raw_message: dict) -> AstrBotMessage: + """ + 将 WeChatPadPro 原始消息转换为 AstrBotMessage。 + """ + abm = AstrBotMessage() + abm.raw_message = raw_message + abm.message_id = str(raw_message.get("msg_id")) + abm.timestamp = raw_message.get("create_time") + abm.self_id = self.wxid # 机器人的 wxid + + from_user_name = raw_message.get("from_user_name", {}).get("str", "") + to_user_name = raw_message.get("to_user_name", {}).get("str", "") + content = raw_message.get("content", {}).get("str", "") + msg_type = raw_message.get("msg_type") + + abm.message_str = content # 纯文本消息内容 (初始值) + abm.message = [] # Initialize message components list + + # 先判断群聊/私聊并设置基本属性 + await self._process_chat_type(abm, raw_message, from_user_name, to_user_name, content) + + # 如果是机器人自己发送的消息,忽略 + if from_user_name == self.wxid: + return None + + # 再根据消息类型处理消息内容 + self._process_message_content(abm, raw_message, msg_type, content) + + return abm + + async def _process_chat_type(self, abm: AstrBotMessage, raw_message: dict, from_user_name: str, to_user_name: str, content: str): + """ + 判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。 + """ + if "@chatroom" in from_user_name: + abm.type = MessageType.GROUP_MESSAGE + abm.group_id = from_user_name # 群聊 ID + + parts = content.split(":\n", 1) + sender_wxid = "" + if len(parts) == 2: + sender_wxid = parts[0] + sender_name_from_content = parts[1] + + abm.sender = MessageMember(user_id=sender_wxid, nickname="") + + # 如果需要更准确的群昵称,调用 GetChatroomMemberDetail 接口 + if sender_wxid: # 只有当发送者 wxid 可用时才尝试获取更准确的昵称 + accurate_nickname = await self._get_group_member_nickname(abm.group_id, sender_wxid) + if accurate_nickname: + abm.sender.nickname = accurate_nickname + + # 对于群聊,session_id 可以是群聊 ID 或发送者 ID + 群聊 ID (如果 unique_session 为 True) + if self.unique_session: + # 需要获取发送者的 wxid,这可能需要额外的接口调用或从消息中解析 + # 暂时使用 from_user_name 作为 session_id 的一部分 + abm.session_id = f"{from_user_name}_{to_user_name}" # 示例,可能需要调整 + else: + abm.session_id = from_user_name + # logger.info("跳过群消息") + # pass + else: + abm.type = MessageType.FRIEND_MESSAGE + abm.group_id = "" # 私聊没有群组 ID + abm.sender = MessageMember(user_id=from_user_name, nickname="") # 暂时没有私聊发送者的昵称 + abm.session_id = from_user_name # 私聊的 session_id 是发送者 ID + # 如果是来自 'weixin' 的消息,忽略 + if from_user_name == 'weixin': + logger.info("收到来自 'weixin' 的消息,忽略!") + return + + async def _get_group_member_nickname(self, group_id: str, member_wxid: str) -> Optional[str]: + """ + 通过接口获取群成员的昵称。 + """ + url = f"{self.base_url}/group/GetChatroomMemberDetail" + params = {"key": self.auth_key} + payload = { + "ChatRoomName": group_id, + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + response_data = await response.json() + if response.status == 200 and response_data.get("Code") == 200: + # 从返回数据中查找对应成员的昵称 + member_list = response_data.get("Data", {}).get("member_data", {}).get("chatroom_member_list", []) + for member in member_list: + if member.get("user_name") == member_wxid: + return member.get("nick_name") + logger.warning(f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称") + return None + else: + logger.error(f"获取群成员详情失败: {response.status}, {response_data}") + return None + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + return None + except Exception as e: + logger.error(f"获取群成员详情时发生错误: {e}") + return None + + @staticmethod + def _process_message_content(abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str): + """ + 根据消息类型处理消息内容,填充 AstrBotMessage 的 message 列表。 + """ + if msg_type == 1: # 文本消息 + # 对于群聊消息,从 content 中提取实际消息内容 + if abm.type == MessageType.GROUP_MESSAGE: + parts = content.split(":\n", 1) + if len(parts) == 2: + abm.message_str = parts[1] # 更新纯文本消息内容为实际消息内容 + abm.message.append(Plain(abm.message_str)) + else: + # 如果群聊消息格式不符合预期,仍然使用原始 content + abm.message.append(Plain(abm.message_str)) + else: # 私聊消息 + abm.message.append(Plain(abm.message_str)) + elif msg_type == 3: # 图片消息 + # TODO: 从 raw_message 中提取图片信息并创建 Image 组件 + logger.warning(f"收到图片消息,待实现处理: {raw_message}") + # 示例:abm.message.append(Image(file="图片文件路径或URL")) + pass + elif msg_type == 47: # 视频消息 (注意:表情消息也是 47,需要区分) + # TODO: 从 raw_message 中提取视频信息并创建 Video 组件 + logger.warning(f"收到视频消息,待实现处理: {raw_message}") + # 示例:abm.message.append(Video(file="视频文件路径或URL")) + pass + elif msg_type == 50: # 语音/视频 (根据上下文判断是语音还是视频) + # TODO: 从 raw_message 中提取语音信息并创建 Record 组件 + logger.warning(f"收到语音/视频消息,待实现处理: {raw_message}") + # 示例:abm.message.append(Record(file="语音文件路径或URL")) + pass + elif msg_type == 49: # 引用消息 + # TODO: 解析 content 中的 XML,提取引用内容和发送者信息 + logger.warning(f"收到引用消息,待实现处理: {raw_message}") + # 示例:abm.message.append(Reply(id="被引用消息ID", sender_id="被引用消息发送者ID")) + try: + import xml.etree.ElementTree as ET + root = ET.fromstring(content) + # 示例:提取被引用消息的发送者和内容 + # referenced_sender = root.find('.//dataitemsource/fromusr').text + # referenced_content = root.find('.//datadesc').text + # logger.info(f"引用消息解析结果 - 发送者: {referenced_sender}, 内容: {referenced_content}") + # 根据需要创建 Reply 组件或其他组件 + except Exception as e: + logger.error(f"解析引用消息 XML 失败: {e}") + pass + # elif msg_type == ... # Add handling for other message types here + else: + logger.warning(f"收到未处理的消息类型: {msg_type}, 原始消息: {raw_message}") + # abm.message remains empty [] for unhandled types + + async def terminate(self): + """ + 终止一个平台的运行实例。 + """ + logger.info("正在终止 WeChatPadPro 适配器...") + # 关闭 WebSocket 连接 + if self._websocket: + await self._websocket.close() + # 在这里实现终止 WeChatPadPro 客户端或连接的逻辑 + # await self.client.stop() + if hasattr(self, '_shutdown_event'): + self._shutdown_event.set() + + + def meta(self) -> PlatformMetadata: + """ + 得到一个平台的元数据。 + """ + return self.metadata + + async def send_by_session( + self, session: MessageSesion, message_chain: MessageChain + ) -> Awaitable[Any]: + """ + 通过会话发送消息。 + """ + logger.info(f"向会话 {session} 发送消息: {message_chain}") + # 在这里实现将 MessageChain 转换为 WeChatPadPro 消息格式并发送的逻辑 + # 例如: + # message_text = "".join([comp.text for comp in message_chain if isinstance(comp, Plain)]) + # await self.client.send_message(session.session_id, message_text) + pass # 待实现 diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py new file mode 100644 index 000000000..203a0f814 --- /dev/null +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -0,0 +1,183 @@ +import time +import aiohttp +from astrbot.core.platform.astr_message_event import AstrMessageEvent +from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType +from astrbot.core.platform.platform_metadata import PlatformMetadata +from astrbot.core.message.message_event_result import MessageChain +from astrbot.core.message.components import Plain, Image # Import Image +from astrbot import logger +import base64 +from PIL import Image as PILImage # 使用别名避免冲突 +import io + + +class WeChatPadProMessageEvent(AstrMessageEvent): + def __init__( + self, + message_str: str, + message_obj: AstrBotMessage, + platform_meta: PlatformMetadata, + session_id: str, + # 添加平台特定的参数,例如适配器实例 + adapter: object, # 传递适配器实例 + ): + # logger.info(f"WeChatPadProMessageEvent __init__ called with:") + # logger.info(f" message_str: {message_str}") + # logger.info(f" message_obj: {message_obj}") + # logger.info(f" message_obj.message: {message_obj.message}") # Log the message components list + # logger.info(f" platform_meta: {platform_meta}") + # logger.info(f" session_id: {session_id}") + # logger.info(f" adapter: {adapter}") + + # Pass the message components list to the parent class constructor + # Pass message_str to the parent class constructor, similar to gewechat adapter + # Pass message_str and message_obj to the parent class constructor, similar to fake adapter + super().__init__(message_str, message_obj, platform_meta, session_id) + self.message_obj = message_obj # Save the full message object + self.adapter = adapter # Save the adapter instance + + async def send(self, message: MessageChain): + """ + 发送消息到 WeChatPadPro 平台。 + """ + # 在这里实现将 MessageChain 转换为 WeChatPadPro 消息格式并发送的逻辑 + # 遍历消息链,处理不同类型的消息组件 + for component in message.chain: + # logger.info(f"Processing component: {component}") # Log the component + # logger.info(f"Type of component: {type(component)}") # Log the type of the component + # logger.info(f"Image class in scope: {Image}") # Log the Image class itself + time.sleep(1) + if isinstance(component, Plain): + # 发送文本消息 + message_text = component.text + # 实现 reply_with_mention 功能 + if ( + self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息 + and self.adapter.settings.get("reply_with_mention", False) # 检查适配器设置是否启用 reply_with_mention + and self.message_obj.sender # 确保有发送者信息 + and (self.message_obj.sender.user_id or self.message_obj.sender.nickname) # 确保发送者有 ID 或昵称 + ): + # 在文本消息前加上 @ 消息发送者的信息 + # 优先使用 nickname,如果没有则使用 user_id + mention_text = self.message_obj.sender.nickname if self.message_obj.sender.nickname else self.message_obj.sender.user_id + message_text = f"@{mention_text} {message_text}" + logger.info(f"已添加 @ 信息: {message_text}") + + if message_text: + payload = { + "MsgItem": [ + { + "MsgType": 1, # 1 for Text + "TextContent": message_text, + "ToUserName": self.session_id, # 接收者 wxid + } + ] + } + url = f"{self.adapter.base_url}/message/SendTextMessage" # 使用文本消息发送接口 + params = {"key": self.adapter.auth_key} + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + response_data = await response.json() + if response.status == 200 and response_data.get("Code") == 200: + logger.info(f"成功发送文本消息到 {self.session_id}: {message_text}") + else: + logger.error(f"发送文本消息失败到 {self.session_id}: {response.status}, {response_data}") + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + except Exception as e: + logger.error(f"发送文本消息时发生错误: {e}") + + elif isinstance(component, Image): + # 发送图片消息 + try: + # 假设 Image 对象有 to_base64() 方法 + image_base64 = await component.convert_to_base64() # 需要 Image 组件支持转为 base64 + # logger.info(f"转换后的base64图片:{image_base64}") + + # Base64图片格式校验 + try: + image_data = base64.b64decode(image_base64, validate=True) + logger.info("Base64图片格式校验成功。") + except (base64.binascii.Error, ValueError) as e: + logger.error(f"Base64图片格式校验失败: {e}") + await self.send(MessageChain([Plain("发送图片失败:图片编码格式不正确。")])) + continue # 跳过发送此图片 + + # 图片压缩处理 + try: + img = PILImage.open(io.BytesIO(image_data)) # 使用别名 PILImage + + # 示例压缩:对于 JPEG 格式,降低质量;对于其他格式,转换为 JPEG 并降低质量 + output_buffer = io.BytesIO() + if img.format == 'JPEG': + img.save(output_buffer, format='JPEG', quality=80) # 降低JPEG质量到80 + else: + # 尝试转换为JPEG进行压缩,如果图片是透明的,先转换为RGB + if img.mode in ('RGBA', 'P'): + img = img.convert('RGB') + img.save(output_buffer, format='JPEG', quality=80) # 转换为JPEG并降低质量 + + compressed_image_base64 = base64.b64encode(output_buffer.getvalue()).decode('utf-8') + logger.info(f"图片压缩成功,原大小: {len(image_base64)} bytes, 压缩后大小: {len(compressed_image_base64)} bytes") + image_base64_to_send = compressed_image_base64 # 使用压缩后的base64 + except Exception as e: + logger.error(f"图片压缩处理失败: {e}") + # 如果压缩失败,可以选择发送原图或者跳过 + # 这里选择发送原图,或者可以根据需求发送错误消息并跳过 + image_base64_to_send = image_base64 # 压缩失败,发送原图 + logger.warning("图片压缩失败,将尝试发送原图。") + + + payload = { + "MsgItem": [ + { + "AtWxIDList": [], # 根据需要添加 @ 的用户 wxid 列表 + "ImageContent": image_base64_to_send, # 使用处理后的base64 + "MsgType": 3, # 图片消息类型 + "TextContent": "", + "ToUserName": self.session_id, # 接收者 wxid + } + ] + } + url = f"{self.adapter.base_url}/message/SendImageNewMessage" # 使用新的图片发送接口 + params = {"key": self.adapter.auth_key} + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + response_data = await response.json() + logger.info(response_data) + if response.status == 200 and response_data.get("Code") == 200: + logger.info(f"成功发送图片消息到 {self.session_id}") + else: + logger.error(f"发送图片消息失败到 {self.session_id}: {response.status}, {response_data}") + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + except Exception as e: + logger.error(f"发送图片消息时发生错误: {e}") + + except Exception as e: + logger.error(f"处理图片消息失败: {e}") + # 可以选择发送一个错误提示文本消息 + await self.send(MessageChain([Plain("发送图片失败。")])) + # TODO: 添加对其他消息组件类型的处理 (Record, Video, At等) + # elif isinstance(component, Record): + # pass + # elif isinstance(component, Video): + # pass + # elif isinstance(component, At): + # pass + # ... + + await super().send(message) # 调用父类的 send 方法进行指标上报等操作 + + + # 根据 WeChatPadPro 的事件特点,可能需要重写 AstrMessageEvent 中的其他方法 + # 例如: + # def get_sender_id(self) -> str: + # # 从 self.message_obj 中获取发送者 ID + # return self.message_obj.sender.user_id + # + # def is_private_chat(self) -> bool: + # # 根据 self.message_obj 判断是否是私聊 + # return self.message_obj.type == MessageType.FRIEND_MESSAGE \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f800f131e..4bfd9e89d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,4 +33,5 @@ telegramify-markdown google-genai click filelock -watchfiles \ No newline at end of file +watchfiles +websockets \ No newline at end of file From 1593bcb5372601f86335b737e2d13a40e6ae87c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Thu, 15 May 2025 17:50:29 +0800 Subject: [PATCH 14/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index ad0a5fe16..ec1d9aa3c 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -345,8 +345,10 @@ class WeChatPadProAdapter(Platform): # 先判断群聊/私聊并设置基本属性 await self._process_chat_type(abm, raw_message, from_user_name, to_user_name, content) - # 如果是机器人自己发送的消息,忽略 - if from_user_name == self.wxid: + # 如果是机器人自己发送的消息、回显消息或系统消息,忽略 + is_echo = raw_message.get("is_echo", False) or raw_message.get("msg_source") == "send" + is_system = msg_type in ("system", "sys") + if from_user_name == self.wxid or to_user_name == self.wxid or is_echo or is_system: return None # 再根据消息类型处理消息内容 From f70b8f0c109c373265e5beb5bf6a43c93a4c668a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Thu, 15 May 2025 19:09:56 +0800 Subject: [PATCH 15/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index ec1d9aa3c..1452ee7af 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -508,8 +508,3 @@ class WeChatPadProAdapter(Platform): 通过会话发送消息。 """ logger.info(f"向会话 {session} 发送消息: {message_chain}") - # 在这里实现将 MessageChain 转换为 WeChatPadPro 消息格式并发送的逻辑 - # 例如: - # message_text = "".join([comp.text for comp in message_chain if isinstance(comp, Plain)]) - # await self.client.send_message(session.session_id, message_text) - pass # 待实现 From 6ea5b7581f1c1bd834dee51ff1cb8a0c78015457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Thu, 15 May 2025 19:12:42 +0800 Subject: [PATCH 16/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../wechatpadpro/wechatpadpro_message_event.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 203a0f814..513a47e34 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -90,9 +90,15 @@ class WeChatPadProMessageEvent(AstrMessageEvent): elif isinstance(component, Image): # 发送图片消息 - try: - # 假设 Image 对象有 to_base64() 方法 - image_base64 = await component.convert_to_base64() # 需要 Image 组件支持转为 base64 + if hasattr(component, "convert_to_base64"): + try: + image_base64 = await component.convert_to_base64() + except Exception as e: + logger.error(f"Error converting image to base64: {e}") + continue + else: + logger.error("Image component missing convert_to_base64, skipping image send.") + continue # logger.info(f"转换后的base64图片:{image_base64}") # Base64图片格式校验 From 864b6bc56d5daad3ed19851e7f6b26698054921b Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Thu, 15 May 2025 20:00:46 +0800 Subject: [PATCH 17/54] =?UTF-8?q?fix:=20=F0=9F=A4=A0=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E5=90=8E=E6=9C=89@=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=A7=A6=E5=8F=91=E6=8C=87=E4=BB=A4=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 9d882741c..890ff0f8d 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -316,7 +316,7 @@ class AiocqhttpAdapter(Platform): first_at_self_processed = True else: # 非第一个@机器人或@其他用户,添加到message_str - message_str += f"@{nickname} " + message_str += f" @{nickname} " else: abm.message.append(At(qq=str(m["data"]["qq"]), name="")) except ActionFailed as e: From 946595216a3166b63b7cf9a28c3631ec701dd8b4 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Thu, 15 May 2025 20:43:33 +0800 Subject: [PATCH 18/54] =?UTF-8?q?=E4=BC=98=E5=8C=96wechapadpro=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 66 ++---- .../wechatpadpro_message_event.py | 216 +++++------------- 2 files changed, 87 insertions(+), 195 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 1452ee7af..9e5160742 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -6,8 +6,6 @@ import websockets from typing import Awaitable, Any, Optional, Coroutine from astrbot.api.message_components import Plain, Image, At, Record, Video from astrbot.api.platform import Platform, PlatformMetadata -from astrbot.api.event import MessageChain -from astrbot.core.platform.astr_message_event import MessageSesion from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageMember, MessageType from ...register import register_platform_adapter from astrbot import logger @@ -20,6 +18,8 @@ class WeChatPadProAdapter(Platform): self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue ) -> None: super().__init__(event_queue) + self._shutdown_event = None + self.wxnewpass = None self.config = platform_config self.settings = platform_settings self.unique_session = platform_settings.get("unique_session", False) @@ -236,7 +236,6 @@ class WeChatPadProAdapter(Platform): status = response_data["Data"]["state"] logger.info(f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown-attempts*5}秒") if status == 2: # 状态 2 表示登录成功 - logger.info("登录成功!") self.wxid = response_data["Data"].get("wxid") self.wxnewpass = response_data["Data"].get("wxnewpass") logger.info(f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}") @@ -324,7 +323,7 @@ class WeChatPadProAdapter(Platform): logger.error(f"处理 WebSocket 消息时发生错误: {e}") - async def convert_message(self, raw_message: dict) -> AstrBotMessage: + async def convert_message(self, raw_message: dict) -> AstrBotMessage | None: """ 将 WeChatPadPro 原始消息转换为 AstrBotMessage。 """ @@ -332,7 +331,7 @@ class WeChatPadProAdapter(Platform): abm.raw_message = raw_message abm.message_id = str(raw_message.get("msg_id")) abm.timestamp = raw_message.get("create_time") - abm.self_id = self.wxid # 机器人的 wxid + abm.self_id = self.wxid from_user_name = raw_message.get("from_user_name", {}).get("str", "") to_user_name = raw_message.get("to_user_name", {}).get("str", "") @@ -342,60 +341,55 @@ class WeChatPadProAdapter(Platform): abm.message_str = content # 纯文本消息内容 (初始值) abm.message = [] # Initialize message components list - # 先判断群聊/私聊并设置基本属性 - await self._process_chat_type(abm, raw_message, from_user_name, to_user_name, content) - # 如果是机器人自己发送的消息、回显消息或系统消息,忽略 - is_echo = raw_message.get("is_echo", False) or raw_message.get("msg_source") == "send" - is_system = msg_type in ("system", "sys") - if from_user_name == self.wxid or to_user_name == self.wxid or is_echo or is_system: - return None + if from_user_name == self.wxid: + logger.info("忽略自己发送的消息!!!") + return None - # 再根据消息类型处理消息内容 - self._process_message_content(abm, raw_message, msg_type, content) + # 先判断群聊/私聊并设置基本属性 + if await self._process_chat_type(abm, raw_message, from_user_name, to_user_name, content): + # 再根据消息类型处理消息内容 + self._process_message_content(abm, raw_message, msg_type, content) - return abm + return abm + return None async def _process_chat_type(self, abm: AstrBotMessage, raw_message: dict, from_user_name: str, to_user_name: str, content: str): """ 判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。 """ + if from_user_name == 'weixin': + logger.info("忽略微信团队的消息!!!") + return False if "@chatroom" in from_user_name: + logger.info("开始处理群消息!") abm.type = MessageType.GROUP_MESSAGE - abm.group_id = from_user_name # 群聊 ID + abm.group_id = from_user_name parts = content.split(":\n", 1) sender_wxid = "" if len(parts) == 2: sender_wxid = parts[0] - sender_name_from_content = parts[1] abm.sender = MessageMember(user_id=sender_wxid, nickname="") - # 如果需要更准确的群昵称,调用 GetChatroomMemberDetail 接口 - if sender_wxid: # 只有当发送者 wxid 可用时才尝试获取更准确的昵称 + # 获取群聊发送者的nickname + if sender_wxid: accurate_nickname = await self._get_group_member_nickname(abm.group_id, sender_wxid) if accurate_nickname: abm.sender.nickname = accurate_nickname # 对于群聊,session_id 可以是群聊 ID 或发送者 ID + 群聊 ID (如果 unique_session 为 True) if self.unique_session: - # 需要获取发送者的 wxid,这可能需要额外的接口调用或从消息中解析 - # 暂时使用 from_user_name 作为 session_id 的一部分 - abm.session_id = f"{from_user_name}_{to_user_name}" # 示例,可能需要调整 + abm.session_id = f"{from_user_name}_{to_user_name}" else: abm.session_id = from_user_name - # logger.info("跳过群消息") - # pass else: abm.type = MessageType.FRIEND_MESSAGE - abm.group_id = "" # 私聊没有群组 ID + abm.group_id = "" abm.sender = MessageMember(user_id=from_user_name, nickname="") # 暂时没有私聊发送者的昵称 - abm.session_id = from_user_name # 私聊的 session_id 是发送者 ID - # 如果是来自 'weixin' 的消息,忽略 - if from_user_name == 'weixin': - logger.info("收到来自 'weixin' 的消息,忽略!") - return + abm.session_id = from_user_name + return True async def _get_group_member_nickname(self, group_id: str, member_wxid: str) -> Optional[str]: """ @@ -418,10 +412,9 @@ class WeChatPadProAdapter(Platform): if member.get("user_name") == member_wxid: return member.get("nick_name") logger.warning(f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称") - return None else: logger.error(f"获取群成员详情失败: {response.status}, {response_data}") - return None + return None except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") return None @@ -476,7 +469,6 @@ class WeChatPadProAdapter(Platform): except Exception as e: logger.error(f"解析引用消息 XML 失败: {e}") pass - # elif msg_type == ... # Add handling for other message types here else: logger.warning(f"收到未处理的消息类型: {msg_type}, 原始消息: {raw_message}") # abm.message remains empty [] for unhandled types @@ -500,11 +492,3 @@ class WeChatPadProAdapter(Platform): 得到一个平台的元数据。 """ return self.metadata - - async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain - ) -> Awaitable[Any]: - """ - 通过会话发送消息。 - """ - logger.info(f"向会话 {session} 发送消息: {message_chain}") diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 513a47e34..4adf5bb17 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -1,4 +1,4 @@ -import time +import asyncio import aiohttp from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType @@ -21,169 +21,77 @@ class WeChatPadProMessageEvent(AstrMessageEvent): # 添加平台特定的参数,例如适配器实例 adapter: object, # 传递适配器实例 ): - # logger.info(f"WeChatPadProMessageEvent __init__ called with:") - # logger.info(f" message_str: {message_str}") - # logger.info(f" message_obj: {message_obj}") - # logger.info(f" message_obj.message: {message_obj.message}") # Log the message components list - # logger.info(f" platform_meta: {platform_meta}") - # logger.info(f" session_id: {session_id}") - # logger.info(f" adapter: {adapter}") - - # Pass the message components list to the parent class constructor - # Pass message_str to the parent class constructor, similar to gewechat adapter - # Pass message_str and message_obj to the parent class constructor, similar to fake adapter super().__init__(message_str, message_obj, platform_meta, session_id) self.message_obj = message_obj # Save the full message object self.adapter = adapter # Save the adapter instance async def send(self, message: MessageChain): - """ - 发送消息到 WeChatPadPro 平台。 - """ - # 在这里实现将 MessageChain 转换为 WeChatPadPro 消息格式并发送的逻辑 - # 遍历消息链,处理不同类型的消息组件 - for component in message.chain: - # logger.info(f"Processing component: {component}") # Log the component - # logger.info(f"Type of component: {type(component)}") # Log the type of the component - # logger.info(f"Image class in scope: {Image}") # Log the Image class itself - time.sleep(1) - if isinstance(component, Plain): - # 发送文本消息 - message_text = component.text - # 实现 reply_with_mention 功能 - if ( - self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息 - and self.adapter.settings.get("reply_with_mention", False) # 检查适配器设置是否启用 reply_with_mention - and self.message_obj.sender # 确保有发送者信息 - and (self.message_obj.sender.user_id or self.message_obj.sender.nickname) # 确保发送者有 ID 或昵称 - ): - # 在文本消息前加上 @ 消息发送者的信息 - # 优先使用 nickname,如果没有则使用 user_id - mention_text = self.message_obj.sender.nickname if self.message_obj.sender.nickname else self.message_obj.sender.user_id - message_text = f"@{mention_text} {message_text}" - logger.info(f"已添加 @ 信息: {message_text}") + async with aiohttp.ClientSession() as session: + for comp in message.chain: + await asyncio.sleep(1) + if isinstance(comp, Plain): + await self._send_text(session, comp.text) + elif isinstance(comp, Image): + await self._send_image(session, comp) + await super().send(message) - if message_text: - payload = { - "MsgItem": [ - { - "MsgType": 1, # 1 for Text - "TextContent": message_text, - "ToUserName": self.session_id, # 接收者 wxid - } - ] - } - url = f"{self.adapter.base_url}/message/SendTextMessage" # 使用文本消息发送接口 - params = {"key": self.adapter.auth_key} - async with aiohttp.ClientSession() as session: - try: - async with session.post(url, params=params, json=payload) as response: - response_data = await response.json() - if response.status == 200 and response_data.get("Code") == 200: - logger.info(f"成功发送文本消息到 {self.session_id}: {message_text}") - else: - logger.error(f"发送文本消息失败到 {self.session_id}: {response.status}, {response_data}") - except aiohttp.ClientConnectorError as e: - logger.error(f"连接到 WeChatPadPro 服务失败: {e}") - except Exception as e: - logger.error(f"发送文本消息时发生错误: {e}") + async def _send_image(self, session: aiohttp.ClientSession, comp: Image): + b64 = await comp.convert_to_base64() + raw = self._validate_base64(b64) + b64c = self._compress_image(raw) + payload = {"MsgItem":[{"ImageContent": b64c, "MsgType":3, "ToUserName": self.session_id}]} + url = f"{self.adapter.base_url}/message/SendImageNewMessage" + await self._post(session, url, payload) - elif isinstance(component, Image): - # 发送图片消息 - if hasattr(component, "convert_to_base64"): - try: - image_base64 = await component.convert_to_base64() - except Exception as e: - logger.error(f"Error converting image to base64: {e}") - continue - else: - logger.error("Image component missing convert_to_base64, skipping image send.") - continue - # logger.info(f"转换后的base64图片:{image_base64}") + async def _send_text(self, session: aiohttp.ClientSession, text: str): + if ( + self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息 + and self.adapter.settings.get("reply_with_mention", False) # 检查适配器设置是否启用 reply_with_mention + and self.message_obj.sender # 确保有发送者信息 + and (self.message_obj.sender.user_id or self.message_obj.sender.nickname) # 确保发送者有 ID 或昵称 + ): + # 优先使用 nickname,如果没有则使用 user_id + mention_text = self.message_obj.sender.nickname or self.message_obj.sender.user_id + message_text = f"@{mention_text} {text}" + logger.info(f"已添加 @ 信息: {message_text}") - # Base64图片格式校验 - try: - image_data = base64.b64decode(image_base64, validate=True) - logger.info("Base64图片格式校验成功。") - except (base64.binascii.Error, ValueError) as e: - logger.error(f"Base64图片格式校验失败: {e}") - await self.send(MessageChain([Plain("发送图片失败:图片编码格式不正确。")])) - continue # 跳过发送此图片 + payload = {"MsgItem": [{"MsgType": 1, "TextContent": text, "ToUserName": self.session_id}]} + url = f"{self.adapter.base_url}/message/SendTextMessage" + await self._post(session, url, payload) - # 图片压缩处理 - try: - img = PILImage.open(io.BytesIO(image_data)) # 使用别名 PILImage + @staticmethod + def _validate_base64(b64: str) -> bytes: + return base64.b64decode(b64, validate=True) - # 示例压缩:对于 JPEG 格式,降低质量;对于其他格式,转换为 JPEG 并降低质量 - output_buffer = io.BytesIO() - if img.format == 'JPEG': - img.save(output_buffer, format='JPEG', quality=80) # 降低JPEG质量到80 - else: - # 尝试转换为JPEG进行压缩,如果图片是透明的,先转换为RGB - if img.mode in ('RGBA', 'P'): - img = img.convert('RGB') - img.save(output_buffer, format='JPEG', quality=80) # 转换为JPEG并降低质量 + @staticmethod + def _compress_image(data: bytes) -> str: + img = PILImage.open(io.BytesIO(data)) + buf = io.BytesIO() + if img.format == "JPEG": + img.save(buf, "JPEG", quality=80) + else: + if img.mode in ("RGBA","P"): + img = img.convert("RGB") + img.save(buf, "JPEG", quality=80) + # logger.info("图片处理完成!!!") + return base64.b64encode(buf.getvalue()).decode() - compressed_image_base64 = base64.b64encode(output_buffer.getvalue()).decode('utf-8') - logger.info(f"图片压缩成功,原大小: {len(image_base64)} bytes, 压缩后大小: {len(compressed_image_base64)} bytes") - image_base64_to_send = compressed_image_base64 # 使用压缩后的base64 - except Exception as e: - logger.error(f"图片压缩处理失败: {e}") - # 如果压缩失败,可以选择发送原图或者跳过 - # 这里选择发送原图,或者可以根据需求发送错误消息并跳过 - image_base64_to_send = image_base64 # 压缩失败,发送原图 - logger.warning("图片压缩失败,将尝试发送原图。") + async def _post(self, session, url, payload): + params = {"key": self.adapter.auth_key} + try: + async with session.post(url, params=params, json=payload) as resp: + data = await resp.json() + if resp.status != 200 or data.get("Code") != 200: + logger.error(f"{url} failed: {resp.status} {data}") + except Exception as e: + logger.error(f"{url} error: {e}") - payload = { - "MsgItem": [ - { - "AtWxIDList": [], # 根据需要添加 @ 的用户 wxid 列表 - "ImageContent": image_base64_to_send, # 使用处理后的base64 - "MsgType": 3, # 图片消息类型 - "TextContent": "", - "ToUserName": self.session_id, # 接收者 wxid - } - ] - } - url = f"{self.adapter.base_url}/message/SendImageNewMessage" # 使用新的图片发送接口 - params = {"key": self.adapter.auth_key} - async with aiohttp.ClientSession() as session: - try: - async with session.post(url, params=params, json=payload) as response: - response_data = await response.json() - logger.info(response_data) - if response.status == 200 and response_data.get("Code") == 200: - logger.info(f"成功发送图片消息到 {self.session_id}") - else: - logger.error(f"发送图片消息失败到 {self.session_id}: {response.status}, {response_data}") - except aiohttp.ClientConnectorError as e: - logger.error(f"连接到 WeChatPadPro 服务失败: {e}") - except Exception as e: - logger.error(f"发送图片消息时发生错误: {e}") - - except Exception as e: - logger.error(f"处理图片消息失败: {e}") - # 可以选择发送一个错误提示文本消息 - await self.send(MessageChain([Plain("发送图片失败。")])) - # TODO: 添加对其他消息组件类型的处理 (Record, Video, At等) - # elif isinstance(component, Record): - # pass - # elif isinstance(component, Video): - # pass - # elif isinstance(component, At): - # pass - # ... - - await super().send(message) # 调用父类的 send 方法进行指标上报等操作 - - - # 根据 WeChatPadPro 的事件特点,可能需要重写 AstrMessageEvent 中的其他方法 - # 例如: - # def get_sender_id(self) -> str: - # # 从 self.message_obj 中获取发送者 ID - # return self.message_obj.sender.user_id - # - # def is_private_chat(self) -> bool: - # # 根据 self.message_obj 判断是否是私聊 - # return self.message_obj.type == MessageType.FRIEND_MESSAGE \ No newline at end of file +# TODO: 添加对其他消息组件类型的处理 (Record, Video, At等) +# elif isinstance(component, Record): +# pass +# elif isinstance(component, Video): +# pass +# elif isinstance(component, At): +# pass +# ... From e0ce6d9688d07b7118b7192e24cbaf5144305320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Thu, 15 May 2025 20:57:22 +0800 Subject: [PATCH 19/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 9e5160742..059deffe4 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -46,9 +46,7 @@ class WeChatPadProAdapter(Platform): """ logger.info("WeChatPadPro 适配器正在启动...") - # 尝试从文件中加载凭据 - loaded_credentials = self.load_credentials() - if loaded_credentials: + if loaded_credentials := self.load_credentials(): self.auth_key = loaded_credentials.get("auth_key") self.wxid = loaded_credentials.get("wxid") From 26482fc2d39712fc722c0e55249cd02b0558ebff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Thu, 15 May 2025 20:59:53 +0800 Subject: [PATCH 20/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 059deffe4..fc1fb24d4 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -365,10 +365,7 @@ class WeChatPadProAdapter(Platform): abm.group_id = from_user_name parts = content.split(":\n", 1) - sender_wxid = "" - if len(parts) == 2: - sender_wxid = parts[0] - + sender_wxid = parts[0] if len(parts) == 2 else "" abm.sender = MessageMember(user_id=sender_wxid, nickname="") # 获取群聊发送者的nickname From 991dfeb2f2f0334ec1f533327f2fec5bc929e728 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Fri, 16 May 2025 09:28:15 +0800 Subject: [PATCH 21/54] style: format code, disable redundant logs --- astrbot/core/platform/manager.py | 2 +- .../wechatpadpro/wechatpadpro_adapter.py | 216 ++++++++++++------ .../wechatpadpro_message_event.py | 50 ++-- 3 files changed, 175 insertions(+), 93 deletions(-) diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index 9473e9199..494900564 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -64,7 +64,7 @@ class PlatformManager: ) case "wechatpadpro": from .sources.wechatpadpro.wechatpadpro_adapter import ( - WeChatPadProAdapter, + WeChatPadProAdapter, # noqa: F401 ) case "lark": from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401 diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index fc1fb24d4..bc57958f8 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -1,17 +1,25 @@ import asyncio -import aiohttp import json import os +from typing import Optional + +import aiohttp import websockets -from typing import Awaitable, Any, Optional, Coroutine -from astrbot.api.message_components import Plain, Image, At, Record, Video -from astrbot.api.platform import Platform, PlatformMetadata -from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageMember, MessageType -from ...register import register_platform_adapter + from astrbot import logger +from astrbot.api.message_components import Plain +from astrbot.api.platform import Platform, PlatformMetadata +from astrbot.core.platform.astrbot_message import ( + AstrBotMessage, + MessageMember, + MessageType, +) from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +from ...register import register_platform_adapter from .wechatpadpro_message_event import WeChatPadProMessageEvent + @register_platform_adapter("wechatpadpro", "WeChatPadPro 消息平台适配器") class WeChatPadProAdapter(Platform): def __init__( @@ -35,10 +43,12 @@ class WeChatPadProAdapter(Platform): self.host = self.config.get("host") self.port = self.config.get("port") self.base_url = f"http://{self.host}:{self.port}" - self.auth_key = None # 用于保存生成的授权码 - self.wxid = None # 用于保存登录成功后的 wxid - self.credentials_file = os.path.join(get_astrbot_data_path(), "wechatpadpro_credentials.json") # 持久化文件路径 - self._websocket = None # 用于保存 WebSocket 连接 + self.auth_key = None # 用于保存生成的授权码 + self.wxid = None # 用于保存登录成功后的 wxid + self.credentials_file = os.path.join( + get_astrbot_data_path(), "wechatpadpro_credentials.json" + ) # 持久化文件路径 + self._websocket = None # 用于保存 WebSocket 连接 async def run(self) -> None: """ @@ -84,7 +94,6 @@ class WeChatPadProAdapter(Platform): await self.terminate() return - # 示例:保持运行直到终止事件被设置 self._shutdown_event = asyncio.Event() await self._shutdown_event.wait() @@ -140,10 +149,14 @@ class WeChatPadProAdapter(Platform): logger.info("WeChatPadPro 设备当前在线。") return True else: - logger.info(f"WeChatPadPro 设备不在线,登录状态: {login_state}") + logger.info( + f"WeChatPadPro 设备不在线,登录状态: {login_state}" + ) return False else: - logger.error(f"检查在线状态失败: {response.status}, {response_data}") + logger.error( + f"检查在线状态失败: {response.status}, {response_data}" + ) return False except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") @@ -152,14 +165,13 @@ class WeChatPadProAdapter(Platform): logger.error(f"检查在线状态时发生错误: {e}") return False - async def generate_auth_key(self): """ 生成授权码。 """ url = f"{self.base_url}/admin/GenAuthKey1" params = {"key": self.admin_key} - payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码 + payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码 async with aiohttp.ClientSession() as session: try: @@ -168,13 +180,21 @@ class WeChatPadProAdapter(Platform): # 修正成功判断条件和授权码提取路径 if response.status == 200 and response_data.get("Code") == 200: # 授权码在 Data 字段的列表中 - if response_data.get("Data") and isinstance(response_data["Data"], list) and len(response_data["Data"]) > 0: - self.auth_key = response_data["Data"][0] - logger.info(f"成功获取授权码: {self.auth_key}") + if ( + response_data.get("Data") + and isinstance(response_data["Data"], list) + and len(response_data["Data"]) > 0 + ): + self.auth_key = response_data["Data"][0] + logger.info(f"成功获取授权码: {self.auth_key}") else: - logger.error(f"生成授权码成功但未找到授权码: {response_data}") + logger.error( + f"生成授权码成功但未找到授权码: {response_data}" + ) else: - logger.error(f"生成授权码失败: {response.status}, {response_data}") + logger.error( + f"生成授权码失败: {response.status}, {response_data}" + ) except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") except Exception as e: @@ -186,7 +206,7 @@ class WeChatPadProAdapter(Platform): """ url = f"{self.base_url}/login/GetLoginQrCodeNew" params = {"key": self.auth_key} - payload = {} # 根据文档,这个接口的 body 可以为空 + payload = {} # 根据文档,这个接口的 body 可以为空 async with aiohttp.ClientSession() as session: try: @@ -195,13 +215,19 @@ class WeChatPadProAdapter(Platform): # 修正成功判断条件和数据提取路径 if response.status == 200 and response_data.get("Code") == 200: # 二维码地址在 Data.QrCodeUrl 字段中 - if response_data.get("Data") and response_data["Data"].get("QrCodeUrl"): + if response_data.get("Data") and response_data["Data"].get( + "QrCodeUrl" + ): return response_data["Data"]["QrCodeUrl"] else: - logger.error(f"获取登录二维码成功但未找到二维码地址: {response_data}") + logger.error( + f"获取登录二维码成功但未找到二维码地址: {response_data}" + ) return None else: - logger.error(f"获取登录二维码失败: {response.status}, {response_data}") + logger.error( + f"获取登录二维码失败: {response.status}, {response_data}" + ) return None except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") @@ -230,22 +256,35 @@ class WeChatPadProAdapter(Platform): response_data = await response.json() # 成功判断条件和数据提取路径 if response.status == 200 and response_data.get("Code") == 200: - if response_data.get("Data") and response_data["Data"].get("state") is not None: + if ( + response_data.get("Data") + and response_data["Data"].get("state") is not None + ): status = response_data["Data"]["state"] - logger.info(f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown-attempts*5}秒") + logger.info( + f"第 {attempts + 1} 次尝试,当前登录状态: {status},还剩{countdown - attempts * 5}秒" + ) if status == 2: # 状态 2 表示登录成功 self.wxid = response_data["Data"].get("wxid") - self.wxnewpass = response_data["Data"].get("wxnewpass") - logger.info(f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}") + self.wxnewpass = response_data["Data"].get( + "wxnewpass" + ) + logger.info( + f"登录成功,wxid: {self.wxid}, wxnewpass: {self.wxnewpass}" + ) self.save_credentials() # 登录成功后保存凭据 return True elif status == -2: # 二维码过期 logger.error("二维码已过期,请重新获取。") return False else: - logger.error(f"检测登录状态成功但未找到登录状态: {response_data}") + logger.error( + f"检测登录状态成功但未找到登录状态: {response_data}" + ) else: - logger.info(f"检测登录状态失败: {response.status}, {response_data}") + logger.info( + f"检测登录状态失败: {response.status}, {response_data}" + ) except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") @@ -288,7 +327,7 @@ class WeChatPadProAdapter(Platform): except Exception as e: logger.error(f"WebSocket 连接失败: {e}") # 在这里可以添加重连逻辑 - await asyncio.sleep(5) # 等待一段时间后重试 + await asyncio.sleep(5) # 等待一段时间后重试 async def handle_websocket_message(self, message: str): """ @@ -298,29 +337,31 @@ class WeChatPadProAdapter(Platform): try: message_data = json.loads(message) # 检查消息结构,确保是有效的消息推送 - if message_data.get("msg_id") is not None and message_data.get("from_user_name") is not None: - abm = await self.convert_message(message_data) - if abm: - # 创建 WeChatPadProMessageEvent 实例 - message_event = WeChatPadProMessageEvent( - message_str=abm.message_str, - message_obj=abm, - platform_meta=self.meta(), - session_id=abm.session_id, - # 传递适配器实例,以便在事件中调用 send 方法 - adapter=self, - ) - # 提交事件到事件队列 - self.commit_event(message_event) + if ( + message_data.get("msg_id") is not None + and message_data.get("from_user_name") is not None + ): + abm = await self.convert_message(message_data) + if abm: + # 创建 WeChatPadProMessageEvent 实例 + message_event = WeChatPadProMessageEvent( + message_str=abm.message_str, + message_obj=abm, + platform_meta=self.meta(), + session_id=abm.session_id, + # 传递适配器实例,以便在事件中调用 send 方法 + adapter=self, + ) + # 提交事件到事件队列 + self.commit_event(message_event) else: - logger.warning(f"收到未知结构的 WebSocket 消息: {message_data}") + logger.warning(f"收到未知结构的 WebSocket 消息: {message_data}") except json.JSONDecodeError: logger.error(f"无法解析 WebSocket 消息为 JSON: {message}") except Exception as e: logger.error(f"处理 WebSocket 消息时发生错误: {e}") - async def convert_message(self, raw_message: dict) -> AstrBotMessage | None: """ 将 WeChatPadPro 原始消息转换为 AstrBotMessage。 @@ -336,31 +377,40 @@ class WeChatPadProAdapter(Platform): content = raw_message.get("content", {}).get("str", "") msg_type = raw_message.get("msg_type") - abm.message_str = content # 纯文本消息内容 (初始值) - abm.message = [] # Initialize message components list + abm.message_str = content # 纯文本消息内容 (初始值) + abm.message = [] # Initialize message components list # 如果是机器人自己发送的消息、回显消息或系统消息,忽略 if from_user_name == self.wxid: - logger.info("忽略自己发送的消息!!!") + # logger.info("忽略自己发送的消息!!!") return None # 先判断群聊/私聊并设置基本属性 - if await self._process_chat_type(abm, raw_message, from_user_name, to_user_name, content): + if await self._process_chat_type( + abm, raw_message, from_user_name, to_user_name, content + ): # 再根据消息类型处理消息内容 self._process_message_content(abm, raw_message, msg_type, content) return abm return None - async def _process_chat_type(self, abm: AstrBotMessage, raw_message: dict, from_user_name: str, to_user_name: str, content: str): + async def _process_chat_type( + self, + abm: AstrBotMessage, + raw_message: dict, + from_user_name: str, + to_user_name: str, + content: str, + ): """ 判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。 """ - if from_user_name == 'weixin': - logger.info("忽略微信团队的消息!!!") + if from_user_name == "weixin": + # logger.info("忽略微信团队的消息!!!") return False if "@chatroom" in from_user_name: - logger.info("开始处理群消息!") + # logger.info("开始处理群消息!") abm.type = MessageType.GROUP_MESSAGE abm.group_id = from_user_name @@ -370,23 +420,29 @@ class WeChatPadProAdapter(Platform): # 获取群聊发送者的nickname if sender_wxid: - accurate_nickname = await self._get_group_member_nickname(abm.group_id, sender_wxid) + accurate_nickname = await self._get_group_member_nickname( + abm.group_id, sender_wxid + ) if accurate_nickname: abm.sender.nickname = accurate_nickname # 对于群聊,session_id 可以是群聊 ID 或发送者 ID + 群聊 ID (如果 unique_session 为 True) if self.unique_session: - abm.session_id = f"{from_user_name}_{to_user_name}" + abm.session_id = f"{from_user_name}_{to_user_name}" else: - abm.session_id = from_user_name + abm.session_id = from_user_name else: abm.type = MessageType.FRIEND_MESSAGE abm.group_id = "" - abm.sender = MessageMember(user_id=from_user_name, nickname="") # 暂时没有私聊发送者的昵称 + abm.sender = MessageMember( + user_id=from_user_name, nickname="" + ) # 暂时没有私聊发送者的昵称 abm.session_id = from_user_name return True - async def _get_group_member_nickname(self, group_id: str, member_wxid: str) -> Optional[str]: + async def _get_group_member_nickname( + self, group_id: str, member_wxid: str + ) -> Optional[str]: """ 通过接口获取群成员的昵称。 """ @@ -402,13 +458,21 @@ class WeChatPadProAdapter(Platform): response_data = await response.json() if response.status == 200 and response_data.get("Code") == 200: # 从返回数据中查找对应成员的昵称 - member_list = response_data.get("Data", {}).get("member_data", {}).get("chatroom_member_list", []) + member_list = ( + response_data.get("Data", {}) + .get("member_data", {}) + .get("chatroom_member_list", []) + ) for member in member_list: if member.get("user_name") == member_wxid: return member.get("nick_name") - logger.warning(f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称") + logger.warning( + f"在群 {group_id} 中未找到成员 {member_wxid} 的昵称" + ) else: - logger.error(f"获取群成员详情失败: {response.status}, {response_data}") + logger.error( + f"获取群成员详情失败: {response.status}, {response_data}" + ) return None except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") @@ -418,43 +482,46 @@ class WeChatPadProAdapter(Platform): return None @staticmethod - def _process_message_content(abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str): + def _process_message_content( + abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str + ): """ 根据消息类型处理消息内容,填充 AstrBotMessage 的 message 列表。 """ - if msg_type == 1: # 文本消息 + if msg_type == 1: # 文本消息 # 对于群聊消息,从 content 中提取实际消息内容 if abm.type == MessageType.GROUP_MESSAGE: parts = content.split(":\n", 1) if len(parts) == 2: - abm.message_str = parts[1] # 更新纯文本消息内容为实际消息内容 + abm.message_str = parts[1] # 更新纯文本消息内容为实际消息内容 abm.message.append(Plain(abm.message_str)) else: # 如果群聊消息格式不符合预期,仍然使用原始 content abm.message.append(Plain(abm.message_str)) - else: # 私聊消息 + else: # 私聊消息 abm.message.append(Plain(abm.message_str)) - elif msg_type == 3: # 图片消息 + elif msg_type == 3: # 图片消息 # TODO: 从 raw_message 中提取图片信息并创建 Image 组件 logger.warning(f"收到图片消息,待实现处理: {raw_message}") # 示例:abm.message.append(Image(file="图片文件路径或URL")) pass - elif msg_type == 47: # 视频消息 (注意:表情消息也是 47,需要区分) + elif msg_type == 47: # 视频消息 (注意:表情消息也是 47,需要区分) # TODO: 从 raw_message 中提取视频信息并创建 Video 组件 logger.warning(f"收到视频消息,待实现处理: {raw_message}") # 示例:abm.message.append(Video(file="视频文件路径或URL")) pass - elif msg_type == 50: # 语音/视频 (根据上下文判断是语音还是视频) - # TODO: 从 raw_message 中提取语音信息并创建 Record 组件 + elif msg_type == 50: # 语音/视频 (根据上下文判断是语音还是视频) + # TODO: 从 raw_message 中提取语音信息并创建 Record 组件 logger.warning(f"收到语音/视频消息,待实现处理: {raw_message}") # 示例:abm.message.append(Record(file="语音文件路径或URL")) pass - elif msg_type == 49: # 引用消息 + elif msg_type == 49: # 引用消息 # TODO: 解析 content 中的 XML,提取引用内容和发送者信息 logger.warning(f"收到引用消息,待实现处理: {raw_message}") # 示例:abm.message.append(Reply(id="被引用消息ID", sender_id="被引用消息发送者ID")) try: import xml.etree.ElementTree as ET + root = ET.fromstring(content) # 示例:提取被引用消息的发送者和内容 # referenced_sender = root.find('.//dataitemsource/fromusr').text @@ -478,9 +545,8 @@ class WeChatPadProAdapter(Platform): await self._websocket.close() # 在这里实现终止 WeChatPadPro 客户端或连接的逻辑 # await self.client.stop() - if hasattr(self, '_shutdown_event'): - self._shutdown_event.set() - + if hasattr(self, "_shutdown_event"): + self._shutdown_event.set() def meta(self) -> PlatformMetadata: """ diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 4adf5bb17..850d94c6d 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -1,14 +1,16 @@ import asyncio +import base64 +import io + import aiohttp +from PIL import Image as PILImage # 使用别名避免冲突 + +from astrbot import logger +from astrbot.core.message.components import Image, Plain # Import Image +from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType from astrbot.core.platform.platform_metadata import PlatformMetadata -from astrbot.core.message.message_event_result import MessageChain -from astrbot.core.message.components import Plain, Image # Import Image -from astrbot import logger -import base64 -from PIL import Image as PILImage # 使用别名避免冲突 -import io class WeChatPadProMessageEvent(AstrMessageEvent): @@ -19,11 +21,11 @@ class WeChatPadProMessageEvent(AstrMessageEvent): platform_meta: PlatformMetadata, session_id: str, # 添加平台特定的参数,例如适配器实例 - adapter: object, # 传递适配器实例 + adapter: object, # 传递适配器实例 ): super().__init__(message_str, message_obj, platform_meta, session_id) - self.message_obj = message_obj # Save the full message object - self.adapter = adapter # Save the adapter instance + self.message_obj = message_obj # Save the full message object + self.adapter = adapter # Save the adapter instance async def send(self, message: MessageChain): async with aiohttp.ClientSession() as session: @@ -39,23 +41,37 @@ class WeChatPadProMessageEvent(AstrMessageEvent): b64 = await comp.convert_to_base64() raw = self._validate_base64(b64) b64c = self._compress_image(raw) - payload = {"MsgItem":[{"ImageContent": b64c, "MsgType":3, "ToUserName": self.session_id}]} + payload = { + "MsgItem": [ + {"ImageContent": b64c, "MsgType": 3, "ToUserName": self.session_id} + ] + } url = f"{self.adapter.base_url}/message/SendImageNewMessage" await self._post(session, url, payload) async def _send_text(self, session: aiohttp.ClientSession, text: str): if ( - self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息 - and self.adapter.settings.get("reply_with_mention", False) # 检查适配器设置是否启用 reply_with_mention - and self.message_obj.sender # 确保有发送者信息 - and (self.message_obj.sender.user_id or self.message_obj.sender.nickname) # 确保发送者有 ID 或昵称 + self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息 + and self.adapter.settings.get( + "reply_with_mention", False + ) # 检查适配器设置是否启用 reply_with_mention + and self.message_obj.sender # 确保有发送者信息 + and ( + self.message_obj.sender.user_id or self.message_obj.sender.nickname + ) # 确保发送者有 ID 或昵称 ): # 优先使用 nickname,如果没有则使用 user_id - mention_text = self.message_obj.sender.nickname or self.message_obj.sender.user_id + mention_text = ( + self.message_obj.sender.nickname or self.message_obj.sender.user_id + ) message_text = f"@{mention_text} {text}" logger.info(f"已添加 @ 信息: {message_text}") - payload = {"MsgItem": [{"MsgType": 1, "TextContent": text, "ToUserName": self.session_id}]} + payload = { + "MsgItem": [ + {"MsgType": 1, "TextContent": text, "ToUserName": self.session_id} + ] + } url = f"{self.adapter.base_url}/message/SendTextMessage" await self._post(session, url, payload) @@ -70,7 +86,7 @@ class WeChatPadProMessageEvent(AstrMessageEvent): if img.format == "JPEG": img.save(buf, "JPEG", quality=80) else: - if img.mode in ("RGBA","P"): + if img.mode in ("RGBA", "P"): img = img.convert("RGB") img.save(buf, "JPEG", quality=80) # logger.info("图片处理完成!!!") From 507c3e362938c9bd97854deee7470e9b218d2b43 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 10:14:16 +0800 Subject: [PATCH 22/54] =?UTF-8?q?=E2=9C=A8=20feat:=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=E6=94=AF=E6=8C=81=E4=BB=A3=E7=A0=81=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/shared/AstrBotConfig.vue | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 0117fd2f3..fff89f602 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -1,3 +1,27 @@ + + @@ -107,7 +106,7 @@ function saveEditedContent() { variant="text" color="primary" class="editor-fullscreen-btn" - @click="openEditorDialog(key, iterable[key], metadata[metadataKey].items[key]?.editor_language)" + @click="openEditorDialog(key, iterable, metadata[metadataKey].items[key]?.editor_theme, metadata[metadataKey].items[key]?.editor_language)" title="全屏编辑" > mdi-fullscreen @@ -297,10 +296,10 @@ function saveEditedContent() { From c6eaf3d01079c5f12e8fbd4af2d1012f756e2882 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 11:04:01 +0800 Subject: [PATCH 28/54] refactor: use aiohttp --- .../sources/minimax_tts_api_source.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index 26d08ac75..04170b930 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -1,12 +1,10 @@ import json import os import uuid -from typing import Dict, Iterator, List, Union - -import requests - +import aiohttp +from typing import Dict, List, Union, AsyncIterator from astrbot.core.utils.astrbot_path import get_astrbot_data_path - +from astrbot.api import logger from ..entities import ProviderType from ..provider import TTSProvider from ..register import register_provider_adapter @@ -81,43 +79,54 @@ class ProviderMiniMaxTTSAPI(TTSProvider): return json.dumps(dict_body) - def _call_tts_stream(self, text: str) -> Iterator[bytes]: + async def _call_tts_stream(self, text: str) -> AsyncIterator[bytes]: """进行流式请求""" try: - response = requests.post( - self.concat_base_url, - stream=True, - headers=self.headers, - data=self._build_tts_stream_body(text), - ) - response.raise_for_status() + async with aiohttp.ClientSession() as session: + async with session.post( + self.concat_base_url, + headers=self.headers, + data=self._build_tts_stream_body(text), + timeout=aiohttp.ClientTimeout(total=60), + ) as response: + response.raise_for_status() - for chunk in response.raw: - if not chunk or not chunk.startswith(b"data:"): - continue - data = json.loads(chunk[5:]) - if "extra_info" in data: - continue - audio = data.get("data", {}).get("audio") - if audio is not None: - yield audio + async for chunk in response.content.iter_any(): + if not chunk or not chunk.startswith(b"data:"): + logger.warning(f"Minimax TTS resp: {chunk}") + if "invalid api key" in chunk.decode("utf-8"): + raise Exception("MiniMax TTS: 无效的 API 密钥") + continue + try: + data = json.loads(chunk[5:]) + if "extra_info" in data: + continue + audio = data.get("data", {}).get("audio") + if audio is not None: + yield audio + except json.JSONDecodeError: + continue - except requests.exceptions.RequestException as e: + except aiohttp.ClientError as e: raise Exception(f"MiniMax TTS API请求失败: {str(e)}") - def _audio_play(self, audio_stream: Iterator[bytes]) -> bytes: - """解码数据流到audio比特流""" - return b"".join( - bytes.fromhex(chunk) for chunk in audio_stream if chunk and chunk != b"\n" - ) + async def _audio_play(self, audio_stream: AsyncIterator[bytes]) -> bytes: + """解码数据流到 audio 比特流""" + chunks = [] + async for chunk in audio_stream: + if chunk and chunk != b"\n": + chunks.append(bytes.fromhex(chunk.decode("utf-8"))) + return b"".join(chunks) async def get_audio(self, text: str) -> str: temp_dir = os.path.join(get_astrbot_data_path(), "temp") + os.makedirs(temp_dir, exist_ok=True) path = os.path.join(temp_dir, f"minimax_tts_api_{uuid.uuid4()}.mp3") try: - audio_chunk_iterator = self._call_tts_stream(text) - audio = self._audio_play(audio_chunk_iterator) + # 直接将异步生成器传递给 _audio_play 方法 + audio_stream = self._call_tts_stream(text) + audio = await self._audio_play(audio_stream) # 结果保存至文件 with open(path, "wb") as file: @@ -125,5 +134,5 @@ class ProviderMiniMaxTTSAPI(TTSProvider): return path - except requests.exceptions.RequestException as e: + except aiohttp.ClientError as e: raise e From 500909a28e03a6e8e1c6e2b75f2530e317f8744c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=9B=AE=E4=BE=A7=E8=80=B3?= Date: Fri, 16 May 2025 11:47:52 +0800 Subject: [PATCH 29/54] Update astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 鸦羽 --- .../platform/sources/wechatpadpro/wechatpadpro_message_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 850d94c6d..e65e06a6e 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -21,7 +21,7 @@ class WeChatPadProMessageEvent(AstrMessageEvent): platform_meta: PlatformMetadata, session_id: str, # 添加平台特定的参数,例如适配器实例 - adapter: object, # 传递适配器实例 + adapter: "WeChatPadProAdapter", # 传递适配器实例 ): super().__init__(message_str, message_obj, platform_meta, session_id) self.message_obj = message_obj # Save the full message object From 3a4b73297729a3f472e7ce02991f61f2c052dca6 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 16 May 2025 11:52:54 +0800 Subject: [PATCH 30/54] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D@=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=80=82=E9=85=8D=EF=BC=8C=E5=B9=B6=E5=86=99=E6=98=8E?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/wechatpadpro/wechatpadpro_message_event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index e65e06a6e..fb75dd560 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -11,6 +11,7 @@ from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType from astrbot.core.platform.platform_metadata import PlatformMetadata +from astrbot.core.platform.sources.wechatpadpro.wechatpadpro_adapter import WeChatPadProAdapter class WeChatPadProMessageEvent(AstrMessageEvent): @@ -66,10 +67,11 @@ class WeChatPadProMessageEvent(AstrMessageEvent): ) message_text = f"@{mention_text} {text}" logger.info(f"已添加 @ 信息: {message_text}") - + else: + message_text = text payload = { "MsgItem": [ - {"MsgType": 1, "TextContent": text, "ToUserName": self.session_id} + {"MsgType": 1, "TextContent": message_text, "ToUserName": self.session_id} ] } url = f"{self.adapter.base_url}/message/SendTextMessage" From 4d32b563caa1e58801574288d7f0480240d52f51 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 16 May 2025 12:08:49 +0800 Subject: [PATCH 31/54] =?UTF-8?q?fix:=20=E5=AF=B9auth=5Fkey=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=A0=81=E8=BF=9B=E8=A1=8C=E8=84=B1=E6=95=8F=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index bc57958f8..159406236 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -186,7 +186,7 @@ class WeChatPadProAdapter(Platform): and len(response_data["Data"]) > 0 ): self.auth_key = response_data["Data"][0] - logger.info(f"成功获取授权码: {self.auth_key}") + logger.info("成功获取授权码") else: logger.error( f"生成授权码成功但未找到授权码: {response_data}" @@ -307,7 +307,7 @@ class WeChatPadProAdapter(Platform): """ os.environ["no_proxy"] = f"localhost,127.0.0.1,{self.host}" ws_url = f"ws://{self.host}:{self.port}/ws/GetSyncMsg?key={self.auth_key}" - logger.info(f"正在连接 WebSocket: {ws_url}") + logger.info(f"正在连接 WebSocket: ws://{self.host}:{self.port}/ws/GetSyncMsg?key=***") while True: try: async with websockets.connect(ws_url) as websocket: From 2549e44710b261666db4beed62e0444ac7d613a8 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 16 May 2025 12:25:49 +0800 Subject: [PATCH 32/54] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/wechatpadpro/wechatpadpro_message_event.py | 1 - 1 file changed, 1 deletion(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index fb75dd560..8a4cd0b18 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -11,7 +11,6 @@ from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType from astrbot.core.platform.platform_metadata import PlatformMetadata -from astrbot.core.platform.sources.wechatpadpro.wechatpadpro_adapter import WeChatPadProAdapter class WeChatPadProMessageEvent(AstrMessageEvent): From 98e7ea85d3cacd4032c455405b7576f784d54b41 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Fri, 16 May 2025 12:39:14 +0800 Subject: [PATCH 33/54] =?UTF-8?q?fix:=20=E6=AD=A3=E7=A1=AE=E5=AF=BC?= =?UTF-8?q?=E5=85=A5WeChatPadProAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/wechatpadpro/wechatpadpro_message_event.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 8a4cd0b18..2f87f367d 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -1,6 +1,7 @@ import asyncio import base64 import io +from typing import TYPE_CHECKING import aiohttp from PIL import Image as PILImage # 使用别名避免冲突 @@ -12,6 +13,9 @@ from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType from astrbot.core.platform.platform_metadata import PlatformMetadata +if TYPE_CHECKING: + from .wechatpadpro_adapter import WeChatPadProAdapter + class WeChatPadProMessageEvent(AstrMessageEvent): def __init__( From 960ff438e8cf14f53a5bb385a5f6ec46b49ea55c Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 13:26:45 +0800 Subject: [PATCH 34/54] =?UTF-8?q?=F0=9F=8E=88perf:=20=E6=97=A7=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E4=B8=A2=E5=BC=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 159406236..eaa8afde9 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -1,6 +1,7 @@ import asyncio import json import os +import time from typing import Optional import aiohttp @@ -281,6 +282,9 @@ class WeChatPadProAdapter(Platform): logger.error( f"检测登录状态成功但未找到登录状态: {response_data}" ) + elif response_data.get("Code") == 300: + # "不存在状态" + pass else: logger.info( f"检测登录状态失败: {response.status}, {response_data}" @@ -307,7 +311,9 @@ class WeChatPadProAdapter(Platform): """ os.environ["no_proxy"] = f"localhost,127.0.0.1,{self.host}" ws_url = f"ws://{self.host}:{self.port}/ws/GetSyncMsg?key={self.auth_key}" - logger.info(f"正在连接 WebSocket: ws://{self.host}:{self.port}/ws/GetSyncMsg?key=***") + logger.info( + f"正在连接 WebSocket: ws://{self.host}:{self.port}/ws/GetSyncMsg?key=***" + ) while True: try: async with websockets.connect(ws_url) as websocket: @@ -333,7 +339,7 @@ class WeChatPadProAdapter(Platform): """ 处理从 WebSocket 接收到的消息。 """ - logger.info(f"收到 WebSocket 消息: {message}") + logger.debug(f"收到 WebSocket 消息: {message}") try: message_data = json.loads(message) # 检查消息结构,确保是有效的消息推送 @@ -372,6 +378,12 @@ class WeChatPadProAdapter(Platform): abm.timestamp = raw_message.get("create_time") abm.self_id = self.wxid + if int(time.time()) - abm.timestamp > 60: + logger.warning( + f"忽略 1 分钟前的旧消息:消息时间戳 {abm.timestamp} 超过当前时间 {int(time.time())}。" + ) + return None + from_user_name = raw_message.get("from_user_name", {}).get("str", "") to_user_name = raw_message.get("to_user_name", {}).get("str", "") content = raw_message.get("content", {}).get("str", "") @@ -382,7 +394,11 @@ class WeChatPadProAdapter(Platform): # 如果是机器人自己发送的消息、回显消息或系统消息,忽略 if from_user_name == self.wxid: - # logger.info("忽略自己发送的消息!!!") + logger.info("忽略来自自己的消息。") + return None + + if from_user_name in ["weixin", "newsapp", "newsapp_wechat"]: + logger.info("忽略来自微信团队的消息。") return None # 先判断群聊/私聊并设置基本属性 @@ -503,37 +519,20 @@ class WeChatPadProAdapter(Platform): elif msg_type == 3: # 图片消息 # TODO: 从 raw_message 中提取图片信息并创建 Image 组件 logger.warning(f"收到图片消息,待实现处理: {raw_message}") - # 示例:abm.message.append(Image(file="图片文件路径或URL")) pass elif msg_type == 47: # 视频消息 (注意:表情消息也是 47,需要区分) # TODO: 从 raw_message 中提取视频信息并创建 Video 组件 logger.warning(f"收到视频消息,待实现处理: {raw_message}") - # 示例:abm.message.append(Video(file="视频文件路径或URL")) pass elif msg_type == 50: # 语音/视频 (根据上下文判断是语音还是视频) # TODO: 从 raw_message 中提取语音信息并创建 Record 组件 logger.warning(f"收到语音/视频消息,待实现处理: {raw_message}") - # 示例:abm.message.append(Record(file="语音文件路径或URL")) pass elif msg_type == 49: # 引用消息 # TODO: 解析 content 中的 XML,提取引用内容和发送者信息 logger.warning(f"收到引用消息,待实现处理: {raw_message}") - # 示例:abm.message.append(Reply(id="被引用消息ID", sender_id="被引用消息发送者ID")) - try: - import xml.etree.ElementTree as ET - - root = ET.fromstring(content) - # 示例:提取被引用消息的发送者和内容 - # referenced_sender = root.find('.//dataitemsource/fromusr').text - # referenced_content = root.find('.//datadesc').text - # logger.info(f"引用消息解析结果 - 发送者: {referenced_sender}, 内容: {referenced_content}") - # 根据需要创建 Reply 组件或其他组件 - except Exception as e: - logger.error(f"解析引用消息 XML 失败: {e}") - pass else: logger.warning(f"收到未处理的消息类型: {msg_type}, 原始消息: {raw_message}") - # abm.message remains empty [] for unhandled types async def terminate(self): """ @@ -543,10 +542,10 @@ class WeChatPadProAdapter(Platform): # 关闭 WebSocket 连接 if self._websocket: await self._websocket.close() - # 在这里实现终止 WeChatPadPro 客户端或连接的逻辑 - # await self.client.stop() - if hasattr(self, "_shutdown_event"): + try: self._shutdown_event.set() + except Exception: + pass def meta(self) -> PlatformMetadata: """ From 587bd00a191f2c3f882e92020bdc6f20f9159337 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 16 May 2025 14:28:19 +0800 Subject: [PATCH 35/54] =?UTF-8?q?update:=20=E6=96=B0=E5=A2=9Esend=5Fby=5Fs?= =?UTF-8?q?ession=E6=96=B9=E6=B3=95=EF=BC=8C=E6=8E=A5=E5=8F=97=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=9D=A5=E8=87=AAAstrBot=E6=A0=B8=E5=BF=83=E7=9A=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 29 +++++++++++++++++++ .../wechatpadpro_message_event.py | 3 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index eaa8afde9..9719299c5 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -10,12 +10,14 @@ import websockets from astrbot import logger from astrbot.api.message_components import Plain from astrbot.api.platform import Platform, PlatformMetadata +from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astrbot_message import ( AstrBotMessage, MessageMember, MessageType, ) from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.platform.astr_message_event import MessageSesion from ...register import register_platform_adapter from .wechatpadpro_message_event import WeChatPadProMessageEvent @@ -552,3 +554,30 @@ class WeChatPadProAdapter(Platform): 得到一个平台的元数据。 """ return self.metadata + + async def send_by_session( + self, session: MessageSesion, message_chain: MessageChain + ): + # 创建一个临时的 AstrBotMessage 实例,用于传递会话信息 + dummy_message_obj = AstrBotMessage() + dummy_message_obj.session_id = session.session_id + # 根据 session_id 判断消息类型 + if "@chatroom" in session.session_id: + dummy_message_obj.type = MessageType.GROUP_MESSAGE + dummy_message_obj.group_id = session.session_id + dummy_message_obj.sender = MessageMember(user_id="", nickname="") + else: + dummy_message_obj.type = MessageType.FRIEND_MESSAGE + dummy_message_obj.group_id = "" + dummy_message_obj.sender = MessageMember(user_id="", nickname="") + # logger.info(f"会话消息:{session}") + # logger.info(f"临时消息结构:{dummy_message_obj}") + sending_event = WeChatPadProMessageEvent( + message_str="", + message_obj=dummy_message_obj, + platform_meta=self.meta(), + session_id=session.session_id, + adapter=self, + ) + # 调用实例方法 send + await sending_event.send(message_chain) \ No newline at end of file diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 2f87f367d..04bb02936 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -24,7 +24,6 @@ class WeChatPadProMessageEvent(AstrMessageEvent): message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, - # 添加平台特定的参数,例如适配器实例 adapter: "WeChatPadProAdapter", # 传递适配器实例 ): super().__init__(message_str, message_obj, platform_meta, session_id) @@ -69,7 +68,7 @@ class WeChatPadProMessageEvent(AstrMessageEvent): self.message_obj.sender.nickname or self.message_obj.sender.user_id ) message_text = f"@{mention_text} {text}" - logger.info(f"已添加 @ 信息: {message_text}") + # logger.info(f"已添加 @ 信息: {message_text}") else: message_text = text payload = { From db2989bdb4767ce57ccb7c35663e690a064664a0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 15:42:16 +0800 Subject: [PATCH 36/54] perf: guess private message username --- .../sources/wechatpadpro/wechatpadpro_adapter.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 9719299c5..b5094a025 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -389,6 +389,7 @@ class WeChatPadProAdapter(Platform): from_user_name = raw_message.get("from_user_name", {}).get("str", "") to_user_name = raw_message.get("to_user_name", {}).get("str", "") content = raw_message.get("content", {}).get("str", "") + push_content = raw_message.get("push_content", "") msg_type = raw_message.get("msg_type") abm.message_str = content # 纯文本消息内容 (初始值) @@ -405,7 +406,7 @@ class WeChatPadProAdapter(Platform): # 先判断群聊/私聊并设置基本属性 if await self._process_chat_type( - abm, raw_message, from_user_name, to_user_name, content + abm, raw_message, from_user_name, to_user_name, content, push_content ): # 再根据消息类型处理消息内容 self._process_message_content(abm, raw_message, msg_type, content) @@ -420,6 +421,7 @@ class WeChatPadProAdapter(Platform): from_user_name: str, to_user_name: str, content: str, + push_content: str, ): """ 判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。 @@ -452,9 +454,10 @@ class WeChatPadProAdapter(Platform): else: abm.type = MessageType.FRIEND_MESSAGE abm.group_id = "" - abm.sender = MessageMember( - user_id=from_user_name, nickname="" - ) # 暂时没有私聊发送者的昵称 + nick_name = "" + if push_content and " : " in push_content: + nick_name = push_content.split(" : ")[0] + abm.sender = MessageMember(user_id=from_user_name, nickname=nick_name) abm.session_id = from_user_name return True From 0545653494ae101ff6402960491d70ebe2b565ec Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 16:54:49 +0800 Subject: [PATCH 37/54] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 12 ++++++++ .../wechatpadpro/wechatpadpro_adapter.py | 28 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 9a8ba66eb..f74532aa7 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -162,6 +162,8 @@ CONFIG_METADATA_2 = { "admin_key": "stay33", "host": "这里填写你的局域网IP或者公网服务器IP", "port": 8059, + "wpp_active_message_poll": False, + "wpp_active_message_poll_interval": 3, }, "weixin_official_account(微信公众平台)": { "id": "weixin_official_account", @@ -218,6 +220,16 @@ CONFIG_METADATA_2 = { }, }, "items": { + "wpp_active_message_poll": { + "description": "是否启用主动消息轮询", + "type": "bool", + "hint": "只有当你发现微信消息没有按时同步到 AstrBot 时,才需要启用这个功能,默认不启用。" + }, + "wpp_active_message_poll_interval": { + "description": "主动消息轮询间隔", + "type": "int", + "hint": "主动消息轮询间隔,单位为秒,默认 3 秒,最大不要超过 60 秒,否则可能被认为是旧消息。" + }, "kf_name": { "description": "微信客服账号名", "type": "string", diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index b5094a025..1b9dde9c1 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -45,6 +45,10 @@ class WeChatPadProAdapter(Platform): self.admin_key = self.config.get("admin_key") self.host = self.config.get("host") self.port = self.config.get("port") + self.active_mesasge_poll = self.config.get("wpp_active_message_poll", False) + self.active_message_poll_interval = self.config.get( + "wpp_active_message_poll_interval", 5 + ) self.base_url = f"http://{self.host}:{self.port}" self.auth_key = None # 用于保存生成的授权码 self.wxid = None # 用于保存登录成功后的 wxid @@ -67,7 +71,9 @@ class WeChatPadProAdapter(Platform): if self.auth_key and await self.check_online_status(): logger.info("WeChatPadPro 设备已在线,跳过扫码登录。") # 如果在线,连接 WebSocket 接收消息 - asyncio.create_task(self.connect_websocket()) + self.ws_handle_task = asyncio.create_task(self.connect_websocket()) + if self.active_mesasge_poll: + asyncio.create_task(self._active_message_poll()) else: logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。") # 1. 生成授权码 @@ -91,7 +97,9 @@ class WeChatPadProAdapter(Platform): if login_successful: # 登录成功后,连接 WebSocket 接收消息 - asyncio.create_task(self.connect_websocket()) + self.ws_handle_task = asyncio.create_task(self.connect_websocket()) + if self.active_mesasge_poll: + asyncio.create_task(self._active_message_poll()) else: logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") await self.terminate() @@ -307,6 +315,20 @@ class WeChatPadProAdapter(Platform): logger.warning("登录检测超过最大尝试次数,退出检测。") return False + async def _active_message_poll(self): + """ + 部分 case 下,必须要重建 WebSocket 连接才能同步消息。 + """ + while True: + await asyncio.sleep(self.active_message_poll_interval) + logger.debug("主动拉取消息中...") + try: + if self.ws_handle_task.cancel(): + await asyncio.sleep(0.5) + self.ws_handle_task = asyncio.create_task(self.connect_websocket()) + except Exception as e: + logger.error(f"主动拉取消息时发生错误: {e}") + async def connect_websocket(self): """ 建立 WebSocket 连接并处理接收到的消息。 @@ -583,4 +605,4 @@ class WeChatPadProAdapter(Platform): adapter=self, ) # 调用实例方法 send - await sending_event.send(message_chain) \ No newline at end of file + await sending_event.send(message_chain) From c3fec15f118817452fc632ecf965d0d0c48815d5 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 16 May 2025 17:00:06 +0800 Subject: [PATCH 38/54] =?UTF-8?q?update:=20=E6=B7=BB=E5=8A=A0ws=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E9=87=8D=E8=BF=9E=E6=9C=BA=E5=88=B6=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=BF=87=E9=95=BF=E6=97=B6=E9=97=B4=E6=94=B6=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/wechatpadpro/wechatpadpro_adapter.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 9719299c5..c86df604c 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -321,21 +321,26 @@ class WeChatPadProAdapter(Platform): async with websockets.connect(ws_url) as websocket: self._websocket = websocket logger.info("WebSocket 连接成功。") + #设置空闲超时重连 + wait_time = 120 while True: try: - message = await websocket.recv() + message = await asyncio.wait_for(websocket.recv(), timeout=wait_time) + logger.info(message) asyncio.create_task(self.handle_websocket_message(message)) + except asyncio.TimeoutError: + # 10 分钟内没有收到消息,断开连接并重新尝试 + logger.warning(f"WebSocket 连接空闲超过 {wait_time} 分钟,尝试重新连接。") + break # 跳出内层循环,外层循环会处理重连 except websockets.exceptions.ConnectionClosedOK: logger.info("WebSocket 连接正常关闭。") break except Exception as e: logger.error(f"处理 WebSocket 消息时发生错误: {e}") - # 在这里可以添加重连逻辑 break except Exception as e: logger.error(f"WebSocket 连接失败: {e}") - # 在这里可以添加重连逻辑 - await asyncio.sleep(5) # 等待一段时间后重试 + await asyncio.sleep(5) # 连接失败时,等待 5 秒后重试 async def handle_websocket_message(self, message: str): """ From cb8267be3f504d75c3816e5f76b52828b29b838d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:18:27 +0800 Subject: [PATCH 39/54] =?UTF-8?q?feat:=20wechatpadpro=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8E=A5=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 90 ++++++++++++------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index f67060e5e..ec99a7ee7 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -8,7 +8,7 @@ import aiohttp import websockets from astrbot import logger -from astrbot.api.message_components import Plain +from astrbot.api.message_components import Plain, Image from astrbot.api.platform import Platform, PlatformMetadata from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astrbot_message import ( @@ -371,7 +371,6 @@ class WeChatPadProAdapter(Platform): logger.debug(f"收到 WebSocket 消息: {message}") try: message_data = json.loads(message) - # 检查消息结构,确保是有效的消息推送 if ( message_data.get("msg_id") is not None and message_data.get("from_user_name") is not None @@ -419,8 +418,8 @@ class WeChatPadProAdapter(Platform): push_content = raw_message.get("push_content", "") msg_type = raw_message.get("msg_type") - abm.message_str = content # 纯文本消息内容 (初始值) - abm.message = [] # Initialize message components list + abm.message_str = "" + abm.message = [] # 如果是机器人自己发送的消息、回显消息或系统消息,忽略 if from_user_name == self.wxid: @@ -436,7 +435,7 @@ class WeChatPadProAdapter(Platform): abm, raw_message, from_user_name, to_user_name, content, push_content ): # 再根据消息类型处理消息内容 - self._process_message_content(abm, raw_message, msg_type, content) + await self._process_message_content(abm, raw_message, msg_type, content) return abm return None @@ -454,10 +453,8 @@ class WeChatPadProAdapter(Platform): 判断消息是群聊还是私聊,并设置 AstrBotMessage 的基本属性。 """ if from_user_name == "weixin": - # logger.info("忽略微信团队的消息!!!") return False if "@chatroom" in from_user_name: - # logger.info("开始处理群消息!") abm.type = MessageType.GROUP_MESSAGE abm.group_id = from_user_name @@ -529,42 +526,74 @@ class WeChatPadProAdapter(Platform): logger.error(f"获取群成员详情时发生错误: {e}") return None - @staticmethod - def _process_message_content( - abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str + async def _download_raw_image( + self, from_user_name: str, to_user_name: str, msg_id: int + ): + """下载原始图片。""" + url = f"{self.base_url}/message/GetMsgBigImg" + params = {"key": self.auth_key} + payload = { + "CompressType": 0, + "FromUserName": from_user_name, + "MsgId": msg_id, + "Section": {"DataLen": 61440, "StartPos": 0}, + "ToUserName": to_user_name, + "TotalLen": 0, + } + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + if response.status == 200: + return await response.json() + else: + logger.error(f"下载图片失败: {response.status}") + return None + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + return None + except Exception as e: + logger.error(f"下载图片时发生错误: {e}") + return None + + async def _process_message_content( + self, abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str ): """ 根据消息类型处理消息内容,填充 AstrBotMessage 的 message 列表。 """ if msg_type == 1: # 文本消息 - # 对于群聊消息,从 content 中提取实际消息内容 + abm.message_str = content if abm.type == MessageType.GROUP_MESSAGE: parts = content.split(":\n", 1) if len(parts) == 2: - abm.message_str = parts[1] # 更新纯文本消息内容为实际消息内容 + abm.message_str = parts[1] abm.message.append(Plain(abm.message_str)) else: - # 如果群聊消息格式不符合预期,仍然使用原始 content abm.message.append(Plain(abm.message_str)) else: # 私聊消息 abm.message.append(Plain(abm.message_str)) - elif msg_type == 3: # 图片消息 - # TODO: 从 raw_message 中提取图片信息并创建 Image 组件 - logger.warning(f"收到图片消息,待实现处理: {raw_message}") - pass - elif msg_type == 47: # 视频消息 (注意:表情消息也是 47,需要区分) - # TODO: 从 raw_message 中提取视频信息并创建 Video 组件 - logger.warning(f"收到视频消息,待实现处理: {raw_message}") - pass - elif msg_type == 50: # 语音/视频 (根据上下文判断是语音还是视频) - # TODO: 从 raw_message 中提取语音信息并创建 Record 组件 - logger.warning(f"收到语音/视频消息,待实现处理: {raw_message}") - pass - elif msg_type == 49: # 引用消息 - # TODO: 解析 content 中的 XML,提取引用内容和发送者信息 - logger.warning(f"收到引用消息,待实现处理: {raw_message}") + elif msg_type == 3: + # 图片消息 + from_user_name = raw_message.get("from_user_name", {}).get("str", "") + to_user_name = raw_message.get("to_user_name", {}).get("str", "") + msg_id = raw_message.get("msg_id") + image_resp = await self._download_raw_image( + from_user_name, to_user_name, msg_id + ) + image_bs64_data = image_resp.get("Data", {}).get("Data", {}).get("Buffer", None) + if image_bs64_data: + abm.message.append(Image.fromBase64(image_bs64_data)) + elif msg_type == 47: + # 视频消息 (注意:表情消息也是 47,需要区分) + logger.warning("收到视频消息,待实现。") + elif msg_type == 50: + # 语音/视频 + logger.warning("收到语音/视频消息,待实现。") + elif msg_type == 49: + # 引用消息 + logger.warning("收到引用消息,待实现。") else: - logger.warning(f"收到未处理的消息类型: {msg_type}, 原始消息: {raw_message}") + logger.warning(f"收到未处理的消息类型: {msg_type}。") async def terminate(self): """ @@ -588,7 +617,6 @@ class WeChatPadProAdapter(Platform): async def send_by_session( self, session: MessageSesion, message_chain: MessageChain ): - # 创建一个临时的 AstrBotMessage 实例,用于传递会话信息 dummy_message_obj = AstrBotMessage() dummy_message_obj.session_id = session.session_id # 根据 session_id 判断消息类型 @@ -600,8 +628,6 @@ class WeChatPadProAdapter(Platform): dummy_message_obj.type = MessageType.FRIEND_MESSAGE dummy_message_obj.group_id = "" dummy_message_obj.sender = MessageMember(user_id="", nickname="") - # logger.info(f"会话消息:{session}") - # logger.info(f"临时消息结构:{dummy_message_obj}") sending_event = WeChatPadProMessageEvent( message_str="", message_obj=dummy_message_obj, From 86e51c5cd1d570fb751ba556f67265e3dadf35e6 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:22:10 +0800 Subject: [PATCH 40/54] =?UTF-8?q?perf:=20=E6=94=B9=E8=BF=9B=20wechatpadpro?= =?UTF-8?q?=20=E8=B6=85=E6=97=B6=E9=87=8D=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wechatpadpro/wechatpadpro_adapter.py | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index ec99a7ee7..8b536b753 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -45,8 +45,10 @@ class WeChatPadProAdapter(Platform): self.admin_key = self.config.get("admin_key") self.host = self.config.get("host") self.port = self.config.get("port") - self.active_mesasge_poll = self.config.get("wpp_active_message_poll", False) - self.active_message_poll_interval = self.config.get( + self.active_mesasge_poll: bool = self.config.get( + "wpp_active_message_poll", False + ) + self.active_message_poll_interval: int = self.config.get( "wpp_active_message_poll_interval", 5 ) self.base_url = f"http://{self.host}:{self.port}" @@ -72,8 +74,6 @@ class WeChatPadProAdapter(Platform): logger.info("WeChatPadPro 设备已在线,跳过扫码登录。") # 如果在线,连接 WebSocket 接收消息 self.ws_handle_task = asyncio.create_task(self.connect_websocket()) - if self.active_mesasge_poll: - asyncio.create_task(self._active_message_poll()) else: logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。") # 1. 生成授权码 @@ -98,8 +98,6 @@ class WeChatPadProAdapter(Platform): if login_successful: # 登录成功后,连接 WebSocket 接收消息 self.ws_handle_task = asyncio.create_task(self.connect_websocket()) - if self.active_mesasge_poll: - asyncio.create_task(self._active_message_poll()) else: logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") await self.terminate() @@ -315,20 +313,6 @@ class WeChatPadProAdapter(Platform): logger.warning("登录检测超过最大尝试次数,退出检测。") return False - async def _active_message_poll(self): - """ - 部分 case 下,必须要重建 WebSocket 连接才能同步消息。 - """ - while True: - await asyncio.sleep(self.active_message_poll_interval) - logger.debug("主动拉取消息中...") - try: - if self.ws_handle_task.cancel(): - await asyncio.sleep(0.5) - self.ws_handle_task = asyncio.create_task(self.connect_websocket()) - except Exception as e: - logger.error(f"主动拉取消息时发生错误: {e}") - async def connect_websocket(self): """ 建立 WebSocket 连接并处理接收到的消息。 @@ -343,17 +327,24 @@ class WeChatPadProAdapter(Platform): async with websockets.connect(ws_url) as websocket: self._websocket = websocket logger.info("WebSocket 连接成功。") - #设置空闲超时重连 - wait_time = 120 + # 设置空闲超时重连 + wait_time = ( + self.active_message_poll_interval + if self.active_mesasge_poll + else 120 + ) while True: try: - message = await asyncio.wait_for(websocket.recv(), timeout=wait_time) + message = await asyncio.wait_for( + websocket.recv(), timeout=wait_time + ) logger.info(message) asyncio.create_task(self.handle_websocket_message(message)) except asyncio.TimeoutError: - # 10 分钟内没有收到消息,断开连接并重新尝试 - logger.warning(f"WebSocket 连接空闲超过 {wait_time} 分钟,尝试重新连接。") - break # 跳出内层循环,外层循环会处理重连 + logger.warning( + f"WebSocket 连接空闲超过 {wait_time} s" + ) + break except websockets.exceptions.ConnectionClosedOK: logger.info("WebSocket 连接正常关闭。") break @@ -362,7 +353,7 @@ class WeChatPadProAdapter(Platform): break except Exception as e: logger.error(f"WebSocket 连接失败: {e}") - await asyncio.sleep(5) # 连接失败时,等待 5 秒后重试 + await asyncio.sleep(5) async def handle_websocket_message(self, message: str): """ @@ -406,9 +397,9 @@ class WeChatPadProAdapter(Platform): abm.timestamp = raw_message.get("create_time") abm.self_id = self.wxid - if int(time.time()) - abm.timestamp > 60: + if int(time.time()) - abm.timestamp > 180: logger.warning( - f"忽略 1 分钟前的旧消息:消息时间戳 {abm.timestamp} 超过当前时间 {int(time.time())}。" + f"忽略 3 分钟前的旧消息:消息时间戳 {abm.timestamp} 超过当前时间 {int(time.time())}。" ) return None @@ -580,7 +571,9 @@ class WeChatPadProAdapter(Platform): image_resp = await self._download_raw_image( from_user_name, to_user_name, msg_id ) - image_bs64_data = image_resp.get("Data", {}).get("Data", {}).get("Buffer", None) + image_bs64_data = ( + image_resp.get("Data", {}).get("Data", {}).get("Buffer", None) + ) if image_bs64_data: abm.message.append(Image.fromBase64(image_bs64_data)) elif msg_type == 47: From b29d14e6009329b479c4466e82a2c7590d75c8c7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:29:33 +0800 Subject: [PATCH 41/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=99=A8=E7=BB=88=E6=AD=A2=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/wechatpadpro/wechatpadpro_adapter.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 8b536b753..92fe58715 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -57,7 +57,7 @@ class WeChatPadProAdapter(Platform): self.credentials_file = os.path.join( get_astrbot_data_path(), "wechatpadpro_credentials.json" ) # 持久化文件路径 - self._websocket = None # 用于保存 WebSocket 连接 + self.ws_handle_task = None async def run(self) -> None: """ @@ -103,7 +103,6 @@ class WeChatPadProAdapter(Platform): await self.terminate() return - # 示例:保持运行直到终止事件被设置 self._shutdown_event = asyncio.Event() await self._shutdown_event.wait() logger.info("WeChatPadPro 适配器已停止。") @@ -325,7 +324,6 @@ class WeChatPadProAdapter(Platform): while True: try: async with websockets.connect(ws_url) as websocket: - self._websocket = websocket logger.info("WebSocket 连接成功。") # 设置空闲超时重连 wait_time = ( @@ -592,11 +590,10 @@ class WeChatPadProAdapter(Platform): """ 终止一个平台的运行实例。 """ - logger.info("正在终止 WeChatPadPro 适配器...") - # 关闭 WebSocket 连接 - if self._websocket: - await self._websocket.close() + logger.info("终止 WeChatPadPro 适配器。") try: + if self.ws_handle_task: + self.ws_handle_task.cancel() self._shutdown_event.set() except Exception: pass From ab68094386144b77f5a687f3d067e32525e1e74c Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:33:57 +0800 Subject: [PATCH 42/54] docs: update platform tutprial map --- dashboard/src/stores/common.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js index 410fcc7ce..d186e2db5 100644 --- a/dashboard/src/stores/common.js +++ b/dashboard/src/stores/common.js @@ -17,10 +17,12 @@ export const useCommonStore = defineStore({ "qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html", "aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html", "wecom": "https://astrbot.app/deploy/platform/wecom.html", - "gewechat": "https://astrbot.app/deploy/platform/gewechat.html", + "gewechat": "https://astrbot.app/deploy/platform/wechat/gewechat.html", "lark": "https://astrbot.app/deploy/platform/lark.html", "telegram": "https://astrbot.app/deploy/platform/telegram.html", "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", + "wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html", + "weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html", }, pluginMarketData: [], From b2502746f00055e1065a865689c99c37930a5557 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:49:17 +0800 Subject: [PATCH 43/54] =?UTF-8?q?perf:=20QQ=20=E4=B8=8B=EF=BC=8C=E5=B1=8F?= =?UTF-8?q?=E8=94=BD=20QQ=20=E7=AE=A1=E5=AE=B6=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 890ff0f8d..853ec2288 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -103,6 +103,9 @@ class AiocqhttpAdapter(Platform): if event["post_type"] == "message": abm = await self._convert_handle_message_event(event) + if abm.sender.user_id == "2854196310": + # 屏蔽 QQ 管家的消息 + return elif event["post_type"] == "notice": abm = await self._convert_handle_notice_event(event) elif event["post_type"] == "request": From 7705b8781aef6e7abfb88e83b65170f4da7699f8 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 17:50:56 +0800 Subject: [PATCH 44/54] =?UTF-8?q?=F0=9F=93=A6=20release:=20v3.5.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 2 +- changelogs/v3.5.10.md | 11 +++++++++++ pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changelogs/v3.5.10.md diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index f74532aa7..76b6c49d5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ import os from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "3.5.9" +VERSION = "3.5.10" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") # 默认配置 diff --git a/changelogs/v3.5.10.md b/changelogs/v3.5.10.md new file mode 100644 index 000000000..0439c56e8 --- /dev/null +++ b/changelogs/v3.5.10.md @@ -0,0 +1,11 @@ +# What's Changed + +1. 新增: 支持接入个人微信(WeChatPadPro)替换 gewechat 方式。 +2. 新增:接入 PPIO 派欧云 +3. 新增:支持接入 Minimax TTS +3. ‼️修复:Docker 下重启 AstrBot 会导致 astrbot 容器进程退出的问题。 +4. 优化:速率限制功能 +5. 优化:QQ 和 Telegram 下,群聊的 @ 信息也将发送给模型以获得更好的回复、QQ 支持 @ 全体成员的解析。 +6. 优化:WebUI 配置项支持代码编辑器模式! +7. 优化:语音组件将单独发送以保证全平台兼容性 +8. 优化:QQ 下,屏蔽 QQ 管家(qq=2854196310) 的所有消息。 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 522bb31b7..7b38e8b43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "AstrBot" -version = "3.5.9" +version = "3.5.10" description = "易上手的多平台 LLM 聊天机器人及开发框架" readme = "README.md" requires-python = ">=3.10" From c15f9666694bd581ac5365d8889e7b33adaaee53 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 18:32:08 +0800 Subject: [PATCH 45/54] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20minimax=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sources/minimax_tts_api_source.py | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index 04170b930..5b210835b 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -91,31 +91,42 @@ class ProviderMiniMaxTTSAPI(TTSProvider): ) as response: response.raise_for_status() - async for chunk in response.content.iter_any(): - if not chunk or not chunk.startswith(b"data:"): - logger.warning(f"Minimax TTS resp: {chunk}") - if "invalid api key" in chunk.decode("utf-8"): - raise Exception("MiniMax TTS: 无效的 API 密钥") - continue - try: - data = json.loads(chunk[5:]) - if "extra_info" in data: - continue - audio = data.get("data", {}).get("audio") - if audio is not None: - yield audio - except json.JSONDecodeError: - continue + buffer = b"" + while True: + chunk = await response.content.read(8192) + if not chunk: + break + + buffer += chunk + + while b"\n\n" in buffer: + try: + message, buffer = buffer.split(b"\n\n", 1) + if message.startswith(b"data: "): + try: + data = json.loads(message[6:]) + if "extra_info" in data: + continue + audio = data.get("data", {}).get("audio") + if audio is not None: + yield audio + except json.JSONDecodeError: + logger.warning( + "Failed to parse JSON data from SSE message" + ) + continue + except ValueError: + buffer = buffer[-1024:] except aiohttp.ClientError as e: raise Exception(f"MiniMax TTS API请求失败: {str(e)}") - async def _audio_play(self, audio_stream: AsyncIterator[bytes]) -> bytes: + async def _audio_play(self, audio_stream: AsyncIterator[str]) -> bytes: """解码数据流到 audio 比特流""" chunks = [] async for chunk in audio_stream: - if chunk and chunk != b"\n": - chunks.append(bytes.fromhex(chunk.decode("utf-8"))) + if chunk.strip(): + chunks.append(bytes.fromhex(chunk.strip())) return b"".join(chunks) async def get_audio(self, text: str) -> str: From 8da1b0212dbe77be7edd1a5377ec03047b49e828 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Fri, 16 May 2025 18:46:26 +0800 Subject: [PATCH 46/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24ae331f1..2bc168358 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 > [!NOTE] > -> 个人微信接入所依赖的开源项目 Gewechat 近期已停止维护,我们正在评估其他方案(如 xxxbot 等)并将在数日内接入(很快!)。目前推荐微信用户暂时使用**微信官方**推出的企业微信接入方式和微信客服接入方式(版本 >= v3.5.7)。详情请前往 [#1443](https://github.com/AstrBotDevs/AstrBot/issues/1443) 讨论。 +> 个人微信接入所依赖的开源项目 Gewechat 近期已停止维护,`v3.5.10` 已经支持接入 WeChatPadPro 替换 gewechat 方式。详见文档 [WeChatPadPro](https://astrbot.app/deploy/platform/wechat/wechatpadpro.html) ## ✨ 近期更新 From b50739e1afd12d92fc9c936712a7cedc06ef88c4 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 19:33:32 +0800 Subject: [PATCH 47/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/wechatpadpro/wechatpadpro_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 92fe58715..bfb19e7e8 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -255,8 +255,8 @@ class WeChatPadProAdapter(Platform): attempts = 0 # 初始化尝试次数 max_attempts = 6 # 最大尝试次数 - countdown = 30 # 倒计时时长 - logger.info(f"请在 {countdown} 秒内扫码登录!!!") + countdown = 180 # 倒计时时长 + logger.info(f"请在 {countdown} 秒内扫码登录。") while attempts < max_attempts: async with aiohttp.ClientSession() as session: try: From cb02dfe1a4648467d1740a3a97c58f81ec7819e5 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 20:00:14 +0800 Subject: [PATCH 48/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/platform/sources/wechatpadpro/wechatpadpro_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index bfb19e7e8..47d4fa9df 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -254,7 +254,7 @@ class WeChatPadProAdapter(Platform): params = {"key": self.auth_key} attempts = 0 # 初始化尝试次数 - max_attempts = 6 # 最大尝试次数 + max_attempts = 36 # 最大尝试次数 countdown = 180 # 倒计时时长 logger.info(f"请在 {countdown} 秒内扫码登录。") while attempts < max_attempts: From 5e9eba6478e30a96f476c38fc691180271c97247 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 16 May 2025 22:43:38 -0400 Subject: [PATCH 49/54] fix: extension market plugin card cannot apply installation --- astrbot/core/config/default.py | 4 ++-- dashboard/src/views/ExtensionMarketplace.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 0f648ee06..f05e17f43 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -256,10 +256,10 @@ CONFIG_METADATA_2 = { "hint": "Telegram 命令自动刷新间隔,单位为秒。", }, "id": { - "description": "ID", + "description": "机器人名称", "type": "string", "obvious_hint": True, - "hint": "ID 不能和其它的平台适配器重复,否则将发生严重冲突。", + "hint": "机器人名称(ID)不能和其它的平台适配器重复。", }, "type": { "description": "适配器类型", diff --git a/dashboard/src/views/ExtensionMarketplace.vue b/dashboard/src/views/ExtensionMarketplace.vue index 64914deed..5fb707244 100644 --- a/dashboard/src/views/ExtensionMarketplace.vue +++ b/dashboard/src/views/ExtensionMarketplace.vue @@ -58,7 +58,7 @@ import 'highlight.js/styles/github.css'; - + From 62e70a673a0fd82c45841e6d0e13f89b8b7bf1f7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 17 May 2025 12:04:36 +0800 Subject: [PATCH 50/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20Gemini=20?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/provider/sources/gemini_source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 7626df23f..2ff017c34 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -291,19 +291,19 @@ class ProviderGoogleGenAI(Provider): result_parts: Optional[types.Part] = result.candidates[0].content.parts if finish_reason == types.FinishReason.SAFETY: - raise Exception("模型生成内容未通过用户定义的内容安全检查") + raise Exception("模型生成内容未通过 Gemini 平台的安全检查") if finish_reason in { types.FinishReason.PROHIBITED_CONTENT, types.FinishReason.SPII, types.FinishReason.BLOCKLIST, }: - raise Exception("模型生成内容违反Gemini平台政策") + raise Exception("模型生成内容违反 Gemini 平台政策") # 防止旧版本SDK不存在IMAGE_SAFETY if hasattr(types.FinishReason, "IMAGE_SAFETY"): if finish_reason == types.FinishReason.IMAGE_SAFETY: - raise Exception("模型生成内容违反Gemini平台政策") + raise Exception("模型生成内容违反 Gemini 平台政策") if not result_parts: logger.debug(result.candidates) From d57b7222b2e72deedd58e6ef0bdbfb4180eb8616 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 17 May 2025 13:30:33 +0800 Subject: [PATCH 51/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20WebUI=20Abou?= =?UTF-8?q?t=20=E9=A1=B5=E9=9D=A2=E3=80=81=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E5=92=8C=E9=A1=B6=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/stat.py | 6 +- .../full/vertical-header/VerticalHeader.vue | 34 ++- .../full/vertical-sidebar/VerticalSidebar.vue | 55 +--- dashboard/src/views/AboutPage.vue | 250 ++++++++++++++---- 4 files changed, 240 insertions(+), 105 deletions(-) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 337e544af..b216d3d14 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -8,6 +8,7 @@ from quart import request from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase from astrbot.core.config import VERSION +from astrbot.core.utils.io import get_dashboard_version from astrbot.core import DEMO_MODE @@ -46,7 +47,10 @@ class StatRoute(Route): return f"{h}小时{m}分{s}秒" async def get_version(self): - return Response().ok({"version": VERSION}).__dict__ + return Response().ok({ + "version": VERSION, + "dashboard_version": await get_dashboard_version(), + }).__dict__ async def get_start_time(self): return Response().ok({"start_time": self.core_lifecycle.start_time}).__dict__ diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index f9c22f18d..2a871485e 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -78,6 +78,17 @@ function accountEdit() { }); } +function getVersion() { + axios.get('/api/stat/version') + .then((res) => { + botCurrVersion.value = "v" + res.data.data.version; + dashboardCurrentVersion.value = res.data.data?.dashboard_version; + }) + .catch((err) => { + console.log(err); + }); +} + function checkUpdate() { updateStatus.value = '正在检查更新...'; axios.get('/api/update/check') @@ -90,8 +101,6 @@ function checkUpdate() { } else { updateStatus.value = res.data.message; } - botCurrVersion.value = res.data.data.version; - dashboardCurrentVersion.value = res.data.data.dashboard_version; dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version; }) .catch((err) => { @@ -181,6 +190,7 @@ function updateDashboard() { }); } +getVersion(); checkUpdate(); const commonStore = useCommonStore(); @@ -208,23 +218,29 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha mdi-menu - AstrBot +
+ AstrBot + + {{ botCurrVersion }} +
- 有新版本! + AstrBot 有新版本! + + + WebUI 有新版本!
@@ -353,8 +369,8 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha diff --git a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue index 9b2abed91..d505e0422 100644 --- a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue +++ b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue @@ -9,9 +9,6 @@ const customizer = useCustomizerStore(); const sidebarMenu = shallowRef(sidebarItems); const showIframe = ref(false); -const version = ref(""); -const buildVer = ref(""); -const hasWebUIUpdate = ref(false); // 默认桌面端 iframe 样式 const iframeStyle = ref({ @@ -68,9 +65,10 @@ function toggleIframe() { showIframe.value = !showIframe.value; } -function openIframeLink() { +function openIframeLink(url) { if (typeof window !== 'undefined') { - window.open("https://astrbot.app", "_blank"); + let url_ = url || "https://astrbot.app"; + window.open(url_, "_blank"); } } @@ -149,25 +147,6 @@ function endDrag() { document.removeEventListener('touchend', onTouchEnd); } -// 获取版本和更新信息 -onMounted(() => { - axios.get('/api/stat/version') - .then((res) => { - version.value = "v" + res.data.data.version; - }) - .catch((err) => { - console.log(err); - }); - - axios.get('/api/update/check?type=dashboard') - .then((res) => { - hasWebUIUpdate.value = res.data.data.has_new_version; - buildVer.value = res.data.data.current_version; - }) - .catch((err) => { - console.log(err); - }); -}); -
- {{ version }} -
-
- - - 🤔 点击此处 查看/关闭 悬浮文档! - - - WebUI 版本: {{ buildVer }} - 构建: embedded - - - - AGPL-3.0 +
+ + 官方文档 + +
+ + GitHub + +
+
-
- - -
-
- AstrBot Logo - AstrBot Logo -
+ + +
+ +
+
+
+ AstrBot Logo + AstrBot Logo +
+
+

AstrBot

+

A project out of interests and loves ❤️

+
+ + Star 这个项目! 🌟 + + + 提交 Issue + +
+
+
+
-

AstrBot

+ +
+ + + +

贡献者

+

+ 本项目由众多开源社区成员共同维护。感谢每一位贡献者的付出! +

+

+ 查看 AstrBot 贡献者 +

+
+ + + + + + +
+
+
- A project out of interests and loves ❤️ - - By Soulter, AstrBot Contributors - and AstrBot - Plugin Authors - - - - - Active Contributors of Soulter/AstrBot - Last 28 days - - Active Contributors of Soulter/AstrBot - Last 28 days - - - - - - Star 这个项目! 🌟 - - - - 有使用问题或者功能建议?提交 Issue! - + +
+ + + +

全球部署

+ +
+ +

AstrBot 采用 AGPL v3 协议开源

+
+
+ + + + + + +
+
+
-
+ - \ No newline at end of file From c75156c4ce18e9e2029a26e787441ce12625cdd5 Mon Sep 17 00:00:00 2001 From: Larch-C Date: Sat, 17 May 2025 18:08:55 +0800 Subject: [PATCH 52/54] =?UTF-8?q?=F0=9F=8E=88=20perf:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=86=E7=99=BB=E5=BD=95=E7=95=8C=E9=9D=A2=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/images/astrbot_logo_mini.webp | Bin 0 -> 10668 bytes dashboard/src/components/shared/Logo.vue | 70 +++++++ .../views/authentication/auth/LoginPage.vue | 102 +++++++++- .../authentication/authForms/AuthLogin.vue | 179 +++++++++++++----- 4 files changed, 299 insertions(+), 52 deletions(-) create mode 100644 dashboard/src/assets/images/astrbot_logo_mini.webp create mode 100644 dashboard/src/components/shared/Logo.vue diff --git a/dashboard/src/assets/images/astrbot_logo_mini.webp b/dashboard/src/assets/images/astrbot_logo_mini.webp new file mode 100644 index 0000000000000000000000000000000000000000..f6a868c54357a3e42592a2b2d2c326e194176b27 GIT binary patch literal 10668 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 D2_=XN literal 0 HcmV?d00001 diff --git a/dashboard/src/components/shared/Logo.vue b/dashboard/src/components/shared/Logo.vue new file mode 100644 index 000000000..294fae094 --- /dev/null +++ b/dashboard/src/components/shared/Logo.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue index 63a1f809c..4bcefe07c 100644 --- a/dashboard/src/views/authentication/auth/LoginPage.vue +++ b/dashboard/src/views/authentication/auth/LoginPage.vue @@ -1,23 +1,111 @@ + From f67b9f5f6ec691bfd80557db67d8477e9a79a2cc Mon Sep 17 00:00:00 2001 From: Larch-C Date: Sat, 17 May 2025 18:09:49 +0800 Subject: [PATCH 53/54] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E4=BA=86=E5=A6=82=E6=9E=9C=E6=AD=A4=E5=89=8D=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E4=BD=86=E6=9C=AA=E8=87=AA=E8=A1=8C=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/router/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index a56154f18..c44e0249d 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -24,6 +24,11 @@ router.beforeEach(async (to, from, next) => { const authRequired = !publicPages.includes(to.path); const auth: AuthStore = useAuthStore(); + // 如果用户已登录且试图访问登录页面,则重定向到首页或之前尝试访问的页面 + if (to.path === '/auth/login' && auth.has_token()) { + return next(auth.returnUrl || '/'); + } + if (to.matched.some((record) => record.meta.requiresAuth)) { if (authRequired && !auth.has_token()) { auth.returnUrl = to.fullPath; From ac7f43520b5348caa885ced48d250da4b39e0a99 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 17 May 2025 21:30:05 +0800 Subject: [PATCH 54/54] =?UTF-8?q?=F0=9F=8E=88=20perf:=20adjust=20login=20i?= =?UTF-8?q?nput=20padding=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/views/authentication/authForms/AuthLogin.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/src/views/authentication/authForms/AuthLogin.vue b/dashboard/src/views/authentication/authForms/AuthLogin.vue index ff446ead0..b2b356087 100644 --- a/dashboard/src/views/authentication/authForms/AuthLogin.vue +++ b/dashboard/src/views/authentication/authForms/AuthLogin.vue @@ -104,7 +104,8 @@ async function validate(values: any, { setErrors }: any) { .input-field, .pwd-input { .v-field__field { - padding-top: 10px; + padding-top: 5px; + padding-bottom: 5px; } .v-field__outline {