Skip to content

Commit 516d9e2

Browse files
committed
refactor: 给kook适配器添加kook事件数据类
1 parent 9cfd9e9 commit 516d9e2

23 files changed

+1036
-293
lines changed

astrbot/core/config/default.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,6 @@ class ChatProviderTemplate(TypedDict):
454454
"type": "kook",
455455
"enable": False,
456456
"kook_bot_token": "",
457-
"kook_bot_nickname": "",
458457
"kook_reconnect_delay": 1,
459458
"kook_max_reconnect_delay": 60,
460459
"kook_max_retry_delay": 60,
@@ -809,11 +808,6 @@ class ChatProviderTemplate(TypedDict):
809808
"type": "string",
810809
"hint": "必填项。从 KOOK 开发者平台获取的机器人 Token。",
811810
},
812-
"kook_bot_nickname": {
813-
"description": "Bot Nickname",
814-
"type": "string",
815-
"hint": "可选项。若发送者昵称与此值一致,将忽略该消息以避免广播风暴。",
816-
},
817811
"kook_reconnect_delay": {
818812
"description": "重连延迟",
819813
"type": "int",

astrbot/core/platform/sources/kook/kook_adapter.py

Lines changed: 131 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,28 @@
1313
PlatformMetadata,
1414
register_platform_adapter,
1515
)
16+
from astrbot.core.message.components import File, Record, Video
1617
from astrbot.core.platform.astr_message_event import MessageSesion
1718

1819
from .kook_client import KookClient
1920
from .kook_config import KookConfig
2021
from .kook_event import KookEvent
22+
from .kook_types import (
23+
ContainerModule,
24+
FileModule,
25+
HeaderModule,
26+
ImageGroupModule,
27+
KmarkdownElement,
28+
KookCardMessageContainer,
29+
KookChannelType,
30+
KookMessageEventData,
31+
KookMessageType,
32+
KookModuleType,
33+
PlainTextElement,
34+
SectionModule,
35+
)
36+
37+
KOOK_AT_SELECTOR_REGEX = re.compile(r"\(met\)([^()]+)\(met\)")
2138

2239

2340
@register_platform_adapter(
@@ -57,35 +74,26 @@ def meta(self) -> PlatformMetadata:
5774
name="kook", description="KOOK 适配器", id=self.kook_config.id
5875
)
5976

60-
def _should_ignore_event_by_bot_nickname(self, payload: dict) -> bool:
61-
bot_nickname = self.kook_config.bot_nickname.strip()
62-
if not bot_nickname:
63-
return False
64-
65-
author = payload.get("extra", {}).get("author", {})
66-
if not isinstance(author, dict):
67-
return False
68-
69-
author_nickname = author.get("nickname") or author.get("username") or ""
70-
if not isinstance(author_nickname, str):
71-
author_nickname = str(author_nickname)
72-
73-
return author_nickname.strip().casefold() == bot_nickname.casefold()
74-
75-
async def _on_received(self, data: dict):
76-
logger.debug(f"KOOK 收到数据: {data}")
77-
if "d" in data and data["s"] == 0:
78-
payload = data["d"]
79-
event_type = payload.get("type")
80-
# 支持type=9(文本)和type=10(卡片)
81-
if event_type in (9, 10):
82-
if self._should_ignore_event_by_bot_nickname(payload):
83-
return
84-
try:
85-
abm = await self.convert_message(payload)
86-
await self.handle_msg(abm)
87-
except Exception as e:
88-
logger.error(f"[KOOK] 消息处理异常: {e}")
77+
def _should_ignore_event_by_bot_nickname(self, author_id: str) -> bool:
78+
return self.client.bot_id == author_id
79+
80+
async def _on_received(self, event: KookMessageEventData):
81+
logger.debug(
82+
f'[KOOK] 收到来自"{event.channel_type.name}"渠道的消息, 消息类型为: {event.type.name}({event.type.value})'
83+
)
84+
event_type = event.type
85+
if event_type in (KookMessageType.KMARKDOWN, KookMessageType.CARD):
86+
if self._should_ignore_event_by_bot_nickname(event.author_id):
87+
logger.debug("[KOOK] 收到来自机器人自身的消息, 忽略此消息")
88+
return
89+
try:
90+
abm = await self.convert_message(event)
91+
await self.handle_msg(abm)
92+
except Exception as e:
93+
logger.error(f"[KOOK] 消息处理异常: {e}")
94+
elif event_type == KookMessageType.SYSTEM:
95+
logger.debug(f'[KOOK] 消息为系统通知, 通知类型为: "{event.extra.type}"')
96+
logger.debug(f"[KOOK] 原始消息数据: {event.to_json()}")
8997

9098
async def run(self):
9199
"""主运行循环"""
@@ -184,18 +192,26 @@ async def _cleanup(self):
184192
logger.info("[KOOK] 资源清理完成")
185193

186194
def _parse_kmarkdown_text_message(
187-
self, data: dict, self_id: str
195+
self, data: KookMessageEventData, self_id: str
188196
) -> tuple[list, str]:
189-
kmarkdown = data.get("extra", {}).get("kmarkdown", {})
190-
content = data.get("content") or ""
191-
raw_content = kmarkdown.get("raw_content") or content
197+
kmarkdown = data.extra.kmarkdown
198+
content = data.content or ""
199+
if kmarkdown is None:
200+
logger.error(
201+
f'[KOOK] 无法转换"{KookMessageType.KMARKDOWN.name}"消息, 消息中找不到kmarkdown字段'
202+
)
203+
logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")
204+
return [], ""
205+
206+
raw_content = kmarkdown.raw_content or content
192207
if not isinstance(content, str):
193208
content = str(content)
194209
if not isinstance(raw_content, str):
195210
raw_content = str(raw_content)
196211

212+
# TODO 后面的pydantic类型替换,以后再来探索吧 :(
197213
mention_name_map: dict[str, str] = {}
198-
mention_part = kmarkdown.get("mention_part", [])
214+
mention_part = kmarkdown.mention_part
199215
if isinstance(mention_part, list):
200216
for item in mention_part:
201217
if not isinstance(item, dict):
@@ -207,7 +223,7 @@ def _parse_kmarkdown_text_message(
207223

208224
components = []
209225
cursor = 0
210-
for match in re.finditer(r"\(met\)([^()]+)\(met\)", content):
226+
for match in KOOK_AT_SELECTOR_REGEX.finditer(content):
211227
if match.start() > cursor:
212228
plain_text = content[cursor : match.start()]
213229
if plain_text:
@@ -254,77 +270,109 @@ def _parse_kmarkdown_text_message(
254270

255271
return components, message_str
256272

257-
def _parse_card_message(self, data: dict) -> tuple[list, str]:
258-
content = data.get("content", "[]")
273+
def _parse_card_message(self, data: KookMessageEventData) -> tuple[list, str]:
274+
content = data.content
259275
if not isinstance(content, str):
260276
content = str(content)
261-
card_list = json.loads(content)
277+
278+
card_list = KookCardMessageContainer.from_dict(json.loads(content))
262279

263280
text_parts: list[str] = []
264281
images: list[str] = []
282+
files: list[tuple[KookModuleType, str, str]] = []
265283

266284
for card in card_list:
267-
if not isinstance(card, dict):
268-
continue
269-
for module in card.get("modules", []):
270-
if not isinstance(module, dict):
271-
continue
285+
for module in card.modules:
286+
match module:
287+
case SectionModule():
288+
if content := self._handle_section_text(module):
289+
text_parts.append(content)
272290

273-
module_type = module.get("type")
274-
if module_type == "section":
275-
section_text = module.get("text", {}).get("content", "")
276-
if section_text:
277-
text_parts.append(str(section_text))
278-
continue
291+
case ContainerModule() | ImageGroupModule():
292+
urls = self._handle_image_group(module)
293+
images.extend(urls)
294+
text_parts.append(" [image]" * len(urls))
279295

280-
if module_type != "container":
281-
continue
296+
case HeaderModule():
297+
text_parts.append(module.text.content)
282298

283-
for element in module.get("elements", []):
284-
if not isinstance(element, dict):
285-
continue
286-
if element.get("type") != "image":
287-
continue
299+
case FileModule():
300+
files.append((module.type, module.title, module.src))
301+
text_parts.append(f" [{module.type.value}]")
288302

289-
image_src = element.get("src")
290-
if not isinstance(image_src, str):
291-
logger.warning(
292-
f'[KOOK] 处理卡片中的图片时发生错误,图片url "{image_src}" 应该为str类型, 而不是 "{type(image_src)}" '
293-
)
294-
continue
295-
if not image_src.startswith(("http://", "https://")):
296-
logger.warning(f"[KOOK] 屏蔽非http图片url: {image_src}")
297-
continue
298-
images.append(image_src)
303+
case _:
304+
logger.debug(f"[KOOK] 跳过或未处理模块: {module.type}")
299305

300306
text = "".join(text_parts)
301307
message = []
308+
302309
if text:
310+
for search in KOOK_AT_SELECTOR_REGEX.finditer(text):
311+
search_text = search.group(1).strip()
312+
if search_text == "all":
313+
message.append(AtAll())
314+
continue
315+
message.append(At(qq=search_text))
316+
text = text.replace(f"(met){search_text}(met)", "")
317+
303318
message.append(Plain(text=text))
319+
304320
for img_url in images:
305321
message.append(Image(file=img_url))
322+
for file in files:
323+
file_type = file[0]
324+
file_name = file[1]
325+
file_url = file[2]
326+
if file_type == KookModuleType.FILE:
327+
message.append(File(name=file_name, file=file_url))
328+
elif file_type == KookModuleType.VIDEO:
329+
message.append(Video(file=file_url))
330+
elif file_type == KookModuleType.AUDIO:
331+
message.append(Record(file=file_url))
332+
else:
333+
logger.warning(f"[KOOK] 跳过未知文件类型: {file_type.name}")
334+
306335
return message, text
307336

308-
async def convert_message(self, data: dict) -> AstrBotMessage:
337+
def _handle_section_text(self, module: SectionModule) -> str:
338+
"""专门处理 Section 里的文本提取"""
339+
if isinstance(module.text, (KmarkdownElement, PlainTextElement)):
340+
return module.text.content or ""
341+
return ""
342+
343+
def _handle_image_group(
344+
self, module: ContainerModule | ImageGroupModule
345+
) -> list[str]:
346+
"""专门处理图片组/容器里的合法 URL 提取"""
347+
valid_urls = []
348+
for el in module.elements:
349+
image_src = el.src
350+
if not el.src.startswith(("http://", "https://")):
351+
logger.warning(f"[KOOK] 屏蔽非http图片url: {image_src}")
352+
continue
353+
valid_urls.append(el.src)
354+
return valid_urls
355+
356+
async def convert_message(self, data: KookMessageEventData) -> AstrBotMessage:
309357
abm = AstrBotMessage()
310-
abm.raw_message = data
358+
abm.raw_message = data.to_dict()
311359
abm.self_id = self.client.bot_id
312360

313-
channel_type = data.get("channel_type")
314-
author_id = data.get("author_id", "unknown")
361+
channel_type = data.channel_type
362+
author_id = data.author_id
315363
# channel_type定义: https://developer.kookapp.cn/doc/event/event-introduction
316364
match channel_type:
317-
case "GROUP":
318-
session_id = data.get("target_id") or "unknown"
365+
case KookChannelType.GROUP:
366+
session_id = data.target_id or "unknown"
319367
abm.type = MessageType.GROUP_MESSAGE
320368
abm.group_id = session_id
321369
abm.session_id = session_id
322-
case "PERSON":
370+
case KookChannelType.PERSON:
323371
abm.type = MessageType.FRIEND_MESSAGE
324372
abm.group_id = ""
325-
abm.session_id = data.get("author_id", "unknown")
326-
case "BROADCAST":
327-
session_id = data.get("target_id") or "unknown"
373+
abm.session_id = data.author_id or "unknown"
374+
case KookChannelType.BROADCAST:
375+
session_id = data.target_id or "unknown"
328376
abm.type = MessageType.OTHER_MESSAGE
329377
abm.group_id = session_id
330378
abm.session_id = session_id
@@ -333,28 +381,25 @@ async def convert_message(self, data: dict) -> AstrBotMessage:
333381

334382
abm.sender = MessageMember(
335383
user_id=author_id,
336-
nickname=data.get("extra", {}).get("author", {}).get("username", ""),
384+
nickname=data.extra.author.username if data.extra.author else "unknown",
337385
)
338386

339-
abm.message_id = data.get("msg_id", "unknown")
387+
abm.message_id = data.msg_id or "unknown"
340388

341-
# 普通文本消息
342-
if data.get("type") == 9:
343-
message, message_str = self._parse_kmarkdown_text_message(
344-
data, str(abm.self_id)
345-
)
389+
if data.type == KookMessageType.KMARKDOWN:
390+
message, message_str = self._parse_kmarkdown_text_message(data, abm.self_id)
346391
abm.message = message
347392
abm.message_str = message_str
348-
# 卡片消息
349-
elif data.get("type") == 10:
393+
elif data.type == KookMessageType.CARD:
350394
try:
351395
abm.message, abm.message_str = self._parse_card_message(data)
352396
except Exception as exp:
353397
logger.error(f"[KOOK] 卡片消息解析失败: {exp}")
398+
logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")
354399
abm.message_str = "[卡片消息解析失败]"
355400
abm.message = [Plain(text="[卡片消息解析失败]")]
356401
else:
357-
logger.warning(f'[KOOK] 不支持的kook消息类型: "{data.get("type")}"')
402+
logger.warning(f'[KOOK] 不支持的kook消息类型: "{data.type.name}"')
358403
abm.message_str = "[不支持的消息类型]"
359404
abm.message = [Plain(text="[不支持的消息类型]")]
360405

0 commit comments

Comments
 (0)