AstrBot 4.8.0__py3-none-any.whl → 4.9.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 (106) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
  3. astrbot/core/agent/tool.py +7 -2
  4. astrbot/core/astr_agent_tool_exec.py +5 -1
  5. astrbot/core/config/astrbot_config.py +4 -0
  6. astrbot/core/config/default.py +72 -1
  7. astrbot/core/config/i18n_utils.py +1 -0
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +2 -3
  10. astrbot/core/db/migration/migra_3_to_4.py +2 -0
  11. astrbot/core/db/migration/sqlite_v3.py +6 -4
  12. astrbot/core/db/po.py +16 -15
  13. astrbot/core/db/sqlite.py +4 -3
  14. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
  15. astrbot/core/event_bus.py +6 -1
  16. astrbot/core/knowledge_base/retrieval/manager.py +5 -1
  17. astrbot/core/log.py +2 -1
  18. astrbot/core/message/components.py +9 -3
  19. astrbot/core/persona_mgr.py +2 -2
  20. astrbot/core/pipeline/content_safety_check/stage.py +1 -1
  21. astrbot/core/pipeline/context_utils.py +2 -1
  22. astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
  23. astrbot/core/pipeline/process_stage/stage.py +1 -1
  24. astrbot/core/pipeline/respond/stage.py +8 -2
  25. astrbot/core/pipeline/result_decorate/stage.py +89 -22
  26. astrbot/core/pipeline/scheduler.py +5 -1
  27. astrbot/core/pipeline/waking_check/stage.py +10 -0
  28. astrbot/core/platform/astr_message_event.py +5 -3
  29. astrbot/core/platform/astrbot_message.py +2 -2
  30. astrbot/core/platform/manager.py +4 -0
  31. astrbot/core/platform/platform.py +11 -3
  32. astrbot/core/platform/platform_metadata.py +1 -1
  33. astrbot/core/platform/register.py +1 -0
  34. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +9 -5
  36. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
  37. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  38. astrbot/core/platform/sources/discord/client.py +16 -4
  39. astrbot/core/platform/sources/discord/components.py +2 -2
  40. astrbot/core/platform/sources/discord/discord_platform_adapter.py +52 -24
  41. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  42. astrbot/core/platform/sources/lark/lark_adapter.py +183 -20
  43. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  44. astrbot/core/platform/sources/lark/server.py +206 -0
  45. astrbot/core/platform/sources/misskey/misskey_adapter.py +2 -3
  46. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
  47. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
  48. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
  50. astrbot/core/platform/sources/slack/client.py +9 -2
  51. astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
  52. astrbot/core/platform/sources/slack/slack_event.py +8 -7
  53. astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
  54. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  55. astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
  56. astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
  57. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
  58. astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
  59. astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
  60. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  61. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
  62. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
  63. astrbot/core/provider/func_tool_manager.py +3 -3
  64. astrbot/core/provider/manager.py +130 -74
  65. astrbot/core/provider/provider.py +12 -1
  66. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  67. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  68. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  69. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  70. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  71. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  72. astrbot/core/provider/sources/gemini_source.py +12 -10
  73. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  74. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  75. astrbot/core/provider/sources/openai_source.py +4 -0
  76. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  77. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  78. astrbot/core/provider/sources/whisper_api_source.py +1 -1
  79. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  80. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  81. astrbot/core/star/context.py +2 -2
  82. astrbot/core/star/register/star_handler.py +22 -5
  83. astrbot/core/star/star_handler.py +85 -4
  84. astrbot/core/updator.py +3 -3
  85. astrbot/core/utils/io.py +1 -1
  86. astrbot/core/utils/session_waiter.py +17 -10
  87. astrbot/core/utils/shared_preferences.py +32 -0
  88. astrbot/core/utils/t2i/__init__.py +2 -2
  89. astrbot/core/utils/t2i/local_strategy.py +25 -31
  90. astrbot/core/utils/tencent_record_helper.py +1 -1
  91. astrbot/core/utils/version_comparator.py +6 -3
  92. astrbot/core/utils/webhook_utils.py +19 -0
  93. astrbot/dashboard/routes/chat.py +14 -9
  94. astrbot/dashboard/routes/config.py +10 -20
  95. astrbot/dashboard/routes/conversation.py +91 -1
  96. astrbot/dashboard/routes/knowledge_base.py +253 -78
  97. astrbot/dashboard/routes/log.py +13 -8
  98. astrbot/dashboard/routes/platform.py +1 -1
  99. astrbot/dashboard/routes/plugin.py +113 -52
  100. astrbot/dashboard/routes/route.py +2 -0
  101. astrbot/dashboard/server.py +6 -3
  102. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/METADATA +9 -1
  103. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/RECORD +106 -105
  104. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/WHEEL +0 -0
  105. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/entry_points.txt +0 -0
  106. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,4 @@
1
+ import random
1
2
  import re
2
3
  import time
3
4
  import traceback
@@ -6,6 +7,7 @@ from collections.abc import AsyncGenerator
6
7
  from astrbot.core import file_token_service, html_renderer, logger
7
8
  from astrbot.core.message.components import At, File, Image, Node, Plain, Record, Reply
8
9
  from astrbot.core.message.message_event_result import ResultContentType
10
+ from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage
9
11
  from astrbot.core.platform.astr_message_event import AstrMessageEvent
10
12
  from astrbot.core.platform.message_type import MessageType
11
13
  from astrbot.core.star.session_llm_manager import SessionServiceManager
@@ -41,6 +43,18 @@ class ResultDecorateStage(Stage):
41
43
  "forward_threshold"
42
44
  ]
43
45
 
46
+ trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
47
+ "trigger_probability",
48
+ 1,
49
+ )
50
+ try:
51
+ self.tts_trigger_probability = max(
52
+ 0.0,
53
+ min(float(trigger_probability), 1.0),
54
+ )
55
+ except (TypeError, ValueError):
56
+ self.tts_trigger_probability = 1.0
57
+
44
58
  # 分段回复
45
59
  self.words_count_threshold = int(
46
60
  ctx.astrbot_config["platform_settings"]["segmented_reply"][
@@ -53,7 +67,22 @@ class ResultDecorateStage(Stage):
53
67
  self.only_llm_result = ctx.astrbot_config["platform_settings"][
54
68
  "segmented_reply"
55
69
  ]["only_llm_result"]
70
+ self.split_mode = ctx.astrbot_config["platform_settings"][
71
+ "segmented_reply"
72
+ ].get("split_mode", "regex")
56
73
  self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"]
74
+ self.split_words = ctx.astrbot_config["platform_settings"][
75
+ "segmented_reply"
76
+ ].get("split_words", ["。", "?", "!", "~", "…"])
77
+ if self.split_words:
78
+ escaped_words = sorted(
79
+ [re.escape(word) for word in self.split_words], key=len, reverse=True
80
+ )
81
+ self.split_words_pattern = re.compile(
82
+ f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL
83
+ )
84
+ else:
85
+ self.split_words_pattern = None
57
86
  self.content_cleanup_rule = ctx.astrbot_config["platform_settings"][
58
87
  "segmented_reply"
59
88
  ]["content_cleanup_rule"]
@@ -69,6 +98,28 @@ class ResultDecorateStage(Stage):
69
98
  self.content_safe_check_stage = stage_cls()
70
99
  await self.content_safe_check_stage.initialize(ctx)
71
100
 
101
+ def _split_text_by_words(self, text: str) -> list[str]:
102
+ """使用分段词列表分段文本"""
103
+ if not self.split_words_pattern:
104
+ return [text]
105
+
106
+ segments = self.split_words_pattern.findall(text)
107
+ result = []
108
+ for seg in segments:
109
+ if isinstance(seg, tuple):
110
+ content = seg[0]
111
+ if not isinstance(content, str):
112
+ continue
113
+ for word in self.split_words:
114
+ if content.endswith(word):
115
+ content = content[: -len(word)]
116
+ break
117
+ if content.strip():
118
+ result.append(content)
119
+ elif seg and seg.strip():
120
+ result.append(seg)
121
+ return result if result else [text]
122
+
72
123
  async def process(
73
124
  self,
74
125
  event: AstrMessageEvent,
@@ -93,11 +144,13 @@ class ResultDecorateStage(Stage):
93
144
  for comp in result.chain:
94
145
  if isinstance(comp, Plain):
95
146
  text += comp.text
96
- async for _ in self.content_safe_check_stage.process(
97
- event,
98
- check_text=text,
99
- ):
100
- yield
147
+
148
+ if isinstance(self.content_safe_check_stage, ContentSafetyCheckStage):
149
+ async for _ in self.content_safe_check_stage.process(
150
+ event,
151
+ check_text=text,
152
+ ):
153
+ yield
101
154
 
102
155
  # 发送消息前事件钩子
103
156
  handlers = star_handlers_registry.get_handlers_by_event_type(
@@ -114,7 +167,8 @@ class ResultDecorateStage(Stage):
114
167
  "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作",
115
168
  )
116
169
  await handler.handler(event)
117
- if event.get_result() is None or not event.get_result().chain:
170
+
171
+ if (result := event.get_result()) is None or not result.chain:
118
172
  logger.debug(
119
173
  f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。",
120
174
  )
@@ -161,21 +215,27 @@ class ResultDecorateStage(Stage):
161
215
  # 不分段回复
162
216
  new_chain.append(comp)
163
217
  continue
164
- try:
165
- split_response = re.findall(
166
- self.regex,
167
- comp.text,
168
- re.DOTALL | re.MULTILINE,
169
- )
170
- except re.error:
171
- logger.error(
172
- f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
173
- )
174
- split_response = re.findall(
175
- r".*?[。?!~…]+|.+$",
176
- comp.text,
177
- re.DOTALL | re.MULTILINE,
178
- )
218
+
219
+ # 根据 split_mode 选择分段方式
220
+ if self.split_mode == "words":
221
+ split_response = self._split_text_by_words(comp.text)
222
+ else: # regex 模式
223
+ try:
224
+ split_response = re.findall(
225
+ self.regex,
226
+ comp.text,
227
+ re.DOTALL | re.MULTILINE,
228
+ )
229
+ except re.error:
230
+ logger.error(
231
+ f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
232
+ )
233
+ split_response = re.findall(
234
+ r".*?[。?!~…]+|.+$",
235
+ comp.text,
236
+ re.DOTALL | re.MULTILINE,
237
+ )
238
+
179
239
  if not split_response:
180
240
  new_chain.append(comp)
181
241
  continue
@@ -199,7 +259,14 @@ class ResultDecorateStage(Stage):
199
259
  and result.is_llm_result()
200
260
  and SessionServiceManager.should_process_tts_request(event)
201
261
  ):
202
- if not tts_provider:
262
+ should_tts = self.tts_trigger_probability >= 1.0 or (
263
+ self.tts_trigger_probability > 0.0
264
+ and random.random() <= self.tts_trigger_probability
265
+ )
266
+
267
+ if not should_tts:
268
+ logger.debug("跳过 TTS:触发概率未命中。")
269
+ elif not tts_provider:
203
270
  logger.warning(
204
271
  f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
205
272
  )
@@ -2,6 +2,10 @@ from collections.abc import AsyncGenerator
2
2
 
3
3
  from astrbot.core import logger
4
4
  from astrbot.core.platform import AstrMessageEvent
5
+ from astrbot.core.platform.sources.webchat.webchat_event import WebChatMessageEvent
6
+ from astrbot.core.platform.sources.wecom_ai_bot.wecomai_event import (
7
+ WecomAIBotMessageEvent,
8
+ )
5
9
 
6
10
  from . import STAGES_ORDER
7
11
  from .context import PipelineContext
@@ -78,7 +82,7 @@ class PipelineScheduler:
78
82
  await self._process_stages(event)
79
83
 
80
84
  # 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
81
- if event.get_platform_name() in ["webchat", "wecom_ai_bot"]:
85
+ if isinstance(event, (WebChatMessageEvent, WecomAIBotMessageEvent)):
82
86
  await event.send(None)
83
87
 
84
88
  logger.debug("pipeline 执行完毕。")
@@ -50,6 +50,9 @@ class WakingCheckStage(Stage):
50
50
  "ignore_at_all",
51
51
  False,
52
52
  )
53
+ self.disable_builtin_commands = self.ctx.astrbot_config.get(
54
+ "disable_builtin_commands", False
55
+ )
53
56
 
54
57
  async def process(
55
58
  self,
@@ -131,6 +134,13 @@ class WakingCheckStage(Stage):
131
134
  EventType.AdapterMessageEvent,
132
135
  plugins_name=event.plugins_name,
133
136
  ):
137
+ if (
138
+ self.disable_builtin_commands
139
+ and handler.handler_module_path == "packages.builtin_commands.main"
140
+ ):
141
+ logger.debug("skipping builtin command")
142
+ continue
143
+
134
144
  # filter 需满足 AND 逻辑关系
135
145
  passed = True
136
146
  permission_not_pass = False
@@ -153,7 +153,9 @@ class AstrMessageEvent(abc.ABC):
153
153
 
154
154
  def get_sender_name(self) -> str:
155
155
  """获取消息发送者的名称。(可能会返回空字符串)"""
156
- return self.message_obj.sender.nickname
156
+ if isinstance(self.message_obj.sender.nickname, str):
157
+ return self.message_obj.sender.nickname
158
+ return ""
157
159
 
158
160
  def set_extra(self, key, value):
159
161
  """设置额外的信息。"""
@@ -270,7 +272,7 @@ class AstrMessageEvent(abc.ABC):
270
272
  """
271
273
  self.call_llm = call_llm
272
274
 
273
- def get_result(self) -> MessageEventResult:
275
+ def get_result(self) -> MessageEventResult | None:
274
276
  """获取消息事件的结果。"""
275
277
  return self._result
276
278
 
@@ -320,7 +322,7 @@ class AstrMessageEvent(abc.ABC):
320
322
  self,
321
323
  prompt: str,
322
324
  func_tool_manager=None,
323
- session_id: str = None,
325
+ session_id: str = "",
324
326
  image_urls: list[str] | None = None,
325
327
  contexts: list | None = None,
326
328
  system_prompt: str = "",
@@ -54,7 +54,7 @@ class AstrBotMessage:
54
54
  self_id: str # 机器人的识别id
55
55
  session_id: str # 会话id。取决于 unique_session 的设置。
56
56
  message_id: str # 消息id
57
- group: Group # 群组
57
+ group: Group | None # 群组
58
58
  sender: MessageMember # 发送者
59
59
  message: list[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
60
60
  message_str: str # 最直观的纯文本消息字符串
@@ -78,7 +78,7 @@ class AstrBotMessage:
78
78
  return ""
79
79
 
80
80
  @group_id.setter
81
- def group_id(self, value: str):
81
+ def group_id(self, value: str | None):
82
82
  """设置 group_id"""
83
83
  if value:
84
84
  if self.group:
@@ -5,6 +5,7 @@ from asyncio import Queue
5
5
  from astrbot.core import logger
6
6
  from astrbot.core.config.astrbot_config import AstrBotConfig
7
7
  from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
8
+ from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
8
9
 
9
10
  from .platform import Platform, PlatformStatus
10
11
  from .register import platform_cls_map
@@ -18,6 +19,7 @@ class PlatformManager:
18
19
 
19
20
  self._inst_map: dict[str, dict] = {}
20
21
 
22
+ self.astrbot_config = config
21
23
  self.platforms_config = config["platform"]
22
24
  self.settings = config["platform_settings"]
23
25
  """NOTE: 这里是 default 的配置文件,以保证最大的兼容性;
@@ -29,6 +31,8 @@ class PlatformManager:
29
31
  """初始化所有平台适配器"""
30
32
  for platform in self.platforms_config:
31
33
  try:
34
+ if ensure_platform_webhook_config(platform):
35
+ self.astrbot_config.save_config()
32
36
  await self.load_platform(platform)
33
37
  except Exception as e:
34
38
  logger.error(f"初始化 {platform} 平台适配器失败: {e}")
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
  import uuid
3
3
  from asyncio import Queue
4
- from collections.abc import Awaitable
4
+ from collections.abc import Coroutine
5
5
  from dataclasses import dataclass, field
6
6
  from datetime import datetime
7
7
  from enum import Enum
@@ -80,6 +80,13 @@ class Platform(abc.ABC):
80
80
  if self._status == PlatformStatus.ERROR:
81
81
  self._status = PlatformStatus.RUNNING
82
82
 
83
+ def unified_webhook(self) -> bool:
84
+ """是否正在使用统一 Webhook 模式"""
85
+ return bool(
86
+ self.config.get("unified_webhook_mode", False)
87
+ and self.config.get("webhook_uuid")
88
+ )
89
+
83
90
  def get_stats(self) -> dict:
84
91
  """获取平台统计信息"""
85
92
  meta = self.meta()
@@ -97,10 +104,11 @@ class Platform(abc.ABC):
97
104
  }
98
105
  if self.last_error
99
106
  else None,
107
+ "unified_webhook": self.unified_webhook(),
100
108
  }
101
109
 
102
110
  @abc.abstractmethod
103
- def run(self) -> Awaitable[Any]:
111
+ def run(self) -> Coroutine[Any, Any, None]:
104
112
  """得到一个平台的运行实例,需要返回一个协程对象。"""
105
113
  raise NotImplementedError
106
114
 
@@ -116,7 +124,7 @@ class Platform(abc.ABC):
116
124
  self,
117
125
  session: MessageSesion,
118
126
  message_chain: MessageChain,
119
- ):
127
+ ) -> None:
120
128
  """通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。
121
129
 
122
130
  异步方法。
@@ -7,7 +7,7 @@ class PlatformMetadata:
7
7
  """平台的名称,即平台的类型,如 aiocqhttp, discord, slack"""
8
8
  description: str
9
9
  """平台的描述"""
10
- id: str | None = None
10
+ id: str
11
11
  """平台的唯一标识符,用于配置中识别特定平台"""
12
12
 
13
13
  default_config_tmpl: dict | None = None
@@ -40,6 +40,7 @@ def register_platform_adapter(
40
40
  pm = PlatformMetadata(
41
41
  name=adapter_name,
42
42
  description=desc,
43
+ id=adapter_name,
43
44
  default_config_tmpl=default_config_tmpl,
44
45
  adapter_display_name=adapter_display_name,
45
46
  logo_path=logo_path,
@@ -70,16 +70,18 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
70
70
  bot: CQHttp,
71
71
  event: Event | None,
72
72
  is_group: bool,
73
- session_id: str,
73
+ session_id: str | None,
74
74
  messages: list[dict],
75
75
  ):
76
76
  # session_id 必须是纯数字字符串
77
- session_id = int(session_id) if session_id.isdigit() else None
77
+ session_id_int = (
78
+ int(session_id) if session_id and session_id.isdigit() else None
79
+ )
78
80
 
79
- if is_group and isinstance(session_id, int):
80
- await bot.send_group_msg(group_id=session_id, message=messages)
81
- elif not is_group and isinstance(session_id, int):
82
- await bot.send_private_msg(user_id=session_id, message=messages)
81
+ if is_group and isinstance(session_id_int, int):
82
+ await bot.send_group_msg(group_id=session_id_int, message=messages)
83
+ elif not is_group and isinstance(session_id_int, int):
84
+ await bot.send_private_msg(user_id=session_id_int, message=messages)
83
85
  elif isinstance(event, Event): # 最后兜底
84
86
  await bot.send(event=event, message=messages)
85
87
  else:
@@ -4,7 +4,7 @@ import logging
4
4
  import time
5
5
  import uuid
6
6
  from collections.abc import Awaitable
7
- from typing import Any
7
+ from typing import Any, cast
8
8
 
9
9
  from aiocqhttp import CQHttp, Event
10
10
  from aiocqhttp.exceptions import ActionFailed
@@ -48,7 +48,7 @@ class AiocqhttpAdapter(Platform):
48
48
  self.metadata = PlatformMetadata(
49
49
  name="aiocqhttp",
50
50
  description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。",
51
- id=self.config.get("id"),
51
+ id=cast(str, self.config.get("id")),
52
52
  support_streaming_message=False,
53
53
  )
54
54
 
@@ -127,7 +127,9 @@ class AiocqhttpAdapter(Platform):
127
127
  """OneBot V11 请求类事件"""
128
128
  abm = AstrBotMessage()
129
129
  abm.self_id = str(event.self_id)
130
- abm.sender = MessageMember(user_id=str(event.user_id), nickname=event.user_id)
130
+ abm.sender = MessageMember(
131
+ user_id=str(event.user_id), nickname=str(event.user_id)
132
+ )
131
133
  abm.type = MessageType.OTHER_MESSAGE
132
134
  if event.get("group_id"):
133
135
  abm.type = MessageType.GROUP_MESSAGE
@@ -194,6 +196,7 @@ class AiocqhttpAdapter(Platform):
194
196
  @param event: 事件对象
195
197
  @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。
196
198
  """
199
+ assert event.sender is not None
197
200
  abm = AstrBotMessage()
198
201
  abm.self_id = str(event.self_id)
199
202
  abm.sender = MessageMember(
@@ -203,6 +206,7 @@ class AiocqhttpAdapter(Platform):
203
206
  if event["message_type"] == "group":
204
207
  abm.type = MessageType.GROUP_MESSAGE
205
208
  abm.group_id = str(event.group_id)
209
+ abm.group = Group(str(event.group_id))
206
210
  abm.group.group_name = event.get("group_name", "N/A")
207
211
  elif event["message_type"] == "private":
208
212
  abm.type = MessageType.FRIEND_MESSAGE
@@ -228,7 +232,7 @@ class AiocqhttpAdapter(Platform):
228
232
  await self.bot.send(event, err)
229
233
  except BaseException as e:
230
234
  logger.error(f"回复消息失败: {e}")
231
- return None
235
+ raise ValueError(err)
232
236
 
233
237
  # 按消息段类型类型适配
234
238
  for t, m_group in itertools.groupby(event.message, key=lambda x: x["type"]):
@@ -417,7 +421,7 @@ class AiocqhttpAdapter(Platform):
417
421
 
418
422
  async def shutdown_trigger_placeholder(self):
419
423
  await self.shutdown_event.wait()
420
- logger.info("aiocqhttp 适配器已被优雅地关闭")
424
+ logger.info("aiocqhttp 适配器已被关闭")
421
425
 
422
426
  def meta(self) -> PlatformMetadata:
423
427
  return self.metadata
@@ -2,6 +2,7 @@ import asyncio
2
2
  import os
3
3
  import threading
4
4
  import uuid
5
+ from typing import cast
5
6
 
6
7
  import aiohttp
7
8
  import dingtalk_stream
@@ -54,12 +55,14 @@ class DingtalkPlatformAdapter(Platform):
54
55
  self.client_id = platform_config["client_id"]
55
56
  self.client_secret = platform_config["client_secret"]
56
57
 
58
+ outer_self = self
59
+
57
60
  class AstrCallbackClient(dingtalk_stream.ChatbotHandler):
58
- async def process(self_, message: dingtalk_stream.CallbackMessage):
61
+ async def process(self, message: dingtalk_stream.CallbackMessage):
59
62
  logger.debug(f"dingtalk: {message.data}")
60
63
  im = dingtalk_stream.ChatbotMessage.from_dict(message.data)
61
- abm = await self.convert_msg(im)
62
- await self.handle_msg(abm)
64
+ abm = await outer_self.convert_msg(im)
65
+ await outer_self.handle_msg(abm)
63
66
 
64
67
  return AckMessage.STATUS_OK, "OK"
65
68
 
@@ -73,6 +76,7 @@ class DingtalkPlatformAdapter(Platform):
73
76
  self.client,
74
77
  )
75
78
  self.client_ = client # 用于 websockets 的 client
79
+ self._shutdown_event: threading.Event | None = None
76
80
 
77
81
  def _id_to_sid(self, dingtalk_id: str | None) -> str:
78
82
  if not dingtalk_id:
@@ -93,7 +97,7 @@ class DingtalkPlatformAdapter(Platform):
93
97
  return PlatformMetadata(
94
98
  name="dingtalk",
95
99
  description="钉钉机器人官方 API 适配器",
96
- id=self.config.get("id"),
100
+ id=cast(str, self.config.get("id")),
97
101
  support_streaming_message=False,
98
102
  )
99
103
 
@@ -104,7 +108,7 @@ class DingtalkPlatformAdapter(Platform):
104
108
  abm = AstrBotMessage()
105
109
  abm.message = []
106
110
  abm.message_str = ""
107
- abm.timestamp = int(message.create_at / 1000)
111
+ abm.timestamp = int(cast(int, message.create_at) / 1000)
108
112
  abm.type = (
109
113
  MessageType.GROUP_MESSAGE
110
114
  if message.conversation_type == "2"
@@ -115,7 +119,7 @@ class DingtalkPlatformAdapter(Platform):
115
119
  nickname=message.sender_nick,
116
120
  )
117
121
  abm.self_id = self._id_to_sid(message.chatbot_user_id)
118
- abm.message_id = message.message_id
122
+ abm.message_id = cast(str, message.message_id)
119
123
  abm.raw_message = message
120
124
 
121
125
  if abm.type == MessageType.GROUP_MESSAGE:
@@ -132,14 +136,16 @@ class DingtalkPlatformAdapter(Platform):
132
136
  else:
133
137
  abm.session_id = abm.sender.user_id
134
138
 
135
- message_type: str = message.message_type
139
+ message_type: str = cast(str, message.message_type)
136
140
  match message_type:
137
141
  case "text":
138
142
  abm.message_str = message.text.content.strip()
139
143
  abm.message.append(Plain(abm.message_str))
140
144
  case "richText":
141
- rtc: dingtalk_stream.RichTextContent = message.rich_text_content
142
- contents: list[dict] = rtc.rich_text_list
145
+ rtc: dingtalk_stream.RichTextContent = cast(
146
+ dingtalk_stream.RichTextContent, message.rich_text_content
147
+ )
148
+ contents: list[dict] = cast(list[dict], rtc.rich_text_list)
143
149
  for content in contents:
144
150
  plains = ""
145
151
  if "text" in content:
@@ -148,7 +154,7 @@ class DingtalkPlatformAdapter(Platform):
148
154
  elif "type" in content and content["type"] == "picture":
149
155
  f_path = await self.download_ding_file(
150
156
  content["downloadCode"],
151
- message.robot_code,
157
+ cast(str, message.robot_code),
152
158
  "jpg",
153
159
  )
154
160
  abm.message.append(Image.fromFileSystem(f_path))
@@ -193,7 +199,7 @@ class DingtalkPlatformAdapter(Platform):
193
199
  logger.error(
194
200
  f"下载钉钉文件失败: {resp.status}, {await resp.text()}",
195
201
  )
196
- return None
202
+ return ""
197
203
  resp_data = await resp.json()
198
204
  download_url = resp_data["data"]["downloadUrl"]
199
205
  await download_file(download_url, f_path)
@@ -213,7 +219,7 @@ class DingtalkPlatformAdapter(Platform):
213
219
  logger.error(
214
220
  f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}",
215
221
  )
216
- return None
222
+ return ""
217
223
  return (await resp.json())["data"]["accessToken"]
218
224
 
219
225
  async def handle_msg(self, abm: AstrBotMessage):
@@ -239,7 +245,7 @@ class DingtalkPlatformAdapter(Platform):
239
245
  task.result()
240
246
  except Exception as e:
241
247
  if "Graceful shutdown" in str(e):
242
- logger.info("钉钉适配器已被优雅地关闭")
248
+ logger.info("钉钉适配器已被关闭")
243
249
  return
244
250
  logger.error(f"钉钉机器人启动失败: {e}")
245
251
 
@@ -250,9 +256,11 @@ class DingtalkPlatformAdapter(Platform):
250
256
  def monkey_patch_close():
251
257
  raise KeyboardInterrupt("Graceful shutdown")
252
258
 
253
- self.client_.open_connection = monkey_patch_close
254
- await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
255
- self._shutdown_event.set()
259
+ if self.client_.websocket is not None:
260
+ self.client_.open_connection = monkey_patch_close
261
+ await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
262
+ if self._shutdown_event is not None:
263
+ self._shutdown_event.set()
256
264
 
257
265
  def get_client(self):
258
266
  return self.client
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from typing import cast
2
3
 
3
4
  import dingtalk_stream
4
5
 
@@ -32,7 +33,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
32
33
  client.reply_markdown,
33
34
  segment.text,
34
35
  segment.text,
35
- self.message_obj.raw_message,
36
+ cast(dingtalk_stream.ChatbotMessage, self.message_obj.raw_message),
36
37
  )
37
38
  elif isinstance(segment, Comp.Image):
38
39
  markdown_str = ""
@@ -53,7 +54,9 @@ class DingtalkMessageEvent(AstrMessageEvent):
53
54
  client.reply_markdown,
54
55
  "😄",
55
56
  markdown_str,
56
- self.message_obj.raw_message,
57
+ cast(
58
+ dingtalk_stream.ChatbotMessage, self.message_obj.raw_message
59
+ ),
57
60
  )
58
61
  logger.debug(f"send image: {ret}")
59
62
 
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ from collections.abc import Awaitable, Callable
2
3
 
3
4
  import discord
4
5
 
@@ -27,13 +28,16 @@ class DiscordBotClient(discord.Bot):
27
28
  super().__init__(intents=intents, proxy=proxy)
28
29
 
29
30
  # 回调函数
30
- self.on_message_received = None
31
- self.on_ready_once_callback = None
31
+ self.on_message_received: Callable[[dict], Awaitable[None]] | None = None
32
+ self.on_ready_once_callback: Callable[[], Awaitable[None]] | None = None
32
33
  self._ready_once_fired = False
33
34
 
34
- @override
35
35
  async def on_ready(self):
36
36
  """当机器人成功连接并准备就绪时触发"""
37
+ if self.user is None:
38
+ logger.error("[Discord] 客户端未正确加载用户信息 (self.user is None)")
39
+ return
40
+
37
41
  logger.info(f"[Discord] 已作为 {self.user} (ID: {self.user.id}) 登录")
38
42
  logger.info("[Discord] 客户端已准备就绪。")
39
43
 
@@ -49,6 +53,9 @@ class DiscordBotClient(discord.Bot):
49
53
 
50
54
  def _create_message_data(self, message: discord.Message) -> dict:
51
55
  """从 discord.Message 创建数据字典"""
56
+ if self.user is None:
57
+ raise RuntimeError("Bot is not ready: self.user is None")
58
+
52
59
  is_mentioned = self.user in message.mentions
53
60
  return {
54
61
  "message": message,
@@ -66,6 +73,12 @@ class DiscordBotClient(discord.Bot):
66
73
 
67
74
  def _create_interaction_data(self, interaction: discord.Interaction) -> dict:
68
75
  """从 discord.Interaction 创建数据字典"""
76
+ if self.user is None:
77
+ raise RuntimeError("Bot is not ready: self.user is None")
78
+
79
+ if interaction.user is None:
80
+ raise ValueError("Interaction received without a valid user")
81
+
69
82
  return {
70
83
  "interaction": interaction,
71
84
  "bot_id": str(self.user.id),
@@ -80,7 +93,6 @@ class DiscordBotClient(discord.Bot):
80
93
  "type": "interaction",
81
94
  }
82
95
 
83
- @override
84
96
  async def on_message(self, message: discord.Message):
85
97
  """当接收到消息时触发"""
86
98
  if message.author.bot:
@@ -97,8 +97,8 @@ class DiscordView(BaseMessageComponent):
97
97
 
98
98
  def __init__(
99
99
  self,
100
- components: list[BaseMessageComponent] = None,
101
- timeout: float = None,
100
+ components: list[BaseMessageComponent] | None = None,
101
+ timeout: float | None = None,
102
102
  ):
103
103
  self.components = components or []
104
104
  self.timeout = timeout