AstrBot 4.5.1__py3-none-any.whl → 4.5.2__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 (244) hide show
  1. astrbot/api/__init__.py +10 -11
  2. astrbot/api/event/__init__.py +5 -6
  3. astrbot/api/event/filter/__init__.py +37 -36
  4. astrbot/api/platform/__init__.py +7 -8
  5. astrbot/api/provider/__init__.py +7 -7
  6. astrbot/api/star/__init__.py +3 -4
  7. astrbot/api/util/__init__.py +2 -2
  8. astrbot/cli/__main__.py +5 -5
  9. astrbot/cli/commands/__init__.py +3 -3
  10. astrbot/cli/commands/cmd_conf.py +19 -16
  11. astrbot/cli/commands/cmd_init.py +3 -2
  12. astrbot/cli/commands/cmd_plug.py +8 -10
  13. astrbot/cli/commands/cmd_run.py +5 -6
  14. astrbot/cli/utils/__init__.py +6 -6
  15. astrbot/cli/utils/basic.py +14 -14
  16. astrbot/cli/utils/plugin.py +24 -15
  17. astrbot/cli/utils/version_comparator.py +10 -12
  18. astrbot/core/__init__.py +8 -6
  19. astrbot/core/agent/agent.py +3 -2
  20. astrbot/core/agent/handoff.py +6 -2
  21. astrbot/core/agent/hooks.py +9 -6
  22. astrbot/core/agent/mcp_client.py +50 -15
  23. astrbot/core/agent/message.py +168 -0
  24. astrbot/core/agent/response.py +2 -1
  25. astrbot/core/agent/run_context.py +2 -3
  26. astrbot/core/agent/runners/base.py +10 -13
  27. astrbot/core/agent/runners/tool_loop_agent_runner.py +52 -51
  28. astrbot/core/agent/tool.py +60 -41
  29. astrbot/core/agent/tool_executor.py +9 -3
  30. astrbot/core/astr_agent_context.py +3 -1
  31. astrbot/core/astrbot_config_mgr.py +29 -9
  32. astrbot/core/config/__init__.py +2 -2
  33. astrbot/core/config/astrbot_config.py +28 -26
  34. astrbot/core/config/default.py +4 -6
  35. astrbot/core/conversation_mgr.py +105 -36
  36. astrbot/core/core_lifecycle.py +68 -54
  37. astrbot/core/db/__init__.py +33 -18
  38. astrbot/core/db/migration/helper.py +12 -10
  39. astrbot/core/db/migration/migra_3_to_4.py +53 -34
  40. astrbot/core/db/migration/migra_45_to_46.py +1 -1
  41. astrbot/core/db/migration/shared_preferences_v3.py +2 -1
  42. astrbot/core/db/migration/sqlite_v3.py +26 -23
  43. astrbot/core/db/po.py +27 -18
  44. astrbot/core/db/sqlite.py +74 -45
  45. astrbot/core/db/vec_db/base.py +10 -14
  46. astrbot/core/db/vec_db/faiss_impl/document_storage.py +90 -77
  47. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +9 -3
  48. astrbot/core/db/vec_db/faiss_impl/vec_db.py +36 -31
  49. astrbot/core/event_bus.py +8 -6
  50. astrbot/core/file_token_service.py +6 -5
  51. astrbot/core/initial_loader.py +7 -5
  52. astrbot/core/knowledge_base/chunking/__init__.py +1 -3
  53. astrbot/core/knowledge_base/chunking/base.py +1 -0
  54. astrbot/core/knowledge_base/chunking/fixed_size.py +2 -0
  55. astrbot/core/knowledge_base/chunking/recursive.py +16 -10
  56. astrbot/core/knowledge_base/kb_db_sqlite.py +50 -48
  57. astrbot/core/knowledge_base/kb_helper.py +30 -17
  58. astrbot/core/knowledge_base/kb_mgr.py +6 -7
  59. astrbot/core/knowledge_base/models.py +10 -4
  60. astrbot/core/knowledge_base/parsers/__init__.py +3 -5
  61. astrbot/core/knowledge_base/parsers/base.py +1 -0
  62. astrbot/core/knowledge_base/parsers/markitdown_parser.py +2 -1
  63. astrbot/core/knowledge_base/parsers/pdf_parser.py +2 -1
  64. astrbot/core/knowledge_base/parsers/text_parser.py +1 -0
  65. astrbot/core/knowledge_base/parsers/util.py +1 -1
  66. astrbot/core/knowledge_base/retrieval/__init__.py +6 -8
  67. astrbot/core/knowledge_base/retrieval/manager.py +17 -14
  68. astrbot/core/knowledge_base/retrieval/rank_fusion.py +7 -3
  69. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +11 -5
  70. astrbot/core/log.py +21 -13
  71. astrbot/core/message/components.py +123 -217
  72. astrbot/core/message/message_event_result.py +24 -24
  73. astrbot/core/persona_mgr.py +20 -11
  74. astrbot/core/pipeline/__init__.py +7 -7
  75. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  76. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  77. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +12 -13
  78. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -0
  79. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  80. astrbot/core/pipeline/context.py +4 -1
  81. astrbot/core/pipeline/context_utils.py +77 -7
  82. astrbot/core/pipeline/preprocess_stage/stage.py +12 -9
  83. astrbot/core/pipeline/process_stage/method/llm_request.py +125 -72
  84. astrbot/core/pipeline/process_stage/method/star_request.py +19 -17
  85. astrbot/core/pipeline/process_stage/stage.py +13 -10
  86. astrbot/core/pipeline/process_stage/utils.py +6 -5
  87. astrbot/core/pipeline/rate_limit_check/stage.py +37 -36
  88. astrbot/core/pipeline/respond/stage.py +23 -20
  89. astrbot/core/pipeline/result_decorate/stage.py +31 -23
  90. astrbot/core/pipeline/scheduler.py +12 -8
  91. astrbot/core/pipeline/session_status_check/stage.py +12 -8
  92. astrbot/core/pipeline/stage.py +10 -4
  93. astrbot/core/pipeline/waking_check/stage.py +24 -18
  94. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  95. astrbot/core/platform/__init__.py +6 -6
  96. astrbot/core/platform/astr_message_event.py +76 -110
  97. astrbot/core/platform/astrbot_message.py +11 -13
  98. astrbot/core/platform/manager.py +16 -15
  99. astrbot/core/platform/message_session.py +5 -3
  100. astrbot/core/platform/platform.py +16 -24
  101. astrbot/core/platform/platform_metadata.py +4 -4
  102. astrbot/core/platform/register.py +8 -8
  103. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +23 -15
  104. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +51 -33
  105. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +42 -27
  106. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +7 -3
  107. astrbot/core/platform/sources/discord/client.py +9 -6
  108. astrbot/core/platform/sources/discord/components.py +18 -14
  109. astrbot/core/platform/sources/discord/discord_platform_adapter.py +45 -30
  110. astrbot/core/platform/sources/discord/discord_platform_event.py +38 -30
  111. astrbot/core/platform/sources/lark/lark_adapter.py +23 -17
  112. astrbot/core/platform/sources/lark/lark_event.py +21 -14
  113. astrbot/core/platform/sources/misskey/misskey_adapter.py +107 -67
  114. astrbot/core/platform/sources/misskey/misskey_api.py +153 -129
  115. astrbot/core/platform/sources/misskey/misskey_event.py +20 -15
  116. astrbot/core/platform/sources/misskey/misskey_utils.py +74 -62
  117. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +63 -44
  118. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  119. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  120. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  121. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +12 -7
  122. astrbot/core/platform/sources/satori/satori_adapter.py +56 -38
  123. astrbot/core/platform/sources/satori/satori_event.py +34 -25
  124. astrbot/core/platform/sources/slack/client.py +11 -9
  125. astrbot/core/platform/sources/slack/slack_adapter.py +52 -36
  126. astrbot/core/platform/sources/slack/slack_event.py +34 -24
  127. astrbot/core/platform/sources/telegram/tg_adapter.py +38 -18
  128. astrbot/core/platform/sources/telegram/tg_event.py +32 -18
  129. astrbot/core/platform/sources/webchat/webchat_adapter.py +27 -17
  130. astrbot/core/platform/sources/webchat/webchat_event.py +14 -10
  131. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +115 -120
  132. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +9 -8
  133. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +15 -16
  134. astrbot/core/platform/sources/wecom/wecom_adapter.py +35 -18
  135. astrbot/core/platform/sources/wecom/wecom_event.py +55 -48
  136. astrbot/core/platform/sources/wecom/wecom_kf.py +34 -44
  137. astrbot/core/platform/sources/wecom/wecom_kf_message.py +26 -10
  138. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +18 -10
  139. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +3 -5
  140. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +0 -1
  141. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +61 -37
  142. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +67 -28
  143. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +8 -9
  144. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +18 -9
  145. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +14 -12
  146. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +22 -12
  147. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +40 -26
  148. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +47 -45
  149. astrbot/core/platform_message_history_mgr.py +5 -3
  150. astrbot/core/provider/__init__.py +2 -3
  151. astrbot/core/provider/entites.py +8 -8
  152. astrbot/core/provider/entities.py +61 -75
  153. astrbot/core/provider/func_tool_manager.py +59 -55
  154. astrbot/core/provider/manager.py +32 -22
  155. astrbot/core/provider/provider.py +72 -46
  156. astrbot/core/provider/register.py +7 -7
  157. astrbot/core/provider/sources/anthropic_source.py +48 -30
  158. astrbot/core/provider/sources/azure_tts_source.py +17 -13
  159. astrbot/core/provider/sources/coze_api_client.py +27 -17
  160. astrbot/core/provider/sources/coze_source.py +104 -87
  161. astrbot/core/provider/sources/dashscope_source.py +18 -11
  162. astrbot/core/provider/sources/dashscope_tts.py +36 -23
  163. astrbot/core/provider/sources/dify_source.py +25 -20
  164. astrbot/core/provider/sources/edge_tts_source.py +21 -17
  165. astrbot/core/provider/sources/fishaudio_tts_api_source.py +22 -14
  166. astrbot/core/provider/sources/gemini_embedding_source.py +12 -13
  167. astrbot/core/provider/sources/gemini_source.py +72 -58
  168. astrbot/core/provider/sources/gemini_tts_source.py +8 -6
  169. astrbot/core/provider/sources/gsv_selfhosted_source.py +17 -14
  170. astrbot/core/provider/sources/gsvi_tts_source.py +11 -7
  171. astrbot/core/provider/sources/minimax_tts_api_source.py +50 -40
  172. astrbot/core/provider/sources/openai_embedding_source.py +6 -8
  173. astrbot/core/provider/sources/openai_source.py +77 -69
  174. astrbot/core/provider/sources/openai_tts_api_source.py +14 -6
  175. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  176. astrbot/core/provider/sources/vllm_rerank_source.py +10 -4
  177. astrbot/core/provider/sources/volcengine_tts.py +38 -31
  178. astrbot/core/provider/sources/whisper_api_source.py +14 -12
  179. astrbot/core/provider/sources/whisper_selfhosted_source.py +15 -11
  180. astrbot/core/provider/sources/xinference_rerank_source.py +16 -8
  181. astrbot/core/provider/sources/xinference_stt_provider.py +35 -25
  182. astrbot/core/star/__init__.py +16 -11
  183. astrbot/core/star/config.py +10 -15
  184. astrbot/core/star/context.py +97 -75
  185. astrbot/core/star/filter/__init__.py +4 -3
  186. astrbot/core/star/filter/command.py +30 -28
  187. astrbot/core/star/filter/command_group.py +27 -24
  188. astrbot/core/star/filter/custom_filter.py +6 -5
  189. astrbot/core/star/filter/event_message_type.py +4 -2
  190. astrbot/core/star/filter/permission.py +4 -2
  191. astrbot/core/star/filter/platform_adapter_type.py +4 -2
  192. astrbot/core/star/filter/regex.py +4 -2
  193. astrbot/core/star/register/__init__.py +19 -19
  194. astrbot/core/star/register/star.py +6 -2
  195. astrbot/core/star/register/star_handler.py +96 -73
  196. astrbot/core/star/session_llm_manager.py +48 -14
  197. astrbot/core/star/session_plugin_manager.py +29 -15
  198. astrbot/core/star/star.py +1 -2
  199. astrbot/core/star/star_handler.py +13 -8
  200. astrbot/core/star/star_manager.py +151 -59
  201. astrbot/core/star/star_tools.py +44 -37
  202. astrbot/core/star/updator.py +10 -10
  203. astrbot/core/umop_config_router.py +10 -4
  204. astrbot/core/updator.py +13 -5
  205. astrbot/core/utils/astrbot_path.py +3 -5
  206. astrbot/core/utils/dify_api_client.py +33 -15
  207. astrbot/core/utils/io.py +66 -42
  208. astrbot/core/utils/log_pipe.py +1 -1
  209. astrbot/core/utils/metrics.py +7 -7
  210. astrbot/core/utils/path_util.py +15 -16
  211. astrbot/core/utils/pip_installer.py +5 -5
  212. astrbot/core/utils/session_waiter.py +19 -20
  213. astrbot/core/utils/shared_preferences.py +45 -20
  214. astrbot/core/utils/t2i/__init__.py +4 -1
  215. astrbot/core/utils/t2i/network_strategy.py +35 -26
  216. astrbot/core/utils/t2i/renderer.py +11 -5
  217. astrbot/core/utils/t2i/template_manager.py +14 -15
  218. astrbot/core/utils/tencent_record_helper.py +19 -13
  219. astrbot/core/utils/version_comparator.py +10 -13
  220. astrbot/core/zip_updator.py +43 -40
  221. astrbot/dashboard/routes/__init__.py +18 -18
  222. astrbot/dashboard/routes/auth.py +10 -8
  223. astrbot/dashboard/routes/chat.py +30 -21
  224. astrbot/dashboard/routes/config.py +92 -75
  225. astrbot/dashboard/routes/conversation.py +46 -39
  226. astrbot/dashboard/routes/file.py +4 -2
  227. astrbot/dashboard/routes/knowledge_base.py +47 -40
  228. astrbot/dashboard/routes/log.py +9 -4
  229. astrbot/dashboard/routes/persona.py +19 -16
  230. astrbot/dashboard/routes/plugin.py +69 -55
  231. astrbot/dashboard/routes/route.py +3 -1
  232. astrbot/dashboard/routes/session_management.py +130 -116
  233. astrbot/dashboard/routes/stat.py +34 -34
  234. astrbot/dashboard/routes/t2i.py +15 -12
  235. astrbot/dashboard/routes/tools.py +47 -52
  236. astrbot/dashboard/routes/update.py +32 -28
  237. astrbot/dashboard/server.py +30 -26
  238. astrbot/dashboard/utils.py +8 -4
  239. {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/METADATA +2 -1
  240. astrbot-4.5.2.dist-info/RECORD +261 -0
  241. astrbot-4.5.1.dist-info/RECORD +0 -260
  242. {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/WHEEL +0 -0
  243. {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/entry_points.txt +0 -0
  244. {astrbot-4.5.1.dist-info → astrbot-4.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,20 @@
1
1
  import asyncio
2
2
  import re
3
- from typing import AsyncGenerator
3
+ from collections.abc import AsyncGenerator
4
+
4
5
  from astrbot.api import logger
5
6
  from astrbot.api.event import AstrMessageEvent, MessageChain
6
- from astrbot.api.platform import PlatformMetadata, AstrBotMessage
7
7
  from astrbot.api.message_components import Plain
8
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
8
9
 
9
10
  from .misskey_utils import (
10
- serialize_message_chain,
11
- resolve_visibility_from_raw_message,
12
- is_valid_user_session_id,
13
- is_valid_room_session_id,
14
11
  add_at_mention_if_needed,
15
- extract_user_id_from_session_id,
16
12
  extract_room_id_from_session_id,
13
+ extract_user_id_from_session_id,
14
+ is_valid_room_session_id,
15
+ is_valid_user_session_id,
16
+ resolve_visibility_from_raw_message,
17
+ serialize_message_chain,
17
18
  )
18
19
 
19
20
 
@@ -43,7 +44,7 @@ class MisskeyPlatformEvent(AstrMessageEvent):
43
44
  """发送消息,使用适配器的完整上传和发送逻辑"""
44
45
  try:
45
46
  logger.debug(
46
- f"[MisskeyEvent] send 方法被调用,消息链包含 {len(message.chain)} 个组件"
47
+ f"[MisskeyEvent] send 方法被调用,消息链包含 {len(message.chain)} 个组件",
47
48
  )
48
49
 
49
50
  # 使用适配器的 send_by_session 方法,它包含文件上传逻辑
@@ -65,7 +66,7 @@ class MisskeyPlatformEvent(AstrMessageEvent):
65
66
  )
66
67
 
67
68
  logger.debug(
68
- f"[MisskeyEvent] 检查适配器方法: hasattr(self.client, 'send_by_session') = {hasattr(self.client, 'send_by_session')}"
69
+ f"[MisskeyEvent] 检查适配器方法: hasattr(self.client, 'send_by_session') = {hasattr(self.client, 'send_by_session')}",
69
70
  )
70
71
 
71
72
  # 调用适配器的 send_by_session 方法
@@ -88,25 +89,27 @@ class MisskeyPlatformEvent(AstrMessageEvent):
88
89
  user_info = {
89
90
  "username": user_data.get("username", ""),
90
91
  "nickname": user_data.get(
91
- "name", user_data.get("username", "")
92
+ "name",
93
+ user_data.get("username", ""),
92
94
  ),
93
95
  }
94
96
  content = add_at_mention_if_needed(content, user_info, has_at)
95
97
 
96
98
  # 根据会话类型选择发送方式
97
99
  if hasattr(self.client, "send_message") and is_valid_user_session_id(
98
- self.session_id
100
+ self.session_id,
99
101
  ):
100
102
  user_id = extract_user_id_from_session_id(self.session_id)
101
103
  await self.client.send_message(user_id, content)
102
104
  elif hasattr(
103
- self.client, "send_room_message"
105
+ self.client,
106
+ "send_room_message",
104
107
  ) and is_valid_room_session_id(self.session_id):
105
108
  room_id = extract_room_id_from_session_id(self.session_id)
106
109
  await self.client.send_room_message(room_id, content)
107
110
  elif original_message_id and hasattr(self.client, "create_note"):
108
111
  visibility, visible_user_ids = resolve_visibility_from_raw_message(
109
- raw_message
112
+ raw_message,
110
113
  )
111
114
  await self.client.create_note(
112
115
  content,
@@ -124,7 +127,9 @@ class MisskeyPlatformEvent(AstrMessageEvent):
124
127
  logger.error(f"[MisskeyEvent] 发送失败: {e}")
125
128
 
126
129
  async def send_streaming(
127
- self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
130
+ self,
131
+ generator: AsyncGenerator[MessageChain, None],
132
+ use_fallback: bool = False,
128
133
  ):
129
134
  if not use_fallback:
130
135
  buffer = None
@@ -134,7 +139,7 @@ class MisskeyPlatformEvent(AstrMessageEvent):
134
139
  else:
135
140
  buffer.chain.extend(chain.chain)
136
141
  if not buffer:
137
- return
142
+ return None
138
143
  buffer.squash_plain()
139
144
  await self.send(buffer)
140
145
  return await super().send_streaming(generator, use_fallback)
@@ -1,6 +1,7 @@
1
1
  """Misskey 平台适配器通用工具函数"""
2
2
 
3
- from typing import Dict, Any, List, Tuple, Optional, Union
3
+ from typing import Any
4
+
4
5
  import astrbot.api.message_components as Comp
5
6
  from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType
6
7
 
@@ -9,7 +10,7 @@ class FileIDExtractor:
9
10
  """从 API 响应中提取文件 ID 的帮助类(无状态)。"""
10
11
 
11
12
  @staticmethod
12
- def extract_file_id(result: Any) -> Optional[str]:
13
+ def extract_file_id(result: Any) -> str | None:
13
14
  if not isinstance(result, dict):
14
15
  return None
15
16
 
@@ -34,8 +35,10 @@ class MessagePayloadBuilder:
34
35
 
35
36
  @staticmethod
36
37
  def build_chat_payload(
37
- user_id: str, text: Optional[str], file_id: Optional[str] = None
38
- ) -> Dict[str, Any]:
38
+ user_id: str,
39
+ text: str | None,
40
+ file_id: str | None = None,
41
+ ) -> dict[str, Any]:
39
42
  payload = {"toUserId": user_id}
40
43
  if text:
41
44
  payload["text"] = text
@@ -45,8 +48,10 @@ class MessagePayloadBuilder:
45
48
 
46
49
  @staticmethod
47
50
  def build_room_payload(
48
- room_id: str, text: Optional[str], file_id: Optional[str] = None
49
- ) -> Dict[str, Any]:
51
+ room_id: str,
52
+ text: str | None,
53
+ file_id: str | None = None,
54
+ ) -> dict[str, Any]:
50
55
  payload = {"toRoomId": room_id}
51
56
  if text:
52
57
  payload["text"] = text
@@ -56,9 +61,11 @@ class MessagePayloadBuilder:
56
61
 
57
62
  @staticmethod
58
63
  def build_note_payload(
59
- text: Optional[str], file_ids: Optional[List[str]] = None, **kwargs
60
- ) -> Dict[str, Any]:
61
- payload: Dict[str, Any] = {}
64
+ text: str | None,
65
+ file_ids: list[str] | None = None,
66
+ **kwargs,
67
+ ) -> dict[str, Any]:
68
+ payload: dict[str, Any] = {}
62
69
  if text:
63
70
  payload["text"] = text
64
71
  if file_ids:
@@ -67,7 +74,7 @@ class MessagePayloadBuilder:
67
74
  return payload
68
75
 
69
76
 
70
- def serialize_message_chain(chain: List[Any]) -> Tuple[str, bool]:
77
+ def serialize_message_chain(chain: list[Any]) -> tuple[str, bool]:
71
78
  """将消息链序列化为文本字符串"""
72
79
  text_parts = []
73
80
  has_at = False
@@ -76,27 +83,25 @@ def serialize_message_chain(chain: List[Any]) -> Tuple[str, bool]:
76
83
  nonlocal has_at
77
84
  if isinstance(component, Comp.Plain):
78
85
  return component.text
79
- elif isinstance(component, Comp.File):
86
+ if isinstance(component, Comp.File):
80
87
  # 为文件组件返回占位符,但适配器仍会处理原组件
81
88
  return "[文件]"
82
- elif isinstance(component, Comp.Image):
89
+ if isinstance(component, Comp.Image):
83
90
  # 为图片组件返回占位符,但适配器仍会处理原组件
84
91
  return "[图片]"
85
- elif isinstance(component, Comp.At):
92
+ if isinstance(component, Comp.At):
86
93
  has_at = True
87
94
  # 优先使用name字段(用户名),如果没有则使用qq字段
88
95
  # 这样可以避免在Misskey中生成 @<user_id> 这样的无效提及
89
96
  if hasattr(component, "name") and component.name:
90
97
  return f"@{component.name}"
91
- else:
92
- return f"@{component.qq}"
93
- elif hasattr(component, "text"):
98
+ return f"@{component.qq}"
99
+ if hasattr(component, "text"):
94
100
  text = getattr(component, "text", "")
95
101
  if "@" in text:
96
102
  has_at = True
97
103
  return text
98
- else:
99
- return str(component)
104
+ return str(component)
100
105
 
101
106
  for component in chain:
102
107
  if isinstance(component, Comp.Node) and component.content:
@@ -113,12 +118,12 @@ def serialize_message_chain(chain: List[Any]) -> Tuple[str, bool]:
113
118
 
114
119
 
115
120
  def resolve_message_visibility(
116
- user_id: Optional[str] = None,
117
- user_cache: Optional[Dict[str, Any]] = None,
118
- self_id: Optional[str] = None,
119
- raw_message: Optional[Dict[str, Any]] = None,
121
+ user_id: str | None = None,
122
+ user_cache: dict[str, Any] | None = None,
123
+ self_id: str | None = None,
124
+ raw_message: dict[str, Any] | None = None,
120
125
  default_visibility: str = "public",
121
- ) -> Tuple[str, Optional[List[str]]]:
126
+ ) -> tuple[str, list[str] | None]:
122
127
  """解析 Misskey 消息的可见性设置
123
128
 
124
129
  可以从 user_cache 或 raw_message 中解析,支持两种调用方式:
@@ -169,13 +174,14 @@ def resolve_message_visibility(
169
174
 
170
175
  # 保留旧函数名作为向后兼容的别名
171
176
  def resolve_visibility_from_raw_message(
172
- raw_message: Dict[str, Any], self_id: Optional[str] = None
173
- ) -> Tuple[str, Optional[List[str]]]:
177
+ raw_message: dict[str, Any],
178
+ self_id: str | None = None,
179
+ ) -> tuple[str, list[str] | None]:
174
180
  """从原始消息数据中解析可见性设置(已弃用,使用 resolve_message_visibility 替代)"""
175
181
  return resolve_message_visibility(raw_message=raw_message, self_id=self_id)
176
182
 
177
183
 
178
- def is_valid_user_session_id(session_id: Union[str, Any]) -> bool:
184
+ def is_valid_user_session_id(session_id: str | Any) -> bool:
179
185
  """检查 session_id 是否是有效的聊天用户 session_id (仅限chat%前缀)"""
180
186
  if not isinstance(session_id, str) or "%" not in session_id:
181
187
  return False
@@ -189,7 +195,7 @@ def is_valid_user_session_id(session_id: Union[str, Any]) -> bool:
189
195
  )
190
196
 
191
197
 
192
- def is_valid_room_session_id(session_id: Union[str, Any]) -> bool:
198
+ def is_valid_room_session_id(session_id: str | Any) -> bool:
193
199
  """检查 session_id 是否是有效的房间 session_id (仅限room%前缀)"""
194
200
  if not isinstance(session_id, str) or "%" not in session_id:
195
201
  return False
@@ -203,7 +209,7 @@ def is_valid_room_session_id(session_id: Union[str, Any]) -> bool:
203
209
  )
204
210
 
205
211
 
206
- def is_valid_chat_session_id(session_id: Union[str, Any]) -> bool:
212
+ def is_valid_chat_session_id(session_id: str | Any) -> bool:
207
213
  """检查 session_id 是否是有效的聊天 session_id (仅限chat%前缀)"""
208
214
  if not isinstance(session_id, str) or "%" not in session_id:
209
215
  return False
@@ -236,7 +242,9 @@ def extract_room_id_from_session_id(session_id: str) -> str:
236
242
 
237
243
 
238
244
  def add_at_mention_if_needed(
239
- text: str, user_info: Optional[Dict[str, Any]], has_at: bool = False
245
+ text: str,
246
+ user_info: dict[str, Any] | None,
247
+ has_at: bool = False,
240
248
  ) -> str:
241
249
  """如果需要且没有@用户,则添加@用户
242
250
 
@@ -258,7 +266,7 @@ def add_at_mention_if_needed(
258
266
  return text
259
267
 
260
268
 
261
- def create_file_component(file_info: Dict[str, Any]) -> Tuple[Any, str]:
269
+ def create_file_component(file_info: dict[str, Any]) -> tuple[Any, str]:
262
270
  """创建文件组件和描述文本"""
263
271
  file_url = file_info.get("url", "")
264
272
  file_name = file_info.get("name", "未知文件")
@@ -266,16 +274,17 @@ def create_file_component(file_info: Dict[str, Any]) -> Tuple[Any, str]:
266
274
 
267
275
  if file_type.startswith("image/"):
268
276
  return Comp.Image(url=file_url, file=file_name), f"图片[{file_name}]"
269
- elif file_type.startswith("audio/"):
277
+ if file_type.startswith("audio/"):
270
278
  return Comp.Record(url=file_url, file=file_name), f"音频[{file_name}]"
271
- elif file_type.startswith("video/"):
279
+ if file_type.startswith("video/"):
272
280
  return Comp.Video(url=file_url, file=file_name), f"视频[{file_name}]"
273
- else:
274
- return Comp.File(name=file_name, url=file_url), f"文件[{file_name}]"
281
+ return Comp.File(name=file_name, url=file_url), f"文件[{file_name}]"
275
282
 
276
283
 
277
284
  def process_files(
278
- message: AstrBotMessage, files: list, include_text_parts: bool = True
285
+ message: AstrBotMessage,
286
+ files: list,
287
+ include_text_parts: bool = True,
279
288
  ) -> list:
280
289
  """处理文件列表,添加到消息组件中并返回文本描述"""
281
290
  file_parts = []
@@ -287,7 +296,7 @@ def process_files(
287
296
  return file_parts
288
297
 
289
298
 
290
- def format_poll(poll: Dict[str, Any]) -> str:
299
+ def format_poll(poll: dict[str, Any]) -> str:
291
300
  """将 Misskey 的 poll 对象格式化为可读字符串。"""
292
301
  if not poll or not isinstance(poll, dict):
293
302
  return ""
@@ -304,8 +313,9 @@ def format_poll(poll: Dict[str, Any]) -> str:
304
313
 
305
314
 
306
315
  def extract_sender_info(
307
- raw_data: Dict[str, Any], is_chat: bool = False
308
- ) -> Dict[str, Any]:
316
+ raw_data: dict[str, Any],
317
+ is_chat: bool = False,
318
+ ) -> dict[str, Any]:
309
319
  """提取发送者信息"""
310
320
  if is_chat:
311
321
  sender = raw_data.get("fromUser", {})
@@ -323,11 +333,11 @@ def extract_sender_info(
323
333
 
324
334
 
325
335
  def create_base_message(
326
- raw_data: Dict[str, Any],
327
- sender_info: Dict[str, Any],
336
+ raw_data: dict[str, Any],
337
+ sender_info: dict[str, Any],
328
338
  client_self_id: str,
329
339
  is_chat: bool = False,
330
- room_id: Optional[str] = None,
340
+ room_id: str | None = None,
331
341
  unique_session: bool = False,
332
342
  ) -> AstrBotMessage:
333
343
  """创建基础消息对象"""
@@ -366,8 +376,11 @@ def create_base_message(
366
376
 
367
377
 
368
378
  def process_at_mention(
369
- message: AstrBotMessage, raw_text: str, bot_username: str, client_self_id: str
370
- ) -> Tuple[List[str], str]:
379
+ message: AstrBotMessage,
380
+ raw_text: str,
381
+ bot_username: str,
382
+ client_self_id: str,
383
+ ) -> tuple[list[str], str]:
371
384
  """处理@提及逻辑,返回消息部分列表和处理后的文本"""
372
385
  message_parts = []
373
386
 
@@ -382,16 +395,15 @@ def process_at_mention(
382
395
  message.message.append(Comp.Plain(remaining_text))
383
396
  message_parts.append(remaining_text)
384
397
  return message_parts, remaining_text
385
- else:
386
- message.message.append(Comp.Plain(raw_text))
387
- message_parts.append(raw_text)
388
- return message_parts, raw_text
398
+ message.message.append(Comp.Plain(raw_text))
399
+ message_parts.append(raw_text)
400
+ return message_parts, raw_text
389
401
 
390
402
 
391
403
  def cache_user_info(
392
- user_cache: Dict[str, Any],
393
- sender_info: Dict[str, Any],
394
- raw_data: Dict[str, Any],
404
+ user_cache: dict[str, Any],
405
+ sender_info: dict[str, Any],
406
+ raw_data: dict[str, Any],
395
407
  client_self_id: str,
396
408
  is_chat: bool = False,
397
409
  ):
@@ -417,7 +429,9 @@ def cache_user_info(
417
429
 
418
430
 
419
431
  def cache_room_info(
420
- user_cache: Dict[str, Any], raw_data: Dict[str, Any], client_self_id: str
432
+ user_cache: dict[str, Any],
433
+ raw_data: dict[str, Any],
434
+ client_self_id: str,
421
435
  ):
422
436
  """缓存房间信息"""
423
437
  room_data = raw_data.get("toRoom")
@@ -437,7 +451,7 @@ def cache_room_info(
437
451
 
438
452
  async def resolve_component_url_or_path(
439
453
  comp: Any,
440
- ) -> Tuple[Optional[str], Optional[str]]:
454
+ ) -> tuple[str | None, str | None]:
441
455
  """尝试从组件解析可上传的远程 URL 或本地路径。
442
456
 
443
457
  返回 (url_candidate, local_path)。两者可能都为 None。
@@ -468,8 +482,7 @@ async def resolve_component_url_or_path(
468
482
  if value.startswith("http"):
469
483
  url_candidate = value
470
484
  break
471
- else:
472
- local_path = value
485
+ local_path = value
473
486
  except Exception:
474
487
  continue
475
488
 
@@ -491,9 +504,8 @@ async def resolve_component_url_or_path(
491
504
  if value.startswith("http"):
492
505
  url_candidate = value
493
506
  break
494
- else:
495
- local_path = value
496
- break
507
+ local_path = value
508
+ break
497
509
  except Exception:
498
510
  continue
499
511
 
@@ -503,7 +515,7 @@ async def resolve_component_url_or_path(
503
515
  return url_candidate, local_path
504
516
 
505
517
 
506
- def summarize_component_for_log(comp: Any) -> Dict[str, Any]:
518
+ def summarize_component_for_log(comp: Any) -> dict[str, Any]:
507
519
  """生成适合日志的组件属性字典(尽量不抛异常)。"""
508
520
  attrs = {}
509
521
  for a in ("file", "url", "path", "src", "source", "name"):
@@ -519,15 +531,15 @@ def summarize_component_for_log(comp: Any) -> Dict[str, Any]:
519
531
  async def upload_local_with_retries(
520
532
  api: Any,
521
533
  local_path: str,
522
- preferred_name: Optional[str],
523
- folder_id: Optional[str],
524
- ) -> Optional[str]:
534
+ preferred_name: str | None,
535
+ folder_id: str | None,
536
+ ) -> str | None:
525
537
  """尝试本地上传,返回 file id 或 None。如果文件类型不允许则直接失败。"""
526
538
  try:
527
539
  res = await api.upload_file(local_path, preferred_name, folder_id)
528
540
  if isinstance(res, dict):
529
541
  fid = res.get("id") or (res.get("raw") or {}).get("createdFile", {}).get(
530
- "id"
542
+ "id",
531
543
  )
532
544
  if fid:
533
545
  return str(fid)
@@ -1,25 +1,26 @@
1
+ import asyncio
2
+ import base64
3
+ import os
4
+ import random
5
+ import uuid
6
+
7
+ import aiofiles
1
8
  import botpy
2
9
  import botpy.message
3
10
  import botpy.types
4
11
  import botpy.types.message
5
- import asyncio
6
- import base64
7
- import aiofiles
8
- from astrbot.core.utils.io import file_to_base64, download_image_by_url
9
- from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
10
- from astrbot.core.utils.astrbot_path import get_astrbot_data_path
11
- from astrbot.api.event import AstrMessageEvent, MessageChain
12
- from astrbot.api.platform import AstrBotMessage, PlatformMetadata
13
- from astrbot.api.message_components import Plain, Image, Record
14
12
  from botpy import Client
15
13
  from botpy.http import Route
16
- from astrbot.api import logger
17
- from botpy.types.message import Media
18
14
  from botpy.types import message
19
- from typing import Optional
20
- import random
21
- import uuid
22
- import os
15
+ from botpy.types.message import Media
16
+
17
+ from astrbot.api import logger
18
+ from astrbot.api.event import AstrMessageEvent, MessageChain
19
+ from astrbot.api.message_components import Image, Plain, Record
20
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
21
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
22
+ from astrbot.core.utils.io import download_image_by_url, file_to_base64
23
+ from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
23
24
 
24
25
 
25
26
  class QQOfficialMessageEvent(AstrMessageEvent):
@@ -75,9 +76,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
75
76
 
76
77
  return await super().send_streaming(generator, use_fallback)
77
78
 
78
- async def _post_send(self, stream: dict = None):
79
+ async def _post_send(self, stream: dict | None = None):
79
80
  if not self.send_buffer:
80
- return
81
+ return None
81
82
 
82
83
  source = self.message_obj.raw_message
83
84
  assert isinstance(
@@ -103,7 +104,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
103
104
  and not image_path
104
105
  and not record_file_path
105
106
  ):
106
- return
107
+ return None
107
108
 
108
109
  payload = {
109
110
  "content": plain_text,
@@ -119,29 +120,38 @@ class QQOfficialMessageEvent(AstrMessageEvent):
119
120
  case botpy.message.GroupMessage:
120
121
  if image_base64:
121
122
  media = await self.upload_group_and_c2c_image(
122
- image_base64, 1, group_openid=source.group_openid
123
+ image_base64,
124
+ 1,
125
+ group_openid=source.group_openid,
123
126
  )
124
127
  payload["media"] = media
125
128
  payload["msg_type"] = 7
126
129
  if record_file_path: # group record msg
127
130
  media = await self.upload_group_and_c2c_record(
128
- record_file_path, 3, group_openid=source.group_openid
131
+ record_file_path,
132
+ 3,
133
+ group_openid=source.group_openid,
129
134
  )
130
135
  payload["media"] = media
131
136
  payload["msg_type"] = 7
132
137
  ret = await self.bot.api.post_group_message(
133
- group_openid=source.group_openid, **payload
138
+ group_openid=source.group_openid,
139
+ **payload,
134
140
  )
135
141
  case botpy.message.C2CMessage:
136
142
  if image_base64:
137
143
  media = await self.upload_group_and_c2c_image(
138
- image_base64, 1, openid=source.author.user_openid
144
+ image_base64,
145
+ 1,
146
+ openid=source.author.user_openid,
139
147
  )
140
148
  payload["media"] = media
141
149
  payload["msg_type"] = 7
142
150
  if record_file_path: # c2c record
143
151
  media = await self.upload_group_and_c2c_record(
144
- record_file_path, 3, openid=source.author.user_openid
152
+ record_file_path,
153
+ 3,
154
+ openid=source.author.user_openid,
145
155
  )
146
156
  payload["media"] = media
147
157
  payload["msg_type"] = 7
@@ -153,14 +163,16 @@ class QQOfficialMessageEvent(AstrMessageEvent):
153
163
  )
154
164
  else:
155
165
  ret = await self.post_c2c_message(
156
- openid=source.author.user_openid, **payload
166
+ openid=source.author.user_openid,
167
+ **payload,
157
168
  )
158
169
  logger.debug(f"Message sent to C2C: {ret}")
159
170
  case botpy.message.Message:
160
171
  if image_path:
161
172
  payload["file_image"] = image_path
162
173
  ret = await self.bot.api.post_message(
163
- channel_id=source.channel_id, **payload
174
+ channel_id=source.channel_id,
175
+ **payload,
164
176
  )
165
177
  case botpy.message.DirectMessage:
166
178
  if image_path:
@@ -174,7 +186,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
174
186
  return ret
175
187
 
176
188
  async def upload_group_and_c2c_image(
177
- self, image_base64: str, file_type: int, **kwargs
189
+ self,
190
+ image_base64: str,
191
+ file_type: int,
192
+ **kwargs,
178
193
  ) -> botpy.types.message.Media:
179
194
  payload = {
180
195
  "file_data": image_base64,
@@ -185,7 +200,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
185
200
  payload["openid"] = kwargs["openid"]
186
201
  route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
187
202
  return await self.bot.api._http.request(route, json=payload)
188
- elif "group_openid" in kwargs:
203
+ if "group_openid" in kwargs:
189
204
  payload["group_openid"] = kwargs["group_openid"]
190
205
  route = Route(
191
206
  "POST",
@@ -195,11 +210,13 @@ class QQOfficialMessageEvent(AstrMessageEvent):
195
210
  return await self.bot.api._http.request(route, json=payload)
196
211
 
197
212
  async def upload_group_and_c2c_record(
198
- self, file_source: str, file_type: int, srv_send_msg: bool = False, **kwargs
199
- ) -> Optional[Media]:
200
- """
201
- 上传媒体文件
202
- """
213
+ self,
214
+ file_source: str,
215
+ file_type: int,
216
+ srv_send_msg: bool = False,
217
+ **kwargs,
218
+ ) -> Media | None:
219
+ """上传媒体文件"""
203
220
  # 构建基础payload
204
221
  payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
205
222
 
@@ -248,17 +265,17 @@ class QQOfficialMessageEvent(AstrMessageEvent):
248
265
  self,
249
266
  openid: str,
250
267
  msg_type: int = 0,
251
- content: str = None,
252
- embed: message.Embed = None,
253
- ark: message.Ark = None,
254
- message_reference: message.Reference = None,
255
- media: message.Media = None,
256
- msg_id: str = None,
268
+ content: str | None = None,
269
+ embed: message.Embed | None = None,
270
+ ark: message.Ark | None = None,
271
+ message_reference: message.Reference | None = None,
272
+ media: message.Media | None = None,
273
+ msg_id: str | None = None,
257
274
  msg_seq: str = 1,
258
- event_id: str = None,
259
- markdown: message.MarkdownPayload = None,
260
- keyboard: message.Keyboard = None,
261
- stream: dict = None,
275
+ event_id: str | None = None,
276
+ markdown: message.MarkdownPayload | None = None,
277
+ keyboard: message.Keyboard | None = None,
278
+ stream: dict | None = None,
262
279
  ) -> message.Message:
263
280
  payload = locals()
264
281
  payload.pop("self", None)
@@ -291,11 +308,13 @@ class QQOfficialMessageEvent(AstrMessageEvent):
291
308
  record_wav_path = await i.convert_to_file_path() # wav 路径
292
309
  temp_dir = os.path.join(get_astrbot_data_path(), "temp")
293
310
  record_tecent_silk_path = os.path.join(
294
- temp_dir, f"{uuid.uuid4()}.silk"
311
+ temp_dir,
312
+ f"{uuid.uuid4()}.silk",
295
313
  )
296
314
  try:
297
315
  duration = await wav_to_tencent_silk(
298
- record_wav_path, record_tecent_silk_path
316
+ record_wav_path,
317
+ record_tecent_silk_path,
299
318
  )
300
319
  if duration > 0:
301
320
  record_file_path = record_tecent_silk_path