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.
- 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_run_util.py +15 -1
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +116 -1
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +32 -4
- 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 +56 -1
- 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/agent_sub_stages/third_party.py +1 -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 +71 -9
- astrbot/core/platform/platform.py +109 -4
- 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 +13 -8
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
- 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 +53 -26
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
- 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 +3 -5
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +58 -40
- astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
- astrbot/core/platform/sources/slack/slack_event.py +11 -10
- astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
- astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
- astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
- astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
- astrbot/core/platform_message_history_mgr.py +3 -3
- 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 +44 -12
- 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 +2 -2
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +66 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +311 -76
- astrbot/dashboard/routes/config.py +14 -5
- astrbot/dashboard/routes/knowledge_base.py +254 -79
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +108 -51
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +9 -4
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
299
|
+
if not is_mention and raw_message.role_mentions:
|
|
273
300
|
bot_member = None
|
|
274
|
-
if
|
|
301
|
+
if raw_message.guild:
|
|
275
302
|
try:
|
|
276
|
-
bot_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(
|
|
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
|
|
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(
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
if m.id.open_id is not None:
|
|
210
|
+
abm.self_id = m.id.open_id
|
|
144
211
|
|
|
145
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
183
|
-
image_key = comp
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
+
)
|