AstrBot 4.11.0__py3-none-any.whl → 4.11.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/config/default.py +1 -11
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +17 -7
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- astrbot/core/platform/manager.py +0 -4
- astrbot/core/star/filter/platform_adapter_type.py +0 -3
- astrbot/core/star/star_manager.py +17 -0
- astrbot/dashboard/routes/config.py +2 -2
- astrbot/dashboard/routes/plugin.py +50 -0
- {astrbot-4.11.0.dist-info → astrbot-4.11.2.dist-info}/METADATA +1 -1
- {astrbot-4.11.0.dist-info → astrbot-4.11.2.dist-info}/RECORD +14 -17
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +0 -940
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +0 -178
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +0 -159
- {astrbot-4.11.0.dist-info → astrbot-4.11.2.dist-info}/WHEEL +0 -0
- {astrbot-4.11.0.dist-info → astrbot-4.11.2.dist-info}/entry_points.txt +0 -0
- {astrbot-4.11.0.dist-info → astrbot-4.11.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import base64
|
|
3
|
-
import io
|
|
4
|
-
from collections.abc import AsyncGenerator
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
import aiohttp
|
|
8
|
-
from PIL import Image as PILImage # 使用别名避免冲突
|
|
9
|
-
|
|
10
|
-
from astrbot import logger
|
|
11
|
-
from astrbot.core.message.components import (
|
|
12
|
-
Image,
|
|
13
|
-
Plain,
|
|
14
|
-
Record,
|
|
15
|
-
WechatEmoji,
|
|
16
|
-
) # Import Image
|
|
17
|
-
from astrbot.core.message.message_event_result import MessageChain
|
|
18
|
-
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
|
19
|
-
from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType
|
|
20
|
-
from astrbot.core.platform.platform_metadata import PlatformMetadata
|
|
21
|
-
from astrbot.core.utils.tencent_record_helper import audio_to_tencent_silk_base64
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from .wechatpadpro_adapter import WeChatPadProAdapter
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class WeChatPadProMessageEvent(AstrMessageEvent):
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
message_str: str,
|
|
31
|
-
message_obj: AstrBotMessage,
|
|
32
|
-
platform_meta: PlatformMetadata,
|
|
33
|
-
session_id: str,
|
|
34
|
-
adapter: "WeChatPadProAdapter", # 传递适配器实例
|
|
35
|
-
):
|
|
36
|
-
super().__init__(message_str, message_obj, platform_meta, session_id)
|
|
37
|
-
self.message_obj = message_obj # Save the full message object
|
|
38
|
-
self.adapter = adapter # Save the adapter instance
|
|
39
|
-
|
|
40
|
-
async def send(self, message: MessageChain):
|
|
41
|
-
async with aiohttp.ClientSession() as session:
|
|
42
|
-
for comp in message.chain:
|
|
43
|
-
await asyncio.sleep(1)
|
|
44
|
-
if isinstance(comp, Plain):
|
|
45
|
-
await self._send_text(session, comp.text)
|
|
46
|
-
elif isinstance(comp, Image):
|
|
47
|
-
await self._send_image(session, comp)
|
|
48
|
-
elif isinstance(comp, WechatEmoji):
|
|
49
|
-
await self._send_emoji(session, comp)
|
|
50
|
-
elif isinstance(comp, Record):
|
|
51
|
-
await self._send_voice(session, comp)
|
|
52
|
-
await super().send(message)
|
|
53
|
-
|
|
54
|
-
async def send_streaming(
|
|
55
|
-
self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
|
|
56
|
-
):
|
|
57
|
-
buffer = None
|
|
58
|
-
async for chain in generator:
|
|
59
|
-
if not buffer:
|
|
60
|
-
buffer = chain
|
|
61
|
-
else:
|
|
62
|
-
buffer.chain.extend(chain.chain)
|
|
63
|
-
if not buffer:
|
|
64
|
-
return None
|
|
65
|
-
buffer.squash_plain()
|
|
66
|
-
await self.send(buffer)
|
|
67
|
-
return await super().send_streaming(generator, use_fallback)
|
|
68
|
-
|
|
69
|
-
async def _send_image(self, session: aiohttp.ClientSession, comp: Image):
|
|
70
|
-
b64 = await comp.convert_to_base64()
|
|
71
|
-
raw = self._validate_base64(b64)
|
|
72
|
-
b64c = self._compress_image(raw)
|
|
73
|
-
payload = {
|
|
74
|
-
"MsgItem": [
|
|
75
|
-
{"ImageContent": b64c, "MsgType": 3, "ToUserName": self.session_id},
|
|
76
|
-
],
|
|
77
|
-
}
|
|
78
|
-
url = f"{self.adapter.base_url}/message/SendImageNewMessage"
|
|
79
|
-
await self._post(session, url, payload)
|
|
80
|
-
|
|
81
|
-
async def _send_text(self, session: aiohttp.ClientSession, text: str):
|
|
82
|
-
if (
|
|
83
|
-
self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息
|
|
84
|
-
and self.adapter.settings.get(
|
|
85
|
-
"reply_with_mention",
|
|
86
|
-
False,
|
|
87
|
-
) # 检查适配器设置是否启用 reply_with_mention
|
|
88
|
-
and self.message_obj.sender # 确保有发送者信息
|
|
89
|
-
and (
|
|
90
|
-
self.message_obj.sender.user_id or self.message_obj.sender.nickname
|
|
91
|
-
) # 确保发送者有 ID 或昵称
|
|
92
|
-
):
|
|
93
|
-
# 优先使用 nickname,如果没有则使用 user_id
|
|
94
|
-
mention_text = (
|
|
95
|
-
self.message_obj.sender.nickname or self.message_obj.sender.user_id
|
|
96
|
-
)
|
|
97
|
-
message_text = f"@{mention_text} {text}"
|
|
98
|
-
# logger.info(f"已添加 @ 信息: {message_text}")
|
|
99
|
-
else:
|
|
100
|
-
message_text = text
|
|
101
|
-
if self.get_group_id() and "#" in self.session_id:
|
|
102
|
-
session_id = self.session_id.split("#")[0]
|
|
103
|
-
else:
|
|
104
|
-
session_id = self.session_id
|
|
105
|
-
payload = {
|
|
106
|
-
"MsgItem": [
|
|
107
|
-
{
|
|
108
|
-
"MsgType": 1,
|
|
109
|
-
"TextContent": message_text,
|
|
110
|
-
"ToUserName": session_id,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
}
|
|
114
|
-
url = f"{self.adapter.base_url}/message/SendTextMessage"
|
|
115
|
-
await self._post(session, url, payload)
|
|
116
|
-
|
|
117
|
-
async def _send_emoji(self, session: aiohttp.ClientSession, comp: WechatEmoji):
|
|
118
|
-
payload = {
|
|
119
|
-
"EmojiList": [
|
|
120
|
-
{
|
|
121
|
-
"EmojiMd5": comp.md5,
|
|
122
|
-
"EmojiSize": comp.md5_len,
|
|
123
|
-
"ToUserName": self.session_id,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
}
|
|
127
|
-
url = f"{self.adapter.base_url}/message/SendEmojiMessage"
|
|
128
|
-
await self._post(session, url, payload)
|
|
129
|
-
|
|
130
|
-
async def _send_voice(self, session: aiohttp.ClientSession, comp: Record):
|
|
131
|
-
record_path = await comp.convert_to_file_path()
|
|
132
|
-
# 默认已经存在 data/temp 中
|
|
133
|
-
b64, duration = await audio_to_tencent_silk_base64(record_path)
|
|
134
|
-
payload = {
|
|
135
|
-
"ToUserName": self.session_id,
|
|
136
|
-
"VoiceData": b64,
|
|
137
|
-
"VoiceFormat": 4,
|
|
138
|
-
"VoiceSecond": duration,
|
|
139
|
-
}
|
|
140
|
-
url = f"{self.adapter.base_url}/message/SendVoice"
|
|
141
|
-
await self._post(session, url, payload)
|
|
142
|
-
|
|
143
|
-
@staticmethod
|
|
144
|
-
def _validate_base64(b64: str) -> bytes:
|
|
145
|
-
return base64.b64decode(b64, validate=True)
|
|
146
|
-
|
|
147
|
-
@staticmethod
|
|
148
|
-
def _compress_image(data: bytes) -> str:
|
|
149
|
-
img = PILImage.open(io.BytesIO(data))
|
|
150
|
-
buf = io.BytesIO()
|
|
151
|
-
if img.format == "JPEG":
|
|
152
|
-
img.save(buf, "JPEG", quality=80)
|
|
153
|
-
else:
|
|
154
|
-
if img.mode in ("RGBA", "P"):
|
|
155
|
-
img = img.convert("RGB")
|
|
156
|
-
img.save(buf, "JPEG", quality=80)
|
|
157
|
-
# logger.info("图片处理完成!!!")
|
|
158
|
-
return base64.b64encode(buf.getvalue()).decode()
|
|
159
|
-
|
|
160
|
-
async def _post(self, session, url, payload):
|
|
161
|
-
params = {"key": self.adapter.auth_key}
|
|
162
|
-
try:
|
|
163
|
-
async with session.post(url, params=params, json=payload) as resp:
|
|
164
|
-
data = await resp.json()
|
|
165
|
-
if resp.status != 200 or data.get("Code") != 200:
|
|
166
|
-
logger.error(f"{url} failed: {resp.status} {data}")
|
|
167
|
-
except Exception as e:
|
|
168
|
-
logger.error(f"{url} error: {e}")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# TODO: 添加对其他消息组件类型的处理 (Record, Video, At等)
|
|
172
|
-
# elif isinstance(component, Record):
|
|
173
|
-
# pass
|
|
174
|
-
# elif isinstance(component, Video):
|
|
175
|
-
# pass
|
|
176
|
-
# elif isinstance(component, At):
|
|
177
|
-
# pass
|
|
178
|
-
# ...
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
from defusedxml import ElementTree as eT
|
|
2
|
-
|
|
3
|
-
from astrbot.api import logger
|
|
4
|
-
from astrbot.api.message_components import (
|
|
5
|
-
BaseMessageComponent,
|
|
6
|
-
Image,
|
|
7
|
-
Plain,
|
|
8
|
-
)
|
|
9
|
-
from astrbot.api.message_components import (
|
|
10
|
-
WechatEmoji as Emoji,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class GeweDataParser:
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
content: str,
|
|
18
|
-
is_private_chat: bool = False,
|
|
19
|
-
cached_texts=None,
|
|
20
|
-
cached_images=None,
|
|
21
|
-
raw_message: dict | None = None,
|
|
22
|
-
downloader=None,
|
|
23
|
-
):
|
|
24
|
-
self._xml = None
|
|
25
|
-
self.content = content
|
|
26
|
-
self.is_private_chat = is_private_chat
|
|
27
|
-
self.cached_texts = cached_texts or {}
|
|
28
|
-
self.cached_images = cached_images or {}
|
|
29
|
-
self.downloader = downloader
|
|
30
|
-
|
|
31
|
-
raw_message = raw_message or {}
|
|
32
|
-
self.from_user_name = raw_message.get("from_user_name", {}).get("str", "")
|
|
33
|
-
self.to_user_name = raw_message.get("to_user_name", {}).get("str", "")
|
|
34
|
-
self.msg_id = raw_message.get("msg_id", "")
|
|
35
|
-
|
|
36
|
-
def _format_to_xml(self):
|
|
37
|
-
if self._xml:
|
|
38
|
-
return self._xml
|
|
39
|
-
|
|
40
|
-
try:
|
|
41
|
-
msg_str = self.content
|
|
42
|
-
if not self.is_private_chat:
|
|
43
|
-
parts = self.content.split(":\n", 1)
|
|
44
|
-
msg_str = parts[1] if len(parts) == 2 else self.content
|
|
45
|
-
|
|
46
|
-
self._xml = eT.fromstring(msg_str)
|
|
47
|
-
return self._xml
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.error(f"[XML解析失败] {e}")
|
|
50
|
-
raise
|
|
51
|
-
|
|
52
|
-
async def parse_mutil_49(self) -> list[BaseMessageComponent] | None:
|
|
53
|
-
"""处理 msg_type == 49 的多种 appmsg 类型(目前支持 type==57)"""
|
|
54
|
-
try:
|
|
55
|
-
appmsg_type = self._format_to_xml().findtext(".//appmsg/type")
|
|
56
|
-
if appmsg_type == "57":
|
|
57
|
-
return await self.parse_reply()
|
|
58
|
-
except Exception as e:
|
|
59
|
-
logger.warning(f"[parse_mutil_49] 解析失败: {e}")
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
async def parse_reply(self) -> list[BaseMessageComponent]:
|
|
63
|
-
"""处理 type == 57 的引用消息:支持文本(1)、图片(3)、嵌套49(49)"""
|
|
64
|
-
components = []
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
appmsg = self._format_to_xml().find("appmsg")
|
|
68
|
-
if appmsg is None:
|
|
69
|
-
return [Plain("[引用消息解析失败]")]
|
|
70
|
-
|
|
71
|
-
refermsg = appmsg.find("refermsg")
|
|
72
|
-
if refermsg is None:
|
|
73
|
-
return [Plain("[引用消息解析失败]")]
|
|
74
|
-
|
|
75
|
-
quote_type = int(refermsg.findtext("type", "0"))
|
|
76
|
-
nickname = refermsg.findtext("displayname", "未知发送者")
|
|
77
|
-
quote_content = refermsg.findtext("content", "")
|
|
78
|
-
svrid = refermsg.findtext("svrid")
|
|
79
|
-
|
|
80
|
-
match quote_type:
|
|
81
|
-
case 1: # 文本引用
|
|
82
|
-
quoted_text = self.cached_texts.get(str(svrid), quote_content)
|
|
83
|
-
components.append(Plain(f"[引用] {nickname}: {quoted_text}"))
|
|
84
|
-
|
|
85
|
-
case 3: # 图片引用
|
|
86
|
-
quoted_image_b64 = self.cached_images.get(str(svrid))
|
|
87
|
-
if not quoted_image_b64:
|
|
88
|
-
try:
|
|
89
|
-
quote_xml = eT.fromstring(quote_content)
|
|
90
|
-
img = quote_xml.find("img")
|
|
91
|
-
cdn_url = (
|
|
92
|
-
img.get("cdnbigimgurl") or img.get("cdnmidimgurl")
|
|
93
|
-
if img is not None
|
|
94
|
-
else None
|
|
95
|
-
)
|
|
96
|
-
if cdn_url and self.downloader:
|
|
97
|
-
image_resp = await self.downloader(
|
|
98
|
-
self.from_user_name,
|
|
99
|
-
self.to_user_name,
|
|
100
|
-
self.msg_id,
|
|
101
|
-
)
|
|
102
|
-
quoted_image_b64 = (
|
|
103
|
-
image_resp.get("Data", {})
|
|
104
|
-
.get("Data", {})
|
|
105
|
-
.get("Buffer")
|
|
106
|
-
)
|
|
107
|
-
except Exception as e:
|
|
108
|
-
logger.warning(f"[引用图片解析失败] svrid={svrid} err={e}")
|
|
109
|
-
|
|
110
|
-
if quoted_image_b64:
|
|
111
|
-
components.extend(
|
|
112
|
-
[
|
|
113
|
-
Image.fromBase64(quoted_image_b64),
|
|
114
|
-
Plain(f"[引用] {nickname}: [引用的图片]"),
|
|
115
|
-
],
|
|
116
|
-
)
|
|
117
|
-
else:
|
|
118
|
-
components.append(
|
|
119
|
-
Plain(f"[引用] {nickname}: [引用的图片 - 未能获取]"),
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
case 49: # 嵌套引用
|
|
123
|
-
try:
|
|
124
|
-
nested_root = eT.fromstring(quote_content)
|
|
125
|
-
nested_title = nested_root.findtext(".//appmsg/title", "")
|
|
126
|
-
components.append(Plain(f"[引用] {nickname}: {nested_title}"))
|
|
127
|
-
except Exception as e:
|
|
128
|
-
logger.warning(f"[嵌套引用解析失败] err={e}")
|
|
129
|
-
components.append(Plain(f"[引用] {nickname}: [嵌套引用消息]"))
|
|
130
|
-
|
|
131
|
-
case _: # 其他未识别类型
|
|
132
|
-
logger.info(f"[未知引用类型] quote_type={quote_type}")
|
|
133
|
-
components.append(Plain(f"[引用] {nickname}: [不支持的引用类型]"))
|
|
134
|
-
|
|
135
|
-
# 主消息标题
|
|
136
|
-
title = appmsg.findtext("title", "")
|
|
137
|
-
if title:
|
|
138
|
-
components.append(Plain(title))
|
|
139
|
-
|
|
140
|
-
except Exception as e:
|
|
141
|
-
logger.error(f"[parse_reply] 总体解析失败: {e}")
|
|
142
|
-
return [Plain("[引用消息解析失败]")]
|
|
143
|
-
|
|
144
|
-
return components
|
|
145
|
-
|
|
146
|
-
def parse_emoji(self) -> Emoji | None:
|
|
147
|
-
"""处理 msg_type == 47 的表情消息(emoji)"""
|
|
148
|
-
try:
|
|
149
|
-
emoji_element = self._format_to_xml().find(".//emoji")
|
|
150
|
-
if emoji_element is not None:
|
|
151
|
-
return Emoji(
|
|
152
|
-
md5=emoji_element.get("md5"),
|
|
153
|
-
md5_len=emoji_element.get("len"),
|
|
154
|
-
cdnurl=emoji_element.get("cdnurl"),
|
|
155
|
-
)
|
|
156
|
-
except Exception as e:
|
|
157
|
-
logger.error(f"[parse_emoji] 解析失败: {e}")
|
|
158
|
-
|
|
159
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|