AstrBot 4.1.3__py3-none-any.whl → 4.1.5__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 (40) hide show
  1. astrbot/core/agent/agent.py +1 -1
  2. astrbot/core/agent/mcp_client.py +3 -1
  3. astrbot/core/agent/runners/tool_loop_agent_runner.py +6 -27
  4. astrbot/core/agent/tool.py +28 -17
  5. astrbot/core/config/default.py +50 -14
  6. astrbot/core/db/sqlite.py +15 -1
  7. astrbot/core/pipeline/content_safety_check/stage.py +1 -1
  8. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +1 -1
  9. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -1
  10. astrbot/core/pipeline/context_utils.py +4 -1
  11. astrbot/core/pipeline/process_stage/method/llm_request.py +23 -4
  12. astrbot/core/pipeline/process_stage/method/star_request.py +8 -6
  13. astrbot/core/platform/manager.py +4 -0
  14. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +2 -1
  15. astrbot/core/platform/sources/misskey/misskey_adapter.py +391 -0
  16. astrbot/core/platform/sources/misskey/misskey_api.py +404 -0
  17. astrbot/core/platform/sources/misskey/misskey_event.py +123 -0
  18. astrbot/core/platform/sources/misskey/misskey_utils.py +327 -0
  19. astrbot/core/platform/sources/satori/satori_adapter.py +290 -24
  20. astrbot/core/platform/sources/satori/satori_event.py +9 -0
  21. astrbot/core/platform/sources/telegram/tg_event.py +0 -1
  22. astrbot/core/provider/entities.py +13 -3
  23. astrbot/core/provider/func_tool_manager.py +4 -4
  24. astrbot/core/provider/manager.py +35 -19
  25. astrbot/core/star/context.py +26 -12
  26. astrbot/core/star/filter/command.py +3 -4
  27. astrbot/core/star/filter/command_group.py +4 -4
  28. astrbot/core/star/filter/platform_adapter_type.py +10 -5
  29. astrbot/core/star/register/star.py +3 -1
  30. astrbot/core/star/register/star_handler.py +65 -36
  31. astrbot/core/star/session_plugin_manager.py +3 -0
  32. astrbot/core/star/star_handler.py +4 -4
  33. astrbot/core/star/star_manager.py +10 -4
  34. astrbot/core/star/star_tools.py +6 -2
  35. astrbot/core/star/updator.py +3 -0
  36. {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/METADATA +6 -7
  37. {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/RECORD +40 -36
  38. {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/WHEEL +0 -0
  39. {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/entry_points.txt +0 -0
  40. {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,391 @@
1
+ import asyncio
2
+ import json
3
+ from typing import Dict, Any, Optional, Awaitable
4
+
5
+ from astrbot.api import logger
6
+ from astrbot.api.event import MessageChain
7
+ from astrbot.api.platform import (
8
+ AstrBotMessage,
9
+ Platform,
10
+ PlatformMetadata,
11
+ register_platform_adapter,
12
+ )
13
+ from astrbot.core.platform.astr_message_event import MessageSession
14
+ import astrbot.api.message_components as Comp
15
+
16
+ from .misskey_api import MisskeyAPI
17
+ from .misskey_event import MisskeyPlatformEvent
18
+ from .misskey_utils import (
19
+ serialize_message_chain,
20
+ resolve_message_visibility,
21
+ is_valid_user_session_id,
22
+ is_valid_room_session_id,
23
+ add_at_mention_if_needed,
24
+ process_files,
25
+ extract_sender_info,
26
+ create_base_message,
27
+ process_at_mention,
28
+ cache_user_info,
29
+ cache_room_info,
30
+ )
31
+
32
+
33
+ @register_platform_adapter("misskey", "Misskey 平台适配器")
34
+ class MisskeyPlatformAdapter(Platform):
35
+ def __init__(
36
+ self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
37
+ ) -> None:
38
+ super().__init__(event_queue)
39
+ self.config = platform_config or {}
40
+ self.settings = platform_settings or {}
41
+ self.instance_url = self.config.get("misskey_instance_url", "")
42
+ self.access_token = self.config.get("misskey_token", "")
43
+ self.max_message_length = self.config.get("max_message_length", 3000)
44
+ self.default_visibility = self.config.get(
45
+ "misskey_default_visibility", "public"
46
+ )
47
+ self.local_only = self.config.get("misskey_local_only", False)
48
+ self.enable_chat = self.config.get("misskey_enable_chat", True)
49
+
50
+ self.unique_session = platform_settings["unique_session"]
51
+
52
+ self.api: Optional[MisskeyAPI] = None
53
+ self._running = False
54
+ self.client_self_id = ""
55
+ self._bot_username = ""
56
+ self._user_cache = {}
57
+
58
+ def meta(self) -> PlatformMetadata:
59
+ default_config = {
60
+ "misskey_instance_url": "",
61
+ "misskey_token": "",
62
+ "max_message_length": 3000,
63
+ "misskey_default_visibility": "public",
64
+ "misskey_local_only": False,
65
+ "misskey_enable_chat": True,
66
+ }
67
+ default_config.update(self.config)
68
+
69
+ return PlatformMetadata(
70
+ name="misskey",
71
+ description="Misskey 平台适配器",
72
+ id=self.config.get("id", "misskey"),
73
+ default_config_tmpl=default_config,
74
+ )
75
+
76
+ async def run(self):
77
+ if not self.instance_url or not self.access_token:
78
+ logger.error("[Misskey] 配置不完整,无法启动")
79
+ return
80
+
81
+ self.api = MisskeyAPI(self.instance_url, self.access_token)
82
+ self._running = True
83
+
84
+ try:
85
+ user_info = await self.api.get_current_user()
86
+ self.client_self_id = str(user_info.get("id", ""))
87
+ self._bot_username = user_info.get("username", "")
88
+ logger.info(
89
+ f"[Misskey] 已连接用户: {self._bot_username} (ID: {self.client_self_id})"
90
+ )
91
+ except Exception as e:
92
+ logger.error(f"[Misskey] 获取用户信息失败: {e}")
93
+ self._running = False
94
+ return
95
+
96
+ await self._start_websocket_connection()
97
+
98
+ async def _start_websocket_connection(self):
99
+ backoff_delay = 1.0
100
+ max_backoff = 300.0
101
+ backoff_multiplier = 1.5
102
+ connection_attempts = 0
103
+
104
+ while self._running:
105
+ try:
106
+ connection_attempts += 1
107
+ if not self.api:
108
+ logger.error("[Misskey] API 客户端未初始化")
109
+ break
110
+
111
+ streaming = self.api.get_streaming_client()
112
+ streaming.add_message_handler("notification", self._handle_notification)
113
+ if self.enable_chat:
114
+ streaming.add_message_handler(
115
+ "newChatMessage", self._handle_chat_message
116
+ )
117
+ streaming.add_message_handler("_debug", self._debug_handler)
118
+
119
+ if await streaming.connect():
120
+ logger.info(
121
+ f"[Misskey] WebSocket 已连接 (尝试 #{connection_attempts})"
122
+ )
123
+ connection_attempts = 0 # 重置计数器
124
+ await streaming.subscribe_channel("main")
125
+ if self.enable_chat:
126
+ await streaming.subscribe_channel("messaging")
127
+ await streaming.subscribe_channel("messagingIndex")
128
+ logger.info("[Misskey] 聊天频道已订阅")
129
+
130
+ backoff_delay = 1.0 # 重置延迟
131
+ await streaming.listen()
132
+ else:
133
+ logger.error(
134
+ f"[Misskey] WebSocket 连接失败 (尝试 #{connection_attempts})"
135
+ )
136
+
137
+ except Exception as e:
138
+ logger.error(
139
+ f"[Misskey] WebSocket 异常 (尝试 #{connection_attempts}): {e}"
140
+ )
141
+
142
+ if self._running:
143
+ logger.info(
144
+ f"[Misskey] {backoff_delay:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})"
145
+ )
146
+ await asyncio.sleep(backoff_delay)
147
+ backoff_delay = min(backoff_delay * backoff_multiplier, max_backoff)
148
+
149
+ async def _handle_notification(self, data: Dict[str, Any]):
150
+ try:
151
+ logger.debug(
152
+ f"[Misskey] 收到通知事件:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
153
+ )
154
+ notification_type = data.get("type")
155
+ if notification_type in ["mention", "reply", "quote"]:
156
+ note = data.get("note")
157
+ if note and self._is_bot_mentioned(note):
158
+ logger.info(
159
+ f"[Misskey] 处理贴文提及: {note.get('text', '')[:50]}..."
160
+ )
161
+ message = await self.convert_message(note)
162
+ event = MisskeyPlatformEvent(
163
+ message_str=message.message_str,
164
+ message_obj=message,
165
+ platform_meta=self.meta(),
166
+ session_id=message.session_id,
167
+ client=self.api,
168
+ )
169
+ self.commit_event(event)
170
+ except Exception as e:
171
+ logger.error(f"[Misskey] 处理通知失败: {e}")
172
+
173
+ async def _handle_chat_message(self, data: Dict[str, Any]):
174
+ try:
175
+ logger.debug(
176
+ f"[Misskey] 收到聊天事件数据:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
177
+ )
178
+
179
+ sender_id = str(
180
+ data.get("fromUserId", "") or data.get("fromUser", {}).get("id", "")
181
+ )
182
+ if sender_id == self.client_self_id:
183
+ return
184
+
185
+ room_id = data.get("toRoomId")
186
+ if room_id:
187
+ raw_text = data.get("text", "")
188
+ logger.debug(
189
+ f"[Misskey] 检查群聊消息: '{raw_text}', 机器人用户名: '{self._bot_username}'"
190
+ )
191
+
192
+ message = await self.convert_room_message(data)
193
+ logger.info(f"[Misskey] 处理群聊消息: {message.message_str[:50]}...")
194
+ else:
195
+ message = await self.convert_chat_message(data)
196
+ logger.info(f"[Misskey] 处理私聊消息: {message.message_str[:50]}...")
197
+
198
+ event = MisskeyPlatformEvent(
199
+ message_str=message.message_str,
200
+ message_obj=message,
201
+ platform_meta=self.meta(),
202
+ session_id=message.session_id,
203
+ client=self.api,
204
+ )
205
+ self.commit_event(event)
206
+ except Exception as e:
207
+ logger.error(f"[Misskey] 处理聊天消息失败: {e}")
208
+
209
+ async def _debug_handler(self, data: Dict[str, Any]):
210
+ logger.debug(
211
+ f"[Misskey] 收到未处理事件:\n{json.dumps(data, indent=2, ensure_ascii=False)}"
212
+ )
213
+
214
+ def _is_bot_mentioned(self, note: Dict[str, Any]) -> bool:
215
+ text = note.get("text", "")
216
+ if not text:
217
+ return False
218
+
219
+ mentions = note.get("mentions", [])
220
+ if self._bot_username and f"@{self._bot_username}" in text:
221
+ return True
222
+ if self.client_self_id in [str(uid) for uid in mentions]:
223
+ return True
224
+
225
+ reply = note.get("reply")
226
+ if reply and isinstance(reply, dict):
227
+ reply_user_id = str(reply.get("user", {}).get("id", ""))
228
+ if reply_user_id == self.client_self_id:
229
+ return bool(self._bot_username and f"@{self._bot_username}" in text)
230
+
231
+ return False
232
+
233
+ async def send_by_session(
234
+ self, session: MessageSession, message_chain: MessageChain
235
+ ) -> Awaitable[Any]:
236
+ if not self.api:
237
+ logger.error("[Misskey] API 客户端未初始化")
238
+ return await super().send_by_session(session, message_chain)
239
+
240
+ try:
241
+ session_id = session.session_id
242
+ text, has_at_user = serialize_message_chain(message_chain.chain)
243
+
244
+ if not has_at_user and session_id:
245
+ user_info = self._user_cache.get(session_id)
246
+ text = add_at_mention_if_needed(text, user_info, has_at_user)
247
+
248
+ if not text or not text.strip():
249
+ logger.warning("[Misskey] 消息内容为空,跳过发送")
250
+ return await super().send_by_session(session, message_chain)
251
+
252
+ if len(text) > self.max_message_length:
253
+ text = text[: self.max_message_length] + "..."
254
+
255
+ if session_id and is_valid_user_session_id(session_id):
256
+ from .misskey_utils import extract_user_id_from_session_id
257
+
258
+ user_id = extract_user_id_from_session_id(session_id)
259
+ await self.api.send_message(user_id, text)
260
+ elif session_id and is_valid_room_session_id(session_id):
261
+ from .misskey_utils import extract_room_id_from_session_id
262
+
263
+ room_id = extract_room_id_from_session_id(session_id)
264
+ await self.api.send_room_message(room_id, text)
265
+ else:
266
+ visibility, visible_user_ids = resolve_message_visibility(
267
+ user_id=session_id,
268
+ user_cache=self._user_cache,
269
+ self_id=self.client_self_id,
270
+ default_visibility=self.default_visibility,
271
+ )
272
+
273
+ await self.api.create_note(
274
+ text,
275
+ visibility=visibility,
276
+ visible_user_ids=visible_user_ids,
277
+ local_only=self.local_only,
278
+ )
279
+
280
+ except Exception as e:
281
+ logger.error(f"[Misskey] 发送消息失败: {e}")
282
+
283
+ return await super().send_by_session(session, message_chain)
284
+
285
+ async def convert_message(self, raw_data: Dict[str, Any]) -> AstrBotMessage:
286
+ """将 Misskey 贴文数据转换为 AstrBotMessage 对象"""
287
+ sender_info = extract_sender_info(raw_data, is_chat=False)
288
+ message = create_base_message(
289
+ raw_data,
290
+ sender_info,
291
+ self.client_self_id,
292
+ is_chat=False,
293
+ unique_session=self.unique_session,
294
+ )
295
+ cache_user_info(
296
+ self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=False
297
+ )
298
+
299
+ message_parts = []
300
+ raw_text = raw_data.get("text", "")
301
+
302
+ if raw_text:
303
+ text_parts, processed_text = process_at_mention(
304
+ message, raw_text, self._bot_username, self.client_self_id
305
+ )
306
+ message_parts.extend(text_parts)
307
+
308
+ files = raw_data.get("files", [])
309
+ file_parts = process_files(message, files)
310
+ message_parts.extend(file_parts)
311
+
312
+ message.message_str = (
313
+ " ".join(part for part in message_parts if part.strip())
314
+ if message_parts
315
+ else ""
316
+ )
317
+ return message
318
+
319
+ async def convert_chat_message(self, raw_data: Dict[str, Any]) -> AstrBotMessage:
320
+ """将 Misskey 聊天消息数据转换为 AstrBotMessage 对象"""
321
+ sender_info = extract_sender_info(raw_data, is_chat=True)
322
+ message = create_base_message(
323
+ raw_data,
324
+ sender_info,
325
+ self.client_self_id,
326
+ is_chat=True,
327
+ unique_session=self.unique_session,
328
+ )
329
+ cache_user_info(
330
+ self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=True
331
+ )
332
+
333
+ raw_text = raw_data.get("text", "")
334
+ if raw_text:
335
+ message.message.append(Comp.Plain(raw_text))
336
+
337
+ files = raw_data.get("files", [])
338
+ process_files(message, files, include_text_parts=False)
339
+
340
+ message.message_str = raw_text if raw_text else ""
341
+ return message
342
+
343
+ async def convert_room_message(self, raw_data: Dict[str, Any]) -> AstrBotMessage:
344
+ """将 Misskey 群聊消息数据转换为 AstrBotMessage 对象"""
345
+ sender_info = extract_sender_info(raw_data, is_chat=True)
346
+ room_id = raw_data.get("toRoomId", "")
347
+ message = create_base_message(
348
+ raw_data,
349
+ sender_info,
350
+ self.client_self_id,
351
+ is_chat=False,
352
+ room_id=room_id,
353
+ unique_session=self.unique_session,
354
+ )
355
+
356
+ cache_user_info(
357
+ self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=False
358
+ )
359
+ cache_room_info(self._user_cache, raw_data, self.client_self_id)
360
+
361
+ raw_text = raw_data.get("text", "")
362
+ message_parts = []
363
+
364
+ if raw_text:
365
+ if self._bot_username and f"@{self._bot_username}" in raw_text:
366
+ text_parts, processed_text = process_at_mention(
367
+ message, raw_text, self._bot_username, self.client_self_id
368
+ )
369
+ message_parts.extend(text_parts)
370
+ else:
371
+ message.message.append(Comp.Plain(raw_text))
372
+ message_parts.append(raw_text)
373
+
374
+ files = raw_data.get("files", [])
375
+ file_parts = process_files(message, files)
376
+ message_parts.extend(file_parts)
377
+
378
+ message.message_str = (
379
+ " ".join(part for part in message_parts if part.strip())
380
+ if message_parts
381
+ else ""
382
+ )
383
+ return message
384
+
385
+ async def terminate(self):
386
+ self._running = False
387
+ if self.api:
388
+ await self.api.close()
389
+
390
+ def get_client(self) -> Any:
391
+ return self.api