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
@@ -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
 
@@ -38,7 +39,7 @@ class SlackMessageEvent(AstrMessageEvent):
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": {
@@ -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", ""),
@@ -424,6 +424,6 @@ class TelegramPlatformAdapter(Platform):
424
424
  if self.application.updater is not None:
425
425
  await self.application.updater.stop()
426
426
 
427
- logger.info("Telegram 适配器已被优雅地关闭")
427
+ logger.info("Telegram 适配器已被关闭")
428
428
  except Exception as e:
429
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(
@@ -2,7 +2,7 @@ import asyncio
2
2
  import os
3
3
  import time
4
4
  import uuid
5
- from collections.abc import Awaitable, Callable
5
+ from collections.abc import Callable, Coroutine
6
6
  from typing import Any
7
7
 
8
8
  from astrbot import logger
@@ -207,7 +207,7 @@ class WebChatAdapter(Platform):
207
207
  abm.raw_message = data
208
208
  return abm
209
209
 
210
- def run(self) -> Awaitable[Any]:
210
+ def run(self) -> Coroutine[Any, Any, None]:
211
211
  async def callback(data: tuple):
212
212
  abm = await self.convert_message(data)
213
213
  await self.handle_msg(abm)
@@ -101,9 +101,9 @@ class WebChatMessageEvent(AstrMessageEvent):
101
101
 
102
102
  return data
103
103
 
104
- async def send(self, message: MessageChain):
104
+ async def send(self, message: MessageChain | None):
105
105
  await WebChatMessageEvent._send(message, session_id=self.session_id)
106
- await super().send(message)
106
+ await super().send(MessageChain([]))
107
107
 
108
108
  async def send_streaming(self, generator, use_fallback: bool = False):
109
109
  final_data = ""
@@ -4,6 +4,7 @@ import json
4
4
  import os
5
5
  import time
6
6
  import traceback
7
+ from typing import cast
7
8
 
8
9
  import aiohttp
9
10
  import anyio
@@ -69,7 +70,7 @@ class WeChatPadProAdapter(Platform):
69
70
  )
70
71
  self.base_url = f"http://{self.host}:{self.port}"
71
72
  self.auth_key = None # 用于保存生成的授权码
72
- self.wxid = None # 用于保存登录成功后的 wxid
73
+ self.wxid: str | None = None # 用于保存登录成功后的 wxid
73
74
  self.credentials_file = os.path.join(
74
75
  get_astrbot_data_path(),
75
76
  "wechatpadpro_credentials.json",
@@ -398,7 +399,7 @@ class WeChatPadProAdapter(Platform):
398
399
  )
399
400
  await asyncio.sleep(5)
400
401
 
401
- async def handle_websocket_message(self, message: str):
402
+ async def handle_websocket_message(self, message: str | bytes):
402
403
  """处理从 WebSocket 接收到的消息。"""
403
404
  logger.debug(f"收到 WebSocket 消息: {message}")
404
405
  try:
@@ -430,10 +431,13 @@ class WeChatPadProAdapter(Platform):
430
431
 
431
432
  async def convert_message(self, raw_message: dict) -> AstrBotMessage | None:
432
433
  """将 WeChatPadPro 原始消息转换为 AstrBotMessage。"""
434
+ if self.wxid is None:
435
+ logger.error("WeChatPadPro 适配器未登录或未获取到 wxid,无法处理消息。")
436
+ return None
433
437
  abm = AstrBotMessage()
434
438
  abm.raw_message = raw_message
435
439
  abm.message_id = str(raw_message.get("msg_id"))
436
- abm.timestamp = raw_message.get("create_time")
440
+ abm.timestamp = cast(int, raw_message.get("create_time"))
437
441
  abm.self_id = self.wxid
438
442
 
439
443
  if int(time.time()) - abm.timestamp > 180:
@@ -446,7 +450,7 @@ class WeChatPadProAdapter(Platform):
446
450
  to_user_name = raw_message.get("to_user_name", {}).get("str", "")
447
451
  content = raw_message.get("content", {}).get("str", "")
448
452
  push_content = raw_message.get("push_content", "")
449
- msg_type = raw_message.get("msg_type")
453
+ msg_type = cast(int, raw_message.get("msg_type"))
450
454
 
451
455
  abm.message_str = ""
452
456
  abm.message = []
@@ -574,7 +578,7 @@ class WeChatPadProAdapter(Platform):
574
578
  from_user_name: str,
575
579
  to_user_name: str,
576
580
  msg_id: int,
577
- ):
581
+ ) -> dict | None:
578
582
  """下载原始图片。"""
579
583
  url = f"{self.base_url}/message/GetMsgBigImg"
580
584
  params = {"key": self.auth_key}
@@ -725,12 +729,15 @@ class WeChatPadProAdapter(Platform):
725
729
  # 图片消息
726
730
  from_user_name = raw_message.get("from_user_name", {}).get("str", "")
727
731
  to_user_name = raw_message.get("to_user_name", {}).get("str", "")
728
- msg_id = raw_message.get("msg_id")
732
+ msg_id = cast(int, raw_message.get("msg_id"))
729
733
  image_resp = await self._download_raw_image(
730
734
  from_user_name,
731
735
  to_user_name,
732
736
  msg_id,
733
737
  )
738
+ if image_resp is None:
739
+ logger.error(f"下载图片失败: msg_id={msg_id}")
740
+ return
734
741
  image_bs64_data = (
735
742
  image_resp.get("Data", {}).get("Data", {}).get("Buffer", None)
736
743
  )
@@ -771,6 +778,9 @@ class WeChatPadProAdapter(Platform):
771
778
  bufid = 0
772
779
  to_user_name = raw_message.get("to_user_name", {}).get("str", "")
773
780
  new_msg_id = raw_message.get("new_msg_id")
781
+ if new_msg_id is None:
782
+ logger.error("语音消息缺少 new_msg_id")
783
+ return
774
784
  data_parser = GeweDataParser(
775
785
  content=content,
776
786
  is_private_chat=(abm.type != MessageType.GROUP_MESSAGE),
@@ -778,6 +788,9 @@ class WeChatPadProAdapter(Platform):
778
788
  )
779
789
 
780
790
  voicemsg = data_parser._format_to_xml().find("voicemsg")
791
+ if voicemsg is None:
792
+ logger.error("无法从 XML 解析 voicemsg 节点")
793
+ return
781
794
  bufid = voicemsg.get("bufid") or "0"
782
795
  length = int(voicemsg.get("length") or 0)
783
796
  voice_resp = await self.download_voice(
@@ -786,6 +799,9 @@ class WeChatPadProAdapter(Platform):
786
799
  bufid=bufid,
787
800
  length=length,
788
801
  )
802
+ if voice_resp is None:
803
+ logger.error(f"下载语音失败: new_msg_id={new_msg_id}")
804
+ return
789
805
  voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None)
790
806
  if voice_bs64_data:
791
807
  voice_bs64_data = base64.b64decode(voice_bs64_data)
@@ -827,7 +843,8 @@ class WeChatPadProAdapter(Platform):
827
843
  try:
828
844
  if self.ws_handle_task:
829
845
  self.ws_handle_task.cancel()
830
- self._shutdown_event.set()
846
+ if self._shutdown_event is not None:
847
+ self._shutdown_event.set()
831
848
  except Exception:
832
849
  pass
833
850
 
@@ -894,8 +911,8 @@ class WeChatPadProAdapter(Platform):
894
911
 
895
912
  async def get_contact_details_list(
896
913
  self,
897
- room_wx_id_list: list[str] = None,
898
- user_names: list[str] = None,
914
+ room_wx_id_list: list[str] | None = None,
915
+ user_names: list[str] | None = None,
899
916
  ) -> dict | None:
900
917
  """获取联系人详情列表。"""
901
918
  if room_wx_id_list is None:
@@ -2,7 +2,8 @@ import asyncio
2
2
  import os
3
3
  import sys
4
4
  import uuid
5
- from typing import Any
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any, cast
6
7
 
7
8
  import quart
8
9
  from requests import Response
@@ -40,7 +41,7 @@ else:
40
41
  class WecomServer:
41
42
  def __init__(self, event_queue: asyncio.Queue, config: dict):
42
43
  self.server = quart.Quart(__name__)
43
- self.port = int(config.get("port"))
44
+ self.port = int(cast(str, config.get("port")))
44
45
  self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
45
46
  self.server.add_url_rule(
46
47
  "/callback/command",
@@ -60,7 +61,7 @@ class WecomServer:
60
61
  config["corpid"].strip(),
61
62
  )
62
63
 
63
- self.callback = None
64
+ self.callback: Callable[[BaseMessage], Awaitable[None]] | None = None
64
65
  self.shutdown_event = asyncio.Event()
65
66
 
66
67
  async def verify(self):
@@ -114,7 +115,7 @@ class WecomServer:
114
115
  logger.error("解密失败,签名异常,请检查配置。")
115
116
  raise
116
117
  else:
117
- msg = parse_message(xml)
118
+ msg = cast(BaseMessage, parse_message(xml))
118
119
  logger.info(f"解析成功: {msg}")
119
120
 
120
121
  if self.callback:
@@ -176,10 +177,10 @@ class WecomPlatformAdapter(Platform):
176
177
  # inject
177
178
  self.wechat_kf_api = WeChatKF(client=self.client)
178
179
  self.wechat_kf_message_api = WeChatKFMessage(self.client)
179
- self.client.kf = self.wechat_kf_api
180
- self.client.kf_message = self.wechat_kf_message_api
180
+ self.client.__setattr__("kf", self.wechat_kf_api)
181
+ self.client.__setattr__("kf_message", self.wechat_kf_message_api)
181
182
 
182
- self.client.API_BASE_URL = self.api_base_url
183
+ self.client.__setattr__("API_BASE_URL", self.api_base_url)
183
184
 
184
185
  async def callback(msg: BaseMessage):
185
186
  if msg.type == "unknown" and msg._data["Event"] == "kf_msg_or_event":
@@ -278,37 +279,33 @@ class WecomPlatformAdapter(Platform):
278
279
 
279
280
  async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
280
281
  abm = AstrBotMessage()
281
- if msg.type == "text":
282
- assert isinstance(msg, TextMessage)
282
+ if isinstance(msg, TextMessage):
283
283
  abm.message_str = msg.content
284
284
  abm.self_id = str(msg.agent)
285
285
  abm.message = [Plain(msg.content)]
286
286
  abm.type = MessageType.FRIEND_MESSAGE
287
287
  abm.sender = MessageMember(
288
- msg.source,
289
- msg.source,
288
+ cast(str, msg.source),
289
+ cast(str, msg.source),
290
290
  )
291
- abm.message_id = msg.id
292
- abm.timestamp = msg.time
291
+ abm.message_id = str(msg.id)
292
+ abm.timestamp = int(cast(int | str, msg.time))
293
293
  abm.session_id = abm.sender.user_id
294
294
  abm.raw_message = msg
295
- elif msg.type == "image":
296
- assert isinstance(msg, ImageMessage)
295
+ elif isinstance(msg, ImageMessage):
297
296
  abm.message_str = "[图片]"
298
297
  abm.self_id = str(msg.agent)
299
298
  abm.message = [Image(file=msg.image, url=msg.image)]
300
299
  abm.type = MessageType.FRIEND_MESSAGE
301
300
  abm.sender = MessageMember(
302
- msg.source,
303
- msg.source,
301
+ cast(str, msg.source),
302
+ cast(str, msg.source),
304
303
  )
305
- abm.message_id = msg.id
306
- abm.timestamp = msg.time
304
+ abm.message_id = str(msg.id)
305
+ abm.timestamp = int(cast(int | str, msg.time))
307
306
  abm.session_id = abm.sender.user_id
308
307
  abm.raw_message = msg
309
- elif msg.type == "voice":
310
- assert isinstance(msg, VoiceMessage)
311
-
308
+ elif isinstance(msg, VoiceMessage):
312
309
  resp: Response = await asyncio.get_event_loop().run_in_executor(
313
310
  None,
314
311
  self.client.media.download,
@@ -335,11 +332,11 @@ class WecomPlatformAdapter(Platform):
335
332
  abm.message = [Record(file=path_wav, url=path_wav)]
336
333
  abm.type = MessageType.FRIEND_MESSAGE
337
334
  abm.sender = MessageMember(
338
- msg.source,
339
- msg.source,
335
+ cast(str, msg.source),
336
+ cast(str, msg.source),
340
337
  )
341
- abm.message_id = msg.id
342
- abm.timestamp = msg.time
338
+ abm.message_id = str(msg.id)
339
+ abm.timestamp = int(cast(int | str, msg.time))
343
340
  abm.session_id = abm.sender.user_id
344
341
  abm.raw_message = msg
345
342
  else:
@@ -351,7 +348,7 @@ class WecomPlatformAdapter(Platform):
351
348
 
352
349
  async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
353
350
  msgtype = msg.get("msgtype")
354
- external_userid = msg.get("external_userid")
351
+ external_userid = cast(str, msg.get("external_userid"))
355
352
  abm = AstrBotMessage()
356
353
  abm.raw_message = msg
357
354
  abm.raw_message["_wechat_kf_flag"] = None # 方便处理
@@ -425,4 +422,4 @@ class WecomPlatformAdapter(Platform):
425
422
  await self.server.server.shutdown()
426
423
  except Exception as _:
427
424
  pass
428
- logger.info("企业微信 适配器已被优雅地关闭")
425
+ logger.info("企业微信 适配器已被关闭")
@@ -93,10 +93,10 @@ class WecomPlatformEvent(AstrMessageEvent):
93
93
  if is_wechat_kf:
94
94
  # 微信客服
95
95
  kf_message_api = getattr(self.client, "kf_message", None)
96
- if not kf_message_api:
96
+ if not isinstance(kf_message_api, WeChatKFMessage):
97
97
  logger.warning("未找到微信客服发送消息方法。")
98
98
  return
99
- assert isinstance(kf_message_api, WeChatKFMessage)
99
+
100
100
  user_id = self.get_sender_id()
101
101
  for comp in message.chain:
102
102
  if isinstance(comp, Plain):
@@ -39,7 +39,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent):
39
39
 
40
40
  @staticmethod
41
41
  async def _send(
42
- message_chain: MessageChain,
42
+ message_chain: MessageChain | None,
43
43
  stream_id: str,
44
44
  queue_mgr: WecomAIQueueMgr,
45
45
  streaming: bool = False,
@@ -90,7 +90,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent):
90
90
 
91
91
  return data
92
92
 
93
- async def send(self, message: MessageChain):
93
+ async def send(self, message: MessageChain | None):
94
94
  """发送消息"""
95
95
  raw = self.message_obj.raw_message
96
96
  assert isinstance(raw, dict), (
@@ -98,7 +98,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent):
98
98
  )
99
99
  stream_id = raw.get("stream_id", self.session_id)
100
100
  await WecomAIBotMessageEvent._send(message, stream_id, self.queue_mgr)
101
- await super().send(message)
101
+ await super().send(MessageChain([]))
102
102
 
103
103
  async def send_streaming(self, generator, use_fallback=False):
104
104
  """流式发送消息,参考webchat的send_streaming设计"""
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  import sys
3
3
  import uuid
4
- from typing import Any
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any, cast
5
6
 
6
7
  import quart
7
8
  from requests import Response
@@ -36,7 +37,7 @@ else:
36
37
  class WeixinOfficialAccountServer:
37
38
  def __init__(self, event_queue: asyncio.Queue, config: dict):
38
39
  self.server = quart.Quart(__name__)
39
- self.port = int(config.get("port"))
40
+ self.port = int(cast(int | str, config.get("port")))
40
41
  self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
41
42
  self.token = config.get("token")
42
43
  self.encoding_aes_key = config.get("encoding_aes_key")
@@ -55,7 +56,7 @@ class WeixinOfficialAccountServer:
55
56
 
56
57
  self.event_queue = event_queue
57
58
 
58
- self.callback = None
59
+ self.callback: Callable[[BaseMessage], Awaitable[None]] | None = None
59
60
  self.shutdown_event = asyncio.Event()
60
61
 
61
62
  async def verify(self):
@@ -114,6 +115,9 @@ class WeixinOfficialAccountServer:
114
115
  raise
115
116
  else:
116
117
  msg = parse_message(xml)
118
+ if not msg:
119
+ logger.error("解析失败。msg为None。")
120
+ raise
117
121
  logger.info(f"解析成功: {msg}")
118
122
 
119
123
  if self.callback:
@@ -176,7 +180,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
176
180
  self.config["secret"].strip(),
177
181
  )
178
182
 
179
- self.client.API_BASE_URL = self.api_base_url
183
+ self.client.__setattr__("API_BASE_URL", self.api_base_url)
180
184
 
181
185
  # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重
182
186
  # msgid -> Future
@@ -188,11 +192,11 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
188
192
  await self.convert_message(msg, None)
189
193
  else:
190
194
  if msg.id in self.wexin_event_workers:
191
- future = self.wexin_event_workers[msg.id]
195
+ future = self.wexin_event_workers[str(cast(str | int, msg.id))]
192
196
  logger.debug(f"duplicate message id checked: {msg.id}")
193
197
  else:
194
198
  future = asyncio.get_event_loop().create_future()
195
- self.wexin_event_workers[msg.id] = future
199
+ self.wexin_event_workers[str(cast(str | int, msg.id))] = future
196
200
  await self.convert_message(msg, future)
197
201
  # I love shield so much!
198
202
  result = await asyncio.wait_for(
@@ -200,7 +204,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
200
204
  60,
201
205
  ) # wait for 60s
202
206
  logger.debug(f"Got future result: {result}")
203
- self.wexin_event_workers.pop(msg.id, None)
207
+ self.wexin_event_workers.pop(str(cast(str | int, msg.id)), None)
204
208
  return result # xml. see weixin_offacc_event.py
205
209
  except asyncio.TimeoutError:
206
210
  pass
@@ -248,33 +252,33 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
248
252
  async def convert_message(
249
253
  self,
250
254
  msg,
251
- future: asyncio.Future = None,
255
+ future: asyncio.Future | None = None,
252
256
  ) -> AstrBotMessage | None:
253
257
  abm = AstrBotMessage()
254
258
  if isinstance(msg, TextMessage):
255
- abm.message_str = msg.content
259
+ abm.message_str = cast(str, msg.content)
256
260
  abm.self_id = str(msg.target)
257
- abm.message = [Plain(msg.content)]
261
+ abm.message = [Plain(cast(str, msg.content))]
258
262
  abm.type = MessageType.FRIEND_MESSAGE
259
263
  abm.sender = MessageMember(
260
- msg.source,
261
- msg.source,
264
+ cast(str, msg.source),
265
+ cast(str, msg.source),
262
266
  )
263
- abm.message_id = msg.id
264
- abm.timestamp = msg.time
267
+ abm.message_id = str(cast(str | int, msg.id))
268
+ abm.timestamp = cast(int, msg.time)
265
269
  abm.session_id = abm.sender.user_id
266
270
  elif msg.type == "image":
267
271
  assert isinstance(msg, ImageMessage)
268
272
  abm.message_str = "[图片]"
269
273
  abm.self_id = str(msg.target)
270
- abm.message = [Image(file=msg.image, url=msg.image)]
274
+ abm.message = [Image(file=cast(str, msg.image), url=cast(str, msg.image))]
271
275
  abm.type = MessageType.FRIEND_MESSAGE
272
276
  abm.sender = MessageMember(
273
- msg.source,
274
- msg.source,
277
+ cast(str, msg.source),
278
+ cast(str, msg.source),
275
279
  )
276
- abm.message_id = msg.id
277
- abm.timestamp = msg.time
280
+ abm.message_id = str(cast(str | int, msg.id))
281
+ abm.timestamp = cast(int, msg.time)
278
282
  abm.session_id = abm.sender.user_id
279
283
  elif msg.type == "voice":
280
284
  assert isinstance(msg, VoiceMessage)
@@ -306,15 +310,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
306
310
  abm.message = [Record(file=path_wav, url=path_wav)]
307
311
  abm.type = MessageType.FRIEND_MESSAGE
308
312
  abm.sender = MessageMember(
309
- msg.source,
310
- msg.source,
313
+ cast(str, msg.source),
314
+ cast(str, msg.source),
311
315
  )
312
- abm.message_id = msg.id
313
- abm.timestamp = msg.time
316
+ abm.message_id = str(cast(str | int, msg.id))
317
+ abm.timestamp = cast(int, msg.time)
314
318
  abm.session_id = abm.sender.user_id
315
319
  else:
316
320
  logger.warning(f"暂未实现的事件: {msg.type}")
317
- future.set_result(None)
321
+ if future:
322
+ future.set_result(None)
318
323
  return
319
324
  # 很不优雅 :(
320
325
  abm.raw_message = {
@@ -344,4 +349,4 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
344
349
  await self.server.server.shutdown()
345
350
  except Exception as _:
346
351
  pass
347
- logger.info("微信公众平台 适配器已被优雅地关闭")
352
+ logger.info("微信公众平台 适配器已被关闭")