1313 PlatformMetadata ,
1414 register_platform_adapter ,
1515)
16+ from astrbot .core .message .components import File , Record , Video
1617from astrbot .core .platform .astr_message_event import MessageSesion
1718
1819from .kook_client import KookClient
1920from .kook_config import KookConfig
2021from .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