AstrBot 4.8.0__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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
- astrbot/core/agent/tool.py +7 -2
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +59 -1
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +2 -3
- astrbot/core/db/migration/migra_3_to_4.py +2 -0
- astrbot/core/db/migration/sqlite_v3.py +6 -4
- astrbot/core/db/po.py +16 -15
- astrbot/core/db/sqlite.py +4 -3
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
- astrbot/core/event_bus.py +6 -1
- astrbot/core/knowledge_base/retrieval/manager.py +5 -1
- astrbot/core/log.py +2 -1
- astrbot/core/message/components.py +9 -3
- astrbot/core/persona_mgr.py +2 -2
- astrbot/core/pipeline/content_safety_check/stage.py +1 -1
- astrbot/core/pipeline/context_utils.py +2 -1
- astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
- astrbot/core/pipeline/process_stage/stage.py +1 -1
- astrbot/core/pipeline/respond/stage.py +4 -2
- astrbot/core/pipeline/result_decorate/stage.py +68 -21
- astrbot/core/pipeline/scheduler.py +5 -1
- astrbot/core/pipeline/waking_check/stage.py +10 -0
- astrbot/core/platform/astr_message_event.py +5 -3
- astrbot/core/platform/astrbot_message.py +2 -2
- astrbot/core/platform/manager.py +4 -0
- astrbot/core/platform/platform.py +11 -3
- astrbot/core/platform/platform_metadata.py +1 -1
- astrbot/core/platform/register.py +1 -0
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +9 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
- astrbot/core/platform/sources/discord/client.py +16 -4
- astrbot/core/platform/sources/discord/components.py +2 -2
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +52 -24
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +177 -19
- astrbot/core/platform/sources/lark/lark_event.py +39 -4
- astrbot/core/platform/sources/lark/server.py +206 -0
- astrbot/core/platform/sources/misskey/misskey_adapter.py +2 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
- astrbot/core/platform/sources/slack/client.py +9 -2
- astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
- astrbot/core/platform/sources/slack/slack_event.py +8 -7
- astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
- astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
- astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
- astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
- astrbot/core/provider/func_tool_manager.py +3 -3
- astrbot/core/provider/manager.py +130 -74
- astrbot/core/provider/provider.py +12 -1
- astrbot/core/provider/sources/azure_tts_source.py +31 -9
- astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
- astrbot/core/provider/sources/dashscope_tts.py +3 -2
- astrbot/core/provider/sources/edge_tts_source.py +1 -1
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
- astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
- astrbot/core/provider/sources/gemini_source.py +12 -10
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
- astrbot/core/provider/sources/openai_embedding_source.py +2 -2
- astrbot/core/provider/sources/openai_source.py +4 -0
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
- astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
- astrbot/core/provider/sources/whisper_api_source.py +1 -1
- astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
- astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
- astrbot/core/star/context.py +2 -2
- astrbot/core/star/register/star_handler.py +22 -5
- astrbot/core/star/star_handler.py +85 -4
- astrbot/core/updator.py +3 -3
- astrbot/core/utils/io.py +1 -1
- astrbot/core/utils/session_waiter.py +17 -10
- astrbot/core/utils/shared_preferences.py +32 -0
- astrbot/core/utils/t2i/__init__.py +2 -2
- astrbot/core/utils/t2i/local_strategy.py +25 -31
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +19 -0
- astrbot/dashboard/routes/chat.py +14 -9
- astrbot/dashboard/routes/config.py +10 -20
- astrbot/dashboard/routes/knowledge_base.py +253 -78
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +1 -1
- astrbot/dashboard/routes/plugin.py +108 -51
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +6 -3
- {astrbot-4.8.0.dist-info → astrbot-4.9.0.dist-info}/METADATA +3 -1
- {astrbot-4.8.0.dist-info → astrbot-4.9.0.dist-info}/RECORD +104 -103
- {astrbot-4.8.0.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
- {astrbot-4.8.0.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.8.0.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
|
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
299
|
+
if not is_mention and raw_message.role_mentions:
|
|
272
300
|
bot_member = None
|
|
273
|
-
if
|
|
301
|
+
if raw_message.guild:
|
|
274
302
|
try:
|
|
275
|
-
bot_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(
|
|
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
|
|
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(
|
|
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[
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -69,11 +84,48 @@ class LarkPlatformAdapter(Platform):
|
|
|
69
84
|
lark.Client.builder().app_id(self.appid).app_secret(self.appsecret).build()
|
|
70
85
|
)
|
|
71
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
|
+
|
|
72
120
|
async def send_by_session(
|
|
73
121
|
self,
|
|
74
122
|
session: MessageSesion,
|
|
75
123
|
message_chain: MessageChain,
|
|
76
124
|
):
|
|
125
|
+
if self.lark_api.im is None:
|
|
126
|
+
logger.error("[Lark] API Client im 模块未初始化,无法发送消息")
|
|
127
|
+
return
|
|
128
|
+
|
|
77
129
|
res = await LarkMessageEvent._convert_to_lark(message_chain, self.lark_api)
|
|
78
130
|
wrapped = {
|
|
79
131
|
"zh_cn": {
|
|
@@ -114,14 +166,25 @@ class LarkPlatformAdapter(Platform):
|
|
|
114
166
|
return PlatformMetadata(
|
|
115
167
|
name="lark",
|
|
116
168
|
description="飞书机器人官方 API 适配器",
|
|
117
|
-
id=self.config.get("id"),
|
|
169
|
+
id=cast(str, self.config.get("id")),
|
|
118
170
|
support_streaming_message=False,
|
|
119
171
|
)
|
|
120
172
|
|
|
121
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
|
|
122
177
|
message = event.event.message
|
|
178
|
+
if message is None:
|
|
179
|
+
logger.debug("[Lark] 事件中没有消息体(message is None)")
|
|
180
|
+
return
|
|
181
|
+
|
|
123
182
|
abm = AstrBotMessage()
|
|
124
|
-
|
|
183
|
+
|
|
184
|
+
if message.create_time:
|
|
185
|
+
abm.timestamp = int(message.create_time) // 1000
|
|
186
|
+
else:
|
|
187
|
+
abm.timestamp = int(time.time())
|
|
125
188
|
abm.message = []
|
|
126
189
|
abm.type = (
|
|
127
190
|
MessageType.GROUP_MESSAGE
|
|
@@ -136,14 +199,28 @@ class LarkPlatformAdapter(Platform):
|
|
|
136
199
|
at_list = {}
|
|
137
200
|
if message.mentions:
|
|
138
201
|
for m in message.mentions:
|
|
139
|
-
|
|
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
|
+
|
|
140
208
|
if m.name == self.bot_name:
|
|
141
|
-
|
|
209
|
+
if m.id.open_id is not None:
|
|
210
|
+
abm.self_id = m.id.open_id
|
|
211
|
+
|
|
212
|
+
if message.content is None:
|
|
213
|
+
logger.warning("[Lark] 消息内容为空")
|
|
214
|
+
return
|
|
142
215
|
|
|
143
|
-
|
|
216
|
+
try:
|
|
217
|
+
content_json_b = json.loads(message.content)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
logger.error(f"[Lark] 解析消息内容失败: {message.content}")
|
|
220
|
+
return
|
|
144
221
|
|
|
145
222
|
if message.message_type == "text":
|
|
146
|
-
message_str_raw = content_json_b
|
|
223
|
+
message_str_raw = content_json_b.get("text", "") # 带有 @ 的消息
|
|
147
224
|
at_pattern = r"(@_user_\d+)" # 可以根据需求修改正则
|
|
148
225
|
# at_users = re.findall(at_pattern, message_str_raw)
|
|
149
226
|
# 拆分文本,去掉AT符号部分
|
|
@@ -168,27 +245,47 @@ class LarkPlatformAdapter(Platform):
|
|
|
168
245
|
content_json_b = _ls
|
|
169
246
|
elif message.message_type == "image":
|
|
170
247
|
content_json_b = [
|
|
171
|
-
{
|
|
248
|
+
{
|
|
249
|
+
"tag": "img",
|
|
250
|
+
"image_key": content_json_b.get("image_key"),
|
|
251
|
+
"style": [],
|
|
252
|
+
},
|
|
172
253
|
]
|
|
173
254
|
|
|
174
255
|
if message.message_type in ("post", "image"):
|
|
175
256
|
for comp in content_json_b:
|
|
176
|
-
if comp
|
|
177
|
-
|
|
178
|
-
|
|
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():
|
|
179
262
|
abm.message.append(Comp.Plain(comp["text"].strip()))
|
|
180
|
-
elif comp
|
|
181
|
-
image_key = comp
|
|
263
|
+
elif comp.get("tag") == "img":
|
|
264
|
+
image_key = comp.get("image_key")
|
|
265
|
+
if not image_key:
|
|
266
|
+
continue
|
|
267
|
+
|
|
182
268
|
request = (
|
|
183
269
|
GetMessageResourceRequest.builder()
|
|
184
|
-
.message_id(message.message_id)
|
|
270
|
+
.message_id(cast(str, message.message_id))
|
|
185
271
|
.file_key(image_key)
|
|
186
272
|
.type("image")
|
|
187
273
|
.build()
|
|
188
274
|
)
|
|
275
|
+
|
|
276
|
+
if self.lark_api.im is None:
|
|
277
|
+
logger.error("[Lark] API Client im 模块未初始化")
|
|
278
|
+
continue
|
|
279
|
+
|
|
189
280
|
response = await self.lark_api.im.v1.message_resource.aget(request)
|
|
190
281
|
if not response.success():
|
|
191
282
|
logger.error(f"无法下载飞书图片: {image_key}")
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if response.file is None:
|
|
286
|
+
logger.error(f"飞书图片响应中不包含文件流: {image_key}")
|
|
287
|
+
continue
|
|
288
|
+
|
|
192
289
|
image_bytes = response.file.read()
|
|
193
290
|
image_base64 = base64.b64encode(image_bytes).decode()
|
|
194
291
|
abm.message.append(Comp.Image.fromBase64(image_base64))
|
|
@@ -196,6 +293,19 @@ class LarkPlatformAdapter(Platform):
|
|
|
196
293
|
for comp in abm.message:
|
|
197
294
|
if isinstance(comp, Comp.Plain):
|
|
198
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
|
+
|
|
199
309
|
abm.message_id = message.message_id
|
|
200
310
|
abm.raw_message = message
|
|
201
311
|
abm.sender = MessageMember(
|
|
@@ -227,13 +337,61 @@ class LarkPlatformAdapter(Platform):
|
|
|
227
337
|
|
|
228
338
|
self._event_queue.put_nowait(event)
|
|
229
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
|
+
|
|
230
362
|
async def run(self):
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
233
384
|
|
|
234
385
|
async def terminate(self):
|
|
235
|
-
|
|
236
|
-
|
|
386
|
+
if self.connection_mode == "socket":
|
|
387
|
+
await self.client._disconnect()
|
|
388
|
+
logger.info("飞书(Lark) 适配器已关闭")
|
|
237
389
|
|
|
238
|
-
def get_client(self) -> lark.Client:
|
|
390
|
+
def get_client(self) -> lark.ws.Client:
|
|
239
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
|
+
)
|
|
@@ -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
|
-
|
|
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}")
|