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