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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from typing import Any, cast
|
|
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
|
|
@@ -34,7 +36,9 @@ class botClient(Client):
|
|
|
34
36
|
MessageType.GROUP_MESSAGE,
|
|
35
37
|
)
|
|
36
38
|
abm.session_id = (
|
|
37
|
-
abm.sender.user_id
|
|
39
|
+
abm.sender.user_id
|
|
40
|
+
if self.platform.unique_session
|
|
41
|
+
else cast(str, message.group_openid)
|
|
38
42
|
)
|
|
39
43
|
self._commit(abm)
|
|
40
44
|
|
|
@@ -87,13 +91,12 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
87
91
|
platform_settings: dict,
|
|
88
92
|
event_queue: asyncio.Queue,
|
|
89
93
|
) -> None:
|
|
90
|
-
super().__init__(event_queue)
|
|
91
|
-
|
|
92
|
-
self.config = platform_config
|
|
94
|
+
super().__init__(platform_config, event_queue)
|
|
93
95
|
|
|
94
96
|
self.appid = platform_config["appid"]
|
|
95
97
|
self.secret = platform_config["secret"]
|
|
96
98
|
self.unique_session = platform_settings["unique_session"]
|
|
99
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
97
100
|
|
|
98
101
|
intents = botpy.Intents(
|
|
99
102
|
public_messages=True,
|
|
@@ -106,6 +109,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
106
109
|
timeout=20,
|
|
107
110
|
)
|
|
108
111
|
self.client.set_platform(self)
|
|
112
|
+
self.webhook_helper = None
|
|
109
113
|
|
|
110
114
|
async def send_by_session(
|
|
111
115
|
self,
|
|
@@ -118,7 +122,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
118
122
|
return PlatformMetadata(
|
|
119
123
|
name="qq_official_webhook",
|
|
120
124
|
description="QQ 机器人官方 API 适配器",
|
|
121
|
-
id=self.config.get("id"),
|
|
125
|
+
id=cast(str, self.config.get("id")),
|
|
122
126
|
)
|
|
123
127
|
|
|
124
128
|
async def run(self):
|
|
@@ -128,16 +132,37 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
128
132
|
self.client,
|
|
129
133
|
)
|
|
130
134
|
await self.webhook_helper.initialize()
|
|
131
|
-
|
|
135
|
+
|
|
136
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
137
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
138
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
139
|
+
log_webhook_info(f"{self.meta().id}(QQ 官方机器人 Webhook)", webhook_uuid)
|
|
140
|
+
# 保持运行状态,等待 shutdown
|
|
141
|
+
await self.webhook_helper.shutdown_event.wait()
|
|
142
|
+
else:
|
|
143
|
+
await self.webhook_helper.start_polling()
|
|
132
144
|
|
|
133
145
|
def get_client(self) -> botClient:
|
|
134
146
|
return self.client
|
|
135
147
|
|
|
148
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
149
|
+
"""统一 Webhook 回调入口"""
|
|
150
|
+
if not self.webhook_helper:
|
|
151
|
+
return {"error": "Webhook helper not initialized"}, 500
|
|
152
|
+
|
|
153
|
+
# 复用 webhook_helper 的回调处理逻辑
|
|
154
|
+
return await self.webhook_helper.handle_callback(request)
|
|
155
|
+
|
|
136
156
|
async def terminate(self):
|
|
137
|
-
self.webhook_helper
|
|
157
|
+
if self.webhook_helper:
|
|
158
|
+
self.webhook_helper.shutdown_event.set()
|
|
138
159
|
await self.client.close()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
if self.webhook_helper and not self.unified_webhook_mode:
|
|
161
|
+
try:
|
|
162
|
+
await self.webhook_helper.server.shutdown()
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.warning(
|
|
165
|
+
f"Exception occurred during QQOfficialWebhook server shutdown: {exc}",
|
|
166
|
+
exc_info=True,
|
|
167
|
+
)
|
|
143
168
|
logger.info("QQ 机器人官方 API 适配器已经被优雅地关闭")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from typing import cast
|
|
3
4
|
|
|
4
5
|
import quart
|
|
5
6
|
from botpy import BotAPI, BotHttp, BotWebSocket, Client, ConnectionSession, Token
|
|
@@ -78,7 +79,19 @@ class QQOfficialWebhook:
|
|
|
78
79
|
return response
|
|
79
80
|
|
|
80
81
|
async def callback(self):
|
|
81
|
-
|
|
82
|
+
"""内部服务器的回调入口"""
|
|
83
|
+
return await self.handle_callback(quart.request)
|
|
84
|
+
|
|
85
|
+
async def handle_callback(self, request) -> dict:
|
|
86
|
+
"""处理 webhook 回调,可被统一 webhook 入口复用
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
request: Quart 请求对象
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
响应数据
|
|
93
|
+
"""
|
|
94
|
+
msg: dict = await request.json
|
|
82
95
|
logger.debug(f"收到 qq_official_webhook 回调: {msg}")
|
|
83
96
|
|
|
84
97
|
event = msg.get("t")
|
|
@@ -87,7 +100,7 @@ class QQOfficialWebhook:
|
|
|
87
100
|
|
|
88
101
|
if opcode == 13:
|
|
89
102
|
# validation
|
|
90
|
-
signed = await self.webhook_validation(data)
|
|
103
|
+
signed = await self.webhook_validation(cast(dict, data))
|
|
91
104
|
print(signed)
|
|
92
105
|
return signed
|
|
93
106
|
|
|
@@ -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(
|
|
@@ -4,9 +4,11 @@ import hmac
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from typing import cast
|
|
7
8
|
|
|
8
9
|
from quart import Quart, Response, request
|
|
9
10
|
from slack_sdk.socket_mode.aiohttp import SocketModeClient
|
|
11
|
+
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
|
|
10
12
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
|
11
13
|
from slack_sdk.socket_mode.response import SocketModeResponse
|
|
12
14
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
@@ -47,51 +49,62 @@ class SlackWebhookClient:
|
|
|
47
49
|
|
|
48
50
|
@self.app.route(self.path, methods=["POST"])
|
|
49
51
|
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)
|
|
52
|
+
"""内部服务器的 POST 回调入口"""
|
|
53
|
+
return await self.handle_callback(request)
|
|
89
54
|
|
|
90
55
|
@self.app.route("/health", methods=["GET"])
|
|
91
56
|
async def health_check():
|
|
92
57
|
"""健康检查端点"""
|
|
93
58
|
return {"status": "ok", "service": "slack-webhook"}
|
|
94
59
|
|
|
60
|
+
async def handle_callback(self, req):
|
|
61
|
+
"""处理 Slack 回调请求,可被统一 webhook 入口复用
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
req: Quart 请求对象
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Response 对象或字典
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
# 获取请求体和头部
|
|
71
|
+
body = cast(bytes, await req.get_data())
|
|
72
|
+
event_data = json.loads(body.decode("utf-8"))
|
|
73
|
+
|
|
74
|
+
# Verify Slack request signature
|
|
75
|
+
timestamp = req.headers.get("X-Slack-Request-Timestamp")
|
|
76
|
+
signature = req.headers.get("X-Slack-Signature")
|
|
77
|
+
if not timestamp or not signature:
|
|
78
|
+
return Response("Missing headers", status=400)
|
|
79
|
+
# Calculate the HMAC signature
|
|
80
|
+
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
81
|
+
my_signature = (
|
|
82
|
+
"v0="
|
|
83
|
+
+ hmac.new(
|
|
84
|
+
self.signing_secret.encode("utf-8"),
|
|
85
|
+
sig_basestring.encode("utf-8"),
|
|
86
|
+
hashlib.sha256,
|
|
87
|
+
).hexdigest()
|
|
88
|
+
)
|
|
89
|
+
# Verify the signature
|
|
90
|
+
if not hmac.compare_digest(my_signature, signature):
|
|
91
|
+
logger.warning("Slack request signature verification failed")
|
|
92
|
+
return Response("Invalid signature", status=400)
|
|
93
|
+
logger.info(f"Received Slack event: {event_data}")
|
|
94
|
+
|
|
95
|
+
# 处理 URL 验证事件
|
|
96
|
+
if event_data.get("type") == "url_verification":
|
|
97
|
+
return {"challenge": event_data.get("challenge")}
|
|
98
|
+
# 处理事件
|
|
99
|
+
if self.event_handler and event_data.get("type") == "event_callback":
|
|
100
|
+
await self.event_handler(event_data)
|
|
101
|
+
|
|
102
|
+
return Response("", status=200)
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"处理 Slack 事件时出错: {e}")
|
|
106
|
+
return Response("Internal Server Error", status=500)
|
|
107
|
+
|
|
95
108
|
async def start(self):
|
|
96
109
|
"""启动 Webhook 服务器"""
|
|
97
110
|
logger.info(
|
|
@@ -128,9 +141,14 @@ class SlackSocketClient:
|
|
|
128
141
|
self.event_handler = event_handler
|
|
129
142
|
self.socket_client = None
|
|
130
143
|
|
|
131
|
-
async def _handle_events(
|
|
144
|
+
async def _handle_events(
|
|
145
|
+
self, _: AsyncBaseSocketModeClient, req: SocketModeRequest
|
|
146
|
+
):
|
|
132
147
|
"""处理 Socket Mode 事件"""
|
|
133
148
|
try:
|
|
149
|
+
if self.socket_client is None:
|
|
150
|
+
raise RuntimeError("Socket client is not initialized")
|
|
151
|
+
|
|
134
152
|
# 确认收到事件
|
|
135
153
|
response = SocketModeResponse(envelope_id=req.envelope_id)
|
|
136
154
|
await self.socket_client.send_socket_mode_response(response)
|
|
@@ -3,8 +3,7 @@ import base64
|
|
|
3
3
|
import re
|
|
4
4
|
import time
|
|
5
5
|
import uuid
|
|
6
|
-
from
|
|
7
|
-
from typing import Any
|
|
6
|
+
from typing import Any, cast
|
|
8
7
|
|
|
9
8
|
import aiohttp
|
|
10
9
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
|
@@ -21,6 +20,7 @@ from astrbot.api.platform import (
|
|
|
21
20
|
PlatformMetadata,
|
|
22
21
|
)
|
|
23
22
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
23
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
24
24
|
|
|
25
25
|
from ...register import register_platform_adapter
|
|
26
26
|
from .client import SlackSocketClient, SlackWebhookClient
|
|
@@ -39,9 +39,7 @@ class SlackAdapter(Platform):
|
|
|
39
39
|
platform_settings: dict,
|
|
40
40
|
event_queue: asyncio.Queue,
|
|
41
41
|
) -> None:
|
|
42
|
-
super().__init__(event_queue)
|
|
43
|
-
|
|
44
|
-
self.config = platform_config
|
|
42
|
+
super().__init__(platform_config, event_queue)
|
|
45
43
|
self.settings = platform_settings
|
|
46
44
|
self.unique_session = platform_settings.get("unique_session", False)
|
|
47
45
|
|
|
@@ -49,6 +47,7 @@ class SlackAdapter(Platform):
|
|
|
49
47
|
self.app_token = platform_config.get("app_token")
|
|
50
48
|
self.signing_secret = platform_config.get("signing_secret")
|
|
51
49
|
self.connection_mode = platform_config.get("slack_connection_mode", "socket")
|
|
50
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
52
51
|
self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0")
|
|
53
52
|
self.webhook_port = platform_config.get("slack_webhook_port", 3000)
|
|
54
53
|
self.webhook_path = platform_config.get(
|
|
@@ -68,7 +67,7 @@ class SlackAdapter(Platform):
|
|
|
68
67
|
self.metadata = PlatformMetadata(
|
|
69
68
|
name="slack",
|
|
70
69
|
description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。",
|
|
71
|
-
id=self.config.get("id"),
|
|
70
|
+
id=cast(str, self.config.get("id")),
|
|
72
71
|
support_streaming_message=False,
|
|
73
72
|
)
|
|
74
73
|
|
|
@@ -118,13 +117,13 @@ class SlackAdapter(Platform):
|
|
|
118
117
|
logger.debug(f"[slack] RawMessage {event}")
|
|
119
118
|
|
|
120
119
|
abm = AstrBotMessage()
|
|
121
|
-
abm.self_id = self.bot_self_id
|
|
120
|
+
abm.self_id = cast(str, self.bot_self_id)
|
|
122
121
|
|
|
123
122
|
# 获取用户信息
|
|
124
123
|
user_id = event.get("user", "")
|
|
125
124
|
try:
|
|
126
125
|
user_info = await self.web_client.users_info(user=user_id)
|
|
127
|
-
user_data = user_info["user"]
|
|
126
|
+
user_data = cast(dict, user_info["user"])
|
|
128
127
|
user_name = user_data.get("real_name") or user_data.get("name", user_id)
|
|
129
128
|
except Exception:
|
|
130
129
|
user_name = user_id
|
|
@@ -135,7 +134,7 @@ class SlackAdapter(Platform):
|
|
|
135
134
|
channel_id = event.get("channel", "")
|
|
136
135
|
try:
|
|
137
136
|
channel_info = await self.web_client.conversations_info(channel=channel_id)
|
|
138
|
-
is_im = channel_info["channel"]["is_im"]
|
|
137
|
+
is_im = cast(dict, channel_info["channel"])["is_im"]
|
|
139
138
|
|
|
140
139
|
if is_im:
|
|
141
140
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
@@ -178,7 +177,7 @@ class SlackAdapter(Platform):
|
|
|
178
177
|
for mention in mentions:
|
|
179
178
|
try:
|
|
180
179
|
mentioned_user = await self.web_client.users_info(user=mention)
|
|
181
|
-
user_data = mentioned_user["user"]
|
|
180
|
+
user_data = cast(dict, mentioned_user["user"])
|
|
182
181
|
user_name = user_data.get("real_name") or user_data.get(
|
|
183
182
|
"name",
|
|
184
183
|
mention,
|
|
@@ -329,7 +328,7 @@ class SlackAdapter(Platform):
|
|
|
329
328
|
)
|
|
330
329
|
raise Exception(f"下载文件失败: {resp.status}")
|
|
331
330
|
|
|
332
|
-
async def run(self) ->
|
|
331
|
+
async def run(self) -> None:
|
|
333
332
|
self.bot_self_id = await self.get_bot_user_id()
|
|
334
333
|
logger.info(f"Slack auth test OK. Bot ID: {self.bot_self_id}")
|
|
335
334
|
|
|
@@ -361,10 +360,17 @@ class SlackAdapter(Platform):
|
|
|
361
360
|
self._handle_webhook_event,
|
|
362
361
|
)
|
|
363
362
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
363
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
364
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
365
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
366
|
+
log_webhook_info(f"{self.meta().id}(Slack)", webhook_uuid)
|
|
367
|
+
# 保持运行状态,等待 shutdown
|
|
368
|
+
await self.webhook_client.shutdown_event.wait()
|
|
369
|
+
else:
|
|
370
|
+
logger.info(
|
|
371
|
+
f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
|
|
372
|
+
)
|
|
373
|
+
await self.webhook_client.start()
|
|
368
374
|
|
|
369
375
|
else:
|
|
370
376
|
raise ValueError(
|
|
@@ -391,12 +397,19 @@ class SlackAdapter(Platform):
|
|
|
391
397
|
if abm:
|
|
392
398
|
await self.handle_msg(abm)
|
|
393
399
|
|
|
400
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
401
|
+
"""统一 Webhook 回调入口"""
|
|
402
|
+
if self.connection_mode != "webhook" or not self.webhook_client:
|
|
403
|
+
return {"error": "Slack adapter is not in webhook mode"}, 400
|
|
404
|
+
|
|
405
|
+
return await self.webhook_client.handle_callback(request)
|
|
406
|
+
|
|
394
407
|
async def terminate(self):
|
|
395
408
|
if self.socket_client:
|
|
396
409
|
await self.socket_client.stop()
|
|
397
410
|
if self.webhook_client:
|
|
398
411
|
await self.webhook_client.stop()
|
|
399
|
-
logger.info("Slack
|
|
412
|
+
logger.info("Slack 适配器已被关闭")
|
|
400
413
|
|
|
401
414
|
def meta(self) -> PlatformMetadata:
|
|
402
415
|
return self.metadata
|
|
@@ -414,3 +427,10 @@ class SlackAdapter(Platform):
|
|
|
414
427
|
|
|
415
428
|
def get_client(self):
|
|
416
429
|
return self.web_client
|
|
430
|
+
|
|
431
|
+
def unified_webhook(self) -> bool:
|
|
432
|
+
return bool(
|
|
433
|
+
self.config.get("unified_webhook_mode", False)
|
|
434
|
+
and self.config.get("slack_connection_mode", "") == "webhook"
|
|
435
|
+
and self.config.get("webhook_uuid")
|
|
436
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import re
|
|
3
|
-
from collections.abc import AsyncGenerator
|
|
3
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
4
|
+
from typing import cast
|
|
4
5
|
|
|
5
6
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
6
7
|
|
|
@@ -31,14 +32,14 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
31
32
|
async def _from_segment_to_slack_block(
|
|
32
33
|
segment: BaseMessageComponent,
|
|
33
34
|
web_client: AsyncWebClient,
|
|
34
|
-
) -> dict:
|
|
35
|
+
) -> dict | None:
|
|
35
36
|
"""将消息段转换为 Slack 块格式"""
|
|
36
37
|
if isinstance(segment, Plain):
|
|
37
38
|
return {"type": "section", "text": {"type": "mrkdwn", "text": segment.text}}
|
|
38
39
|
if isinstance(segment, Image):
|
|
39
40
|
# upload file
|
|
40
41
|
url = segment.url or segment.file
|
|
41
|
-
if url.startswith("http"):
|
|
42
|
+
if url and url.startswith("http"):
|
|
42
43
|
return {
|
|
43
44
|
"type": "image",
|
|
44
45
|
"image_url": url,
|
|
@@ -55,7 +56,7 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
55
56
|
"type": "section",
|
|
56
57
|
"text": {"type": "mrkdwn", "text": "图片上传失败"},
|
|
57
58
|
}
|
|
58
|
-
image_url = response["files"][0]["url_private"]
|
|
59
|
+
image_url = cast(list, response["files"])[0]["url_private"]
|
|
59
60
|
logger.debug(f"Slack file upload response: {response}")
|
|
60
61
|
return {
|
|
61
62
|
"type": "image",
|
|
@@ -77,7 +78,7 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
77
78
|
"type": "section",
|
|
78
79
|
"text": {"type": "mrkdwn", "text": "文件上传失败"},
|
|
79
80
|
}
|
|
80
|
-
file_url = response["files"][0]["permalink"]
|
|
81
|
+
file_url = cast(list, response["files"])[0]["permalink"]
|
|
81
82
|
return {
|
|
82
83
|
"type": "section",
|
|
83
84
|
"text": {
|
|
@@ -85,7 +86,6 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
85
86
|
"text": f"文件: <{file_url}|{segment.name or '文件'}>",
|
|
86
87
|
},
|
|
87
88
|
}
|
|
88
|
-
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
|
|
89
89
|
|
|
90
90
|
@staticmethod
|
|
91
91
|
async def _parse_slack_blocks(
|
|
@@ -115,7 +115,8 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
115
115
|
segment,
|
|
116
116
|
web_client,
|
|
117
117
|
)
|
|
118
|
-
|
|
118
|
+
if block:
|
|
119
|
+
blocks.append(block)
|
|
119
120
|
|
|
120
121
|
# 如果最后还有文本内容
|
|
121
122
|
if text_content.strip():
|
|
@@ -225,10 +226,10 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
225
226
|
)
|
|
226
227
|
|
|
227
228
|
members = []
|
|
228
|
-
for member_id in members_response["members"]:
|
|
229
|
+
for member_id in cast(Iterable, members_response["members"]):
|
|
229
230
|
try:
|
|
230
231
|
user_info = await self.web_client.users_info(user=member_id)
|
|
231
|
-
user_data = user_info["user"]
|
|
232
|
+
user_data = cast(dict, user_info["user"])
|
|
232
233
|
members.append(
|
|
233
234
|
MessageMember(
|
|
234
235
|
user_id=member_id,
|
|
@@ -240,7 +241,7 @@ class SlackMessageEvent(AstrMessageEvent):
|
|
|
240
241
|
# 如果获取用户信息失败,使用默认信息
|
|
241
242
|
members.append(MessageMember(user_id=member_id, nickname=member_id))
|
|
242
243
|
|
|
243
|
-
channel_data = channel_info["channel"]
|
|
244
|
+
channel_data = cast(dict, channel_info["channel"])
|
|
244
245
|
return Group(
|
|
245
246
|
group_id=channel_id,
|
|
246
247
|
group_name=channel_data.get("name", ""),
|
|
@@ -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
|
|
|
@@ -425,6 +424,6 @@ class TelegramPlatformAdapter(Platform):
|
|
|
425
424
|
if self.application.updater is not None:
|
|
426
425
|
await self.application.updater.stop()
|
|
427
426
|
|
|
428
|
-
logger.info("Telegram
|
|
427
|
+
logger.info("Telegram 适配器已被关闭")
|
|
429
428
|
except Exception as e:
|
|
430
429
|
logger.error(f"Telegram 适配器关闭时出错: {e}")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
+
from typing import Any, cast
|
|
4
5
|
|
|
5
6
|
import telegramify_markdown
|
|
6
7
|
from telegram import ReactionTypeCustomEmoji, ReactionTypeEmoji
|
|
@@ -17,8 +18,6 @@ from astrbot.api.message_components import (
|
|
|
17
18
|
Reply,
|
|
18
19
|
)
|
|
19
20
|
from astrbot.api.platform import AstrBotMessage, MessageType, PlatformMetadata
|
|
20
|
-
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
21
|
-
from astrbot.core.utils.io import download_file
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
class TelegramPlatformEvent(AstrMessageEvent):
|
|
@@ -97,7 +96,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
97
96
|
"chat_id": user_name,
|
|
98
97
|
}
|
|
99
98
|
if has_reply:
|
|
100
|
-
payload["reply_to_message_id"] = reply_message_id
|
|
99
|
+
payload["reply_to_message_id"] = str(reply_message_id)
|
|
101
100
|
if message_thread_id:
|
|
102
101
|
payload["message_thread_id"] = message_thread_id
|
|
103
102
|
|
|
@@ -110,33 +109,30 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
110
109
|
try:
|
|
111
110
|
md_text = telegramify_markdown.markdownify(
|
|
112
111
|
chunk,
|
|
113
|
-
max_line_length=None,
|
|
114
112
|
normalize_whitespace=False,
|
|
115
113
|
)
|
|
116
114
|
await client.send_message(
|
|
117
115
|
text=md_text,
|
|
118
116
|
parse_mode="MarkdownV2",
|
|
119
|
-
**payload,
|
|
117
|
+
**cast(Any, payload),
|
|
120
118
|
)
|
|
121
119
|
except Exception as e:
|
|
122
120
|
logger.warning(
|
|
123
121
|
f"MarkdownV2 send failed: {e}. Using plain text instead.",
|
|
124
122
|
)
|
|
125
|
-
await client.send_message(text=chunk, **payload)
|
|
123
|
+
await client.send_message(text=chunk, **cast(Any, payload))
|
|
126
124
|
elif isinstance(i, Image):
|
|
127
125
|
image_path = await i.convert_to_file_path()
|
|
128
|
-
await client.send_photo(photo=image_path, **payload)
|
|
126
|
+
await client.send_photo(photo=image_path, **cast(Any, payload))
|
|
129
127
|
elif isinstance(i, File):
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await client.send_document(document=i.file, filename=i.name, **payload)
|
|
128
|
+
path = await i.get_file()
|
|
129
|
+
name = i.name or os.path.basename(path)
|
|
130
|
+
await client.send_document(
|
|
131
|
+
document=path, filename=name, **cast(Any, payload)
|
|
132
|
+
)
|
|
137
133
|
elif isinstance(i, Record):
|
|
138
134
|
path = await i.convert_to_file_path()
|
|
139
|
-
await client.send_voice(voice=path, **payload)
|
|
135
|
+
await client.send_voice(voice=path, **cast(Any, payload))
|
|
140
136
|
|
|
141
137
|
async def send(self, message: MessageChain):
|
|
142
138
|
if self.get_message_type() == MessageType.GROUP_MESSAGE:
|
|
@@ -214,24 +210,23 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
214
210
|
delta += i.text
|
|
215
211
|
elif isinstance(i, Image):
|
|
216
212
|
image_path = await i.convert_to_file_path()
|
|
217
|
-
await self.client.send_photo(
|
|
213
|
+
await self.client.send_photo(
|
|
214
|
+
photo=image_path, **cast(Any, payload)
|
|
215
|
+
)
|
|
218
216
|
continue
|
|
219
217
|
elif isinstance(i, File):
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
path = os.path.join(temp_dir, i.name)
|
|
223
|
-
await download_file(i.file, path)
|
|
224
|
-
i.file = path
|
|
218
|
+
path = await i.get_file()
|
|
219
|
+
name = i.name or os.path.basename(path)
|
|
225
220
|
|
|
226
221
|
await self.client.send_document(
|
|
227
|
-
document=
|
|
228
|
-
filename=
|
|
229
|
-
**payload,
|
|
222
|
+
document=path,
|
|
223
|
+
filename=name,
|
|
224
|
+
**cast(Any, payload),
|
|
230
225
|
)
|
|
231
226
|
continue
|
|
232
227
|
elif isinstance(i, Record):
|
|
233
228
|
path = await i.convert_to_file_path()
|
|
234
|
-
await self.client.send_voice(voice=path, **payload)
|
|
229
|
+
await self.client.send_voice(voice=path, **cast(Any, payload))
|
|
235
230
|
continue
|
|
236
231
|
else:
|
|
237
232
|
logger.warning(f"不支持的消息类型: {type(i)}")
|
|
@@ -260,7 +255,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
260
255
|
else:
|
|
261
256
|
# delta 长度一般不会大于 4096,因此这里直接发送
|
|
262
257
|
try:
|
|
263
|
-
msg = await self.client.send_message(
|
|
258
|
+
msg = await self.client.send_message(
|
|
259
|
+
text=delta, **cast(Any, payload)
|
|
260
|
+
)
|
|
264
261
|
current_content = delta
|
|
265
262
|
except Exception as e:
|
|
266
263
|
logger.warning(f"发送消息失败(streaming): {e!s}")
|
|
@@ -274,7 +271,6 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
274
271
|
try:
|
|
275
272
|
markdown_text = telegramify_markdown.markdownify(
|
|
276
273
|
delta,
|
|
277
|
-
max_line_length=None,
|
|
278
274
|
normalize_whitespace=False,
|
|
279
275
|
)
|
|
280
276
|
await self.client.edit_message_text(
|