feat: add NapCat built-in adapter with configuration, filters, and transport layer
- Implemented configuration parsing for NapCat adapter including server, chat, and filter settings. - Added message filtering logic to handle inbound chat messages based on user and group lists. - Developed a transport layer for WebSocket communication with the NapCat server. - Created a query service for fetching user and group information from the QQ platform. - Implemented runtime state management to report connection status to the host. - Added notice handling for various QQ platform events.
This commit is contained in:
@@ -1,10 +1,25 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, TypedDict, Optional, List
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
from src.common.data_models.mai_message_data_model import UserInfo, GroupInfo, MessageInfo
|
||||
from src.common.data_models.message_component_data_model import MessageSequence
|
||||
from src.common.data_models.message_component_data_model import (
|
||||
AtComponent,
|
||||
DictComponent,
|
||||
EmojiComponent,
|
||||
ForwardComponent,
|
||||
ForwardNodeComponent,
|
||||
ImageComponent,
|
||||
MessageSequence,
|
||||
ReplyComponent,
|
||||
StandardMessageComponents,
|
||||
TextComponent,
|
||||
VoiceComponent,
|
||||
)
|
||||
|
||||
logger = get_logger("plugin_runtime.host.message_utils")
|
||||
|
||||
@@ -45,6 +60,251 @@ class MessageDict(TypedDict, total=False):
|
||||
|
||||
|
||||
class PluginMessageUtils:
|
||||
@staticmethod
|
||||
def _message_sequence_to_dict(message_sequence: MessageSequence) -> List[Dict[str, Any]]:
|
||||
"""将消息组件序列转换为插件运行时使用的字典结构。
|
||||
|
||||
Args:
|
||||
message_sequence: 待转换的消息组件序列。
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 供插件运行时协议使用的消息段字典列表。
|
||||
"""
|
||||
return [PluginMessageUtils._component_to_dict(component) for component in message_sequence.components]
|
||||
|
||||
@staticmethod
|
||||
def _component_to_dict(component: StandardMessageComponents) -> Dict[str, Any]:
|
||||
"""将单个消息组件转换为插件运行时字典结构。
|
||||
|
||||
Args:
|
||||
component: 待转换的消息组件。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 序列化后的消息组件字典。
|
||||
"""
|
||||
if isinstance(component, TextComponent):
|
||||
return {"type": "text", "data": component.text}
|
||||
|
||||
if isinstance(component, ImageComponent):
|
||||
serialized = {
|
||||
"type": "image",
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
return serialized
|
||||
|
||||
if isinstance(component, EmojiComponent):
|
||||
serialized = {
|
||||
"type": "emoji",
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
return serialized
|
||||
|
||||
if isinstance(component, VoiceComponent):
|
||||
serialized = {
|
||||
"type": "voice",
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
return serialized
|
||||
|
||||
if isinstance(component, AtComponent):
|
||||
return {
|
||||
"type": "at",
|
||||
"data": {
|
||||
"target_user_id": component.target_user_id,
|
||||
"target_user_nickname": component.target_user_nickname,
|
||||
"target_user_cardname": component.target_user_cardname,
|
||||
},
|
||||
}
|
||||
|
||||
if isinstance(component, ReplyComponent):
|
||||
return {
|
||||
"type": "reply",
|
||||
"data": {
|
||||
"target_message_id": component.target_message_id,
|
||||
"target_message_content": component.target_message_content,
|
||||
"target_message_sender_id": component.target_message_sender_id,
|
||||
"target_message_sender_nickname": component.target_message_sender_nickname,
|
||||
"target_message_sender_cardname": component.target_message_sender_cardname,
|
||||
},
|
||||
}
|
||||
|
||||
if isinstance(component, ForwardNodeComponent):
|
||||
return {
|
||||
"type": "forward",
|
||||
"data": [PluginMessageUtils._forward_component_to_dict(item) for item in component.forward_components],
|
||||
}
|
||||
|
||||
return {"type": "dict", "data": component.data}
|
||||
|
||||
@staticmethod
|
||||
def _forward_component_to_dict(component: ForwardComponent) -> Dict[str, Any]:
|
||||
"""将单个转发节点组件转换为字典结构。
|
||||
|
||||
Args:
|
||||
component: 待转换的转发节点组件。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 序列化后的转发节点字典。
|
||||
"""
|
||||
return {
|
||||
"user_id": component.user_id,
|
||||
"user_nickname": component.user_nickname,
|
||||
"user_cardname": component.user_cardname,
|
||||
"message_id": component.message_id,
|
||||
"content": [PluginMessageUtils._component_to_dict(item) for item in component.content],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _message_sequence_from_dict(raw_message_data: List[Dict[str, Any]]) -> MessageSequence:
|
||||
"""从插件运行时字典结构恢复消息组件序列。
|
||||
|
||||
Args:
|
||||
raw_message_data: 插件运行时消息段字典列表。
|
||||
|
||||
Returns:
|
||||
MessageSequence: 恢复后的消息组件序列。
|
||||
"""
|
||||
components = [PluginMessageUtils._component_from_dict(item) for item in raw_message_data]
|
||||
return MessageSequence(components=components)
|
||||
|
||||
@staticmethod
|
||||
def _component_from_dict(item: Dict[str, Any]) -> StandardMessageComponents:
|
||||
"""从插件运行时字典结构恢复单个消息组件。
|
||||
|
||||
Args:
|
||||
item: 单个消息组件的字典表示。
|
||||
|
||||
Returns:
|
||||
StandardMessageComponents: 恢复后的内部消息组件对象。
|
||||
"""
|
||||
item_type = str(item.get("type") or "").strip()
|
||||
if item_type == "text":
|
||||
return TextComponent(text=str(item.get("data") or ""))
|
||||
|
||||
if item_type == "image":
|
||||
return PluginMessageUtils._build_binary_component(ImageComponent, item)
|
||||
|
||||
if item_type == "emoji":
|
||||
return PluginMessageUtils._build_binary_component(EmojiComponent, item)
|
||||
|
||||
if item_type == "voice":
|
||||
return PluginMessageUtils._build_binary_component(VoiceComponent, item)
|
||||
|
||||
if item_type == "at":
|
||||
item_data = item.get("data", {})
|
||||
if not isinstance(item_data, dict):
|
||||
item_data = {}
|
||||
return AtComponent(
|
||||
target_user_id=str(item_data.get("target_user_id") or ""),
|
||||
target_user_nickname=PluginMessageUtils._normalize_optional_string(item_data.get("target_user_nickname")),
|
||||
target_user_cardname=PluginMessageUtils._normalize_optional_string(item_data.get("target_user_cardname")),
|
||||
)
|
||||
|
||||
if item_type == "reply":
|
||||
reply_data = item.get("data")
|
||||
if isinstance(reply_data, dict):
|
||||
return ReplyComponent(
|
||||
target_message_id=str(reply_data.get("target_message_id") or ""),
|
||||
target_message_content=PluginMessageUtils._normalize_optional_string(
|
||||
reply_data.get("target_message_content")
|
||||
),
|
||||
target_message_sender_id=PluginMessageUtils._normalize_optional_string(
|
||||
reply_data.get("target_message_sender_id")
|
||||
),
|
||||
target_message_sender_nickname=PluginMessageUtils._normalize_optional_string(
|
||||
reply_data.get("target_message_sender_nickname")
|
||||
),
|
||||
target_message_sender_cardname=PluginMessageUtils._normalize_optional_string(
|
||||
reply_data.get("target_message_sender_cardname")
|
||||
),
|
||||
)
|
||||
return ReplyComponent(target_message_id=str(reply_data or ""))
|
||||
|
||||
if item_type == "forward":
|
||||
forward_nodes: List[ForwardComponent] = []
|
||||
raw_forward_nodes = item.get("data", [])
|
||||
if isinstance(raw_forward_nodes, list):
|
||||
for node in raw_forward_nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
raw_content = node.get("content", [])
|
||||
node_components: List[StandardMessageComponents] = []
|
||||
if isinstance(raw_content, list):
|
||||
node_components = [
|
||||
PluginMessageUtils._component_from_dict(content)
|
||||
for content in raw_content
|
||||
if isinstance(content, dict)
|
||||
]
|
||||
if not node_components:
|
||||
node_components = [TextComponent(text="[empty forward node]")]
|
||||
forward_nodes.append(
|
||||
ForwardComponent(
|
||||
user_nickname=str(node.get("user_nickname") or "未知用户"),
|
||||
user_id=PluginMessageUtils._normalize_optional_string(node.get("user_id")),
|
||||
user_cardname=PluginMessageUtils._normalize_optional_string(node.get("user_cardname")),
|
||||
message_id=str(node.get("message_id") or ""),
|
||||
content=node_components,
|
||||
)
|
||||
)
|
||||
if not forward_nodes:
|
||||
return DictComponent(data={"type": "forward", "data": item.get("data", [])})
|
||||
return ForwardNodeComponent(forward_components=forward_nodes)
|
||||
|
||||
component_data = item.get("data")
|
||||
if isinstance(component_data, dict):
|
||||
return DictComponent(data=component_data)
|
||||
return DictComponent(data=item)
|
||||
|
||||
@staticmethod
|
||||
def _build_binary_component(component_cls: Any, item: Dict[str, Any]) -> StandardMessageComponents:
|
||||
"""从字典构造带二进制负载的消息组件。
|
||||
|
||||
Args:
|
||||
component_cls: 目标组件类型。
|
||||
item: 消息组件字典。
|
||||
|
||||
Returns:
|
||||
StandardMessageComponents: 构造后的组件对象。
|
||||
"""
|
||||
content = str(item.get("data") or "")
|
||||
binary_hash = str(item.get("hash") or "")
|
||||
raw_binary_base64 = item.get("binary_data_base64")
|
||||
binary_data = b""
|
||||
if isinstance(raw_binary_base64, str) and raw_binary_base64:
|
||||
try:
|
||||
binary_data = base64.b64decode(raw_binary_base64)
|
||||
except Exception:
|
||||
binary_data = b""
|
||||
|
||||
if not binary_hash and binary_data:
|
||||
binary_hash = hashlib.sha256(binary_data).hexdigest()
|
||||
|
||||
return component_cls(binary_hash=binary_hash, content=content, binary_data=binary_data)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_optional_string(value: Any) -> Optional[str]:
|
||||
"""将任意值规范化为可选字符串。
|
||||
|
||||
Args:
|
||||
value: 待规范化的值。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 规范化后的字符串;若值为空则返回 ``None``。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
normalized_value = str(value)
|
||||
return normalized_value if normalized_value else None
|
||||
|
||||
@staticmethod
|
||||
def _message_info_to_dict(message_info: MessageInfo) -> MessageInfoDict:
|
||||
"""
|
||||
@@ -92,7 +352,7 @@ class PluginMessageUtils:
|
||||
timestamp=str(session_message.timestamp.timestamp()), # 转换为时间戳字符串
|
||||
platform=session_message.platform,
|
||||
message_info=PluginMessageUtils._message_info_to_dict(session_message.message_info),
|
||||
raw_message=session_message.raw_message.to_dict(), # 复用 MessageSequence.to_dict()
|
||||
raw_message=PluginMessageUtils._message_sequence_to_dict(session_message.raw_message),
|
||||
is_mentioned=session_message.is_mentioned,
|
||||
is_at=session_message.is_at,
|
||||
is_emoji=session_message.is_emoji,
|
||||
@@ -186,7 +446,7 @@ class PluginMessageUtils:
|
||||
# 构建原始消息组件序列(复用 MessageSequence.from_dict 方法)
|
||||
raw_message_data = message_dict["raw_message"]
|
||||
if isinstance(raw_message_data, list):
|
||||
session_message.raw_message = MessageSequence.from_dict(raw_message_data)
|
||||
session_message.raw_message = PluginMessageUtils._message_sequence_from_dict(raw_message_data)
|
||||
else:
|
||||
raise ValueError("消息字典中 'raw_message' 字段必须是一个列表")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user