AstrBot 4.7.4__py3-none-any.whl → 4.9.0__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/agent/runners/tool_loop_agent_runner.py +0 -1
- astrbot/core/agent/tool.py +7 -2
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +116 -1
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +32 -4
- astrbot/core/db/migration/migra_3_to_4.py +2 -0
- astrbot/core/db/migration/sqlite_v3.py +6 -4
- astrbot/core/db/po.py +16 -15
- astrbot/core/db/sqlite.py +56 -1
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
- astrbot/core/event_bus.py +6 -1
- astrbot/core/knowledge_base/retrieval/manager.py +5 -1
- astrbot/core/log.py +2 -1
- astrbot/core/message/components.py +9 -3
- astrbot/core/persona_mgr.py +2 -2
- astrbot/core/pipeline/content_safety_check/stage.py +1 -1
- astrbot/core/pipeline/context_utils.py +2 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
- astrbot/core/pipeline/process_stage/stage.py +1 -1
- astrbot/core/pipeline/respond/stage.py +4 -2
- astrbot/core/pipeline/result_decorate/stage.py +68 -21
- astrbot/core/pipeline/scheduler.py +5 -1
- astrbot/core/pipeline/waking_check/stage.py +10 -0
- astrbot/core/platform/astr_message_event.py +5 -3
- astrbot/core/platform/astrbot_message.py +2 -2
- astrbot/core/platform/manager.py +71 -9
- astrbot/core/platform/platform.py +109 -4
- astrbot/core/platform/platform_metadata.py +1 -1
- astrbot/core/platform/register.py +1 -0
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +13 -8
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
- astrbot/core/platform/sources/discord/client.py +16 -4
- astrbot/core/platform/sources/discord/components.py +2 -2
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +53 -26
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
- astrbot/core/platform/sources/lark/lark_event.py +39 -4
- astrbot/core/platform/sources/lark/server.py +206 -0
- astrbot/core/platform/sources/misskey/misskey_adapter.py +3 -5
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +58 -40
- astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
- astrbot/core/platform/sources/slack/slack_event.py +11 -10
- astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
- astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
- astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
- astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/func_tool_manager.py +3 -3
- astrbot/core/provider/manager.py +130 -74
- astrbot/core/provider/provider.py +12 -1
- astrbot/core/provider/sources/azure_tts_source.py +31 -9
- astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
- astrbot/core/provider/sources/dashscope_tts.py +3 -2
- astrbot/core/provider/sources/edge_tts_source.py +1 -1
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
- astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
- astrbot/core/provider/sources/gemini_source.py +12 -10
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
- astrbot/core/provider/sources/openai_embedding_source.py +2 -2
- astrbot/core/provider/sources/openai_source.py +4 -0
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
- astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
- astrbot/core/provider/sources/whisper_api_source.py +44 -12
- astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
- astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
- astrbot/core/star/context.py +2 -2
- astrbot/core/star/register/star_handler.py +22 -5
- astrbot/core/star/star_handler.py +85 -4
- astrbot/core/updator.py +3 -3
- astrbot/core/utils/io.py +1 -1
- astrbot/core/utils/session_waiter.py +17 -10
- astrbot/core/utils/shared_preferences.py +32 -0
- astrbot/core/utils/t2i/__init__.py +2 -2
- astrbot/core/utils/t2i/local_strategy.py +25 -31
- astrbot/core/utils/tencent_record_helper.py +2 -2
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +66 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +311 -76
- astrbot/dashboard/routes/config.py +14 -5
- astrbot/dashboard/routes/knowledge_base.py +254 -79
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +108 -51
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +9 -4
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,11 +2,13 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
|
-
from collections.abc import
|
|
5
|
+
from collections.abc import Callable, Coroutine
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from astrbot import logger
|
|
9
|
-
from astrbot.core
|
|
9
|
+
from astrbot.core import db_helper
|
|
10
|
+
from astrbot.core.db.po import PlatformMessageHistory
|
|
11
|
+
from astrbot.core.message.components import File, Image, Plain, Record, Reply, Video
|
|
10
12
|
from astrbot.core.message.message_event_result import MessageChain
|
|
11
13
|
from astrbot.core.platform import (
|
|
12
14
|
AstrBotMessage,
|
|
@@ -74,9 +76,8 @@ class WebChatAdapter(Platform):
|
|
|
74
76
|
platform_settings: dict,
|
|
75
77
|
event_queue: asyncio.Queue,
|
|
76
78
|
) -> None:
|
|
77
|
-
super().__init__(event_queue)
|
|
79
|
+
super().__init__(platform_config, event_queue)
|
|
78
80
|
|
|
79
|
-
self.config = platform_config
|
|
80
81
|
self.settings = platform_settings
|
|
81
82
|
self.unique_session = platform_settings["unique_session"]
|
|
82
83
|
self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
|
|
@@ -96,6 +97,92 @@ class WebChatAdapter(Platform):
|
|
|
96
97
|
await WebChatMessageEvent._send(message_chain, session.session_id)
|
|
97
98
|
await super().send_by_session(session, message_chain)
|
|
98
99
|
|
|
100
|
+
async def _get_message_history(
|
|
101
|
+
self, message_id: int
|
|
102
|
+
) -> PlatformMessageHistory | None:
|
|
103
|
+
return await db_helper.get_platform_message_history_by_id(message_id)
|
|
104
|
+
|
|
105
|
+
async def _parse_message_parts(
|
|
106
|
+
self,
|
|
107
|
+
message_parts: list,
|
|
108
|
+
depth: int = 0,
|
|
109
|
+
max_depth: int = 1,
|
|
110
|
+
) -> tuple[list, list[str]]:
|
|
111
|
+
"""解析消息段列表,返回消息组件列表和纯文本列表
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
message_parts: 消息段列表
|
|
115
|
+
depth: 当前递归深度
|
|
116
|
+
max_depth: 最大递归深度(用于处理 reply)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
tuple[list, list[str]]: (消息组件列表, 纯文本列表)
|
|
120
|
+
"""
|
|
121
|
+
components = []
|
|
122
|
+
text_parts = []
|
|
123
|
+
|
|
124
|
+
for part in message_parts:
|
|
125
|
+
part_type = part.get("type")
|
|
126
|
+
if part_type == "plain":
|
|
127
|
+
text = part.get("text", "")
|
|
128
|
+
components.append(Plain(text))
|
|
129
|
+
text_parts.append(text)
|
|
130
|
+
elif part_type == "reply":
|
|
131
|
+
message_id = part.get("message_id")
|
|
132
|
+
reply_chain = []
|
|
133
|
+
reply_message_str = ""
|
|
134
|
+
sender_id = None
|
|
135
|
+
sender_name = None
|
|
136
|
+
|
|
137
|
+
# recursively get the content of the referenced message
|
|
138
|
+
if depth < max_depth and message_id:
|
|
139
|
+
history = await self._get_message_history(message_id)
|
|
140
|
+
if history and history.content:
|
|
141
|
+
reply_parts = history.content.get("message", [])
|
|
142
|
+
if isinstance(reply_parts, list):
|
|
143
|
+
(
|
|
144
|
+
reply_chain,
|
|
145
|
+
reply_text_parts,
|
|
146
|
+
) = await self._parse_message_parts(
|
|
147
|
+
reply_parts,
|
|
148
|
+
depth=depth + 1,
|
|
149
|
+
max_depth=max_depth,
|
|
150
|
+
)
|
|
151
|
+
reply_message_str = "".join(reply_text_parts)
|
|
152
|
+
sender_id = history.sender_id
|
|
153
|
+
sender_name = history.sender_name
|
|
154
|
+
|
|
155
|
+
components.append(
|
|
156
|
+
Reply(
|
|
157
|
+
id=message_id,
|
|
158
|
+
chain=reply_chain,
|
|
159
|
+
message_str=reply_message_str,
|
|
160
|
+
sender_id=sender_id,
|
|
161
|
+
sender_nickname=sender_name,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
elif part_type == "image":
|
|
165
|
+
path = part.get("path")
|
|
166
|
+
if path:
|
|
167
|
+
components.append(Image.fromFileSystem(path))
|
|
168
|
+
elif part_type == "record":
|
|
169
|
+
path = part.get("path")
|
|
170
|
+
if path:
|
|
171
|
+
components.append(Record.fromFileSystem(path))
|
|
172
|
+
elif part_type == "file":
|
|
173
|
+
path = part.get("path")
|
|
174
|
+
if path:
|
|
175
|
+
filename = part.get("filename") or (
|
|
176
|
+
os.path.basename(path) if path else "file"
|
|
177
|
+
)
|
|
178
|
+
components.append(File(name=filename, file=path))
|
|
179
|
+
elif part_type == "video":
|
|
180
|
+
path = part.get("path")
|
|
181
|
+
if path:
|
|
182
|
+
components.append(Video.fromFileSystem(path))
|
|
183
|
+
|
|
184
|
+
return components, text_parts
|
|
185
|
+
|
|
99
186
|
async def convert_message(self, data: tuple) -> AstrBotMessage:
|
|
100
187
|
username, cid, payload = data
|
|
101
188
|
|
|
@@ -108,40 +195,19 @@ class WebChatAdapter(Platform):
|
|
|
108
195
|
abm.session_id = f"webchat!{username}!{cid}"
|
|
109
196
|
|
|
110
197
|
abm.message_id = str(uuid.uuid4())
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if payload["image_url"]:
|
|
116
|
-
if isinstance(payload["image_url"], list):
|
|
117
|
-
for img in payload["image_url"]:
|
|
118
|
-
abm.message.append(
|
|
119
|
-
Image.fromFileSystem(os.path.join(self.imgs_dir, img)),
|
|
120
|
-
)
|
|
121
|
-
else:
|
|
122
|
-
abm.message.append(
|
|
123
|
-
Image.fromFileSystem(
|
|
124
|
-
os.path.join(self.imgs_dir, payload["image_url"]),
|
|
125
|
-
),
|
|
126
|
-
)
|
|
127
|
-
if payload["audio_url"]:
|
|
128
|
-
if isinstance(payload["audio_url"], list):
|
|
129
|
-
for audio in payload["audio_url"]:
|
|
130
|
-
path = os.path.join(self.imgs_dir, audio)
|
|
131
|
-
abm.message.append(Record(file=path, path=path))
|
|
132
|
-
else:
|
|
133
|
-
path = os.path.join(self.imgs_dir, payload["audio_url"])
|
|
134
|
-
abm.message.append(Record(file=path, path=path))
|
|
198
|
+
|
|
199
|
+
# 处理消息段列表
|
|
200
|
+
message_parts = payload.get("message", [])
|
|
201
|
+
abm.message, message_str_parts = await self._parse_message_parts(message_parts)
|
|
135
202
|
|
|
136
203
|
logger.debug(f"WebChatAdapter: {abm.message}")
|
|
137
204
|
|
|
138
|
-
message_str = payload["message"]
|
|
139
205
|
abm.timestamp = int(time.time())
|
|
140
|
-
abm.message_str =
|
|
206
|
+
abm.message_str = "".join(message_str_parts)
|
|
141
207
|
abm.raw_message = data
|
|
142
208
|
return abm
|
|
143
209
|
|
|
144
|
-
def run(self) ->
|
|
210
|
+
def run(self) -> Coroutine[Any, Any, None]:
|
|
145
211
|
async def callback(data: tuple):
|
|
146
212
|
abm = await self.convert_message(data)
|
|
147
213
|
await self.handle_msg(abm)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import os
|
|
3
|
+
import shutil
|
|
3
4
|
import uuid
|
|
4
5
|
|
|
5
6
|
from astrbot.api import logger
|
|
6
7
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
|
7
|
-
from astrbot.api.message_components import Image, Plain, Record
|
|
8
|
+
from astrbot.api.message_components import File, Image, Plain, Record
|
|
8
9
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
9
|
-
from astrbot.core.utils.io import download_image_by_url
|
|
10
10
|
|
|
11
11
|
from .webchat_queue_mgr import webchat_queue_mgr
|
|
12
12
|
|
|
@@ -19,7 +19,9 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
19
19
|
os.makedirs(imgs_dir, exist_ok=True)
|
|
20
20
|
|
|
21
21
|
@staticmethod
|
|
22
|
-
async def _send(
|
|
22
|
+
async def _send(
|
|
23
|
+
message: MessageChain | None, session_id: str, streaming: bool = False
|
|
24
|
+
) -> str | None:
|
|
23
25
|
cid = session_id.split("!")[-1]
|
|
24
26
|
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
|
25
27
|
if not message:
|
|
@@ -30,7 +32,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
30
32
|
"streaming": False,
|
|
31
33
|
}, # end means this request is finished
|
|
32
34
|
)
|
|
33
|
-
return
|
|
35
|
+
return
|
|
34
36
|
|
|
35
37
|
data = ""
|
|
36
38
|
for comp in message.chain:
|
|
@@ -47,24 +49,11 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
47
49
|
)
|
|
48
50
|
elif isinstance(comp, Image):
|
|
49
51
|
# save image to local
|
|
50
|
-
filename = str(uuid.uuid4())
|
|
52
|
+
filename = f"{str(uuid.uuid4())}.jpg"
|
|
51
53
|
path = os.path.join(imgs_dir, filename)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
with open(ph, "rb") as f2:
|
|
56
|
-
f.write(f2.read())
|
|
57
|
-
elif comp.file.startswith("base64://"):
|
|
58
|
-
base64_str = comp.file[9:]
|
|
59
|
-
image_data = base64.b64decode(base64_str)
|
|
60
|
-
with open(path, "wb") as f:
|
|
61
|
-
f.write(image_data)
|
|
62
|
-
elif comp.file and comp.file.startswith("http"):
|
|
63
|
-
await download_image_by_url(comp.file, path=path)
|
|
64
|
-
else:
|
|
65
|
-
with open(path, "wb") as f:
|
|
66
|
-
with open(comp.file, "rb") as f2:
|
|
67
|
-
f.write(f2.read())
|
|
54
|
+
image_base64 = await comp.convert_to_base64()
|
|
55
|
+
with open(path, "wb") as f:
|
|
56
|
+
f.write(base64.b64decode(image_base64))
|
|
68
57
|
data = f"[IMAGE]{filename}"
|
|
69
58
|
await web_chat_back_queue.put(
|
|
70
59
|
{
|
|
@@ -76,19 +65,11 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
76
65
|
)
|
|
77
66
|
elif isinstance(comp, Record):
|
|
78
67
|
# save record to local
|
|
79
|
-
filename = str(uuid.uuid4())
|
|
68
|
+
filename = f"{str(uuid.uuid4())}.wav"
|
|
80
69
|
path = os.path.join(imgs_dir, filename)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
with open(ph, "rb") as f2:
|
|
85
|
-
f.write(f2.read())
|
|
86
|
-
elif comp.file and comp.file.startswith("http"):
|
|
87
|
-
await download_image_by_url(comp.file, path=path)
|
|
88
|
-
else:
|
|
89
|
-
with open(path, "wb") as f:
|
|
90
|
-
with open(comp.file, "rb") as f2:
|
|
91
|
-
f.write(f2.read())
|
|
70
|
+
record_base64 = await comp.convert_to_base64()
|
|
71
|
+
with open(path, "wb") as f:
|
|
72
|
+
f.write(base64.b64decode(record_base64))
|
|
92
73
|
data = f"[RECORD]{filename}"
|
|
93
74
|
await web_chat_back_queue.put(
|
|
94
75
|
{
|
|
@@ -98,14 +79,31 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
98
79
|
"streaming": streaming,
|
|
99
80
|
},
|
|
100
81
|
)
|
|
82
|
+
elif isinstance(comp, File):
|
|
83
|
+
# save file to local
|
|
84
|
+
file_path = await comp.get_file()
|
|
85
|
+
original_name = comp.name or os.path.basename(file_path)
|
|
86
|
+
ext = os.path.splitext(original_name)[1] or ""
|
|
87
|
+
filename = f"{uuid.uuid4()!s}{ext}"
|
|
88
|
+
dest_path = os.path.join(imgs_dir, filename)
|
|
89
|
+
shutil.copy2(file_path, dest_path)
|
|
90
|
+
data = f"[FILE]{filename}|{original_name}"
|
|
91
|
+
await web_chat_back_queue.put(
|
|
92
|
+
{
|
|
93
|
+
"type": "file",
|
|
94
|
+
"cid": cid,
|
|
95
|
+
"data": data,
|
|
96
|
+
"streaming": streaming,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
101
99
|
else:
|
|
102
100
|
logger.debug(f"webchat 忽略: {comp.type}")
|
|
103
101
|
|
|
104
102
|
return data
|
|
105
103
|
|
|
106
|
-
async def send(self, message: MessageChain):
|
|
104
|
+
async def send(self, message: MessageChain | None):
|
|
107
105
|
await WebChatMessageEvent._send(message, session_id=self.session_id)
|
|
108
|
-
await super().send(
|
|
106
|
+
await super().send(MessageChain([]))
|
|
109
107
|
|
|
110
108
|
async def send_streaming(self, generator, use_fallback: bool = False):
|
|
111
109
|
final_data = ""
|
|
@@ -131,6 +129,8 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
131
129
|
session_id=self.session_id,
|
|
132
130
|
streaming=True,
|
|
133
131
|
)
|
|
132
|
+
if not r:
|
|
133
|
+
continue
|
|
134
134
|
if chain.type == "reasoning":
|
|
135
135
|
reasoning_content += chain.get_plain_text()
|
|
136
136
|
else:
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
import traceback
|
|
7
|
+
from typing import cast
|
|
7
8
|
|
|
8
9
|
import aiohttp
|
|
9
10
|
import anyio
|
|
@@ -42,10 +43,9 @@ class WeChatPadProAdapter(Platform):
|
|
|
42
43
|
platform_settings: dict,
|
|
43
44
|
event_queue: asyncio.Queue,
|
|
44
45
|
) -> None:
|
|
45
|
-
super().__init__(event_queue)
|
|
46
|
+
super().__init__(platform_config, event_queue)
|
|
46
47
|
self._shutdown_event = None
|
|
47
48
|
self.wxnewpass = None
|
|
48
|
-
self.config = platform_config
|
|
49
49
|
self.settings = platform_settings
|
|
50
50
|
self.unique_session = platform_settings.get("unique_session", False)
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ class WeChatPadProAdapter(Platform):
|
|
|
70
70
|
)
|
|
71
71
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
72
72
|
self.auth_key = None # 用于保存生成的授权码
|
|
73
|
-
self.wxid = None # 用于保存登录成功后的 wxid
|
|
73
|
+
self.wxid: str | None = None # 用于保存登录成功后的 wxid
|
|
74
74
|
self.credentials_file = os.path.join(
|
|
75
75
|
get_astrbot_data_path(),
|
|
76
76
|
"wechatpadpro_credentials.json",
|
|
@@ -399,7 +399,7 @@ class WeChatPadProAdapter(Platform):
|
|
|
399
399
|
)
|
|
400
400
|
await asyncio.sleep(5)
|
|
401
401
|
|
|
402
|
-
async def handle_websocket_message(self, message: str):
|
|
402
|
+
async def handle_websocket_message(self, message: str | bytes):
|
|
403
403
|
"""处理从 WebSocket 接收到的消息。"""
|
|
404
404
|
logger.debug(f"收到 WebSocket 消息: {message}")
|
|
405
405
|
try:
|
|
@@ -431,10 +431,13 @@ class WeChatPadProAdapter(Platform):
|
|
|
431
431
|
|
|
432
432
|
async def convert_message(self, raw_message: dict) -> AstrBotMessage | None:
|
|
433
433
|
"""将 WeChatPadPro 原始消息转换为 AstrBotMessage。"""
|
|
434
|
+
if self.wxid is None:
|
|
435
|
+
logger.error("WeChatPadPro 适配器未登录或未获取到 wxid,无法处理消息。")
|
|
436
|
+
return None
|
|
434
437
|
abm = AstrBotMessage()
|
|
435
438
|
abm.raw_message = raw_message
|
|
436
439
|
abm.message_id = str(raw_message.get("msg_id"))
|
|
437
|
-
abm.timestamp = raw_message.get("create_time")
|
|
440
|
+
abm.timestamp = cast(int, raw_message.get("create_time"))
|
|
438
441
|
abm.self_id = self.wxid
|
|
439
442
|
|
|
440
443
|
if int(time.time()) - abm.timestamp > 180:
|
|
@@ -447,7 +450,7 @@ class WeChatPadProAdapter(Platform):
|
|
|
447
450
|
to_user_name = raw_message.get("to_user_name", {}).get("str", "")
|
|
448
451
|
content = raw_message.get("content", {}).get("str", "")
|
|
449
452
|
push_content = raw_message.get("push_content", "")
|
|
450
|
-
msg_type = raw_message.get("msg_type")
|
|
453
|
+
msg_type = cast(int, raw_message.get("msg_type"))
|
|
451
454
|
|
|
452
455
|
abm.message_str = ""
|
|
453
456
|
abm.message = []
|
|
@@ -575,7 +578,7 @@ class WeChatPadProAdapter(Platform):
|
|
|
575
578
|
from_user_name: str,
|
|
576
579
|
to_user_name: str,
|
|
577
580
|
msg_id: int,
|
|
578
|
-
):
|
|
581
|
+
) -> dict | None:
|
|
579
582
|
"""下载原始图片。"""
|
|
580
583
|
url = f"{self.base_url}/message/GetMsgBigImg"
|
|
581
584
|
params = {"key": self.auth_key}
|
|
@@ -726,12 +729,15 @@ class WeChatPadProAdapter(Platform):
|
|
|
726
729
|
# 图片消息
|
|
727
730
|
from_user_name = raw_message.get("from_user_name", {}).get("str", "")
|
|
728
731
|
to_user_name = raw_message.get("to_user_name", {}).get("str", "")
|
|
729
|
-
msg_id = raw_message.get("msg_id")
|
|
732
|
+
msg_id = cast(int, raw_message.get("msg_id"))
|
|
730
733
|
image_resp = await self._download_raw_image(
|
|
731
734
|
from_user_name,
|
|
732
735
|
to_user_name,
|
|
733
736
|
msg_id,
|
|
734
737
|
)
|
|
738
|
+
if image_resp is None:
|
|
739
|
+
logger.error(f"下载图片失败: msg_id={msg_id}")
|
|
740
|
+
return
|
|
735
741
|
image_bs64_data = (
|
|
736
742
|
image_resp.get("Data", {}).get("Data", {}).get("Buffer", None)
|
|
737
743
|
)
|
|
@@ -772,6 +778,9 @@ class WeChatPadProAdapter(Platform):
|
|
|
772
778
|
bufid = 0
|
|
773
779
|
to_user_name = raw_message.get("to_user_name", {}).get("str", "")
|
|
774
780
|
new_msg_id = raw_message.get("new_msg_id")
|
|
781
|
+
if new_msg_id is None:
|
|
782
|
+
logger.error("语音消息缺少 new_msg_id")
|
|
783
|
+
return
|
|
775
784
|
data_parser = GeweDataParser(
|
|
776
785
|
content=content,
|
|
777
786
|
is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
|
|
@@ -779,6 +788,9 @@ class WeChatPadProAdapter(Platform):
|
|
|
779
788
|
)
|
|
780
789
|
|
|
781
790
|
voicemsg = data_parser._format_to_xml().find("voicemsg")
|
|
791
|
+
if voicemsg is None:
|
|
792
|
+
logger.error("无法从 XML 解析 voicemsg 节点")
|
|
793
|
+
return
|
|
782
794
|
bufid = voicemsg.get("bufid") or "0"
|
|
783
795
|
length = int(voicemsg.get("length") or 0)
|
|
784
796
|
voice_resp = await self.download_voice(
|
|
@@ -787,6 +799,9 @@ class WeChatPadProAdapter(Platform):
|
|
|
787
799
|
bufid=bufid,
|
|
788
800
|
length=length,
|
|
789
801
|
)
|
|
802
|
+
if voice_resp is None:
|
|
803
|
+
logger.error(f"下载语音失败: new_msg_id={new_msg_id}")
|
|
804
|
+
return
|
|
790
805
|
voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None)
|
|
791
806
|
if voice_bs64_data:
|
|
792
807
|
voice_bs64_data = base64.b64decode(voice_bs64_data)
|
|
@@ -828,7 +843,8 @@ class WeChatPadProAdapter(Platform):
|
|
|
828
843
|
try:
|
|
829
844
|
if self.ws_handle_task:
|
|
830
845
|
self.ws_handle_task.cancel()
|
|
831
|
-
self._shutdown_event
|
|
846
|
+
if self._shutdown_event is not None:
|
|
847
|
+
self._shutdown_event.set()
|
|
832
848
|
except Exception:
|
|
833
849
|
pass
|
|
834
850
|
|
|
@@ -895,8 +911,8 @@ class WeChatPadProAdapter(Platform):
|
|
|
895
911
|
|
|
896
912
|
async def get_contact_details_list(
|
|
897
913
|
self,
|
|
898
|
-
room_wx_id_list: list[str] = None,
|
|
899
|
-
user_names: list[str] = None,
|
|
914
|
+
room_wx_id_list: list[str] | None = None,
|
|
915
|
+
user_names: list[str] | None = None,
|
|
900
916
|
) -> dict | None:
|
|
901
917
|
"""获取联系人详情列表。"""
|
|
902
918
|
if room_wx_id_list is None:
|
|
@@ -2,6 +2,8 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import uuid
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any, cast
|
|
5
7
|
|
|
6
8
|
import quart
|
|
7
9
|
from requests import Response
|
|
@@ -24,6 +26,7 @@ from astrbot.api.platform import (
|
|
|
24
26
|
from astrbot.core import logger
|
|
25
27
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
26
28
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
29
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
27
30
|
|
|
28
31
|
from .wecom_event import WecomPlatformEvent
|
|
29
32
|
from .wecom_kf import WeChatKF
|
|
@@ -38,7 +41,7 @@ else:
|
|
|
38
41
|
class WecomServer:
|
|
39
42
|
def __init__(self, event_queue: asyncio.Queue, config: dict):
|
|
40
43
|
self.server = quart.Quart(__name__)
|
|
41
|
-
self.port = int(config.get("port"))
|
|
44
|
+
self.port = int(cast(str, config.get("port")))
|
|
42
45
|
self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
|
|
43
46
|
self.server.add_url_rule(
|
|
44
47
|
"/callback/command",
|
|
@@ -58,12 +61,24 @@ class WecomServer:
|
|
|
58
61
|
config["corpid"].strip(),
|
|
59
62
|
)
|
|
60
63
|
|
|
61
|
-
self.callback = None
|
|
64
|
+
self.callback: Callable[[BaseMessage], Awaitable[None]] | None = None
|
|
62
65
|
self.shutdown_event = asyncio.Event()
|
|
63
66
|
|
|
64
67
|
async def verify(self):
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
"""内部服务器的 GET 验证入口"""
|
|
69
|
+
return await self.handle_verify(quart.request)
|
|
70
|
+
|
|
71
|
+
async def handle_verify(self, request) -> str:
|
|
72
|
+
"""处理验证请求,可被统一 webhook 入口复用
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
request: Quart 请求对象
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
验证响应
|
|
79
|
+
"""
|
|
80
|
+
logger.info(f"验证请求有效性: {request.args}")
|
|
81
|
+
args = request.args
|
|
67
82
|
try:
|
|
68
83
|
echo_str = self.crypto.check_signature(
|
|
69
84
|
args.get("msg_signature"),
|
|
@@ -78,17 +93,29 @@ class WecomServer:
|
|
|
78
93
|
raise
|
|
79
94
|
|
|
80
95
|
async def callback_command(self):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
"""内部服务器的 POST 回调入口"""
|
|
97
|
+
return await self.handle_callback(quart.request)
|
|
98
|
+
|
|
99
|
+
async def handle_callback(self, request) -> str:
|
|
100
|
+
"""处理回调请求,可被统一 webhook 入口复用
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
request: Quart 请求对象
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
响应内容
|
|
107
|
+
"""
|
|
108
|
+
data = await request.get_data()
|
|
109
|
+
msg_signature = request.args.get("msg_signature")
|
|
110
|
+
timestamp = request.args.get("timestamp")
|
|
111
|
+
nonce = request.args.get("nonce")
|
|
85
112
|
try:
|
|
86
113
|
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
|
87
114
|
except InvalidSignatureException:
|
|
88
115
|
logger.error("解密失败,签名异常,请检查配置。")
|
|
89
116
|
raise
|
|
90
117
|
else:
|
|
91
|
-
msg = parse_message(xml)
|
|
118
|
+
msg = cast(BaseMessage, parse_message(xml))
|
|
92
119
|
logger.info(f"解析成功: {msg}")
|
|
93
120
|
|
|
94
121
|
if self.callback:
|
|
@@ -118,14 +145,14 @@ class WecomPlatformAdapter(Platform):
|
|
|
118
145
|
platform_settings: dict,
|
|
119
146
|
event_queue: asyncio.Queue,
|
|
120
147
|
) -> None:
|
|
121
|
-
super().__init__(event_queue)
|
|
122
|
-
self.config = platform_config
|
|
148
|
+
super().__init__(platform_config, event_queue)
|
|
123
149
|
self.settingss = platform_settings
|
|
124
150
|
self.client_self_id = uuid.uuid4().hex[:8]
|
|
125
151
|
self.api_base_url = platform_config.get(
|
|
126
152
|
"api_base_url",
|
|
127
153
|
"https://qyapi.weixin.qq.com/cgi-bin/",
|
|
128
154
|
)
|
|
155
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
129
156
|
|
|
130
157
|
if not self.api_base_url:
|
|
131
158
|
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
|
@@ -150,10 +177,10 @@ class WecomPlatformAdapter(Platform):
|
|
|
150
177
|
# inject
|
|
151
178
|
self.wechat_kf_api = WeChatKF(client=self.client)
|
|
152
179
|
self.wechat_kf_message_api = WeChatKFMessage(self.client)
|
|
153
|
-
self.client.kf
|
|
154
|
-
self.client.kf_message
|
|
180
|
+
self.client.__setattr__("kf", self.wechat_kf_api)
|
|
181
|
+
self.client.__setattr__("kf_message", self.wechat_kf_message_api)
|
|
155
182
|
|
|
156
|
-
self.client.API_BASE_URL
|
|
183
|
+
self.client.__setattr__("API_BASE_URL", self.api_base_url)
|
|
157
184
|
|
|
158
185
|
async def callback(msg: BaseMessage):
|
|
159
186
|
if msg.type == "unknown" and msg._data["Event"] == "kf_msg_or_event":
|
|
@@ -232,41 +259,53 @@ class WecomPlatformAdapter(Platform):
|
|
|
232
259
|
)
|
|
233
260
|
except Exception as e:
|
|
234
261
|
logger.error(e)
|
|
235
|
-
|
|
262
|
+
|
|
263
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
264
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
265
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
266
|
+
log_webhook_info(f"{self.meta().id}(企业微信)", webhook_uuid)
|
|
267
|
+
# 保持运行状态,等待 shutdown
|
|
268
|
+
await self.server.shutdown_event.wait()
|
|
269
|
+
else:
|
|
270
|
+
await self.server.start_polling()
|
|
271
|
+
|
|
272
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
273
|
+
"""统一 Webhook 回调入口"""
|
|
274
|
+
# 根据请求方法分发到不同的处理函数
|
|
275
|
+
if request.method == "GET":
|
|
276
|
+
return await self.server.handle_verify(request)
|
|
277
|
+
else:
|
|
278
|
+
return await self.server.handle_callback(request)
|
|
236
279
|
|
|
237
280
|
async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
|
|
238
281
|
abm = AstrBotMessage()
|
|
239
|
-
if msg
|
|
240
|
-
assert isinstance(msg, TextMessage)
|
|
282
|
+
if isinstance(msg, TextMessage):
|
|
241
283
|
abm.message_str = msg.content
|
|
242
284
|
abm.self_id = str(msg.agent)
|
|
243
285
|
abm.message = [Plain(msg.content)]
|
|
244
286
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
245
287
|
abm.sender = MessageMember(
|
|
246
|
-
msg.source,
|
|
247
|
-
msg.source,
|
|
288
|
+
cast(str, msg.source),
|
|
289
|
+
cast(str, msg.source),
|
|
248
290
|
)
|
|
249
|
-
abm.message_id = msg.id
|
|
250
|
-
abm.timestamp = msg.time
|
|
291
|
+
abm.message_id = str(msg.id)
|
|
292
|
+
abm.timestamp = int(cast(int | str, msg.time))
|
|
251
293
|
abm.session_id = abm.sender.user_id
|
|
252
294
|
abm.raw_message = msg
|
|
253
|
-
elif msg
|
|
254
|
-
assert isinstance(msg, ImageMessage)
|
|
295
|
+
elif isinstance(msg, ImageMessage):
|
|
255
296
|
abm.message_str = "[图片]"
|
|
256
297
|
abm.self_id = str(msg.agent)
|
|
257
298
|
abm.message = [Image(file=msg.image, url=msg.image)]
|
|
258
299
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
259
300
|
abm.sender = MessageMember(
|
|
260
|
-
msg.source,
|
|
261
|
-
msg.source,
|
|
301
|
+
cast(str, msg.source),
|
|
302
|
+
cast(str, msg.source),
|
|
262
303
|
)
|
|
263
|
-
abm.message_id = msg.id
|
|
264
|
-
abm.timestamp = msg.time
|
|
304
|
+
abm.message_id = str(msg.id)
|
|
305
|
+
abm.timestamp = int(cast(int | str, msg.time))
|
|
265
306
|
abm.session_id = abm.sender.user_id
|
|
266
307
|
abm.raw_message = msg
|
|
267
|
-
elif msg
|
|
268
|
-
assert isinstance(msg, VoiceMessage)
|
|
269
|
-
|
|
308
|
+
elif isinstance(msg, VoiceMessage):
|
|
270
309
|
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
|
271
310
|
None,
|
|
272
311
|
self.client.media.download,
|
|
@@ -293,11 +332,11 @@ class WecomPlatformAdapter(Platform):
|
|
|
293
332
|
abm.message = [Record(file=path_wav, url=path_wav)]
|
|
294
333
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
295
334
|
abm.sender = MessageMember(
|
|
296
|
-
msg.source,
|
|
297
|
-
msg.source,
|
|
335
|
+
cast(str, msg.source),
|
|
336
|
+
cast(str, msg.source),
|
|
298
337
|
)
|
|
299
|
-
abm.message_id = msg.id
|
|
300
|
-
abm.timestamp = msg.time
|
|
338
|
+
abm.message_id = str(msg.id)
|
|
339
|
+
abm.timestamp = int(cast(int | str, msg.time))
|
|
301
340
|
abm.session_id = abm.sender.user_id
|
|
302
341
|
abm.raw_message = msg
|
|
303
342
|
else:
|
|
@@ -309,7 +348,7 @@ class WecomPlatformAdapter(Platform):
|
|
|
309
348
|
|
|
310
349
|
async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
|
|
311
350
|
msgtype = msg.get("msgtype")
|
|
312
|
-
external_userid = msg.get("external_userid")
|
|
351
|
+
external_userid = cast(str, msg.get("external_userid"))
|
|
313
352
|
abm = AstrBotMessage()
|
|
314
353
|
abm.raw_message = msg
|
|
315
354
|
abm.raw_message["_wechat_kf_flag"] = None # 方便处理
|
|
@@ -383,4 +422,4 @@ class WecomPlatformAdapter(Platform):
|
|
|
383
422
|
await self.server.server.shutdown()
|
|
384
423
|
except Exception as _:
|
|
385
424
|
pass
|
|
386
|
-
logger.info("企业微信
|
|
425
|
+
logger.info("企业微信 适配器已被关闭")
|
|
@@ -16,7 +16,7 @@ try:
|
|
|
16
16
|
import pydub
|
|
17
17
|
except Exception:
|
|
18
18
|
logger.warning(
|
|
19
|
-
"检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 ->
|
|
19
|
+
"检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 -> 平台日志 -> 安装 Pip 库安装 pydub。",
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -93,10 +93,10 @@ class WecomPlatformEvent(AstrMessageEvent):
|
|
|
93
93
|
if is_wechat_kf:
|
|
94
94
|
# 微信客服
|
|
95
95
|
kf_message_api = getattr(self.client, "kf_message", None)
|
|
96
|
-
if not kf_message_api:
|
|
96
|
+
if not isinstance(kf_message_api, WeChatKFMessage):
|
|
97
97
|
logger.warning("未找到微信客服发送消息方法。")
|
|
98
98
|
return
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
user_id = self.get_sender_id()
|
|
101
101
|
for comp in message.chain:
|
|
102
102
|
if isinstance(comp, Plain):
|