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
@@ -5,7 +5,15 @@ import uuid
5
5
  from io import BytesIO
6
6
 
7
7
  import lark_oapi as lark
8
- from lark_oapi.api.im.v1 import *
8
+ from lark_oapi.api.im.v1 import (
9
+ CreateImageRequest,
10
+ CreateImageRequestBody,
11
+ CreateMessageReactionRequest,
12
+ CreateMessageReactionRequestBody,
13
+ Emoji,
14
+ ReplyMessageRequest,
15
+ ReplyMessageRequestBody,
16
+ )
9
17
 
10
18
  from astrbot import logger
11
19
  from astrbot.api.event import AstrMessageEvent, MessageChain
@@ -44,7 +52,7 @@ class LarkMessageEvent(AstrMessageEvent):
44
52
  file_path = comp.file.replace("file:///", "")
45
53
  elif comp.file and comp.file.startswith("http"):
46
54
  image_file_path = await download_image_by_url(comp.file)
47
- file_path = image_file_path
55
+ file_path = image_file_path if image_file_path else ""
48
56
  elif comp.file and comp.file.startswith("base64://"):
49
57
  base64_str = comp.file.removeprefix("base64://")
50
58
  image_data = base64.b64decode(base64_str)
@@ -54,10 +62,17 @@ class LarkMessageEvent(AstrMessageEvent):
54
62
  with open(file_path, "wb") as f:
55
63
  f.write(BytesIO(image_data).getvalue())
56
64
  else:
57
- file_path = comp.file
65
+ file_path = comp.file if comp.file else ""
58
66
 
59
67
  if image_file is None:
60
- image_file = open(file_path, "rb")
68
+ if not file_path:
69
+ logger.error("[Lark] 图片路径为空,无法上传")
70
+ continue
71
+ try:
72
+ image_file = open(file_path, "rb")
73
+ except Exception as e:
74
+ logger.error(f"[Lark] 无法打开图片文件: {e}")
75
+ continue
61
76
 
62
77
  request = (
63
78
  CreateImageRequest.builder()
@@ -69,9 +84,20 @@ class LarkMessageEvent(AstrMessageEvent):
69
84
  )
70
85
  .build()
71
86
  )
87
+
88
+ if lark_client.im is None:
89
+ logger.error("[Lark] API Client im 模块未初始化,无法上传图片")
90
+ continue
91
+
72
92
  response = await lark_client.im.v1.image.acreate(request)
73
93
  if not response.success():
74
94
  logger.error(f"无法上传飞书图片({response.code}): {response.msg}")
95
+ continue
96
+
97
+ if response.data is None:
98
+ logger.error("[Lark] 上传图片成功但未返回数据(data is None)")
99
+ continue
100
+
75
101
  image_key = response.data.image_key
76
102
  logger.debug(image_key)
77
103
  ret.append(_stage)
@@ -107,6 +133,10 @@ class LarkMessageEvent(AstrMessageEvent):
107
133
  .build()
108
134
  )
109
135
 
136
+ if self.bot.im is None:
137
+ logger.error("[Lark] API Client im 模块未初始化,无法回复消息")
138
+ return
139
+
110
140
  response = await self.bot.im.v1.message.areply(request)
111
141
 
112
142
  if not response.success():
@@ -115,6 +145,10 @@ class LarkMessageEvent(AstrMessageEvent):
115
145
  await super().send(message)
116
146
 
117
147
  async def react(self, emoji: str):
148
+ if self.bot.im is None:
149
+ logger.error("[Lark] API Client im 模块未初始化,无法发送表情")
150
+ return
151
+
118
152
  request = (
119
153
  CreateMessageReactionRequest.builder()
120
154
  .message_id(self.message_obj.message_id)
@@ -125,6 +159,7 @@ class LarkMessageEvent(AstrMessageEvent):
125
159
  )
126
160
  .build()
127
161
  )
162
+
128
163
  response = await self.bot.im.v1.message_reaction.acreate(request)
129
164
  if not response.success():
130
165
  logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}")
@@ -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
@@ -55,8 +54,7 @@ class MisskeyPlatformAdapter(Platform):
55
54
  platform_settings: dict,
56
55
  event_queue: asyncio.Queue,
57
56
  ) -> None:
58
- super().__init__(event_queue)
59
- self.config = platform_config or {}
57
+ super().__init__(platform_config or {}, event_queue)
60
58
  self.settings = platform_settings or {}
61
59
  self.instance_url = self.config.get("misskey_instance_url", "")
62
60
  self.access_token = self.config.get("misskey_token", "")
@@ -204,7 +202,7 @@ class MisskeyPlatformAdapter(Platform):
204
202
  if not isinstance(message.raw_message, dict):
205
203
  message.raw_message = {}
206
204
  message.raw_message["poll"] = poll
207
- message.poll = poll
205
+ message.__setattr__("poll", poll)
208
206
  except Exception:
209
207
  pass
210
208
 
@@ -373,7 +371,7 @@ class MisskeyPlatformAdapter(Platform):
373
371
  self,
374
372
  session: MessageSession,
375
373
  message_chain: MessageChain,
376
- ) -> Awaitable[Any]:
374
+ ) -> None:
377
375
  if not self.api:
378
376
  logger.error("[Misskey] API 客户端未初始化")
379
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()
@@ -69,6 +73,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
69
73
  # 结束流式对话,并且传输 buffer 中剩余的消息
70
74
  stream_payload["state"] = 10
71
75
  ret = await self._post_send(stream=stream_payload)
76
+ else:
77
+ ret = await self._post_send()
72
78
 
73
79
  except Exception as e:
74
80
  logger.error(f"发送流式消息时出错: {e}", exc_info=True)
@@ -81,7 +87,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
81
87
  return None
82
88
 
83
89
  source = self.message_obj.raw_message
84
- assert isinstance(
90
+
91
+ if not isinstance(
85
92
  source,
86
93
  (
87
94
  botpy.message.Message,
@@ -89,7 +96,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
89
96
  botpy.message.DirectMessage,
90
97
  botpy.message.C2CMessage,
91
98
  ),
92
- )
99
+ ):
100
+ logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}")
101
+ return None
93
102
 
94
103
  (
95
104
  plain_text,
@@ -106,7 +115,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
106
115
  ):
107
116
  return None
108
117
 
109
- payload = {
118
+ payload: dict = {
110
119
  "content": plain_text,
111
120
  "msg_id": self.message_obj.message_id,
112
121
  }
@@ -116,8 +125,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
116
125
 
117
126
  ret = None
118
127
 
119
- match type(source):
120
- 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
+
121
134
  if image_base64:
122
135
  media = await self.upload_group_and_c2c_image(
123
136
  image_base64,
@@ -138,7 +151,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
138
151
  group_openid=source.group_openid,
139
152
  **payload,
140
153
  )
141
- case botpy.message.C2CMessage:
154
+
155
+ case botpy.message.C2CMessage():
142
156
  if image_base64:
143
157
  media = await self.upload_group_and_c2c_image(
144
158
  image_base64,
@@ -167,18 +181,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
167
181
  **payload,
168
182
  )
169
183
  logger.debug(f"Message sent to C2C: {ret}")
170
- case botpy.message.Message:
184
+
185
+ case botpy.message.Message():
171
186
  if image_path:
172
187
  payload["file_image"] = image_path
173
188
  ret = await self.bot.api.post_message(
174
189
  channel_id=source.channel_id,
175
190
  **payload,
176
191
  )
177
- case botpy.message.DirectMessage:
192
+
193
+ case botpy.message.DirectMessage():
178
194
  if image_path:
179
195
  payload["file_image"] = image_path
180
196
  ret = await self.bot.api.post_dms(guild_id=source.guild_id, **payload)
181
197
 
198
+ case _:
199
+ pass
200
+
182
201
  await super().send(self.send_buffer)
183
202
 
184
203
  self.send_buffer = None
@@ -196,18 +215,33 @@ class QQOfficialMessageEvent(AstrMessageEvent):
196
215
  "file_type": file_type,
197
216
  "srv_send_msg": False,
198
217
  }
218
+
219
+ result = None
199
220
  if "openid" in kwargs:
200
221
  payload["openid"] = kwargs["openid"]
201
222
  route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
202
- return await self.bot.api._http.request(route, json=payload)
203
- if "group_openid" in kwargs:
223
+ result = await self.bot.api._http.request(route, json=payload)
224
+ elif "group_openid" in kwargs:
204
225
  payload["group_openid"] = kwargs["group_openid"]
205
226
  route = Route(
206
227
  "POST",
207
228
  "/v2/groups/{group_openid}/files",
208
229
  group_openid=kwargs["group_openid"],
209
230
  )
210
- 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
+ )
211
245
 
212
246
  async def upload_group_and_c2c_record(
213
247
  self,
@@ -250,11 +284,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
250
284
  result = await self.bot.api._http.request(route, json=payload)
251
285
 
252
286
  if result:
287
+ if not isinstance(result, dict):
288
+ logger.error(f"上传文件响应格式错误: {result}")
289
+ return None
290
+
253
291
  return Media(
254
- file_uuid=result.get("file_uuid"),
255
- file_info=result.get("file_info"),
292
+ file_uuid=result["file_uuid"],
293
+ file_info=result["file_info"],
256
294
  ttl=result.get("ttl", 0),
257
- file_id=result.get("id", ""),
258
295
  )
259
296
  except Exception as e:
260
297
  logger.error(f"上传请求错误: {e}")
@@ -271,7 +308,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
271
308
  message_reference: message.Reference | None = None,
272
309
  media: message.Media | None = None,
273
310
  msg_id: str | None = None,
274
- msg_seq: str = 1,
311
+ msg_seq: int | None = 1,
275
312
  event_id: str | None = None,
276
313
  markdown: message.MarkdownPayload | None = None,
277
314
  keyboard: message.Keyboard | None = None,
@@ -280,7 +317,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
280
317
  payload = locals()
281
318
  payload.pop("self", None)
282
319
  route = Route("POST", "/v2/users/{openid}/messages", openid=openid)
283
- 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)
284
328
 
285
329
  @staticmethod
286
330
  async def _parse_to_qqofficial(message: MessageChain):
@@ -300,8 +344,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
300
344
  image_base64 = file_to_base64(image_file_path)
301
345
  elif i.file and i.file.startswith("base64://"):
302
346
  image_base64 = i.file
303
- else:
347
+ elif i.file:
304
348
  image_base64 = file_to_base64(i.file)
349
+ else:
350
+ raise ValueError("Unsupported image file format")
305
351
  image_base64 = image_base64.removeprefix("base64://")
306
352
  elif isinstance(i, Record):
307
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
 
@@ -97,13 +100,11 @@ class QQOfficialPlatformAdapter(Platform):
97
100
  platform_settings: dict,
98
101
  event_queue: asyncio.Queue,
99
102
  ) -> None:
100
- super().__init__(event_queue)
101
-
102
- self.config = platform_config
103
+ super().__init__(platform_config, event_queue)
103
104
 
104
105
  self.appid = platform_config["appid"]
105
106
  self.secret = platform_config["secret"]
106
- self.unique_session = platform_settings["unique_session"]
107
+ self.unique_session: bool = platform_settings["unique_session"]
107
108
  qq_group = platform_config["enable_group_c2c"]
108
109
  guild_dm = platform_config["enable_guild_direct_message"]
109
110
 
@@ -139,12 +140,15 @@ class QQOfficialPlatformAdapter(Platform):
139
140
  return PlatformMetadata(
140
141
  name="qq_official",
141
142
  description="QQ 机器人官方 API 适配器",
142
- id=self.config.get("id"),
143
+ id=cast(str, self.config.get("id")),
143
144
  )
144
145
 
145
146
  @staticmethod
146
147
  def _parse_from_qqofficial(
147
- message: botpy.message.Message | botpy.message.GroupMessage,
148
+ message: botpy.message.Message
149
+ | botpy.message.GroupMessage
150
+ | botpy.message.DirectMessage
151
+ | botpy.message.C2CMessage,
148
152
  message_type: MessageType,
149
153
  ):
150
154
  abm = AstrBotMessage()
@@ -152,7 +156,7 @@ class QQOfficialPlatformAdapter(Platform):
152
156
  abm.timestamp = int(time.time())
153
157
  abm.raw_message = message
154
158
  abm.message_id = message.id
155
- abm.tag = "qq_official"
159
+ # abm.tag = "qq_official"
156
160
  msg: list[BaseMessageComponent] = []
157
161
 
158
162
  if isinstance(message, botpy.message.GroupMessage) or isinstance(
@@ -182,9 +186,9 @@ class QQOfficialPlatformAdapter(Platform):
182
186
  message,
183
187
  botpy.message.DirectMessage,
184
188
  ):
185
- try:
189
+ if isinstance(message, botpy.message.Message):
186
190
  abm.self_id = str(message.mentions[0].id)
187
- except BaseException as _:
191
+ else:
188
192
  abm.self_id = ""
189
193
 
190
194
  plain_content = message.content.replace(