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,10 +1,10 @@
1
1
  import asyncio
2
2
  import re
3
3
  import sys
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  import discord
7
- from discord.abc import Messageable
7
+ from discord.abc import GuildChannel, Messageable, PrivateChannel
8
8
  from discord.channel import DMChannel
9
9
 
10
10
  from astrbot import logger
@@ -46,7 +46,7 @@ class DiscordPlatformAdapter(Platform):
46
46
  ) -> None:
47
47
  super().__init__(platform_config, event_queue)
48
48
  self.settings = platform_settings
49
- self.client_self_id = None
49
+ self.client_self_id: str | None = None
50
50
  self.registered_handlers = []
51
51
  # 指令注册相关
52
52
  self.enable_command_register = self.config.get("discord_command_register", True)
@@ -62,6 +62,12 @@ class DiscordPlatformAdapter(Platform):
62
62
  message_chain: MessageChain,
63
63
  ):
64
64
  """通过会话发送消息"""
65
+ if self.client.user is None:
66
+ logger.error(
67
+ "[Discord] 客户端未就绪 (self.client.user is None),无法发送消息"
68
+ )
69
+ return
70
+
65
71
  # 创建一个 message_obj 以便在 event 中使用
66
72
  message_obj = AstrBotMessage()
67
73
  if "_" in session.session_id:
@@ -89,7 +95,7 @@ class DiscordPlatformAdapter(Platform):
89
95
  user_id=str(self.client_self_id),
90
96
  nickname=self.client.user.display_name,
91
97
  )
92
- message_obj.self_id = self.client_self_id
98
+ message_obj.self_id = cast(str, self.client_self_id)
93
99
  message_obj.session_id = session.session_id
94
100
  message_obj.message = message_chain.chain
95
101
 
@@ -110,7 +116,7 @@ class DiscordPlatformAdapter(Platform):
110
116
  return PlatformMetadata(
111
117
  "discord",
112
118
  "Discord 适配器",
113
- id=self.config.get("id"),
119
+ id=cast(str, self.config.get("id")),
114
120
  default_config_tmpl=self.config,
115
121
  support_streaming_message=False,
116
122
  )
@@ -160,7 +166,7 @@ class DiscordPlatformAdapter(Platform):
160
166
 
161
167
  def _get_message_type(
162
168
  self,
163
- channel: Messageable,
169
+ channel: Messageable | GuildChannel | PrivateChannel,
164
170
  guild_id: int | None = None,
165
171
  ) -> MessageType:
166
172
  """根据 channel 对象和 guild_id 判断消息类型"""
@@ -170,13 +176,15 @@ class DiscordPlatformAdapter(Platform):
170
176
  return MessageType.FRIEND_MESSAGE
171
177
  return MessageType.GROUP_MESSAGE
172
178
 
173
- def _get_channel_id(self, channel: Messageable) -> str:
179
+ def _get_channel_id(
180
+ self, channel: Messageable | GuildChannel | PrivateChannel
181
+ ) -> str:
174
182
  """根据 channel 对象获取ID"""
175
183
  return str(getattr(channel, "id", None))
176
184
 
177
185
  def _convert_message_to_abm(self, data: dict) -> AstrBotMessage:
178
186
  """将普通消息转换为 AstrBotMessage"""
179
- message: discord.Message = data["message"]
187
+ message = data["message"]
180
188
 
181
189
  content = message.content
182
190
 
@@ -233,7 +241,7 @@ class DiscordPlatformAdapter(Platform):
233
241
  )
234
242
  abm.message = message_chain
235
243
  abm.raw_message = message
236
- abm.self_id = self.client_self_id
244
+ abm.self_id = cast(str, self.client_self_id)
237
245
  abm.session_id = str(message.channel.id)
238
246
  abm.message_id = str(message.id)
239
247
  return abm
@@ -254,32 +262,52 @@ class DiscordPlatformAdapter(Platform):
254
262
  interaction_followup_webhook=followup_webhook,
255
263
  )
256
264
 
265
+ if self.client.user is None:
266
+ logger.error(
267
+ "[Discord] 客户端未就绪 (self.client.user is None),无法处理消息"
268
+ )
269
+ return
270
+
257
271
  # 检查是否为斜杠指令
258
272
  is_slash_command = message_event.interaction_followup_webhook is not None
259
273
 
274
+ # 1. 优先处理斜杠指令
275
+ if is_slash_command:
276
+ message_event.is_wake = True
277
+ message_event.is_at_or_wake_command = True
278
+ self.commit_event(message_event)
279
+ return
280
+
281
+ # 2. 处理普通消息(提及检测)
282
+ # 确保 raw_message 是 discord.Message 类型,以便静态检查通过
283
+ raw_message = message.raw_message
284
+ if not isinstance(raw_message, discord.Message):
285
+ logger.warning(
286
+ f"[Discord] 收到非 Message 类型的消息: {type(raw_message)},已忽略。"
287
+ )
288
+ return
289
+
260
290
  # 检查是否被@(User Mention 或 Bot 拥有的 Role Mention)
261
291
  is_mention = False
292
+
262
293
  # User Mention
263
- if (
264
- self.client
265
- and self.client.user
266
- and hasattr(message.raw_message, "mentions")
267
- ):
268
- if self.client.user in message.raw_message.mentions:
269
- is_mention = True
294
+ # 此时 Pylance 知道 raw_message 是 discord.Message,具有 mentions 属性
295
+ if self.client.user in raw_message.mentions:
296
+ is_mention = True
297
+
270
298
  # Role Mention(Bot 拥有的角色被提及)
271
- if not is_mention and hasattr(message.raw_message, "role_mentions"):
299
+ if not is_mention and raw_message.role_mentions:
272
300
  bot_member = None
273
- if hasattr(message.raw_message, "guild") and message.raw_message.guild:
301
+ if raw_message.guild:
274
302
  try:
275
- bot_member = message.raw_message.guild.get_member(
303
+ bot_member = raw_message.guild.get_member(
276
304
  self.client.user.id,
277
305
  )
278
306
  except Exception:
279
307
  bot_member = None
280
308
  if bot_member and hasattr(bot_member, "roles"):
281
309
  bot_roles = set(bot_member.roles)
282
- mentioned_roles = set(message.raw_message.role_mentions)
310
+ mentioned_roles = set(raw_message.role_mentions)
283
311
  if (
284
312
  bot_roles
285
313
  and mentioned_roles
@@ -287,8 +315,8 @@ class DiscordPlatformAdapter(Platform):
287
315
  ):
288
316
  is_mention = True
289
317
 
290
- # 如果是斜杠指令或被@的消息,设置为唤醒状态
291
- if is_slash_command or is_mention:
318
+ # 如果是被@的消息,设置为唤醒状态
319
+ if is_mention:
292
320
  message_event.is_wake = True
293
321
  message_event.is_at_or_wake_command = True
294
322
 
@@ -424,7 +452,7 @@ class DiscordPlatformAdapter(Platform):
424
452
  )
425
453
  abm.message = [Plain(text=message_str_for_filter)]
426
454
  abm.raw_message = ctx.interaction
427
- abm.self_id = self.client_self_id
455
+ abm.self_id = cast(str, self.client_self_id)
428
456
  abm.session_id = str(ctx.channel_id)
429
457
  abm.message_id = str(ctx.interaction.id)
430
458
 
@@ -437,7 +465,7 @@ class DiscordPlatformAdapter(Platform):
437
465
  def _extract_command_info(
438
466
  event_filter: Any,
439
467
  handler_metadata: StarHandlerMetadata,
440
- ) -> tuple[str, str, CommandFilter] | None:
468
+ ) -> tuple[str, str, CommandFilter | None] | None:
441
469
  """从事件过滤器中提取指令信息"""
442
470
  cmd_name = None
443
471
  # is_group = False
@@ -4,8 +4,10 @@ import binascii
4
4
  from collections.abc import AsyncGenerator
5
5
  from io import BytesIO
6
6
  from pathlib import Path
7
+ from typing import cast
7
8
 
8
9
  import discord
10
+ from discord.types.interactions import ComponentInteractionData
9
11
 
10
12
  from astrbot import logger
11
13
  from astrbot.api.event import AstrMessageEvent, MessageChain
@@ -85,6 +87,9 @@ class DiscordPlatformEvent(AstrMessageEvent):
85
87
  channel = await self._get_channel()
86
88
  if not channel:
87
89
  return
90
+ if not isinstance(channel, discord.abc.Messageable):
91
+ logger.error(f"[Discord] 频道 {channel.id} 不是可发送消息的类型")
92
+ return
88
93
  await channel.send(**kwargs)
89
94
 
90
95
  except Exception as e:
@@ -107,7 +112,9 @@ class DiscordPlatformEvent(AstrMessageEvent):
107
112
  await self.send(buffer)
108
113
  return await super().send_streaming(generator, use_fallback)
109
114
 
110
- async def _get_channel(self) -> discord.abc.Messageable | None:
115
+ async def _get_channel(
116
+ self,
117
+ ) -> discord.Thread | discord.abc.GuildChannel | discord.abc.PrivateChannel | None:
111
118
  """获取当前事件对应的频道对象"""
112
119
  try:
113
120
  channel_id = int(self.session_id)
@@ -121,7 +128,13 @@ class DiscordPlatformEvent(AstrMessageEvent):
121
128
  async def _parse_to_discord(
122
129
  self,
123
130
  message: MessageChain,
124
- ) -> tuple[str, list[discord.File], discord.ui.View | None, list[discord.Embed]]:
131
+ ) -> tuple[
132
+ str,
133
+ list[discord.File],
134
+ discord.ui.View | None,
135
+ list[discord.Embed],
136
+ str | int | None,
137
+ ]:
125
138
  """将 MessageChain 解析为 Discord 发送所需的内容"""
126
139
  content_parts = []
127
140
  files = []
@@ -261,7 +274,9 @@ class DiscordPlatformEvent(AstrMessageEvent):
261
274
  self.message_obj.raw_message,
262
275
  "add_reaction",
263
276
  ):
264
- await self.message_obj.raw_message.add_reaction(emoji)
277
+ await cast(discord.Message, self.message_obj.raw_message).add_reaction(
278
+ emoji
279
+ )
265
280
  except Exception as e:
266
281
  logger.error(f"[Discord] 添加反应失败: {e}")
267
282
 
@@ -270,7 +285,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
270
285
  return (
271
286
  hasattr(self.message_obj, "raw_message")
272
287
  and hasattr(self.message_obj.raw_message, "type")
273
- and self.message_obj.raw_message.type
288
+ and cast(discord.Interaction, self.message_obj.raw_message).type
274
289
  == discord.InteractionType.application_command
275
290
  )
276
291
 
@@ -279,14 +294,18 @@ class DiscordPlatformEvent(AstrMessageEvent):
279
294
  return (
280
295
  hasattr(self.message_obj, "raw_message")
281
296
  and hasattr(self.message_obj.raw_message, "type")
282
- and self.message_obj.raw_message.type == discord.InteractionType.component
297
+ and cast(discord.Interaction, self.message_obj.raw_message).type
298
+ == discord.InteractionType.component
283
299
  )
284
300
 
285
301
  def get_interaction_custom_id(self) -> str:
286
302
  """获取交互组件的custom_id"""
287
303
  if self.is_button_interaction():
288
304
  try:
289
- return self.message_obj.raw_message.data.get("custom_id", "")
305
+ return cast(
306
+ ComponentInteractionData,
307
+ cast(discord.Interaction, self.message_obj.raw_message).data,
308
+ ).get("custom_id", "")
290
309
  except Exception:
291
310
  pass
292
311
  return ""
@@ -299,7 +318,9 @@ class DiscordPlatformEvent(AstrMessageEvent):
299
318
  ):
300
319
  return any(
301
320
  mention.id == int(self.message_obj.self_id)
302
- for mention in self.message_obj.raw_message.mentions
321
+ for mention in cast(
322
+ discord.Message, self.message_obj.raw_message
323
+ ).mentions
303
324
  )
304
325
  return False
305
326
 
@@ -309,5 +330,5 @@ class DiscordPlatformEvent(AstrMessageEvent):
309
330
  self.message_obj.raw_message,
310
331
  "clean_content",
311
332
  ):
312
- return self.message_obj.raw_message.clean_content
333
+ return cast(discord.Message, self.message_obj.raw_message).clean_content
313
334
  return self.message_str
@@ -2,10 +2,17 @@ import asyncio
2
2
  import base64
3
3
  import json
4
4
  import re
5
+ import time
5
6
  import uuid
7
+ from typing import Any, cast
6
8
 
7
9
  import lark_oapi as lark
8
- from lark_oapi.api.im.v1 import *
10
+ from lark_oapi.api.im.v1 import (
11
+ CreateMessageRequest,
12
+ CreateMessageRequestBody,
13
+ GetMessageResourceRequest,
14
+ )
15
+ from lark_oapi.api.im.v1.processor import P2ImMessageReceiveV1Processor
9
16
 
10
17
  import astrbot.api.message_components as Comp
11
18
  from astrbot import logger
@@ -18,9 +25,11 @@ from astrbot.api.platform import (
18
25
  PlatformMetadata,
19
26
  )
20
27
  from astrbot.core.platform.astr_message_event import MessageSesion
28
+ from astrbot.core.utils.webhook_utils import log_webhook_info
21
29
 
22
30
  from ...register import register_platform_adapter
23
31
  from .lark_event import LarkMessageEvent
32
+ from .server import LarkWebhookServer
24
33
 
25
34
 
26
35
  @register_platform_adapter(
@@ -42,9 +51,13 @@ class LarkPlatformAdapter(Platform):
42
51
  self.domain = platform_config.get("domain", lark.FEISHU_DOMAIN)
43
52
  self.bot_name = platform_config.get("lark_bot_name", "astrbot")
44
53
 
54
+ # socket or webhook
55
+ self.connection_mode = platform_config.get("lark_connection_mode", "socket")
56
+
45
57
  if not self.bot_name:
46
58
  logger.warning("未设置飞书机器人名称,@ 机器人可能得不到回复。")
47
59
 
60
+ # 初始化 WebSocket 长连接相关配置
48
61
  async def on_msg_event_recv(event: lark.im.v1.P2ImMessageReceiveV1):
49
62
  await self.convert_msg(event)
50
63
 
@@ -57,6 +70,8 @@ class LarkPlatformAdapter(Platform):
57
70
  .build()
58
71
  )
59
72
 
73
+ self.do_v2_msg_event = do_v2_msg_event
74
+
60
75
  self.client = lark.ws.Client(
61
76
  app_id=self.appid,
62
77
  app_secret=self.appsecret,
@@ -66,14 +81,56 @@ class LarkPlatformAdapter(Platform):
66
81
  )
67
82
 
68
83
  self.lark_api = (
69
- lark.Client.builder().app_id(self.appid).app_secret(self.appsecret).build()
84
+ lark.Client.builder()
85
+ .app_id(self.appid)
86
+ .app_secret(self.appsecret)
87
+ .log_level(lark.LogLevel.ERROR)
88
+ .domain(self.domain)
89
+ .build()
70
90
  )
71
91
 
92
+ self.webhook_server = None
93
+ if self.connection_mode == "webhook":
94
+ self.webhook_server = LarkWebhookServer(platform_config, event_queue)
95
+ self.webhook_server.set_callback(self.handle_webhook_event)
96
+
97
+ self.event_id_timestamps: dict[str, float] = {}
98
+
99
+ def _clean_expired_events(self):
100
+ """清理超过 30 分钟的事件记录"""
101
+ current_time = time.time()
102
+ expired_keys = [
103
+ event_id
104
+ for event_id, timestamp in self.event_id_timestamps.items()
105
+ if current_time - timestamp > 1800
106
+ ]
107
+ for event_id in expired_keys:
108
+ del self.event_id_timestamps[event_id]
109
+
110
+ def _is_duplicate_event(self, event_id: str) -> bool:
111
+ """检查事件是否重复
112
+
113
+ Args:
114
+ event_id: 事件ID
115
+
116
+ Returns:
117
+ True 表示重复事件,False 表示新事件
118
+ """
119
+ self._clean_expired_events()
120
+ if event_id in self.event_id_timestamps:
121
+ return True
122
+ self.event_id_timestamps[event_id] = time.time()
123
+ return False
124
+
72
125
  async def send_by_session(
73
126
  self,
74
127
  session: MessageSesion,
75
128
  message_chain: MessageChain,
76
129
  ):
130
+ if self.lark_api.im is None:
131
+ logger.error("[Lark] API Client im 模块未初始化,无法发送消息")
132
+ return
133
+
77
134
  res = await LarkMessageEvent._convert_to_lark(message_chain, self.lark_api)
78
135
  wrapped = {
79
136
  "zh_cn": {
@@ -114,14 +171,25 @@ class LarkPlatformAdapter(Platform):
114
171
  return PlatformMetadata(
115
172
  name="lark",
116
173
  description="飞书机器人官方 API 适配器",
117
- id=self.config.get("id"),
174
+ id=cast(str, self.config.get("id")),
118
175
  support_streaming_message=False,
119
176
  )
120
177
 
121
178
  async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1):
179
+ if event.event is None:
180
+ logger.debug("[Lark] 收到空事件(event.event is None)")
181
+ return
122
182
  message = event.event.message
183
+ if message is None:
184
+ logger.debug("[Lark] 事件中没有消息体(message is None)")
185
+ return
186
+
123
187
  abm = AstrBotMessage()
124
- abm.timestamp = int(message.create_time) / 1000
188
+
189
+ if message.create_time:
190
+ abm.timestamp = int(message.create_time) // 1000
191
+ else:
192
+ abm.timestamp = int(time.time())
125
193
  abm.message = []
126
194
  abm.type = (
127
195
  MessageType.GROUP_MESSAGE
@@ -136,14 +204,28 @@ class LarkPlatformAdapter(Platform):
136
204
  at_list = {}
137
205
  if message.mentions:
138
206
  for m in message.mentions:
139
- at_list[m.key] = Comp.At(qq=m.id.open_id, name=m.name)
207
+ if m.id is None:
208
+ continue
209
+ # 飞书 open_id 可能是 None,这里做个防护
210
+ open_id = m.id.open_id if m.id.open_id else ""
211
+ at_list[m.key] = Comp.At(qq=open_id, name=m.name)
212
+
140
213
  if m.name == self.bot_name:
141
- abm.self_id = m.id.open_id
214
+ if m.id.open_id is not None:
215
+ abm.self_id = m.id.open_id
142
216
 
143
- content_json_b = json.loads(message.content)
217
+ if message.content is None:
218
+ logger.warning("[Lark] 消息内容为空")
219
+ return
220
+
221
+ try:
222
+ content_json_b = json.loads(message.content)
223
+ except json.JSONDecodeError:
224
+ logger.error(f"[Lark] 解析消息内容失败: {message.content}")
225
+ return
144
226
 
145
227
  if message.message_type == "text":
146
- message_str_raw = content_json_b["text"] # 带有 @ 的消息
228
+ message_str_raw = content_json_b.get("text", "") # 带有 @ 的消息
147
229
  at_pattern = r"(@_user_\d+)" # 可以根据需求修改正则
148
230
  # at_users = re.findall(at_pattern, message_str_raw)
149
231
  # 拆分文本,去掉AT符号部分
@@ -168,27 +250,47 @@ class LarkPlatformAdapter(Platform):
168
250
  content_json_b = _ls
169
251
  elif message.message_type == "image":
170
252
  content_json_b = [
171
- {"tag": "img", "image_key": content_json_b["image_key"], "style": []},
253
+ {
254
+ "tag": "img",
255
+ "image_key": content_json_b.get("image_key"),
256
+ "style": [],
257
+ },
172
258
  ]
173
259
 
174
260
  if message.message_type in ("post", "image"):
175
261
  for comp in content_json_b:
176
- if comp["tag"] == "at":
177
- abm.message.append(at_list[comp["user_id"]])
178
- elif comp["tag"] == "text" and comp["text"].strip():
262
+ if comp.get("tag") == "at":
263
+ user_id = comp.get("user_id")
264
+ if user_id in at_list:
265
+ abm.message.append(at_list[user_id])
266
+ elif comp.get("tag") == "text" and comp.get("text", "").strip():
179
267
  abm.message.append(Comp.Plain(comp["text"].strip()))
180
- elif comp["tag"] == "img":
181
- image_key = comp["image_key"]
268
+ elif comp.get("tag") == "img":
269
+ image_key = comp.get("image_key")
270
+ if not image_key:
271
+ continue
272
+
182
273
  request = (
183
274
  GetMessageResourceRequest.builder()
184
- .message_id(message.message_id)
275
+ .message_id(cast(str, message.message_id))
185
276
  .file_key(image_key)
186
277
  .type("image")
187
278
  .build()
188
279
  )
280
+
281
+ if self.lark_api.im is None:
282
+ logger.error("[Lark] API Client im 模块未初始化")
283
+ continue
284
+
189
285
  response = await self.lark_api.im.v1.message_resource.aget(request)
190
286
  if not response.success():
191
287
  logger.error(f"无法下载飞书图片: {image_key}")
288
+ continue
289
+
290
+ if response.file is None:
291
+ logger.error(f"飞书图片响应中不包含文件流: {image_key}")
292
+ continue
293
+
192
294
  image_bytes = response.file.read()
193
295
  image_base64 = base64.b64encode(image_bytes).decode()
194
296
  abm.message.append(Comp.Image.fromBase64(image_base64))
@@ -196,6 +298,19 @@ class LarkPlatformAdapter(Platform):
196
298
  for comp in abm.message:
197
299
  if isinstance(comp, Comp.Plain):
198
300
  abm.message_str += comp.text
301
+
302
+ if message.message_id is None:
303
+ logger.error("[Lark] 消息缺少 message_id")
304
+ return
305
+
306
+ if (
307
+ event.event.sender is None
308
+ or event.event.sender.sender_id is None
309
+ or event.event.sender.sender_id.open_id is None
310
+ ):
311
+ logger.error("[Lark] 消息发送者信息不完整")
312
+ return
313
+
199
314
  abm.message_id = message.message_id
200
315
  abm.raw_message = message
201
316
  abm.sender = MessageMember(
@@ -227,13 +342,61 @@ class LarkPlatformAdapter(Platform):
227
342
 
228
343
  self._event_queue.put_nowait(event)
229
344
 
345
+ async def handle_webhook_event(self, event_data: dict):
346
+ """处理 Webhook 事件
347
+
348
+ Args:
349
+ event_data: Webhook 事件数据
350
+ """
351
+ try:
352
+ header = event_data.get("header", {})
353
+ event_id = header.get("event_id", "")
354
+ if event_id and self._is_duplicate_event(event_id):
355
+ logger.debug(f"[Lark Webhook] 跳过重复事件: {event_id}")
356
+ return
357
+ event_type = header.get("event_type", "")
358
+ if event_type == "im.message.receive_v1":
359
+ processor = P2ImMessageReceiveV1Processor(self.do_v2_msg_event)
360
+ data = (processor.type())(event_data)
361
+ processor.do(data)
362
+ else:
363
+ logger.debug(f"[Lark Webhook] 未处理的事件类型: {event_type}")
364
+ except Exception as e:
365
+ logger.error(f"[Lark Webhook] 处理事件失败: {e}", exc_info=True)
366
+
230
367
  async def run(self):
231
- # self.client.start()
232
- await self.client._connect()
368
+ if self.connection_mode == "webhook":
369
+ # Webhook 模式
370
+ if self.webhook_server is None:
371
+ logger.error("[Lark] Webhook 模式已启用,但 webhook_server 未初始化")
372
+ return
373
+
374
+ webhook_uuid = self.config.get("webhook_uuid")
375
+ if webhook_uuid:
376
+ log_webhook_info(f"{self.meta().id}(飞书 Webhook)", webhook_uuid)
377
+ else:
378
+ logger.warning("[Lark] Webhook 模式已启用,但未配置 webhook_uuid")
379
+ else:
380
+ # 长连接模式
381
+ await self.client._connect()
382
+
383
+ async def webhook_callback(self, request: Any) -> Any:
384
+ """统一 Webhook 回调入口"""
385
+ if not self.webhook_server:
386
+ return {"error": "Webhook server not initialized"}, 500
387
+
388
+ return await self.webhook_server.handle_callback(request)
233
389
 
234
390
  async def terminate(self):
235
- await self.client._disconnect()
236
- logger.info("飞书(Lark) 适配器已被优雅地关闭")
391
+ if self.connection_mode == "socket":
392
+ await self.client._disconnect()
393
+ logger.info("飞书(Lark) 适配器已关闭")
237
394
 
238
- def get_client(self) -> lark.Client:
395
+ def get_client(self) -> lark.ws.Client:
239
396
  return self.client
397
+
398
+ def unified_webhook(self) -> bool:
399
+ return bool(
400
+ self.config.get("lark_connection_mode", "") == "webhook"
401
+ and self.config.get("webhook_uuid")
402
+ )
@@ -5,7 +5,15 @@ import uuid
5
5
  from io import BytesIO
6
6
 
7
7
  import lark_oapi as lark
8
- from lark_oapi.api.im.v1 import *
8
+ from lark_oapi.api.im.v1 import (
9
+ CreateImageRequest,
10
+ CreateImageRequestBody,
11
+ CreateMessageReactionRequest,
12
+ CreateMessageReactionRequestBody,
13
+ Emoji,
14
+ ReplyMessageRequest,
15
+ ReplyMessageRequestBody,
16
+ )
9
17
 
10
18
  from astrbot import logger
11
19
  from astrbot.api.event import AstrMessageEvent, MessageChain
@@ -44,7 +52,7 @@ class LarkMessageEvent(AstrMessageEvent):
44
52
  file_path = comp.file.replace("file:///", "")
45
53
  elif comp.file and comp.file.startswith("http"):
46
54
  image_file_path = await download_image_by_url(comp.file)
47
- file_path = image_file_path
55
+ file_path = image_file_path if image_file_path else ""
48
56
  elif comp.file and comp.file.startswith("base64://"):
49
57
  base64_str = comp.file.removeprefix("base64://")
50
58
  image_data = base64.b64decode(base64_str)
@@ -54,10 +62,17 @@ class LarkMessageEvent(AstrMessageEvent):
54
62
  with open(file_path, "wb") as f:
55
63
  f.write(BytesIO(image_data).getvalue())
56
64
  else:
57
- file_path = comp.file
65
+ file_path = comp.file if comp.file else ""
58
66
 
59
67
  if image_file is None:
60
- image_file = open(file_path, "rb")
68
+ if not file_path:
69
+ logger.error("[Lark] 图片路径为空,无法上传")
70
+ continue
71
+ try:
72
+ image_file = open(file_path, "rb")
73
+ except Exception as e:
74
+ logger.error(f"[Lark] 无法打开图片文件: {e}")
75
+ continue
61
76
 
62
77
  request = (
63
78
  CreateImageRequest.builder()
@@ -69,9 +84,20 @@ class LarkMessageEvent(AstrMessageEvent):
69
84
  )
70
85
  .build()
71
86
  )
87
+
88
+ if lark_client.im is None:
89
+ logger.error("[Lark] API Client im 模块未初始化,无法上传图片")
90
+ continue
91
+
72
92
  response = await lark_client.im.v1.image.acreate(request)
73
93
  if not response.success():
74
94
  logger.error(f"无法上传飞书图片({response.code}): {response.msg}")
95
+ continue
96
+
97
+ if response.data is None:
98
+ logger.error("[Lark] 上传图片成功但未返回数据(data is None)")
99
+ continue
100
+
75
101
  image_key = response.data.image_key
76
102
  logger.debug(image_key)
77
103
  ret.append(_stage)
@@ -107,6 +133,10 @@ class LarkMessageEvent(AstrMessageEvent):
107
133
  .build()
108
134
  )
109
135
 
136
+ if self.bot.im is None:
137
+ logger.error("[Lark] API Client im 模块未初始化,无法回复消息")
138
+ return
139
+
110
140
  response = await self.bot.im.v1.message.areply(request)
111
141
 
112
142
  if not response.success():
@@ -115,6 +145,10 @@ class LarkMessageEvent(AstrMessageEvent):
115
145
  await super().send(message)
116
146
 
117
147
  async def react(self, emoji: str):
148
+ if self.bot.im is None:
149
+ logger.error("[Lark] API Client im 模块未初始化,无法发送表情")
150
+ return
151
+
118
152
  request = (
119
153
  CreateMessageReactionRequest.builder()
120
154
  .message_id(self.message_obj.message_id)
@@ -125,6 +159,7 @@ class LarkMessageEvent(AstrMessageEvent):
125
159
  )
126
160
  .build()
127
161
  )
162
+
128
163
  response = await self.bot.im.v1.message_reaction.acreate(request)
129
164
  if not response.success():
130
165
  logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}")