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:
DrSmoothl
2026-03-22 00:19:26 +08:00
parent 4e2e7a279e
commit baabe4463e
18 changed files with 2755 additions and 897 deletions

View File

@@ -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' 字段必须是一个列表")