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.
- astrbot/core/agent/agent.py +1 -1
- astrbot/core/agent/mcp_client.py +3 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +6 -27
- astrbot/core/agent/tool.py +28 -17
- astrbot/core/config/default.py +50 -14
- astrbot/core/db/sqlite.py +15 -1
- astrbot/core/pipeline/content_safety_check/stage.py +1 -1
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +1 -1
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -1
- astrbot/core/pipeline/context_utils.py +4 -1
- astrbot/core/pipeline/process_stage/method/llm_request.py +23 -4
- astrbot/core/pipeline/process_stage/method/star_request.py +8 -6
- astrbot/core/platform/manager.py +4 -0
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +2 -1
- astrbot/core/platform/sources/misskey/misskey_adapter.py +391 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +404 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +123 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +327 -0
- astrbot/core/platform/sources/satori/satori_adapter.py +290 -24
- astrbot/core/platform/sources/satori/satori_event.py +9 -0
- astrbot/core/platform/sources/telegram/tg_event.py +0 -1
- astrbot/core/provider/entities.py +13 -3
- astrbot/core/provider/func_tool_manager.py +4 -4
- astrbot/core/provider/manager.py +35 -19
- astrbot/core/star/context.py +26 -12
- astrbot/core/star/filter/command.py +3 -4
- astrbot/core/star/filter/command_group.py +4 -4
- astrbot/core/star/filter/platform_adapter_type.py +10 -5
- astrbot/core/star/register/star.py +3 -1
- astrbot/core/star/register/star_handler.py +65 -36
- astrbot/core/star/session_plugin_manager.py +3 -0
- astrbot/core/star/star_handler.py +4 -4
- astrbot/core/star/star_manager.py +10 -4
- astrbot/core/star/star_tools.py +6 -2
- astrbot/core/star/updator.py +3 -0
- {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/METADATA +6 -7
- {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/RECORD +40 -36
- {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/WHEEL +0 -0
- {astrbot-4.1.3.dist-info → astrbot-4.1.5.dist-info}/entry_points.txt +0 -0
- {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
|