AstrBot 4.7.4__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/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +58 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- 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 +4 -3
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +4 -6
- 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 +1 -2
- 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/sources/whisper_api_source.py +43 -11
- 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 +19 -0
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/RECORD +46 -44
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import botpy
|
|
5
6
|
import botpy.message
|
|
@@ -11,6 +12,7 @@ from astrbot import logger
|
|
|
11
12
|
from astrbot.api.event import MessageChain
|
|
12
13
|
from astrbot.api.platform import AstrBotMessage, MessageType, Platform, PlatformMetadata
|
|
13
14
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
15
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
14
16
|
|
|
15
17
|
from ...register import register_platform_adapter
|
|
16
18
|
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
|
@@ -87,13 +89,12 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
87
89
|
platform_settings: dict,
|
|
88
90
|
event_queue: asyncio.Queue,
|
|
89
91
|
) -> None:
|
|
90
|
-
super().__init__(event_queue)
|
|
91
|
-
|
|
92
|
-
self.config = platform_config
|
|
92
|
+
super().__init__(platform_config, event_queue)
|
|
93
93
|
|
|
94
94
|
self.appid = platform_config["appid"]
|
|
95
95
|
self.secret = platform_config["secret"]
|
|
96
96
|
self.unique_session = platform_settings["unique_session"]
|
|
97
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
97
98
|
|
|
98
99
|
intents = botpy.Intents(
|
|
99
100
|
public_messages=True,
|
|
@@ -106,6 +107,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
106
107
|
timeout=20,
|
|
107
108
|
)
|
|
108
109
|
self.client.set_platform(self)
|
|
110
|
+
self.webhook_helper = None
|
|
109
111
|
|
|
110
112
|
async def send_by_session(
|
|
111
113
|
self,
|
|
@@ -128,16 +130,37 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
128
130
|
self.client,
|
|
129
131
|
)
|
|
130
132
|
await self.webhook_helper.initialize()
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
135
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
136
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
137
|
+
log_webhook_info(f"{self.meta().id}(QQ 官方机器人 Webhook)", webhook_uuid)
|
|
138
|
+
# 保持运行状态,等待 shutdown
|
|
139
|
+
await self.webhook_helper.shutdown_event.wait()
|
|
140
|
+
else:
|
|
141
|
+
await self.webhook_helper.start_polling()
|
|
132
142
|
|
|
133
143
|
def get_client(self) -> botClient:
|
|
134
144
|
return self.client
|
|
135
145
|
|
|
146
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
147
|
+
"""统一 Webhook 回调入口"""
|
|
148
|
+
if not self.webhook_helper:
|
|
149
|
+
return {"error": "Webhook helper not initialized"}, 500
|
|
150
|
+
|
|
151
|
+
# 复用 webhook_helper 的回调处理逻辑
|
|
152
|
+
return await self.webhook_helper.handle_callback(request)
|
|
153
|
+
|
|
136
154
|
async def terminate(self):
|
|
137
|
-
self.webhook_helper
|
|
155
|
+
if self.webhook_helper:
|
|
156
|
+
self.webhook_helper.shutdown_event.set()
|
|
138
157
|
await self.client.close()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
if self.webhook_helper and not self.unified_webhook_mode:
|
|
159
|
+
try:
|
|
160
|
+
await self.webhook_helper.server.shutdown()
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"Exception occurred during QQOfficialWebhook server shutdown: {exc}",
|
|
164
|
+
exc_info=True,
|
|
165
|
+
)
|
|
143
166
|
logger.info("QQ 机器人官方 API 适配器已经被优雅地关闭")
|
|
@@ -78,7 +78,19 @@ class QQOfficialWebhook:
|
|
|
78
78
|
return response
|
|
79
79
|
|
|
80
80
|
async def callback(self):
|
|
81
|
-
|
|
81
|
+
"""内部服务器的回调入口"""
|
|
82
|
+
return await self.handle_callback(quart.request)
|
|
83
|
+
|
|
84
|
+
async def handle_callback(self, request) -> dict:
|
|
85
|
+
"""处理 webhook 回调,可被统一 webhook 入口复用
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
request: Quart 请求对象
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
响应数据
|
|
92
|
+
"""
|
|
93
|
+
msg: dict = await request.json
|
|
82
94
|
logger.debug(f"收到 qq_official_webhook 回调: {msg}")
|
|
83
95
|
|
|
84
96
|
event = msg.get("t")
|
|
@@ -38,8 +38,7 @@ class SatoriPlatformAdapter(Platform):
|
|
|
38
38
|
platform_settings: dict,
|
|
39
39
|
event_queue: asyncio.Queue,
|
|
40
40
|
) -> None:
|
|
41
|
-
super().__init__(event_queue)
|
|
42
|
-
self.config = platform_config
|
|
41
|
+
super().__init__(platform_config, event_queue)
|
|
43
42
|
self.settings = platform_settings
|
|
44
43
|
|
|
45
44
|
self.api_base_url = self.config.get(
|
|
@@ -47,51 +47,62 @@ class SlackWebhookClient:
|
|
|
47
47
|
|
|
48
48
|
@self.app.route(self.path, methods=["POST"])
|
|
49
49
|
async def slack_events():
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
# 获取请求体和头部
|
|
53
|
-
body = await request.get_data()
|
|
54
|
-
event_data = json.loads(body.decode("utf-8"))
|
|
55
|
-
|
|
56
|
-
# Verify Slack request signature
|
|
57
|
-
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
|
58
|
-
signature = request.headers.get("X-Slack-Signature")
|
|
59
|
-
if not timestamp or not signature:
|
|
60
|
-
return Response("Missing headers", status=400)
|
|
61
|
-
# Calculate the HMAC signature
|
|
62
|
-
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
63
|
-
my_signature = (
|
|
64
|
-
"v0="
|
|
65
|
-
+ hmac.new(
|
|
66
|
-
self.signing_secret.encode("utf-8"),
|
|
67
|
-
sig_basestring.encode("utf-8"),
|
|
68
|
-
hashlib.sha256,
|
|
69
|
-
).hexdigest()
|
|
70
|
-
)
|
|
71
|
-
# Verify the signature
|
|
72
|
-
if not hmac.compare_digest(my_signature, signature):
|
|
73
|
-
logger.warning("Slack request signature verification failed")
|
|
74
|
-
return Response("Invalid signature", status=400)
|
|
75
|
-
logger.info(f"Received Slack event: {event_data}")
|
|
76
|
-
|
|
77
|
-
# 处理 URL 验证事件
|
|
78
|
-
if event_data.get("type") == "url_verification":
|
|
79
|
-
return {"challenge": event_data.get("challenge")}
|
|
80
|
-
# 处理事件
|
|
81
|
-
if self.event_handler and event_data.get("type") == "event_callback":
|
|
82
|
-
await self.event_handler(event_data)
|
|
83
|
-
|
|
84
|
-
return Response("", status=200)
|
|
85
|
-
|
|
86
|
-
except Exception as e:
|
|
87
|
-
logger.error(f"处理 Slack 事件时出错: {e}")
|
|
88
|
-
return Response("Internal Server Error", status=500)
|
|
50
|
+
"""内部服务器的 POST 回调入口"""
|
|
51
|
+
return await self.handle_callback(request)
|
|
89
52
|
|
|
90
53
|
@self.app.route("/health", methods=["GET"])
|
|
91
54
|
async def health_check():
|
|
92
55
|
"""健康检查端点"""
|
|
93
56
|
return {"status": "ok", "service": "slack-webhook"}
|
|
94
57
|
|
|
58
|
+
async def handle_callback(self, req):
|
|
59
|
+
"""处理 Slack 回调请求,可被统一 webhook 入口复用
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
req: Quart 请求对象
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Response 对象或字典
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
# 获取请求体和头部
|
|
69
|
+
body = await req.get_data()
|
|
70
|
+
event_data = json.loads(body.decode("utf-8"))
|
|
71
|
+
|
|
72
|
+
# Verify Slack request signature
|
|
73
|
+
timestamp = req.headers.get("X-Slack-Request-Timestamp")
|
|
74
|
+
signature = req.headers.get("X-Slack-Signature")
|
|
75
|
+
if not timestamp or not signature:
|
|
76
|
+
return Response("Missing headers", status=400)
|
|
77
|
+
# Calculate the HMAC signature
|
|
78
|
+
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
79
|
+
my_signature = (
|
|
80
|
+
"v0="
|
|
81
|
+
+ hmac.new(
|
|
82
|
+
self.signing_secret.encode("utf-8"),
|
|
83
|
+
sig_basestring.encode("utf-8"),
|
|
84
|
+
hashlib.sha256,
|
|
85
|
+
).hexdigest()
|
|
86
|
+
)
|
|
87
|
+
# Verify the signature
|
|
88
|
+
if not hmac.compare_digest(my_signature, signature):
|
|
89
|
+
logger.warning("Slack request signature verification failed")
|
|
90
|
+
return Response("Invalid signature", status=400)
|
|
91
|
+
logger.info(f"Received Slack event: {event_data}")
|
|
92
|
+
|
|
93
|
+
# 处理 URL 验证事件
|
|
94
|
+
if event_data.get("type") == "url_verification":
|
|
95
|
+
return {"challenge": event_data.get("challenge")}
|
|
96
|
+
# 处理事件
|
|
97
|
+
if self.event_handler and event_data.get("type") == "event_callback":
|
|
98
|
+
await self.event_handler(event_data)
|
|
99
|
+
|
|
100
|
+
return Response("", status=200)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"处理 Slack 事件时出错: {e}")
|
|
104
|
+
return Response("Internal Server Error", status=500)
|
|
105
|
+
|
|
95
106
|
async def start(self):
|
|
96
107
|
"""启动 Webhook 服务器"""
|
|
97
108
|
logger.info(
|
|
@@ -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
|
|
|
@@ -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
|
|