AstrBot 4.7.3__py3-none-any.whl → 4.8.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/message.py +21 -5
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +113 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- astrbot/core/message/components.py +6 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/platform/manager.py +67 -9
- astrbot/core/platform/platform.py +99 -2
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
- astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
- astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +50 -39
- astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
- astrbot/core/platform/sources/slack/slack_event.py +3 -3
- astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
- astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
- astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
- astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
- astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/provider.py +35 -0
- astrbot/core/provider/sources/whisper_api_source.py +43 -11
- astrbot/core/utils/file_extract.py +23 -0
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/webhook_utils.py +47 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +300 -70
- astrbot/dashboard/routes/config.py +32 -165
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +65 -6
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,6 +21,7 @@ from astrbot.api.platform import (
|
|
|
21
21
|
PlatformMetadata,
|
|
22
22
|
)
|
|
23
23
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
24
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
24
25
|
|
|
25
26
|
from ...register import register_platform_adapter
|
|
26
27
|
from .client import SlackSocketClient, SlackWebhookClient
|
|
@@ -39,9 +40,7 @@ class SlackAdapter(Platform):
|
|
|
39
40
|
platform_settings: dict,
|
|
40
41
|
event_queue: asyncio.Queue,
|
|
41
42
|
) -> None:
|
|
42
|
-
super().__init__(event_queue)
|
|
43
|
-
|
|
44
|
-
self.config = platform_config
|
|
43
|
+
super().__init__(platform_config, event_queue)
|
|
45
44
|
self.settings = platform_settings
|
|
46
45
|
self.unique_session = platform_settings.get("unique_session", False)
|
|
47
46
|
|
|
@@ -49,6 +48,7 @@ class SlackAdapter(Platform):
|
|
|
49
48
|
self.app_token = platform_config.get("app_token")
|
|
50
49
|
self.signing_secret = platform_config.get("signing_secret")
|
|
51
50
|
self.connection_mode = platform_config.get("slack_connection_mode", "socket")
|
|
51
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
52
52
|
self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0")
|
|
53
53
|
self.webhook_port = platform_config.get("slack_webhook_port", 3000)
|
|
54
54
|
self.webhook_path = platform_config.get(
|
|
@@ -361,10 +361,17 @@ class SlackAdapter(Platform):
|
|
|
361
361
|
self._handle_webhook_event,
|
|
362
362
|
)
|
|
363
363
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
365
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
366
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
367
|
+
log_webhook_info(f"{self.meta().id}(Slack)", webhook_uuid)
|
|
368
|
+
# 保持运行状态,等待 shutdown
|
|
369
|
+
await self.webhook_client.shutdown_event.wait()
|
|
370
|
+
else:
|
|
371
|
+
logger.info(
|
|
372
|
+
f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
|
|
373
|
+
)
|
|
374
|
+
await self.webhook_client.start()
|
|
368
375
|
|
|
369
376
|
else:
|
|
370
377
|
raise ValueError(
|
|
@@ -391,6 +398,13 @@ class SlackAdapter(Platform):
|
|
|
391
398
|
if abm:
|
|
392
399
|
await self.handle_msg(abm)
|
|
393
400
|
|
|
401
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
402
|
+
"""统一 Webhook 回调入口"""
|
|
403
|
+
if self.connection_mode != "webhook" or not self.webhook_client:
|
|
404
|
+
return {"error": "Slack adapter is not in webhook mode"}, 400
|
|
405
|
+
|
|
406
|
+
return await self.webhook_client.handle_callback(request)
|
|
407
|
+
|
|
394
408
|
async def terminate(self):
|
|
395
409
|
if self.socket_client:
|
|
396
410
|
await self.socket_client.stop()
|
|
@@ -31,7 +31,7 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
31
31
|
async def _from_segment_to_slack_block(
|
|
32
32
|
segment: BaseMessageComponent,
|
|
33
33
|
web_client: AsyncWebClient,
|
|
34
|
-
) -> dict:
|
|
34
|
+
) -> dict | None:
|
|
35
35
|
"""将消息段转换为 Slack 块格式"""
|
|
36
36
|
if isinstance(segment, Plain):
|
|
37
37
|
return {"type": "section", "text": {"type": "mrkdwn", "text": segment.text}}
|
|
@@ -85,7 +85,6 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
85
85
|
"text": f"文件: <{file_url}|{segment.name or '文件'}>",
|
|
86
86
|
},
|
|
87
87
|
}
|
|
88
|
-
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
|
|
89
88
|
|
|
90
89
|
@staticmethod
|
|
91
90
|
async def _parse_slack_blocks(
|
|
@@ -115,7 +114,8 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
115
114
|
segment,
|
|
116
115
|
web_client,
|
|
117
116
|
)
|
|
118
|
-
|
|
117
|
+
if block:
|
|
118
|
+
blocks.append(block)
|
|
119
119
|
|
|
120
120
|
# 如果最后还有文本内容
|
|
121
121
|
if text_content.strip():
|
|
@@ -42,8 +42,7 @@ class TelegramPlatformAdapter(Platform):
|
|
|
42
42
|
platform_settings: dict,
|
|
43
43
|
event_queue: asyncio.Queue,
|
|
44
44
|
) -> None:
|
|
45
|
-
super().__init__(event_queue)
|
|
46
|
-
self.config = platform_config
|
|
45
|
+
super().__init__(platform_config, event_queue)
|
|
47
46
|
self.settings = platform_settings
|
|
48
47
|
self.client_self_id = uuid.uuid4().hex[:8]
|
|
49
48
|
|
|
@@ -381,7 +380,9 @@ class TelegramPlatformAdapter(Platform):
|
|
|
381
380
|
f"Telegram document file_path is None, cannot save the file {file_name}.",
|
|
382
381
|
)
|
|
383
382
|
else:
|
|
384
|
-
message.message.append(
|
|
383
|
+
message.message.append(
|
|
384
|
+
Comp.File(file=file_path, name=file_name, url=file_path)
|
|
385
|
+
)
|
|
385
386
|
|
|
386
387
|
elif update.message.video:
|
|
387
388
|
file = await update.message.video.get_file()
|
|
@@ -6,7 +6,9 @@ from collections.abc import Awaitable, Callable
|
|
|
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,36 +195,15 @@ 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
|
|
|
@@ -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,6 +79,23 @@ 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
|
|
|
@@ -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:
|
|
@@ -42,10 +42,9 @@ class WeChatPadProAdapter(Platform):
|
|
|
42
42
|
platform_settings: dict,
|
|
43
43
|
event_queue: asyncio.Queue,
|
|
44
44
|
) -> None:
|
|
45
|
-
super().__init__(event_queue)
|
|
45
|
+
super().__init__(platform_config, event_queue)
|
|
46
46
|
self._shutdown_event = None
|
|
47
47
|
self.wxnewpass = None
|
|
48
|
-
self.config = platform_config
|
|
49
48
|
self.settings = platform_settings
|
|
50
49
|
self.unique_session = platform_settings.get("unique_session", False)
|
|
51
50
|
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import uuid
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
import quart
|
|
7
8
|
from requests import Response
|
|
@@ -24,6 +25,7 @@ from astrbot.api.platform import (
|
|
|
24
25
|
from astrbot.core import logger
|
|
25
26
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
26
27
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
28
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
27
29
|
|
|
28
30
|
from .wecom_event import WecomPlatformEvent
|
|
29
31
|
from .wecom_kf import WeChatKF
|
|
@@ -62,8 +64,20 @@ class WecomServer:
|
|
|
62
64
|
self.shutdown_event = asyncio.Event()
|
|
63
65
|
|
|
64
66
|
async def verify(self):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
"""内部服务器的 GET 验证入口"""
|
|
68
|
+
return await self.handle_verify(quart.request)
|
|
69
|
+
|
|
70
|
+
async def handle_verify(self, request) -> str:
|
|
71
|
+
"""处理验证请求,可被统一 webhook 入口复用
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
request: Quart 请求对象
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
验证响应
|
|
78
|
+
"""
|
|
79
|
+
logger.info(f"验证请求有效性: {request.args}")
|
|
80
|
+
args = request.args
|
|
67
81
|
try:
|
|
68
82
|
echo_str = self.crypto.check_signature(
|
|
69
83
|
args.get("msg_signature"),
|
|
@@ -78,10 +92,22 @@ class WecomServer:
|
|
|
78
92
|
raise
|
|
79
93
|
|
|
80
94
|
async def callback_command(self):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
"""内部服务器的 POST 回调入口"""
|
|
96
|
+
return await self.handle_callback(quart.request)
|
|
97
|
+
|
|
98
|
+
async def handle_callback(self, request) -> str:
|
|
99
|
+
"""处理回调请求,可被统一 webhook 入口复用
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
request: Quart 请求对象
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
响应内容
|
|
106
|
+
"""
|
|
107
|
+
data = await request.get_data()
|
|
108
|
+
msg_signature = request.args.get("msg_signature")
|
|
109
|
+
timestamp = request.args.get("timestamp")
|
|
110
|
+
nonce = request.args.get("nonce")
|
|
85
111
|
try:
|
|
86
112
|
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
|
87
113
|
except InvalidSignatureException:
|
|
@@ -118,14 +144,14 @@ class WecomPlatformAdapter(Platform):
|
|
|
118
144
|
platform_settings: dict,
|
|
119
145
|
event_queue: asyncio.Queue,
|
|
120
146
|
) -> None:
|
|
121
|
-
super().__init__(event_queue)
|
|
122
|
-
self.config = platform_config
|
|
147
|
+
super().__init__(platform_config, event_queue)
|
|
123
148
|
self.settingss = platform_settings
|
|
124
149
|
self.client_self_id = uuid.uuid4().hex[:8]
|
|
125
150
|
self.api_base_url = platform_config.get(
|
|
126
151
|
"api_base_url",
|
|
127
152
|
"https://qyapi.weixin.qq.com/cgi-bin/",
|
|
128
153
|
)
|
|
154
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
129
155
|
|
|
130
156
|
if not self.api_base_url:
|
|
131
157
|
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
|
@@ -232,7 +258,23 @@ class WecomPlatformAdapter(Platform):
|
|
|
232
258
|
)
|
|
233
259
|
except Exception as e:
|
|
234
260
|
logger.error(e)
|
|
235
|
-
|
|
261
|
+
|
|
262
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
263
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
264
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
265
|
+
log_webhook_info(f"{self.meta().id}(企业微信)", webhook_uuid)
|
|
266
|
+
# 保持运行状态,等待 shutdown
|
|
267
|
+
await self.server.shutdown_event.wait()
|
|
268
|
+
else:
|
|
269
|
+
await self.server.start_polling()
|
|
270
|
+
|
|
271
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
272
|
+
"""统一 Webhook 回调入口"""
|
|
273
|
+
# 根据请求方法分发到不同的处理函数
|
|
274
|
+
if request.method == "GET":
|
|
275
|
+
return await self.server.handle_verify(request)
|
|
276
|
+
else:
|
|
277
|
+
return await self.server.handle_callback(request)
|
|
236
278
|
|
|
237
279
|
async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
|
|
238
280
|
abm = AstrBotMessage()
|
|
@@ -22,6 +22,7 @@ from astrbot.api.platform import (
|
|
|
22
22
|
PlatformMetadata,
|
|
23
23
|
)
|
|
24
24
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
25
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
25
26
|
|
|
26
27
|
from ...register import register_platform_adapter
|
|
27
28
|
from .wecomai_api import (
|
|
@@ -103,9 +104,7 @@ class WecomAIBotAdapter(Platform):
|
|
|
103
104
|
platform_settings: dict,
|
|
104
105
|
event_queue: asyncio.Queue,
|
|
105
106
|
) -> None:
|
|
106
|
-
super().__init__(event_queue)
|
|
107
|
-
|
|
108
|
-
self.config = platform_config
|
|
107
|
+
super().__init__(platform_config, event_queue)
|
|
109
108
|
self.settings = platform_settings
|
|
110
109
|
|
|
111
110
|
# 初始化配置参数
|
|
@@ -122,6 +121,7 @@ class WecomAIBotAdapter(Platform):
|
|
|
122
121
|
"wecomaibot_friend_message_welcome_text",
|
|
123
122
|
"",
|
|
124
123
|
)
|
|
124
|
+
self.unified_webhook_mode = self.config.get("unified_webhook_mode", False)
|
|
125
125
|
|
|
126
126
|
# 平台元数据
|
|
127
127
|
self.metadata = PlatformMetadata(
|
|
@@ -425,17 +425,34 @@ class WecomAIBotAdapter(Platform):
|
|
|
425
425
|
|
|
426
426
|
def run(self) -> Awaitable[Any]:
|
|
427
427
|
"""运行适配器,同时启动HTTP服务器和队列监听器"""
|
|
428
|
-
logger.info("启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port)
|
|
429
428
|
|
|
430
429
|
async def run_both():
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
self.
|
|
435
|
-
|
|
430
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
431
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
432
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
433
|
+
log_webhook_info(f"{self.meta().id}(企业微信智能机器人)", webhook_uuid)
|
|
434
|
+
# 只运行队列监听器
|
|
435
|
+
await self.queue_listener.run()
|
|
436
|
+
else:
|
|
437
|
+
logger.info(
|
|
438
|
+
"启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port
|
|
439
|
+
)
|
|
440
|
+
# 同时运行HTTP服务器和队列监听器
|
|
441
|
+
await asyncio.gather(
|
|
442
|
+
self.server.start_server(),
|
|
443
|
+
self.queue_listener.run(),
|
|
444
|
+
)
|
|
436
445
|
|
|
437
446
|
return run_both()
|
|
438
447
|
|
|
448
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
449
|
+
"""统一 Webhook 回调入口"""
|
|
450
|
+
# 根据请求方法分发到不同的处理函数
|
|
451
|
+
if request.method == "GET":
|
|
452
|
+
return await self.server.handle_verify(request)
|
|
453
|
+
else:
|
|
454
|
+
return await self.server.handle_callback(request)
|
|
455
|
+
|
|
439
456
|
async def terminate(self):
|
|
440
457
|
"""终止适配器"""
|
|
441
458
|
logger.info("企业微信智能机器人适配器正在关闭...")
|
|
@@ -59,8 +59,19 @@ class WecomAIBotServer:
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
async def verify_url(self):
|
|
62
|
-
"""
|
|
63
|
-
|
|
62
|
+
"""内部服务器的 GET 验证入口"""
|
|
63
|
+
return await self.handle_verify(quart.request)
|
|
64
|
+
|
|
65
|
+
async def handle_verify(self, request):
|
|
66
|
+
"""处理 URL 验证请求,可被统一 webhook 入口复用
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
request: Quart 请求对象
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
验证响应元组 (content, status_code, headers)
|
|
73
|
+
"""
|
|
74
|
+
args = request.args
|
|
64
75
|
msg_signature = args.get("msg_signature")
|
|
65
76
|
timestamp = args.get("timestamp")
|
|
66
77
|
nonce = args.get("nonce")
|
|
@@ -81,8 +92,19 @@ class WecomAIBotServer:
|
|
|
81
92
|
return result, 200, {"Content-Type": "text/plain"}
|
|
82
93
|
|
|
83
94
|
async def handle_message(self):
|
|
84
|
-
"""
|
|
85
|
-
|
|
95
|
+
"""内部服务器的 POST 消息回调入口"""
|
|
96
|
+
return await self.handle_callback(quart.request)
|
|
97
|
+
|
|
98
|
+
async def handle_callback(self, request):
|
|
99
|
+
"""处理消息回调,可被统一 webhook 入口复用
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
request: Quart 请求对象
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
响应元组 (content, status_code, headers)
|
|
106
|
+
"""
|
|
107
|
+
args = request.args
|
|
86
108
|
msg_signature = args.get("msg_signature")
|
|
87
109
|
timestamp = args.get("timestamp")
|
|
88
110
|
nonce = args.get("nonce")
|
|
@@ -102,7 +124,7 @@ class WecomAIBotServer:
|
|
|
102
124
|
|
|
103
125
|
try:
|
|
104
126
|
# 获取请求体
|
|
105
|
-
post_data = await
|
|
127
|
+
post_data = await request.get_data()
|
|
106
128
|
|
|
107
129
|
# 确保 post_data 是 bytes 类型
|
|
108
130
|
if isinstance(post_data, str):
|