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.
Files changed (111) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
  3. astrbot/core/agent/tool.py +7 -2
  4. astrbot/core/astr_agent_run_util.py +15 -1
  5. astrbot/core/astr_agent_tool_exec.py +5 -1
  6. astrbot/core/config/astrbot_config.py +4 -0
  7. astrbot/core/config/default.py +116 -1
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +32 -4
  10. astrbot/core/db/migration/migra_3_to_4.py +2 -0
  11. astrbot/core/db/migration/sqlite_v3.py +6 -4
  12. astrbot/core/db/po.py +16 -15
  13. astrbot/core/db/sqlite.py +56 -1
  14. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
  15. astrbot/core/event_bus.py +6 -1
  16. astrbot/core/knowledge_base/retrieval/manager.py +5 -1
  17. astrbot/core/log.py +2 -1
  18. astrbot/core/message/components.py +9 -3
  19. astrbot/core/persona_mgr.py +2 -2
  20. astrbot/core/pipeline/content_safety_check/stage.py +1 -1
  21. astrbot/core/pipeline/context_utils.py +2 -1
  22. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  23. astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
  24. astrbot/core/pipeline/process_stage/stage.py +1 -1
  25. astrbot/core/pipeline/respond/stage.py +4 -2
  26. astrbot/core/pipeline/result_decorate/stage.py +68 -21
  27. astrbot/core/pipeline/scheduler.py +5 -1
  28. astrbot/core/pipeline/waking_check/stage.py +10 -0
  29. astrbot/core/platform/astr_message_event.py +5 -3
  30. astrbot/core/platform/astrbot_message.py +2 -2
  31. astrbot/core/platform/manager.py +71 -9
  32. astrbot/core/platform/platform.py +109 -4
  33. astrbot/core/platform/platform_metadata.py +1 -1
  34. astrbot/core/platform/register.py +1 -0
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  36. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +13 -8
  37. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
  38. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  39. astrbot/core/platform/sources/discord/client.py +16 -4
  40. astrbot/core/platform/sources/discord/components.py +2 -2
  41. astrbot/core/platform/sources/discord/discord_platform_adapter.py +53 -26
  42. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  43. astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
  44. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  45. astrbot/core/platform/sources/lark/server.py +206 -0
  46. astrbot/core/platform/sources/misskey/misskey_adapter.py +3 -5
  47. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
  48. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
  50. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
  51. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  52. astrbot/core/platform/sources/slack/client.py +58 -40
  53. astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
  54. astrbot/core/platform/sources/slack/slack_event.py +11 -10
  55. astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
  56. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  57. astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
  58. astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
  59. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
  60. astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
  61. astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
  62. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  63. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  64. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  65. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
  66. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
  67. astrbot/core/platform_message_history_mgr.py +3 -3
  68. astrbot/core/provider/func_tool_manager.py +3 -3
  69. astrbot/core/provider/manager.py +130 -74
  70. astrbot/core/provider/provider.py +12 -1
  71. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  72. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  73. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  74. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  75. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  76. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  77. astrbot/core/provider/sources/gemini_source.py +12 -10
  78. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  79. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  80. astrbot/core/provider/sources/openai_source.py +4 -0
  81. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  82. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  83. astrbot/core/provider/sources/whisper_api_source.py +44 -12
  84. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  85. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  86. astrbot/core/star/context.py +2 -2
  87. astrbot/core/star/register/star_handler.py +22 -5
  88. astrbot/core/star/star_handler.py +85 -4
  89. astrbot/core/updator.py +3 -3
  90. astrbot/core/utils/io.py +1 -1
  91. astrbot/core/utils/session_waiter.py +17 -10
  92. astrbot/core/utils/shared_preferences.py +32 -0
  93. astrbot/core/utils/t2i/__init__.py +2 -2
  94. astrbot/core/utils/t2i/local_strategy.py +25 -31
  95. astrbot/core/utils/tencent_record_helper.py +2 -2
  96. astrbot/core/utils/version_comparator.py +6 -3
  97. astrbot/core/utils/webhook_utils.py +66 -0
  98. astrbot/dashboard/routes/__init__.py +2 -0
  99. astrbot/dashboard/routes/chat.py +311 -76
  100. astrbot/dashboard/routes/config.py +14 -5
  101. astrbot/dashboard/routes/knowledge_base.py +254 -79
  102. astrbot/dashboard/routes/log.py +13 -8
  103. astrbot/dashboard/routes/platform.py +100 -0
  104. astrbot/dashboard/routes/plugin.py +108 -51
  105. astrbot/dashboard/routes/route.py +2 -0
  106. astrbot/dashboard/server.py +9 -4
  107. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
  108. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
  109. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
  110. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
  111. {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 if self.platform.unique_session else message.group_openid
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
- await self.webhook_helper.start_polling()
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.shutdown_event.set()
157
+ if self.webhook_helper:
158
+ self.webhook_helper.shutdown_event.set()
138
159
  await self.client.close()
139
- try:
140
- await self.webhook_helper.server.shutdown()
141
- except Exception as _:
142
- pass
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
- msg: dict = await quart.request.json
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
- """处理 Slack 事件"""
51
- try:
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(self, _: SocketModeClient, req: SocketModeRequest):
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 collections.abc import Awaitable
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) -> Awaitable[Any]:
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
- logger.info(
365
- f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
366
- )
367
- await self.webhook_client.start()
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
- blocks.append(block)
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
- if i.file.startswith("https://"):
131
- temp_dir = os.path.join(get_astrbot_data_path(), "temp")
132
- path = os.path.join(temp_dir, i.name)
133
- await download_file(i.file, path)
134
- i.file = path
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(photo=image_path, **payload)
213
+ await self.client.send_photo(
214
+ photo=image_path, **cast(Any, payload)
215
+ )
218
216
  continue
219
217
  elif isinstance(i, File):
220
- if i.file.startswith("https://"):
221
- temp_dir = os.path.join(get_astrbot_data_path(), "temp")
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=i.file,
228
- filename=i.name,
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(text=delta, **payload)
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(