AstrBot 4.7.4__py3-none-any.whl → 4.9.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 (111) 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_run_util.py +15 -1
  5. astrbot/core/astr_agent_tool_exec.py +5 -1
  6. astrbot/core/config/astrbot_config.py +4 -0
  7. astrbot/core/config/default.py +116 -1
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +32 -4
  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 +56 -1
  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/agent_sub_stages/third_party.py +1 -1
  23. astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
  24. astrbot/core/pipeline/process_stage/stage.py +1 -1
  25. astrbot/core/pipeline/respond/stage.py +4 -2
  26. astrbot/core/pipeline/result_decorate/stage.py +68 -21
  27. astrbot/core/pipeline/scheduler.py +5 -1
  28. astrbot/core/pipeline/waking_check/stage.py +10 -0
  29. astrbot/core/platform/astr_message_event.py +5 -3
  30. astrbot/core/platform/astrbot_message.py +2 -2
  31. astrbot/core/platform/manager.py +71 -9
  32. astrbot/core/platform/platform.py +109 -4
  33. astrbot/core/platform/platform_metadata.py +1 -1
  34. astrbot/core/platform/register.py +1 -0
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  36. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +13 -8
  37. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
  38. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  39. astrbot/core/platform/sources/discord/client.py +16 -4
  40. astrbot/core/platform/sources/discord/components.py +2 -2
  41. astrbot/core/platform/sources/discord/discord_platform_adapter.py +53 -26
  42. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  43. astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
  44. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  45. astrbot/core/platform/sources/lark/server.py +206 -0
  46. astrbot/core/platform/sources/misskey/misskey_adapter.py +3 -5
  47. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
  48. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
  50. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
  51. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  52. astrbot/core/platform/sources/slack/client.py +58 -40
  53. astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
  54. astrbot/core/platform/sources/slack/slack_event.py +11 -10
  55. astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
  56. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  57. astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
  58. astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
  59. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
  60. astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
  61. astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
  62. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  63. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  64. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  65. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
  66. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
  67. astrbot/core/platform_message_history_mgr.py +3 -3
  68. astrbot/core/provider/func_tool_manager.py +3 -3
  69. astrbot/core/provider/manager.py +130 -74
  70. astrbot/core/provider/provider.py +12 -1
  71. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  72. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  73. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  74. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  75. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  76. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  77. astrbot/core/provider/sources/gemini_source.py +12 -10
  78. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  79. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  80. astrbot/core/provider/sources/openai_source.py +4 -0
  81. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  82. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  83. astrbot/core/provider/sources/whisper_api_source.py +44 -12
  84. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  85. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  86. astrbot/core/star/context.py +2 -2
  87. astrbot/core/star/register/star_handler.py +22 -5
  88. astrbot/core/star/star_handler.py +85 -4
  89. astrbot/core/updator.py +3 -3
  90. astrbot/core/utils/io.py +1 -1
  91. astrbot/core/utils/session_waiter.py +17 -10
  92. astrbot/core/utils/shared_preferences.py +32 -0
  93. astrbot/core/utils/t2i/__init__.py +2 -2
  94. astrbot/core/utils/t2i/local_strategy.py +25 -31
  95. astrbot/core/utils/tencent_record_helper.py +2 -2
  96. astrbot/core/utils/version_comparator.py +6 -3
  97. astrbot/core/utils/webhook_utils.py +66 -0
  98. astrbot/dashboard/routes/__init__.py +2 -0
  99. astrbot/dashboard/routes/chat.py +311 -76
  100. astrbot/dashboard/routes/config.py +14 -5
  101. astrbot/dashboard/routes/knowledge_base.py +254 -79
  102. astrbot/dashboard/routes/log.py +13 -8
  103. astrbot/dashboard/routes/platform.py +100 -0
  104. astrbot/dashboard/routes/plugin.py +108 -51
  105. astrbot/dashboard/routes/route.py +2 -0
  106. astrbot/dashboard/server.py +9 -4
  107. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
  108. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
  109. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
  110. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
  111. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,10 @@
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
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from enum import Enum
5
8
  from typing import Any
6
9
 
7
10
  from astrbot.core.message.message_event_result import MessageChain
@@ -12,15 +15,100 @@ from .message_session import MessageSesion
12
15
  from .platform_metadata import PlatformMetadata
13
16
 
14
17
 
18
+ class PlatformStatus(Enum):
19
+ """平台运行状态"""
20
+
21
+ PENDING = "pending" # 待启动
22
+ RUNNING = "running" # 运行中
23
+ ERROR = "error" # 发生错误
24
+ STOPPED = "stopped" # 已停止
25
+
26
+
27
+ @dataclass
28
+ class PlatformError:
29
+ """平台错误信息"""
30
+
31
+ message: str
32
+ timestamp: datetime = field(default_factory=datetime.now)
33
+ traceback: str | None = None
34
+
35
+
15
36
  class Platform(abc.ABC):
16
- def __init__(self, event_queue: Queue):
37
+ def __init__(self, config: dict, event_queue: Queue):
17
38
  super().__init__()
39
+ # 平台配置
40
+ self.config = config
18
41
  # 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。
19
42
  self._event_queue = event_queue
20
43
  self.client_self_id = uuid.uuid4().hex
21
44
 
45
+ # 平台运行状态
46
+ self._status: PlatformStatus = PlatformStatus.PENDING
47
+ self._errors: list[PlatformError] = []
48
+ self._started_at: datetime | None = None
49
+
50
+ @property
51
+ def status(self) -> PlatformStatus:
52
+ """获取平台运行状态"""
53
+ return self._status
54
+
55
+ @status.setter
56
+ def status(self, value: PlatformStatus):
57
+ """设置平台运行状态"""
58
+ self._status = value
59
+ if value == PlatformStatus.RUNNING and self._started_at is None:
60
+ self._started_at = datetime.now()
61
+
62
+ @property
63
+ def errors(self) -> list[PlatformError]:
64
+ """获取错误列表"""
65
+ return self._errors
66
+
67
+ @property
68
+ def last_error(self) -> PlatformError | None:
69
+ """获取最近的错误"""
70
+ return self._errors[-1] if self._errors else None
71
+
72
+ def record_error(self, message: str, traceback_str: str | None = None):
73
+ """记录一个错误"""
74
+ self._errors.append(PlatformError(message=message, traceback=traceback_str))
75
+ self._status = PlatformStatus.ERROR
76
+
77
+ def clear_errors(self):
78
+ """清除错误记录"""
79
+ self._errors.clear()
80
+ if self._status == PlatformStatus.ERROR:
81
+ self._status = PlatformStatus.RUNNING
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
+
90
+ def get_stats(self) -> dict:
91
+ """获取平台统计信息"""
92
+ meta = self.meta()
93
+ return {
94
+ "id": meta.id or self.config.get("id"),
95
+ "type": meta.name,
96
+ "display_name": meta.adapter_display_name or meta.name,
97
+ "status": self._status.value,
98
+ "started_at": self._started_at.isoformat() if self._started_at else None,
99
+ "error_count": len(self._errors),
100
+ "last_error": {
101
+ "message": self.last_error.message,
102
+ "timestamp": self.last_error.timestamp.isoformat(),
103
+ "traceback": self.last_error.traceback,
104
+ }
105
+ if self.last_error
106
+ else None,
107
+ "unified_webhook": self.unified_webhook(),
108
+ }
109
+
22
110
  @abc.abstractmethod
23
- def run(self) -> Awaitable[Any]:
111
+ def run(self) -> Coroutine[Any, Any, None]:
24
112
  """得到一个平台的运行实例,需要返回一个协程对象。"""
25
113
  raise NotImplementedError
26
114
 
@@ -36,7 +124,7 @@ class Platform(abc.ABC):
36
124
  self,
37
125
  session: MessageSesion,
38
126
  message_chain: MessageChain,
39
- ) -> Awaitable[Any]:
127
+ ) -> None:
40
128
  """通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。
41
129
 
42
130
  异步方法。
@@ -49,3 +137,20 @@ class Platform(abc.ABC):
49
137
 
50
138
  def get_client(self):
51
139
  """获取平台的客户端对象。"""
140
+
141
+ async def webhook_callback(self, request: Any) -> Any:
142
+ """统一 Webhook 回调入口。
143
+
144
+ 支持统一 Webhook 模式的平台需要实现此方法。
145
+ 当 Dashboard 收到 /api/platform/webhook/{uuid} 请求时,会调用此方法。
146
+
147
+ Args:
148
+ request: Quart 请求对象
149
+
150
+ Returns:
151
+ 响应内容,格式取决于具体平台的要求
152
+
153
+ Raises:
154
+ NotImplementedError: 平台未实现统一 Webhook 模式
155
+ """
156
+ raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式")
@@ -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
@@ -38,9 +38,8 @@ class AiocqhttpAdapter(Platform):
38
38
  platform_settings: dict,
39
39
  event_queue: asyncio.Queue,
40
40
  ) -> None:
41
- super().__init__(event_queue)
41
+ super().__init__(platform_config, event_queue)
42
42
 
43
- self.config = platform_config
44
43
  self.settings = platform_settings
45
44
  self.unique_session = platform_settings["unique_session"]
46
45
  self.host = platform_config["ws_reverse_host"]
@@ -49,7 +48,7 @@ class AiocqhttpAdapter(Platform):
49
48
  self.metadata = PlatformMetadata(
50
49
  name="aiocqhttp",
51
50
  description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。",
52
- id=self.config.get("id"),
51
+ id=cast(str, self.config.get("id")),
53
52
  support_streaming_message=False,
54
53
  )
55
54
 
@@ -128,7 +127,9 @@ class AiocqhttpAdapter(Platform):
128
127
  """OneBot V11 请求类事件"""
129
128
  abm = AstrBotMessage()
130
129
  abm.self_id = str(event.self_id)
131
- 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
+ )
132
133
  abm.type = MessageType.OTHER_MESSAGE
133
134
  if event.get("group_id"):
134
135
  abm.type = MessageType.GROUP_MESSAGE
@@ -154,7 +155,9 @@ class AiocqhttpAdapter(Platform):
154
155
  """OneBot V11 通知类事件"""
155
156
  abm = AstrBotMessage()
156
157
  abm.self_id = str(event.self_id)
157
- abm.sender = MessageMember(user_id=str(event.user_id), nickname=event.user_id)
158
+ abm.sender = MessageMember(
159
+ user_id=str(event.user_id), nickname=str(event.user_id)
160
+ )
158
161
  abm.type = MessageType.OTHER_MESSAGE
159
162
  if event.get("group_id"):
160
163
  abm.group_id = str(event.group_id)
@@ -193,6 +196,7 @@ class AiocqhttpAdapter(Platform):
193
196
  @param event: 事件对象
194
197
  @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。
195
198
  """
199
+ assert event.sender is not None
196
200
  abm = AstrBotMessage()
197
201
  abm.self_id = str(event.self_id)
198
202
  abm.sender = MessageMember(
@@ -202,6 +206,7 @@ class AiocqhttpAdapter(Platform):
202
206
  if event["message_type"] == "group":
203
207
  abm.type = MessageType.GROUP_MESSAGE
204
208
  abm.group_id = str(event.group_id)
209
+ abm.group = Group(str(event.group_id))
205
210
  abm.group.group_name = event.get("group_name", "N/A")
206
211
  elif event["message_type"] == "private":
207
212
  abm.type = MessageType.FRIEND_MESSAGE
@@ -227,7 +232,7 @@ class AiocqhttpAdapter(Platform):
227
232
  await self.bot.send(event, err)
228
233
  except BaseException as e:
229
234
  logger.error(f"回复消息失败: {e}")
230
- return None
235
+ raise ValueError(err)
231
236
 
232
237
  # 按消息段类型类型适配
233
238
  for t, m_group in itertools.groupby(event.message, key=lambda x: x["type"]):
@@ -416,7 +421,7 @@ class AiocqhttpAdapter(Platform):
416
421
 
417
422
  async def shutdown_trigger_placeholder(self):
418
423
  await self.shutdown_event.wait()
419
- logger.info("aiocqhttp 适配器已被优雅地关闭")
424
+ logger.info("aiocqhttp 适配器已被关闭")
420
425
 
421
426
  def meta(self) -> PlatformMetadata:
422
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
@@ -47,21 +48,21 @@ class DingtalkPlatformAdapter(Platform):
47
48
  platform_settings: dict,
48
49
  event_queue: asyncio.Queue,
49
50
  ) -> None:
50
- super().__init__(event_queue)
51
-
52
- self.config = platform_config
51
+ super().__init__(platform_config, event_queue)
53
52
 
54
53
  self.unique_session = platform_settings["unique_session"]
55
54
 
56
55
  self.client_id = platform_config["client_id"]
57
56
  self.client_secret = platform_config["client_secret"]
58
57
 
58
+ outer_self = self
59
+
59
60
  class AstrCallbackClient(dingtalk_stream.ChatbotHandler):
60
- async def process(self_, message: dingtalk_stream.CallbackMessage):
61
+ async def process(self, message: dingtalk_stream.CallbackMessage):
61
62
  logger.debug(f"dingtalk: {message.data}")
62
63
  im = dingtalk_stream.ChatbotMessage.from_dict(message.data)
63
- abm = await self.convert_msg(im)
64
- await self.handle_msg(abm)
64
+ abm = await outer_self.convert_msg(im)
65
+ await outer_self.handle_msg(abm)
65
66
 
66
67
  return AckMessage.STATUS_OK, "OK"
67
68
 
@@ -75,14 +76,15 @@ class DingtalkPlatformAdapter(Platform):
75
76
  self.client,
76
77
  )
77
78
  self.client_ = client # 用于 websockets 的 client
79
+ self._shutdown_event: threading.Event | None = None
78
80
 
79
- def _id_to_sid(self, dingtalk_id: str | None) -> str | None:
81
+ def _id_to_sid(self, dingtalk_id: str | None) -> str:
80
82
  if not dingtalk_id:
81
- return dingtalk_id
83
+ return dingtalk_id or "unknown"
82
84
  prefix = "$:LWCP_v1:$"
83
85
  if dingtalk_id.startswith(prefix):
84
86
  return dingtalk_id[len(prefix) :]
85
- return dingtalk_id
87
+ return dingtalk_id or "unknown"
86
88
 
87
89
  async def send_by_session(
88
90
  self,
@@ -95,7 +97,7 @@ class DingtalkPlatformAdapter(Platform):
95
97
  return PlatformMetadata(
96
98
  name="dingtalk",
97
99
  description="钉钉机器人官方 API 适配器",
98
- id=self.config.get("id"),
100
+ id=cast(str, self.config.get("id")),
99
101
  support_streaming_message=False,
100
102
  )
101
103
 
@@ -106,7 +108,7 @@ class DingtalkPlatformAdapter(Platform):
106
108
  abm = AstrBotMessage()
107
109
  abm.message = []
108
110
  abm.message_str = ""
109
- abm.timestamp = int(message.create_at / 1000)
111
+ abm.timestamp = int(cast(int, message.create_at) / 1000)
110
112
  abm.type = (
111
113
  MessageType.GROUP_MESSAGE
112
114
  if message.conversation_type == "2"
@@ -117,7 +119,7 @@ class DingtalkPlatformAdapter(Platform):
117
119
  nickname=message.sender_nick,
118
120
  )
119
121
  abm.self_id = self._id_to_sid(message.chatbot_user_id)
120
- abm.message_id = message.message_id
122
+ abm.message_id = cast(str, message.message_id)
121
123
  abm.raw_message = message
122
124
 
123
125
  if abm.type == MessageType.GROUP_MESSAGE:
@@ -134,14 +136,16 @@ class DingtalkPlatformAdapter(Platform):
134
136
  else:
135
137
  abm.session_id = abm.sender.user_id
136
138
 
137
- message_type: str = message.message_type
139
+ message_type: str = cast(str, message.message_type)
138
140
  match message_type:
139
141
  case "text":
140
142
  abm.message_str = message.text.content.strip()
141
143
  abm.message.append(Plain(abm.message_str))
142
144
  case "richText":
143
- rtc: dingtalk_stream.RichTextContent = message.rich_text_content
144
- 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)
145
149
  for content in contents:
146
150
  plains = ""
147
151
  if "text" in content:
@@ -150,7 +154,7 @@ class DingtalkPlatformAdapter(Platform):
150
154
  elif "type" in content and content["type"] == "picture":
151
155
  f_path = await self.download_ding_file(
152
156
  content["downloadCode"],
153
- message.robot_code,
157
+ cast(str, message.robot_code),
154
158
  "jpg",
155
159
  )
156
160
  abm.message.append(Image.fromFileSystem(f_path))
@@ -195,7 +199,7 @@ class DingtalkPlatformAdapter(Platform):
195
199
  logger.error(
196
200
  f"下载钉钉文件失败: {resp.status}, {await resp.text()}",
197
201
  )
198
- return None
202
+ return ""
199
203
  resp_data = await resp.json()
200
204
  download_url = resp_data["data"]["downloadUrl"]
201
205
  await download_file(download_url, f_path)
@@ -215,7 +219,7 @@ class DingtalkPlatformAdapter(Platform):
215
219
  logger.error(
216
220
  f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}",
217
221
  )
218
- return None
222
+ return ""
219
223
  return (await resp.json())["data"]["accessToken"]
220
224
 
221
225
  async def handle_msg(self, abm: AstrBotMessage):
@@ -241,7 +245,7 @@ class DingtalkPlatformAdapter(Platform):
241
245
  task.result()
242
246
  except Exception as e:
243
247
  if "Graceful shutdown" in str(e):
244
- logger.info("钉钉适配器已被优雅地关闭")
248
+ logger.info("钉钉适配器已被关闭")
245
249
  return
246
250
  logger.error(f"钉钉机器人启动失败: {e}")
247
251
 
@@ -252,9 +256,11 @@ class DingtalkPlatformAdapter(Platform):
252
256
  def monkey_patch_close():
253
257
  raise KeyboardInterrupt("Graceful shutdown")
254
258
 
255
- self.client_.open_connection = monkey_patch_close
256
- await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
257
- 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()
258
264
 
259
265
  def get_client(self):
260
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