AstrBot 4.8.0__py3-none-any.whl → 4.9.1__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 (106) 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_tool_exec.py +5 -1
  5. astrbot/core/config/astrbot_config.py +4 -0
  6. astrbot/core/config/default.py +72 -1
  7. astrbot/core/config/i18n_utils.py +1 -0
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +2 -3
  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 +4 -3
  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/star_request.py +1 -2
  23. astrbot/core/pipeline/process_stage/stage.py +1 -1
  24. astrbot/core/pipeline/respond/stage.py +8 -2
  25. astrbot/core/pipeline/result_decorate/stage.py +89 -22
  26. astrbot/core/pipeline/scheduler.py +5 -1
  27. astrbot/core/pipeline/waking_check/stage.py +10 -0
  28. astrbot/core/platform/astr_message_event.py +5 -3
  29. astrbot/core/platform/astrbot_message.py +2 -2
  30. astrbot/core/platform/manager.py +4 -0
  31. astrbot/core/platform/platform.py +11 -3
  32. astrbot/core/platform/platform_metadata.py +1 -1
  33. astrbot/core/platform/register.py +1 -0
  34. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +9 -5
  36. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
  37. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  38. astrbot/core/platform/sources/discord/client.py +16 -4
  39. astrbot/core/platform/sources/discord/components.py +2 -2
  40. astrbot/core/platform/sources/discord/discord_platform_adapter.py +52 -24
  41. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  42. astrbot/core/platform/sources/lark/lark_adapter.py +183 -20
  43. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  44. astrbot/core/platform/sources/lark/server.py +206 -0
  45. astrbot/core/platform/sources/misskey/misskey_adapter.py +2 -3
  46. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
  47. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
  48. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
  50. astrbot/core/platform/sources/slack/client.py +9 -2
  51. astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
  52. astrbot/core/platform/sources/slack/slack_event.py +8 -7
  53. astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
  54. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  55. astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
  56. astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
  57. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
  58. astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
  59. astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
  60. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  61. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
  62. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
  63. astrbot/core/provider/func_tool_manager.py +3 -3
  64. astrbot/core/provider/manager.py +130 -74
  65. astrbot/core/provider/provider.py +12 -1
  66. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  67. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  68. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  69. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  70. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  71. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  72. astrbot/core/provider/sources/gemini_source.py +12 -10
  73. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  74. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  75. astrbot/core/provider/sources/openai_source.py +4 -0
  76. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  77. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  78. astrbot/core/provider/sources/whisper_api_source.py +1 -1
  79. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  80. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  81. astrbot/core/star/context.py +2 -2
  82. astrbot/core/star/register/star_handler.py +22 -5
  83. astrbot/core/star/star_handler.py +85 -4
  84. astrbot/core/updator.py +3 -3
  85. astrbot/core/utils/io.py +1 -1
  86. astrbot/core/utils/session_waiter.py +17 -10
  87. astrbot/core/utils/shared_preferences.py +32 -0
  88. astrbot/core/utils/t2i/__init__.py +2 -2
  89. astrbot/core/utils/t2i/local_strategy.py +25 -31
  90. astrbot/core/utils/tencent_record_helper.py +1 -1
  91. astrbot/core/utils/version_comparator.py +6 -3
  92. astrbot/core/utils/webhook_utils.py +19 -0
  93. astrbot/dashboard/routes/chat.py +14 -9
  94. astrbot/dashboard/routes/config.py +10 -20
  95. astrbot/dashboard/routes/conversation.py +91 -1
  96. astrbot/dashboard/routes/knowledge_base.py +253 -78
  97. astrbot/dashboard/routes/log.py +13 -8
  98. astrbot/dashboard/routes/platform.py +1 -1
  99. astrbot/dashboard/routes/plugin.py +113 -52
  100. astrbot/dashboard/routes/route.py +2 -0
  101. astrbot/dashboard/server.py +6 -3
  102. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/METADATA +9 -1
  103. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/RECORD +106 -105
  104. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/WHEEL +0 -0
  105. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/entry_points.txt +0 -0
  106. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,206 @@
1
+ """飞书(Lark) Webhook 服务器实现
2
+
3
+ 实现飞书事件订阅的 Webhook 模式,支持:
4
+ 1. 请求 URL 验证 (challenge 验证)
5
+ 2. 事件加密/解密 (AES-256-CBC)
6
+ 3. 签名校验 (SHA256)
7
+ 4. 事件接收和处理
8
+ """
9
+
10
+ import asyncio
11
+ import base64
12
+ import hashlib
13
+ import json
14
+ from collections.abc import Awaitable, Callable
15
+
16
+ from Crypto.Cipher import AES
17
+
18
+ from astrbot.api import logger
19
+
20
+
21
+ class AESCipher:
22
+ """AES 加密/解密工具类"""
23
+
24
+ def __init__(self, key: str):
25
+ self.bs = AES.block_size
26
+ self.key = hashlib.sha256(self.str_to_bytes(key)).digest()
27
+
28
+ @staticmethod
29
+ def str_to_bytes(data):
30
+ u_type = type(b"".decode("utf8"))
31
+ if isinstance(data, u_type):
32
+ return data.encode("utf8")
33
+ return data
34
+
35
+ @staticmethod
36
+ def _unpad(s):
37
+ return s[: -ord(s[len(s) - 1 :])]
38
+
39
+ def decrypt(self, enc):
40
+ iv = enc[: AES.block_size]
41
+ cipher = AES.new(self.key, AES.MODE_CBC, iv)
42
+ return self._unpad(cipher.decrypt(enc[AES.block_size :]))
43
+
44
+ def decrypt_string(self, enc):
45
+ enc = base64.b64decode(enc)
46
+ return self.decrypt(enc).decode("utf8")
47
+
48
+
49
+ class LarkWebhookServer:
50
+ """飞书 Webhook 服务器
51
+
52
+ 仅支持统一 Webhook 模式
53
+ """
54
+
55
+ def __init__(self, config: dict, event_queue: asyncio.Queue):
56
+ """初始化 Webhook 服务器
57
+
58
+ Args:
59
+ config: 飞书配置
60
+ event_queue: 事件队列
61
+ """
62
+ self.app_id = config["app_id"]
63
+ self.app_secret = config["app_secret"]
64
+ self.encrypt_key = config.get("lark_encrypt_key", "")
65
+ self.verification_token = config.get("lark_verification_token", "")
66
+
67
+ self.event_queue = event_queue
68
+ self.callback: Callable[[dict], Awaitable[None]] | None = None
69
+
70
+ # 初始化加密工具
71
+ self.cipher = None
72
+ if self.encrypt_key:
73
+ self.cipher = AESCipher(self.encrypt_key)
74
+
75
+ def verify_signature(
76
+ self,
77
+ timestamp: str,
78
+ nonce: str,
79
+ encrypt_key: str,
80
+ body: bytes,
81
+ signature: str,
82
+ ) -> bool:
83
+ """验证签名
84
+
85
+ Args:
86
+ timestamp: 请求时间戳
87
+ nonce: 随机数
88
+ encrypt_key: 加密密钥
89
+ body: 请求体
90
+ signature: 签名
91
+
92
+ Returns:
93
+ 签名是否有效
94
+ """
95
+ # 拼接字符串: timestamp + nonce + encrypt_key + body
96
+ bytes_b1 = (timestamp + nonce + encrypt_key).encode("utf-8")
97
+ bytes_b = bytes_b1 + body
98
+ h = hashlib.sha256(bytes_b)
99
+ calculated_signature = h.hexdigest()
100
+ return calculated_signature == signature
101
+
102
+ def decrypt_event(self, encrypted_data: str) -> dict:
103
+ """解密事件数据
104
+
105
+ Args:
106
+ encrypted_data: 加密的事件数据
107
+
108
+ Returns:
109
+ 解密后的事件字典
110
+ """
111
+ if not self.cipher:
112
+ raise ValueError("未配置 encrypt_key,无法解密事件")
113
+
114
+ decrypted_str = self.cipher.decrypt_string(encrypted_data)
115
+ return json.loads(decrypted_str)
116
+
117
+ async def handle_challenge(self, event_data: dict) -> dict:
118
+ """处理 challenge 验证请求
119
+
120
+ Args:
121
+ event_data: 事件数据
122
+
123
+ Returns:
124
+ 包含 challenge 的响应
125
+ """
126
+ challenge = event_data.get("challenge", "")
127
+ logger.info(f"[Lark Webhook] 收到 challenge 验证请求: {challenge}")
128
+
129
+ return {"challenge": challenge}
130
+
131
+ async def handle_callback(self, request) -> tuple[dict, int] | dict:
132
+ """处理 webhook 回调,可被统一 webhook 入口复用
133
+
134
+ Args:
135
+ request: Quart 请求对象
136
+
137
+ Returns:
138
+ 响应数据
139
+ """
140
+ # 获取原始请求体
141
+ body = await request.get_data()
142
+
143
+ try:
144
+ event_data = await request.json
145
+ except Exception as e:
146
+ logger.error(f"[Lark Webhook] 解析请求体失败: {e}")
147
+ return {"error": "Invalid JSON"}, 400
148
+
149
+ if not event_data:
150
+ logger.error("[Lark Webhook] 请求体为空")
151
+ return {"error": "Empty request body"}, 400
152
+
153
+ # 如果配置了 encrypt_key,进行签名验证
154
+ if self.encrypt_key:
155
+ timestamp = request.headers.get("X-Lark-Request-Timestamp", "")
156
+ nonce = request.headers.get("X-Lark-Request-Nonce", "")
157
+ signature = request.headers.get("X-Lark-Signature", "")
158
+
159
+ if timestamp and nonce and signature:
160
+ if not self.verify_signature(
161
+ timestamp, nonce, self.encrypt_key, body, signature
162
+ ):
163
+ logger.error("[Lark Webhook] 签名验证失败")
164
+ return {"error": "Invalid signature"}, 401
165
+
166
+ # 检查是否是加密事件
167
+ if "encrypt" in event_data:
168
+ try:
169
+ event_data = self.decrypt_event(event_data["encrypt"])
170
+ logger.debug(f"[Lark Webhook] 解密后的事件: {event_data}")
171
+ except Exception as e:
172
+ logger.error(f"[Lark Webhook] 解密事件失败: {e}")
173
+ return {"error": "Decryption failed"}, 400
174
+
175
+ # 验证 token
176
+ if self.verification_token:
177
+ header = event_data.get("header", {})
178
+ if header:
179
+ token = header.get("token", "")
180
+ else:
181
+ token = event_data.get("token", "")
182
+ if token != self.verification_token:
183
+ logger.error("[Lark Webhook] Verification Token 不匹配。")
184
+ return {"error": "Invalid verification token"}, 401
185
+
186
+ # 处理 URL 验证 (challenge)
187
+ if event_data.get("type") == "url_verification":
188
+ return await self.handle_challenge(event_data)
189
+
190
+ # 调用回调函数处理事件
191
+ if self.callback:
192
+ try:
193
+ await self.callback(event_data)
194
+ except Exception as e:
195
+ logger.error(f"[Lark Webhook] 处理事件回调失败: {e}", exc_info=True)
196
+ return {"error": "Event processing failed"}, 500
197
+
198
+ return {}
199
+
200
+ def set_callback(self, callback: Callable[[dict], Awaitable[None]]):
201
+ """设置事件回调函数
202
+
203
+ Args:
204
+ callback: 处理事件的异步函数
205
+ """
206
+ self.callback = callback
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import os
3
3
  import random
4
- from collections.abc import Awaitable
5
4
  from typing import Any
6
5
 
7
6
  import astrbot.api.message_components as Comp
@@ -203,7 +202,7 @@ class MisskeyPlatformAdapter(Platform):
203
202
  if not isinstance(message.raw_message, dict):
204
203
  message.raw_message = {}
205
204
  message.raw_message["poll"] = poll
206
- message.poll = poll
205
+ message.__setattr__("poll", poll)
207
206
  except Exception:
208
207
  pass
209
208
 
@@ -372,7 +371,7 @@ class MisskeyPlatformAdapter(Platform):
372
371
  self,
373
372
  session: MessageSession,
374
373
  message_chain: MessageChain,
375
- ) -> Awaitable[Any]:
374
+ ) -> None:
376
375
  if not self.api:
377
376
  logger.error("[Misskey] API 客户端未初始化")
378
377
  return await super().send_by_session(session, message_chain)
@@ -3,6 +3,7 @@ import base64
3
3
  import os
4
4
  import random
5
5
  import uuid
6
+ from typing import cast
6
7
 
7
8
  import aiofiles
8
9
  import botpy
@@ -60,7 +61,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
60
61
  time_since_last_edit = current_time - last_edit_time
61
62
 
62
63
  if time_since_last_edit >= throttle_interval:
63
- ret = await self._post_send(stream=stream_payload)
64
+ ret = cast(
65
+ message.Message,
66
+ await self._post_send(stream=stream_payload),
67
+ )
64
68
  stream_payload["index"] += 1
65
69
  stream_payload["id"] = ret["id"]
66
70
  last_edit_time = asyncio.get_event_loop().time()
@@ -83,7 +87,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
83
87
  return None
84
88
 
85
89
  source = self.message_obj.raw_message
86
- assert isinstance(
90
+
91
+ if not isinstance(
87
92
  source,
88
93
  (
89
94
  botpy.message.Message,
@@ -91,7 +96,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
91
96
  botpy.message.DirectMessage,
92
97
  botpy.message.C2CMessage,
93
98
  ),
94
- )
99
+ ):
100
+ logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}")
101
+ return None
95
102
 
96
103
  (
97
104
  plain_text,
@@ -108,7 +115,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
108
115
  ):
109
116
  return None
110
117
 
111
- payload = {
118
+ payload: dict = {
112
119
  "content": plain_text,
113
120
  "msg_id": self.message_obj.message_id,
114
121
  }
@@ -118,8 +125,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
118
125
 
119
126
  ret = None
120
127
 
121
- match type(source):
122
- case botpy.message.GroupMessage:
128
+ match source:
129
+ case botpy.message.GroupMessage():
130
+ if not source.group_openid:
131
+ logger.error("[QQOfficial] GroupMessage 缺少 group_openid")
132
+ return None
133
+
123
134
  if image_base64:
124
135
  media = await self.upload_group_and_c2c_image(
125
136
  image_base64,
@@ -140,7 +151,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
140
151
  group_openid=source.group_openid,
141
152
  **payload,
142
153
  )
143
- case botpy.message.C2CMessage:
154
+
155
+ case botpy.message.C2CMessage():
144
156
  if image_base64:
145
157
  media = await self.upload_group_and_c2c_image(
146
158
  image_base64,
@@ -169,18 +181,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
169
181
  **payload,
170
182
  )
171
183
  logger.debug(f"Message sent to C2C: {ret}")
172
- case botpy.message.Message:
184
+
185
+ case botpy.message.Message():
173
186
  if image_path:
174
187
  payload["file_image"] = image_path
175
188
  ret = await self.bot.api.post_message(
176
189
  channel_id=source.channel_id,
177
190
  **payload,
178
191
  )
179
- case botpy.message.DirectMessage:
192
+
193
+ case botpy.message.DirectMessage():
180
194
  if image_path:
181
195
  payload["file_image"] = image_path
182
196
  ret = await self.bot.api.post_dms(guild_id=source.guild_id, **payload)
183
197
 
198
+ case _:
199
+ pass
200
+
184
201
  await super().send(self.send_buffer)
185
202
 
186
203
  self.send_buffer = None
@@ -198,18 +215,33 @@ class QQOfficialMessageEvent(AstrMessageEvent):
198
215
  "file_type": file_type,
199
216
  "srv_send_msg": False,
200
217
  }
218
+
219
+ result = None
201
220
  if "openid" in kwargs:
202
221
  payload["openid"] = kwargs["openid"]
203
222
  route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
204
- return await self.bot.api._http.request(route, json=payload)
205
- if "group_openid" in kwargs:
223
+ result = await self.bot.api._http.request(route, json=payload)
224
+ elif "group_openid" in kwargs:
206
225
  payload["group_openid"] = kwargs["group_openid"]
207
226
  route = Route(
208
227
  "POST",
209
228
  "/v2/groups/{group_openid}/files",
210
229
  group_openid=kwargs["group_openid"],
211
230
  )
212
- return await self.bot.api._http.request(route, json=payload)
231
+ result = await self.bot.api._http.request(route, json=payload)
232
+ else:
233
+ raise ValueError("Invalid upload parameters")
234
+
235
+ if not isinstance(result, dict):
236
+ raise RuntimeError(
237
+ f"Failed to upload image, response is not dict: {result}"
238
+ )
239
+
240
+ return Media(
241
+ file_uuid=result["file_uuid"],
242
+ file_info=result["file_info"],
243
+ ttl=result.get("ttl", 0),
244
+ )
213
245
 
214
246
  async def upload_group_and_c2c_record(
215
247
  self,
@@ -252,11 +284,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
252
284
  result = await self.bot.api._http.request(route, json=payload)
253
285
 
254
286
  if result:
287
+ if not isinstance(result, dict):
288
+ logger.error(f"上传文件响应格式错误: {result}")
289
+ return None
290
+
255
291
  return Media(
256
- file_uuid=result.get("file_uuid"),
257
- file_info=result.get("file_info"),
292
+ file_uuid=result["file_uuid"],
293
+ file_info=result["file_info"],
258
294
  ttl=result.get("ttl", 0),
259
- file_id=result.get("id", ""),
260
295
  )
261
296
  except Exception as e:
262
297
  logger.error(f"上传请求错误: {e}")
@@ -273,7 +308,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
273
308
  message_reference: message.Reference | None = None,
274
309
  media: message.Media | None = None,
275
310
  msg_id: str | None = None,
276
- msg_seq: str = 1,
311
+ msg_seq: int | None = 1,
277
312
  event_id: str | None = None,
278
313
  markdown: message.MarkdownPayload | None = None,
279
314
  keyboard: message.Keyboard | None = None,
@@ -282,7 +317,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
282
317
  payload = locals()
283
318
  payload.pop("self", None)
284
319
  route = Route("POST", "/v2/users/{openid}/messages", openid=openid)
285
- return await self.bot.api._http.request(route, json=payload)
320
+ result = await self.bot.api._http.request(route, json=payload)
321
+
322
+ if not isinstance(result, dict):
323
+ raise RuntimeError(
324
+ f"Failed to post c2c message, response is not dict: {result}"
325
+ )
326
+
327
+ return message.Message(**result)
286
328
 
287
329
  @staticmethod
288
330
  async def _parse_to_qqofficial(message: MessageChain):
@@ -302,8 +344,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
302
344
  image_base64 = file_to_base64(image_file_path)
303
345
  elif i.file and i.file.startswith("base64://"):
304
346
  image_base64 = i.file
305
- else:
347
+ elif i.file:
306
348
  image_base64 = file_to_base64(i.file)
349
+ else:
350
+ raise ValueError("Unsupported image file format")
307
351
  image_base64 = image_base64.removeprefix("base64://")
308
352
  elif isinstance(i, Record):
309
353
  if i.file:
@@ -4,6 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import os
6
6
  import time
7
+ from typing import cast
7
8
 
8
9
  import botpy
9
10
  import botpy.message
@@ -44,7 +45,9 @@ class botClient(Client):
44
45
  MessageType.GROUP_MESSAGE,
45
46
  )
46
47
  abm.session_id = (
47
- abm.sender.user_id if self.platform.unique_session else message.group_openid
48
+ abm.sender.user_id
49
+ if self.platform.unique_session
50
+ else cast(str, message.group_openid)
48
51
  )
49
52
  self._commit(abm)
50
53
 
@@ -101,7 +104,7 @@ class QQOfficialPlatformAdapter(Platform):
101
104
 
102
105
  self.appid = platform_config["appid"]
103
106
  self.secret = platform_config["secret"]
104
- self.unique_session = platform_settings["unique_session"]
107
+ self.unique_session: bool = platform_settings["unique_session"]
105
108
  qq_group = platform_config["enable_group_c2c"]
106
109
  guild_dm = platform_config["enable_guild_direct_message"]
107
110
 
@@ -137,12 +140,15 @@ class QQOfficialPlatformAdapter(Platform):
137
140
  return PlatformMetadata(
138
141
  name="qq_official",
139
142
  description="QQ 机器人官方 API 适配器",
140
- id=self.config.get("id"),
143
+ id=cast(str, self.config.get("id")),
141
144
  )
142
145
 
143
146
  @staticmethod
144
147
  def _parse_from_qqofficial(
145
- message: botpy.message.Message | botpy.message.GroupMessage,
148
+ message: botpy.message.Message
149
+ | botpy.message.GroupMessage
150
+ | botpy.message.DirectMessage
151
+ | botpy.message.C2CMessage,
146
152
  message_type: MessageType,
147
153
  ):
148
154
  abm = AstrBotMessage()
@@ -150,7 +156,7 @@ class QQOfficialPlatformAdapter(Platform):
150
156
  abm.timestamp = int(time.time())
151
157
  abm.raw_message = message
152
158
  abm.message_id = message.id
153
- abm.tag = "qq_official"
159
+ # abm.tag = "qq_official"
154
160
  msg: list[BaseMessageComponent] = []
155
161
 
156
162
  if isinstance(message, botpy.message.GroupMessage) or isinstance(
@@ -180,9 +186,9 @@ class QQOfficialPlatformAdapter(Platform):
180
186
  message,
181
187
  botpy.message.DirectMessage,
182
188
  ):
183
- try:
189
+ if isinstance(message, botpy.message.Message):
184
190
  abm.self_id = str(message.mentions[0].id)
185
- except BaseException as _:
191
+ else:
186
192
  abm.self_id = ""
187
193
 
188
194
  plain_content = message.content.replace(
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import Any
3
+ from typing import Any, cast
4
4
 
5
5
  import botpy
6
6
  import botpy.message
@@ -36,7 +36,9 @@ class botClient(Client):
36
36
  MessageType.GROUP_MESSAGE,
37
37
  )
38
38
  abm.session_id = (
39
- 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)
40
42
  )
41
43
  self._commit(abm)
42
44
 
@@ -120,7 +122,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
120
122
  return PlatformMetadata(
121
123
  name="qq_official_webhook",
122
124
  description="QQ 机器人官方 API 适配器",
123
- id=self.config.get("id"),
125
+ id=cast(str, self.config.get("id")),
124
126
  )
125
127
 
126
128
  async def run(self):
@@ -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
@@ -99,7 +100,7 @@ class QQOfficialWebhook:
99
100
 
100
101
  if opcode == 13:
101
102
  # validation
102
- signed = await self.webhook_validation(data)
103
+ signed = await self.webhook_validation(cast(dict, data))
103
104
  print(signed)
104
105
  return signed
105
106
 
@@ -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
@@ -66,7 +68,7 @@ class SlackWebhookClient:
66
68
  """
67
69
  try:
68
70
  # 获取请求体和头部
69
- body = await req.get_data()
71
+ body = cast(bytes, await req.get_data())
70
72
  event_data = json.loads(body.decode("utf-8"))
71
73
 
72
74
  # Verify Slack request signature
@@ -139,9 +141,14 @@ class SlackSocketClient:
139
141
  self.event_handler = event_handler
140
142
  self.socket_client = None
141
143
 
142
- async def _handle_events(self, _: SocketModeClient, req: SocketModeRequest):
144
+ async def _handle_events(
145
+ self, _: AsyncBaseSocketModeClient, req: SocketModeRequest
146
+ ):
143
147
  """处理 Socket Mode 事件"""
144
148
  try:
149
+ if self.socket_client is None:
150
+ raise RuntimeError("Socket client is not initialized")
151
+
145
152
  # 确认收到事件
146
153
  response = SocketModeResponse(envelope_id=req.envelope_id)
147
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
@@ -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
 
@@ -410,7 +409,7 @@ class SlackAdapter(Platform):
410
409
  await self.socket_client.stop()
411
410
  if self.webhook_client:
412
411
  await self.webhook_client.stop()
413
- logger.info("Slack 适配器已被优雅地关闭")
412
+ logger.info("Slack 适配器已被关闭")
414
413
 
415
414
  def meta(self) -> PlatformMetadata:
416
415
  return self.metadata
@@ -428,3 +427,10 @@ class SlackAdapter(Platform):
428
427
 
429
428
  def get_client(self):
430
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
+ )