重构并增强Napcat适配器的功能
- 更新了`BaseAdapter`以简化子进程处理。 - 对`AdapterManager`进行了重构,以便根据适配器的`run_in_subprocess`属性来管理适配器。 - 增强了`NapcatAdapter`,以利用新的`CoreSinkManager`实现更优的进程管理。 - 在`utils.py`中实现了针对群组和成员信息的缓存机制。 - 改进了`message_handler.py`中的消息处理,以支持各种消息类型和格式。 - 已将插件配置版本更新至7.8.3。
This commit is contained in:
@@ -17,12 +17,13 @@ from typing import Any, ClassVar, Dict, List, Optional
|
||||
import orjson
|
||||
import websockets
|
||||
|
||||
from mofox_bus import CoreMessageSink, MessageEnvelope, WebSocketAdapterOptions
|
||||
from mofox_bus import CoreSink, MessageEnvelope, WebSocketAdapterOptions
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import register_plugin
|
||||
from src.plugin_system.base import BaseAdapter, BasePlugin
|
||||
from src.plugin_system.apis import config_api
|
||||
|
||||
from .src.handlers import utils as handler_utils
|
||||
from .src.handlers.to_core.message_handler import MessageHandler
|
||||
from .src.handlers.to_core.notice_handler import NoticeHandler
|
||||
from .src.handlers.to_core.meta_event_handler import MetaEventHandler
|
||||
@@ -41,22 +42,16 @@ class NapcatAdapter(BaseAdapter):
|
||||
platform = "qq"
|
||||
|
||||
run_in_subprocess = False
|
||||
subprocess_entry = None
|
||||
|
||||
def __init__(self, core_sink: CoreMessageSink, plugin: Optional[BasePlugin] = None):
|
||||
def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None):
|
||||
"""初始化 Napcat 适配器"""
|
||||
# 从插件配置读取 WebSocket URL
|
||||
if plugin:
|
||||
mode = config_api.get_plugin_config(plugin.config, "napcat_server.mode", "reverse")
|
||||
host = config_api.get_plugin_config(plugin.config, "napcat_server.host", "localhost")
|
||||
port = config_api.get_plugin_config(plugin.config, "napcat_server.port", 8095)
|
||||
url = config_api.get_plugin_config(plugin.config, "napcat_server.url", "")
|
||||
access_token = config_api.get_plugin_config(plugin.config, "napcat_server.access_token", "")
|
||||
|
||||
if mode == "forward" and url:
|
||||
ws_url = url
|
||||
else:
|
||||
ws_url = f"ws://{host}:{port}"
|
||||
|
||||
ws_url = f"ws://{host}:{port}"
|
||||
|
||||
headers = {}
|
||||
if access_token:
|
||||
@@ -69,8 +64,6 @@ class NapcatAdapter(BaseAdapter):
|
||||
transport = WebSocketAdapterOptions(
|
||||
url=ws_url,
|
||||
headers=headers if headers else None,
|
||||
incoming_parser=self._parse_napcat_message,
|
||||
outgoing_encoder=self._encode_napcat_response,
|
||||
)
|
||||
|
||||
super().__init__(core_sink, plugin=plugin, transport=transport)
|
||||
@@ -89,6 +82,9 @@ class NapcatAdapter(BaseAdapter):
|
||||
# 注意:_ws 继承自 BaseAdapter,是 WebSocketLike 协议类型
|
||||
self._napcat_ws = None # 可选的额外连接引用
|
||||
|
||||
# 注册 utils 内部使用的适配器实例,便于工具方法自动获取 WS
|
||||
handler_utils.register_adapter(self)
|
||||
|
||||
async def on_adapter_loaded(self) -> None:
|
||||
"""适配器加载时的初始化"""
|
||||
logger.info("Napcat 适配器正在启动...")
|
||||
@@ -114,22 +110,6 @@ class NapcatAdapter(BaseAdapter):
|
||||
|
||||
logger.info("Napcat 适配器已关闭")
|
||||
|
||||
def _parse_napcat_message(self, raw: str | bytes) -> Any:
|
||||
"""解析 Napcat/OneBot 消息"""
|
||||
try:
|
||||
if isinstance(raw, bytes):
|
||||
data = orjson.loads(raw)
|
||||
else:
|
||||
data = orjson.loads(raw)
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Napcat 消息失败: {e}")
|
||||
raise
|
||||
|
||||
def _encode_napcat_response(self, envelope: MessageEnvelope) -> bytes:
|
||||
"""编码响应消息为 Napcat 格式(暂未使用,通过 API 调用发送)"""
|
||||
return orjson.dumps(envelope)
|
||||
|
||||
async def from_platform_message(self, raw: Dict[str, Any]) -> MessageEnvelope: # type: ignore[override]
|
||||
"""
|
||||
将 Napcat/OneBot 原始消息转换为 MessageEnvelope
|
||||
@@ -178,20 +158,6 @@ class NapcatAdapter(BaseAdapter):
|
||||
"""
|
||||
await self.send_handler.handle_message(envelope)
|
||||
|
||||
def _create_empty_envelope(self) -> MessageEnvelope: # type: ignore[return]
|
||||
"""创建一个空的消息信封(用于不需要处理的事件)"""
|
||||
import time
|
||||
return {
|
||||
"direction": "incoming",
|
||||
"message_info": {
|
||||
"platform": self.platform,
|
||||
"message_id": str(uuid.uuid4()),
|
||||
"time": time.time(),
|
||||
},
|
||||
"message_segment": {"type": "text", "data": "[系统事件]"},
|
||||
"timestamp_ms": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
async def send_napcat_api(self, action: str, params: Dict[str, Any], timeout: float = 30.0) -> Dict[str, Any]:
|
||||
"""
|
||||
发送 Napcat API 请求并等待响应
|
||||
@@ -260,18 +226,12 @@ class NapcatAdapterPlugin(BasePlugin):
|
||||
config_schema: ClassVar[dict] = {
|
||||
"plugin": {
|
||||
"name": {"type": str, "default": "napcat_adapter_plugin"},
|
||||
"version": {"type": str, "default": "2.0.0"},
|
||||
"version": {"type": str, "default": "1.0.0"},
|
||||
"enabled": {"type": bool, "default": True},
|
||||
},
|
||||
"napcat_server": {
|
||||
"mode": {
|
||||
"type": str,
|
||||
"default": "reverse",
|
||||
"description": "连接模式:reverse=反向连接(作为服务器), forward=正向连接(作为客户端)",
|
||||
},
|
||||
"host": {"type": str, "default": "localhost"},
|
||||
"port": {"type": int, "default": 8095},
|
||||
"url": {"type": str, "default": "", "description": "正向连接时的完整URL"},
|
||||
"access_token": {"type": str, "default": ""},
|
||||
},
|
||||
"features": {
|
||||
@@ -284,34 +244,18 @@ class NapcatAdapterPlugin(BasePlugin):
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, plugin_dir: str = "", metadata: Any = None):
|
||||
# 如果没有提供参数,创建一个默认的元数据
|
||||
if metadata is None:
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
metadata = PluginMetadata(
|
||||
name=self.plugin_name,
|
||||
version=self.plugin_version,
|
||||
author=self.plugin_author,
|
||||
description=self.plugin_description,
|
||||
usage="",
|
||||
dependencies=[],
|
||||
python_dependencies=[],
|
||||
)
|
||||
|
||||
if not plugin_dir:
|
||||
from pathlib import Path
|
||||
plugin_dir = str(Path(__file__).parent)
|
||||
|
||||
super().__init__(plugin_dir, metadata)
|
||||
def __init__(self):
|
||||
self._adapter: Optional[NapcatAdapter] = None
|
||||
|
||||
async def on_plugin_loaded(self):
|
||||
"""插件加载时启动适配器"""
|
||||
logger.info("Napcat 适配器插件正在加载...")
|
||||
|
||||
# 获取核心 Sink
|
||||
from src.common.core_sink import get_core_sink
|
||||
core_sink = get_core_sink()
|
||||
# 从 CoreSinkManager 获取 InProcessCoreSink
|
||||
from src.common.core_sink_manager import get_core_sink_manager
|
||||
|
||||
core_sink_manager = get_core_sink_manager()
|
||||
core_sink = core_sink_manager.get_in_process_sink()
|
||||
|
||||
# 创建并启动适配器
|
||||
self._adapter = NapcatAdapter(core_sink, plugin=self)
|
||||
|
||||
@@ -5,11 +5,28 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from mofox_bus import MessageBuilder
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import config_api
|
||||
from mofox_bus import (
|
||||
MessageEnvelope,
|
||||
SegPayload,
|
||||
MessageInfoPayload,
|
||||
UserInfoPayload,
|
||||
GroupInfoPayload,
|
||||
)
|
||||
|
||||
from ...event_models import ACCEPT_FORMAT, QQ_FACE
|
||||
from ..utils import (
|
||||
get_group_info,
|
||||
get_image_base64,
|
||||
get_self_info,
|
||||
get_member_info,
|
||||
get_message_detail,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...plugin import NapcatAdapter
|
||||
from ....plugin import NapcatAdapter
|
||||
|
||||
logger = get_logger("napcat_adapter.message_handler")
|
||||
|
||||
@@ -28,99 +45,190 @@ class MessageHandler:
|
||||
async def handle_raw_message(self, raw: Dict[str, Any]):
|
||||
"""
|
||||
处理原始消息并转换为 MessageEnvelope
|
||||
|
||||
|
||||
Args:
|
||||
raw: OneBot 原始消息数据
|
||||
|
||||
|
||||
Returns:
|
||||
MessageEnvelope (dict)
|
||||
"""
|
||||
from mofox_bus import MessageEnvelope, SegPayload, MessageInfoPayload, UserInfoPayload, GroupInfoPayload
|
||||
|
||||
message_type = raw.get("message_type")
|
||||
message_id = str(raw.get("message_id", ""))
|
||||
message_time = time.time()
|
||||
|
||||
|
||||
msg_builder = MessageBuilder()
|
||||
|
||||
# 构造用户信息
|
||||
sender_info = raw.get("sender", {})
|
||||
user_info: UserInfoPayload = {
|
||||
"platform": "qq",
|
||||
"user_id": str(sender_info.get("user_id", "")),
|
||||
"user_nickname": sender_info.get("nickname", ""),
|
||||
"user_cardname": sender_info.get("card", ""),
|
||||
"user_avatar": sender_info.get("avatar", ""),
|
||||
}
|
||||
|
||||
(
|
||||
msg_builder.direction("incoming")
|
||||
.message_id(message_id)
|
||||
.timestamp_ms(int(message_time * 1000))
|
||||
.from_user(
|
||||
user_id=str(sender_info.get("user_id", "")),
|
||||
platform="qq",
|
||||
nickname=sender_info.get("nickname", ""),
|
||||
cardname=sender_info.get("card", ""),
|
||||
user_avatar=sender_info.get("avatar", ""),
|
||||
)
|
||||
)
|
||||
|
||||
# 构造群组信息(如果是群消息)
|
||||
group_info: Optional[GroupInfoPayload] = None
|
||||
if message_type == "group":
|
||||
group_id = raw.get("group_id")
|
||||
if group_id:
|
||||
group_info = {
|
||||
"platform": "qq",
|
||||
"group_id": str(group_id),
|
||||
"group_name": "", # 可以通过 API 获取
|
||||
}
|
||||
fetched_group_info = await get_group_info(group_id)
|
||||
(
|
||||
msg_builder.from_group(
|
||||
group_id=str(group_id),
|
||||
platform="qq",
|
||||
name=(
|
||||
fetched_group_info.get("group_name", "")
|
||||
if fetched_group_info
|
||||
else raw.get("group_name", "")
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# 解析消息段
|
||||
message_segments = raw.get("message", [])
|
||||
seg_list: List[SegPayload] = []
|
||||
|
||||
for seg in message_segments:
|
||||
seg_type = seg.get("type", "")
|
||||
seg_data = seg.get("data", {})
|
||||
|
||||
# 转换为 SegPayload
|
||||
if seg_type == "text":
|
||||
seg_list.append({
|
||||
"type": "text",
|
||||
"data": seg_data.get("text", "")
|
||||
})
|
||||
elif seg_type == "image":
|
||||
# 这里需要下载图片并转换为 base64(简化版本)
|
||||
seg_list.append({
|
||||
"type": "image",
|
||||
"data": seg_data.get("url", "") # 实际应该转换为 base64
|
||||
})
|
||||
elif seg_type == "at":
|
||||
seg_list.append({
|
||||
"type": "at",
|
||||
"data": f"{seg_data.get('qq', '')}"
|
||||
})
|
||||
# 其他消息类型...
|
||||
|
||||
# 构造 MessageInfoPayload
|
||||
message_info = {
|
||||
"platform": "qq",
|
||||
"message_id": message_id,
|
||||
"time": message_time,
|
||||
"user_info": user_info,
|
||||
"format_info": {
|
||||
"content_format": ["text", "image"], # 根据实际消息类型设置
|
||||
"accept_format": ["text", "image", "emoji", "voice"],
|
||||
},
|
||||
}
|
||||
|
||||
# 添加群组信息(如果存在)
|
||||
if group_info:
|
||||
message_info["group_info"] = group_info
|
||||
for segment in message_segments:
|
||||
seg_message = await self.handle_single_segment(segment, raw)
|
||||
if seg_message:
|
||||
seg_list.append(seg_message)
|
||||
|
||||
# 构造 MessageEnvelope
|
||||
envelope = {
|
||||
"direction": "incoming",
|
||||
"message_info": message_info,
|
||||
"message_segment": {"type": "seglist", "data": seg_list} if len(seg_list) > 1 else (seg_list[0] if seg_list else {"type": "text", "data": ""}),
|
||||
"raw_message": raw.get("raw_message", ""),
|
||||
"platform": "qq",
|
||||
"message_id": message_id,
|
||||
"timestamp_ms": int(message_time * 1000),
|
||||
}
|
||||
msg_builder.format_info(
|
||||
content_format=[seg["type"] for seg in seg_list],
|
||||
accept_format=ACCEPT_FORMAT,
|
||||
)
|
||||
|
||||
return envelope
|
||||
return msg_builder.build()
|
||||
|
||||
async def handle_single_segment(
|
||||
self, segment: dict, raw_message: dict, in_reply: bool = False
|
||||
) -> SegPayload | List[SegPayload] | None:
|
||||
"""
|
||||
处理单一消息段并转换为 MessageEnvelope
|
||||
|
||||
Args:
|
||||
segment: 单一原始消息段
|
||||
raw_message: 完整的原始消息数据
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
SegPayload | List[SegPayload] | None
|
||||
"""
|
||||
seg_type = segment.get("type")
|
||||
seg_data: dict = segment.get("data", {})
|
||||
match seg_type:
|
||||
case "text":
|
||||
return {"type": "text", "data": seg_data.get("text", "")}
|
||||
case "image":
|
||||
image_sub_type = seg_data.get("sub_type")
|
||||
try:
|
||||
image_base64 = await get_image_base64(seg_data.get("url", ""))
|
||||
except Exception as e:
|
||||
logger.error(f"图片消息处理失败: {str(e)}")
|
||||
return None
|
||||
if image_sub_type == 0:
|
||||
"""这部分认为是图片"""
|
||||
return {"type": "image", "data": image_base64}
|
||||
elif image_sub_type not in [4, 9]:
|
||||
"""这部分认为是表情包"""
|
||||
return {"type": "emoji", "data": image_base64}
|
||||
else:
|
||||
logger.warning(f"不支持的图片子类型:{image_sub_type}")
|
||||
return None
|
||||
case "face":
|
||||
message_data: dict = segment.get("data", {})
|
||||
face_raw_id: str = str(message_data.get("id"))
|
||||
if face_raw_id in QQ_FACE:
|
||||
face_content: str = QQ_FACE.get(face_raw_id, "[未知表情]")
|
||||
return {"type": "text", "data": face_content}
|
||||
else:
|
||||
logger.warning(f"不支持的表情:{face_raw_id}")
|
||||
return None
|
||||
case "at":
|
||||
if seg_data:
|
||||
qq_id = seg_data.get("qq")
|
||||
self_id = raw_message.get("self_id")
|
||||
group_id = raw_message.get("group_id")
|
||||
if str(self_id) == str(qq_id):
|
||||
logger.debug("机器人被at")
|
||||
self_info = await get_self_info()
|
||||
if self_info:
|
||||
# 返回包含昵称和用户ID的at格式,便于后续处理
|
||||
return {
|
||||
"type": "at",
|
||||
"data": f"{self_info.get('nickname')}:{self_info.get('user_id')}",
|
||||
}
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
if qq_id and group_id:
|
||||
member_info = await get_member_info(
|
||||
group_id=group_id, user_id=qq_id
|
||||
)
|
||||
if member_info:
|
||||
# 返回包含昵称和用户ID的at格式,便于后续处理
|
||||
return {
|
||||
"type": "at",
|
||||
"data": f"{member_info.get('nickname')}:{member_info.get('user_id')}",
|
||||
}
|
||||
else:
|
||||
return None
|
||||
case "emoji":
|
||||
seg_data = segment.get("id", "")
|
||||
case "reply":
|
||||
if not in_reply:
|
||||
message_id = None
|
||||
if seg_data:
|
||||
message_id = seg_data.get("id")
|
||||
else:
|
||||
return None
|
||||
message_detail = await get_message_detail(message_id)
|
||||
if not message_detail:
|
||||
logger.warning("获取被引用的消息详情失败")
|
||||
return None
|
||||
reply_message = await self.handle_single_segment(
|
||||
message_detail, raw_message, in_reply=True
|
||||
)
|
||||
if reply_message is None:
|
||||
reply_message = [
|
||||
{"type": "text", "data": "[无法获取被引用的消息]"}
|
||||
]
|
||||
sender_info: dict = message_detail.get("sender", {})
|
||||
sender_nickname: str = sender_info.get("nickname", "")
|
||||
sender_id = sender_info.get("user_id")
|
||||
seg_message: List[SegPayload] = []
|
||||
if not sender_nickname:
|
||||
logger.warning("无法获取被引用的人的昵称,返回默认值")
|
||||
seg_message.append(
|
||||
{
|
||||
"type": "text",
|
||||
"data": f"[回复<未知用户>:{reply_message}],说:",
|
||||
}
|
||||
)
|
||||
else:
|
||||
if sender_id:
|
||||
seg_message.append(
|
||||
{
|
||||
"type": "text",
|
||||
"data": f"[回复<{sender_nickname}({sender_id})>:{reply_message}],说:",
|
||||
}
|
||||
)
|
||||
else:
|
||||
seg_message.append(
|
||||
{
|
||||
"type": "text",
|
||||
"data": f"[回复<{sender_nickname}>:{reply_message}],说:",
|
||||
}
|
||||
)
|
||||
return seg_message
|
||||
case "voice":
|
||||
seg_data = segment.get("url", "")
|
||||
case _:
|
||||
logger.warning(f"Unsupported segment type: {seg_type}")
|
||||
|
||||
361
src/plugins/built_in/NEW_napcat_adapter/src/handlers/utils.py
Normal file
361
src/plugins/built_in/NEW_napcat_adapter/src/handlers/utils.py
Normal file
@@ -0,0 +1,361 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import ssl
|
||||
import time
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
|
||||
import orjson
|
||||
import urllib3
|
||||
from PIL import Image
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...plugin import NapcatAdapter
|
||||
|
||||
logger = get_logger("napcat_adapter")
|
||||
|
||||
# 简单的缓存实现,通过 JSON 文件实现磁盘一价存储
|
||||
_CACHE_FILE = Path(__file__).resolve().parent / "napcat_cache.json"
|
||||
_CACHE_LOCK = asyncio.Lock()
|
||||
_CACHE: Dict[str, Dict[str, Dict[str, Any]]] = {
|
||||
"group_info": {},
|
||||
"group_detail_info": {},
|
||||
"member_info": {},
|
||||
"stranger_info": {},
|
||||
"self_info": {},
|
||||
}
|
||||
|
||||
# 各类信息的 TTL 缓存过期时间设置
|
||||
GROUP_INFO_TTL = 300 # 5 min
|
||||
GROUP_DETAIL_TTL = 300
|
||||
MEMBER_INFO_TTL = 180
|
||||
STRANGER_INFO_TTL = 300
|
||||
SELF_INFO_TTL = 300
|
||||
|
||||
_adapter_ref: weakref.ReferenceType["NapcatAdapter"] | None = None
|
||||
|
||||
|
||||
def register_adapter(adapter: "NapcatAdapter") -> None:
|
||||
"""注册 NapcatAdapter 实例,以便 utils 模块可以获取 WebSocket"""
|
||||
global _adapter_ref
|
||||
_adapter_ref = weakref.ref(adapter)
|
||||
logger.debug("Napcat adapter registered in utils for websocket access")
|
||||
|
||||
|
||||
def _load_cache_from_disk() -> None:
|
||||
if not _CACHE_FILE.exists():
|
||||
return
|
||||
try:
|
||||
data = orjson.loads(_CACHE_FILE.read_bytes())
|
||||
if isinstance(data, dict):
|
||||
for key, section in _CACHE.items():
|
||||
cached_section = data.get(key)
|
||||
if isinstance(cached_section, dict):
|
||||
section.update(cached_section)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to load napcat cache: {e}")
|
||||
|
||||
|
||||
def _save_cache_to_disk_locked() -> None:
|
||||
"""重要提示:不要在持有 _CACHE_LOCK 时调用此函数"""
|
||||
_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
_CACHE_FILE.write_bytes(orjson.dumps(_CACHE))
|
||||
|
||||
|
||||
async def _get_cached(section: str, key: str, ttl: int) -> Any | None:
|
||||
now = time.time()
|
||||
async with _CACHE_LOCK:
|
||||
entry = _CACHE.get(section, {}).get(key)
|
||||
if not entry:
|
||||
return None
|
||||
ts = entry.get("ts", 0)
|
||||
if ts and now - ts <= ttl:
|
||||
return entry.get("data")
|
||||
_CACHE.get(section, {}).pop(key, None)
|
||||
try:
|
||||
_save_cache_to_disk_locked()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def _set_cached(section: str, key: str, data: Any) -> None:
|
||||
async with _CACHE_LOCK:
|
||||
_CACHE.setdefault(section, {})[key] = {"data": data, "ts": time.time()}
|
||||
try:
|
||||
_save_cache_to_disk_locked()
|
||||
except Exception:
|
||||
logger.debug("Write napcat cache failed", exc_info=True)
|
||||
|
||||
|
||||
def _get_adapter(adapter: "NapcatAdapter | None" = None) -> "NapcatAdapter":
|
||||
target = adapter
|
||||
if target is None and _adapter_ref:
|
||||
target = _adapter_ref()
|
||||
if target is None:
|
||||
raise RuntimeError("NapcatAdapter 未注册,请确保已调用 utils.register_adapter 注册")
|
||||
return target
|
||||
|
||||
|
||||
async def _call_adapter_api(
|
||||
action: str,
|
||||
params: Dict[str, Any],
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
timeout: float = 30.0,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""统一通过 adapter 发送和接收 API 调用"""
|
||||
try:
|
||||
target = _get_adapter(adapter)
|
||||
# 确保 WS 已连接
|
||||
target.get_ws_connection()
|
||||
except Exception as e: # pragma: no cover - 难以在单元测试中查看
|
||||
logger.error(f"WebSocket 未准备好,无法调用 API: {e}")
|
||||
return None
|
||||
|
||||
try:
|
||||
return await target.send_napcat_api(action, params, timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.error(f"{action} 调用失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# 加载缓存到内存一次,避免在每次调用缓存时重复加载
|
||||
_load_cache_from_disk()
|
||||
|
||||
|
||||
class SSLAdapter(urllib3.PoolManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
context = ssl.create_default_context()
|
||||
context.set_ciphers("DEFAULT@SECLEVEL=1")
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
kwargs["ssl_context"] = context
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
async def get_group_info(
|
||||
group_id: int,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取群组基本信息
|
||||
|
||||
返回值可能是None,需要调用方检查空值
|
||||
"""
|
||||
logger.debug("获取群组基本信息中")
|
||||
cache_key = str(group_id)
|
||||
if use_cache and not force_refresh:
|
||||
cached = await _get_cached("group_info", cache_key, GROUP_INFO_TTL)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
socket_response = await _call_adapter_api(
|
||||
"get_group_info",
|
||||
{"group_id": group_id},
|
||||
adapter=adapter,
|
||||
)
|
||||
data = socket_response.get("data") if socket_response else None
|
||||
if data is not None and use_cache:
|
||||
await _set_cached("group_info", cache_key, data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_group_detail_info(
|
||||
group_id: int,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取群组详细信息
|
||||
|
||||
返回值可能是None,需要调用方检查空值
|
||||
"""
|
||||
logger.debug("获取群组详细信息中")
|
||||
cache_key = str(group_id)
|
||||
if use_cache and not force_refresh:
|
||||
cached = await _get_cached("group_detail_info", cache_key, GROUP_DETAIL_TTL)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
socket_response = await _call_adapter_api(
|
||||
"get_group_detail_info",
|
||||
{"group_id": group_id},
|
||||
adapter=adapter,
|
||||
)
|
||||
data = socket_response.get("data") if socket_response else None
|
||||
if data is not None and use_cache:
|
||||
await _set_cached("group_detail_info", cache_key, data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_member_info(
|
||||
group_id: int,
|
||||
user_id: int,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取群组成员信息
|
||||
|
||||
返回值可能是None,需要调用方检查空值
|
||||
"""
|
||||
logger.debug("获取群组成员信息中")
|
||||
cache_key = f"{group_id}:{user_id}"
|
||||
if use_cache and not force_refresh:
|
||||
cached = await _get_cached("member_info", cache_key, MEMBER_INFO_TTL)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
socket_response = await _call_adapter_api(
|
||||
"get_group_member_info",
|
||||
{"group_id": group_id, "user_id": user_id, "no_cache": True},
|
||||
adapter=adapter,
|
||||
)
|
||||
data = socket_response.get("data") if socket_response else None
|
||||
if data is not None and use_cache:
|
||||
await _set_cached("member_info", cache_key, data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_image_base64(url: str) -> str:
|
||||
# sourcery skip: raise-specific-error
|
||||
"""下载图片/视频并返回Base64"""
|
||||
logger.debug(f"下载图片: {url}")
|
||||
http = SSLAdapter()
|
||||
try:
|
||||
response = http.request("GET", url, timeout=10)
|
||||
if response.status != 200:
|
||||
raise Exception(f"HTTP Error: {response.status}")
|
||||
image_bytes = response.data
|
||||
return base64.b64encode(image_bytes).decode("utf-8")
|
||||
except Exception as e:
|
||||
logger.error(f"图片下载失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def convert_image_to_gif(image_base64: str) -> str:
|
||||
# sourcery skip: extract-method
|
||||
"""
|
||||
将Base64编码的图片转换为GIF格式
|
||||
Parameters:
|
||||
image_base64: str: Base64编码的图片数据
|
||||
Returns:
|
||||
str: Base64编码的GIF图片数据
|
||||
"""
|
||||
logger.debug("转换图片为GIF格式")
|
||||
try:
|
||||
image_bytes = base64.b64decode(image_base64)
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
output_buffer = io.BytesIO()
|
||||
image.save(output_buffer, format="GIF")
|
||||
output_buffer.seek(0)
|
||||
return base64.b64encode(output_buffer.read()).decode("utf-8")
|
||||
except Exception as e:
|
||||
logger.error(f"图片转换为GIF失败: {str(e)}")
|
||||
return image_base64
|
||||
|
||||
|
||||
async def get_self_info(
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取机器人信息
|
||||
"""
|
||||
logger.debug("获取机器人信息中")
|
||||
cache_key = "self"
|
||||
if use_cache and not force_refresh:
|
||||
cached = await _get_cached("self_info", cache_key, SELF_INFO_TTL)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
response = await _call_adapter_api("get_login_info", {}, adapter=adapter)
|
||||
data = response.get("data") if response else None
|
||||
if data is not None and use_cache:
|
||||
await _set_cached("self_info", cache_key, data)
|
||||
return data
|
||||
|
||||
|
||||
def get_image_format(raw_data: str) -> str:
|
||||
"""
|
||||
从Base64编码的数据中确定图片的格式类型
|
||||
Parameters:
|
||||
raw_data: str: Base64编码的图片数据
|
||||
Returns:
|
||||
format: str: 图片的格式类型,如 'jpeg', 'png', 'gif'等
|
||||
"""
|
||||
image_bytes = base64.b64decode(raw_data)
|
||||
return Image.open(io.BytesIO(image_bytes)).format.lower()
|
||||
|
||||
|
||||
async def get_stranger_info(
|
||||
user_id: int,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取陌生人信息
|
||||
"""
|
||||
logger.debug("获取陌生人信息中")
|
||||
cache_key = str(user_id)
|
||||
if use_cache and not force_refresh:
|
||||
cached = await _get_cached("stranger_info", cache_key, STRANGER_INFO_TTL)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
response = await _call_adapter_api("get_stranger_info", {"user_id": user_id}, adapter=adapter)
|
||||
data = response.get("data") if response else None
|
||||
if data is not None and use_cache:
|
||||
await _set_cached("stranger_info", cache_key, data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_message_detail(
|
||||
message_id: Union[str, int],
|
||||
*,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取消息详情,仅作为参考
|
||||
"""
|
||||
logger.debug("获取消息详情中")
|
||||
response = await _call_adapter_api(
|
||||
"get_msg",
|
||||
{"message_id": message_id},
|
||||
adapter=adapter,
|
||||
timeout=30,
|
||||
)
|
||||
return response.get("data") if response else None
|
||||
|
||||
|
||||
async def get_record_detail(
|
||||
file: str,
|
||||
file_id: Optional[str] = None,
|
||||
*,
|
||||
adapter: "NapcatAdapter | None" = None,
|
||||
) -> dict | None:
|
||||
"""
|
||||
获取语音信息详情
|
||||
"""
|
||||
logger.debug("获取语音信息详情中")
|
||||
response = await _call_adapter_api(
|
||||
"get_record",
|
||||
{"file": file, "file_id": file_id, "out_format": "wav"},
|
||||
adapter=adapter,
|
||||
timeout=30,
|
||||
)
|
||||
return response.get("data") if response else None
|
||||
@@ -3,7 +3,7 @@ import random
|
||||
import time
|
||||
import websockets as Server
|
||||
import uuid
|
||||
from mofox_bus import (
|
||||
from maim_message import (
|
||||
UserInfo,
|
||||
GroupInfo,
|
||||
Seg,
|
||||
|
||||
Reference in New Issue
Block a user