AstrBot 4.5.1__py3-none-any.whl → 4.5.3__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 +56 -53
  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.3.dist-info}/METADATA +2 -1
  240. astrbot-4.5.3.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.3.dist-info}/WHEEL +0 -0
  243. {astrbot-4.5.1.dist-info → astrbot-4.5.3.dist-info}/entry_points.txt +0 -0
  244. {astrbot-4.5.1.dist-info → astrbot-4.5.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,20 @@
1
+ import asyncio
1
2
  import json
2
3
  import random
3
- import asyncio
4
- from typing import Any, Optional, Dict, List, Callable, Awaitable
5
4
  import uuid
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
6
7
 
7
8
  try:
8
9
  import aiohttp
9
10
  import websockets
10
11
  except ImportError as e:
11
12
  raise ImportError(
12
- "aiohttp and websockets are required for Misskey API. Please install them with: pip install aiohttp websockets"
13
+ "aiohttp and websockets are required for Misskey API. Please install them with: pip install aiohttp websockets",
13
14
  ) from e
14
15
 
15
16
  from astrbot.api import logger
17
+
16
18
  from .misskey_utils import FileIDExtractor
17
19
 
18
20
  # Constants
@@ -23,54 +25,47 @@ HTTP_OK = 200
23
25
  class APIError(Exception):
24
26
  """Misskey API 基础异常"""
25
27
 
26
- pass
27
-
28
28
 
29
29
  class APIConnectionError(APIError):
30
30
  """网络连接异常"""
31
31
 
32
- pass
33
-
34
32
 
35
33
  class APIRateLimitError(APIError):
36
34
  """API 频率限制异常"""
37
35
 
38
- pass
39
-
40
36
 
41
37
  class AuthenticationError(APIError):
42
38
  """认证失败异常"""
43
39
 
44
- pass
45
-
46
40
 
47
41
  class WebSocketError(APIError):
48
42
  """WebSocket 连接异常"""
49
43
 
50
- pass
51
-
52
44
 
53
45
  class StreamingClient:
54
46
  def __init__(self, instance_url: str, access_token: str):
55
47
  self.instance_url = instance_url.rstrip("/")
56
48
  self.access_token = access_token
57
- self.websocket: Optional[Any] = None
49
+ self.websocket: Any | None = None
58
50
  self.is_connected = False
59
- self.message_handlers: Dict[str, Callable] = {}
60
- self.channels: Dict[str, str] = {}
61
- self.desired_channels: Dict[str, Optional[Dict]] = {}
51
+ self.message_handlers: dict[str, Callable] = {}
52
+ self.channels: dict[str, str] = {}
53
+ self.desired_channels: dict[str, dict | None] = {}
62
54
  self._running = False
63
55
  self._last_pong = None
64
56
 
65
57
  async def connect(self) -> bool:
66
58
  try:
67
59
  ws_url = self.instance_url.replace("https://", "wss://").replace(
68
- "http://", "ws://"
60
+ "http://",
61
+ "ws://",
69
62
  )
70
63
  ws_url += f"/streaming?i={self.access_token}"
71
64
 
72
65
  self.websocket = await websockets.connect(
73
- ws_url, ping_interval=30, ping_timeout=10
66
+ ws_url,
67
+ ping_interval=30,
68
+ ping_timeout=10,
74
69
  )
75
70
  self.is_connected = True
76
71
  self._running = True
@@ -84,7 +79,7 @@ class StreamingClient:
84
79
  await self.subscribe_channel(channel_type, params)
85
80
  except Exception as e:
86
81
  logger.warning(
87
- f"[Misskey WebSocket] 重新订阅 {channel_type} 失败: {e}"
82
+ f"[Misskey WebSocket] 重新订阅 {channel_type} 失败: {e}",
88
83
  )
89
84
  except Exception:
90
85
  pass
@@ -104,7 +99,9 @@ class StreamingClient:
104
99
  logger.info("[Misskey WebSocket] 连接已断开")
105
100
 
106
101
  async def subscribe_channel(
107
- self, channel_type: str, params: Optional[Dict] = None
102
+ self,
103
+ channel_type: str,
104
+ params: dict | None = None,
108
105
  ) -> str:
109
106
  if not self.is_connected or not self.websocket:
110
107
  raise WebSocketError("WebSocket 未连接")
@@ -136,7 +133,9 @@ class StreamingClient:
136
133
  self.desired_channels.pop(channel_type, None)
137
134
 
138
135
  def add_message_handler(
139
- self, event_type: str, handler: Callable[[Dict], Awaitable[None]]
136
+ self,
137
+ event_type: str,
138
+ handler: Callable[[dict], Awaitable[None]],
140
139
  ):
141
140
  self.message_handlers[event_type] = handler
142
141
 
@@ -166,7 +165,7 @@ class StreamingClient:
166
165
  pass
167
166
  except websockets.exceptions.ConnectionClosed as e:
168
167
  logger.warning(
169
- f"[Misskey WebSocket] 连接已关闭 (代码: {e.code}, 原因: {e.reason})"
168
+ f"[Misskey WebSocket] 连接已关闭 (代码: {e.code}, 原因: {e.reason})",
170
169
  )
171
170
  self.is_connected = False
172
171
  try:
@@ -188,11 +187,11 @@ class StreamingClient:
188
187
  except Exception:
189
188
  pass
190
189
 
191
- async def _handle_message(self, data: Dict[str, Any]):
190
+ async def _handle_message(self, data: dict[str, Any]):
192
191
  message_type = data.get("type")
193
192
  body = data.get("body", {})
194
193
 
195
- def _build_channel_summary(message_type: Optional[str], body: Any) -> str:
194
+ def _build_channel_summary(message_type: str | None, body: Any) -> str:
196
195
  try:
197
196
  if not isinstance(body, dict):
198
197
  return f"[Misskey WebSocket] 收到消息类型: {message_type}"
@@ -228,7 +227,7 @@ class StreamingClient:
228
227
  event_body = body.get("body", {})
229
228
 
230
229
  logger.debug(
231
- f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}"
230
+ f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}",
232
231
  )
233
232
 
234
233
  if channel_id in self.channels:
@@ -243,7 +242,7 @@ class StreamingClient:
243
242
  await self.message_handlers[event_type](event_body)
244
243
  else:
245
244
  logger.debug(
246
- f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}"
245
+ f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}",
247
246
  )
248
247
  if "_debug" in self.message_handlers:
249
248
  await self.message_handlers["_debug"](
@@ -251,7 +250,7 @@ class StreamingClient:
251
250
  "type": event_type,
252
251
  "body": event_body,
253
252
  "channel": channel_type,
254
- }
253
+ },
255
254
  )
256
255
 
257
256
  elif message_type in self.message_handlers:
@@ -269,14 +268,14 @@ def retry_async(
269
268
  backoff_base: float = 1.0,
270
269
  max_backoff: float = 30.0,
271
270
  ):
272
- """
273
- 智能异步重试装饰器
271
+ """智能异步重试装饰器
274
272
 
275
273
  Args:
276
274
  max_retries: 最大重试次数
277
275
  retryable_exceptions: 可重试的异常类型
278
276
  backoff_base: 退避基数
279
277
  max_backoff: 最大退避时间
278
+
280
279
  """
281
280
 
282
281
  def decorator(func):
@@ -291,7 +290,7 @@ def retry_async(
291
290
  last_exc = e
292
291
  if attempt == max_retries:
293
292
  logger.error(
294
- f"[Misskey API] {func_name} 重试 {max_retries} 次后仍失败: {e}"
293
+ f"[Misskey API] {func_name} 重试 {max_retries} 次后仍失败: {e}",
295
294
  )
296
295
  break
297
296
 
@@ -308,7 +307,7 @@ def retry_async(
308
307
 
309
308
  logger.warning(
310
309
  f"[Misskey API] {func_name} 第 {attempt} 次重试失败: {e},"
311
- f"{sleep_time:.1f}s后重试"
310
+ f"{sleep_time:.1f}s后重试",
312
311
  )
313
312
  await asyncio.sleep(sleep_time)
314
313
  continue
@@ -334,12 +333,12 @@ class MisskeyAPI:
334
333
  allow_insecure_downloads: bool = False,
335
334
  download_timeout: int = 15,
336
335
  chunk_size: int = 64 * 1024,
337
- max_download_bytes: Optional[int] = None,
336
+ max_download_bytes: int | None = None,
338
337
  ):
339
338
  self.instance_url = instance_url.rstrip("/")
340
339
  self.access_token = access_token
341
- self._session: Optional[aiohttp.ClientSession] = None
342
- self.streaming: Optional[StreamingClient] = None
340
+ self._session: aiohttp.ClientSession | None = None
341
+ self.streaming: StreamingClient | None = None
343
342
  # download options
344
343
  self.allow_insecure_downloads = allow_insecure_downloads
345
344
  self.download_timeout = download_timeout
@@ -381,39 +380,40 @@ class MisskeyAPI:
381
380
  if status == 400:
382
381
  logger.error(f"[Misskey API] 请求参数错误: {endpoint} (HTTP {status})")
383
382
  raise APIError(f"Bad request for {endpoint}")
384
- elif status == 401:
383
+ if status == 401:
385
384
  logger.error(f"[Misskey API] 未授权访问: {endpoint} (HTTP {status})")
386
385
  raise AuthenticationError(f"Unauthorized access for {endpoint}")
387
- elif status == 403:
386
+ if status == 403:
388
387
  logger.error(f"[Misskey API] 访问被禁止: {endpoint} (HTTP {status})")
389
388
  raise AuthenticationError(f"Forbidden access for {endpoint}")
390
- elif status == 404:
389
+ if status == 404:
391
390
  logger.error(f"[Misskey API] 资源不存在: {endpoint} (HTTP {status})")
392
391
  raise APIError(f"Resource not found for {endpoint}")
393
- elif status == 413:
392
+ if status == 413:
394
393
  logger.error(f"[Misskey API] 请求体过大: {endpoint} (HTTP {status})")
395
394
  raise APIError(f"Request entity too large for {endpoint}")
396
- elif status == 429:
395
+ if status == 429:
397
396
  logger.warning(f"[Misskey API] 请求频率限制: {endpoint} (HTTP {status})")
398
397
  raise APIRateLimitError(f"Rate limit exceeded for {endpoint}")
399
- elif status == 500:
398
+ if status == 500:
400
399
  logger.error(f"[Misskey API] 服务器内部错误: {endpoint} (HTTP {status})")
401
400
  raise APIConnectionError(f"Internal server error for {endpoint}")
402
- elif status == 502:
401
+ if status == 502:
403
402
  logger.error(f"[Misskey API] 网关错误: {endpoint} (HTTP {status})")
404
403
  raise APIConnectionError(f"Bad gateway for {endpoint}")
405
- elif status == 503:
404
+ if status == 503:
406
405
  logger.error(f"[Misskey API] 服务不可用: {endpoint} (HTTP {status})")
407
406
  raise APIConnectionError(f"Service unavailable for {endpoint}")
408
- elif status == 504:
407
+ if status == 504:
409
408
  logger.error(f"[Misskey API] 网关超时: {endpoint} (HTTP {status})")
410
409
  raise APIConnectionError(f"Gateway timeout for {endpoint}")
411
- else:
412
- logger.error(f"[Misskey API] 未知错误: {endpoint} (HTTP {status})")
413
- raise APIConnectionError(f"HTTP {status} for {endpoint}")
410
+ logger.error(f"[Misskey API] 未知错误: {endpoint} (HTTP {status})")
411
+ raise APIConnectionError(f"HTTP {status} for {endpoint}")
414
412
 
415
413
  async def _process_response(
416
- self, response: aiohttp.ClientResponse, endpoint: str
414
+ self,
415
+ response: aiohttp.ClientResponse,
416
+ endpoint: str,
417
417
  ) -> Any:
418
418
  """处理 API 响应"""
419
419
  if response.status == HTTP_OK:
@@ -429,7 +429,7 @@ class MisskeyAPI:
429
429
  )
430
430
  if notifications_data:
431
431
  logger.debug(
432
- f"[Misskey API] 获取到 {len(notifications_data)} 条新通知"
432
+ f"[Misskey API] 获取到 {len(notifications_data)} 条新通知",
433
433
  )
434
434
  else:
435
435
  logger.debug(f"[Misskey API] 请求成功: {endpoint}")
@@ -441,11 +441,11 @@ class MisskeyAPI:
441
441
  try:
442
442
  error_text = await response.text()
443
443
  logger.error(
444
- f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}, 响应: {error_text}"
444
+ f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}, 响应: {error_text}",
445
445
  )
446
446
  except Exception:
447
447
  logger.error(
448
- f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}"
448
+ f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}",
449
449
  )
450
450
 
451
451
  self._handle_response_status(response.status, endpoint)
@@ -456,7 +456,9 @@ class MisskeyAPI:
456
456
  retryable_exceptions=(APIConnectionError, APIRateLimitError),
457
457
  )
458
458
  async def _make_request(
459
- self, endpoint: str, data: Optional[Dict[str, Any]] = None
459
+ self,
460
+ endpoint: str,
461
+ data: dict[str, Any] | None = None,
460
462
  ) -> Any:
461
463
  url = f"{self.instance_url}/api/{endpoint}"
462
464
  payload = {"i": self.access_token}
@@ -472,24 +474,24 @@ class MisskeyAPI:
472
474
 
473
475
  async def create_note(
474
476
  self,
475
- text: Optional[str] = None,
477
+ text: str | None = None,
476
478
  visibility: str = "public",
477
- reply_id: Optional[str] = None,
478
- visible_user_ids: Optional[List[str]] = None,
479
- file_ids: Optional[List[str]] = None,
479
+ reply_id: str | None = None,
480
+ visible_user_ids: list[str] | None = None,
481
+ file_ids: list[str] | None = None,
480
482
  local_only: bool = False,
481
- cw: Optional[str] = None,
482
- poll: Optional[Dict[str, Any]] = None,
483
- renote_id: Optional[str] = None,
484
- channel_id: Optional[str] = None,
485
- reaction_acceptance: Optional[str] = None,
486
- no_extract_mentions: Optional[bool] = None,
487
- no_extract_hashtags: Optional[bool] = None,
488
- no_extract_emojis: Optional[bool] = None,
489
- media_ids: Optional[List[str]] = None,
490
- ) -> Dict[str, Any]:
483
+ cw: str | None = None,
484
+ poll: dict[str, Any] | None = None,
485
+ renote_id: str | None = None,
486
+ channel_id: str | None = None,
487
+ reaction_acceptance: str | None = None,
488
+ no_extract_mentions: bool | None = None,
489
+ no_extract_hashtags: bool | None = None,
490
+ no_extract_emojis: bool | None = None,
491
+ media_ids: list[str] | None = None,
492
+ ) -> dict[str, Any]:
491
493
  """Create a note (wrapper for notes/create). All additional fields are optional and passed through to the API."""
492
- data: Dict[str, Any] = {}
494
+ data: dict[str, Any] = {}
493
495
 
494
496
  if text is not None:
495
497
  data["text"] = text
@@ -537,9 +539,9 @@ class MisskeyAPI:
537
539
  async def upload_file(
538
540
  self,
539
541
  file_path: str,
540
- name: Optional[str] = None,
541
- folder_id: Optional[str] = None,
542
- ) -> Dict[str, Any]:
542
+ name: str | None = None,
543
+ folder_id: str | None = None,
544
+ ) -> dict[str, Any]:
543
545
  """Upload a file to Misskey drive/files/create and return a dict containing id and raw result."""
544
546
  if not file_path:
545
547
  raise APIError("No file path provided for upload")
@@ -565,7 +567,7 @@ class MisskeyAPI:
565
567
  result = await self._process_response(resp, "drive/files/create")
566
568
  file_id = FileIDExtractor.extract_file_id(result)
567
569
  logger.debug(
568
- f"[Misskey API] 本地文件上传成功: {filename} -> {file_id}"
570
+ f"[Misskey API] 本地文件上传成功: {filename} -> {file_id}",
569
571
  )
570
572
  return {"id": file_id, "raw": result}
571
573
  finally:
@@ -574,7 +576,7 @@ class MisskeyAPI:
574
576
  logger.error(f"[Misskey API] 文件上传网络错误: {e}")
575
577
  raise APIConnectionError(f"Upload failed: {e}") from e
576
578
 
577
- async def find_files_by_hash(self, md5_hash: str) -> List[Dict[str, Any]]:
579
+ async def find_files_by_hash(self, md5_hash: str) -> list[dict[str, Any]]:
578
580
  """Find files by MD5 hash"""
579
581
  if not md5_hash:
580
582
  raise APIError("No MD5 hash provided for find-by-hash")
@@ -585,7 +587,7 @@ class MisskeyAPI:
585
587
  logger.debug(f"[Misskey API] find-by-hash 请求: md5={md5_hash}")
586
588
  result = await self._make_request("drive/files/find-by-hash", data)
587
589
  logger.debug(
588
- f"[Misskey API] find-by-hash 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
590
+ f"[Misskey API] find-by-hash 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
589
591
  )
590
592
  return result if isinstance(result, list) else []
591
593
  except Exception as e:
@@ -593,13 +595,15 @@ class MisskeyAPI:
593
595
  raise
594
596
 
595
597
  async def find_files_by_name(
596
- self, name: str, folder_id: Optional[str] = None
597
- ) -> List[Dict[str, Any]]:
598
+ self,
599
+ name: str,
600
+ folder_id: str | None = None,
601
+ ) -> list[dict[str, Any]]:
598
602
  """Find files by name"""
599
603
  if not name:
600
604
  raise APIError("No name provided for find")
601
605
 
602
- data: Dict[str, Any] = {"name": name}
606
+ data: dict[str, Any] = {"name": name}
603
607
  if folder_id:
604
608
  data["folderId"] = folder_id
605
609
 
@@ -607,7 +611,7 @@ class MisskeyAPI:
607
611
  logger.debug(f"[Misskey API] find 请求: name={name}, folder_id={folder_id}")
608
612
  result = await self._make_request("drive/files/find", data)
609
613
  logger.debug(
610
- f"[Misskey API] find 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
614
+ f"[Misskey API] find 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
611
615
  )
612
616
  return result if isinstance(result, list) else []
613
617
  except Exception as e:
@@ -617,11 +621,11 @@ class MisskeyAPI:
617
621
  async def find_files(
618
622
  self,
619
623
  limit: int = 10,
620
- folder_id: Optional[str] = None,
621
- type: Optional[str] = None,
622
- ) -> List[Dict[str, Any]]:
624
+ folder_id: str | None = None,
625
+ type: str | None = None,
626
+ ) -> list[dict[str, Any]]:
623
627
  """List files with optional filters"""
624
- data: Dict[str, Any] = {"limit": limit}
628
+ data: dict[str, Any] = {"limit": limit}
625
629
  if folder_id is not None:
626
630
  data["folderId"] = folder_id
627
631
  if type is not None:
@@ -629,11 +633,11 @@ class MisskeyAPI:
629
633
 
630
634
  try:
631
635
  logger.debug(
632
- f"[Misskey API] 列表文件请求: limit={limit}, folder_id={folder_id}, type={type}"
636
+ f"[Misskey API] 列表文件请求: limit={limit}, folder_id={folder_id}, type={type}",
633
637
  )
634
638
  result = await self._make_request("drive/files", data)
635
639
  logger.debug(
636
- f"[Misskey API] 列表文件响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件"
640
+ f"[Misskey API] 列表文件响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件",
637
641
  )
638
642
  return result if isinstance(result, list) else []
639
643
  except Exception as e:
@@ -641,27 +645,34 @@ class MisskeyAPI:
641
645
  raise
642
646
 
643
647
  async def _download_with_existing_session(
644
- self, url: str, ssl_verify: bool = True
645
- ) -> Optional[bytes]:
648
+ self,
649
+ url: str,
650
+ ssl_verify: bool = True,
651
+ ) -> bytes | None:
646
652
  """使用现有会话下载文件"""
647
653
  if not (hasattr(self, "session") and self.session):
648
654
  raise APIConnectionError("No existing session available")
649
655
 
650
656
  async with self.session.get(
651
- url, timeout=aiohttp.ClientTimeout(total=15), ssl=ssl_verify
657
+ url,
658
+ timeout=aiohttp.ClientTimeout(total=15),
659
+ ssl=ssl_verify,
652
660
  ) as response:
653
661
  if response.status == 200:
654
662
  return await response.read()
655
663
  return None
656
664
 
657
665
  async def _download_with_temp_session(
658
- self, url: str, ssl_verify: bool = True
659
- ) -> Optional[bytes]:
666
+ self,
667
+ url: str,
668
+ ssl_verify: bool = True,
669
+ ) -> bytes | None:
660
670
  """使用临时会话下载文件"""
661
671
  connector = aiohttp.TCPConnector(ssl=ssl_verify)
662
672
  async with aiohttp.ClientSession(connector=connector) as temp_session:
663
673
  async with temp_session.get(
664
- url, timeout=aiohttp.ClientTimeout(total=15)
674
+ url,
675
+ timeout=aiohttp.ClientTimeout(total=15),
665
676
  ) as response:
666
677
  if response.status == 200:
667
678
  return await response.read()
@@ -670,13 +681,12 @@ class MisskeyAPI:
670
681
  async def upload_and_find_file(
671
682
  self,
672
683
  url: str,
673
- name: Optional[str] = None,
674
- folder_id: Optional[str] = None,
684
+ name: str | None = None,
685
+ folder_id: str | None = None,
675
686
  max_wait_time: float = 30.0,
676
687
  check_interval: float = 2.0,
677
- ) -> Optional[Dict[str, Any]]:
678
- """
679
- 简化的文件上传:尝试 URL 上传,失败则下载后本地上传
688
+ ) -> dict[str, Any] | None:
689
+ """简化的文件上传:尝试 URL 上传,失败则下载后本地上传
680
690
 
681
691
  Args:
682
692
  url: 文件URL
@@ -687,28 +697,31 @@ class MisskeyAPI:
687
697
 
688
698
  Returns:
689
699
  包含文件ID和元信息的字典,失败时返回None
700
+
690
701
  """
691
702
  if not url:
692
703
  raise APIError("URL不能为空")
693
704
 
694
705
  # 通过本地上传获取即时文件 ID(下载文件 → 上传 → 返回 ID)
695
706
  try:
696
- import tempfile
697
707
  import os
708
+ import tempfile
698
709
 
699
710
  # SSL 验证下载,失败则重试不验证 SSL
700
711
  tmp_bytes = None
701
712
  try:
702
713
  tmp_bytes = await self._download_with_existing_session(
703
- url, ssl_verify=True
714
+ url,
715
+ ssl_verify=True,
704
716
  ) or await self._download_with_temp_session(url, ssl_verify=True)
705
717
  except Exception as ssl_error:
706
718
  logger.debug(
707
- f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL"
719
+ f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL",
708
720
  )
709
721
  try:
710
722
  tmp_bytes = await self._download_with_existing_session(
711
- url, ssl_verify=False
723
+ url,
724
+ ssl_verify=False,
712
725
  ) or await self._download_with_temp_session(url, ssl_verify=False)
713
726
  except Exception:
714
727
  pass
@@ -732,13 +745,15 @@ class MisskeyAPI:
732
745
 
733
746
  return None
734
747
 
735
- async def get_current_user(self) -> Dict[str, Any]:
748
+ async def get_current_user(self) -> dict[str, Any]:
736
749
  """获取当前用户信息"""
737
750
  return await self._make_request("i", {})
738
751
 
739
752
  async def send_message(
740
- self, user_id_or_payload: Any, text: Optional[str] = None
741
- ) -> Dict[str, Any]:
753
+ self,
754
+ user_id_or_payload: Any,
755
+ text: str | None = None,
756
+ ) -> dict[str, Any]:
742
757
  """发送聊天消息。
743
758
 
744
759
  Accepts either (user_id: str, text: str) or a single dict payload prepared by caller.
@@ -754,8 +769,10 @@ class MisskeyAPI:
754
769
  return result
755
770
 
756
771
  async def send_room_message(
757
- self, room_id_or_payload: Any, text: Optional[str] = None
758
- ) -> Dict[str, Any]:
772
+ self,
773
+ room_id_or_payload: Any,
774
+ text: str | None = None,
775
+ ) -> dict[str, Any]:
759
776
  """发送房间消息。
760
777
 
761
778
  Accepts either (room_id: str, text: str) or a single dict payload.
@@ -771,10 +788,13 @@ class MisskeyAPI:
771
788
  return result
772
789
 
773
790
  async def get_messages(
774
- self, user_id: str, limit: int = 10, since_id: Optional[str] = None
775
- ) -> List[Dict[str, Any]]:
791
+ self,
792
+ user_id: str,
793
+ limit: int = 10,
794
+ since_id: str | None = None,
795
+ ) -> list[dict[str, Any]]:
776
796
  """获取聊天消息历史"""
777
- data: Dict[str, Any] = {"userId": user_id, "limit": limit}
797
+ data: dict[str, Any] = {"userId": user_id, "limit": limit}
778
798
  if since_id:
779
799
  data["sinceId"] = since_id
780
800
 
@@ -785,10 +805,12 @@ class MisskeyAPI:
785
805
  return []
786
806
 
787
807
  async def get_mentions(
788
- self, limit: int = 10, since_id: Optional[str] = None
789
- ) -> List[Dict[str, Any]]:
808
+ self,
809
+ limit: int = 10,
810
+ since_id: str | None = None,
811
+ ) -> list[dict[str, Any]]:
790
812
  """获取提及通知"""
791
- data: Dict[str, Any] = {"limit": limit}
813
+ data: dict[str, Any] = {"limit": limit}
792
814
  if since_id:
793
815
  data["sinceId"] = since_id
794
816
  data["includeTypes"] = ["mention", "reply", "quote"]
@@ -796,23 +818,21 @@ class MisskeyAPI:
796
818
  result = await self._make_request("i/notifications", data)
797
819
  if isinstance(result, list):
798
820
  return result
799
- elif isinstance(result, dict) and "notifications" in result:
821
+ if isinstance(result, dict) and "notifications" in result:
800
822
  return result["notifications"]
801
- else:
802
- logger.warning(f"[Misskey API] 提及通知响应格式异常: {type(result)}")
803
- return []
823
+ logger.warning(f"[Misskey API] 提及通知响应格式异常: {type(result)}")
824
+ return []
804
825
 
805
826
  async def send_message_with_media(
806
827
  self,
807
828
  message_type: str,
808
829
  target_id: str,
809
- text: Optional[str] = None,
810
- media_urls: Optional[List[str]] = None,
811
- local_files: Optional[List[str]] = None,
830
+ text: str | None = None,
831
+ media_urls: list[str] | None = None,
832
+ local_files: list[str] | None = None,
812
833
  **kwargs,
813
- ) -> Dict[str, Any]:
814
- """
815
- 通用消息发送函数:统一处理文本+媒体发送
834
+ ) -> dict[str, Any]:
835
+ """通用消息发送函数:统一处理文本+媒体发送
816
836
 
817
837
  Args:
818
838
  message_type: 消息类型 ('chat', 'room', 'note')
@@ -827,6 +847,7 @@ class MisskeyAPI:
827
847
 
828
848
  Raises:
829
849
  APIError: 参数错误或发送失败
850
+
830
851
  """
831
852
  if not text and not media_urls and not local_files:
832
853
  raise APIError("消息内容不能为空:需要文本或媒体文件")
@@ -843,10 +864,14 @@ class MisskeyAPI:
843
864
 
844
865
  # 根据消息类型发送
845
866
  return await self._dispatch_message(
846
- message_type, target_id, text, file_ids, **kwargs
867
+ message_type,
868
+ target_id,
869
+ text,
870
+ file_ids,
871
+ **kwargs,
847
872
  )
848
873
 
849
- async def _process_media_urls(self, urls: List[str]) -> List[str]:
874
+ async def _process_media_urls(self, urls: list[str]) -> list[str]:
850
875
  """处理远程媒体文件URL列表,返回文件ID列表"""
851
876
  file_ids = []
852
877
  for url in urls:
@@ -863,7 +888,7 @@ class MisskeyAPI:
863
888
  continue
864
889
  return file_ids
865
890
 
866
- async def _process_local_files(self, file_paths: List[str]) -> List[str]:
891
+ async def _process_local_files(self, file_paths: list[str]) -> list[str]:
867
892
  """处理本地文件路径列表,返回文件ID列表"""
868
893
  file_ids = []
869
894
  for file_path in file_paths:
@@ -883,10 +908,10 @@ class MisskeyAPI:
883
908
  self,
884
909
  message_type: str,
885
910
  target_id: str,
886
- text: Optional[str],
887
- file_ids: List[str],
911
+ text: str | None,
912
+ file_ids: list[str],
888
913
  **kwargs,
889
- ) -> Dict[str, Any]:
914
+ ) -> dict[str, Any]:
890
915
  """根据消息类型分发到对应的发送方法"""
891
916
  if message_type == "chat":
892
917
  # 聊天消息使用 fileId (单数)
@@ -907,7 +932,7 @@ class MisskeyAPI:
907
932
  return {"multiple": True, "results": results}
908
933
  return await self.send_message(payload)
909
934
 
910
- elif message_type == "room":
935
+ if message_type == "room":
911
936
  # 房间消息使用 fileId (单数)
912
937
  payload = {"toRoomId": target_id}
913
938
  if text:
@@ -926,7 +951,7 @@ class MisskeyAPI:
926
951
  return {"multiple": True, "results": results}
927
952
  return await self.send_room_message(payload)
928
953
 
929
- elif message_type == "note":
954
+ if message_type == "note":
930
955
  # 发帖使用 fileIds (复数)
931
956
  note_kwargs = {
932
957
  "text": text,
@@ -936,5 +961,4 @@ class MisskeyAPI:
936
961
  note_kwargs.update(kwargs)
937
962
  return await self.create_note(**note_kwargs)
938
963
 
939
- else:
940
- raise APIError(f"不支持的消息类型: {message_type}")
964
+ raise APIError(f"不支持的消息类型: {message_type}")