AstrBot 4.0.0b4__py3-none-any.whl → 4.1.0__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 (43) hide show
  1. astrbot/api/event/filter/__init__.py +2 -0
  2. astrbot/cli/utils/basic.py +12 -3
  3. astrbot/core/astrbot_config_mgr.py +16 -9
  4. astrbot/core/config/default.py +82 -4
  5. astrbot/core/initial_loader.py +4 -1
  6. astrbot/core/message/components.py +59 -50
  7. astrbot/core/pipeline/process_stage/method/llm_request.py +6 -2
  8. astrbot/core/pipeline/result_decorate/stage.py +5 -1
  9. astrbot/core/platform/manager.py +25 -3
  10. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +26 -14
  11. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +11 -4
  12. astrbot/core/platform/sources/satori/satori_adapter.py +482 -0
  13. astrbot/core/platform/sources/satori/satori_event.py +221 -0
  14. astrbot/core/platform/sources/telegram/tg_adapter.py +0 -1
  15. astrbot/core/provider/entities.py +17 -15
  16. astrbot/core/provider/sources/gemini_source.py +57 -18
  17. astrbot/core/provider/sources/openai_source.py +12 -5
  18. astrbot/core/provider/sources/vllm_rerank_source.py +6 -0
  19. astrbot/core/star/__init__.py +7 -5
  20. astrbot/core/star/filter/command.py +9 -3
  21. astrbot/core/star/filter/platform_adapter_type.py +3 -0
  22. astrbot/core/star/register/__init__.py +2 -0
  23. astrbot/core/star/register/star_handler.py +18 -4
  24. astrbot/core/star/star_handler.py +9 -1
  25. astrbot/core/star/star_tools.py +116 -21
  26. astrbot/core/updator.py +7 -5
  27. astrbot/core/utils/io.py +1 -1
  28. astrbot/core/utils/t2i/network_strategy.py +11 -18
  29. astrbot/core/utils/t2i/renderer.py +8 -2
  30. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  31. astrbot/core/utils/t2i/template_manager.py +112 -0
  32. astrbot/core/zip_updator.py +26 -4
  33. astrbot/dashboard/routes/chat.py +6 -1
  34. astrbot/dashboard/routes/config.py +24 -49
  35. astrbot/dashboard/routes/route.py +19 -2
  36. astrbot/dashboard/routes/t2i.py +230 -0
  37. astrbot/dashboard/routes/update.py +3 -5
  38. astrbot/dashboard/server.py +13 -4
  39. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/METADATA +40 -53
  40. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/RECORD +43 -38
  41. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/WHEEL +0 -0
  42. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/entry_points.txt +0 -0
  43. {astrbot-4.0.0b4.dist-info → astrbot-4.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -37,7 +37,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
37
37
  from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64
38
38
 
39
39
 
40
- class ComponentType(Enum):
40
+ class ComponentType(str, Enum):
41
41
  Plain = "Plain" # 纯文本消息
42
42
  Face = "Face" # QQ表情
43
43
  Record = "Record" # 语音
@@ -108,7 +108,7 @@ class BaseMessageComponent(BaseModel):
108
108
 
109
109
 
110
110
  class Plain(BaseMessageComponent):
111
- type: ComponentType = "Plain"
111
+ type = ComponentType.Plain
112
112
  text: str
113
113
  convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
114
114
 
@@ -128,8 +128,9 @@ class Plain(BaseMessageComponent):
128
128
  async def to_dict(self):
129
129
  return {"type": "text", "data": {"text": self.text}}
130
130
 
131
+
131
132
  class Face(BaseMessageComponent):
132
- type: ComponentType = "Face"
133
+ type = ComponentType.Face
133
134
  id: int
134
135
 
135
136
  def __init__(self, **_):
@@ -137,7 +138,7 @@ class Face(BaseMessageComponent):
137
138
 
138
139
 
139
140
  class Record(BaseMessageComponent):
140
- type: ComponentType = "Record"
141
+ type = ComponentType.Record
141
142
  file: T.Optional[str] = ""
142
143
  magic: T.Optional[bool] = False
143
144
  url: T.Optional[str] = ""
@@ -164,19 +165,24 @@ class Record(BaseMessageComponent):
164
165
  return Record(file=url, **_)
165
166
  raise Exception("not a valid url")
166
167
 
168
+ @staticmethod
169
+ def fromBase64(bs64_data: str, **_):
170
+ return Record(file=f"base64://{bs64_data}", **_)
171
+
167
172
  async def convert_to_file_path(self) -> str:
168
173
  """将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。
169
174
 
170
175
  Returns:
171
176
  str: 语音的本地路径,以绝对路径表示。
172
177
  """
173
- if self.file and self.file.startswith("file:///"):
174
- file_path = self.file[8:]
175
- return file_path
176
- elif self.file and self.file.startswith("http"):
178
+ if not self.file:
179
+ raise Exception(f"not a valid file: {self.file}")
180
+ if self.file.startswith("file:///"):
181
+ return self.file[8:]
182
+ elif self.file.startswith("http"):
177
183
  file_path = await download_image_by_url(self.file)
178
184
  return os.path.abspath(file_path)
179
- elif self.file and self.file.startswith("base64://"):
185
+ elif self.file.startswith("base64://"):
180
186
  bs64_data = self.file.removeprefix("base64://")
181
187
  image_bytes = base64.b64decode(bs64_data)
182
188
  temp_dir = os.path.join(get_astrbot_data_path(), "temp")
@@ -185,8 +191,7 @@ class Record(BaseMessageComponent):
185
191
  f.write(image_bytes)
186
192
  return os.path.abspath(file_path)
187
193
  elif os.path.exists(self.file):
188
- file_path = self.file
189
- return os.path.abspath(file_path)
194
+ return os.path.abspath(self.file)
190
195
  else:
191
196
  raise Exception(f"not a valid file: {self.file}")
192
197
 
@@ -197,12 +202,14 @@ class Record(BaseMessageComponent):
197
202
  str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
198
203
  """
199
204
  # convert to base64
200
- if self.file and self.file.startswith("file:///"):
205
+ if not self.file:
206
+ raise Exception(f"not a valid file: {self.file}")
207
+ if self.file.startswith("file:///"):
201
208
  bs64_data = file_to_base64(self.file[8:])
202
- elif self.file and self.file.startswith("http"):
209
+ elif self.file.startswith("http"):
203
210
  file_path = await download_image_by_url(self.file)
204
211
  bs64_data = file_to_base64(file_path)
205
- elif self.file and self.file.startswith("base64://"):
212
+ elif self.file.startswith("base64://"):
206
213
  bs64_data = self.file
207
214
  elif os.path.exists(self.file):
208
215
  bs64_data = file_to_base64(self.file)
@@ -236,7 +243,7 @@ class Record(BaseMessageComponent):
236
243
 
237
244
 
238
245
  class Video(BaseMessageComponent):
239
- type: ComponentType = "Video"
246
+ type = ComponentType.Video
240
247
  file: str
241
248
  cover: T.Optional[str] = ""
242
249
  c: T.Optional[int] = 2
@@ -322,7 +329,7 @@ class Video(BaseMessageComponent):
322
329
 
323
330
 
324
331
  class At(BaseMessageComponent):
325
- type: ComponentType = "At"
332
+ type = ComponentType.At
326
333
  qq: T.Union[int, str] # 此处str为all时代表所有人
327
334
  name: T.Optional[str] = ""
328
335
 
@@ -344,28 +351,28 @@ class AtAll(At):
344
351
 
345
352
 
346
353
  class RPS(BaseMessageComponent): # TODO
347
- type: ComponentType = "RPS"
354
+ type = ComponentType.RPS
348
355
 
349
356
  def __init__(self, **_):
350
357
  super().__init__(**_)
351
358
 
352
359
 
353
360
  class Dice(BaseMessageComponent): # TODO
354
- type: ComponentType = "Dice"
361
+ type = ComponentType.Dice
355
362
 
356
363
  def __init__(self, **_):
357
364
  super().__init__(**_)
358
365
 
359
366
 
360
367
  class Shake(BaseMessageComponent): # TODO
361
- type: ComponentType = "Shake"
368
+ type = ComponentType.Shake
362
369
 
363
370
  def __init__(self, **_):
364
371
  super().__init__(**_)
365
372
 
366
373
 
367
374
  class Anonymous(BaseMessageComponent): # TODO
368
- type: ComponentType = "Anonymous"
375
+ type = ComponentType.Anonymous
369
376
  ignore: T.Optional[bool] = False
370
377
 
371
378
  def __init__(self, **_):
@@ -373,7 +380,7 @@ class Anonymous(BaseMessageComponent): # TODO
373
380
 
374
381
 
375
382
  class Share(BaseMessageComponent):
376
- type: ComponentType = "Share"
383
+ type = ComponentType.Share
377
384
  url: str
378
385
  title: str
379
386
  content: T.Optional[str] = ""
@@ -384,7 +391,7 @@ class Share(BaseMessageComponent):
384
391
 
385
392
 
386
393
  class Contact(BaseMessageComponent): # TODO
387
- type: ComponentType = "Contact"
394
+ type = ComponentType.Contact
388
395
  _type: str # type 字段冲突
389
396
  id: T.Optional[int] = 0
390
397
 
@@ -393,7 +400,7 @@ class Contact(BaseMessageComponent): # TODO
393
400
 
394
401
 
395
402
  class Location(BaseMessageComponent): # TODO
396
- type: ComponentType = "Location"
403
+ type = ComponentType.Location
397
404
  lat: float
398
405
  lon: float
399
406
  title: T.Optional[str] = ""
@@ -404,7 +411,7 @@ class Location(BaseMessageComponent): # TODO
404
411
 
405
412
 
406
413
  class Music(BaseMessageComponent):
407
- type: ComponentType = "Music"
414
+ type = ComponentType.Music
408
415
  _type: str
409
416
  id: T.Optional[int] = 0
410
417
  url: T.Optional[str] = ""
@@ -421,7 +428,7 @@ class Music(BaseMessageComponent):
421
428
 
422
429
 
423
430
  class Image(BaseMessageComponent):
424
- type: ComponentType = "Image"
431
+ type = ComponentType.Image
425
432
  file: T.Optional[str] = ""
426
433
  _type: T.Optional[str] = ""
427
434
  subType: T.Optional[int] = 0
@@ -464,14 +471,15 @@ class Image(BaseMessageComponent):
464
471
  Returns:
465
472
  str: 图片的本地路径,以绝对路径表示。
466
473
  """
467
- url = self.url if self.url else self.file
468
- if url and url.startswith("file:///"):
469
- image_file_path = url[8:]
470
- return image_file_path
471
- elif url and url.startswith("http"):
474
+ url = self.url or self.file
475
+ if not url:
476
+ raise ValueError("No valid file or URL provided")
477
+ if url.startswith("file:///"):
478
+ return url[8:]
479
+ elif url.startswith("http"):
472
480
  image_file_path = await download_image_by_url(url)
473
481
  return os.path.abspath(image_file_path)
474
- elif url and url.startswith("base64://"):
482
+ elif url.startswith("base64://"):
475
483
  bs64_data = url.removeprefix("base64://")
476
484
  image_bytes = base64.b64decode(bs64_data)
477
485
  temp_dir = os.path.join(get_astrbot_data_path(), "temp")
@@ -480,8 +488,7 @@ class Image(BaseMessageComponent):
480
488
  f.write(image_bytes)
481
489
  return os.path.abspath(image_file_path)
482
490
  elif os.path.exists(url):
483
- image_file_path = url
484
- return os.path.abspath(image_file_path)
491
+ return os.path.abspath(url)
485
492
  else:
486
493
  raise Exception(f"not a valid file: {url}")
487
494
 
@@ -492,13 +499,15 @@ class Image(BaseMessageComponent):
492
499
  str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。
493
500
  """
494
501
  # convert to base64
495
- url = self.url if self.url else self.file
496
- if url and url.startswith("file:///"):
502
+ url = self.url or self.file
503
+ if not url:
504
+ raise ValueError("No valid file or URL provided")
505
+ if url.startswith("file:///"):
497
506
  bs64_data = file_to_base64(url[8:])
498
- elif url and url.startswith("http"):
507
+ elif url.startswith("http"):
499
508
  image_file_path = await download_image_by_url(url)
500
509
  bs64_data = file_to_base64(image_file_path)
501
- elif url and url.startswith("base64://"):
510
+ elif url.startswith("base64://"):
502
511
  bs64_data = url
503
512
  elif os.path.exists(url):
504
513
  bs64_data = file_to_base64(url)
@@ -532,7 +541,7 @@ class Image(BaseMessageComponent):
532
541
 
533
542
 
534
543
  class Reply(BaseMessageComponent):
535
- type: ComponentType = "Reply"
544
+ type = ComponentType.Reply
536
545
  id: T.Union[str, int]
537
546
  """所引用的消息 ID"""
538
547
  chain: T.Optional[T.List["BaseMessageComponent"]] = []
@@ -558,7 +567,7 @@ class Reply(BaseMessageComponent):
558
567
 
559
568
 
560
569
  class RedBag(BaseMessageComponent):
561
- type: ComponentType = "RedBag"
570
+ type = ComponentType.RedBag
562
571
  title: str
563
572
 
564
573
  def __init__(self, **_):
@@ -566,7 +575,7 @@ class RedBag(BaseMessageComponent):
566
575
 
567
576
 
568
577
  class Poke(BaseMessageComponent):
569
- type: str = ""
578
+ type: str = ComponentType.Poke
570
579
  id: T.Optional[int] = 0
571
580
  qq: T.Optional[int] = 0
572
581
 
@@ -576,7 +585,7 @@ class Poke(BaseMessageComponent):
576
585
 
577
586
 
578
587
  class Forward(BaseMessageComponent):
579
- type: ComponentType = "Forward"
588
+ type = ComponentType.Forward
580
589
  id: str
581
590
 
582
591
  def __init__(self, **_):
@@ -586,7 +595,7 @@ class Forward(BaseMessageComponent):
586
595
  class Node(BaseMessageComponent):
587
596
  """群合并转发消息"""
588
597
 
589
- type: ComponentType = "Node"
598
+ type = ComponentType.Node
590
599
  id: T.Optional[int] = 0 # 忽略
591
600
  name: T.Optional[str] = "" # qq昵称
592
601
  uin: T.Optional[str] = "0" # qq号
@@ -638,7 +647,7 @@ class Node(BaseMessageComponent):
638
647
 
639
648
 
640
649
  class Nodes(BaseMessageComponent):
641
- type: ComponentType = "Nodes"
650
+ type = ComponentType.Nodes
642
651
  nodes: T.List[Node]
643
652
 
644
653
  def __init__(self, nodes: T.List[Node], **_):
@@ -664,7 +673,7 @@ class Nodes(BaseMessageComponent):
664
673
 
665
674
 
666
675
  class Xml(BaseMessageComponent):
667
- type: ComponentType = "Xml"
676
+ type = ComponentType.Xml
668
677
  data: str
669
678
  resid: T.Optional[int] = 0
670
679
 
@@ -673,7 +682,7 @@ class Xml(BaseMessageComponent):
673
682
 
674
683
 
675
684
  class Json(BaseMessageComponent):
676
- type: ComponentType = "Json"
685
+ type = ComponentType.Json
677
686
  data: T.Union[str, dict]
678
687
  resid: T.Optional[int] = 0
679
688
 
@@ -684,7 +693,7 @@ class Json(BaseMessageComponent):
684
693
 
685
694
 
686
695
  class CardImage(BaseMessageComponent):
687
- type: ComponentType = "CardImage"
696
+ type = ComponentType.CardImage
688
697
  file: str
689
698
  cache: T.Optional[bool] = True
690
699
  minwidth: T.Optional[int] = 400
@@ -703,7 +712,7 @@ class CardImage(BaseMessageComponent):
703
712
 
704
713
 
705
714
  class TTS(BaseMessageComponent):
706
- type: ComponentType = "TTS"
715
+ type = ComponentType.TTS
707
716
  text: str
708
717
 
709
718
  def __init__(self, **_):
@@ -711,7 +720,7 @@ class TTS(BaseMessageComponent):
711
720
 
712
721
 
713
722
  class Unknown(BaseMessageComponent):
714
- type: ComponentType = "Unknown"
723
+ type = ComponentType.Unknown
715
724
  text: str
716
725
 
717
726
  def toString(self):
@@ -723,7 +732,7 @@ class File(BaseMessageComponent):
723
732
  文件消息段
724
733
  """
725
734
 
726
- type: ComponentType = "File"
735
+ type = ComponentType.File
727
736
  name: T.Optional[str] = "" # 名字
728
737
  file_: T.Optional[str] = "" # 本地路径
729
738
  url: T.Optional[str] = "" # url
@@ -853,7 +862,7 @@ class File(BaseMessageComponent):
853
862
 
854
863
 
855
864
  class WechatEmoji(BaseMessageComponent):
856
- type: ComponentType = "WechatEmoji"
865
+ type = ComponentType.WechatEmoji
857
866
  md5: T.Optional[str] = ""
858
867
  md5_len: T.Optional[int] = 0
859
868
  cdnurl: T.Optional[str] = ""
@@ -299,7 +299,9 @@ class LLMRequestSubStage(Stage):
299
299
  self.max_context_length - 1,
300
300
  )
301
301
  self.streaming_response: bool = settings["streaming_response"]
302
- self.max_step: int = settings.get("max_agent_step", 10)
302
+ self.max_step: int = settings.get("max_agent_step", 30)
303
+ if isinstance(self.max_step, bool): # workaround: #2622
304
+ self.max_step = 30
303
305
  self.show_tool_use: bool = settings.get("show_tool_use_status", True)
304
306
 
305
307
  for bwp in self.bot_wake_prefixs:
@@ -434,7 +436,9 @@ class LLMRequestSubStage(Stage):
434
436
  provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
435
437
  # 如果模型不支持工具使用,但请求中包含工具列表,则清空。
436
438
  if "tool_use" not in provider_cfg:
437
- logger.debug(f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。")
439
+ logger.debug(
440
+ f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。"
441
+ )
438
442
  req.func_tool = None
439
443
  # 插件可用性设置
440
444
  if event.plugins_name is not None and req.func_tool:
@@ -36,6 +36,7 @@ class ResultDecorateStage(Stage):
36
36
  self.t2i_word_threshold = 150
37
37
  self.t2i_strategy = ctx.astrbot_config["t2i_strategy"]
38
38
  self.t2i_use_network = self.t2i_strategy == "remote"
39
+ self.t2i_active_template = ctx.astrbot_config["t2i_active_template"]
39
40
 
40
41
  self.forward_threshold = ctx.astrbot_config["platform_settings"][
41
42
  "forward_threshold"
@@ -247,7 +248,10 @@ class ResultDecorateStage(Stage):
247
248
  render_start = time.time()
248
249
  try:
249
250
  url = await html_renderer.render_t2i(
250
- plain_str, return_url=True, use_network=self.t2i_use_network
251
+ plain_str,
252
+ return_url=True,
253
+ use_network=self.t2i_use_network,
254
+ template_name=self.t2i_active_template,
251
255
  )
252
256
  except BaseException:
253
257
  logger.error("文本转图片失败,使用文本发送。")
@@ -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:
@@ -67,12 +67,19 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
67
67
  session_id: str,
68
68
  messages: list[dict],
69
69
  ):
70
- if event:
71
- await bot.send(event=event, message=messages)
72
- elif is_group:
70
+ # session_id 必须是纯数字字符串
71
+ session_id = int(session_id) if session_id.isdigit() else None
72
+
73
+ if is_group and isinstance(session_id, int):
73
74
  await bot.send_group_msg(group_id=session_id, message=messages)
74
- else:
75
+ elif not is_group and isinstance(session_id, int):
75
76
  await bot.send_private_msg(user_id=session_id, message=messages)
77
+ elif isinstance(event, Event): # 最后兜底
78
+ await bot.send(event=event, message=messages)
79
+ else:
80
+ raise ValueError(
81
+ f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})"
82
+ )
76
83
 
77
84
  @classmethod
78
85
  async def send_message(
@@ -83,7 +90,15 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
83
90
  is_group: bool = False,
84
91
  session_id: str = None,
85
92
  ):
86
- """发送消息"""
93
+ """发送消息至 QQ 协议端(aiocqhttp)。
94
+
95
+ Args:
96
+ bot (CQHttp): aiocqhttp 机器人实例
97
+ message_chain (MessageChain): 要发送的消息链
98
+ event (Event | None, optional): aiocqhttp 事件对象.
99
+ is_group (bool, optional): 是否为群消息.
100
+ session_id (str | None, optional): 会话 ID(群号或 QQ 号
101
+ """
87
102
 
88
103
  # 转发消息、文件消息不能和普通消息混在一起发送
89
104
  send_one_by_one = any(
@@ -122,18 +137,15 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
122
137
 
123
138
  async def send(self, message: MessageChain):
124
139
  """发送消息"""
125
- event = self.message_obj.raw_message
126
- assert isinstance(event, Event), "Event must be an instance of aiocqhttp.Event"
127
- is_group = False
128
- if self.get_group_id():
129
- is_group = True
130
- session_id = self.get_group_id()
131
- else:
132
- session_id = self.get_sender_id()
140
+ event = getattr(self.message_obj, "raw_message", None)
141
+
142
+ is_group = bool(self.get_group_id())
143
+ session_id = self.get_group_id() if is_group else self.get_sender_id()
144
+
133
145
  await self.send_message(
134
146
  bot=self.bot,
135
147
  message_chain=message,
136
- event=event,
148
+ event=event, # 不强制要求一定是 Event
137
149
  is_group=is_group,
138
150
  session_id=session_id,
139
151
  )
@@ -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(