AstrBot 4.0.0b5__py3-none-any.whl → 4.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. astrbot/api/event/filter/__init__.py +2 -0
  2. astrbot/core/config/default.py +73 -3
  3. astrbot/core/initial_loader.py +4 -1
  4. astrbot/core/message/components.py +59 -50
  5. astrbot/core/pipeline/result_decorate/stage.py +5 -1
  6. astrbot/core/platform/manager.py +25 -3
  7. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +11 -4
  8. astrbot/core/platform/sources/satori/satori_adapter.py +482 -0
  9. astrbot/core/platform/sources/satori/satori_event.py +221 -0
  10. astrbot/core/platform/sources/telegram/tg_adapter.py +0 -1
  11. astrbot/core/provider/sources/openai_source.py +14 -5
  12. astrbot/core/provider/sources/vllm_rerank_source.py +6 -0
  13. astrbot/core/star/__init__.py +7 -5
  14. astrbot/core/star/filter/command.py +9 -3
  15. astrbot/core/star/filter/platform_adapter_type.py +3 -0
  16. astrbot/core/star/register/__init__.py +2 -0
  17. astrbot/core/star/register/star_handler.py +18 -4
  18. astrbot/core/star/star_handler.py +9 -1
  19. astrbot/core/star/star_tools.py +116 -21
  20. astrbot/core/utils/t2i/network_strategy.py +11 -18
  21. astrbot/core/utils/t2i/renderer.py +8 -2
  22. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  23. astrbot/core/utils/t2i/template_manager.py +112 -0
  24. astrbot/dashboard/routes/chat.py +6 -1
  25. astrbot/dashboard/routes/config.py +10 -49
  26. astrbot/dashboard/routes/route.py +19 -2
  27. astrbot/dashboard/routes/t2i.py +230 -0
  28. astrbot/dashboard/server.py +13 -4
  29. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/METADATA +39 -52
  30. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/RECORD +33 -28
  31. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/WHEEL +0 -0
  32. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/entry_points.txt +0 -0
  33. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ from typing import List
6
6
  from asyncio import Queue
7
7
  from .register import platform_cls_map
8
8
  from astrbot.core import logger
9
+ from astrbot.core.star.star_handler import star_handlers_registry, star_map, EventType
9
10
  from .sources.webchat.webchat_adapter import WebChatAdapter
10
11
 
11
12
 
@@ -66,15 +67,21 @@ class PlatformManager:
66
67
  WeChatPadProAdapter, # noqa: F401
67
68
  )
68
69
  case "lark":
69
- from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401
70
+ from .sources.lark.lark_adapter import (
71
+ LarkPlatformAdapter,
72
+ ) # noqa: F401
70
73
  case "dingtalk":
71
74
  from .sources.dingtalk.dingtalk_adapter import (
72
75
  DingtalkPlatformAdapter, # noqa: F401
73
76
  )
74
77
  case "telegram":
75
- from .sources.telegram.tg_adapter import TelegramPlatformAdapter # noqa: F401
78
+ from .sources.telegram.tg_adapter import (
79
+ TelegramPlatformAdapter,
80
+ ) # noqa: F401
76
81
  case "wecom":
77
- from .sources.wecom.wecom_adapter import WecomPlatformAdapter # noqa: F401
82
+ from .sources.wecom.wecom_adapter import (
83
+ WecomPlatformAdapter,
84
+ ) # noqa: F401
78
85
  case "weixin_official_account":
79
86
  from .sources.weixin_official_account.weixin_offacc_adapter import (
80
87
  WeixinOfficialAccountPlatformAdapter, # noqa
@@ -85,6 +92,10 @@ class PlatformManager:
85
92
  )
86
93
  case "slack":
87
94
  from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
95
+ case "satori":
96
+ from .sources.satori.satori_adapter import (
97
+ SatoriPlatformAdapter,
98
+ ) # noqa: F401
88
99
  except (ImportError, ModuleNotFoundError) as e:
89
100
  logger.error(
90
101
  f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
@@ -113,6 +124,17 @@ class PlatformManager:
113
124
  )
114
125
  )
115
126
  )
127
+ handlers = star_handlers_registry.get_handlers_by_event_type(
128
+ EventType.OnPlatformLoadedEvent
129
+ )
130
+ for handler in handlers:
131
+ try:
132
+ logger.info(
133
+ f"hook(on_platform_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
134
+ )
135
+ await handler.handler()
136
+ except Exception:
137
+ logger.error(traceback.format_exc())
116
138
 
117
139
  async def _task_wrapper(self, task: asyncio.Task):
118
140
  try:
@@ -308,13 +308,20 @@ class AiocqhttpAdapter(Platform):
308
308
  continue
309
309
 
310
310
  at_info = await self.bot.call_action(
311
- action="get_stranger_info",
311
+ action="get_group_member_info",
312
+ group_id=event.group_id,
312
313
  user_id=int(m["data"]["qq"]),
314
+ no_cache=False,
313
315
  )
314
316
  if at_info:
315
- nickname = at_info.get("nick", "") or at_info.get(
316
- "nickname", ""
317
- )
317
+ nickname = at_info.get("card", "")
318
+ if nickname == "":
319
+ at_info = await self.bot.call_action(
320
+ action="get_stranger_info",
321
+ user_id=int(m["data"]["qq"]),
322
+ no_cache=False,
323
+ )
324
+ nickname = at_info.get("nick", "") or at_info.get("nickname", "")
318
325
  is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
319
326
 
320
327
  abm.message.append(
@@ -0,0 +1,482 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ import websockets
5
+ from websockets.asyncio.client import connect
6
+ from typing import Optional
7
+ from aiohttp import ClientSession, ClientTimeout
8
+ from websockets.asyncio.client import ClientConnection
9
+ from astrbot.api import logger
10
+ from astrbot.api.event import MessageChain
11
+ from astrbot.api.platform import (
12
+ AstrBotMessage,
13
+ MessageMember,
14
+ MessageType,
15
+ Platform,
16
+ PlatformMetadata,
17
+ register_platform_adapter,
18
+ )
19
+ from astrbot.core.platform.astr_message_event import MessageSession
20
+ from astrbot.api.message_components import Plain, Image, At, File, Record
21
+ from xml.etree import ElementTree as ET
22
+
23
+
24
+ @register_platform_adapter(
25
+ "satori",
26
+ "Satori 协议适配器",
27
+ )
28
+ class SatoriPlatformAdapter(Platform):
29
+ def __init__(
30
+ self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
31
+ ) -> None:
32
+ super().__init__(event_queue)
33
+ self.config = platform_config
34
+ self.settings = platform_settings
35
+
36
+ self.api_base_url = self.config.get(
37
+ "satori_api_base_url", "http://localhost:5140/satori/v1"
38
+ )
39
+ self.token = self.config.get("satori_token", "")
40
+ self.endpoint = self.config.get(
41
+ "satori_endpoint", "ws://127.0.0.1:5140/satori/v1/events"
42
+ )
43
+ self.auto_reconnect = self.config.get("satori_auto_reconnect", True)
44
+ self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10)
45
+ self.reconnect_delay = self.config.get("satori_reconnect_delay", 5)
46
+
47
+ self.ws: Optional[ClientConnection] = None
48
+ self.session: Optional[ClientSession] = None
49
+ self.sequence = 0
50
+ self.logins = []
51
+ self.running = False
52
+ self.heartbeat_task: Optional[asyncio.Task] = None
53
+ self.ready_received = False
54
+
55
+ async def send_by_session(
56
+ self, session: MessageSession, message_chain: MessageChain
57
+ ):
58
+ from .satori_event import SatoriPlatformEvent
59
+
60
+ await SatoriPlatformEvent.send_with_adapter(
61
+ self, message_chain, session.session_id
62
+ )
63
+ await super().send_by_session(session, message_chain)
64
+
65
+ def meta(self) -> PlatformMetadata:
66
+ return PlatformMetadata(name="satori", description="Satori 通用协议适配器")
67
+
68
+ def _is_websocket_closed(self, ws) -> bool:
69
+ """检查WebSocket连接是否已关闭"""
70
+ if not ws:
71
+ return True
72
+ try:
73
+ if hasattr(ws, "closed"):
74
+ return ws.closed
75
+ elif hasattr(ws, "close_code"):
76
+ return ws.close_code is not None
77
+ else:
78
+ return False
79
+ except AttributeError:
80
+ return False
81
+
82
+ async def run(self):
83
+ self.running = True
84
+ self.session = ClientSession(timeout=ClientTimeout(total=30))
85
+
86
+ retry_count = 0
87
+ max_retries = 10
88
+
89
+ while self.running:
90
+ try:
91
+ await self.connect_websocket()
92
+ retry_count = 0
93
+ except websockets.exceptions.ConnectionClosed as e:
94
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
95
+ retry_count += 1
96
+ except Exception as e:
97
+ logger.error(f"Satori WebSocket 连接失败: {e}")
98
+ retry_count += 1
99
+
100
+ if not self.running:
101
+ break
102
+
103
+ if retry_count >= max_retries:
104
+ logger.error(f"达到最大重试次数 ({max_retries}),停止重试")
105
+ break
106
+
107
+ if not self.auto_reconnect:
108
+ break
109
+
110
+ delay = min(self.reconnect_delay * (2 ** (retry_count - 1)), 60)
111
+ await asyncio.sleep(delay)
112
+
113
+ if self.session:
114
+ await self.session.close()
115
+
116
+ async def connect_websocket(self):
117
+ logger.info(f"Satori 适配器正在连接到 WebSocket: {self.endpoint}")
118
+ logger.info(f"Satori 适配器 HTTP API 地址: {self.api_base_url}")
119
+
120
+ if not self.endpoint.startswith(("ws://", "wss://")):
121
+ logger.error(f"无效的WebSocket URL: {self.endpoint}")
122
+ raise ValueError(f"WebSocket URL必须以ws://或wss://开头: {self.endpoint}")
123
+
124
+ try:
125
+ websocket = await connect(self.endpoint, additional_headers={})
126
+ self.ws = websocket
127
+
128
+ await asyncio.sleep(0.1)
129
+
130
+ await self.send_identify()
131
+
132
+ self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())
133
+
134
+ async for message in websocket:
135
+ try:
136
+ await self.handle_message(message) # type: ignore
137
+ except Exception as e:
138
+ logger.error(f"Satori 处理消息异常: {e}")
139
+
140
+ except websockets.exceptions.ConnectionClosed as e:
141
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
142
+ raise
143
+ except Exception as e:
144
+ logger.error(f"Satori WebSocket 连接异常: {e}")
145
+ raise
146
+ finally:
147
+ if self.heartbeat_task:
148
+ self.heartbeat_task.cancel()
149
+ try:
150
+ await self.heartbeat_task
151
+ except asyncio.CancelledError:
152
+ pass
153
+ if self.ws:
154
+ try:
155
+ await self.ws.close()
156
+ except Exception as e:
157
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
158
+
159
+ async def send_identify(self):
160
+ if not self.ws:
161
+ raise Exception("WebSocket连接未建立")
162
+
163
+ if self._is_websocket_closed(self.ws):
164
+ raise Exception("WebSocket连接已关闭")
165
+
166
+ identify_payload = {
167
+ "op": 3, # IDENTIFY
168
+ "body": {
169
+ "token": str(self.token) if self.token else "", # 字符串
170
+ },
171
+ }
172
+
173
+ # 只有在有序列号时才添加sn字段
174
+ if self.sequence > 0:
175
+ identify_payload["body"]["sn"] = self.sequence
176
+
177
+ try:
178
+ message_str = json.dumps(identify_payload, ensure_ascii=False)
179
+ await self.ws.send(message_str)
180
+ except websockets.exceptions.ConnectionClosed as e:
181
+ logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}")
182
+ raise
183
+ except Exception as e:
184
+ logger.error(f"发送 IDENTIFY 信令失败: {e}")
185
+ raise
186
+
187
+ async def heartbeat_loop(self):
188
+ try:
189
+ while self.running and self.ws:
190
+ await asyncio.sleep(self.heartbeat_interval)
191
+
192
+ if self.ws and not self._is_websocket_closed(self.ws):
193
+ try:
194
+ ping_payload = {
195
+ "op": 1, # PING
196
+ "body": {},
197
+ }
198
+ await self.ws.send(json.dumps(ping_payload, ensure_ascii=False))
199
+ except websockets.exceptions.ConnectionClosed as e:
200
+ logger.error(f"Satori WebSocket 连接关闭: {e}")
201
+ break
202
+ except Exception as e:
203
+ logger.error(f"Satori WebSocket 发送心跳失败: {e}")
204
+ break
205
+ else:
206
+ break
207
+ except asyncio.CancelledError:
208
+ pass
209
+ except Exception as e:
210
+ logger.error(f"心跳任务异常: {e}")
211
+
212
+ async def handle_message(self, message: str):
213
+ try:
214
+ data = json.loads(message)
215
+ op = data.get("op")
216
+ body = data.get("body", {})
217
+
218
+ if op == 4: # READY
219
+ self.logins = body.get("logins", [])
220
+ self.ready_received = True
221
+
222
+ # 输出连接成功的bot信息
223
+ if self.logins:
224
+ for i, login in enumerate(self.logins):
225
+ platform = login.get("platform", "")
226
+ user = login.get("user", {})
227
+ user_id = user.get("id", "")
228
+ user_name = user.get("name", "")
229
+ logger.info(
230
+ f"Satori 连接成功 - Bot {i + 1}: platform={platform}, user_id={user_id}, user_name={user_name}"
231
+ )
232
+
233
+ if "sn" in body:
234
+ self.sequence = body["sn"]
235
+
236
+ elif op == 2: # PONG
237
+ pass
238
+
239
+ elif op == 0: # EVENT
240
+ await self.handle_event(body)
241
+ if "sn" in body:
242
+ self.sequence = body["sn"]
243
+
244
+ elif op == 5: # META
245
+ if "sn" in body:
246
+ self.sequence = body["sn"]
247
+
248
+ except json.JSONDecodeError as e:
249
+ logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}")
250
+ except Exception as e:
251
+ logger.error(f"处理 WebSocket 消息异常: {e}")
252
+
253
+ async def handle_event(self, event_data: dict):
254
+ try:
255
+ event_type = event_data.get("type")
256
+ sn = event_data.get("sn")
257
+ if sn:
258
+ self.sequence = sn
259
+
260
+ if event_type == "message-created":
261
+ message = event_data.get("message", {})
262
+ user = event_data.get("user", {})
263
+ channel = event_data.get("channel", {})
264
+ guild = event_data.get("guild")
265
+ login = event_data.get("login", {})
266
+ timestamp = event_data.get("timestamp")
267
+
268
+ if user.get("id") == login.get("user", {}).get("id"):
269
+ return
270
+
271
+ abm = await self.convert_satori_message(
272
+ message, user, channel, guild, login, timestamp
273
+ )
274
+ if abm:
275
+ await self.handle_msg(abm)
276
+
277
+ except Exception as e:
278
+ logger.error(f"处理事件失败: {e}")
279
+
280
+ async def convert_satori_message(
281
+ self,
282
+ message: dict,
283
+ user: dict,
284
+ channel: dict,
285
+ guild: Optional[dict],
286
+ login: dict,
287
+ timestamp: Optional[int] = None,
288
+ ) -> Optional[AstrBotMessage]:
289
+ try:
290
+ abm = AstrBotMessage()
291
+ abm.message_id = message.get("id", "")
292
+ abm.raw_message = {
293
+ "message": message,
294
+ "user": user,
295
+ "channel": channel,
296
+ "guild": guild,
297
+ "login": login,
298
+ }
299
+
300
+ if guild and guild.get("id"):
301
+ abm.type = MessageType.GROUP_MESSAGE
302
+ abm.group_id = guild.get("id", "")
303
+ abm.session_id = channel.get("id", "")
304
+ else:
305
+ abm.type = MessageType.FRIEND_MESSAGE
306
+ abm.session_id = channel.get("id", "")
307
+
308
+ abm.sender = MessageMember(
309
+ user_id=user.get("id", ""),
310
+ nickname=user.get("nick", user.get("name", "")),
311
+ )
312
+
313
+ abm.self_id = login.get("user", {}).get("id", "")
314
+
315
+ content = message.get("content", "")
316
+ abm.message = await self.parse_satori_elements(content)
317
+
318
+ # parse message_str
319
+ abm.message_str = ""
320
+ for comp in abm.message:
321
+ if isinstance(comp, Plain):
322
+ abm.message_str += comp.text
323
+
324
+ # 优先使用Satori事件中的时间戳
325
+ if timestamp is not None:
326
+ abm.timestamp = timestamp
327
+ else:
328
+ abm.timestamp = int(time.time())
329
+
330
+ return abm
331
+
332
+ except Exception as e:
333
+ logger.error(f"转换 Satori 消息失败: {e}")
334
+ return None
335
+
336
+ async def parse_satori_elements(self, content: str) -> list:
337
+ """解析 Satori 消息元素"""
338
+ elements = []
339
+
340
+ if not content:
341
+ return elements
342
+
343
+ try:
344
+ wrapped_content = f"<root>{content}</root>"
345
+ root = ET.fromstring(wrapped_content)
346
+ await self._parse_xml_node(root, elements)
347
+ except ET.ParseError as e:
348
+ raise ValueError(f"解析 Satori 元素时发生解析错误: {e}")
349
+ except Exception as e:
350
+ raise e
351
+
352
+ # 如果没有解析到任何元素,将整个内容当作纯文本
353
+ if not elements and content.strip():
354
+ elements.append(Plain(text=content))
355
+
356
+ return elements
357
+
358
+ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None:
359
+ """递归解析 XML 节点"""
360
+ if node.text and node.text.strip():
361
+ elements.append(Plain(text=node.text))
362
+
363
+ for child in node:
364
+ tag_name = child.tag.lower()
365
+ attrs = child.attrib
366
+
367
+ if tag_name == "at":
368
+ user_id = attrs.get("id") or attrs.get("name", "")
369
+ elements.append(At(qq=user_id, name=user_id))
370
+
371
+ elif tag_name in ("img", "image"):
372
+ src = attrs.get("src", "")
373
+ if not src:
374
+ continue
375
+ if src.startswith("data:image/"):
376
+ src = src.split(",")[1]
377
+ elements.append(Image.fromBase64(src))
378
+ elif src.startswith("http"):
379
+ elements.append(Image.fromURL(src))
380
+ else:
381
+ logger.error(f"未知的图片 src 格式: {str(src)[:16]}")
382
+
383
+ elif tag_name == "file":
384
+ src = attrs.get("src", "")
385
+ name = attrs.get("name", "文件")
386
+ if src:
387
+ elements.append(File(file=src, name=name))
388
+
389
+ elif tag_name in ("audio", "record"):
390
+ src = attrs.get("src", "")
391
+ if not src:
392
+ continue
393
+ if src.startswith("data:audio/"):
394
+ src = src.split(",")[1]
395
+ elements.append(Record.fromBase64(src))
396
+ elif src.startswith("http"):
397
+ elements.append(Record.fromURL(src))
398
+ else:
399
+ logger.error(f"未知的音频 src 格式: {str(src)[:16]}")
400
+
401
+ else:
402
+ # 未知标签,递归处理其内容
403
+ if child.text and child.text.strip():
404
+ elements.append(Plain(text=child.text))
405
+ await self._parse_xml_node(child, elements)
406
+
407
+ # 处理标签后的文本
408
+ if child.tail and child.tail.strip():
409
+ elements.append(Plain(text=child.tail))
410
+
411
+ async def handle_msg(self, message: AstrBotMessage):
412
+ from .satori_event import SatoriPlatformEvent
413
+
414
+ message_event = SatoriPlatformEvent(
415
+ message_str=message.message_str,
416
+ message_obj=message,
417
+ platform_meta=self.meta(),
418
+ session_id=message.session_id,
419
+ adapter=self,
420
+ )
421
+ self.commit_event(message_event)
422
+
423
+ async def send_http_request(
424
+ self,
425
+ method: str,
426
+ path: str,
427
+ data: dict | None = None,
428
+ platform: str | None = None,
429
+ user_id: str | None = None,
430
+ ) -> dict:
431
+ if not self.session:
432
+ raise Exception("HTTP session 未初始化")
433
+
434
+ headers = {
435
+ "Content-Type": "application/json",
436
+ }
437
+
438
+ if self.token:
439
+ headers["Authorization"] = f"Bearer {self.token}"
440
+
441
+ if platform and user_id:
442
+ headers["satori-platform"] = platform
443
+ headers["satori-user-id"] = user_id
444
+ elif self.logins:
445
+ current_login = self.logins[0]
446
+ headers["satori-platform"] = current_login.get("platform", "")
447
+ user = current_login.get("user", {})
448
+ headers["satori-user-id"] = user.get("id", "") if user else ""
449
+
450
+ if not path.startswith("/"):
451
+ path = "/" + path
452
+
453
+ # 使用新的API地址配置
454
+ url = f"{self.api_base_url.rstrip('/')}{path}"
455
+
456
+ try:
457
+ async with self.session.request(
458
+ method, url, json=data, headers=headers
459
+ ) as response:
460
+ if response.status == 200:
461
+ result = await response.json()
462
+ return result
463
+ else:
464
+ return {}
465
+ except Exception as e:
466
+ logger.error(f"Satori HTTP 请求异常: {e}")
467
+ return {}
468
+
469
+ async def terminate(self):
470
+ self.running = False
471
+
472
+ if self.heartbeat_task:
473
+ self.heartbeat_task.cancel()
474
+
475
+ if self.ws:
476
+ try:
477
+ await self.ws.close()
478
+ except Exception as e:
479
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
480
+
481
+ if self.session:
482
+ await self.session.close()