chore: import private baseline from gitea state

This commit is contained in:
Losita
2026-05-11 19:24:06 +08:00
parent 161fc42c52
commit 1ba863d135
111 changed files with 10873 additions and 7347 deletions

View File

@@ -0,0 +1,21 @@
{
"name": "MaiBot-Napcat-Adapter-DevContainer",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
"packages": [
"tmux"
]
},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"forwardPorts": [
"8095:8095"
],
"postCreateCommand": "pip3 install --user -r requirements.txt",
"customizations" : {
"jetbrains" : {
"backend" : "PyCharm"
}
}
}

View File

@@ -0,0 +1,54 @@
name: Docker Image CI
on:
push:
branches: [ "main", "dev" ]
workflow_dispatch: # 允许手动触发工作流
jobs:
build:
runs-on: ubuntu-latest
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }}
DATE_TAG: $(date -u +'%Y-%m-%dT%H-%M-%S')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Clone maim_message
run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Determine Image Tags
id: tags
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:latest,${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:main-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
elif [ "${{ github.ref_name }}" == "dev" ]; then
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:dev,${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:dev-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
tags: ${{ steps.tags.outputs.tags }}
push: true
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:buildcache-${{ github.ref_name }}
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot-adapter:buildcache-${{ github.ref_name }},mode=max
labels: |
org.opencontainers.image.created=${{ steps.tags.outputs.date_tag }}
org.opencontainers.image.revision=${{ github.sha }}

View File

@@ -0,0 +1 @@
"""NapCat 内置适配器插件包。"""

View File

@@ -0,0 +1,35 @@
{
"manifest_version": 2,
"version": "1.0.1",
"name": "Napcat_Adapter",
"description": "Built-in NapCat adapter plugin for QQ / NapCat message gateway and platform actions.",
"author": {
"name": "MaiBot Team",
"url": "https://github.com/Mai-with-u"
},
"license": "GPL-v3.0-or-later",
"urls": {
"repository": "https://github.com/Mai-with-u/MaiBot-Napcat-Adapter",
"homepage": "https://github.com/Mai-with-u/MaiBot-Napcat-Adapter",
"documentation": "https://github.com/Mai-with-u/MaiBot-Napcat-Adapter",
"issues": "https://github.com/Mai-with-u/MaiBot-Napcat-Adapter/issues"
},
"host_application": {
"min_version": "1.0.0",
"max_version": "1.0.0"
},
"sdk": {
"min_version": "2.0.0",
"max_version": "2.99.99"
},
"dependencies": [],
"capabilities": [],
"i18n": {
"default_locale": "zh-CN",
"supported_locales": [
"zh-CN",
"en-US"
]
},
"id": "maibot-team.napcat-adapter"
}

View File

@@ -0,0 +1,18 @@
"""NapCat API mixin 导出。"""
from .account import NapCatAccountApiMixin
from .file import NapCatFileApiMixin
from .group import NapCatGroupApiMixin
from .message import NapCatMessageApiMixin
from . import message_tool_patch as _message_tool_patch
from .support import NapCatApiSupportMixin
from .system import NapCatSystemApiMixin
__all__ = [
"NapCatAccountApiMixin",
"NapCatApiSupportMixin",
"NapCatFileApiMixin",
"NapCatGroupApiMixin",
"NapCatMessageApiMixin",
"NapCatSystemApiMixin",
]

View File

@@ -0,0 +1,366 @@
"""NapCat 账号与用户侧 API 端点。"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from maibot_sdk import API
from .support import NapCatApiIdInput, NapCatApiParamsInput, NapCatApiSupportMixin
class NapCatAccountApiMixin(NapCatApiSupportMixin):
"""NapCat 账号、好友与资料相关 API。"""
@API("adapter.napcat.account.set_qq_profile", description="设置 QQ 账号资料", version="1", public=True)
async def api_set_qq_profile(
self,
nickname: object,
personal_note: str = "",
sex: str = "",
) -> Dict[str, Any]:
"""设置 QQ 账号资料。
Args:
nickname: 新昵称。
personal_note: 个性签名。
sex: 性别,支持 ``male``、``female``、``unknown``。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
normalized_sex = str(sex or "").strip().lower()
if normalized_sex and normalized_sex not in {"male", "female", "unknown"}:
raise ValueError("sex 必须为 male、female 或 unknown")
return await self._require_query_service().set_qq_profile(
nickname=self._normalize_non_empty_string(nickname, "nickname"),
personal_note=str(personal_note or "").strip(),
sex=normalized_sex,
)
@API("adapter.napcat.account.get_stranger_info", description="获取陌生人信息", version="1", public=True)
async def api_get_stranger_info(
self,
user_id: NapCatApiIdInput,
no_cache: bool = False,
) -> Optional[Dict[str, Any]]:
"""获取陌生人信息。
Args:
user_id: 用户号。
no_cache: 是否禁用缓存。
Returns:
Optional[Dict[str, Any]]: 陌生人信息字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_stranger_info(
str(self._normalize_positive_int(user_id, "user_id")),
no_cache=bool(no_cache),
)
@API("adapter.napcat.account.get_friend_list", description="获取好友列表", version="1", public=True)
async def api_get_friend_list(self, no_cache: bool = False) -> Optional[List[Dict[str, Any]]]:
"""获取好友列表。
Args:
no_cache: 是否禁用缓存。
Returns:
Optional[List[Dict[str, Any]]]: 好友信息列表;失败时返回 ``None``。
"""
return await self._require_query_service().get_friend_list(no_cache=bool(no_cache))
@API("adapter.napcat.account.create_collection", description="创建收藏", version="1", public=True)
async def api_action_create_collection(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``create_collection`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("create_collection", params)
@API("adapter.napcat.account.delete_friend", description="删除好友", version="1", public=True)
async def api_action_delete_friend(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``delete_friend`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("delete_friend", params)
@API("adapter.napcat.account.fetch_custom_face", description="获取自定义表情", version="1", public=True)
async def api_action_fetch_custom_face(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``fetch_custom_face`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("fetch_custom_face", params)
@API("adapter.napcat.account.get_ai_characters", description="获取AI角色列表", version="1", public=True)
async def api_action_get_ai_characters(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_ai_characters`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_ai_characters", params)
@API("adapter.napcat.account.get_clientkey", description="获取ClientKey", version="1", public=True)
async def api_action_get_clientkey(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_clientkey`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_clientkey", params)
@API("adapter.napcat.account.get_collection_list", description="获取收藏列表", version="1", public=True)
async def api_action_get_collection_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_collection_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_collection_list", params)
@API("adapter.napcat.account.get_cookies", description="获取 Cookies", version="1", public=True)
async def api_action_get_cookies(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_cookies`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_cookies", params)
@API(
"adapter.napcat.account.get_friends_with_category", description="获取带分组的好友列表", version="1", public=True
)
async def api_action_get_friends_with_category(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_friends_with_category`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_friends_with_category", params)
@API("adapter.napcat.account.get_mini_app_ark", description="获取小程序 Ark", version="1", public=True)
async def api_action_get_mini_app_ark(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_mini_app_ark`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_mini_app_ark", params)
@API("adapter.napcat.account.get_profile_like", description="获取资料点赞", version="1", public=True)
async def api_action_get_profile_like(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_profile_like`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_profile_like", params)
@API("adapter.napcat.account.get_recent_contact", description="获取最近会话", version="1", public=True)
async def api_action_get_recent_contact(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_recent_contact`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_recent_contact", params)
@API("adapter.napcat.account.get_rkey", description="获取扩展 RKey", version="1", public=True)
async def api_action_get_rkey(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_rkey`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_rkey", params)
@API("adapter.napcat.account.get_rkey_server", description="获取 RKey 服务器", version="1", public=True)
async def api_action_get_rkey_server(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_rkey_server`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_rkey_server", params)
@API(
"adapter.napcat.account.get_unidirectional_friend_list",
description="获取单向好友列表",
version="1",
public=True,
)
async def api_action_get_unidirectional_friend_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_unidirectional_friend_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_unidirectional_friend_list", params)
@API("adapter.napcat.account.internal_ocr_image", description="图片 OCR 识别 (内部)", version="1", public=True)
async def api_action_internal_ocr_image(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``.ocr_image`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action(".ocr_image", params)
@API("adapter.napcat.account.nc_get_rkey", description="获取 RKey", version="1", public=True)
async def api_action_nc_get_rkey(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``nc_get_rkey`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("nc_get_rkey", params)
@API("adapter.napcat.account.ocr_image", description="图片 OCR 识别", version="1", public=True)
async def api_action_ocr_image(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``ocr_image`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("ocr_image", params)
@API("adapter.napcat.account.send_like", description="点赞", version="1", public=True)
async def api_action_send_like(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_like`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_like", params)
@API("adapter.napcat.account.set_diy_online_status", description="设置自定义在线状态", version="1", public=True)
async def api_action_set_diy_online_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_diy_online_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_diy_online_status", params)
@API("adapter.napcat.account.set_friend_add_request", description="处理加好友请求", version="1", public=True)
async def api_action_set_friend_add_request(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_friend_add_request`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_friend_add_request", params)
@API("adapter.napcat.account.set_friend_remark", description="设置好友备注", version="1", public=True)
async def api_action_set_friend_remark(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_friend_remark`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_friend_remark", params)
@API("adapter.napcat.account.set_qq_avatar", description="设置QQ头像", version="1", public=True)
async def api_action_set_qq_avatar(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_qq_avatar`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_qq_avatar", params)
@API("adapter.napcat.account.set_self_longnick", description="设置个性签名", version="1", public=True)
async def api_action_set_self_longnick(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_self_longnick`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_self_longnick", params)
@API("adapter.napcat.account.translate_en2zh", description="英文单词翻译", version="1", public=True)
async def api_action_translate_en2zh(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``translate_en2zh`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("translate_en2zh", params)

View File

@@ -0,0 +1,535 @@
"""NapCat 文件与流式 API 端点。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from maibot_sdk import API
from .support import NapCatApiParamsInput, NapCatApiSupportMixin
class NapCatFileApiMixin(NapCatApiSupportMixin):
"""NapCat 文件、媒体与流式相关 API。"""
@API("adapter.napcat.file.get_record", description="获取语音文件详情", version="1", public=True)
async def api_get_record(
self,
file: object = "",
file_id: str = "",
out_format: str = "wav",
) -> Optional[Dict[str, Any]]:
"""获取语音文件详情。
Args:
file: 语音文件名。
file_id: 可选文件 ID。
out_format: 输出格式;默认保持兼容旧行为的 ``wav``。
Returns:
Optional[Dict[str, Any]]: 语音文件详情;失败时返回 ``None``。
"""
normalized_file_name = str(file or "").strip() or None
normalized_file_id = str(file_id or "").strip() or None
normalized_out_format = str(out_format or "").strip()
if normalized_file_name is None and normalized_file_id is None:
raise ValueError("file 或 file_id 至少提供一个")
return await self._require_query_service().get_record_detail(
file_name=normalized_file_name,
file_id=normalized_file_id,
out_format=normalized_out_format,
)
@API("adapter.napcat.file.cancel_online_file", description="取消在线文件", version="1", public=True)
async def api_action_cancel_online_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``cancel_online_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("cancel_online_file", params)
@API("adapter.napcat.file.clean_stream_temp_file", description="清理流式传输临时文件", version="1", public=True)
async def api_action_clean_stream_temp_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``clean_stream_temp_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("clean_stream_temp_file", params)
@API("adapter.napcat.file.create_flash_task", description="创建闪传任务", version="1", public=True)
async def api_action_create_flash_task(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``create_flash_task`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("create_flash_task", params)
@API("adapter.napcat.file.create_group_file_folder", description="创建群文件目录", version="1", public=True)
async def api_action_create_group_file_folder(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``create_group_file_folder`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("create_group_file_folder", params)
@API("adapter.napcat.file.del_group_album_media", description="删除群相册媒体", version="1", public=True)
async def api_action_del_group_album_media(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``del_group_album_media`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("del_group_album_media", params)
@API("adapter.napcat.file.delete_group_file", description="删除群文件", version="1", public=True)
async def api_action_delete_group_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``delete_group_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("delete_group_file", params)
@API("adapter.napcat.file.delete_group_folder", description="删除群文件目录", version="1", public=True)
async def api_action_delete_group_folder(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``delete_group_folder`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("delete_group_folder", params)
@API("adapter.napcat.file.do_group_album_comment", description="发表群相册评论", version="1", public=True)
async def api_action_do_group_album_comment(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``do_group_album_comment`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("do_group_album_comment", params)
@API("adapter.napcat.file.download_file", description="下载文件", version="1", public=True)
async def api_action_download_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``download_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("download_file", params)
@API("adapter.napcat.file.download_file_image_stream", description="下载图片文件流", version="1", public=True)
async def api_action_download_file_image_stream(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``download_file_image_stream`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("download_file_image_stream", params)
@API("adapter.napcat.file.download_file_record_stream", description="下载语音文件流", version="1", public=True)
async def api_action_download_file_record_stream(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``download_file_record_stream`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("download_file_record_stream", params)
@API("adapter.napcat.file.download_file_stream", description="下载文件流", version="1", public=True)
async def api_action_download_file_stream(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``download_file_stream`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("download_file_stream", params)
@API("adapter.napcat.file.download_fileset", description="下载文件集", version="1", public=True)
async def api_action_download_fileset(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``download_fileset`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("download_fileset", params)
@API("adapter.napcat.file.get_file", description="获取文件", version="1", public=True)
async def api_action_get_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_file", params)
@API("adapter.napcat.file.get_fileset_id", description="获取文件集 ID", version="1", public=True)
async def api_action_get_fileset_id(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_fileset_id`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_fileset_id", params)
@API("adapter.napcat.file.get_fileset_info", description="获取文件集信息", version="1", public=True)
async def api_action_get_fileset_info(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_fileset_info`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_fileset_info", params)
@API("adapter.napcat.file.get_flash_file_list", description="获取闪传文件列表", version="1", public=True)
async def api_action_get_flash_file_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_flash_file_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_flash_file_list", params)
@API("adapter.napcat.file.get_flash_file_url", description="获取闪传文件链接", version="1", public=True)
async def api_action_get_flash_file_url(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_flash_file_url`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_flash_file_url", params)
@API("adapter.napcat.file.get_group_album_media_list", description="获取群相册媒体列表", version="1", public=True)
async def api_action_get_group_album_media_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_album_media_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_album_media_list", params)
@API("adapter.napcat.file.get_group_file_system_info", description="获取群文件系统信息", version="1", public=True)
async def api_action_get_group_file_system_info(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_file_system_info`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_file_system_info", params)
@API("adapter.napcat.file.get_group_file_url", description="获取群文件URL", version="1", public=True)
async def api_action_get_group_file_url(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_file_url`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_file_url", params)
@API("adapter.napcat.file.get_group_files_by_folder", description="获取群文件夹文件列表", version="1", public=True)
async def api_action_get_group_files_by_folder(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_files_by_folder`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_files_by_folder", params)
@API("adapter.napcat.file.get_group_root_files", description="获取群根目录文件列表", version="1", public=True)
async def api_action_get_group_root_files(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_root_files`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_root_files", params)
@API("adapter.napcat.file.get_image", description="获取图片", version="1", public=True)
async def api_action_get_image(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_image`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_image", params)
@API("adapter.napcat.file.get_online_file_msg", description="获取在线文件消息", version="1", public=True)
async def api_action_get_online_file_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_online_file_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_online_file_msg", params)
@API("adapter.napcat.file.get_private_file_url", description="获取私聊文件URL", version="1", public=True)
async def api_action_get_private_file_url(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_private_file_url`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_private_file_url", params)
@API("adapter.napcat.file.get_qun_album_list", description="获取群相册列表", version="1", public=True)
async def api_action_get_qun_album_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_qun_album_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_qun_album_list", params)
@API("adapter.napcat.file.get_share_link", description="获取文件分享链接", version="1", public=True)
async def api_action_get_share_link(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_share_link`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_share_link", params)
@API("adapter.napcat.file.move_group_file", description="移动群文件", version="1", public=True)
async def api_action_move_group_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``move_group_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("move_group_file", params)
@API("adapter.napcat.file.receive_online_file", description="接收在线文件", version="1", public=True)
async def api_action_receive_online_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``receive_online_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("receive_online_file", params)
@API("adapter.napcat.file.refuse_online_file", description="拒绝在线文件", version="1", public=True)
async def api_action_refuse_online_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``refuse_online_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("refuse_online_file", params)
@API("adapter.napcat.file.rename_group_file", description="重命名群文件", version="1", public=True)
async def api_action_rename_group_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``rename_group_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("rename_group_file", params)
@API("adapter.napcat.file.send_flash_msg", description="发送闪传消息", version="1", public=True)
async def api_action_send_flash_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_flash_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_flash_msg", params)
@API("adapter.napcat.file.send_online_file", description="发送在线文件", version="1", public=True)
async def api_action_send_online_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_online_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_online_file", params)
@API("adapter.napcat.file.send_online_folder", description="发送在线文件夹", version="1", public=True)
async def api_action_send_online_folder(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_online_folder`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_online_folder", params)
@API("adapter.napcat.file.set_group_album_media_like", description="点赞群相册媒体", version="1", public=True)
async def api_action_set_group_album_media_like(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_album_media_like`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_album_media_like", params)
@API("adapter.napcat.file.trans_group_file", description="传输群文件", version="1", public=True)
async def api_action_trans_group_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``trans_group_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("trans_group_file", params)
@API("adapter.napcat.file.upload_file_stream", description="上传文件流", version="1", public=True)
async def api_action_upload_file_stream(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``upload_file_stream`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("upload_file_stream", params)
@API("adapter.napcat.file.upload_group_file", description="上传群文件", version="1", public=True)
async def api_action_upload_group_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``upload_group_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("upload_group_file", params)
@API("adapter.napcat.file.upload_image_to_qun_album", description="上传图片到群相册", version="1", public=True)
async def api_action_upload_image_to_qun_album(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``upload_image_to_qun_album`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("upload_image_to_qun_album", params)
@API("adapter.napcat.file.upload_private_file", description="上传私聊文件", version="1", public=True)
async def api_action_upload_private_file(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``upload_private_file`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("upload_private_file", params)

View File

@@ -0,0 +1,593 @@
"""NapCat 群组与频道 API 端点。"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from maibot_sdk import API
from .support import NapCatApiIdInput, NapCatApiSupportMixin, NapCatApiParamsInput
class NapCatGroupApiMixin(NapCatApiSupportMixin):
"""NapCat 群组、频道与群扩展相关 API。"""
@API("adapter.napcat.group.set_group_ban", description="设置群成员禁言", version="1", public=True)
async def api_set_group_ban(
self,
group_id: NapCatApiIdInput,
user_id: NapCatApiIdInput,
duration: NapCatApiIdInput,
) -> Dict[str, Any]:
"""设置群成员禁言。
Args:
group_id: 群号。
user_id: 用户号。
duration: 禁言秒数。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
normalized_duration = self._normalize_non_negative_int(duration, "duration")
if normalized_duration > 2592000:
raise ValueError("duration 不能超过 2592000 秒")
return await self._require_query_service().set_group_ban(
group_id=self._normalize_positive_int(group_id, "group_id"),
user_id=self._normalize_positive_int(user_id, "user_id"),
duration=normalized_duration,
)
@API("adapter.napcat.group.set_group_whole_ban", description="设置群全体禁言", version="1", public=True)
async def api_set_group_whole_ban(self, group_id: NapCatApiIdInput, enable: bool) -> Dict[str, Any]:
"""设置群全体禁言。
Args:
group_id: 群号。
enable: 是否开启全体禁言。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().set_group_whole_ban(
group_id=self._normalize_positive_int(group_id, "group_id"),
enable=self._normalize_bool(enable, "enable"),
)
@API("adapter.napcat.group.set_group_kick", description="踢出单个群成员", version="1", public=True)
async def api_set_group_kick(
self,
group_id: NapCatApiIdInput,
user_id: NapCatApiIdInput,
reject_add_request: bool = False,
) -> Dict[str, Any]:
"""踢出单个群成员。
Args:
group_id: 群号。
user_id: 用户号。
reject_add_request: 是否拒绝再次加群。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().set_group_kick(
group_id=self._normalize_positive_int(group_id, "group_id"),
user_id=self._normalize_positive_int(user_id, "user_id"),
reject_add_request=bool(reject_add_request),
)
@API("adapter.napcat.group.set_group_kick_members", description="批量踢出群成员", version="1", public=True)
async def api_set_group_kick_members(
self,
group_id: NapCatApiIdInput,
user_id: object,
reject_add_request: bool = False,
) -> Dict[str, Any]:
"""批量踢出群成员。
Args:
group_id: 群号。
user_id: 用户号数组。
reject_add_request: 是否拒绝再次加群。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().set_group_kick_members(
group_id=self._normalize_positive_int(group_id, "group_id"),
user_ids=self._normalize_user_id_list(user_id, "user_id"),
reject_add_request=bool(reject_add_request),
)
@API("adapter.napcat.group.set_group_name", description="设置群名称", version="1", public=True)
async def api_set_group_name(self, group_id: NapCatApiIdInput, group_name: object) -> Dict[str, Any]:
"""设置群名称。
Args:
group_id: 群号。
group_name: 新群名称。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().set_group_name(
group_id=self._normalize_positive_int(group_id, "group_id"),
group_name=self._normalize_non_empty_string(group_name, "group_name"),
)
@API("adapter.napcat.group.get_group_info", description="获取群信息", version="1", public=True)
async def api_get_group_info(self, group_id: NapCatApiIdInput) -> Optional[Dict[str, Any]]:
"""获取群信息。
Args:
group_id: 群号。
Returns:
Optional[Dict[str, Any]]: 群信息字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_info(
str(self._normalize_positive_int(group_id, "group_id"))
)
@API("adapter.napcat.group.get_group_detail_info", description="获取群详细信息", version="1", public=True)
async def api_get_group_detail_info(self, group_id: NapCatApiIdInput) -> Optional[Dict[str, Any]]:
"""获取群详细信息。
Args:
group_id: 群号。
Returns:
Optional[Dict[str, Any]]: 群详细信息字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_detail_info(
str(self._normalize_positive_int(group_id, "group_id"))
)
@API("adapter.napcat.group.get_group_list", description="获取群列表", version="1", public=True)
async def api_get_group_list(self, no_cache: bool = False) -> Optional[List[Dict[str, Any]]]:
"""获取群列表。
Args:
no_cache: 是否禁用缓存。
Returns:
Optional[List[Dict[str, Any]]]: 群信息列表;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_list(no_cache=bool(no_cache))
@API("adapter.napcat.group.get_group_at_all_remain", description="获取群 @ 全体剩余次数", version="1", public=True)
async def api_get_group_at_all_remain(self, group_id: NapCatApiIdInput) -> Optional[Dict[str, Any]]:
"""获取群 @ 全体剩余次数。
Args:
group_id: 群号。
Returns:
Optional[Dict[str, Any]]: 剩余次数信息;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_at_all_remain(
str(self._normalize_positive_int(group_id, "group_id"))
)
@API("adapter.napcat.group.get_group_member_info", description="获取群成员信息", version="1", public=True)
async def api_get_group_member_info(
self,
group_id: NapCatApiIdInput,
user_id: NapCatApiIdInput,
no_cache: bool = True,
) -> Optional[Dict[str, Any]]:
"""获取群成员信息。
Args:
group_id: 群号。
user_id: 用户号。
no_cache: 是否禁用缓存。
Returns:
Optional[Dict[str, Any]]: 群成员信息字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_member_info(
group_id=str(self._normalize_positive_int(group_id, "group_id")),
user_id=str(self._normalize_positive_int(user_id, "user_id")),
no_cache=bool(no_cache),
)
@API("adapter.napcat.group.get_group_member_list", description="获取群成员列表", version="1", public=True)
async def api_get_group_member_list(
self,
group_id: NapCatApiIdInput,
no_cache: bool = False,
) -> Optional[List[Dict[str, Any]]]:
"""获取群成员列表。
Args:
group_id: 群号。
no_cache: 是否禁用缓存。
Returns:
Optional[List[Dict[str, Any]]]: 群成员信息列表;失败时返回 ``None``。
"""
return await self._require_query_service().get_group_member_list(
group_id=str(self._normalize_positive_int(group_id, "group_id")),
no_cache=bool(no_cache),
)
@API("adapter.napcat.group.delete_essence_msg", description="移出精华消息", version="1", public=True)
async def api_action_delete_essence_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``delete_essence_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("delete_essence_msg", params)
@API("adapter.napcat.group.delete_group_notice", description="删除群公告", version="1", public=True)
async def api_action_delete_group_notice(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_del_group_notice`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_del_group_notice", params)
@API("adapter.napcat.group.get_essence_msg_list", description="获取群精华消息", version="1", public=True)
async def api_action_get_essence_msg_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_essence_msg_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_essence_msg_list", params)
@API("adapter.napcat.group.get_group_honor_info", description="获取群荣誉信息", version="1", public=True)
async def api_action_get_group_honor_info(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_honor_info`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_honor_info", params)
@API(
"adapter.napcat.group.get_group_ignore_add_request",
description="获取群被忽略的加群请求",
version="1",
public=True,
)
async def api_action_get_group_ignore_add_request(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_ignore_add_request`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_ignore_add_request", params)
@API("adapter.napcat.group.get_group_ignored_notifies", description="获取群忽略通知", version="1", public=True)
async def api_action_get_group_ignored_notifies(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_ignored_notifies`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_ignored_notifies", params)
@API("adapter.napcat.group.get_group_info_ex", description="获取群详细信息 (扩展)", version="1", public=True)
async def api_action_get_group_info_ex(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_info_ex`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_info_ex", params)
@API("adapter.napcat.group.get_group_notice", description="获取群公告", version="1", public=True)
async def api_action_get_group_notice(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_get_group_notice`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_get_group_notice", params)
@API("adapter.napcat.group.get_group_shut_list", description="获取群禁言列表", version="1", public=True)
async def api_action_get_group_shut_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_shut_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_shut_list", params)
@API("adapter.napcat.group.get_group_system_msg", description="获取群系统消息", version="1", public=True)
async def api_action_get_group_system_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_system_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_system_msg", params)
@API("adapter.napcat.group.get_guild_list", description="获取频道列表", version="1", public=True)
async def api_action_get_guild_list(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_guild_list`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_guild_list", params)
@API("adapter.napcat.group.get_guild_service_profile", description="获取频道个人信息", version="1", public=True)
async def api_action_get_guild_service_profile(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_guild_service_profile`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_guild_service_profile", params)
@API("adapter.napcat.group.group_poke", description="发送戳一戳", version="1", public=True)
async def api_action_group_poke(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``group_poke`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("group_poke", params)
@API("adapter.napcat.group.handle_quick_operation_internal", description="处理快速操作", version="1", public=True)
async def api_action_handle_quick_operation_internal(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``.handle_quick_operation`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action(".handle_quick_operation", params)
@API("adapter.napcat.group.send_group_msg", description="发送群消息", version="1", public=True)
async def api_action_send_group_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_group_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_group_msg", params)
@API("adapter.napcat.group.send_group_notice", description="发送群公告", version="1", public=True)
async def api_action_send_group_notice(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_send_group_notice`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_send_group_notice", params)
@API("adapter.napcat.group.send_group_sign", description="群打卡", version="1", public=True)
async def api_action_send_group_sign(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_group_sign`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_group_sign", params)
@API("adapter.napcat.group.set_essence_msg", description="设置精华消息", version="1", public=True)
async def api_action_set_essence_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_essence_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_essence_msg", params)
@API("adapter.napcat.group.set_group_add_option", description="设置群加群选项", version="1", public=True)
async def api_action_set_group_add_option(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_add_option`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_add_option", params)
@API("adapter.napcat.group.set_group_add_request", description="处理加群请求", version="1", public=True)
async def api_action_set_group_add_request(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_add_request`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_add_request", params)
@API("adapter.napcat.group.set_group_admin", description="设置群管理员", version="1", public=True)
async def api_action_set_group_admin(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_admin`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_admin", params)
@API("adapter.napcat.group.set_group_card", description="设置群名片", version="1", public=True)
async def api_action_set_group_card(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_card`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_card", params)
@API("adapter.napcat.group.set_group_leave", description="退出群组", version="1", public=True)
async def api_action_set_group_leave(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_leave`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_leave", params)
@API("adapter.napcat.group.set_group_portrait", description="设置群头像", version="1", public=True)
async def api_action_set_group_portrait(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_portrait`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_portrait", params)
@API("adapter.napcat.group.set_group_remark", description="设置群备注", version="1", public=True)
async def api_action_set_group_remark(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_remark`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_remark", params)
@API(
"adapter.napcat.group.set_group_robot_add_option", description="设置群机器人加群选项", version="1", public=True
)
async def api_action_set_group_robot_add_option(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_robot_add_option`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_robot_add_option", params)
@API("adapter.napcat.group.set_group_search", description="设置群搜索选项", version="1", public=True)
async def api_action_set_group_search(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_search`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_search", params)
@API("adapter.napcat.group.set_group_sign", description="群打卡", version="1", public=True)
async def api_action_set_group_sign(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_sign`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_sign", params)
@API("adapter.napcat.group.set_group_special_title", description="设置专属头衔", version="1", public=True)
async def api_action_set_group_special_title(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_special_title`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_special_title", params)
@API("adapter.napcat.group.set_group_todo", description="设置群待办", version="1", public=True)
async def api_action_set_group_todo(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_group_todo`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_group_todo", params)
@API("adapter.napcat.file.test_download_stream", description="测试下载流", version="1", public=True)
async def api_action_test_download_stream(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``test_download_stream`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("test_download_stream", params)

View File

@@ -0,0 +1,431 @@
"""NapCat 消息与互动 API 端点。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from maibot_sdk import API
from .support import NapCatApiIdInput, NapCatApiParamsInput, NapCatApiSupportMixin
class NapCatMessageApiMixin(NapCatApiSupportMixin):
"""NapCat 消息、互动与 AI 相关 API。"""
@API("adapter.napcat.message.send_poke", description="发送戳一戳", version="1", public=True)
async def api_send_poke(
self,
user_id: Optional[NapCatApiIdInput] = None,
group_id: Optional[NapCatApiIdInput] = None,
target_id: Optional[NapCatApiIdInput] = None,
qq_id: Optional[NapCatApiIdInput] = None,
) -> Dict[str, Any]:
"""发送戳一戳。
Args:
user_id: 目标用户号。
group_id: 可选群号。
target_id: 官方 ``send_poke`` 动作支持的目标 ID。
qq_id: 兼容旧版调用方式的 ``user_id`` 别名。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
normalized_user_id_input = str(user_id).strip() if user_id is not None else ""
normalized_qq_id_input = str(qq_id).strip() if qq_id is not None else ""
if normalized_user_id_input and normalized_qq_id_input:
resolved_user_id = self._normalize_positive_int(user_id, "user_id")
resolved_qq_id = self._normalize_positive_int(qq_id, "qq_id")
if resolved_user_id != resolved_qq_id:
raise ValueError("user_id 与 qq_id 不能同时传递不同的值")
elif normalized_user_id_input:
resolved_user_id = self._normalize_positive_int(user_id, "user_id")
elif normalized_qq_id_input:
resolved_user_id = self._normalize_positive_int(qq_id, "qq_id")
else:
raise ValueError("user_id 不能为空")
normalized_group_id: Optional[int] = None
if group_id is not None and str(group_id).strip():
normalized_group_id = self._normalize_positive_int(group_id, "group_id")
normalized_target_id: Optional[int] = None
if target_id is not None and str(target_id).strip():
normalized_target_id = self._normalize_positive_int(target_id, "target_id")
return await self._require_query_service().send_poke(
user_id=resolved_user_id,
group_id=normalized_group_id,
target_id=normalized_target_id,
)
@API("adapter.napcat.message.delete_msg", description="撤回消息", version="1", public=True)
async def api_delete_msg(self, message_id: NapCatApiIdInput) -> Dict[str, Any]:
"""撤回消息。
Args:
message_id: 消息 ID。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().delete_message(
message_id=self._normalize_positive_int(message_id, "message_id")
)
@API("adapter.napcat.message.send_group_ai_record", description="发送群 AI 语音", version="1", public=True)
async def api_send_group_ai_record(
self,
group_id: NapCatApiIdInput,
character: object,
text: object,
) -> Dict[str, Any]:
"""发送群 AI 语音。
Args:
group_id: 群号。
character: 角色标识。
text: 语音文本。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().send_group_ai_record(
group_id=self._normalize_positive_int(group_id, "group_id"),
character=self._normalize_non_empty_string(character, "character"),
text=self._normalize_non_empty_string(text, "text"),
)
@API("adapter.napcat.message.set_msg_emoji_like", description="给消息贴表情", version="1", public=True)
async def api_set_msg_emoji_like(
self,
message_id: NapCatApiIdInput,
emoji_id: NapCatApiIdInput,
set: bool = True,
) -> Dict[str, Any]:
"""给消息贴表情或取消表情。
Args:
message_id: 消息 ID。
emoji_id: 表情 ID。
set: 是否设置为已贴表情。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._require_query_service().set_message_emoji_like(
message_id=self._normalize_positive_int(message_id, "message_id"),
emoji_id=self._normalize_positive_int(emoji_id, "emoji_id"),
set_like=bool(set),
)
@API("adapter.napcat.message.get_msg", description="获取消息详情", version="1", public=True)
async def api_get_msg(self, message_id: NapCatApiIdInput) -> Optional[Dict[str, Any]]:
"""获取消息详情。
Args:
message_id: 消息 ID。
Returns:
Optional[Dict[str, Any]]: 消息详情字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_message_detail(
str(self._normalize_positive_int(message_id, "message_id"))
)
@API("adapter.napcat.message.get_forward_msg", description="获取合并转发消息", version="1", public=True)
async def api_get_forward_msg(
self,
message_id: object = "",
id: object = "",
) -> Optional[Dict[str, Any]]:
"""获取合并转发消息详情。
Args:
message_id: 合并转发消息 ID。
id: NapCat 官方文档中的兼容字段。
Returns:
Optional[Dict[str, Any]]: 合并转发消息详情;失败时返回 ``None``。
"""
normalized_message_id = str(message_id or "").strip()
normalized_forward_id = str(id or "").strip()
if normalized_message_id and normalized_forward_id and normalized_message_id != normalized_forward_id:
raise ValueError("message_id 与 id 不能同时传递不同的值")
if not normalized_message_id and not normalized_forward_id:
raise ValueError("message_id 或 id 至少提供一个")
return await self._require_query_service().get_forward_message(
message_id=normalized_message_id or None,
forward_id=normalized_forward_id or None,
)
@API("adapter.napcat.message.ark_share_group", description="分享群 (Ark)", version="1", public=True)
async def api_action_ark_share_group(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``ArkShareGroup`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("ArkShareGroup", params)
@API("adapter.napcat.message.ark_share_peer", description="分享用户 (Ark)", version="1", public=True)
async def api_action_ark_share_peer(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``ArkSharePeer`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("ArkSharePeer", params)
@API(
"adapter.napcat.message.click_inline_keyboard_button", description="点击内联键盘按钮", version="1", public=True
)
async def api_action_click_inline_keyboard_button(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``click_inline_keyboard_button`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("click_inline_keyboard_button", params)
@API("adapter.napcat.message.fetch_emoji_like", description="获取表情点赞详情", version="1", public=True)
async def api_action_fetch_emoji_like(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``fetch_emoji_like`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("fetch_emoji_like", params)
@API("adapter.napcat.message.forward_friend_single_msg", description="转发单条消息", version="1", public=True)
async def api_action_forward_friend_single_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``forward_friend_single_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("forward_friend_single_msg", params)
@API("adapter.napcat.message.forward_group_single_msg", description="转发单条消息", version="1", public=True)
async def api_action_forward_group_single_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``forward_group_single_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("forward_group_single_msg", params)
@API("adapter.napcat.message.friend_poke", description="发送戳一戳", version="1", public=True)
async def api_action_friend_poke(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``friend_poke`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("friend_poke", params)
@API("adapter.napcat.message.get_ai_record", description="获取 AI 语音", version="1", public=True)
async def api_action_get_ai_record(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_ai_record`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_ai_record", params)
@API("adapter.napcat.message.get_emoji_likes", description="获取消息表情点赞列表", version="1", public=True)
async def api_action_get_emoji_likes(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_emoji_likes`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_emoji_likes", params)
@API("adapter.napcat.message.get_friend_msg_history", description="获取好友历史消息", version="1", public=True)
async def api_action_get_friend_msg_history(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_friend_msg_history`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_friend_msg_history", params)
@API("adapter.napcat.message.get_group_msg_history", description="获取群历史消息", version="1", public=True)
async def api_action_get_group_msg_history(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_group_msg_history`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_group_msg_history", params)
@API("adapter.napcat.message.mark_all_as_read", description="标记所有消息已读", version="1", public=True)
async def api_action_mark_all_as_read(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_mark_all_as_read`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_mark_all_as_read", params)
@API("adapter.napcat.message.mark_group_msg_as_read", description="标记群聊已读", version="1", public=True)
async def api_action_mark_group_msg_as_read(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``mark_group_msg_as_read`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("mark_group_msg_as_read", params)
@API("adapter.napcat.message.mark_msg_as_read", description="标记消息已读 (Go-CQHTTP)", version="1", public=True)
async def api_action_mark_msg_as_read(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``mark_msg_as_read`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("mark_msg_as_read", params)
@API("adapter.napcat.message.mark_private_msg_as_read", description="标记私聊已读", version="1", public=True)
async def api_action_mark_private_msg_as_read(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``mark_private_msg_as_read`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("mark_private_msg_as_read", params)
@API("adapter.napcat.message.send_ark_share", description="分享用户 (Ark)", version="1", public=True)
async def api_action_send_ark_share(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_ark_share`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_ark_share", params)
@API("adapter.napcat.message.send_forward_msg", description="发送合并转发消息", version="1", public=True)
async def api_action_send_forward_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_forward_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_forward_msg", params)
@API("adapter.napcat.message.send_group_ark_share", description="分享群 (Ark)", version="1", public=True)
async def api_action_send_group_ark_share(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_group_ark_share`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_group_ark_share", params)
@API("adapter.napcat.message.send_group_forward_msg", description="发送群合并转发消息", version="1", public=True)
async def api_action_send_group_forward_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_group_forward_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_group_forward_msg", params)
@API("adapter.napcat.message.send_msg", description="发送消息", version="1", public=True)
async def api_action_send_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_msg", params)
@API(
"adapter.napcat.message.send_private_forward_msg", description="发送私聊合并转发消息", version="1", public=True
)
async def api_action_send_private_forward_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_private_forward_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_private_forward_msg", params)
@API("adapter.napcat.message.send_private_msg", description="发送私聊消息", version="1", public=True)
async def api_action_send_private_msg(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_private_msg`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_private_msg", params)

View File

@@ -0,0 +1,69 @@
"""NapCat 消息工具扩展。"""
from __future__ import annotations
from typing import Any, Dict
from maibot_sdk import Tool
from maibot_sdk.types import ToolParameterInfo, ToolParamType
from .message import NapCatMessageApiMixin
def _tool_param(name: str, param_type: ToolParamType, description: str, required: bool) -> ToolParameterInfo:
"""构造工具参数声明。"""
return ToolParameterInfo(name=name, param_type=param_type, description=description, required=required)
@Tool(
"find_user_qq_id",
description="根据指定消息的 msg_id 查询该条消息发送者的 QQ 号信息",
parameters=[
_tool_param("msg_id", ToolParamType.STRING, "要查询的消息 ID", True),
],
)
async def handle_find_user_qq_id(self: NapCatMessageApiMixin, msg_id: str = "", **kwargs: Any) -> Dict[str, Any]:
"""根据消息 ID 查询发送者的 QQ 号信息。"""
del kwargs
normalized_msg_id = str(self._normalize_positive_int(msg_id, "msg_id"))
message_detail = await self._require_query_service().get_message_detail(normalized_msg_id)
if not isinstance(message_detail, dict):
return {
"success": False,
"content": f"未找到 msg_id={normalized_msg_id} 对应的消息记录",
"msg_id": normalized_msg_id,
}
sender = message_detail.get("sender", {})
if not isinstance(sender, dict):
sender = {}
user_id = str(message_detail.get("user_id") or sender.get("user_id") or sender.get("uin") or "").strip()
if not user_id:
return {
"success": False,
"content": f"已获取消息详情,但未解析出 msg_id={normalized_msg_id} 的发送者 QQ 号",
"msg_id": normalized_msg_id,
"message_detail": message_detail,
}
nickname = str(sender.get("nickname") or sender.get("name") or "").strip()
cardname = str(sender.get("card") or "").strip()
sender_info = {
"user_id": user_id,
"nickname": nickname or None,
"cardname": cardname or None,
}
display_name = cardname or nickname or user_id
return {
"success": True,
"content": f"msg_id={normalized_msg_id} 的发送者 QQ 号是 {user_id}(显示名:{display_name}",
"msg_id": normalized_msg_id,
"sender": sender_info,
}
NapCatMessageApiMixin.handle_find_user_qq_id = handle_find_user_qq_id

View File

@@ -0,0 +1,275 @@
"""NapCat API 端点的公共辅助能力。"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, TypeAlias
from maibot_sdk import API
from ..types import NapCatActionParamsInput, NapCatActionResponse, NapCatIdInput
if TYPE_CHECKING:
from ..services import NapCatActionService, NapCatQueryService
NapCatApiIdInput: TypeAlias = NapCatIdInput
NapCatApiParamsInput: TypeAlias = NapCatActionParamsInput
class NapCatApiSupportMixin:
"""NapCat API 端点共享辅助逻辑。"""
_action_service: Optional["NapCatActionService"]
_query_service: Optional["NapCatQueryService"]
def _ensure_runtime_components(self) -> None:
"""确保运行时组件已经初始化。"""
raise NotImplementedError
@staticmethod
def _coerce_int(value: object, field_name: str, expectation: str) -> int:
"""将受支持的输入值转换为整数。
Args:
value: 待转换的值。
field_name: 字段名,用于错误提示。
expectation: 期望的取值描述,例如“正整数”。
Returns:
int: 转换后的整数值。
Raises:
ValueError: 当值无法转换为整数时抛出。
"""
if isinstance(value, bool):
raise ValueError(f"{field_name} 必须是{expectation}")
if isinstance(value, int):
return value
if isinstance(value, float):
try:
return int(value)
except (OverflowError, ValueError) as exc:
raise ValueError(f"{field_name} 必须是{expectation}") from exc
if isinstance(value, str):
normalized_value = value.strip()
if not normalized_value:
raise ValueError(f"{field_name} 必须是{expectation}")
try:
return int(normalized_value)
except ValueError as exc:
raise ValueError(f"{field_name} 必须是{expectation}") from exc
raise ValueError(f"{field_name} 必须是{expectation}")
def _require_query_service(self) -> "NapCatQueryService":
"""返回当前可用的 NapCat 查询服务。
Returns:
NapCatQueryService: 已初始化的查询服务。
Raises:
RuntimeError: 当查询服务尚未初始化时抛出。
"""
self._ensure_runtime_components()
query_service = self._query_service
if query_service is None:
raise RuntimeError("NapCat 查询服务尚未初始化")
return query_service
def _require_action_service(self) -> "NapCatActionService":
"""返回当前可用的 NapCat 动作服务。
Returns:
NapCatActionService: 已初始化的动作服务。
Raises:
RuntimeError: 当动作服务尚未初始化时抛出。
"""
self._ensure_runtime_components()
action_service = self._action_service
if action_service is None:
raise RuntimeError("NapCat 动作服务尚未初始化")
return action_service
@staticmethod
def _normalize_positive_int(value: object, field_name: str) -> int:
"""将任意值规范化为正整数。
Args:
value: 待规范化的值。
field_name: 字段名,用于错误提示。
Returns:
int: 规范化后的正整数。
Raises:
ValueError: 当值无法转换为正整数时抛出。
"""
normalized_value = NapCatApiSupportMixin._coerce_int(value, field_name, "正整数")
if normalized_value <= 0:
raise ValueError(f"{field_name} 必须是正整数")
return normalized_value
@staticmethod
def _normalize_non_negative_int(value: object, field_name: str) -> int:
"""将任意值规范化为非负整数。
Args:
value: 待规范化的值。
field_name: 字段名,用于错误提示。
Returns:
int: 规范化后的非负整数。
Raises:
ValueError: 当值无法转换为非负整数时抛出。
"""
normalized_value = NapCatApiSupportMixin._coerce_int(value, field_name, "非负整数")
if normalized_value < 0:
raise ValueError(f"{field_name} 必须是非负整数")
return normalized_value
@staticmethod
def _normalize_bool(value: object, field_name: str) -> bool:
"""将任意值规范化为布尔值。
Args:
value: 待规范化的值。
field_name: 字段名,用于错误提示。
Returns:
bool: 规范化后的布尔值。
Raises:
ValueError: 当值不是布尔值时抛出。
"""
if not isinstance(value, bool):
raise ValueError(f"{field_name} 必须是布尔值")
return value
@staticmethod
def _normalize_non_empty_string(value: object, field_name: str) -> str:
"""将任意值规范化为非空字符串。
Args:
value: 待规范化的值。
field_name: 字段名,用于错误提示。
Returns:
str: 规范化后的字符串。
Raises:
ValueError: 当值为空时抛出。
"""
normalized_value = str(value or "").strip()
if not normalized_value:
raise ValueError(f"{field_name} 不能为空")
return normalized_value
@classmethod
def _normalize_user_id_list(cls, values: object, field_name: str) -> List[int]:
"""将任意值规范化为用户号列表。
Args:
values: 待规范化的值。
field_name: 字段名,用于错误提示。
Returns:
List[int]: 规范化后的用户号列表。
Raises:
ValueError: 当值不是非空数组时抛出。
"""
if not isinstance(values, list) or not values:
raise ValueError(f"{field_name} 必须是非空数组")
return [cls._normalize_positive_int(value, field_name) for value in values]
@staticmethod
def _normalize_params(params: NapCatApiParamsInput) -> Dict[str, Any]:
"""将动作参数规范化为可变字典。
Args:
params: 调用方提供的参数对象。
Returns:
Dict[str, Any]: 规范化后的参数字典。
Raises:
ValueError: 当 ``params`` 不是映射对象时抛出。
"""
if params is None:
return {}
if not isinstance(params, Mapping):
raise ValueError("params 必须是对象")
return {str(key): value for key, value in params.items()}
async def _call_napcat_action(
self,
action_name: str,
params: NapCatApiParamsInput = None,
) -> NapCatActionResponse:
"""调用 NapCat 动作并返回原始响应。
Args:
action_name: NapCat 动作名称。
params: 传递给 NapCat 的动作参数。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
normalized_action_name = self._normalize_non_empty_string(action_name, "action_name")
normalized_params = self._normalize_params(params)
return await self._require_action_service().call_action(normalized_action_name, normalized_params)
async def _call_napcat_action_data(
self,
action_name: str,
params: NapCatApiParamsInput = None,
) -> Any:
"""调用 NapCat 动作并返回 ``data`` 字段。
Args:
action_name: NapCat 动作名称。
params: 传递给 NapCat 的动作参数。
Returns:
Any: NapCat 响应中的 ``data`` 字段。
"""
normalized_action_name = self._normalize_non_empty_string(action_name, "action_name")
normalized_params = self._normalize_params(params)
return await self._require_action_service().call_action_data(normalized_action_name, normalized_params)
@API("adapter.napcat.action.call", description="调用任意 OneBot 动作", version="1", public=True)
async def api_call_action(
self,
action_name: str = "",
params: NapCatApiParamsInput = None,
) -> NapCatActionResponse:
"""调用任意 OneBot 动作。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action(action_name, params)
@API(
"adapter.napcat.action.call_data", description="调用任意 OneBot 动作并返回 data 字段", version="1", public=True
)
async def api_call_action_data(
self,
action_name: str = "",
params: NapCatApiParamsInput = None,
) -> Any:
"""调用任意 OneBot 动作并返回 ``data`` 字段。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Any: NapCat 响应中的 ``data`` 字段。
"""
return await self._call_napcat_action_data(action_name, params)

View File

@@ -0,0 +1,290 @@
"""NapCat 系统与运行时 API 端点。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from maibot_sdk import API
from .support import NapCatApiParamsInput, NapCatApiSupportMixin
class NapCatSystemApiMixin(NapCatApiSupportMixin):
"""NapCat 系统状态、凭证与运行控制相关 API。"""
@API("adapter.napcat.system.get_login_info", description="获取当前登录账号信息", version="1", public=True)
async def api_get_login_info(self) -> Optional[Dict[str, Any]]:
"""获取当前登录账号信息。
Returns:
Optional[Dict[str, Any]]: 登录信息字典;失败时返回 ``None``。
"""
return await self._require_query_service().get_login_info()
@API("adapter.napcat.system.bot_exit", description="退出登录", version="1", public=True)
async def api_action_bot_exit(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``bot_exit`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("bot_exit", params)
@API("adapter.napcat.system.can_send_image", description="是否可以发送图片", version="1", public=True)
async def api_action_can_send_image(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``can_send_image`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("can_send_image", params)
@API("adapter.napcat.system.can_send_record", description="是否可以发送语音", version="1", public=True)
async def api_action_can_send_record(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``can_send_record`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("can_send_record", params)
@API("adapter.napcat.system.check_url_safely", description="检查URL安全性", version="1", public=True)
async def api_action_check_url_safely(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``check_url_safely`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("check_url_safely", params)
@API("adapter.napcat.system.clean_cache", description="清理缓存", version="1", public=True)
async def api_action_clean_cache(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``clean_cache`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("clean_cache", params)
@API("adapter.napcat.system.get_credentials", description="获取登录凭证", version="1", public=True)
async def api_action_get_credentials(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_credentials`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_credentials", params)
@API("adapter.napcat.system.get_csrf_token", description="获取 CSRF Token", version="1", public=True)
async def api_action_get_csrf_token(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_csrf_token`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_csrf_token", params)
@API(
"adapter.napcat.system.get_doubt_friends_add_request", description="获取可疑好友申请", version="1", public=True
)
async def api_action_get_doubt_friends_add_request(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_doubt_friends_add_request`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_doubt_friends_add_request", params)
@API("adapter.napcat.system.get_model_show", description="获取机型显示", version="1", public=True)
async def api_action_get_model_show(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_get_model_show`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_get_model_show", params)
@API("adapter.napcat.system.get_online_clients", description="获取在线客户端", version="1", public=True)
async def api_action_get_online_clients(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_online_clients`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_online_clients", params)
@API("adapter.napcat.system.get_robot_uin_range", description="获取机器人 UIN 范围", version="1", public=True)
async def api_action_get_robot_uin_range(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_robot_uin_range`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_robot_uin_range", params)
@API("adapter.napcat.system.get_status", description="获取运行状态", version="1", public=True)
async def api_action_get_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_status", params)
@API("adapter.napcat.system.get_version_info", description="获取版本信息", version="1", public=True)
async def api_action_get_version_info(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``get_version_info`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("get_version_info", params)
@API("adapter.napcat.system.nc_get_packet_status", description="获取Packet状态", version="1", public=True)
async def api_action_nc_get_packet_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``nc_get_packet_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("nc_get_packet_status", params)
@API("adapter.napcat.system.nc_get_user_status", description="获取用户在线状态", version="1", public=True)
async def api_action_nc_get_user_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``nc_get_user_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("nc_get_user_status", params)
@API("adapter.napcat.system.send_packet", description="发送原始数据包", version="1", public=True)
async def api_action_send_packet(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``send_packet`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("send_packet", params)
@API(
"adapter.napcat.system.set_doubt_friends_add_request", description="处理可疑好友申请", version="1", public=True
)
async def api_action_set_doubt_friends_add_request(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_doubt_friends_add_request`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_doubt_friends_add_request", params)
@API("adapter.napcat.system.set_input_status", description="设置输入状态", version="1", public=True)
async def api_action_set_input_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_input_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_input_status", params)
@API("adapter.napcat.system.set_model_show", description="设置机型", version="1", public=True)
async def api_action_set_model_show(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``_set_model_show`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("_set_model_show", params)
@API("adapter.napcat.system.set_online_status", description="设置在线状态", version="1", public=True)
async def api_action_set_online_status(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_online_status`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_online_status", params)
@API("adapter.napcat.system.set_restart", description="重启服务", version="1", public=True)
async def api_action_set_restart(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``set_restart`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("set_restart", params)
@API("adapter.napcat.system.unknown_action", description="unknown", version="1", public=True)
async def api_action_unknown_action(self, params: NapCatApiParamsInput = None) -> Dict[str, Any]:
"""调用 NapCat 的 ``unknown`` 动作。
Args:
params: 传递给 NapCat 的动作参数字典;具体字段请参考 NapCat 官方文档。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
"""
return await self._call_napcat_action("unknown", params)

View File

@@ -0,0 +1 @@
"""NapCat 编解码组件导出。"""

View File

@@ -0,0 +1,5 @@
"""NapCat 入站编解码导出。"""
from .message_codec import NapCatInboundCodec
__all__ = ["NapCatInboundCodec"]

View File

@@ -0,0 +1,545 @@
"""NapCat 入站 JSON 卡片解析辅助。"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Mapping, Optional
import hashlib
import json
import re
from ...qq_emoji_list import QQ_FACE
from ...types import NapCatSegment, NapCatSegments
if TYPE_CHECKING:
from ...services import NapCatQueryService
class NapCatInboundCardMixin:
"""封装入站 JSON 卡片与预览内容转换逻辑。"""
if TYPE_CHECKING:
_query_service: NapCatQueryService
@staticmethod
def _build_text_segment(text: str) -> NapCatSegment: ...
@staticmethod
def _encode_binary(binary_data: bytes) -> str: ...
async def _build_json_segments(self, segment_data: Mapping[str, Any]) -> NapCatSegments:
"""将 JSON 卡片最佳努力转换为消息段列表。
Args:
segment_data: OneBot ``json`` 段的 ``data`` 字典。
Returns:
NapCatSegments: 转换后的消息段列表。
"""
json_data = str(segment_data.get("data") or "").strip()
if not json_data:
return [self._build_text_segment("[json]")]
try:
parsed_json = json.loads(json_data)
except Exception:
return [self._build_text_segment("[json]")]
if not isinstance(parsed_json, Mapping):
return [self._build_text_segment("[json]")]
app_name = str(parsed_json.get("app") or "").strip()
meta = parsed_json.get("meta", {})
if not isinstance(meta, Mapping):
meta = {}
if app_name == "com.tencent.mannounce":
return [self._build_mannounce_segment(meta)]
if app_name in {"com.tencent.music.lua", "com.tencent.structmsg"}:
music_segments = self._build_music_card_segments(meta)
if music_segments:
return music_segments
if app_name == "com.tencent.miniapp_01":
return await self._build_preview_text_segments(
self._build_miniapp_text(meta),
self._extract_preview_url(meta, "detail_1"),
)
if app_name == "com.tencent.giftmall.giftark":
gift_text = self._build_gift_text(meta)
if gift_text:
return [self._build_text_segment(gift_text)]
if app_name == "com.tencent.contact.lua":
return [self._build_text_segment(self._build_contact_text(meta, "推荐联系人"))]
if app_name == "com.tencent.troopsharecard":
return [self._build_text_segment(self._build_contact_text(meta, "推荐群聊"))]
if app_name == "com.tencent.tuwen.lua":
return await self._build_preview_text_segments(
self._build_news_text(meta, default_tag="图文分享"),
self._extract_preview_url(meta, "news"),
)
if app_name == "com.tencent.feed.lua":
return await self._build_preview_text_segments(
self._build_feed_text(meta),
self._extract_preview_url(meta, "feed", field_name="cover"),
)
if app_name == "com.tencent.template.qqfavorite.share":
return await self._build_preview_text_segments(
self._build_favorite_text(meta),
self._extract_preview_url(meta, "news"),
)
if app_name == "com.tencent.miniapp.lua":
return await self._build_preview_text_segments(
self._build_simple_title_text(meta, "miniapp", "QQ空间"),
self._extract_preview_url(meta, "miniapp"),
)
if app_name == "com.tencent.forum":
forum_segments = await self._build_forum_segments(meta)
if forum_segments:
return forum_segments
if app_name == "com.tencent.map":
location_text = self._build_location_text(meta)
if location_text:
return [self._build_text_segment(location_text)]
if app_name == "com.tencent.together":
together_text = self._build_together_text(meta)
if together_text:
return [self._build_text_segment(together_text)]
prompt = str(parsed_json.get("prompt") or "").strip()
if not prompt and isinstance(meta, Mapping):
prompt = str(meta.get("prompt") or "").strip()
text = prompt or app_name or "json"
return [self._build_text_segment(f"[json:{text}]")]
def _build_mannounce_segment(self, meta: Mapping[str, Any]) -> NapCatSegment:
"""构造群公告文本段。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
NapCatSegment: 群公告文本段。
"""
mannounce = meta.get("mannounce", {})
if not isinstance(mannounce, Mapping):
mannounce = {}
title = str(mannounce.get("title") or "").strip()
text = str(mannounce.get("text") or "").strip()
encode_flag = mannounce.get("encode")
if encode_flag == 1:
title = self._safe_base64_decode(title)
text = self._safe_base64_decode(text)
if title and text:
content = f"[{title}]{text}"
elif title:
content = f"[{title}]"
elif text:
content = text
else:
content = "[群公告]"
return self._build_text_segment(content)
def _build_music_card_segments(self, meta: Mapping[str, Any]) -> NapCatSegments:
"""构造音乐卡片文本段。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
NapCatSegments: 音乐卡片转换后的消息段列表。
"""
music = meta.get("music", {})
if not isinstance(music, Mapping):
return []
title = str(music.get("title") or "").strip()
singer = str(music.get("desc") or music.get("singer") or "").strip()
tag = str(music.get("tag") or "音乐分享").strip()
text_parts: List[str] = [f"[{tag}]"]
if title:
text_parts.append(title)
if singer:
text_parts.append(f"- {singer}")
content = " ".join(text_parts).strip() or "[音乐分享]"
return [self._build_text_segment(content)]
async def _build_preview_text_segments(
self,
text: str,
preview_url: str,
) -> NapCatSegments:
"""构造“文本 + 预览图”消息段列表。
Args:
text: 主文本内容。
preview_url: 预览图地址。
Returns:
NapCatSegments: 转换后的消息段列表。
"""
segments: NapCatSegments = [self._build_text_segment(text or "[卡片消息]")]
image_segment = await self._build_remote_image_segment(preview_url)
if image_segment is not None:
segments.append(image_segment)
return segments
async def _build_remote_image_segment(self, image_url: str) -> Optional[NapCatSegment]:
"""从远端图片地址构造图片消息段。
Args:
image_url: 图片地址。
Returns:
Optional[NapCatSegment]: 成功时返回图片消息段,否则返回 ``None``。
"""
normalized_url = str(image_url or "").strip()
if not normalized_url:
return None
binary_data = await self._query_service.download_binary(normalized_url)
if not binary_data:
return None
return {
"type": "image",
"data": "",
"hash": hashlib.sha256(binary_data).hexdigest(),
"binary_data_base64": self._encode_binary(binary_data),
}
def _build_miniapp_text(self, meta: Mapping[str, Any]) -> str:
"""构造小程序分享文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: 小程序分享文本。
"""
detail = meta.get("detail_1", {})
if not isinstance(detail, Mapping):
return "[小程序]"
title = str(detail.get("title") or "").strip()
desc = str(detail.get("desc") or "").strip()
if title and desc:
return f"[小程序] {title}{desc}"
if title:
return f"[小程序] {title}"
if desc:
return f"[小程序] {desc}"
return "[小程序]"
def _build_gift_text(self, meta: Mapping[str, Any]) -> str:
"""构造礼物卡片文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: 礼物卡片文本。
"""
giftark = meta.get("giftark", {})
if not isinstance(giftark, Mapping):
return "[赠送礼物]"
gift_name = str(giftark.get("title") or "礼物").strip()
desc = str(giftark.get("desc") or "").strip()
if desc:
return f"[赠送礼物: {gift_name}] {desc}"
return f"[赠送礼物: {gift_name}]"
def _build_contact_text(self, meta: Mapping[str, Any], default_tag: str) -> str:
"""构造推荐联系人或群聊文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
default_tag: 默认标签文本。
Returns:
str: 推荐对象文本。
"""
contact = meta.get("contact", {})
if not isinstance(contact, Mapping):
return f"[{default_tag}]"
name = str(contact.get("nickname") or "未知对象").strip()
tag = str(contact.get("tag") or default_tag).strip() or default_tag
return f"[{tag}] {name}"
def _build_news_text(self, meta: Mapping[str, Any], default_tag: str) -> str:
"""构造图文分享文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
default_tag: 默认标签文本。
Returns:
str: 图文分享文本。
"""
news = meta.get("news", {})
if not isinstance(news, Mapping):
return f"[{default_tag}]"
title = str(news.get("title") or "未知标题").strip()
desc = str(news.get("desc") or "").replace("[图片]", "").strip()
tag = str(news.get("tag") or default_tag).strip() or default_tag
if tag and title and tag in title:
title = self._trim_card_title(title.replace(tag, "", 1))
if desc:
return f"[{tag}] {title}{desc}"
return f"[{tag}] {title}".strip()
def _build_feed_text(self, meta: Mapping[str, Any]) -> str:
"""构造群相册分享文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: 群相册分享文本。
"""
feed = meta.get("feed", {})
if not isinstance(feed, Mapping):
return "[群相册]"
title = str(feed.get("title") or "群相册").strip()
tag = str(feed.get("tagName") or "群相册").strip() or "群相册"
desc = str(feed.get("forwardMessage") or "").strip()
if tag and title and tag in title:
title = self._trim_card_title(title.replace(tag, "", 1))
if desc:
return f"[{tag}] {title}{desc}"
return f"[{tag}] {title}".strip()
def _build_favorite_text(self, meta: Mapping[str, Any]) -> str:
"""构造 QQ 收藏分享文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: QQ 收藏分享文本。
"""
news = meta.get("news", {})
if not isinstance(news, Mapping):
return "[QQ收藏]"
desc = str(news.get("desc") or "").replace("[图片]", "").strip()
tag = str(news.get("tag") or "QQ收藏").strip() or "QQ收藏"
if desc:
return f"[{tag}] {desc}"
return f"[{tag}]"
def _build_simple_title_text(
self,
meta: Mapping[str, Any],
key: str,
default_tag: str,
) -> str:
"""构造简单标题类卡片文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
key: 子对象键名。
default_tag: 默认标签文本。
Returns:
str: 简单标题文本。
"""
nested_payload = meta.get(key, {})
if not isinstance(nested_payload, Mapping):
return f"[{default_tag}]"
title = str(nested_payload.get("title") or "未知标题").strip()
tag = str(nested_payload.get("tag") or default_tag).strip() or default_tag
return f"[{tag}] {title}".strip()
async def _build_forum_segments(self, meta: Mapping[str, Any]) -> NapCatSegments:
"""构造 QQ 频道帖子消息段。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
NapCatSegments: 频道帖子转换后的消息段列表。
"""
detail = meta.get("detail", {})
if not isinstance(detail, Mapping):
return []
feed = detail.get("feed", {})
poster = detail.get("poster", {})
channel_info = detail.get("channel_info", {})
if not isinstance(feed, Mapping) or not isinstance(poster, Mapping) or not isinstance(channel_info, Mapping):
return []
guild_name = str(channel_info.get("guild_name") or "").strip()
nick = str(poster.get("nick") or "QQ用户").strip() or "QQ用户"
title = self._extract_forum_title(feed)
face_content = self._extract_forum_face_text(feed)
text_prefix = "[频道帖子]"
if guild_name:
text_prefix = f"{text_prefix} [{guild_name}]"
text_content = f"{text_prefix}{nick}:{title}{face_content}"
segments: NapCatSegments = [self._build_text_segment(text_content)]
images = feed.get("images", [])
if not isinstance(images, list):
return segments
for image_item in images:
if not isinstance(image_item, Mapping):
continue
image_segment = await self._build_remote_image_segment(str(image_item.get("pic_url") or "").strip())
if image_segment is not None:
segments.append(image_segment)
return segments
def _extract_forum_title(self, feed: Mapping[str, Any]) -> str:
"""提取 QQ 频道帖子标题。
Args:
feed: 频道帖子 ``feed`` 数据。
Returns:
str: 帖子标题。
"""
title_payload = feed.get("title", {})
if not isinstance(title_payload, Mapping):
return "帖子"
contents = title_payload.get("contents", [])
if not isinstance(contents, list) or not contents:
return "帖子"
first_content = contents[0]
if not isinstance(first_content, Mapping):
return "帖子"
text_content = first_content.get("text_content", {})
if not isinstance(text_content, Mapping):
return "帖子"
return str(text_content.get("text") or "帖子").strip() or "帖子"
def _extract_forum_face_text(self, feed: Mapping[str, Any]) -> str:
"""提取 QQ 频道帖子中的表情文本。
Args:
feed: 频道帖子 ``feed`` 数据。
Returns:
str: 合并后的表情文本。
"""
contents_payload = feed.get("contents", {})
if not isinstance(contents_payload, Mapping):
return ""
contents = contents_payload.get("contents", [])
if not isinstance(contents, list):
return ""
face_text_parts: List[str] = []
for item in contents:
if not isinstance(item, Mapping):
continue
emoji_content = item.get("emoji_content", {})
if not isinstance(emoji_content, Mapping):
continue
emoji_id = str(emoji_content.get("id") or "").strip()
if emoji_id in QQ_FACE:
face_text_parts.append(QQ_FACE[emoji_id])
return "".join(face_text_parts)
def _build_location_text(self, meta: Mapping[str, Any]) -> str:
"""构造位置分享文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: 位置分享文本。
"""
location = meta.get("Location.Search", {})
if not isinstance(location, Mapping):
return "[位置]"
name = str(location.get("name") or "未知地点").strip()
address = str(location.get("address") or "").strip()
if address:
return f"[位置] {address} · {name}"
return f"[位置] {name}"
def _build_together_text(self, meta: Mapping[str, Any]) -> str:
"""构造“一起听歌”文本。
Args:
meta: JSON 卡片 ``meta`` 数据。
Returns:
str: 一起听歌文本。
"""
invite = meta.get("invite", {})
if not isinstance(invite, Mapping):
return "[一起听歌]"
title = str(invite.get("title") or "一起听歌").strip() or "一起听歌"
summary = str(invite.get("summary") or "").strip()
if summary:
return f"[{title}] {summary}"
return f"[{title}]"
def _extract_preview_url(
self,
meta: Mapping[str, Any],
key: str,
field_name: str = "preview",
) -> str:
"""从卡片元数据中提取预览图地址。
Args:
meta: JSON 卡片 ``meta`` 数据。
key: 子对象键名。
field_name: 预览图字段名。
Returns:
str: 预览图地址;不存在时返回空字符串。
"""
nested_payload = meta.get(key, {})
if not isinstance(nested_payload, Mapping):
return ""
return str(nested_payload.get(field_name) or "").strip()
@staticmethod
def _trim_card_title(title: str) -> str:
"""清理卡片标题两侧的常见分隔符。
Args:
title: 原始标题文本。
Returns:
str: 清理后的标题文本。
"""
return re.sub(r"^[:\s\-—]+|[:\s\-—]+$", "", str(title or "").strip())
@staticmethod
def _safe_base64_decode(encoded_text: str) -> str:
"""安全地解码 Base64 文本。
Args:
encoded_text: 待解码的 Base64 文本。
Returns:
str: 解码结果;失败时返回原始文本。
"""
normalized_text = str(encoded_text or "").strip()
if not normalized_text:
return ""
try:
import base64
return base64.b64decode(normalized_text).decode("utf-8", errors="ignore")
except Exception:
return normalized_text

View File

@@ -0,0 +1,661 @@
"""NapCat 入站消息编解码。"""
from __future__ import annotations
from typing import Any, Dict, List, Mapping, Optional, Tuple
from uuid import uuid4
import hashlib
import time
from ...qq_emoji_list import QQ_FACE
from ...services import NapCatQueryService
from ...types import NapCatIncomingSegment, NapCatIncomingSegments, NapCatPayload, NapCatSegment, NapCatSegments
from ..notice.helpers import normalize_optional_string
from .cards import NapCatInboundCardMixin
from .text import NapCatInboundTextMixin
class NapCatInboundCodec(NapCatInboundCardMixin, NapCatInboundTextMixin):
"""NapCat 入站消息编码器。"""
def __init__(self, logger: Any, query_service: NapCatQueryService) -> None:
"""初始化入站消息编码器。
Args:
logger: 插件日志对象。
query_service: QQ 查询服务。
"""
self._logger = logger
self._query_service = query_service
async def build_message_dict(
self,
payload: NapCatPayload,
self_id: str,
sender_user_id: str,
sender: Mapping[str, Any],
) -> Dict[str, Any]:
"""构造 Host 侧可接受的 ``MessageDict``。
Args:
payload: NapCat 原始消息事件。
self_id: 当前机器人账号 ID。
sender_user_id: 发送者用户 ID。
sender: 发送者信息字典。
Returns:
Dict[str, Any]: 规范化后的 ``MessageDict``。
"""
message_type = str(payload.get("message_type") or "").strip() or "private"
group_id = str(payload.get("group_id") or "").strip()
group_name = str(payload.get("group_name") or "").strip() or (f"group_{group_id}" if group_id else "")
user_nickname = str(sender.get("nickname") or sender.get("card") or sender_user_id).strip() or sender_user_id
user_cardname = str(sender.get("card") or "").strip() or None
raw_message, is_at = await self.convert_segments(payload, self_id)
if not raw_message:
raw_message = [self._build_text_segment("[unsupported]")]
plain_text = self.build_plain_text(raw_message)
timestamp_seconds = payload.get("time")
if not isinstance(timestamp_seconds, (int, float)):
timestamp_seconds = time.time()
additional_config: Dict[str, Any] = {"self_id": self_id, "napcat_message_type": message_type}
if group_id:
additional_config["platform_io_target_group_id"] = group_id
else:
additional_config["platform_io_target_user_id"] = sender_user_id
message_info: Dict[str, Any] = {
"user_info": {
"user_id": sender_user_id,
"user_nickname": user_nickname,
"user_cardname": user_cardname,
},
"additional_config": additional_config,
}
if group_id:
message_info["group_info"] = {"group_id": group_id, "group_name": group_name}
message_id = str(payload.get("message_id") or f"napcat-{uuid4().hex}").strip()
return {
"message_id": message_id,
"timestamp": str(float(timestamp_seconds)),
"platform": "qq",
"message_info": message_info,
"raw_message": raw_message,
"is_mentioned": is_at,
"is_at": is_at,
"is_emoji": False,
"is_picture": False,
"is_command": plain_text.startswith("/"),
"is_notify": False,
"session_id": "",
"processed_plain_text": plain_text,
"display_message": plain_text,
}
async def convert_segments(self, payload: NapCatPayload, self_id: str) -> Tuple[NapCatSegments, bool]:
"""将 OneBot 消息段转换为 Host 消息段结构。
Args:
payload: OneBot 原始消息事件。
self_id: 当前机器人账号 ID。
Returns:
Tuple[NapCatSegments, bool]: 转换后的消息段列表,以及是否 @ 到当前机器人。
Raises:
ValueError: 当载荷缺少结构化 ``message`` 段列表时抛出。
"""
message_payload = self._require_message_segments(payload)
group_id = str(payload.get("group_id") or "").strip()
return await self._convert_incoming_segments(message_payload, self_id, group_id)
def _require_message_segments(self, payload: NapCatPayload) -> NapCatIncomingSegments:
"""从 NapCat 载荷中提取结构化消息段列表。
Args:
payload: NapCat / OneBot 原始载荷。
Returns:
NapCatIncomingSegments: 规范化后的结构化消息段列表。
Raises:
ValueError: 当 ``message`` 字段不是结构化段列表时抛出。
"""
message_payload = payload.get("message")
if not isinstance(message_payload, list):
raise ValueError("NapCat 入站消息缺少结构化 message 段列表")
normalized_segments = self._normalize_incoming_segments(message_payload)
if not normalized_segments:
raise ValueError("NapCat 入站消息未包含可识别的结构化消息段")
return normalized_segments
def _normalize_incoming_segments(self, message_payload: List[Any]) -> NapCatIncomingSegments:
"""规范化 NapCat / OneBot 原始消息段列表。
Args:
message_payload: 原始 ``message`` 字段值。
Returns:
NapCatIncomingSegments: 过滤并标准化后的消息段列表。
"""
normalized_segments: NapCatIncomingSegments = []
for segment in message_payload:
if not isinstance(segment, Mapping):
continue
segment_type = str(segment.get("type") or "").strip()
segment_data = segment.get("data", {})
if not segment_type or not isinstance(segment_data, Mapping):
continue
normalized_segments.append(
NapCatIncomingSegment(
type=segment_type,
data=dict(segment_data),
)
)
return normalized_segments
async def _convert_incoming_segments(
self,
message_payload: NapCatIncomingSegments,
self_id: str,
group_id: str,
) -> Tuple[NapCatSegments, bool]:
"""将结构化 OneBot 消息段转换为 Host 消息段结构。
Args:
message_payload: NapCat / OneBot 结构化消息段列表。
self_id: 当前机器人账号 ID。
group_id: 当前消息所在群号;私聊消息为空字符串。
Returns:
Tuple[NapCatSegments, bool]: 转换后的消息段列表,以及是否 @ 到当前机器人。
"""
converted_segments: NapCatSegments = []
at_target_cache: Dict[str, Tuple[Optional[str], Optional[str]]] = {}
is_at = False
for segment in message_payload:
segment_type = str(segment.get("type") or "").strip()
segment_data = segment.get("data", {})
if not isinstance(segment_data, Mapping):
segment_data = {}
if segment_type == "text":
if text_value := str(segment_data.get("text") or ""):
converted_segments.append(self._build_text_segment(text_value))
continue
if segment_type == "at":
if target_user_id := str(segment_data.get("qq") or "").strip():
if target_user_id in at_target_cache:
target_user_nickname, target_user_cardname = at_target_cache[target_user_id]
else:
target_user_nickname, target_user_cardname = await self._resolve_at_target_info(
group_id=group_id,
target_user_id=target_user_id,
)
at_target_cache[target_user_id] = (target_user_nickname, target_user_cardname)
converted_segments.append(
{
"type": "at",
"data": {
"target_user_id": target_user_id,
"target_user_nickname": target_user_nickname,
"target_user_cardname": target_user_cardname,
},
}
)
if self_id and target_user_id == self_id:
is_at = True
continue
if segment_type == "reply":
if reply_segment := await self._build_reply_segment(segment_data):
converted_segments.append(reply_segment)
continue
if segment_type == "face":
converted_segments.append(self._build_face_text_segment(segment_data))
continue
if segment_type == "image":
converted_segments.append(await self._build_image_like_segment(segment_data, is_emoji=False))
continue
if segment_type == "record":
converted_segments.append(await self._build_record_segment(segment_data))
continue
if segment_type == "video":
converted_segments.append(self._build_video_text_segment(segment_data))
continue
if segment_type == "file":
converted_segments.append(self._build_file_text_segment(segment_data))
continue
if segment_type == "json":
converted_segments.extend(await self._build_json_segments(segment_data))
continue
if segment_type == "forward":
if forward_segment := await self._build_forward_segment(segment_data):
converted_segments.append(forward_segment)
continue
if segment_type in {"xml", "share"}:
converted_segments.append(self._build_text_segment(f"[{segment_type}]"))
return converted_segments, is_at
async def _resolve_at_target_info(
self,
group_id: str,
target_user_id: str,
) -> Tuple[Optional[str], Optional[str]]:
"""解析 ``at`` 目标的展示信息。
Args:
group_id: 当前消息所在群号;私聊消息为空字符串。
target_user_id: 被 ``at`` 的用户号。
Returns:
Tuple[Optional[str], Optional[str]]: 依次返回 QQ 昵称和群昵称。
"""
if not target_user_id or target_user_id == "all":
return None, None
target_user_nickname: Optional[str] = None
target_user_cardname: Optional[str] = None
if group_id:
member_info = await self._query_service.get_group_member_info(group_id, target_user_id, no_cache=True)
if member_info is not None:
target_user_nickname = normalize_optional_string(member_info.get("nickname"))
target_user_cardname = normalize_optional_string(member_info.get("card"))
if target_user_nickname or target_user_cardname:
return target_user_nickname, target_user_cardname
stranger_info = await self._query_service.get_stranger_info(target_user_id)
if stranger_info is None:
return None, None
return normalize_optional_string(stranger_info.get("nickname")), target_user_cardname
@staticmethod
def _build_text_segment(text: str) -> NapCatSegment:
"""构造一条纯文本 Host 消息段。
Args:
text: 文本内容。
Returns:
NapCatSegment: Host 侧纯文本消息段。
"""
return {"type": "text", "data": text}
async def _build_reply_segment(self, segment_data: Mapping[str, Any]) -> Optional[NapCatSegment]:
"""构造回复消息段。
Args:
segment_data: OneBot ``reply`` 段的 ``data`` 字典。
Returns:
Optional[NapCatSegment]: 转换后的回复消息段;缺少消息 ID 时返回 ``None``。
"""
target_message_id = str(segment_data.get("id") or "").strip()
if not target_message_id:
return None
message_detail = await self._query_service.get_message_detail(target_message_id)
reply_payload: Dict[str, Any] = {"target_message_id": target_message_id}
if message_detail is not None:
sender = message_detail.get("sender", {})
if not isinstance(sender, Mapping):
sender = {}
reply_payload["target_message_content"] = await self._build_reply_preview_text(message_detail)
reply_payload["target_message_sender_id"] = (
str(message_detail.get("user_id") or sender.get("user_id") or "").strip() or None
)
reply_payload["target_message_sender_nickname"] = str(sender.get("nickname") or "").strip() or None
reply_payload["target_message_sender_cardname"] = str(sender.get("card") or "").strip() or None
return {"type": "reply", "data": reply_payload}
async def _build_reply_preview_text(self, message_detail: NapCatPayload) -> Optional[str]:
"""为回复引用构造结构化消息预览文本。
Args:
message_detail: ``get_msg`` 返回的消息详情。
Returns:
Optional[str]: 基于结构化消息段生成的预览文本;无法生成时返回 ``None``。
"""
try:
reply_segments, _ = await self.convert_segments(message_detail, "")
except ValueError:
return None
if not reply_segments:
return None
return self.build_plain_text(reply_segments)
async def _build_image_like_segment(
self,
segment_data: Mapping[str, Any],
is_emoji: bool,
) -> NapCatSegment:
"""构造图片或表情消息段。
Args:
segment_data: OneBot ``image`` 段的 ``data`` 字典。
is_emoji: 是否按表情组件处理。
Returns:
NapCatSegment: 转换后的图片或表情消息段。
"""
subtype = self._normalize_numeric_segment_value(segment_data.get("sub_type"))
actual_is_emoji = is_emoji or (subtype is not None and subtype not in {0, 4, 9})
image_url = str(segment_data.get("url") or "").strip()
binary_data = await self._query_service.download_binary(image_url)
if not binary_data:
return self._build_text_segment("[emoji]" if actual_is_emoji else "[image]")
return {
"type": "emoji" if actual_is_emoji else "image",
"data": "",
"hash": hashlib.sha256(binary_data).hexdigest(),
"binary_data_base64": self._encode_binary(binary_data),
}
async def _build_record_segment(self, segment_data: Mapping[str, Any]) -> NapCatSegment:
"""构造语音消息段。
Args:
segment_data: OneBot ``record`` 段的 ``data`` 字典。
Returns:
NapCatSegment: 转换后的语音或占位文本消息段。
"""
file_name = str(segment_data.get("file") or "").strip()
file_id = str(segment_data.get("file_id") or "").strip() or None
if not file_name:
return self._build_text_segment("[voice]")
record_detail = await self._query_service.get_record_detail(file_name=file_name, file_id=file_id)
if record_detail is None:
return self._build_text_segment("[voice]")
record_base64 = str(record_detail.get("base64") or "").strip()
if not record_base64:
return self._build_text_segment("[voice]")
try:
binary_data = self._decode_binary(record_base64)
except Exception:
return self._build_text_segment("[voice]")
return {
"type": "voice",
"data": "",
"hash": hashlib.sha256(binary_data).hexdigest(),
"binary_data_base64": self._encode_binary(binary_data),
}
def _build_face_text_segment(self, segment_data: Mapping[str, Any]) -> NapCatSegment:
"""构造 QQ 原生表情文本段。
Args:
segment_data: OneBot ``face`` 段的 ``data`` 字典。
Returns:
NapCatSegment: 转换后的文本消息段。
"""
face_id = str(segment_data.get("id") or "").strip()
face_text = QQ_FACE.get(face_id, "[表情]")
return self._build_text_segment(face_text)
def _build_video_text_segment(self, segment_data: Mapping[str, Any]) -> NapCatSegment:
"""构造视频消息的可读文本段。
Args:
segment_data: OneBot ``video`` 段的 ``data`` 字典。
Returns:
NapCatSegment: 转换后的文本消息段。
"""
file_name = str(segment_data.get("file") or "").strip()
file_size = str(segment_data.get("file_size") or "").strip()
parts: List[str] = []
if file_name:
parts.append(f"文件: {file_name}")
if file_size:
parts.append(f"大小: {file_size}")
if parts:
return self._build_text_segment(f"[视频] {''.join(parts)}")
return self._build_text_segment("[视频]")
def _build_file_text_segment(self, segment_data: Mapping[str, Any]) -> NapCatSegment:
"""构造文件消息的可读文本段。
Args:
segment_data: OneBot ``file`` 段的 ``data`` 字典。
Returns:
NapCatSegment: 转换后的文本消息段。
"""
file_name = str(segment_data.get("file") or segment_data.get("name") or "").strip()
file_size = str(segment_data.get("file_size") or "").strip()
file_url = str(segment_data.get("url") or "").strip()
text_parts: List[str] = []
if file_name:
text_parts.append(file_name)
if file_size:
text_parts.append(f"大小: {file_size}")
file_text = "[文件]"
if text_parts:
file_text = f"[文件] {''.join(text_parts)}"
if file_url:
file_text = f"{file_text},链接: {file_url}"
return self._build_text_segment(file_text)
async def _build_forward_segment(self, segment_data: Mapping[str, Any]) -> Optional[NapCatSegment]:
"""构造合并转发消息段。
Args:
segment_data: OneBot ``forward`` 段的 ``data`` 字典。
Returns:
Optional[NapCatSegment]: 转换后的合并转发消息段;失败时返回 ``None``。
"""
inline_messages = self._extract_forward_messages(segment_data)
messages = inline_messages
if messages is None:
message_id = str(segment_data.get("id") or "").strip()
if not message_id:
return None
forward_detail = await self._query_service.get_forward_message(message_id)
if forward_detail is None:
return self._build_text_segment("[forward]")
messages = self._extract_forward_messages(forward_detail)
if not isinstance(messages, list):
return self._build_text_segment("[forward]")
forward_nodes = await self._build_forward_nodes(messages)
if not forward_nodes:
return self._build_text_segment("[forward]")
return {"type": "forward", "data": forward_nodes}
def _extract_forward_messages(self, payload: Mapping[str, Any]) -> Optional[List[Any]]:
"""从转发载荷中提取节点列表。
Args:
payload: 转发段 ``data`` 或 ``get_forward_msg`` 返回的载荷。
Returns:
Optional[List[Any]]: 提取到的节点列表;当载荷中不存在节点列表时返回 ``None``。
"""
direct_messages = payload.get("messages")
if isinstance(direct_messages, list):
return direct_messages
direct_content = payload.get("content")
if isinstance(direct_content, list):
return direct_content
nested_data = payload.get("data")
if isinstance(nested_data, Mapping):
nested_messages = nested_data.get("messages")
if isinstance(nested_messages, list):
return nested_messages
nested_content = nested_data.get("content")
if isinstance(nested_content, list):
return nested_content
return None
async def _build_forward_nodes(self, messages: List[Any]) -> List[Dict[str, Any]]:
"""将 NapCat 转发节点列表转换为 Host 转发节点列表。
Args:
messages: NapCat 返回的转发节点列表。
Returns:
List[Dict[str, Any]]: Host 侧可识别的转发节点列表。
"""
forward_nodes: List[Dict[str, Any]] = []
for forward_message in messages:
if not isinstance(forward_message, Mapping):
continue
raw_content = self._extract_forward_node_content(forward_message)
content_segments = await self._convert_forward_content(raw_content, "")
sender = self._extract_forward_node_sender(forward_message)
node_data = forward_message.get("data", {})
if not isinstance(node_data, Mapping):
node_data = {}
forward_nodes.append(
{
"user_id": str(
sender.get("user_id")
or sender.get("uin")
or node_data.get("user_id")
or node_data.get("uin")
or ""
).strip()
or None,
"user_nickname": str(
sender.get("nickname")
or sender.get("name")
or node_data.get("nickname")
or node_data.get("name")
or "未知用户"
),
"user_cardname": str(sender.get("card") or node_data.get("card") or "").strip() or None,
"message_id": str(
forward_message.get("message_id")
or forward_message.get("id")
or node_data.get("id")
or uuid4().hex
),
"content": content_segments or [self._build_text_segment("[empty]")],
}
)
return forward_nodes
def _extract_forward_node_content(self, forward_message: Mapping[str, Any]) -> Any:
"""提取单个转发节点中的消息段列表。
Args:
forward_message: NapCat 返回的单个转发节点。
Returns:
Any: 原始消息段列表;不存在时返回空列表。
"""
direct_content = forward_message.get("content")
if isinstance(direct_content, list):
return direct_content
direct_message = forward_message.get("message")
if isinstance(direct_message, list):
return direct_message
node_data = forward_message.get("data", {})
if not isinstance(node_data, Mapping):
return []
nested_content = node_data.get("content")
if isinstance(nested_content, list):
return nested_content
nested_message = node_data.get("message")
if isinstance(nested_message, list):
return nested_message
return []
def _extract_forward_node_sender(self, forward_message: Mapping[str, Any]) -> Mapping[str, Any]:
"""提取单个转发节点的发送者信息。
Args:
forward_message: NapCat 返回的单个转发节点。
Returns:
Mapping[str, Any]: 归一化后的发送者信息映射。
"""
sender = forward_message.get("sender", {})
if isinstance(sender, Mapping):
return sender
node_data = forward_message.get("data", {})
if not isinstance(node_data, Mapping):
return {}
normalized_sender: Dict[str, Any] = {}
user_id = str(node_data.get("user_id") or node_data.get("uin") or "").strip()
nickname = str(node_data.get("nickname") or node_data.get("name") or "").strip()
cardname = str(node_data.get("card") or "").strip()
if user_id:
normalized_sender["user_id"] = user_id
normalized_sender["uin"] = user_id
if nickname:
normalized_sender["nickname"] = nickname
normalized_sender["name"] = nickname
if cardname:
normalized_sender["card"] = cardname
return normalized_sender
async def _convert_forward_content(self, raw_content: Any, self_id: str) -> NapCatSegments:
"""转换转发节点内部的消息段列表。
Args:
raw_content: 转发节点原始内容。
self_id: 当前机器人账号 ID。
Returns:
NapCatSegments: 转换后的消息段列表。
"""
if not isinstance(raw_content, list):
return []
normalized_segments = self._normalize_incoming_segments(raw_content)
if not normalized_segments:
return []
segments, _ = await self._convert_incoming_segments(normalized_segments, self_id, "")
return segments

View File

@@ -0,0 +1,90 @@
"""NapCat 入站纯文本与二进制辅助。"""
from __future__ import annotations
from typing import Any, Mapping
import base64
from ...types import NapCatSegments
class NapCatInboundTextMixin:
"""封装入站纯文本与二进制辅助逻辑。"""
def build_plain_text(self, raw_message: NapCatSegments) -> str:
"""从标准消息段中提取可展示的纯文本。
Args:
raw_message: 标准化后的消息段列表。
Returns:
str: 用于 Host 展示和命令判断的纯文本内容。
"""
plain_text_parts: list[str] = []
for item in raw_message:
if not isinstance(item, Mapping):
continue
item_type = str(item.get("type") or "").strip()
item_data = item.get("data")
if item_type == "text":
plain_text_parts.append(str(item_data or ""))
elif item_type == "at" and isinstance(item_data, Mapping):
at_target_name = str(
item_data.get("target_user_cardname")
or item_data.get("target_user_nickname")
or item_data.get("target_user_id")
or ""
).strip()
if at_target_name:
plain_text_parts.append(f"@{at_target_name}")
elif item_type == "reply":
plain_text_parts.append("[reply]")
elif item_type == "forward":
plain_text_parts.append("[forward]")
elif item_type in {"image", "emoji", "voice"}:
plain_text_parts.append(f"[{item_type}]")
plain_text = "".join(part for part in plain_text_parts if part).strip()
return plain_text or "[unsupported]"
@staticmethod
def _encode_binary(binary_data: bytes) -> str:
"""将二进制内容编码为 Base64 字符串。
Args:
binary_data: 待编码的二进制内容。
Returns:
str: Base64 编码字符串。
"""
return base64.b64encode(binary_data).decode("utf-8")
@staticmethod
def _decode_binary(binary_base64: str) -> bytes:
"""将 Base64 字符串解码为二进制内容。
Args:
binary_base64: Base64 字符串。
Returns:
bytes: 解码后的二进制内容。
"""
return base64.b64decode(binary_base64)
@staticmethod
def _normalize_numeric_segment_value(value: Any) -> Any:
"""将可安全识别的数字字符串转为整数。
Args:
value: 原始字段值。
Returns:
Any: 规范化后的字段值。
"""
if isinstance(value, str):
stripped_value = value.strip()
if stripped_value.isdigit():
return int(stripped_value)
return stripped_value
return value

View File

@@ -0,0 +1,5 @@
"""NapCat 通知编解码导出。"""
from .message_codec import NapCatNoticeCodec
__all__ = ["NapCatNoticeCodec"]

View File

@@ -0,0 +1,72 @@
"""NapCat 通知事件资料补全器。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from ...services import NapCatQueryService
from .helpers import normalize_optional_string
class NapCatNoticeEntityResolver:
"""为通知事件补全用户和群资料。"""
def __init__(self, query_service: NapCatQueryService) -> None:
"""初始化实体补全器。
Args:
query_service: NapCat 查询服务。
"""
self._query_service = query_service
async def build_user_info(self, group_id: str, user_id: str) -> Dict[str, Optional[str]]:
"""构造通知消息的用户信息。
Args:
group_id: 群号;私聊或系统通知时为空字符串。
user_id: 事件关联用户号。
Returns:
Dict[str, Optional[str]]: 规范化后的用户信息字典。
"""
if not user_id:
return {
"user_id": "notice",
"user_nickname": "系统通知",
"user_cardname": None,
}
member_info: Optional[Dict[str, Any]]
if group_id:
member_info = await self._query_service.get_group_member_info(group_id, user_id)
else:
member_info = await self._query_service.get_stranger_info(user_id)
if member_info is None:
return {
"user_id": user_id,
"user_nickname": user_id,
"user_cardname": None,
}
return {
"user_id": user_id,
"user_nickname": str(member_info.get("nickname") or user_id),
"user_cardname": normalize_optional_string(member_info.get("card")),
}
async def build_group_info(self, group_id: str) -> Optional[Dict[str, str]]:
"""构造通知消息的群信息。
Args:
group_id: 群号。
Returns:
Optional[Dict[str, str]]: 群信息字典;若不是群通知则返回 ``None``。
"""
if not group_id:
return None
group_info = await self._query_service.get_group_info(group_id)
group_name = str(group_info.get("group_name") or f"group_{group_id}") if group_info else f"group_{group_id}"
return {"group_id": group_id, "group_name": group_name}

View File

@@ -0,0 +1,83 @@
"""NapCat 通知编解码公共辅助函数。"""
from __future__ import annotations
from hashlib import sha1
from typing import Any, Mapping, Optional
import json
def build_payload_digest(payload: Mapping[str, Any]) -> str:
"""对通知载荷生成稳定哈希。
Args:
payload: 原始通知载荷。
Returns:
str: 基于规范化 JSON 文本生成的 SHA-1 十六进制摘要。
"""
normalized_payload = normalize_payload_value(payload)
serialized_payload = json.dumps(
normalized_payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
)
return sha1(serialized_payload.encode("utf-8")).hexdigest()
def normalize_optional_string(value: Any) -> Optional[str]:
"""将任意值规范化为可选字符串。
Args:
value: 待规范化的值。
Returns:
Optional[str]: 规范化后的字符串;若值为空则返回 ``None``。
"""
if value is None:
return None
normalized_value = str(value).strip()
return normalized_value if normalized_value else None
def normalize_payload_value(value: Any) -> Any:
"""将通知载荷递归规范化为稳定 JSON 结构。
Args:
value: 待规范化的任意值。
Returns:
Any: 仅包含 JSON 基础类型的稳定结构。
"""
if isinstance(value, Mapping):
return {
str(key): normalize_payload_value(child_value)
for key, child_value in sorted(value.items(), key=lambda item: str(item[0]))
}
if isinstance(value, (list, tuple)):
return [normalize_payload_value(item) for item in value]
if isinstance(value, set):
normalized_items = [normalize_payload_value(item) for item in value]
return sorted(normalized_items, key=lambda item: json.dumps(item, ensure_ascii=False, sort_keys=True))
if value is None or isinstance(value, (bool, int, float, str)):
return value
return str(value)
def resolve_actor_user_id(payload: Mapping[str, Any]) -> str:
"""解析通知事件中的操作者用户号。
Args:
payload: 原始通知事件。
Returns:
str: 规范化后的操作者用户号;无法确定时返回空字符串。
"""
if bool(payload.get("is_natural_lift", False)):
return ""
actor_user_id = str(payload.get("operator_id") or payload.get("user_id") or "").strip()
if actor_user_id == "0":
return ""
return actor_user_id

View File

@@ -0,0 +1,120 @@
"""NapCat 通知事件编解码器。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from uuid import uuid4
import time
from ...services import NapCatQueryService
from ...types import NapCatPayload, NapCatPayloadDict
from .enricher import NapCatNoticeEntityResolver
from .helpers import build_payload_digest, resolve_actor_user_id
from .meta_event_logger import NapCatMetaEventObserver
from .renderer import NapCatNoticeTextRenderer
class NapCatNoticeCodec:
"""NapCat QQ 通知事件编码器。"""
def __init__(self, logger: Any, query_service: NapCatQueryService) -> None:
"""初始化通知事件编码器。
Args:
logger: 插件日志对象。
query_service: QQ 查询服务。
"""
self._entity_resolver = NapCatNoticeEntityResolver(query_service)
self._meta_event_observer = NapCatMetaEventObserver(logger)
self._renderer = NapCatNoticeTextRenderer()
async def build_notice_message_dict(self, payload: NapCatPayload) -> Optional[NapCatPayloadDict]:
"""将 NapCat ``notice`` 事件转换为 Host 可接受的消息字典。
Args:
payload: NapCat 推送的原始通知事件。
Returns:
Optional[NapCatPayloadDict]: 成功时返回标准 ``MessageDict``;无法识别时返回 ``None``。
"""
notice_type = str(payload.get("notice_type") or "").strip()
if not notice_type:
return None
group_id = str(payload.get("group_id") or "").strip()
user_id = resolve_actor_user_id(payload)
self_id = str(payload.get("self_id") or "").strip()
user_info = await self._entity_resolver.build_user_info(group_id=group_id, user_id=user_id)
group_info = await self._entity_resolver.build_group_info(group_id)
actor_name = user_info.get("user_nickname") or user_id or "系统"
notice_text = self._renderer.build_notice_text(payload, actor_name)
if not notice_text:
return None
additional_config: Dict[str, Any] = {
"self_id": self_id,
"napcat_notice_type": notice_type,
"napcat_notice_sub_type": str(payload.get("sub_type") or "").strip(),
"napcat_notice_payload": dict(payload),
}
if group_id:
additional_config["platform_io_target_group_id"] = group_id
elif user_id:
additional_config["platform_io_target_user_id"] = user_id
message_info: Dict[str, Any] = {"user_info": user_info, "additional_config": additional_config}
if group_info is not None:
message_info["group_info"] = group_info
timestamp_seconds = payload.get("time")
if not isinstance(timestamp_seconds, (int, float)):
timestamp_seconds = time.time()
return {
"message_id": f"napcat-notice-{uuid4().hex}",
"timestamp": str(float(timestamp_seconds)),
"platform": "qq",
"message_info": message_info,
"raw_message": [{"type": "text", "data": notice_text}],
"is_mentioned": False,
"is_at": False,
"is_emoji": False,
"is_picture": False,
"is_command": False,
"is_notify": True,
"session_id": "",
"processed_plain_text": notice_text,
"display_message": notice_text,
}
def build_notice_dedupe_key(self, payload: NapCatPayload) -> Optional[str]:
"""为 NapCat ``notice`` 事件构造稳定的技术性去重键。
Args:
payload: NapCat 推送的原始通知事件。
Returns:
Optional[str]: 若可以构造稳定去重键则返回该键,否则返回 ``None``。
"""
external_message_id = str(payload.get("message_id") or "").strip()
if external_message_id:
return external_message_id
notice_type = str(payload.get("notice_type") or "").strip()
if not notice_type:
return None
sub_type = str(payload.get("sub_type") or "").strip()
payload_digest = build_payload_digest(payload)
suffix = f":{sub_type}" if sub_type else ""
return f"notice:{notice_type}{suffix}:{payload_digest}"
async def handle_meta_event(self, payload: NapCatPayload) -> None:
"""处理 ``meta_event`` 事件的日志与状态观测。
Args:
payload: NapCat 推送的原始元事件。
"""
await self._meta_event_observer.handle_meta_event(payload)

View File

@@ -0,0 +1,49 @@
"""NapCat 元事件日志处理器。"""
from __future__ import annotations
from typing import Any, Mapping
class NapCatMetaEventObserver:
"""处理 NapCat 元事件的日志输出。"""
def __init__(self, logger: Any) -> None:
"""初始化元事件观察器。
Args:
logger: 插件日志对象。
"""
self._logger = logger
async def handle_meta_event(self, payload: Mapping[str, Any]) -> None:
"""处理 ``meta_event`` 事件的日志与状态观测。
Args:
payload: NapCat 推送的原始元事件。
"""
meta_event_type = str(payload.get("meta_event_type") or "").strip()
self_id = str(payload.get("self_id") or "").strip() or "unknown"
if meta_event_type == "lifecycle":
sub_type = str(payload.get("sub_type") or "").strip()
if sub_type == "connect":
self._logger.info(f"NapCat 元事件Bot {self_id} 已建立连接")
else:
self._logger.debug(f"NapCat 生命周期事件: self_id={self_id} sub_type={sub_type}")
return
if meta_event_type == "heartbeat":
status = payload.get("status", {})
if not isinstance(status, Mapping):
status = {}
is_online = bool(status.get("online", False))
is_good = bool(status.get("good", False))
interval_ms = payload.get("interval")
self._logger.debug(
f"NapCat 心跳事件: self_id={self_id} online={is_online} good={is_good} interval={interval_ms}"
)
if not is_online:
self._logger.warning(f"NapCat 心跳显示 Bot {self_id} 已离线")
elif not is_good:
self._logger.warning(f"NapCat 心跳显示 Bot {self_id} 状态异常")

View File

@@ -0,0 +1,63 @@
"""NapCat 通知文本渲染器。"""
from __future__ import annotations
from typing import Any, Mapping
class NapCatNoticeTextRenderer:
"""根据通知载荷生成可读文本。"""
def build_notice_text(self, payload: Mapping[str, Any], actor_name: str) -> str:
"""根据 NapCat 通知事件生成可读文本。
Args:
payload: 原始通知事件。
actor_name: 事件操作者显示名。
Returns:
str: 生成的可读通知文本。
"""
notice_type = str(payload.get("notice_type") or "").strip()
sub_type = str(payload.get("sub_type") or "").strip()
target_id = str(payload.get("target_id") or "").strip()
target_user_id = str(payload.get("user_id") or "").strip()
is_natural_lift = bool(payload.get("is_natural_lift", False))
if notice_type in {"group_recall", "friend_recall"}:
return f"{actor_name} 撤回了一条消息"
if notice_type == "notify" and sub_type == "poke":
target_text = f" -> {target_id}" if target_id else ""
return f"{actor_name} 发起了戳一戳{target_text}"
if notice_type == "notify" and sub_type == "group_name":
return f"{actor_name} 修改了群名称"
if notice_type == "group_ban" and sub_type == "ban":
duration = payload.get("duration")
if target_user_id in {"", "0"}:
return f"{actor_name} 开启了全体禁言"
return f"{actor_name} 禁言了用户 {target_user_id},时长 {duration}"
if notice_type == "group_ban" and sub_type == "whole_lift_ban":
if is_natural_lift:
return "群全体禁言已自然解除"
return f"{actor_name} 解除了全体禁言"
if notice_type == "group_ban" and sub_type == "lift_ban":
if is_natural_lift:
return f"用户 {target_user_id} 的禁言已自然解除"
return f"{actor_name} 解除了用户 {target_user_id} 的禁言"
if notice_type == "group_upload":
file_info = payload.get("file", {})
file_name = ""
if isinstance(file_info, Mapping):
file_name = str(file_info.get("name") or "").strip()
return f"{actor_name} 上传了文件{f'{file_name}' if file_name else ''}"
if notice_type == "group_increase":
return f"{actor_name} 加入了群聊"
if notice_type == "group_decrease":
return f"{actor_name} 离开了群聊"
if notice_type == "group_admin":
return f"{actor_name} 的群管理员状态发生变化"
if notice_type == "essence":
return f"{actor_name} 触发了精华消息事件"
if notice_type == "group_msg_emoji_like":
return f"{actor_name} 给一条消息添加了表情回应"
return f"[notice] {notice_type}.{sub_type}".strip(".")

View File

@@ -0,0 +1,5 @@
"""NapCat 出站编解码导出。"""
from .message_codec import NapCatOutboundCodec
__all__ = ["NapCatOutboundCodec"]

View File

@@ -0,0 +1,63 @@
"""NapCat 出站消息编解码。"""
from __future__ import annotations
from typing import Any, Dict, Mapping, Tuple
from .segment_encoder import NapCatOutboundSegmentEncoder
class NapCatOutboundCodec:
"""NapCat 出站消息编码器。"""
def __init__(self) -> None:
"""初始化出站消息编码器。"""
self._segment_encoder = NapCatOutboundSegmentEncoder()
def build_outbound_action(
self,
message: Mapping[str, Any],
route: Mapping[str, Any],
) -> Tuple[str, Dict[str, Any]]:
"""为 Host 出站消息构造 OneBot 动作。
Args:
message: Host 侧标准 ``MessageDict``。
route: Platform IO 路由信息。
Returns:
Tuple[str, Dict[str, Any]]: 动作名称与参数字典。
Raises:
ValueError: 当私聊出站缺少目标用户 ID 时抛出。
"""
message_info = message.get("message_info", {})
if not isinstance(message_info, Mapping):
message_info = {}
group_info = message_info.get("group_info", {})
if not isinstance(group_info, Mapping):
group_info = {}
additional_config = message_info.get("additional_config", {})
if not isinstance(additional_config, Mapping):
additional_config = {}
raw_message = message.get("raw_message", [])
segments = self._segment_encoder.convert_segments(raw_message)
if target_group_id := str(
group_info.get("group_id") or additional_config.get("platform_io_target_group_id") or ""
).strip():
return "send_group_msg", {"group_id": target_group_id, "message": segments}
target_user_id = str(
additional_config.get("platform_io_target_user_id")
or additional_config.get("target_user_id")
or route.get("target_user_id")
or ""
).strip()
if not target_user_id:
raise ValueError("Outbound private message is missing target_user_id")
return "send_private_msg", {"message": segments, "user_id": target_user_id}

View File

@@ -0,0 +1,500 @@
"""NapCat 出站消息段编码器。"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Mapping
class NapCatOutboundSegmentEncoder:
"""将 Host 消息段转换为 NapCat 消息段。"""
def __init__(self) -> None:
"""初始化出站消息段编码器。"""
self._segment_builders: Dict[str, Callable[[Mapping[str, Any]], List[Dict[str, Any]]]] = {
"at": self._build_at_segments,
"dict": self._build_dict_segments,
"emoji": self._build_emoji_segments,
"face": self._build_face_segments,
"file": self._build_file_segments,
"forward": self._build_forward_segments,
"image": self._build_image_segments,
"imageurl": self._build_imageurl_segments,
"music": self._build_music_segments,
"reply": self._build_reply_segments,
"text": self._build_text_segments,
"video": self._build_video_segments,
"videourl": self._build_videourl_segments,
"voice": self._build_voice_segments,
"voiceurl": self._build_voiceurl_segments,
}
def convert_segments(self, raw_message: Any) -> List[Dict[str, Any]]:
"""将 Host 消息段转换为 OneBot 消息段。
Args:
raw_message: Host 侧 ``raw_message`` 字段。
Returns:
List[Dict[str, Any]]: OneBot 消息段列表。
"""
if not isinstance(raw_message, list):
return [{"type": "text", "data": {"text": ""}}]
outbound_segments: List[Dict[str, Any]] = []
for item in raw_message:
if not isinstance(item, Mapping):
continue
item_type = str(item.get("type") or "").strip()
segment_builder = self._segment_builders.get(item_type)
if segment_builder is None:
fallback_text = f"[unsupported:{item_type or 'unknown'}]"
outbound_segments.append({"type": "text", "data": {"text": fallback_text}})
continue
built_segments = segment_builder(item)
if built_segments:
outbound_segments.extend(built_segments)
continue
fallback_text = self._build_empty_segment_fallback(item_type)
outbound_segments.append({"type": "text", "data": {"text": fallback_text}})
if not outbound_segments:
outbound_segments.append({"type": "text", "data": {"text": ""}})
return outbound_segments
@staticmethod
def _build_empty_segment_fallback(item_type: str) -> str:
"""为缺少有效数据的消息段生成占位文本。
Args:
item_type: 原始消息段类型。
Returns:
str: 用于降级展示的占位文本。
"""
normalized_type = item_type or "unknown"
fallback_map = {
"emoji": "[emoji]",
"face": "[face]",
"file": "[file]",
"image": "[image]",
"imageurl": "[image]",
"music": "[music]",
"video": "[video]",
"videourl": "[video]",
"voice": "[voice]",
"voiceurl": "[voice]",
}
return fallback_map.get(normalized_type, f"[unsupported:{normalized_type}]")
def _build_text_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造文本消息段。
Args:
item: Host 侧文本消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 文本消息段列表。
"""
text_value = str(item.get("data") or "")
return [{"type": "text", "data": {"text": text_value}}]
def _build_at_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造 @ 消息段。
Args:
item: Host 侧 @ 消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat @ 消息段列表。
"""
item_data = item.get("data")
if not isinstance(item_data, Mapping):
return []
target_user_id = str(item_data.get("target_user_id") or "").strip()
if not target_user_id:
return []
return [{"type": "at", "data": {"qq": target_user_id}}]
def _build_reply_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造回复消息段。
Args:
item: Host 侧回复消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 回复消息段列表。
"""
item_data = item.get("data")
if isinstance(item_data, Mapping):
target_message_id = str(item_data.get("target_message_id") or "").strip()
else:
target_message_id = str(item_data or "").strip()
if not target_message_id:
return []
return [{"type": "reply", "data": {"id": target_message_id}}]
def _build_image_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造图片消息段。
Args:
item: Host 侧图片消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 图片消息段列表。
"""
binary_base64 = str(item.get("binary_data_base64") or "").strip()
if not binary_base64:
return []
return [{"type": "image", "data": {"file": f"base64://{binary_base64}", "sub_type": 0}}]
def _build_emoji_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造动画表情消息段。
Args:
item: Host 侧表情消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 表情消息段列表。
"""
binary_base64 = str(item.get("binary_data_base64") or "").strip()
if not binary_base64:
return []
return [
{
"type": "image",
"data": {
"file": f"base64://{binary_base64}",
"sub_type": 1,
"summary": "[动画表情]",
},
}
]
def _build_voice_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造语音消息段。
Args:
item: Host 侧语音消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 语音消息段列表。
"""
return [self._build_voice_segment(item)]
def _build_voiceurl_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造基于 URL 的语音消息段。
Args:
item: Host 侧语音 URL 消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 语音消息段列表。
"""
voice_url_segment = self._build_url_media_segment("record", item.get("data"))
return [voice_url_segment] if voice_url_segment else []
def _build_face_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造 QQ 原生表情消息段。
Args:
item: Host 侧表情消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 表情消息段列表。
"""
face_segment = self._build_face_segment(item.get("data"))
return [face_segment] if face_segment else []
def _build_imageurl_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造基于 URL 的图片消息段。
Args:
item: Host 侧图片 URL 消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 图片消息段列表。
"""
image_segment = self._build_url_media_segment("image", item.get("data"))
return [image_segment] if image_segment else []
def _build_videourl_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造基于 URL 的视频消息段。
Args:
item: Host 侧视频 URL 消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 视频消息段列表。
"""
video_segment = self._build_url_media_segment("video", item.get("data"))
return [video_segment] if video_segment else []
def _build_video_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造视频消息段。
Args:
item: Host 侧视频消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 视频消息段列表。
"""
video_segment = self._build_video_segment(item)
return [video_segment] if video_segment else []
def _build_file_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造文件消息段。
Args:
item: Host 侧文件消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 文件消息段列表。
"""
file_segment = self._build_file_segment(item.get("data"))
return [file_segment] if file_segment else []
def _build_music_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造音乐卡片消息段。
Args:
item: Host 侧音乐消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 音乐消息段列表。
"""
music_segment = self._build_music_segment(item.get("data"))
return [music_segment] if music_segment else []
def _build_forward_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造合并转发消息段。
Args:
item: Host 侧转发消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 转发节点列表。
"""
item_data = item.get("data")
if not isinstance(item_data, list):
return []
return self._build_forward_nodes(item_data)
def _build_dict_segments(self, item: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""构造 ``DictComponent`` 消息段。
Args:
item: Host 侧 ``DictComponent`` 消息段。
Returns:
List[Dict[str, Any]]: 构造后的 NapCat 消息段列表。
"""
item_data = item.get("data")
if not isinstance(item_data, Mapping):
return []
dict_segment = self._build_dict_component_segment(item_data)
return [dict_segment] if dict_segment else []
def _build_voice_segment(self, item: Mapping[str, Any]) -> Dict[str, Any]:
"""构造语音消息段。
Args:
item: Host 侧语音消息段。
Returns:
Dict[str, Any]: NapCat ``record`` 消息段;缺少有效数据时返回占位文本段。
"""
binary_base64 = str(item.get("binary_data_base64") or "").strip()
if binary_base64:
return {"type": "record", "data": {"file": f"base64://{binary_base64}"}}
item_data = item.get("data")
if url_media_segment := self._build_url_media_segment("record", item_data):
return url_media_segment
return {"type": "text", "data": {"text": "[voice]"}}
def _build_face_segment(self, item_data: Any) -> Dict[str, Any]:
"""构造 QQ 原生表情消息段。
Args:
item_data: Host 侧表情段数据。
Returns:
Dict[str, Any]: NapCat ``face`` 段;缺少有效表情 ID 时返回空字典。
"""
face_id = ""
if isinstance(item_data, Mapping):
face_id = str(item_data.get("id") or "").strip()
else:
face_id = str(item_data or "").strip()
if not face_id:
return {}
return {"type": "face", "data": {"id": face_id}}
def _build_video_segment(self, item: Mapping[str, Any]) -> Dict[str, Any]:
"""构造视频消息段。
Args:
item: Host 侧视频消息段。
Returns:
Dict[str, Any]: NapCat ``video`` 消息段;缺少有效数据时返回空字典。
"""
binary_base64 = str(item.get("binary_data_base64") or "").strip()
if binary_base64:
return {"type": "video", "data": {"file": f"base64://{binary_base64}"}}
return self._build_url_media_segment("video", item.get("data"))
def _build_file_segment(self, item_data: Any) -> Dict[str, Any]:
"""构造文件消息段。
Args:
item_data: Host 侧文件段数据。
Returns:
Dict[str, Any]: NapCat ``file`` 段;缺少有效数据时返回空字典。
"""
if isinstance(item_data, str):
normalized_file = item_data.strip()
if not normalized_file:
return {}
return {"type": "file", "data": {"file": self._normalize_file_reference(normalized_file)}}
if not isinstance(item_data, Mapping):
return {}
raw_file = str(item_data.get("file") or "").strip()
raw_path = str(item_data.get("path") or "").strip()
raw_url = str(item_data.get("url") or "").strip()
file_ref = raw_file or raw_path or raw_url
if not file_ref:
return {}
data: Dict[str, Any] = {"file": self._normalize_file_reference(file_ref)}
for optional_field in ("name", "thumb"):
optional_value = str(item_data.get(optional_field) or "").strip()
if optional_value:
data[optional_field] = optional_value
return {"type": "file", "data": data}
def _build_music_segment(self, item_data: Any) -> Dict[str, Any]:
"""构造音乐卡片消息段。
Args:
item_data: Host 侧音乐段数据。
Returns:
Dict[str, Any]: NapCat ``music`` 段;缺少有效数据时返回空字典。
"""
if isinstance(item_data, str):
normalized_song_id = item_data.strip()
if not normalized_song_id:
return {}
return {"type": "music", "data": {"type": "163", "id": normalized_song_id}}
if not isinstance(item_data, Mapping):
return {}
platform = str(item_data.get("type") or "163").strip() or "163"
if platform not in {"163", "qq"}:
platform = "163"
song_id = str(item_data.get("id") or "").strip()
if not song_id:
return {}
return {"type": "music", "data": {"type": platform, "id": song_id}}
def _build_url_media_segment(self, segment_type: str, item_data: Any) -> Dict[str, Any]:
"""构造基于 URL 或文件引用的媒体消息段。
Args:
segment_type: 目标消息段类型。
item_data: Host 侧消息段数据。
Returns:
Dict[str, Any]: NapCat 媒体消息段;缺少有效引用时返回空字典。
"""
if isinstance(item_data, Mapping):
file_reference = str(item_data.get("file") or item_data.get("url") or "").strip()
else:
file_reference = str(item_data or "").strip()
if not file_reference:
return {}
return {"type": segment_type, "data": {"file": self._normalize_file_reference(file_reference)}}
@staticmethod
def _normalize_file_reference(file_reference: str) -> str:
"""规范化文件引用字符串。
Args:
file_reference: 原始文件引用。
Returns:
str: 可供 NapCat 使用的文件引用。
"""
if file_reference.startswith(("base64://", "file://", "http://", "https://")):
return file_reference
return f"file://{file_reference}"
def _build_forward_nodes(self, forward_nodes: List[Any]) -> List[Dict[str, Any]]:
"""构造 NapCat 转发节点列表。
Args:
forward_nodes: 内部转发节点列表。
Returns:
List[Dict[str, Any]]: NapCat 转发节点列表。
"""
built_nodes: List[Dict[str, Any]] = []
for node in forward_nodes:
if not isinstance(node, Mapping):
continue
raw_content = node.get("content", [])
node_segments = self.convert_segments(raw_content)
built_nodes.append(
{
"type": "node",
"data": {
"name": str(node.get("user_nickname") or node.get("user_cardname") or "QQ用户"),
"uin": str(node.get("user_id") or ""),
"content": node_segments,
},
}
)
return built_nodes
def _build_dict_component_segment(self, item_data: Mapping[str, Any]) -> Dict[str, Any]:
"""尽力将 ``DictComponent`` 转换为 NapCat 消息段。
Args:
item_data: ``DictComponent`` 原始数据。
Returns:
Dict[str, Any]: NapCat 消息段;不支持时返回占位文本段。
"""
raw_type = str(item_data.get("type") or "").strip()
raw_payload = item_data.get("data", item_data)
if raw_type == "file":
return self._build_file_segment(raw_payload)
if raw_type == "music":
return self._build_music_segment(raw_payload)
if raw_type == "video":
if isinstance(raw_payload, Mapping):
pseudo_item: Dict[str, Any] = {
"binary_data_base64": raw_payload.get("binary_data_base64"),
"data": raw_payload,
}
return self._build_video_segment(pseudo_item)
return self._build_video_segment({"data": raw_payload})
if raw_type == "face":
return self._build_face_segment(raw_payload)
if raw_type == "voiceurl":
return self._build_url_media_segment("record", raw_payload)
if raw_type == "imageurl":
return self._build_url_media_segment("image", raw_payload)
if raw_type == "videourl":
return self._build_url_media_segment("video", raw_payload)
if raw_type in {"image", "record", "reply", "at"} and isinstance(raw_payload, Mapping):
return {"type": raw_type, "data": dict(raw_payload)}
return {"type": "text", "data": {"text": f"[unsupported:{raw_type or 'dict'}]"}}

View File

@@ -0,0 +1,631 @@
"""NapCat 内置适配器配置模型。"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple
from urllib.parse import urlparse
import logging
from maibot_sdk import Field, PluginConfigBase
from pydantic import ValidationInfo, field_validator, model_validator
from .constants import (
DEFAULT_ACTION_TIMEOUT_SEC,
DEFAULT_CHAT_LIST_TYPE,
DEFAULT_HEARTBEAT_INTERVAL_SEC,
DEFAULT_NAPCAT_HOST,
DEFAULT_NAPCAT_PORT,
DEFAULT_RECONNECT_DELAY_SEC,
SUPPORTED_CONFIG_VERSION,
)
LOGGER = logging.getLogger("napcat_adapter.config")
class NapCatPluginOptions(PluginConfigBase):
"""插件级配置。"""
__ui_label__: ClassVar[str] = "插件设置"
__ui_order__: ClassVar[int] = 0
enabled: bool = Field(
default=False,
description="是否启用 NapCat 适配器。",
json_schema_extra={
"hint": "关闭后插件会保持空闲,不会主动建立 NapCat WebSocket 连接。",
"label": "启用适配器",
"order": 0,
},
)
config_version: str = Field(
default=SUPPORTED_CONFIG_VERSION,
description="当前配置结构版本。",
json_schema_extra={
"disabled": True,
"hidden": True,
"label": "配置版本",
"order": 99,
},
)
def should_connect(self) -> bool:
"""判断当前配置下是否应当启动连接。
Returns:
bool: 若插件连接已启用,则返回 ``True``。
"""
return self.enabled
@field_validator("config_version", mode="before")
@classmethod
def _normalize_config_version(cls, value: Any) -> str:
"""规范化配置版本字段。
Args:
value: 原始配置值。
Returns:
str: 去除首尾空白后的配置版本;若为空则回退到当前支持版本。
"""
normalized_value = _normalize_string(value)
return normalized_value or SUPPORTED_CONFIG_VERSION
class NapCatServerConfig(PluginConfigBase):
"""NapCat 正向 WebSocket 连接配置。"""
__ui_label__: ClassVar[str] = "NapCat 连接"
__ui_order__: ClassVar[int] = 1
host: str = Field(
default=DEFAULT_NAPCAT_HOST,
description="NapCat WebSocket 服务主机地址。",
json_schema_extra={
"hint": "通常为运行 NapCat 的宿主机地址,默认使用本机回环地址。",
"label": "主机地址",
"order": 0,
"placeholder": "127.0.0.1",
},
)
port: int = Field(
default=DEFAULT_NAPCAT_PORT,
description="NapCat WebSocket 服务端口。",
json_schema_extra={
"hint": "与 NapCat 正向 WebSocket 服务监听端口保持一致。",
"label": "端口",
"order": 1,
},
)
token: str = Field(
default="",
description="NapCat 访问令牌,未启用鉴权时可留空。",
json_schema_extra={
"hint": "若 NapCat 开启了访问令牌校验,请在这里填写相同的 token。",
"input_type": "password",
"label": "访问令牌",
"order": 2,
"placeholder": "可留空",
},
)
heartbeat_interval: float = Field(
default=DEFAULT_HEARTBEAT_INTERVAL_SEC,
description="心跳超时判定间隔,单位为秒。",
json_schema_extra={
"hint": "用于判断 NapCat 连接是否失活,必须大于 0。",
"label": "心跳间隔(秒)",
"order": 3,
"step": 1,
},
)
reconnect_delay_sec: float = Field(
default=DEFAULT_RECONNECT_DELAY_SEC,
description="连接断开后的重连等待时间,单位为秒。",
json_schema_extra={
"hint": "连接断开后会等待该时长再尝试重新连接。",
"label": "重连等待(秒)",
"order": 4,
"step": 1,
},
)
action_timeout_sec: float = Field(
default=DEFAULT_ACTION_TIMEOUT_SEC,
description="调用 NapCat 动作接口的超时时间,单位为秒。",
json_schema_extra={
"hint": "发送消息、查询信息等动作会在超时后报错。",
"label": "动作超时(秒)",
"order": 5,
"step": 1,
},
)
connection_id: str = Field(
default="",
description="可选连接标识,用于区分多条 NapCat 链路。",
json_schema_extra={
"hint": "当存在多条 NapCat 连接时,可用它作为路由作用域标识。",
"label": "连接标识",
"order": 6,
"placeholder": "例如primary",
},
)
def build_ws_url(self) -> str:
"""构造正向 WebSocket 地址。
Returns:
str: 供适配器作为客户端连接的 NapCat WebSocket 地址。
"""
return f"ws://{self.host}:{self.port}"
@field_validator("host", mode="before")
@classmethod
def _normalize_host(cls, value: Any) -> str:
"""规范化主机地址字段。
Args:
value: 原始配置值。
Returns:
str: 去除首尾空白后的主机地址;若为空则回退到默认主机。
"""
normalized_value = _normalize_string(value)
return normalized_value or DEFAULT_NAPCAT_HOST
@field_validator("port", mode="before")
@classmethod
def _normalize_port(cls, value: Any) -> int:
"""规范化端口字段。
Args:
value: 原始配置值。
Returns:
int: 合法的正整数端口;非法时回退到默认端口。
"""
return _normalize_positive_int(value, DEFAULT_NAPCAT_PORT)
@field_validator("token", "connection_id", mode="before")
@classmethod
def _normalize_text_fields(cls, value: Any) -> str:
"""规范化文本字段。
Args:
value: 原始配置值。
Returns:
str: 去除首尾空白后的字符串值。
"""
return _normalize_string(value)
@field_validator(
"heartbeat_interval",
"reconnect_delay_sec",
"action_timeout_sec",
mode="before",
)
@classmethod
def _normalize_positive_float_fields(cls, value: Any, info: ValidationInfo) -> float:
"""规范化正浮点数字段。
Args:
value: 原始配置值。
info: Pydantic 字段校验上下文。
Returns:
float: 合法的正浮点数;非法时回退到对应默认值。
"""
default_values: Dict[str, float] = {
"action_timeout_sec": DEFAULT_ACTION_TIMEOUT_SEC,
"heartbeat_interval": DEFAULT_HEARTBEAT_INTERVAL_SEC,
"reconnect_delay_sec": DEFAULT_RECONNECT_DELAY_SEC,
}
return _normalize_positive_float(value, default_values[str(info.field_name)])
class NapCatChatConfig(PluginConfigBase):
"""聊天名单配置。"""
__ui_label__: ClassVar[str] = "聊天过滤"
__ui_order__: ClassVar[int] = 2
enable_chat_list_filter: bool = Field(
default=True,
description="是否启用群聊与私聊名单过滤。",
json_schema_extra={
"hint": "关闭后将忽略群聊名单和私聊名单,仅保留全局屏蔽用户与官方机器人屏蔽规则。",
"label": "启用聊天名单过滤",
"order": 0,
},
)
show_dropped_chat_list_messages: bool = Field(
default=False,
description="是否显示未通过聊天名单过滤而被丢弃的消息日志。",
json_schema_extra={
"hint": "关闭后不会记录群聊/私聊因未通过聊天名单过滤而被丢弃的日志,默认关闭以减少刷屏。",
"label": "显示聊天名单丢弃日志",
"order": 1,
},
)
group_list_type: Literal["whitelist", "blacklist"] = Field(
default=DEFAULT_CHAT_LIST_TYPE,
description="群聊名单模式。",
json_schema_extra={
"hint": "白名单模式只接收列表内群聊,黑名单模式则忽略列表内群聊。",
"label": "群聊名单模式",
"order": 2,
},
)
group_list: List[str] = Field(
default_factory=list,
description="群聊名单中的群号列表。",
json_schema_extra={
"hint": "群号会被统一转换为字符串并自动去重。",
"label": "群聊名单",
"order": 3,
"placeholder": "请输入群号",
},
)
private_list_type: Literal["whitelist", "blacklist"] = Field(
default=DEFAULT_CHAT_LIST_TYPE,
description="私聊名单模式。",
json_schema_extra={
"hint": "白名单模式只接收列表内私聊,黑名单模式则忽略列表内私聊。",
"label": "私聊名单模式",
"order": 4,
},
)
private_list: List[str] = Field(
default_factory=list,
description="私聊名单中的用户 ID 列表。",
json_schema_extra={
"hint": "用户 ID 会被统一转换为字符串并自动去重。",
"label": "私聊名单",
"order": 5,
"placeholder": "请输入用户 ID",
},
)
ban_user_id: List[str] = Field(
default_factory=list,
description="全局屏蔽的用户 ID 列表。",
json_schema_extra={
"hint": "这些用户的消息会在进入 Host 之前被直接丢弃。",
"label": "全局屏蔽用户",
"order": 6,
"placeholder": "请输入用户 ID",
},
)
ban_qq_bot: bool = Field(
default=False,
description="是否屏蔽 QQ 官方机器人消息。",
json_schema_extra={
"hint": "开启后会忽略来自 QQ 官方机器人或频道机器人的消息。",
"label": "屏蔽官方机器人",
"order": 7,
},
)
@field_validator("group_list_type", "private_list_type", mode="before")
@classmethod
def _normalize_list_types(cls, value: Any) -> Literal["whitelist", "blacklist"]:
"""规范化名单模式字段。
Args:
value: 原始配置值。
Returns:
Literal["whitelist", "blacklist"]: 合法的名单模式;非法时回退到默认值。
"""
return _normalize_list_mode(value)
@field_validator("group_list", "private_list", "ban_user_id", mode="before")
@classmethod
def _normalize_id_lists(cls, value: Any) -> List[str]:
"""规范化 ID 列表字段。
Args:
value: 原始配置值。
Returns:
List[str]: 规范化后的字符串列表,已去除空白与重复项。
"""
return _normalize_string_list(value)
class NapCatFilterConfig(PluginConfigBase):
"""消息过滤配置。"""
__ui_label__: ClassVar[str] = "消息过滤"
__ui_order__: ClassVar[int] = 3
ignore_self_message: bool = Field(
default=True,
description="是否忽略机器人自身发送的消息。",
json_schema_extra={
"hint": "建议保持开启,避免机器人处理自己刚刚发出的消息。",
"label": "忽略自身消息",
"order": 0,
},
)
class NapCatPluginSettings(PluginConfigBase):
"""NapCat 插件完整配置。"""
plugin: NapCatPluginOptions = Field(default_factory=NapCatPluginOptions)
napcat_server: NapCatServerConfig = Field(default_factory=NapCatServerConfig)
chat: NapCatChatConfig = Field(default_factory=NapCatChatConfig)
filters: NapCatFilterConfig = Field(default_factory=NapCatFilterConfig)
@model_validator(mode="before")
@classmethod
def _upgrade_legacy_config(cls, raw_config: Any) -> Dict[str, Any]:
"""将旧版配置结构迁移为当前配置模型。
Args:
raw_config: Runner 注入的原始配置内容。
Returns:
Dict[str, Any]: 适配到当前配置模型后的字典结构。
"""
raw_mapping = _as_mapping(raw_config)
plugin_section = _as_mapping(raw_mapping.get("plugin"))
server_section = _as_mapping(raw_mapping.get("napcat_server"))
legacy_connection_section = _as_mapping(raw_mapping.get("connection"))
chat_section = _as_mapping(raw_mapping.get("chat"))
filters_section = _as_mapping(raw_mapping.get("filters"))
if legacy_connection_section:
LOGGER.warning("NapCat 适配器检测到旧版 [connection] 配置段,已自动迁移到 [napcat_server]")
if not server_section and legacy_connection_section:
server_section = dict(legacy_connection_section)
normalized_server_section = dict(server_section)
legacy_host, legacy_port = _read_legacy_host_port(normalized_server_section, legacy_connection_section)
current_host = _normalize_string(normalized_server_section.get("host"))
if legacy_host and current_host in {"", DEFAULT_NAPCAT_HOST}:
normalized_server_section["host"] = legacy_host
current_port = _normalize_positive_int(normalized_server_section.get("port"), DEFAULT_NAPCAT_PORT)
if legacy_port is not None and current_port == DEFAULT_NAPCAT_PORT:
normalized_server_section["port"] = legacy_port
legacy_access_token = _normalize_string(normalized_server_section.get("access_token")) or _normalize_string(
legacy_connection_section.get("access_token")
)
if legacy_access_token and not _normalize_string(normalized_server_section.get("token")):
LOGGER.warning("NapCat 适配器检测到旧版 access_token 配置,已自动迁移到 napcat_server.token")
normalized_server_section["token"] = legacy_access_token
legacy_heartbeat_value = normalized_server_section.get("heartbeat_sec", legacy_connection_section.get("heartbeat_sec"))
current_heartbeat = _normalize_positive_float(
normalized_server_section.get("heartbeat_interval"),
DEFAULT_HEARTBEAT_INTERVAL_SEC,
)
legacy_heartbeat = _normalize_positive_float(legacy_heartbeat_value, DEFAULT_HEARTBEAT_INTERVAL_SEC)
if legacy_heartbeat_value is not None and current_heartbeat == DEFAULT_HEARTBEAT_INTERVAL_SEC:
LOGGER.warning(
"NapCat 适配器检测到旧版 heartbeat_sec 配置,已自动迁移到 napcat_server.heartbeat_interval"
)
normalized_server_section["heartbeat_interval"] = legacy_heartbeat
return {
"chat": chat_section,
"filters": filters_section,
"napcat_server": normalized_server_section,
"plugin": plugin_section,
}
@classmethod
def from_mapping(cls, raw_config: Mapping[str, Any], logger: Any) -> "NapCatPluginSettings":
"""从 Runner 注入的原始配置字典解析插件配置。
Args:
raw_config: Runner 注入的原始配置内容。
logger: 兼容旧调用签名保留的日志对象,当前不直接使用。
Returns:
NapCatPluginSettings: 规范化后的插件配置模型。
"""
del logger
return cls.model_validate(dict(raw_config))
def should_connect(self) -> bool:
"""判断当前配置下是否应当启动连接。
Returns:
bool: 若插件连接已启用,则返回 ``True``。
"""
return self.plugin.should_connect()
def validate_runtime_config(self, logger: Any) -> bool:
"""校验当前配置是否满足启动连接的前提条件。
Args:
logger: 插件日志对象。
Returns:
bool: 若配置满足启动连接的前提条件,则返回 ``True``。
"""
config_version = self.plugin.config_version
if not config_version:
logger.error(f"NapCat 适配器配置缺少 plugin.config_version当前插件要求版本 {SUPPORTED_CONFIG_VERSION}")
return False
if config_version != SUPPORTED_CONFIG_VERSION:
logger.error(
f"NapCat 适配器配置版本不兼容: 当前为 {config_version},当前插件要求 {SUPPORTED_CONFIG_VERSION}"
)
return False
if not self.napcat_server.host:
logger.warning("NapCat 适配器已启用,但 napcat_server.host 为空")
return False
if self.napcat_server.port <= 0:
logger.warning("NapCat 适配器已启用,但 napcat_server.port 不是正整数")
return False
return True
def _as_mapping(value: Any) -> Dict[str, Any]:
"""将任意值安全转换为字典。
Args:
value: 待转换的值。
Returns:
Dict[str, Any]: 若原值是映射,则返回普通字典;否则返回空字典。
"""
return dict(value) if isinstance(value, Mapping) else {}
def _normalize_list_mode(value: Any) -> Literal["whitelist", "blacklist"]:
"""规范化名单模式字符串。
Args:
value: 原始配置值。
Returns:
Literal["whitelist", "blacklist"]: 合法的名单模式;非法时回退到默认值。
"""
normalized_value = _normalize_string(value)
if normalized_value == "whitelist":
return "whitelist"
if normalized_value == "blacklist":
return "blacklist"
return DEFAULT_CHAT_LIST_TYPE
def _normalize_positive_float(value: Any, default: float) -> float:
"""规范化正浮点数配置值。
Args:
value: 原始配置值。
default: 非法取值时使用的默认值。
Returns:
float: 合法的正浮点数;非法时回退到默认值。
"""
if isinstance(value, (int, float)) and float(value) > 0:
return float(value)
if isinstance(value, str):
try:
parsed_value = float(value.strip())
except ValueError:
return default
if parsed_value > 0:
return parsed_value
return default
def _normalize_positive_int(value: Any, default: int) -> int:
"""规范化正整数配置值。
Args:
value: 原始配置值。
default: 非法取值时使用的默认值。
Returns:
int: 合法的正整数;非法时回退到默认值。
"""
if isinstance(value, int) and value > 0:
return value
if isinstance(value, str):
normalized_value = value.strip()
if normalized_value.isdigit():
parsed_value = int(normalized_value)
if parsed_value > 0:
return parsed_value
return default
def _normalize_string(value: Any) -> str:
"""规范化字符串配置值。
Args:
value: 原始配置值。
Returns:
str: 去除首尾空白后的字符串;若值为空则返回空字符串。
"""
return "" if value is None else str(value).strip()
def _normalize_string_list(value: Any) -> List[str]:
"""规范化字符串列表配置值。
Args:
value: 原始配置值。
Returns:
List[str]: 去除空白与重复项后的字符串列表。
"""
if not isinstance(value, list):
return []
normalized_values: List[str] = []
seen_values = set()
for item in value:
item_text = _normalize_string(item)
if not item_text or item_text in seen_values:
continue
seen_values.add(item_text)
normalized_values.append(item_text)
return normalized_values
def _read_legacy_host_port(
server_section: Mapping[str, Any],
legacy_connection_section: Mapping[str, Any],
) -> Tuple[str, Optional[int]]:
"""从旧版 ``ws_url`` 配置中提取主机与端口。
Args:
server_section: 新版 ``napcat_server`` 配置段。
legacy_connection_section: 旧版 ``connection`` 配置段。
Returns:
Tuple[str, Optional[int]]: 解析到的主机与端口;若未找到,则返回空主机与 ``None``。
"""
legacy_ws_url = _normalize_string(server_section.get("ws_url")) or _normalize_string(
legacy_connection_section.get("ws_url")
)
if not legacy_ws_url:
return "", None
parsed_url = urlparse(legacy_ws_url)
parsed_host = parsed_url.hostname or ""
parsed_port = parsed_url.port
LOGGER.warning("NapCat 适配器检测到旧版 ws_url 配置,已自动迁移到 napcat_server.host/port")
if parsed_url.path not in {"", "/"}:
LOGGER.warning("NapCat 适配器旧版 ws_url 包含路径,新的 napcat_server 配置不会保留该路径")
return parsed_host, parsed_port

View File

@@ -0,0 +1,13 @@
"""NapCat 内置适配器共享常量。"""
NAPCAT_GATEWAY_NAME = "napcat_gateway"
SUPPORTED_CONFIG_VERSION = "0.1.0"
DEFAULT_NAPCAT_HOST = "127.0.0.1"
DEFAULT_NAPCAT_PORT = 3001
DEFAULT_RECONNECT_DELAY_SEC = 5.0
DEFAULT_HEARTBEAT_INTERVAL_SEC = 30.0
DEFAULT_ACTION_TIMEOUT_SEC = 15.0
DEFAULT_CHAT_LIST_TYPE = "whitelist"
DEFAULT_HISTORY_RECOVERY_BATCH_SIZE = 20
DEFAULT_HISTORY_RECOVERY_CHECKPOINT_LIMIT = 50
DEFAULT_HISTORY_RECOVERY_SEEN_TTL_SEC = 86400.0 * 7

View File

@@ -0,0 +1,90 @@
# MaiBot NapCat Adapter API 文档
当前统计:
- 公开 API 总数:`164`
- 强类型封装 API`24`
- 透传 NapCat action API`140`
- 对照到 NapCat 官方文档的底层 action`162 / 162`
## 文档索引
- [强类型封装 API](./typed-api.md)
- [System 透传 API](./system-api.md)
- [Account 透传 API](./account-api.md)
- [Group 透传 API](./group-api.md)
- [Message 透传 API](./message-api.md)
- [File 透传 API](./file-api.md)
- [核验与兼容性说明](./verification.md)
## 先看调用方式
### 强类型封装 API
这类 API 直接展开参数,不要再套一层 `params`
```python
response = await self.ctx.api.call(
"adapter.napcat.group.get_group_member_info",
group_id=123456789,
user_id=987654321,
no_cache=True,
)
```
### 透传 NapCat action API
这类 API 统一只收 `params` 对象。
```python
response = await self.ctx.api.call(
"adapter.napcat.group.set_group_admin",
params={
"group_id": 123456789,
"user_id": 987654321,
"enable": True,
},
)
```
### 宿主统一返回结构
`self.ctx.api.call(...)` 返回的是宿主包装结构:
```python
{
"success": True,
"result": ...,
}
```
失败时通常为:
```python
{
"success": False,
"error": "...",
}
```
## 这次文档采用的对齐口径
- 透传 API 的“官方请求字段”优先看 NapCat 官方页面的“请求参数”结构。
- 如果官方页面左侧 Schema 没展开字段,改用同页 `curl --data-raw` 示例补齐。
- 如果官方页面 Schema 和 `curl` 示例同时给出、但字段不一致,文档会把冲突显式写出来,不会替官方文档做静默裁剪。
- 强类型封装 API 额外写清“适配器直接参数”和“实际下发给 NapCat 的 body”。
详细例外见 [核验与兼容性说明](./verification.md)。
- NapCat 官方文档地址:[https://napcat.apifox.cn/](https://napcat.apifox.cn/)
## 命名空间数量
| 命名空间 | 数量 | 说明 |
| --- | ---: | --- |
| `adapter.napcat.action` | 2 | 适配器提供的通用动作入口。 |
| `adapter.napcat.system` | 23 | 登录、状态、凭证、系统控制。 |
| `adapter.napcat.account` | 27 | 资料、好友、收藏、OCR、账号能力。 |
| `adapter.napcat.group` | 41 | 群、频道、公告、群管理。 |
| `adapter.napcat.message` | 28 | 消息、互动、转发、AI 语音。 |
| `adapter.napcat.file` | 43 | 文件、群文件、在线文件、相册、流式传输。 |

View File

@@ -0,0 +1,59 @@
# Account 透传 API
这一页覆盖 `adapter.napcat.account.*` 下除强类型封装 API 外的透传 API。
统一调用方式:
```python
response = await self.ctx.api.call(
"adapter.napcat.account.send_like",
params={
"user_id": 123456789,
"times": 10,
},
)
```
字段来源说明:
- `无参`:官方页面当前无请求字段。
- `Schema`:直接来自官方“请求参数”结构。
- `示例`:官方页面 Schema 没展开出具体字段,参数来自同页 `curl --data-raw` 示例。
## API 列表
| API | 底层 action | 官方请求字段 | 来源 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.account.create_collection` | `create_collection` | `rawData``brief` | `Schema` | [官方](https://napcat.apifox.cn/226659178e0) | 创建收藏。 |
| `adapter.napcat.account.delete_friend` | `delete_friend` | `friend_id``user_id``temp_block``temp_both_del` | `Schema` | [官方](https://napcat.apifox.cn/227237873e0) | 删除好友。 |
| `adapter.napcat.account.fetch_custom_face` | `fetch_custom_face` | `count` | `Schema` | [官方](https://napcat.apifox.cn/226659210e0) | 获取自定义表情。 |
| `adapter.napcat.account.get_ai_characters` | `get_ai_characters` | `group_id``chat_type` | `Schema` | [官方](https://napcat.apifox.cn/229485683e0) | 获取 AI 角色列表。 |
| `adapter.napcat.account.get_clientkey` | `get_clientkey` | 无 | `无参` | [官方](https://napcat.apifox.cn/250286915e0) | 获取 ClientKey。 |
| `adapter.napcat.account.get_collection_list` | `get_collection_list` | `category``count` | `Schema` | [官方](https://napcat.apifox.cn/226659182e0) | 获取收藏列表。 |
| `adapter.napcat.account.get_cookies` | `get_cookies` | `domain` | `Schema` | [官方](https://napcat.apifox.cn/226657041e0) | 获取 Cookies。 |
| `adapter.napcat.account.get_friends_with_category` | `get_friends_with_category` | 无 | `无参` | [官方](https://napcat.apifox.cn/226658978e0) | 获取带分组的好友列表。 |
| `adapter.napcat.account.get_mini_app_ark` | `get_mini_app_ark` | `type``title``desc``picUrl``jumpUrl` | `示例` | [官方](https://napcat.apifox.cn/227738594e0) | 官方页当前为 `Any Of` 结构,但左侧 Schema 未展开具体顶层字段,这里按同页示例请求体记录。 |
| `adapter.napcat.account.get_profile_like` | `get_profile_like` | `user_id``start``count` | `Schema` | [官方](https://napcat.apifox.cn/226659197e0) | 获取资料点赞。 |
| `adapter.napcat.account.get_recent_contact` | `get_recent_contact` | `count` | `Schema` | [官方](https://napcat.apifox.cn/226659190e0) | 获取最近会话。 |
| `adapter.napcat.account.get_rkey` | `get_rkey` | 无 | `无参` | [官方](https://napcat.apifox.cn/283136230e0) | 获取扩展 RKey。 |
| `adapter.napcat.account.get_rkey_server` | `get_rkey_server` | 无 | `无参` | [官方](https://napcat.apifox.cn/283136236e0) | 获取 RKey 服务器。 |
| `adapter.napcat.account.get_unidirectional_friend_list` | `get_unidirectional_friend_list` | 无 | `无参` | [官方](https://napcat.apifox.cn/266151878e0) | 获取单向好友列表。 |
| `adapter.napcat.account.internal_ocr_image` | `.ocr_image` | `image` | `Schema` | [官方](https://napcat.apifox.cn/226658234e0) | 内部 OCR 动作。 |
| `adapter.napcat.account.nc_get_rkey` | `nc_get_rkey` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659297e0) | 获取 RKey。 |
| `adapter.napcat.account.ocr_image` | `ocr_image` | `image` | `Schema` | [官方](https://napcat.apifox.cn/226658231e0) | 图片 OCR 识别。 |
| `adapter.napcat.account.send_like` | `send_like` | `user_id``times` | `Schema` | [官方](https://napcat.apifox.cn/226656717e0) | 点赞。 |
| `adapter.napcat.account.set_diy_online_status` | `set_diy_online_status` | `face_id``face_type``wording` | `Schema` | [官方](https://napcat.apifox.cn/266151905e0) | 设置自定义在线状态。 |
| `adapter.napcat.account.set_friend_add_request` | `set_friend_add_request` | `flag``approve``remark` | `Schema` | [官方](https://napcat.apifox.cn/226656932e0) | 处理加好友请求。 |
| `adapter.napcat.account.set_friend_remark` | `set_friend_remark` | `user_id``remark` | `Schema` | [官方](https://napcat.apifox.cn/298305173e0) | 设置好友备注。 |
| `adapter.napcat.account.set_qq_avatar` | `set_qq_avatar` | `file` | `Schema` | [官方](https://napcat.apifox.cn/226658980e0) | 设置 QQ 头像。 |
| `adapter.napcat.account.set_self_longnick` | `set_self_longnick` | `longNick` | `Schema` | [官方](https://napcat.apifox.cn/226659186e0) | 设置个性签名。 |
| `adapter.napcat.account.translate_en2zh` | `translate_en2zh` | `words` | `Schema` | [官方](https://napcat.apifox.cn/226659102e0) | 英文单词翻译。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.account.ocr_image",
params={"image": "https://example.com/demo.png"},
)
```

View File

@@ -0,0 +1,83 @@
# File 透传 API
这一页覆盖 `adapter.napcat.file.*` 下除 `get_record` 外的透传 API。
统一调用方式:
```python
response = await self.ctx.api.call(
"adapter.napcat.file.upload_group_file",
params={
"group_id": 123456789,
"file": "/tmp/demo.txt",
"name": "demo.txt",
},
)
```
字段来源说明:
- `无参`:官方页面当前无请求字段。
- `Schema`:直接来自官方“请求参数”结构。
- `Schema + 示例`:官方页左侧 Schema 和同页 `curl --data-raw` 示例合并后得到。
- `冲突`:官方页同页 Schema / 示例字段互相冲突,文档显式列出。
## API 列表
| API | 底层 action | 官方请求字段 | 来源 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.file.cancel_online_file` | `cancel_online_file` | `user_id``msg_id` | `Schema` | [官方](https://napcat.apifox.cn/410334677e0) | 取消在线文件。 |
| `adapter.napcat.file.clean_stream_temp_file` | `clean_stream_temp_file` | 无 | `无参` | [官方](https://napcat.apifox.cn/395354124e0) | 清理流式传输临时文件。 |
| `adapter.napcat.file.create_flash_task` | `create_flash_task` | `files``name``thumb_path` | `Schema` | [官方](https://napcat.apifox.cn/410334666e0) | 创建闪传任务。 |
| `adapter.napcat.file.create_group_file_folder` | `create_group_file_folder` | `group_id``folder_name``name` | `Schema` | [官方](https://napcat.apifox.cn/226658773e0) | 创建群文件目录。 |
| `adapter.napcat.file.del_group_album_media` | `del_group_album_media` | `group_id``album_id``lloc` | `Schema` | [官方](https://napcat.apifox.cn/395455119e0) | 删除群相册媒体。 |
| `adapter.napcat.file.delete_group_file` | `delete_group_file` | `group_id``file_id` | `Schema` | [官方](https://napcat.apifox.cn/226658755e0) | 删除群文件。 |
| `adapter.napcat.file.delete_group_folder` | `delete_group_folder` | `group_id``folder_id``folder` | `Schema` | [官方](https://napcat.apifox.cn/226658779e0) | 删除群文件目录。 |
| `adapter.napcat.file.do_group_album_comment` | `do_group_album_comment` | `group_id``album_id``lloc``content` | `Schema` | [官方](https://napcat.apifox.cn/395458911e0) | 发表群相册评论。 |
| `adapter.napcat.file.download_file` | `download_file` | `url``base64``name``headers` | `Schema` | [官方](https://napcat.apifox.cn/226658887e0) | 下载文件。 |
| `adapter.napcat.file.download_file_image_stream` | `download_file_image_stream` | `file``file_id``chunk_size` | `Schema` | [官方](https://napcat.apifox.cn/395419462e0) | 下载图片文件流。 |
| `adapter.napcat.file.download_file_record_stream` | `download_file_record_stream` | `file``file_id``chunk_size``out_format` | `Schema` | [官方](https://napcat.apifox.cn/395417040e0) | 下载语音文件流。 |
| `adapter.napcat.file.download_file_stream` | `download_file_stream` | `file``file_id``chunk_size` | `Schema` | [官方](https://napcat.apifox.cn/395413859e0) | 下载文件流。 |
| `adapter.napcat.file.download_fileset` | `download_fileset` | `fileset_id` | `Schema` | [官方](https://napcat.apifox.cn/410334678e0) | 下载文件集。 |
| `adapter.napcat.file.get_file` | `get_file` | `file``file_id` | `Schema` | [官方](https://napcat.apifox.cn/226658985e0) | 获取文件。 |
| `adapter.napcat.file.get_fileset_id` | `get_fileset_id` | `share_code` | `Schema` | [官方](https://napcat.apifox.cn/410334679e0) | 获取文件集 ID。 |
| `adapter.napcat.file.get_fileset_info` | `get_fileset_info` | `fileset_id` | `Schema` | [官方](https://napcat.apifox.cn/410334671e0) | 获取文件集信息。 |
| `adapter.napcat.file.get_flash_file_list` | `get_flash_file_list` | `fileset_id` | `Schema` | [官方](https://napcat.apifox.cn/410334667e0) | 获取闪传文件列表。 |
| `adapter.napcat.file.get_flash_file_url` | `get_flash_file_url` | `fileset_id``file_name``file_index` | `Schema` | [官方](https://napcat.apifox.cn/410334668e0) | 获取闪传文件链接。 |
| `adapter.napcat.file.get_group_album_media_list` | `get_group_album_media_list` | `group_id``album_id``attach_info` | `Schema` | [官方](https://napcat.apifox.cn/395459066e0) | 获取群相册媒体列表。 |
| `adapter.napcat.file.get_group_file_system_info` | `get_group_file_system_info` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658789e0) | 获取群文件系统信息。 |
| `adapter.napcat.file.get_group_file_url` | `get_group_file_url` | `group_id``file_id``busid` | `Schema + 示例` | [官方](https://napcat.apifox.cn/226658867e0) | 官方页左侧 Schema 当前只列 `group_id` / `file_id`,同页示例请求体额外给出 `busid`。 |
| `adapter.napcat.file.get_group_files_by_folder` | `get_group_files_by_folder` | `group_id``folder_id``folder``file_count` | `Schema` | [官方](https://napcat.apifox.cn/226658865e0) | 获取群文件夹文件列表。 |
| `adapter.napcat.file.get_group_root_files` | `get_group_root_files` | `group_id``file_count` | `Schema` | [官方](https://napcat.apifox.cn/226658823e0) | 获取群根目录文件列表。 |
| `adapter.napcat.file.get_image` | `get_image` | `file``file_id` | `Schema` | [官方](https://napcat.apifox.cn/226657066e0) | 获取图片。 |
| `adapter.napcat.file.get_online_file_msg` | `get_online_file_msg` | `user_id` | `Schema` | [官方](https://napcat.apifox.cn/410334672e0) | 获取在线文件消息。 |
| `adapter.napcat.file.get_private_file_url` | `get_private_file_url` | `user_id``file_id` | `Schema + 示例` | [官方](https://napcat.apifox.cn/266151849e0) | 官方页左侧 Schema 当前只列 `file_id`,同页示例请求体额外给出 `user_id`。 |
| `adapter.napcat.file.get_qun_album_list` | `get_qun_album_list` | `group_id``attach_info` | `Schema` | [官方](https://napcat.apifox.cn/395460287e0) | 获取群相册列表。 |
| `adapter.napcat.file.get_share_link` | `get_share_link` | `fileset_id` | `Schema` | [官方](https://napcat.apifox.cn/410334670e0) | 获取文件分享链接。 |
| `adapter.napcat.file.move_group_file` | `move_group_file` | `group_id``file_id``current_parent_directory``target_parent_directory` | `Schema` | [官方](https://napcat.apifox.cn/283136359e0) | 移动群文件。 |
| `adapter.napcat.file.receive_online_file` | `receive_online_file` | `user_id``msg_id``element_id` | `Schema` | [官方](https://napcat.apifox.cn/410334675e0) | 接收在线文件。 |
| `adapter.napcat.file.refuse_online_file` | `refuse_online_file` | `user_id``msg_id``element_id` | `Schema` | [官方](https://napcat.apifox.cn/410334676e0) | 拒绝在线文件。 |
| `adapter.napcat.file.rename_group_file` | `rename_group_file` | `group_id``file_id``current_parent_directory``new_name` | `Schema` | [官方](https://napcat.apifox.cn/283136375e0) | 重命名群文件。 |
| `adapter.napcat.file.send_flash_msg` | `send_flash_msg` | `fileset_id``user_id``group_id` | `Schema` | [官方](https://napcat.apifox.cn/410334669e0) | 发送闪传消息。 |
| `adapter.napcat.file.send_online_file` | `send_online_file` | `user_id``file_path``file_name` | `Schema` | [官方](https://napcat.apifox.cn/410334673e0) | 发送在线文件。 |
| `adapter.napcat.file.send_online_folder` | `send_online_folder` | `user_id``folder_path``folder_name` | `Schema` | [官方](https://napcat.apifox.cn/410334674e0) | 发送在线文件夹。 |
| `adapter.napcat.file.set_group_album_media_like` | `set_group_album_media_like` | `group_id``album_id``lloc``id``set` | `Schema` | [官方](https://napcat.apifox.cn/395457331e0) | 点赞群相册媒体。 |
| `adapter.napcat.file.trans_group_file` | `trans_group_file` | `group_id``file_id` | `Schema` | [官方](https://napcat.apifox.cn/283136366e0) | 传输群文件。 |
| `adapter.napcat.file.upload_file_stream` | `upload_file_stream` | `stream_id``chunk_data``chunk_index``total_chunks``file_size``expected_sha256``is_complete``filename``reset``verify_only``file_retention` | `Schema` | [官方](https://napcat.apifox.cn/395363988e0) | 上传文件流。 |
| `adapter.napcat.file.upload_group_file` | `upload_group_file` | `group_id``file``name``folder``folder_id``upload_file` | `Schema` | [官方](https://napcat.apifox.cn/226658753e0) | 上传群文件。 |
| `adapter.napcat.file.upload_image_to_qun_album` | `upload_image_to_qun_album` | `group_id``album_id``album_name``file` | `Schema` | [官方](https://napcat.apifox.cn/395459739e0) | 上传图片到群相册。 |
| `adapter.napcat.file.upload_private_file` | `upload_private_file` | `user_id``file``name``upload_file` | `Schema` | [官方](https://napcat.apifox.cn/226658883e0) | 上传私聊文件。 |
| `adapter.napcat.file.test_download_stream` | `test_download_stream` | `error``url` | `冲突` | [官方](https://napcat.apifox.cn/395355338e0) | 官方页左侧 Schema 当前字段为 `error`,同页示例请求体却使用 `url`,两者互相冲突。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.file.upload_group_file",
params={
"group_id": 123456789,
"file": "/tmp/demo.txt",
"name": "demo.txt",
},
)
```

View File

@@ -0,0 +1,70 @@
# Group 透传 API
这一页覆盖 `adapter.napcat.group.*` 下除强类型封装 API 外的透传 API。
统一调用方式:
```python
response = await self.ctx.api.call(
"adapter.napcat.group.set_group_admin",
params={
"group_id": 123456789,
"user_id": 987654321,
"enable": True,
},
)
```
字段来源说明:
- `无参`:官方页面当前无请求字段。
- `Schema`:直接来自官方“请求参数”结构。
- `示例`:官方页面 Schema 未展开字段,参数来自同页 `curl --data-raw` 示例。
## API 列表
| API | 底层 action | 官方请求字段 | 来源 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.group.delete_essence_msg` | `delete_essence_msg` | `message_id``msg_seq``msg_random``group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658678e0) | 官方页当前除了 `message_id` 还列出兼容字段;适配器这里完全透传。 |
| `adapter.napcat.group.delete_group_notice` | `_del_group_notice` | `group_id``notice_id` | `Schema` | [官方](https://napcat.apifox.cn/226659240e0) | 删除群公告。 |
| `adapter.napcat.group.get_essence_msg_list` | `get_essence_msg_list` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658664e0) | 获取群精华消息。 |
| `adapter.napcat.group.get_group_honor_info` | `get_group_honor_info` | `group_id``type` | `Schema` | [官方](https://napcat.apifox.cn/226657036e0) | 获取群荣誉信息。 |
| `adapter.napcat.group.get_group_ignore_add_request` | `get_group_ignore_add_request` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659234e0) | 获取群被忽略的加群请求。 |
| `adapter.napcat.group.get_group_ignored_notifies` | `get_group_ignored_notifies` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659323e0) | 获取群忽略通知。 |
| `adapter.napcat.group.get_group_info_ex` | `get_group_info_ex` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226659229e0) | 获取群详细信息(扩展)。 |
| `adapter.napcat.group.get_group_notice` | `_get_group_notice` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658742e0) | 获取群公告。 |
| `adapter.napcat.group.get_group_shut_list` | `get_group_shut_list` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226659300e0) | 获取群禁言列表。 |
| `adapter.napcat.group.get_group_system_msg` | `get_group_system_msg` | `count` | `Schema` | [官方](https://napcat.apifox.cn/226658660e0) | 获取群系统消息。 |
| `adapter.napcat.group.get_guild_list` | `get_guild_list` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659311e0) | 获取频道列表。 |
| `adapter.napcat.group.get_guild_service_profile` | `get_guild_service_profile` | `guild_id` | `示例` | [官方](https://napcat.apifox.cn/226659317e0) | 官方页左侧 Schema 当前只显示 `object`,同页示例请求体给出 `guild_id`。 |
| `adapter.napcat.group.group_poke` | `group_poke` | `group_id``user_id``target_id` | `Schema` | [官方](https://napcat.apifox.cn/226659265e0) | 发送群聊戳一戳。 |
| `adapter.napcat.group.handle_quick_operation_internal` | `.handle_quick_operation` | `context``operation` | `Schema` | [官方](https://napcat.apifox.cn/226658889e0) | 处理快速操作。 |
| `adapter.napcat.group.send_group_msg` | `send_group_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226656598e0) | 官方页当前顶层请求字段就是这一组;真正的消息段细节放在 `message` 内。 |
| `adapter.napcat.group.send_group_notice` | `_send_group_notice` | `group_id``content``image``pinned``type``confirm_required``is_show_edit_card``tip_window_type` | `Schema` | [官方](https://napcat.apifox.cn/226658740e0) | 发送群公告。 |
| `adapter.napcat.group.send_group_sign` | `send_group_sign` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/230897177e0) | NapCat 另外提供的“群打卡”动作。 |
| `adapter.napcat.group.set_essence_msg` | `set_essence_msg` | `message_id` | `Schema` | [官方](https://napcat.apifox.cn/226658674e0) | 设置精华消息。 |
| `adapter.napcat.group.set_group_add_option` | `set_group_add_option` | `group_id``add_type``group_question``group_answer` | `Schema` | [官方](https://napcat.apifox.cn/301542178e0) | 设置群加群选项。 |
| `adapter.napcat.group.set_group_add_request` | `set_group_add_request` | `flag``approve``reason``count` | `Schema` | [官方](https://napcat.apifox.cn/226656947e0) | 官方页当前字段与旧版常见的 `sub_type` 方案不同;文档按当前官方页记录。 |
| `adapter.napcat.group.set_group_admin` | `set_group_admin` | `group_id``user_id``enable` | `Schema` | [官方](https://napcat.apifox.cn/226656815e0) | 设置群管理员。 |
| `adapter.napcat.group.set_group_card` | `set_group_card` | `group_id``user_id``card` | `Schema` | [官方](https://napcat.apifox.cn/226656913e0) | 设置群名片。 |
| `adapter.napcat.group.set_group_leave` | `set_group_leave` | `group_id``is_dismiss` | `Schema` | [官方](https://napcat.apifox.cn/226656926e0) | 退出群组。 |
| `adapter.napcat.group.set_group_portrait` | `set_group_portrait` | `file``group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658669e0) | 设置群头像。 |
| `adapter.napcat.group.set_group_remark` | `set_group_remark` | `group_id``remark` | `Schema` | [官方](https://napcat.apifox.cn/283136268e0) | 设置群备注。 |
| `adapter.napcat.group.set_group_robot_add_option` | `set_group_robot_add_option` | `group_id``robot_member_switch``robot_member_examine` | `Schema` | [官方](https://napcat.apifox.cn/301542198e0) | 设置群机器人加群选项。 |
| `adapter.napcat.group.set_group_search` | `set_group_search` | `group_id``no_code_finger_open``no_finger_open` | `Schema` | [官方](https://napcat.apifox.cn/301542170e0) | 设置群搜索选项。 |
| `adapter.napcat.group.set_group_sign` | `set_group_sign` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226659329e0) | 群打卡。 |
| `adapter.napcat.group.set_group_special_title` | `set_group_special_title` | `group_id``user_id``special_title` | `Schema` | [官方](https://napcat.apifox.cn/226656931e0) | 设置专属头衔。 |
| `adapter.napcat.group.set_group_todo` | `set_group_todo` | `group_id``message_id``message_seq` | `Schema` | [官方](https://napcat.apifox.cn/395460568e0) | 设置群待办。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.group.send_group_msg",
params={
"message_type": "group",
"group_id": 123456789,
"message": [{"type": "text", "data": {"text": "你好"}}],
},
)
```

View File

@@ -0,0 +1,61 @@
# Message 透传 API
这一页覆盖 `adapter.napcat.message.*` 下除强类型封装 API 外的透传 API。
统一调用方式:
```python
response = await self.ctx.api.call(
"adapter.napcat.message.friend_poke",
params={
"group_id": 123456789,
"user_id": 987654321,
"target_id": 987654321,
},
)
```
字段来源说明:
- `无参`:官方页面当前无请求字段。
- `Schema`:直接来自官方“请求参数”结构。
## API 列表
| API | 底层 action | 官方请求字段 | 来源 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.message.ark_share_group` | `ArkShareGroup` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/226658971e0) | 分享群Ark。 |
| `adapter.napcat.message.ark_share_peer` | `ArkSharePeer` | `user_id``group_id``phone_number` | `Schema` | [官方](https://napcat.apifox.cn/226658965e0) | 分享用户Ark。 |
| `adapter.napcat.message.click_inline_keyboard_button` | `click_inline_keyboard_button` | `group_id``bot_appid``button_id``callback_data``msg_seq` | `Schema` | [官方](https://napcat.apifox.cn/266151864e0) | 点击内联键盘按钮。 |
| `adapter.napcat.message.fetch_emoji_like` | `fetch_emoji_like` | `message_id``emojiId``emojiType``count``cookie` | `Schema` | [官方](https://napcat.apifox.cn/226659219e0) | 获取表情点赞详情。 |
| `adapter.napcat.message.forward_friend_single_msg` | `forward_friend_single_msg` | `message_id``group_id``user_id` | `Schema` | [官方](https://napcat.apifox.cn/226659051e0) | 转发单条消息。 |
| `adapter.napcat.message.forward_group_single_msg` | `forward_group_single_msg` | `message_id``group_id``user_id` | `Schema` | [官方](https://napcat.apifox.cn/226659074e0) | 转发单条消息。 |
| `adapter.napcat.message.friend_poke` | `friend_poke` | `group_id``user_id``target_id` | `Schema` | [官方](https://napcat.apifox.cn/226659255e0) | 发送私聊戳一戳。 |
| `adapter.napcat.message.get_ai_record` | `get_ai_record` | `character``group_id``text` | `Schema` | [官方](https://napcat.apifox.cn/229486818e0) | 获取 AI 语音。 |
| `adapter.napcat.message.get_emoji_likes` | `get_emoji_likes` | `group_id``message_id``emoji_id``emoji_type``count` | `Schema` | [官方](https://napcat.apifox.cn/410334663e0) | 获取消息表情点赞列表。 |
| `adapter.napcat.message.get_friend_msg_history` | `get_friend_msg_history` | `user_id``message_seq``count``reverse_order``disable_get_url``parse_mult_msg``quick_reply``reverseOrder` | `Schema` | [官方](https://napcat.apifox.cn/226659174e0) | 官方页当前同时列出 `reverse_order``reverseOrder` 两种写法。 |
| `adapter.napcat.message.get_group_msg_history` | `get_group_msg_history` | `group_id``message_seq``count``reverse_order``disable_get_url``parse_mult_msg``quick_reply``reverseOrder` | `Schema` | [官方](https://napcat.apifox.cn/226657401e0) | 官方页当前同时列出 `reverse_order``reverseOrder` 两种写法。 |
| `adapter.napcat.message.mark_all_as_read` | `_mark_all_as_read` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659194e0) | 标记所有消息已读。 |
| `adapter.napcat.message.mark_group_msg_as_read` | `mark_group_msg_as_read` | `user_id``group_id``message_id` | `Schema` | [官方](https://napcat.apifox.cn/226659167e0) | 标记群聊已读。 |
| `adapter.napcat.message.mark_msg_as_read` | `mark_msg_as_read` | `user_id``group_id``message_id` | `Schema` | [官方](https://napcat.apifox.cn/226657389e0) | 标记消息已读Go-CQHTTP 兼容)。 |
| `adapter.napcat.message.mark_private_msg_as_read` | `mark_private_msg_as_read` | `user_id``group_id``message_id` | `Schema` | [官方](https://napcat.apifox.cn/226659165e0) | 标记私聊已读。 |
| `adapter.napcat.message.send_ark_share` | `send_ark_share` | `user_id``group_id``phone_number` | `Schema` | [官方](https://napcat.apifox.cn/410334665e0) | 分享用户Ark。 |
| `adapter.napcat.message.send_forward_msg` | `send_forward_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226659136e0) | 官方页当前顶层请求字段就是这一组;真正的转发节点细节放在 `message` 内。 |
| `adapter.napcat.message.send_group_ark_share` | `send_group_ark_share` | `group_id` | `Schema` | [官方](https://napcat.apifox.cn/410334664e0) | 分享群Ark。 |
| `adapter.napcat.message.send_group_forward_msg` | `send_group_forward_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226657396e0) | 发送群合并转发消息。 |
| `adapter.napcat.message.send_msg` | `send_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226656652e0) | 通用发送消息。 |
| `adapter.napcat.message.send_private_forward_msg` | `send_private_forward_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226657399e0) | 发送私聊合并转发消息。 |
| `adapter.napcat.message.send_private_msg` | `send_private_msg` | `message_type``user_id``group_id``message``auto_escape``source``news``summary``prompt``timeout` | `Schema` | [官方](https://napcat.apifox.cn/226656553e0) | 发送私聊消息。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.message.send_msg",
params={
"message_type": "group",
"group_id": 123456789,
"message": [{"type": "text", "data": {"text": "你好MaiBot"}}],
},
)
```

View File

@@ -0,0 +1,54 @@
# System 透传 API
这一页覆盖 `adapter.napcat.system.*` 下除 `get_login_info` 外的透传 API。
统一调用方式:
```python
response = await self.ctx.api.call(
"adapter.napcat.system.check_url_safely",
params={"url": "https://example.com"},
)
```
字段来源说明:
- `无参`:官方页面当前无请求字段。
- `Schema`:直接来自官方“请求参数”结构。
- `示例`:官方页面 Schema 只显示泛型 `object`,字段来自同页 `curl --data-raw` 示例。
## API 列表
| API | 底层 action | 官方请求字段 | 来源 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.system.bot_exit` | `bot_exit` | 无 | `无参` | [官方](https://napcat.apifox.cn/283136399e0) | 退出登录。 |
| `adapter.napcat.system.can_send_image` | `can_send_image` | 无 | `无参` | [官方](https://napcat.apifox.cn/226657071e0) | 是否可以发送图片。 |
| `adapter.napcat.system.can_send_record` | `can_send_record` | 无 | `无参` | [官方](https://napcat.apifox.cn/226657080e0) | 是否可以发送语音。 |
| `adapter.napcat.system.check_url_safely` | `check_url_safely` | `url` | `Schema` | [官方](https://napcat.apifox.cn/228534361e0) | 检查 URL 安全性。 |
| `adapter.napcat.system.clean_cache` | `clean_cache` | 无 | `无参` | [官方](https://napcat.apifox.cn/298305106e0) | 清理缓存。 |
| `adapter.napcat.system.get_credentials` | `get_credentials` | `domain` | `Schema` | [官方](https://napcat.apifox.cn/226657054e0) | 获取登录凭证。 |
| `adapter.napcat.system.get_csrf_token` | `get_csrf_token` | 无 | `无参` | [官方](https://napcat.apifox.cn/226657044e0) | 获取 CSRF Token。 |
| `adapter.napcat.system.get_doubt_friends_add_request` | `get_doubt_friends_add_request` | `count` | `Schema` | [官方](https://napcat.apifox.cn/289565516e0) | 获取可疑好友申请。 |
| `adapter.napcat.system.get_model_show` | `_get_model_show` | `model` | `Schema` | [官方](https://napcat.apifox.cn/227233981e0) | 获取机型显示。 |
| `adapter.napcat.system.get_online_clients` | `get_online_clients` | `no_cache` | `示例` | [官方](https://napcat.apifox.cn/226657379e0) | 官方页左侧 Schema 当前只显示 `object`,同页示例请求体给出 `no_cache`。 |
| `adapter.napcat.system.get_robot_uin_range` | `get_robot_uin_range` | 无 | `无参` | [官方](https://napcat.apifox.cn/226658975e0) | 获取机器人 UIN 范围。 |
| `adapter.napcat.system.get_status` | `get_status` | 无 | `无参` | [官方](https://napcat.apifox.cn/226657083e0) | 获取运行状态。 |
| `adapter.napcat.system.get_version_info` | `get_version_info` | 无 | `无参` | [官方](https://napcat.apifox.cn/226657087e0) | 获取版本信息。 |
| `adapter.napcat.system.nc_get_packet_status` | `nc_get_packet_status` | 无 | `无参` | [官方](https://napcat.apifox.cn/226659280e0) | 获取 Packet 状态。 |
| `adapter.napcat.system.nc_get_user_status` | `nc_get_user_status` | `user_id` | `Schema` | [官方](https://napcat.apifox.cn/226659292e0) | 获取用户在线状态。 |
| `adapter.napcat.system.send_packet` | `send_packet` | `cmd``data``rsp` | `Schema` | [官方](https://napcat.apifox.cn/250286903e0) | 发送原始数据包。 |
| `adapter.napcat.system.set_doubt_friends_add_request` | `set_doubt_friends_add_request` | `flag``approve` | `Schema` | [官方](https://napcat.apifox.cn/289565525e0) | 处理可疑好友申请。 |
| `adapter.napcat.system.set_input_status` | `set_input_status` | `user_id``event_type` | `Schema` | [官方](https://napcat.apifox.cn/226659225e0) | 设置输入状态。 |
| `adapter.napcat.system.set_model_show` | `_set_model_show` | `model``model_show` | `示例` | [官方](https://napcat.apifox.cn/227233993e0) | 官方页左侧 Schema 当前只显示 `object`,同页示例请求体给出 `model` / `model_show`。 |
| `adapter.napcat.system.set_online_status` | `set_online_status` | `status``ext_status``battery_status` | `Schema` | [官方](https://napcat.apifox.cn/226658977e0) | 设置在线状态。 |
| `adapter.napcat.system.set_restart` | `set_restart` | 无 | `无参` | [官方](https://napcat.apifox.cn/410334662e0) | 重启服务。 |
| `adapter.napcat.system.unknown_action` | `unknown` | 无 | `无参` | [官方](https://napcat.apifox.cn/411631224e0) | 透传调用名为 `unknown` 的底层动作。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.system.get_online_clients",
params={"no_cache": False},
)
```

View File

@@ -0,0 +1,83 @@
# 强类型封装 API
这一页只写“强类型封装 API”。
调用规则:
- 直接使用 `self.ctx.api.call("完整 API 名", **kwargs)`
- 不要把参数再包进 `params`
- 如果 `response["success"]` 为真,真正的业务结果在 `response["result"]`
## 通用入口
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.action.call` | `action_name``params=None` | 任意 action | 由 `action_name` 决定 | 无 | 适配器通用入口;`result` 为 NapCat 原始响应字典。 |
| `adapter.napcat.action.call_data` | `action_name``params=None` | 任意 action | 由 `action_name` 决定 | 无 | 适配器通用入口;`result` 直接返回 NapCat 响应里的 `data`。 |
## System
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.system.get_login_info` | 无 | `get_login_info` | 无 | [官方](https://napcat.apifox.cn/226656952e0) | `result``dict \| None`;失败返回 `None`。 |
## Account
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.account.set_qq_profile` | `nickname``personal_note=""``sex=""` | `set_qq_profile` | `nickname``personal_note``sex` | [官方](https://napcat.apifox.cn/226657374e0) | `nickname` 必填;`sex` 仅允许 `male` / `female` / `unknown`;空 `personal_note` 和空 `sex` 不会下发。 |
| `adapter.napcat.account.get_stranger_info` | `user_id``no_cache=False` | `get_stranger_info` | `user_id``no_cache` | [官方](https://napcat.apifox.cn/226656970e0) | 适配器已对齐官方隐藏 schema官方默认示例只展示 `user_id`,但页面内嵌 schema 还定义了 `no_cache``result``dict \| None`。 |
| `adapter.napcat.account.get_friend_list` | `no_cache=False` | `get_friend_list` | `no_cache` | [官方](https://napcat.apifox.cn/226656976e0) | `result` 为归一化后的好友列表NapCat 不同版本下若把列表包在 `friend_list` / `data` 里,适配器会自动展开。 |
## Group
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.group.set_group_ban` | `group_id``user_id``duration` | `set_group_ban` | `group_id``user_id``duration` | [官方](https://napcat.apifox.cn/226656791e0) | `duration` 必须是 `0``2592000` 之间的非负整数。 |
| `adapter.napcat.group.set_group_whole_ban` | `group_id``enable` | `set_group_whole_ban` | `group_id``enable` | [官方](https://napcat.apifox.cn/226656802e0) | `enable` 会被规范成布尔值。 |
| `adapter.napcat.group.set_group_kick` | `group_id``user_id``reject_add_request=False` | `set_group_kick` | `group_id``user_id``reject_add_request` | [官方](https://napcat.apifox.cn/226656748e0) | 单个踢人封装。 |
| `adapter.napcat.group.set_group_kick_members` | `group_id``user_id``reject_add_request=False` | `set_group_kick_members` | `group_id``user_id``reject_add_request` | [官方](https://napcat.apifox.cn/301542209e0) | 适配器要求 `user_id` 传数组,并实际下发 `user_id: [ ... ]`。 |
| `adapter.napcat.group.set_group_name` | `group_id``group_name` | `set_group_name` | `group_id``group_name` | [官方](https://napcat.apifox.cn/226656919e0) | `group_name` 会被规范成非空字符串。 |
| `adapter.napcat.group.get_group_info` | `group_id` | `get_group_info` | `group_id` | [官方](https://napcat.apifox.cn/226656979e0) | `result``dict \| None`。 |
| `adapter.napcat.group.get_group_detail_info` | `group_id` | `get_group_detail_info` | `group_id` | [官方](https://napcat.apifox.cn/307180859e0) | `result``dict \| None`。 |
| `adapter.napcat.group.get_group_list` | `no_cache=False` | `get_group_list` | `no_cache` | [官方](https://napcat.apifox.cn/226656992e0) | `result` 为归一化后的群列表。 |
| `adapter.napcat.group.get_group_at_all_remain` | `group_id` | `get_group_at_all_remain` | `group_id` | [官方](https://napcat.apifox.cn/227245941e0) | `result``dict \| None`;不同 NapCat 版本下返回字段名可能不同。 |
| `adapter.napcat.group.get_group_member_info` | `group_id``user_id``no_cache=True` | `get_group_member_info` | `group_id``user_id``no_cache` | [官方](https://napcat.apifox.cn/226657019e0) | `group_id` / `user_id` 会先规范化为正整数,再转字符串下发。 |
| `adapter.napcat.group.get_group_member_list` | `group_id``no_cache=False` | `get_group_member_list` | `group_id``no_cache` | [官方](https://napcat.apifox.cn/226657034e0) | `result` 为归一化后的成员列表。 |
## Message
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.message.send_poke` | `user_id=None``group_id=None``target_id=None``qq_id=None` | `send_poke` | `group_id``user_id``target_id` | [官方](https://napcat.apifox.cn/250286923e0) | 优先使用官方字段 `user_id` / `group_id` / `target_id``qq_id` 仅作为旧版兼容别名,会映射成 `user_id`。 |
| `adapter.napcat.message.delete_msg` | `message_id` | `delete_msg` | `message_id` | [官方](https://napcat.apifox.cn/226919954e0) | `message_id` 必须是正整数。 |
| `adapter.napcat.message.send_group_ai_record` | `group_id``character``text` | `send_group_ai_record` | `character``group_id``text` | [官方](https://napcat.apifox.cn/229486774e0) | `character``text` 都会被规范成非空字符串。 |
| `adapter.napcat.message.set_msg_emoji_like` | `message_id``emoji_id``set=True` | `set_msg_emoji_like` | `message_id``emoji_id``set` | [官方](https://napcat.apifox.cn/226659104e0) | 适配器把 `set` 下发为官方字段 `set`。 |
| `adapter.napcat.message.get_msg` | `message_id` | `get_msg` | `message_id` | [官方](https://napcat.apifox.cn/226656707e0) | `result``dict \| None`。 |
| `adapter.napcat.message.get_forward_msg` | `message_id=""``id=""` | `get_forward_msg` | `message_id``id` | [官方](https://napcat.apifox.cn/226656712e0) | 适配器已对齐官方隐藏 schema至少提供一个字段若两个字段同时传入则要求值一致`result` 会统一整理成 `{\"messages\": [...]}`。 |
## File
| API | 适配器直接参数 | 官方 action | 官方请求字段 | 官方文档 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `adapter.napcat.file.get_record` | `file=""``file_id=""``out_format="wav"` | `get_record` | `file``file_id``out_format` | [官方](https://napcat.apifox.cn/226657058e0) | 适配器已对齐官方隐藏 schema`file` / `file_id` 至少提供一个;`out_format` 默认仍为 `wav`,以兼容旧行为。 |
## 典型示例
```python
response = await self.ctx.api.call(
"adapter.napcat.message.send_poke",
user_id=987654321,
group_id=123456789,
target_id=123456789,
)
```
```python
response = await self.ctx.api.call(
"adapter.napcat.group.get_group_member_info",
group_id=123456789,
user_id=987654321,
no_cache=True,
)
```

View File

@@ -0,0 +1,55 @@
# 核验与兼容性说明
## 1. 这次核验做了什么
- 扫描 `plugins/MaiBot-Napcat-Adapter/apis/*.py` 中全部 `@API(..., public=True)` 公开接口。
- 对每个透传 API 反查其底层 NapCat action。
- 用 NapCat 官方文档 [https://napcat.apifox.cn/](https://napcat.apifox.cn/) 逐项确认底层 action 页面是否存在。
- 用浏览器逐页读取官方页面的“请求参数”结构;遇到官方页左侧只显示泛型 `object` 时,再补读同页 `curl --data-raw` 示例。
- 对请求示例少于隐藏 schema 的页面,额外抓取官方页原始 HTML核对 Apifox 内嵌的 request body schema。
- 对强类型封装 API额外对照 `plugins/MaiBot-Napcat-Adapter/services/query_service.py` 确认适配器实际下发的 body。
## 2. 覆盖范围
- 适配器公开 API 总数:`164`
- 其中适配器自带通用入口:`2`
- `adapter.napcat.action.call`
- `adapter.napcat.action.call_data`
- 其中可映射到底层 NapCat action 的 API`162`
-`162` 个底层 action 的官方文档页面:`162 / 162` 都已找到并写入 docs
## 3. 参数对齐口径
- 普通透传 API文档写“官方请求字段”适配器自己不裁剪只要求调用方传 `params={...}`
- 强类型封装 API文档写“适配器直接参数”和“官方请求字段”两列明确哪里是适配器收敛过的用法。
- 如果官方页 `Schema` 和同页 `curl` 示例不一致,文档不会替官方做静默判断,而是显式标成 `Schema + 示例``冲突`
## 4. 已确认的官方页例外
| action / API | 官方文档 | 现象 | 文档处理方式 |
| --- | --- | --- | --- |
| `get_online_clients` | [官方](https://napcat.apifox.cn/226657379e0) | 左侧 Schema 只显示 `object`,同页示例请求体给出 `no_cache` | 在文档中按 `示例` 记录 `no_cache` |
| `_set_model_show` | [官方](https://napcat.apifox.cn/227233993e0) | 左侧 Schema 只显示 `object`,同页示例请求体给出 `model``model_show` | 在文档中按 `示例` 记录 |
| `get_mini_app_ark` | [官方](https://napcat.apifox.cn/227738594e0) | 官方页为 `Any Of` 结构,但左侧 Schema 未展开可直接抄用的顶层字段 | 在文档中按同页示例请求体记录当前可见字段 |
| `get_guild_service_profile` | [官方](https://napcat.apifox.cn/226659317e0) | 左侧 Schema 只显示 `object`,同页示例请求体给出 `guild_id` | 在文档中按 `示例` 记录 `guild_id` |
| `get_group_file_url` | [官方](https://napcat.apifox.cn/226658867e0) | 左侧 Schema 只列 `group_id``file_id`,同页示例请求体额外给出 `busid` | 在文档中按 `Schema + 示例` 合并记录 |
| `get_private_file_url` | [官方](https://napcat.apifox.cn/266151849e0) | 左侧 Schema 只列 `file_id`,同页示例请求体额外给出 `user_id` | 在文档中按 `Schema + 示例` 合并记录 |
| `test_download_stream` | [官方](https://napcat.apifox.cn/395355338e0) | 左侧 Schema 当前字段为 `error`,同页示例请求体却使用 `url` | 在文档中标记为 `冲突`,两组字段都写出 |
| `get_stranger_info` | [官方](https://napcat.apifox.cn/226656970e0) | 页面默认示例只写 `user_id`,但原始 HTML 的隐藏 schema 还定义了 `no_cache` | 在文档和实现中按隐藏 schema 记录 `user_id``no_cache` |
| `get_forward_msg` | [官方](https://napcat.apifox.cn/226656712e0) | 页面默认示例只写 `message_id`,但原始 HTML 的隐藏 schema 还定义了 `id` | 在文档和实现中按隐藏 schema 同时支持 `message_id``id` |
| `get_record` | [官方](https://napcat.apifox.cn/226657058e0) | 页面默认示例只写 `file``out_format`,但原始 HTML 的隐藏 schema 还定义了 `file_id` | 在文档和实现中按隐藏 schema 记录 `file``file_id``out_format` |
| `send_poke` | [官方](https://napcat.apifox.cn/250286923e0) | 页面默认示例只写 `user_id`,但原始 HTML 的隐藏 schema 还定义了 `group_id``target_id` | 在文档和实现中按隐藏 schema 记录 `user_id``group_id``target_id` |
| `send_group_sign` | [官方](https://napcat.apifox.cn/230897177e0) | 官方页面存在,但侧边序列化索引缺少常规标题字段 | 文档直接写死官方链接,不依赖侧边索引标题 |
| `send_poke` / `friend_poke` / `forward_group_single_msg` / `send_ark_share` / `send_group_ark_share` / `clean_stream_temp_file` | 对应各自官方页 | 官方侧边序列化索引缺少常规标题字段,但页面本身存在 | 文档直接写死官方链接,不依赖侧边索引标题 |
## 5. 适配器实现侧的对齐与兼容策略
| API | 实际下发 | 与官方字段的关系 |
| --- | --- | --- |
| `adapter.napcat.message.send_poke` | `{"user_id": ..., "group_id"?: ..., "target_id"?: ...}` | 已对齐官方隐藏 schema公开 API 额外保留 `qq_id` 作为旧版兼容别名 |
| `adapter.napcat.message.get_forward_msg` | 调 `get_forward_msg({"message_id"?: ..., "id"?: ...})` | 已对齐官方隐藏 schema至少提供一个字段双字段同时传入时要求一致 |
| `adapter.napcat.file.get_record` | 调 `get_record({"file"?: ..., "file_id"?: ..., "out_format"?: ...})` | 已对齐官方隐藏 schema默认 `out_format="wav"` 仅用于兼容旧行为 |
| `adapter.napcat.account.get_stranger_info` | `{"user_id": ..., "no_cache": ...}` | 已对齐官方隐藏 schema`no_cache` 默认值为 `False` |
- 当前强类型封装 API 已无“缺少官方字段”的已知冲突项。
- 当前仍保留的兼容策略只有两类:`send_poke``qq_id` 旧版别名,以及 `get_record``out_format="wav"` 默认值。

View File

@@ -0,0 +1,82 @@
"""NapCat 入站消息过滤。"""
from typing import Any, Collection
from .config import NapCatChatConfig
class NapCatChatFilter:
"""NapCat 聊天名单过滤器。"""
def __init__(self, logger: Any) -> None:
"""初始化聊天名单过滤器。
Args:
logger: 插件日志对象。
"""
self._logger = logger
def is_inbound_chat_allowed(
self,
sender_user_id: str,
group_id: str,
chat_config: NapCatChatConfig,
) -> bool:
"""检查入站消息是否通过聊天名单过滤。
Args:
sender_user_id: 发送者用户 ID。
group_id: 群聊 ID私聊时为空字符串。
chat_config: 当前生效的聊天配置。
Returns:
bool: 若消息允许继续进入 Host则返回 ``True``。
"""
if sender_user_id in chat_config.ban_user_id:
self._logger.warning(f"NapCat 用户 {sender_user_id} 在全局禁止名单中,消息被丢弃")
return False
if not chat_config.enable_chat_list_filter:
return True
if group_id:
if not self._is_id_allowed_by_list_policy(group_id, chat_config.group_list_type, chat_config.group_list):
self._log_chat_list_rejection(
chat_config.show_dropped_chat_list_messages,
f"NapCat 群聊 {group_id} 未通过聊天名单过滤,消息被丢弃",
)
return False
return True
if not self._is_id_allowed_by_list_policy(
sender_user_id,
chat_config.private_list_type,
chat_config.private_list,
):
self._log_chat_list_rejection(
chat_config.show_dropped_chat_list_messages,
f"NapCat 私聊用户 {sender_user_id} 未通过聊天名单过滤,消息被丢弃",
)
return False
return True
def _log_chat_list_rejection(self, enabled: bool, message: str) -> None:
"""按配置决定是否记录聊天名单过滤丢弃日志。"""
if enabled:
self._logger.warning(message)
@staticmethod
def _is_id_allowed_by_list_policy(target_id: str, list_type: str, configured_ids: Collection[str]) -> bool:
"""根据白名单或黑名单规则判断目标 ID 是否允许通过。
Args:
target_id: 待检查的目标 ID。
list_type: 名单模式,仅支持 ``whitelist`` 或 ``blacklist``。
configured_ids: 配置中的 ID 集合或列表。
Returns:
bool: 若目标 ID 允许通过,则返回 ``True``。
"""
if list_type == "whitelist":
return target_id in configured_ids
return target_id not in configured_ids

View File

@@ -0,0 +1,148 @@
"""NapCat 心跳监测。"""
from __future__ import annotations
from typing import Any, Awaitable, Callable, Mapping, Optional
import asyncio
import time
class NapCatHeartbeatMonitor:
"""NapCat 心跳状态监测器。"""
def __init__(
self,
logger: Any,
on_timeout: Callable[[str], Awaitable[None]],
) -> None:
"""初始化心跳监测器。
Args:
logger: 插件日志对象。
on_timeout: 当心跳长时间未更新时触发的异步回调。
"""
self._logger = logger
self._on_timeout = on_timeout
self._last_heartbeat_at: float = 0.0
self._interval_sec: float = 30.0
self._self_id: str = ""
self._check_task: Optional[asyncio.Task[None]] = None
self._timeout_reported: bool = False
async def start(self, self_id: str, default_interval_sec: float) -> None:
"""启动或刷新心跳监测。
Args:
self_id: 当前机器人账号 ID。
default_interval_sec: 默认心跳间隔秒数。
"""
normalized_self_id = str(self_id or "").strip()
if normalized_self_id:
self._self_id = normalized_self_id
self._interval_sec = max(float(default_interval_sec or 30.0), 1.0)
self._touch()
if self._check_task is None or self._check_task.done():
self._check_task = asyncio.create_task(
self._check_loop(),
name="napcat_adapter.heartbeat_monitor",
)
async def stop(self) -> None:
"""停止当前心跳监测循环。"""
check_task = self._check_task
self._check_task = None
self._timeout_reported = False
self._last_heartbeat_at = 0.0
if check_task is not None:
check_task.cancel()
try:
await check_task
except asyncio.CancelledError:
pass
async def observe_meta_event(
self,
payload: Mapping[str, Any],
default_interval_sec: float,
) -> None:
"""根据 NapCat ``meta_event`` 更新心跳监测状态。
Args:
payload: NapCat 推送的元事件载荷。
default_interval_sec: 当前配置中的默认心跳间隔秒数。
"""
meta_event_type = str(payload.get("meta_event_type") or "").strip()
if meta_event_type == "lifecycle":
sub_type = str(payload.get("sub_type") or "").strip()
if sub_type == "connect":
await self.start(str(payload.get("self_id") or "").strip(), default_interval_sec)
return
if meta_event_type != "heartbeat":
return
self_id = str(payload.get("self_id") or "").strip()
interval_sec = self._resolve_interval_sec(payload, default_interval_sec)
status = payload.get("status", {})
if not isinstance(status, Mapping):
status = {}
is_online = bool(status.get("online", False))
is_good = bool(status.get("good", False))
await self.start(self_id, interval_sec)
self._interval_sec = interval_sec
if is_online and is_good:
self._touch()
return
if not is_online:
self._logger.error(f"NapCat 心跳显示 Bot {self._self_id or self_id or 'unknown'} 已离线")
elif not is_good:
self._logger.warning(f"NapCat 心跳显示 Bot {self._self_id or self_id or 'unknown'} 状态异常")
@staticmethod
def _resolve_interval_sec(payload: Mapping[str, Any], default_interval_sec: float) -> float:
"""解析心跳间隔秒数。
Args:
payload: NapCat 推送的元事件载荷。
default_interval_sec: 配置中的默认心跳间隔秒数。
Returns:
float: 规范化后的心跳间隔秒数。
"""
interval_ms = payload.get("interval")
if isinstance(interval_ms, (int, float)) and interval_ms > 0:
return max(float(interval_ms) / 1000.0, 1.0)
return max(float(default_interval_sec or 30.0), 1.0)
def _touch(self) -> None:
"""刷新最近一次心跳时间戳。"""
self._last_heartbeat_at = time.time()
self._timeout_reported = False
async def _check_loop(self) -> None:
"""持续检查心跳是否超时。"""
while True:
await asyncio.sleep(max(self._interval_sec, 1.0))
if self._last_heartbeat_at <= 0:
continue
elapsed_sec = time.time() - self._last_heartbeat_at
if elapsed_sec <= self._interval_sec * 2:
continue
if self._timeout_reported:
continue
self._timeout_reported = True
self._logger.error(f"Bot {self._self_id or 'unknown'} 可能发生了连接断开、被下线,或者 NapCat 心跳卡死")
try:
await self._on_timeout(self._self_id)
except asyncio.CancelledError:
raise
except Exception as exc:
self._logger.warning(f"NapCat 心跳超时回调执行失败: {exc}")

View File

@@ -0,0 +1,244 @@
"""内置 NapCat 适配器插件。
当前实现承担完整的 QQ / NapCat 消息网关职责:
1. 作为客户端连接 NapCat / OneBot v11 WebSocket 服务。
2. 将入站消息、通知事件与元事件转换为 Host 侧结构。
3. 将 Host 出站消息转换为 OneBot 动作并发送。
4. 通过公开 API 暴露 QQ 平台专属查询与管理动作。
"""
from __future__ import annotations
from typing import Any, ClassVar, Dict, Mapping, Optional, cast
from maibot_sdk import MaiBotPlugin, MessageGateway, PluginConfigBase
from .apis import (
NapCatAccountApiMixin,
NapCatFileApiMixin,
NapCatGroupApiMixin,
NapCatMessageApiMixin,
NapCatSystemApiMixin,
)
from .config import NapCatPluginSettings
from .constants import NAPCAT_GATEWAY_NAME
from .runtime import NapCatEventRouter, NapCatRuntimeBuilder, NapCatRuntimeBundle
from .services import NapCatActionService, NapCatQueryService
class NapCatAdapterPlugin(
NapCatAccountApiMixin,
NapCatFileApiMixin,
NapCatGroupApiMixin,
NapCatMessageApiMixin,
NapCatSystemApiMixin,
MaiBotPlugin,
):
"""NapCat 消息网关与 QQ 能力插件。"""
config_model: ClassVar[type[PluginConfigBase] | None] = NapCatPluginSettings
def __init__(self) -> None:
"""初始化 NapCat 适配器插件实例。"""
super().__init__()
self._action_service: Optional[NapCatActionService] = None
self._query_service: Optional[NapCatQueryService] = None
self._event_router: Optional[NapCatEventRouter] = None
self._runtime_bundle: Optional[NapCatRuntimeBundle] = None
async def on_load(self) -> None:
"""在插件加载时根据配置决定是否启动连接。"""
await self._restart_connection_if_needed()
async def on_unload(self) -> None:
"""在插件卸载时关闭连接。"""
await self._stop_connection()
async def on_config_update(self, scope: str, config_data: Dict[str, Any], version: str) -> None:
"""在配置更新后重载连接状态。
Args:
scope: 配置变更范围。
config_data: 最新的配置数据。
version: 配置版本号。
"""
if scope != "self":
return
self.set_plugin_config(config_data)
if version:
self.ctx.logger.debug(f"NapCat 适配器收到配置更新通知: {version}")
await self._restart_connection_if_needed()
@MessageGateway(
name=NAPCAT_GATEWAY_NAME,
route_type="duplex",
platform="qq",
protocol="napcat",
description="NapCat 正向 WebSocket 双工消息网关",
)
async def handle_napcat_gateway(
self,
message: Dict[str, Any],
route: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""处理 Host 出站消息并发送到 NapCat。
Args:
message: Host 侧标准 ``MessageDict``。
route: Platform IO 生成的路由信息。
metadata: Platform IO 附带的投递元数据。
**kwargs: 预留扩展参数。
Returns:
Dict[str, Any]: 标准化后的发送结果。
"""
del metadata
del kwargs
runtime_bundle = self._require_runtime_bundle()
try:
action_name, params = runtime_bundle.outbound_codec.build_outbound_action(message, route or {})
response = await runtime_bundle.transport.call_action(action_name, params)
except Exception as exc:
return {"success": False, "error": str(exc)}
if str(response.get("status", "")).lower() != "ok":
return {
"success": False,
"error": str(response.get("wording") or response.get("message") or "NapCat send failed"),
"metadata": {"retcode": response.get("retcode")},
}
response_data = response.get("data", {})
internal_message_id = str(message.get("message_id") or "").strip()
external_message_id = ""
if isinstance(response_data, Mapping):
external_message_id = str(response_data.get("message_id") or "")
adapter_callbacks = []
if internal_message_id and external_message_id and internal_message_id != external_message_id:
adapter_callbacks.append(
{
"name": "message_id_echo",
"payload": {
"content": {
"type": "echo",
"echo": internal_message_id,
"actual_id": external_message_id,
}
},
}
)
return {
"success": True,
"external_message_id": external_message_id or None,
"metadata": {
"action": action_name,
"adapter_callbacks": adapter_callbacks,
},
}
def _ensure_runtime_components(self) -> None:
"""确保运行时依赖对象已经完成初始化。"""
if self._event_router is None:
self._event_router = NapCatEventRouter(
gateway_capability=self.ctx.gateway,
logger=self.ctx.logger,
gateway_name=NAPCAT_GATEWAY_NAME,
load_settings=self._load_settings,
)
if self._runtime_bundle is None:
runtime_builder = NapCatRuntimeBuilder(
gateway_capability=self.ctx.gateway,
logger=self.ctx.logger,
gateway_name=NAPCAT_GATEWAY_NAME,
)
self._runtime_bundle = runtime_builder.build(
on_connection_opened=self._event_router.bootstrap_adapter_runtime_state,
on_connection_closed=self._event_router.handle_transport_disconnected,
on_payload=self._event_router.handle_transport_payload,
on_natural_lift=self._event_router.emit_natural_lift_notice,
on_heartbeat_timeout=self._event_router.handle_heartbeat_timeout,
)
self._event_router.bind_runtime(self._runtime_bundle)
self._bind_runtime_aliases(self._runtime_bundle)
def _bind_runtime_aliases(self, runtime_bundle: NapCatRuntimeBundle) -> None:
"""同步运行时组件到插件级别的快捷引用。
Args:
runtime_bundle: 已初始化的运行时组件集合。
"""
self._action_service = runtime_bundle.action_service
self._query_service = runtime_bundle.query_service
def _load_settings(self) -> NapCatPluginSettings:
"""返回当前生效的插件配置。
Returns:
NapCatPluginSettings: 当前生效的插件配置。
"""
return cast(NapCatPluginSettings, self.config)
async def _restart_connection_if_needed(self) -> None:
"""根据当前配置重启连接循环。"""
self._ensure_runtime_components()
runtime_bundle = self._require_runtime_bundle()
settings = self._load_settings()
await self._stop_connection()
if not settings.should_connect():
self.ctx.logger.info("NapCat 适配器保持空闲状态,因为插件或配置未启用")
return
if not settings.validate_runtime_config(self.ctx.logger):
return
if not runtime_bundle.transport.is_available():
self.ctx.logger.error("NapCat 适配器依赖 aiohttp但当前环境未安装该依赖")
return
if not settings.chat.enable_chat_list_filter:
self.ctx.logger.info(
"NapCat 聊天名单过滤已关闭:将忽略 group_list 与 private_list仅保留 ban_user_id 和官方机器人屏蔽规则"
)
runtime_bundle.transport.configure(settings.napcat_server)
await runtime_bundle.transport.start()
async def _stop_connection(self) -> None:
"""停止当前连接并清理运行时缓存。"""
runtime_bundle = self._runtime_bundle
if runtime_bundle is None:
return
await runtime_bundle.transport.stop()
if self._event_router is not None:
self._event_router.reset_caches()
def _require_runtime_bundle(self) -> NapCatRuntimeBundle:
"""返回当前已初始化的运行时组件集合。
Returns:
NapCatRuntimeBundle: 当前运行时组件集合。
Raises:
RuntimeError: 当运行时尚未初始化时抛出。
"""
self._ensure_runtime_components()
runtime_bundle = self._runtime_bundle
if runtime_bundle is None:
raise RuntimeError("NapCat 运行时尚未初始化")
return runtime_bundle
def create_plugin() -> NapCatAdapterPlugin:
"""创建插件实例。
Returns:
NapCatAdapterPlugin: NapCat 内置适配器插件实例。
"""
return NapCatAdapterPlugin()

View File

@@ -0,0 +1,226 @@
"""QQ 原生表情映射表。"""
from typing import Dict
QQ_FACE: Dict[str, str] = {
"0": "[表情:惊讶]",
"1": "[表情:撇嘴]",
"2": "[表情:色]",
"3": "[表情:发呆]",
"4": "[表情:得意]",
"5": "[表情:流泪]",
"6": "[表情:害羞]",
"7": "[表情:闭嘴]",
"8": "[表情:睡]",
"9": "[表情:大哭]",
"10": "[表情:尴尬]",
"11": "[表情:发怒]",
"12": "[表情:调皮]",
"13": "[表情:呲牙]",
"14": "[表情:微笑]",
"15": "[表情:难过]",
"16": "[表情:酷]",
"18": "[表情:抓狂]",
"19": "[表情:吐]",
"20": "[表情:偷笑]",
"21": "[表情:可爱]",
"22": "[表情:白眼]",
"23": "[表情:傲慢]",
"24": "[表情:饥饿]",
"25": "[表情:困]",
"26": "[表情:惊恐]",
"27": "[表情:流汗]",
"28": "[表情:憨笑]",
"29": "[表情:悠闲]",
"30": "[表情:奋斗]",
"31": "[表情:咒骂]",
"32": "[表情:疑问]",
"33": "[表情:嘘]",
"34": "[表情:晕]",
"35": "[表情:折磨]",
"36": "[表情:衰]",
"37": "[表情:骷髅]",
"38": "[表情:敲打]",
"39": "[表情:再见]",
"41": "[表情:发抖]",
"42": "[表情:爱情]",
"43": "[表情:跳跳]",
"46": "[表情:猪头]",
"49": "[表情:拥抱]",
"53": "[表情:蛋糕]",
"56": "[表情:刀]",
"59": "[表情:便便]",
"60": "[表情:咖啡]",
"63": "[表情:玫瑰]",
"64": "[表情:凋谢]",
"66": "[表情:爱心]",
"67": "[表情:心碎]",
"74": "[表情:太阳]",
"75": "[表情:月亮]",
"76": "[表情:赞]",
"77": "[表情:踩]",
"78": "[表情:握手]",
"79": "[表情:胜利]",
"85": "[表情:飞吻]",
"86": "[表情:怄火]",
"89": "[表情:西瓜]",
"96": "[表情:冷汗]",
"97": "[表情:擦汗]",
"98": "[表情:抠鼻]",
"99": "[表情:鼓掌]",
"100": "[表情:糗大了]",
"101": "[表情:坏笑]",
"102": "[表情:左哼哼]",
"103": "[表情:右哼哼]",
"104": "[表情:哈欠]",
"105": "[表情:鄙视]",
"106": "[表情:委屈]",
"107": "[表情:快哭了]",
"108": "[表情:阴险]",
"109": "[表情:左亲亲]",
"110": "[表情:吓]",
"111": "[表情:可怜]",
"112": "[表情:菜刀]",
"114": "[表情:篮球]",
"116": "[表情:示爱]",
"118": "[表情:抱拳]",
"119": "[表情:勾引]",
"120": "[表情:拳头]",
"121": "[表情:差劲]",
"123": "[表情NO]",
"124": "[表情OK]",
"125": "[表情:转圈]",
"129": "[表情:挥手]",
"137": "[表情:鞭炮]",
"144": "[表情:喝彩]",
"146": "[表情:爆筋]",
"147": "[表情:棒棒糖]",
"169": "[表情:手枪]",
"171": "[表情:茶]",
"172": "[表情:眨眼睛]",
"173": "[表情:泪奔]",
"174": "[表情:无奈]",
"175": "[表情:卖萌]",
"176": "[表情:小纠结]",
"177": "[表情:喷血]",
"178": "[表情:斜眼笑]",
"179": "[表情doge]",
"181": "[表情:戳一戳]",
"182": "[表情:笑哭]",
"183": "[表情:我最美]",
"185": "[表情:羊驼]",
"187": "[表情:幽灵]",
"201": "[表情:点赞]",
"212": "[表情:托腮]",
"262": "[表情:脑阔疼]",
"263": "[表情:沧桑]",
"264": "[表情:捂脸]",
"265": "[表情:辣眼睛]",
"266": "[表情:哦哟]",
"267": "[表情:头秃]",
"268": "[表情:问号脸]",
"269": "[表情:暗中观察]",
"270": "[表情emm]",
"271": "[表情:吃瓜]",
"272": "[表情:呵呵哒]",
"273": "[表情:我酸了]",
"277": "[表情:汪汪]",
"281": "[表情:无眼笑]",
"282": "[表情:敬礼]",
"283": "[表情:狂笑]",
"284": "[表情:面无表情]",
"285": "[表情:摸鱼]",
"286": "[表情:魔鬼笑]",
"287": "[表情:哦]",
"289": "[表情:睁眼]",
"293": "[表情:摸锦鲤]",
"294": "[表情:期待]",
"295": "[表情:拿到红包]",
"297": "[表情:拜谢]",
"298": "[表情:元宝]",
"299": "[表情:牛啊]",
"300": "[表情:胖三斤]",
"302": "[表情:左拜年]",
"303": "[表情:右拜年]",
"305": "[表情:右亲亲]",
"306": "[表情:牛气冲天]",
"307": "[表情:喵喵]",
"311": "[表情打call]",
"312": "[表情:变形]",
"314": "[表情:仔细分析]",
"317": "[表情:菜汪]",
"318": "[表情:崇拜]",
"319": "[表情:比心]",
"320": "[表情:庆祝]",
"323": "[表情:嫌弃]",
"324": "[表情:吃糖]",
"325": "[表情:惊吓]",
"326": "[表情:生气]",
"332": "[表情:举牌牌]",
"333": "[表情:烟花]",
"334": "[表情:虎虎生威]",
"336": "[表情:豹富]",
"337": "[表情:花朵脸]",
"338": "[表情:我想开了]",
"339": "[表情:舔屏]",
"341": "[表情:打招呼]",
"342": "[表情酸Q]",
"343": "[表情:我方了]",
"344": "[表情:大怨种]",
"345": "[表情:红包多多]",
"346": "[表情:你真棒棒]",
"347": "[表情:大展宏兔]",
"349": "[表情:坚强]",
"350": "[表情:贴贴]",
"351": "[表情:敲敲]",
"352": "[表情:咦]",
"353": "[表情:拜托]",
"354": "[表情:尊嘟假嘟]",
"355": "[表情:耶]",
"356": "[表情666]",
"357": "[表情:裂开]",
"392": "[表情:龙年快乐]",
"393": "[表情:新年中龙]",
"394": "[表情:新年大龙]",
"395": "[表情:略略略]",
"9786": "[表情:可爱]",
"10024": "[表情:闪光]",
"9749": "[表情:咖啡]",
"127801": "[表情:玫瑰]",
"127817": "[表情:西瓜]",
"127822": "[表情:苹果]",
"127827": "[表情:草莓]",
"127836": "[表情:拉面]",
"127838": "[表情:面包]",
"127847": "[表情:刨冰]",
"127866": "[表情:啤酒]",
"127867": "[表情:干杯]",
"127881": "[表情:庆祝]",
"128074": "[表情:拳头]",
"128076": "[表情:好的]",
"128077": "[表情:厉害]",
"128079": "[表情:鼓掌]",
"128157": "[表情:礼物]",
"128164": "[表情:睡觉]",
"128166": "[表情:水]",
"128168": "[表情:吹气]",
"128170": "[表情:肌肉]",
"128293": "[表情:火]",
"128513": "[表情:呲牙]",
"128514": "[表情:激动]",
"128516": "[表情:高兴]",
"128522": "[表情:嘿嘿]",
"128524": "[表情:羞涩]",
"128527": "[表情:哼哼]",
"128530": "[表情:不屑]",
"128531": "[表情:汗]",
"128532": "[表情:失落]",
"128536": "[表情:飞吻]",
"128538": "[表情:亲亲]",
"128540": "[表情:淘气]",
"128541": "[表情:吐舌]",
"128557": "[表情:大哭]",
"128560": "[表情:紧张]",
"128563": "[表情:瞪眼]",
}

View File

@@ -0,0 +1,7 @@
"""NapCat 运行时组件导出。"""
from .builder import NapCatRuntimeBuilder
from .bundle import NapCatRuntimeBundle
from .router import NapCatEventRouter
__all__ = ["NapCatEventRouter", "NapCatRuntimeBuilder", "NapCatRuntimeBundle"]

View File

@@ -0,0 +1,105 @@
"""NapCat 运行时组件构建器。"""
from __future__ import annotations
from typing import Any, Awaitable, Callable, Coroutine
from ..codecs.inbound import NapCatInboundCodec
from ..codecs.notice import NapCatNoticeCodec
from ..codecs.outbound import NapCatOutboundCodec
from ..filters import NapCatChatFilter
from ..heartbeat_monitor import NapCatHeartbeatMonitor
from ..runtime_state import NapCatRuntimeStateManager
from ..services import (
NapCatActionService,
NapCatBanStateStore,
NapCatBanTracker,
NapCatHistoryRecoveryStore,
NapCatOfficialBotGuard,
NapCatQueryService,
)
from ..transport import NapCatTransportClient
from .bundle import NapCatRuntimeBundle
class NapCatRuntimeBuilder:
"""按固定依赖图构建 NapCat 运行时组件。"""
def __init__(self, gateway_capability: Any, logger: Any, gateway_name: str) -> None:
"""初始化运行时构建器。
Args:
gateway_capability: SDK 提供的消息网关能力对象。
logger: 插件日志对象。
gateway_name: 当前消息网关名称。
"""
self._gateway_capability = gateway_capability
self._logger = logger
self._gateway_name = gateway_name
def build(
self,
on_connection_opened: Callable[[], Coroutine[Any, Any, None]],
on_connection_closed: Callable[[], Coroutine[Any, Any, None]],
on_payload: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
on_natural_lift: Callable[[dict[str, Any]], Awaitable[None]],
on_heartbeat_timeout: Callable[[str], Awaitable[None]],
) -> NapCatRuntimeBundle:
"""创建一套完整的运行时组件。
Args:
on_connection_opened: 连接建立回调。
on_connection_closed: 连接断开回调。
on_payload: 非 echo 载荷回调。
on_natural_lift: 自然解除禁言回调。
on_heartbeat_timeout: 心跳超时回调。
Returns:
NapCatRuntimeBundle: 已完成依赖注入的运行时组件集合。
"""
chat_filter = NapCatChatFilter(self._logger)
transport = NapCatTransportClient(
logger=self._logger,
on_connection_opened=on_connection_opened,
on_connection_closed=on_connection_closed,
on_payload=on_payload,
)
action_service = NapCatActionService(self._logger, transport)
query_service = NapCatQueryService(action_service, self._logger)
ban_state_store = NapCatBanStateStore(self._logger)
history_recovery_store = NapCatHistoryRecoveryStore(self._logger)
inbound_codec = NapCatInboundCodec(self._logger, query_service)
notice_codec = NapCatNoticeCodec(self._logger, query_service)
runtime_state = NapCatRuntimeStateManager(
gateway_capability=self._gateway_capability,
logger=self._logger,
gateway_name=self._gateway_name,
)
ban_tracker = NapCatBanTracker(
logger=self._logger,
query_service=query_service,
on_natural_lift=on_natural_lift,
state_store=ban_state_store,
)
heartbeat_monitor = NapCatHeartbeatMonitor(
logger=self._logger,
on_timeout=on_heartbeat_timeout,
)
official_bot_guard = NapCatOfficialBotGuard(self._logger, query_service)
outbound_codec = NapCatOutboundCodec()
return NapCatRuntimeBundle(
action_service=action_service,
ban_state_store=ban_state_store,
ban_tracker=ban_tracker,
chat_filter=chat_filter,
heartbeat_monitor=heartbeat_monitor,
history_recovery_store=history_recovery_store,
inbound_codec=inbound_codec,
notice_codec=notice_codec,
official_bot_guard=official_bot_guard,
outbound_codec=outbound_codec,
query_service=query_service,
runtime_state=runtime_state,
transport=transport,
)

View File

@@ -0,0 +1,40 @@
"""NapCat 运行时组件容器。"""
from __future__ import annotations
from dataclasses import dataclass
from ..codecs.inbound import NapCatInboundCodec
from ..codecs.notice import NapCatNoticeCodec
from ..codecs.outbound import NapCatOutboundCodec
from ..filters import NapCatChatFilter
from ..heartbeat_monitor import NapCatHeartbeatMonitor
from ..runtime_state import NapCatRuntimeStateManager
from ..services import (
NapCatActionService,
NapCatBanStateStore,
NapCatBanTracker,
NapCatHistoryRecoveryStore,
NapCatOfficialBotGuard,
NapCatQueryService,
)
from ..transport import NapCatTransportClient
@dataclass
class NapCatRuntimeBundle:
"""NapCat 运行时依赖集合。"""
action_service: NapCatActionService
ban_state_store: NapCatBanStateStore
ban_tracker: NapCatBanTracker
chat_filter: NapCatChatFilter
heartbeat_monitor: NapCatHeartbeatMonitor
history_recovery_store: NapCatHistoryRecoveryStore
inbound_codec: NapCatInboundCodec
notice_codec: NapCatNoticeCodec
official_bot_guard: NapCatOfficialBotGuard
outbound_codec: NapCatOutboundCodec
query_service: NapCatQueryService
runtime_state: NapCatRuntimeStateManager
transport: NapCatTransportClient

View File

@@ -0,0 +1,611 @@
"""NapCat 事件路由协调器。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Dict, Mapping, Optional, Protocol
import asyncio
from ..config import NapCatPluginSettings
from ..constants import DEFAULT_HISTORY_RECOVERY_BATCH_SIZE, DEFAULT_HISTORY_RECOVERY_CHECKPOINT_LIMIT
from ..services import NapCatChatCheckpoint
from ..types import NapCatPayloadDict
from .bundle import NapCatRuntimeBundle
class _GatewayCapabilityProtocol(Protocol):
"""插件网关能力协议。"""
async def route_message(
self,
gateway_name: str,
message: Dict[str, Any],
*,
route_metadata: Optional[Dict[str, Any]] = None,
external_message_id: str = "",
dedupe_key: str = "",
) -> bool:
"""向 Host 注入一条消息。"""
...
@dataclass(frozen=True)
class _NapCatChatIdentity:
"""描述一条 NapCat 消息所属的会话身份。"""
chat_type: str
chat_id: str
class NapCatEventRouter:
"""协调 NapCat 运行时组件处理各类平台事件。"""
def __init__(
self,
gateway_capability: _GatewayCapabilityProtocol,
logger: Any,
gateway_name: str,
load_settings: Callable[[], NapCatPluginSettings],
) -> None:
"""初始化事件路由器。
Args:
gateway_capability: SDK 提供的消息网关能力对象。
logger: 插件日志对象。
gateway_name: 当前消息网关名称。
load_settings: 返回当前生效插件配置的回调。
"""
self._gateway_capability = gateway_capability
self._logger = logger
self._gateway_name = gateway_name
self._load_settings = load_settings
self._runtime: Optional[NapCatRuntimeBundle] = None
self._recovery_task: Optional[asyncio.Task[None]] = None
def bind_runtime(self, runtime: NapCatRuntimeBundle) -> None:
"""绑定当前路由器使用的运行时依赖。
Args:
runtime: 已初始化的运行时组件集合。
"""
self._runtime = runtime
def reset_caches(self) -> None:
"""重置与路由相关的短期缓存。"""
runtime = self._runtime
if runtime is None:
return
self._cancel_recovery_task()
runtime.official_bot_guard.clear_cache()
async def handle_transport_payload(self, payload: NapCatPayloadDict) -> None:
"""处理来自传输层的非 echo 载荷。
Args:
payload: NapCat 推送的原始事件数据。
"""
post_type = str(payload.get("post_type") or "").strip()
if post_type == "message":
await self.handle_inbound_message(payload)
return
if post_type == "notice":
await self.handle_notice_event(payload)
return
if post_type == "meta_event":
await self.handle_meta_event(payload)
async def handle_inbound_message(self, payload: NapCatPayloadDict) -> bool:
"""处理单条 NapCat 入站消息并注入 Host。
Args:
payload: NapCat / OneBot 推送的原始消息事件。
"""
runtime = self._require_runtime()
settings = self._load_settings()
self_id = str(payload.get("self_id") or "").strip()
if self_id:
await runtime.runtime_state.report_connected(self_id, settings.napcat_server)
sender = payload.get("sender", {})
if not isinstance(sender, Mapping):
sender = {}
sender_user_id = str(payload.get("user_id") or sender.get("user_id") or "").strip()
if not sender_user_id:
return False
group_id = str(payload.get("group_id") or "").strip()
if self_id and sender_user_id == self_id and settings.filters.ignore_self_message:
return False
if not runtime.chat_filter.is_inbound_chat_allowed(sender_user_id, group_id, settings.chat):
return False
if await runtime.official_bot_guard.should_reject(
sender_user_id=sender_user_id,
group_id=group_id,
ban_qq_bot=settings.chat.ban_qq_bot,
):
return False
try:
message_dict = await runtime.inbound_codec.build_message_dict(payload, self_id, sender_user_id, sender)
except ValueError as exc:
self._logger.warning(f"NapCat 入站消息格式不受支持,已丢弃: {exc}")
return False
route_metadata = self._build_route_metadata(self_id, settings.napcat_server.connection_id)
external_message_id = str(payload.get("message_id") or "").strip()
accepted = await self._gateway_capability.route_message(
gateway_name=self._gateway_name,
message=message_dict,
route_metadata=route_metadata,
external_message_id=external_message_id,
dedupe_key=external_message_id,
)
if not accepted:
self._logger.debug(f"Host 丢弃了 NapCat 入站消息: {external_message_id or '无消息 ID'}")
return False
await self._record_inbound_checkpoint(
payload=payload,
self_id=self_id,
external_message_id=external_message_id or str(message_dict.get("message_id") or "").strip(),
scope=settings.napcat_server.connection_id,
)
return True
async def handle_notice_event(self, payload: NapCatPayloadDict) -> None:
"""处理 NapCat ``notice`` 事件并注入 Host。
Args:
payload: NapCat 推送的通知事件。
"""
runtime = self._require_runtime()
settings = self._load_settings()
self_id = str(payload.get("self_id") or "").strip()
if self_id:
await runtime.runtime_state.report_connected(self_id, settings.napcat_server)
await runtime.ban_tracker.record_notice(payload)
await self.route_notice_payload(payload, self_id, settings.napcat_server.connection_id)
async def route_notice_payload(
self,
payload: NapCatPayloadDict,
self_id: str,
connection_id: str,
) -> None:
"""将单条通知载荷转换并注入 Host。
Args:
payload: NapCat 通知载荷。
self_id: 当前机器人账号 ID。
connection_id: 当前连接标识。
"""
runtime = self._require_runtime()
message_dict = await runtime.notice_codec.build_notice_message_dict(payload)
if message_dict is None:
return
route_metadata = self._build_route_metadata(self_id, connection_id)
external_message_id = str(payload.get("message_id") or "").strip()
dedupe_key = runtime.notice_codec.build_notice_dedupe_key(payload) or ""
accepted = await self._gateway_capability.route_message(
gateway_name=self._gateway_name,
message=message_dict,
route_metadata=route_metadata,
external_message_id=external_message_id,
dedupe_key=dedupe_key,
)
if not accepted:
self._logger.debug(f"Host 丢弃了 NapCat 通知事件: {external_message_id or dedupe_key or '无消息 ID'}")
async def emit_natural_lift_notice(self, payload: NapCatPayloadDict) -> None:
"""注入一条由适配器合成的自然解除禁言通知。
Args:
payload: 合成后的 NapCat 通知载荷。
"""
settings = self._load_settings()
self_id = str(payload.get("self_id") or "").strip()
await self.route_notice_payload(payload, self_id, settings.napcat_server.connection_id)
async def handle_meta_event(self, payload: NapCatPayloadDict) -> None:
"""处理 NapCat ``meta_event`` 事件。
Args:
payload: NapCat 推送的元事件。
"""
runtime = self._require_runtime()
settings = self._load_settings()
meta_event_type = str(payload.get("meta_event_type") or "").strip()
self_id = str(payload.get("self_id") or "").strip()
should_report_connected = False
if meta_event_type == "lifecycle":
should_report_connected = str(payload.get("sub_type") or "").strip() == "connect"
elif meta_event_type == "heartbeat":
status = payload.get("status", {})
if not isinstance(status, Mapping):
status = {}
should_report_connected = bool(status.get("online", False)) and bool(status.get("good", False))
if self_id and should_report_connected:
await runtime.runtime_state.report_connected(self_id, settings.napcat_server)
elif meta_event_type == "heartbeat" and not should_report_connected:
await runtime.runtime_state.report_disconnected()
await runtime.heartbeat_monitor.observe_meta_event(payload, settings.napcat_server.heartbeat_interval)
await runtime.notice_codec.handle_meta_event(payload)
async def bootstrap_adapter_runtime_state(self) -> None:
"""在连接建立后主动获取账号信息并激活消息网关路由。"""
runtime = self._require_runtime()
settings = self._load_settings()
max_attempts = 3
last_error: Optional[Exception] = None
for attempt in range(1, max_attempts + 1):
try:
login_info = await runtime.query_service.get_login_info()
self_id = self._extract_self_id_from_login_response(login_info)
await runtime.runtime_state.report_connected(self_id, settings.napcat_server)
await runtime.heartbeat_monitor.start(self_id, settings.napcat_server.heartbeat_interval)
await runtime.ban_tracker.start()
await runtime.history_recovery_store.load()
self._schedule_history_recovery(self_id=self_id, scope=settings.napcat_server.connection_id)
return
except asyncio.CancelledError:
raise
except Exception as exc:
last_error = exc
self._logger.warning(f"NapCat 消息网关获取登录信息失败,第 {attempt}/{max_attempts} 次重试: {exc}")
if attempt < max_attempts:
await asyncio.sleep(1.0)
if last_error is not None:
self._logger.error(f"NapCat 消息网关未能完成路由激活,连接将保持只接收状态: {last_error}")
async def handle_transport_disconnected(self) -> None:
"""处理传输层断开事件。"""
runtime = self._require_runtime()
await runtime.heartbeat_monitor.stop()
await runtime.ban_tracker.stop()
self.reset_caches()
await runtime.runtime_state.report_disconnected()
async def handle_heartbeat_timeout(self, self_id: str) -> None:
"""处理 NapCat 心跳长时间未更新的情况。
Args:
self_id: 当前机器人账号 ID。
"""
runtime = self._require_runtime()
if self_id:
self._logger.warning(f"NapCat Bot {self_id} 心跳超时,暂时将消息网关标记为未就绪")
else:
self._logger.warning("NapCat 心跳超时,暂时将消息网关标记为未就绪")
await runtime.runtime_state.report_disconnected()
def _require_runtime(self) -> NapCatRuntimeBundle:
"""返回当前已绑定的运行时依赖。
Returns:
NapCatRuntimeBundle: 已初始化的运行时依赖。
Raises:
RuntimeError: 当运行时尚未绑定时抛出。
"""
runtime = self._runtime
if runtime is None:
raise RuntimeError("NapCat 运行时尚未初始化")
return runtime
def _schedule_history_recovery(self, self_id: str, scope: str) -> None:
"""在连接恢复后调度一次历史补拉任务。"""
self._cancel_recovery_task()
runtime = self._runtime
if runtime is None:
return
self._recovery_task = asyncio.create_task(
self._recover_recent_history(self_id=self_id, scope=scope),
name="napcat_adapter.history_recovery",
)
def _cancel_recovery_task(self) -> None:
"""取消当前仍在运行的历史补拉任务。"""
recovery_task = self._recovery_task
self._recovery_task = None
if recovery_task is not None and not recovery_task.done():
recovery_task.cancel()
async def _recover_recent_history(self, *, self_id: str, scope: str) -> None:
"""按 checkpoint 列表逐个尝试补拉断线期间遗漏的消息。"""
runtime = self._require_runtime()
checkpoints = await runtime.history_recovery_store.list_checkpoints(
self_id,
scope=scope,
limit=DEFAULT_HISTORY_RECOVERY_CHECKPOINT_LIMIT,
)
if not checkpoints:
return
recovered_count = 0
for checkpoint in checkpoints:
recovered_count += await self._recover_chat_history_from_checkpoint(
self_id=self_id,
scope=scope,
checkpoint=checkpoint,
)
if recovered_count > 0:
self._logger.info(f"NapCat 历史补拉完成,共补回 {recovered_count} 条消息")
async def _recover_chat_history_from_checkpoint(
self,
*,
self_id: str,
scope: str,
checkpoint: NapCatChatCheckpoint,
) -> int:
"""针对单个会话执行一次小批量历史补拉。"""
runtime = self._require_runtime()
history_messages = await self._query_history_messages(checkpoint, limit=DEFAULT_HISTORY_RECOVERY_BATCH_SIZE)
if not history_messages:
return 0
ordered_messages = sorted(
history_messages,
key=lambda item: (
self._extract_message_timestamp(item),
self._extract_message_seq(item),
str(item.get("message_id") or "").strip(),
),
)
recovered_count = 0
for history_payload in ordered_messages:
external_message_id = str(history_payload.get("message_id") or "").strip()
if not external_message_id:
continue
if external_message_id == checkpoint.last_message_id:
continue
if await runtime.history_recovery_store.has_recovered_message_seen(
account_id=self_id,
scope=scope,
chat_type=checkpoint.chat_type,
chat_id=checkpoint.chat_id,
external_message_id=external_message_id,
):
continue
if not self._is_message_after_checkpoint(history_payload, checkpoint):
continue
accepted = await self._reinject_history_payload(history_payload, self_id=self_id)
if not accepted:
continue
await runtime.history_recovery_store.mark_recovered_message_seen(
account_id=self_id,
scope=scope,
chat_type=checkpoint.chat_type,
chat_id=checkpoint.chat_id,
external_message_id=external_message_id,
)
recovered_count += 1
return recovered_count
async def _query_history_messages(
self,
checkpoint: NapCatChatCheckpoint,
*,
limit: int,
) -> list[NapCatPayloadDict]:
"""查询某个会话在 checkpoint 之后的一小批历史消息。"""
runtime = self._require_runtime()
payload_collections: list[list[NapCatPayloadDict]] = []
if checkpoint.last_message_seq is not None:
payload_collections.append(
await self._fetch_history_messages(
chat_type=checkpoint.chat_type,
chat_id=checkpoint.chat_id,
message_seq=checkpoint.last_message_seq,
limit=limit,
)
)
payload_collections.append(
await self._fetch_history_messages(
chat_type=checkpoint.chat_type,
chat_id=checkpoint.chat_id,
message_seq=None,
limit=limit,
)
)
merged_payloads: list[NapCatPayloadDict] = []
seen_message_ids: set[str] = set()
for payloads in payload_collections:
for payload in payloads:
external_message_id = str(payload.get("message_id") or "").strip()
dedupe_key = external_message_id or repr(sorted(payload.items()))
if dedupe_key in seen_message_ids:
continue
seen_message_ids.add(dedupe_key)
merged_payloads.append(payload)
return merged_payloads
async def _fetch_history_messages(
self,
*,
chat_type: str,
chat_id: str,
message_seq: int | None,
limit: int,
) -> list[NapCatPayloadDict]:
"""调用查询服务获取一批历史消息。"""
runtime = self._require_runtime()
if chat_type == "group":
history_payloads = await runtime.query_service.get_group_message_history(
chat_id,
message_seq=message_seq,
count=limit,
reverse_order=False,
)
elif chat_type == "private":
history_payloads = await runtime.query_service.get_friend_message_history(
chat_id,
message_seq=message_seq,
count=limit,
reverse_order=False,
)
else:
return []
if history_payloads is None:
return []
return [dict(payload) for payload in history_payloads if isinstance(payload, Mapping)]
async def _reinject_history_payload(self, payload: NapCatPayloadDict, *, self_id: str) -> bool:
"""将补拉到的历史消息重新送回实时入站路径。"""
try:
normalized_payload = dict(payload)
if self_id and not str(normalized_payload.get("self_id") or "").strip():
normalized_payload["self_id"] = self_id
return await self.handle_inbound_message(normalized_payload)
except asyncio.CancelledError:
raise
except Exception as exc:
external_message_id = str(payload.get("message_id") or "").strip() or "unknown"
self._logger.warning(f"NapCat 历史消息补拉注入失败: message_id={external_message_id} error={exc}")
return False
async def _record_inbound_checkpoint(
self,
*,
payload: NapCatPayloadDict,
self_id: str,
external_message_id: str,
scope: str,
) -> None:
"""在消息被 Host 接受后更新该会话的最新 checkpoint。"""
runtime = self._require_runtime()
chat_identity = self._extract_chat_identity(payload)
if chat_identity is None:
return
await runtime.history_recovery_store.record_checkpoint(
account_id=self_id,
scope=scope,
chat_type=chat_identity.chat_type,
chat_id=chat_identity.chat_id,
message_id=external_message_id,
message_time=self._extract_message_timestamp(payload),
message_seq=self._extract_message_seq(payload),
)
@staticmethod
def _extract_chat_identity(payload: Mapping[str, Any]) -> _NapCatChatIdentity | None:
"""从 NapCat 载荷中提取会话身份。"""
group_id = str(payload.get("group_id") or "").strip()
user_id = str(payload.get("user_id") or "").strip()
if group_id:
return _NapCatChatIdentity(chat_type="group", chat_id=group_id)
if user_id:
return _NapCatChatIdentity(chat_type="private", chat_id=user_id)
return None
@staticmethod
def _extract_message_seq(payload: Mapping[str, Any]) -> int | None:
"""从 NapCat 载荷中提取历史接口可复用的消息序号。"""
for field_name in ("message_seq", "messageSeq", "msg_seq"):
raw_value = payload.get(field_name)
if raw_value is None or str(raw_value).strip() == "":
continue
try:
return int(raw_value)
except (TypeError, ValueError):
continue
return None
@staticmethod
def _extract_message_timestamp(payload: Mapping[str, Any]) -> float:
"""从 NapCat 载荷中提取消息时间戳。"""
raw_timestamp = payload.get("time")
if isinstance(raw_timestamp, (int, float)):
return float(raw_timestamp)
return 0.0
@classmethod
def _is_message_after_checkpoint(
cls,
payload: Mapping[str, Any],
checkpoint: NapCatChatCheckpoint,
) -> bool:
"""判断历史消息是否位于 checkpoint 之后。"""
payload_message_id = str(payload.get("message_id") or "").strip()
if payload_message_id == checkpoint.last_message_id:
return False
payload_message_seq = cls._extract_message_seq(payload)
if payload_message_seq is not None and checkpoint.last_message_seq is not None:
return payload_message_seq > checkpoint.last_message_seq
payload_timestamp = cls._extract_message_timestamp(payload)
if payload_timestamp != checkpoint.last_message_time:
return payload_timestamp > checkpoint.last_message_time
return True
@staticmethod
def _build_route_metadata(self_id: str, connection_id: str) -> Dict[str, Any]:
"""构造注入 Host 时使用的路由元数据。
Args:
self_id: 当前机器人账号 ID。
connection_id: 当前连接标识。
Returns:
Dict[str, Any]: 路由元数据字典。
"""
route_metadata: Dict[str, Any] = {}
if self_id:
route_metadata["self_id"] = self_id
if connection_id:
route_metadata["connection_id"] = connection_id
return route_metadata
@staticmethod
def _extract_self_id_from_login_response(response: Optional[Dict[str, Any]]) -> str:
"""从 ``get_login_info`` 查询结果中提取当前账号 ID。
Args:
response: NapCat 返回的登录信息字典。
Returns:
str: 规范化后的账号 ID 字符串。
Raises:
ValueError: 当响应中缺少有效账号 ID 时抛出。
"""
if not isinstance(response, Mapping):
raise ValueError("get_login_info 响应缺少 data 字段")
self_id = str(response.get("user_id") or "").strip()
if not self_id:
raise ValueError("get_login_info 响应缺少有效的 user_id")
return self_id

View File

@@ -0,0 +1,118 @@
"""NapCat 消息网关运行时状态管理。"""
from typing import Any, Optional, Protocol
from .config import NapCatServerConfig
class _GatewayCapabilityProtocol(Protocol):
"""消息网关能力代理协议。"""
async def update_state(
self,
gateway_name: str,
*,
ready: bool,
platform: str = "",
account_id: str = "",
scope: str = "",
metadata: dict[str, Any] | None = None,
) -> bool:
"""向 Host 上报消息网关运行时状态。"""
...
class NapCatRuntimeStateManager:
"""NapCat 消息网关路由状态上报器。"""
def __init__(
self,
gateway_capability: _GatewayCapabilityProtocol,
logger: Any,
gateway_name: str,
) -> None:
"""初始化运行时状态管理器。
Args:
gateway_capability: SDK 提供的消息网关能力对象。
logger: 插件日志对象。
gateway_name: 当前 NapCat 消息网关组件名称。
"""
self._gateway_capability = gateway_capability
self._gateway_name = gateway_name
self._logger = logger
self._runtime_state_connected: bool = False
self._reported_account_id: Optional[str] = None
self._reported_scope: Optional[str] = None
async def report_connected(self, account_id: str, server_config: NapCatServerConfig) -> bool:
"""向 Host 上报当前消息网关连接已就绪。
Args:
account_id: 当前 NapCat 连接对应的机器人账号 ID。
server_config: 当前生效的 NapCat 服务端配置。
Returns:
bool: 若 Host 接受了运行时状态更新,则返回 ``True``。
"""
normalized_account_id = str(account_id).strip()
if not normalized_account_id:
return False
scope = server_config.connection_id or None
if (
self._runtime_state_connected
and self._reported_account_id == normalized_account_id
and self._reported_scope == scope
):
return True
accepted = False
try:
accepted = await self._gateway_capability.update_state(
gateway_name=self._gateway_name,
ready=True,
platform="qq",
account_id=normalized_account_id,
scope=server_config.connection_id,
metadata={"ws_url": server_config.build_ws_url()},
)
except Exception as exc:
self._logger.warning(f"NapCat 消息网关上报连接就绪状态失败: {exc}")
return False
if not accepted:
self._logger.warning("NapCat 消息网关连接已建立,但 Host 未接受运行时状态更新")
return False
self._runtime_state_connected = True
self._reported_account_id = normalized_account_id
self._reported_scope = scope
self._logger.info(
f"NapCat 消息网关已激活路由: platform=qq account_id={normalized_account_id} "
f"scope={self._reported_scope or '*'}"
)
return True
async def report_disconnected(self) -> None:
"""向 Host 上报当前连接已断开,并撤销消息网关路由。"""
if not self._runtime_state_connected:
self._reported_account_id = None
self._reported_scope = None
return
try:
await self._gateway_capability.update_state(
gateway_name=self._gateway_name,
ready=False,
platform="qq",
)
except Exception as exc:
self._logger.warning(f"NapCat 消息网关上报断开状态失败: {exc}")
finally:
self._runtime_state_connected = False
self._reported_account_id = None
self._reported_scope = None

View File

@@ -0,0 +1,19 @@
"""NapCat 内部服务导出。"""
from .action_service import NapCatActionService
from .ban_tracker import NapCatBanTracker
from .ban_state_store import NapCatBanRecord, NapCatBanStateStore
from .history_recovery_store import NapCatChatCheckpoint, NapCatHistoryRecoveryStore
from .official_bot_guard import NapCatOfficialBotGuard
from .query_service import NapCatQueryService
__all__ = [
"NapCatActionService",
"NapCatBanRecord",
"NapCatBanStateStore",
"NapCatBanTracker",
"NapCatChatCheckpoint",
"NapCatHistoryRecoveryStore",
"NapCatOfficialBotGuard",
"NapCatQueryService",
]

View File

@@ -0,0 +1,119 @@
"""NapCat 底层动作调用服务。"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional
import asyncio
try:
from aiohttp import ClientSession, ClientTimeout
AIOHTTP_AVAILABLE = True
except ImportError:
ClientSession = None # type: ignore[assignment]
ClientTimeout = None # type: ignore[assignment]
AIOHTTP_AVAILABLE = False
if TYPE_CHECKING:
from ..transport import NapCatTransportClient
class NapCatActionService:
"""NapCat 底层动作与资源访问服务。"""
def __init__(self, logger: Any, transport: "NapCatTransportClient") -> None:
"""初始化底层动作服务。
Args:
logger: 插件日志对象。
transport: NapCat 传输层客户端。
"""
self._logger = logger
self._transport = transport
async def call_action(self, action_name: str, params: Mapping[str, Any]) -> Dict[str, Any]:
"""调用 OneBot 动作并要求返回成功结果。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
Raises:
RuntimeError: 当动作执行失败或平台返回非成功状态时抛出。
"""
normalized_params = {str(key): value for key, value in params.items()}
try:
response = await self._transport.call_action(action_name, normalized_params)
except asyncio.CancelledError:
raise
except Exception as exc:
raise RuntimeError(f"NapCat 动作执行失败: action={action_name} error={exc}") from exc
if str(response.get("status") or "").lower() != "ok":
error_message = str(response.get("wording") or response.get("message") or "unknown")
raise RuntimeError(f"NapCat 动作返回失败: action={action_name} message={error_message}")
return response
async def call_action_data(self, action_name: str, params: Mapping[str, Any]) -> Any:
"""调用 OneBot 动作并返回 ``data`` 字段。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Any: NapCat 响应中的 ``data`` 字段。
"""
response = await self.call_action(action_name, params)
return response.get("data")
async def safe_call_action_data(self, action_name: str, params: Mapping[str, Any]) -> Any:
"""安全调用 OneBot 动作并返回 ``data`` 字段。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Any: 响应中的 ``data`` 字段;失败时返回 ``None``。
"""
try:
return await self.call_action_data(action_name, params)
except asyncio.CancelledError:
raise
except Exception as exc:
self._logger.warning(f"NapCat 查询动作执行失败: action={action_name} error={exc}")
return None
async def download_binary(self, url: str) -> Optional[bytes]:
"""下载远程二进制资源。
Args:
url: 资源 URL。
Returns:
Optional[bytes]: 下载到的二进制内容;失败时返回 ``None``。
"""
if not url:
return None
if not AIOHTTP_AVAILABLE or ClientSession is None or ClientTimeout is None:
self._logger.warning("NapCat 查询层缺少 aiohttp无法下载远程资源")
return None
try:
timeout = ClientTimeout(total=15)
async with ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status != 200:
self._logger.warning(f"NapCat 远程资源下载失败: status={response.status} url={url}")
return None
return await response.read()
except asyncio.CancelledError:
raise
except Exception as exc:
self._logger.warning(f"NapCat 远程资源下载失败: {exc}")
return None

View File

@@ -0,0 +1,168 @@
"""NapCat 禁言状态存储。"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional
import asyncio
import json
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
_DEFAULT_STORAGE_PATH = _PROJECT_ROOT / "data" / "napcat_adapter" / "ban_state.json"
@dataclass
class NapCatBanRecord:
"""NapCat 禁言记录。"""
group_id: str
user_id: str
lift_time: int
@property
def record_key(self) -> str:
"""返回当前记录的稳定键。
Returns:
str: 由群号和用户号拼接得到的稳定键。
"""
return f"{self.group_id}:{self.user_id}"
@classmethod
def from_mapping(cls, payload: Mapping[str, Any]) -> Optional["NapCatBanRecord"]:
"""从字典构造禁言记录。
Args:
payload: 原始记录字典。
Returns:
Optional[NapCatBanRecord]: 构造成功时返回记录对象,否则返回 ``None``。
"""
group_id = str(payload.get("group_id") or "").strip()
user_id = str(payload.get("user_id") or "").strip()
if not group_id or not user_id:
return None
try:
lift_time = int(payload.get("lift_time", -1))
except (TypeError, ValueError):
lift_time = -1
return cls(group_id=group_id, user_id=user_id, lift_time=lift_time)
def to_dict(self) -> Dict[str, Any]:
"""将记录转换为可序列化字典。
Returns:
Dict[str, Any]: 可直接写入 JSON 的记录字典。
"""
return asdict(self)
class NapCatBanStateStore:
"""NapCat 禁言状态持久化仓库。"""
def __init__(self, logger: Any, storage_path: Path = _DEFAULT_STORAGE_PATH) -> None:
"""初始化禁言状态仓库。
Args:
logger: 插件日志对象。
storage_path: 持久化文件路径。
"""
self._logger = logger
self._storage_path = storage_path
self._records: Dict[str, NapCatBanRecord] = {}
self._records_lock = asyncio.Lock()
async def load(self) -> None:
"""从本地文件加载禁言记录。"""
if not self._storage_path.exists():
return
try:
raw_payload = json.loads(self._storage_path.read_text(encoding="utf-8"))
except Exception as exc:
self._logger.warning(f"NapCat 禁言状态文件读取失败,将忽略旧记录: {exc}")
return
if not isinstance(raw_payload, list):
self._logger.warning("NapCat 禁言状态文件格式非法,将忽略旧记录")
return
loaded_records: Dict[str, NapCatBanRecord] = {}
for item in raw_payload:
if not isinstance(item, Mapping):
continue
record = NapCatBanRecord.from_mapping(item)
if record is not None:
loaded_records[record.record_key] = record
async with self._records_lock:
self._records = loaded_records
if loaded_records:
self._logger.info(f"NapCat 禁言状态已加载 {len(loaded_records)} 条记录")
async def snapshot(self) -> List[NapCatBanRecord]:
"""返回当前记录快照。
Returns:
List[NapCatBanRecord]: 当前内存中的记录列表副本。
"""
async with self._records_lock:
return list(self._records.values())
async def upsert(self, record: NapCatBanRecord) -> None:
"""新增或更新一条禁言记录。
Args:
record: 待写入的禁言记录。
"""
async with self._records_lock:
self._records[record.record_key] = record
await self.persist()
async def remove(self, group_id: str, user_id: str) -> Optional[NapCatBanRecord]:
"""删除指定禁言记录。
Args:
group_id: 群号。
user_id: 用户号。
Returns:
Optional[NapCatBanRecord]: 被移除的记录;不存在时返回 ``None``。
"""
record_key = f"{group_id}:{user_id}"
return await self.pop(record_key)
async def pop(self, record_key: str) -> Optional[NapCatBanRecord]:
"""按稳定键移除一条记录。
Args:
record_key: 记录稳定键。
Returns:
Optional[NapCatBanRecord]: 被移除的记录;不存在时返回 ``None``。
"""
async with self._records_lock:
removed_record = self._records.pop(record_key, None)
if removed_record is not None:
await self.persist()
return removed_record
async def persist(self) -> None:
"""将当前禁言记录持久化到本地文件。"""
async with self._records_lock:
serialized_records = [
record.to_dict() for record in sorted(self._records.values(), key=lambda item: item.record_key)
]
try:
self._storage_path.parent.mkdir(parents=True, exist_ok=True)
self._storage_path.write_text(
json.dumps(serialized_records, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except Exception as exc:
self._logger.warning(f"NapCat 禁言状态持久化失败: {exc}")

View File

@@ -0,0 +1,176 @@
"""NapCat 群禁言状态跟踪服务。"""
from __future__ import annotations
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional
import asyncio
import contextlib
import time
from .ban_state_store import NapCatBanRecord, NapCatBanStateStore
from .query_service import NapCatQueryService
class NapCatBanTracker:
"""NapCat 群禁言状态跟踪器。"""
def __init__(
self,
logger: Any,
query_service: NapCatQueryService,
on_natural_lift: Callable[[Dict[str, Any]], Awaitable[None]],
state_store: NapCatBanStateStore,
) -> None:
"""初始化群禁言状态跟踪器。
Args:
logger: 插件日志对象。
query_service: NapCat 查询服务。
on_natural_lift: 检测到自然解除禁言后的回调。
state_store: 禁言状态存储仓库。
"""
self._logger = logger
self._query_service = query_service
self._on_natural_lift = on_natural_lift
self._state_store = state_store
self._poll_task: Optional[asyncio.Task[None]] = None
async def start(self) -> None:
"""启动禁言状态跟踪。"""
await self._state_store.load()
await self._refresh_records_from_remote()
if self._poll_task is None or self._poll_task.done():
self._poll_task = asyncio.create_task(self._poll_loop(), name="napcat_adapter.ban_tracker")
async def stop(self) -> None:
"""停止禁言状态跟踪并落盘当前记录。"""
poll_task = self._poll_task
self._poll_task = None
if poll_task is not None:
poll_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await poll_task
await self._state_store.persist()
async def record_notice(self, payload: Mapping[str, Any]) -> None:
"""根据实际 notice 事件更新禁言状态。
Args:
payload: NapCat 推送的原始通知事件。
"""
notice_type = str(payload.get("notice_type") or "").strip()
if notice_type != "group_ban":
return
sub_type = str(payload.get("sub_type") or "").strip()
group_id = str(payload.get("group_id") or "").strip()
user_id = str(payload.get("user_id") or "0").strip() or "0"
if not group_id:
return
if sub_type == "ban":
duration = self._normalize_int(payload.get("duration"), default=-1)
lift_time = -1 if user_id == "0" or duration <= 0 else int(time.time()) + duration
await self._state_store.upsert(NapCatBanRecord(group_id=group_id, user_id=user_id, lift_time=lift_time))
return
if sub_type in {"lift_ban", "whole_lift_ban"}:
await self._state_store.remove(group_id=group_id, user_id=user_id)
async def _refresh_records_from_remote(self) -> None:
"""基于当前 QQ 平台状态校正本地禁言记录。"""
for record in await self._state_store.snapshot():
if record.user_id == "0":
await self._refresh_whole_ban_record(record)
continue
await self._refresh_member_ban_record(record)
async def _refresh_whole_ban_record(self, record: NapCatBanRecord) -> None:
"""刷新全体禁言记录。
Args:
record: 待刷新的禁言记录。
"""
group_info = await self._query_service.get_group_info(record.group_id)
if group_info is None:
await self._emit_natural_lift(record)
return
group_all_shut = self._normalize_int(group_info.get("group_all_shut"), default=0)
if group_all_shut == 0:
await self._emit_natural_lift(record)
async def _refresh_member_ban_record(self, record: NapCatBanRecord) -> None:
"""刷新成员禁言记录。
Args:
record: 待刷新的禁言记录。
"""
member_info = await self._query_service.get_group_member_info(record.group_id, record.user_id, no_cache=True)
if member_info is None:
await self._emit_natural_lift(record)
return
shut_up_timestamp = self._normalize_int(member_info.get("shut_up_timestamp"), default=0)
if shut_up_timestamp == 0:
await self._emit_natural_lift(record)
return
if shut_up_timestamp != record.lift_time:
await self._state_store.upsert(
NapCatBanRecord(group_id=record.group_id, user_id=record.user_id, lift_time=shut_up_timestamp)
)
async def _poll_loop(self) -> None:
"""后台轮询自然解除禁言。"""
while True:
await asyncio.sleep(5.0)
current_timestamp = int(time.time())
for record in await self._state_store.snapshot():
if record.user_id == "0":
await self._refresh_whole_ban_record(record)
continue
if record.lift_time != -1 and record.lift_time <= current_timestamp:
await self._emit_natural_lift(record)
async def _emit_natural_lift(self, record: NapCatBanRecord) -> None:
"""上报自然解除禁言事件。
Args:
record: 已解除的禁言记录。
"""
removed_record = await self._state_store.pop(record.record_key)
if removed_record is None:
return
payload: Dict[str, Any] = {
"post_type": "notice",
"notice_type": "group_ban",
"sub_type": "whole_lift_ban" if record.user_id == "0" else "lift_ban",
"group_id": record.group_id,
"user_id": record.user_id,
"operator_id": None,
"time": time.time(),
"is_natural_lift": True,
}
try:
await self._on_natural_lift(payload)
except Exception as exc:
self._logger.warning(f"NapCat 自然解除禁言回调失败: {exc}")
@staticmethod
def _normalize_int(value: Any, default: int) -> int:
"""将任意值规范化为整数。
Args:
value: 待规范化的值。
default: 转换失败时的默认值。
Returns:
int: 规范化后的整数结果。
"""
try:
return int(value)
except (TypeError, ValueError):
return default

View File

@@ -0,0 +1,423 @@
"""NapCat 历史补拉状态持久化仓库。"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, List, Optional, TypeVar
import asyncio
import sqlite3
import time
from ..constants import DEFAULT_HISTORY_RECOVERY_SEEN_TTL_SEC
_PROJECT_ROOT = Path(__file__).resolve().parents[3]
_DEFAULT_STORAGE_PATH = _PROJECT_ROOT / "data" / "napcat_adapter" / "history_recovery.sqlite3"
_SCHEMA_STATEMENTS = (
"""
CREATE TABLE IF NOT EXISTS napcat_chat_checkpoint (
account_id TEXT NOT NULL,
scope TEXT NOT NULL,
chat_type TEXT NOT NULL,
chat_id TEXT NOT NULL,
last_message_id TEXT NOT NULL,
last_message_time REAL NOT NULL,
last_message_seq INTEGER,
updated_at REAL NOT NULL,
PRIMARY KEY (account_id, scope, chat_type, chat_id)
)
""",
"""
CREATE INDEX IF NOT EXISTS ix_napcat_chat_checkpoint_updated_at
ON napcat_chat_checkpoint (updated_at DESC)
""",
"""
CREATE TABLE IF NOT EXISTS napcat_recovery_seen (
account_id TEXT NOT NULL,
scope TEXT NOT NULL,
chat_type TEXT NOT NULL,
chat_id TEXT NOT NULL,
external_message_id TEXT NOT NULL,
seen_at REAL NOT NULL,
PRIMARY KEY (account_id, scope, chat_type, chat_id, external_message_id)
)
""",
"""
CREATE INDEX IF NOT EXISTS ix_napcat_recovery_seen_seen_at
ON napcat_recovery_seen (seen_at DESC)
""",
)
T = TypeVar("T")
@dataclass(frozen=True)
class NapCatChatCheckpoint:
"""描述一个会话的最近入站锚点。"""
account_id: str
scope: str
chat_type: str
chat_id: str
last_message_id: str
last_message_time: float
last_message_seq: int | None
updated_at: float
@classmethod
def from_row(cls, row: sqlite3.Row) -> "NapCatChatCheckpoint":
"""从 SQLite 行对象恢复 checkpoint。"""
last_message_seq = row["last_message_seq"]
normalized_seq = int(last_message_seq) if isinstance(last_message_seq, int) else None
return cls(
account_id=str(row["account_id"] or "").strip(),
scope=str(row["scope"] or "").strip(),
chat_type=str(row["chat_type"] or "").strip(),
chat_id=str(row["chat_id"] or "").strip(),
last_message_id=str(row["last_message_id"] or "").strip(),
last_message_time=float(row["last_message_time"] or 0.0),
last_message_seq=normalized_seq,
updated_at=float(row["updated_at"] or 0.0),
)
class NapCatHistoryRecoveryStore:
"""负责持久化历史补拉所需的会话状态与去重状态。"""
def __init__(self, logger: Any, storage_path: Path = _DEFAULT_STORAGE_PATH) -> None:
"""初始化历史补拉状态仓库。"""
self._logger = logger
self._storage_path = storage_path
self._store_lock = asyncio.Lock()
self._schema_ready = False
async def load(self) -> None:
"""初始化 SQLite 文件并清理过期去重记录。"""
await self._execute_locked(self._ensure_schema)
pruned_count = await self.prune_recovery_seen(DEFAULT_HISTORY_RECOVERY_SEEN_TTL_SEC)
if pruned_count > 0:
self._logger.debug(f"NapCat 历史补拉去重表已清理 {pruned_count} 条过期记录")
async def list_checkpoints(self, account_id: str, scope: str = "", limit: int = 50) -> List[NapCatChatCheckpoint]:
"""列出指定账号与作用域下的最近会话 checkpoint。"""
normalized_account_id = str(account_id or "").strip()
if not normalized_account_id:
return []
normalized_scope = self._normalize_scope(scope)
normalized_limit = max(1, int(limit))
def _operation(conn: sqlite3.Connection) -> List[NapCatChatCheckpoint]:
cursor = conn.execute(
"""
SELECT
account_id,
scope,
chat_type,
chat_id,
last_message_id,
last_message_time,
last_message_seq,
updated_at
FROM napcat_chat_checkpoint
WHERE account_id = ? AND scope = ?
ORDER BY updated_at DESC
LIMIT ?
""",
(normalized_account_id, normalized_scope, normalized_limit),
)
return [NapCatChatCheckpoint.from_row(row) for row in cursor.fetchall()]
return await self._execute_locked(_operation)
async def record_checkpoint(
self,
*,
account_id: str,
scope: str = "",
chat_type: str,
chat_id: str,
message_id: str,
message_time: float,
message_seq: int | None = None,
) -> None:
"""记录一条已被 Host 接受的最新入站消息锚点。"""
normalized_account_id = str(account_id or "").strip()
normalized_scope = self._normalize_scope(scope)
normalized_chat_type = str(chat_type or "").strip()
normalized_chat_id = str(chat_id or "").strip()
normalized_message_id = str(message_id or "").strip()
if not (
normalized_account_id
and normalized_chat_type
and normalized_chat_id
and normalized_message_id
):
return
normalized_message_time = float(message_time or 0.0)
normalized_message_seq = self._normalize_message_seq(message_seq)
updated_at = time.time()
def _operation(conn: sqlite3.Connection) -> None:
cursor = conn.execute(
"""
SELECT last_message_id, last_message_time, last_message_seq
FROM napcat_chat_checkpoint
WHERE account_id = ? AND scope = ? AND chat_type = ? AND chat_id = ?
""",
(
normalized_account_id,
normalized_scope,
normalized_chat_type,
normalized_chat_id,
),
)
existing_row = cursor.fetchone()
if existing_row is not None and not self._should_advance_checkpoint(
existing_row=existing_row,
message_id=normalized_message_id,
message_time=normalized_message_time,
message_seq=normalized_message_seq,
):
return
conn.execute(
"""
INSERT INTO napcat_chat_checkpoint (
account_id,
scope,
chat_type,
chat_id,
last_message_id,
last_message_time,
last_message_seq,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(account_id, scope, chat_type, chat_id) DO UPDATE SET
last_message_id = excluded.last_message_id,
last_message_time = excluded.last_message_time,
last_message_seq = excluded.last_message_seq,
updated_at = excluded.updated_at
""",
(
normalized_account_id,
normalized_scope,
normalized_chat_type,
normalized_chat_id,
normalized_message_id,
normalized_message_time,
normalized_message_seq,
updated_at,
),
)
await self._execute_locked(_operation)
async def has_recovered_message_seen(
self,
*,
account_id: str,
scope: str = "",
chat_type: str,
chat_id: str,
external_message_id: str,
) -> bool:
"""判断某条历史补拉消息是否已经被当前仓库记录过。"""
normalized_account_id = str(account_id or "").strip()
normalized_scope = self._normalize_scope(scope)
normalized_chat_type = str(chat_type or "").strip()
normalized_chat_id = str(chat_id or "").strip()
normalized_external_message_id = str(external_message_id or "").strip()
if not (
normalized_account_id
and normalized_chat_type
and normalized_chat_id
and normalized_external_message_id
):
return False
def _operation(conn: sqlite3.Connection) -> bool:
cursor = conn.execute(
"""
SELECT 1
FROM napcat_recovery_seen
WHERE account_id = ?
AND scope = ?
AND chat_type = ?
AND chat_id = ?
AND external_message_id = ?
LIMIT 1
""",
(
normalized_account_id,
normalized_scope,
normalized_chat_type,
normalized_chat_id,
normalized_external_message_id,
),
)
return cursor.fetchone() is not None
return await self._execute_locked(_operation)
async def mark_recovered_message_seen(
self,
*,
account_id: str,
scope: str = "",
chat_type: str,
chat_id: str,
external_message_id: str,
) -> None:
"""将一条历史补拉消息标记为已尝试处理。"""
normalized_account_id = str(account_id or "").strip()
normalized_scope = self._normalize_scope(scope)
normalized_chat_type = str(chat_type or "").strip()
normalized_chat_id = str(chat_id or "").strip()
normalized_external_message_id = str(external_message_id or "").strip()
if not (
normalized_account_id
and normalized_chat_type
and normalized_chat_id
and normalized_external_message_id
):
return
def _operation(conn: sqlite3.Connection) -> None:
conn.execute(
"""
INSERT OR REPLACE INTO napcat_recovery_seen (
account_id,
scope,
chat_type,
chat_id,
external_message_id,
seen_at
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
normalized_account_id,
normalized_scope,
normalized_chat_type,
normalized_chat_id,
normalized_external_message_id,
time.time(),
),
)
await self._execute_locked(_operation)
async def prune_recovery_seen(self, ttl_seconds: float) -> int:
"""删除超过保留期的历史补拉去重记录。"""
normalized_ttl_seconds = max(0.0, float(ttl_seconds or 0.0))
if normalized_ttl_seconds <= 0.0:
return 0
cutoff_timestamp = time.time() - normalized_ttl_seconds
def _operation(conn: sqlite3.Connection) -> int:
cursor = conn.execute(
"DELETE FROM napcat_recovery_seen WHERE seen_at < ?",
(cutoff_timestamp,),
)
return int(cursor.rowcount or 0)
return await self._execute_locked(_operation)
async def _execute_locked(self, operation: Callable[[sqlite3.Connection], T]) -> T:
"""在锁保护下打开 SQLite 并执行一次原子操作。"""
async with self._store_lock:
conn = self._open_connection()
try:
self._ensure_schema(conn)
result = operation(conn)
conn.commit()
return result
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _open_connection(self) -> sqlite3.Connection:
"""打开一个带 Row 工厂的 SQLite 连接。"""
self._storage_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self._storage_path))
conn.row_factory = sqlite3.Row
return conn
def _ensure_schema(self, conn: sqlite3.Connection) -> None:
"""确保 SQLite 表结构已经准备完成。"""
if self._schema_ready:
return
for statement in _SCHEMA_STATEMENTS:
conn.execute(statement)
self._schema_ready = True
@staticmethod
def _normalize_scope(scope: str | None) -> str:
"""将空作用域统一折叠为空字符串。"""
return str(scope or "").strip()
@staticmethod
def _normalize_message_seq(message_seq: object) -> int | None:
"""将消息序号规范化为可选整数。"""
try:
if message_seq is None or str(message_seq).strip() == "":
return None
return int(message_seq)
except (TypeError, ValueError):
return None
@classmethod
def _should_advance_checkpoint(
cls,
*,
existing_row: sqlite3.Row,
message_id: str,
message_time: float,
message_seq: int | None,
) -> bool:
"""判断新的锚点是否应覆盖旧锚点。"""
existing_message_id = str(existing_row["last_message_id"] or "").strip()
existing_message_time = float(existing_row["last_message_time"] or 0.0)
existing_message_seq = cls._normalize_message_seq(existing_row["last_message_seq"])
if message_seq is not None and existing_message_seq is not None:
if message_seq != existing_message_seq:
return message_seq > existing_message_seq
if message_id == existing_message_id:
return False
return message_time >= existing_message_time
if message_time != existing_message_time:
return message_time > existing_message_time
if message_id == existing_message_id:
return False
if message_seq is not None and existing_message_seq is None:
return True
return True

View File

@@ -0,0 +1,59 @@
"""NapCat 官方机器人消息拦截服务。"""
from __future__ import annotations
from typing import Any, Dict
from .query_service import NapCatQueryService
class NapCatOfficialBotGuard:
"""根据群成员资料判断是否应拦截 QQ 官方机器人消息。"""
def __init__(self, logger: Any, query_service: NapCatQueryService) -> None:
"""初始化官方机器人拦截服务。
Args:
logger: 插件日志对象。
query_service: NapCat 查询服务。
"""
self._logger = logger
self._query_service = query_service
self._cache: Dict[str, bool] = {}
def clear_cache(self) -> None:
"""清空机器人识别缓存。"""
self._cache.clear()
async def should_reject(self, sender_user_id: str, group_id: str, ban_qq_bot: bool) -> bool:
"""判断是否应拦截当前消息。
Args:
sender_user_id: 发送者用户号。
group_id: 群号。
ban_qq_bot: 是否启用官方机器人拦截。
Returns:
bool: 若应拦截,则返回 ``True``。
"""
if not ban_qq_bot or not group_id:
return False
cache_key = f"{group_id}:{sender_user_id}"
cached_result = self._cache.get(cache_key)
if cached_result is not None:
if cached_result:
self._logger.warning("QQ 官方机器人消息拦截已启用,消息被丢弃")
return cached_result
member_info = await self._query_service.get_group_member_info(group_id, sender_user_id, no_cache=True)
if member_info is None:
self._logger.warning("无法获取用户是否为机器人,默认放行当前消息")
self._cache[cache_key] = False
return False
should_reject = bool(member_info.get("is_robot"))
self._cache[cache_key] = should_reject
if should_reject:
self._logger.warning("QQ 官方机器人消息拦截已启用,消息被丢弃")
return should_reject

View File

@@ -0,0 +1,585 @@
"""NapCat QQ 平台查询服务。"""
from __future__ import annotations
from typing import Any, List, Mapping, Optional
from ..types import NapCatActionParams, NapCatActionResponse, NapCatPayloadDict, NapCatPayloadList
from .action_service import NapCatActionService
class NapCatQueryService:
"""NapCat QQ 平台查询与管理动作服务。"""
def __init__(self, action_service: NapCatActionService, logger: Any) -> None:
"""初始化查询服务。
Args:
action_service: NapCat 底层动作服务。
logger: 插件日志对象。
"""
self._action_service = action_service
self._logger = logger
async def call_action(self, action_name: str, params: NapCatActionParams) -> NapCatActionResponse:
"""调用 OneBot 动作并要求返回成功结果。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
NapCatActionResponse: NapCat 返回的原始响应字典。
"""
return await self._action_service.call_action(action_name, params)
async def call_action_data(self, action_name: str, params: NapCatActionParams) -> Any:
"""调用 OneBot 动作并返回 ``data`` 字段。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Any: NapCat 响应中的 ``data`` 字段。
"""
return await self._action_service.call_action_data(action_name, params)
async def get_login_info(self) -> Optional[NapCatPayloadDict]:
"""获取当前登录账号信息。
Returns:
Optional[NapCatPayloadDict]: 登录信息字典;返回值不是字典时为 ``None``。
"""
response_data = await self._safe_call_action_data("get_login_info", {})
return response_data if isinstance(response_data, dict) else None
async def get_stranger_info(self, user_id: str, no_cache: bool = False) -> Optional[NapCatPayloadDict]:
"""获取陌生人信息。
Args:
user_id: 用户号。
no_cache: 是否禁用缓存。
Returns:
Optional[NapCatPayloadDict]: 陌生人信息字典;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data(
"get_stranger_info",
{"user_id": user_id, "no_cache": bool(no_cache)},
)
return response_data if isinstance(response_data, dict) else None
async def get_friend_list(self, no_cache: bool = False) -> Optional[NapCatPayloadList]:
"""获取好友列表。
Args:
no_cache: 是否禁用缓存。
Returns:
Optional[NapCatPayloadList]: 好友信息列表;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_friend_list", {"no_cache": bool(no_cache)})
return self._normalize_payload_list(response_data, action_name="get_friend_list")
async def get_group_info(self, group_id: str) -> Optional[NapCatPayloadDict]:
"""获取群信息。
Args:
group_id: 群号。
Returns:
Optional[NapCatPayloadDict]: 群信息字典;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_group_info", {"group_id": group_id})
return response_data if isinstance(response_data, dict) else None
async def get_group_detail_info(self, group_id: str) -> Optional[NapCatPayloadDict]:
"""获取群详细信息。
Args:
group_id: 群号。
Returns:
Optional[NapCatPayloadDict]: 群详细信息字典;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_group_detail_info", {"group_id": group_id})
return response_data if isinstance(response_data, dict) else None
async def get_group_list(self, no_cache: bool = False) -> Optional[NapCatPayloadList]:
"""获取群列表。
Args:
no_cache: 是否禁用缓存。
Returns:
Optional[NapCatPayloadList]: 群信息列表;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_group_list", {"no_cache": bool(no_cache)})
return self._normalize_payload_list(response_data, action_name="get_group_list")
async def get_group_at_all_remain(self, group_id: str) -> Optional[NapCatPayloadDict]:
"""获取群 @ 全体成员剩余次数。
Args:
group_id: 群号。
Returns:
Optional[NapCatPayloadDict]: 剩余次数信息;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_group_at_all_remain", {"group_id": group_id})
return response_data if isinstance(response_data, dict) else None
async def get_group_member_info(
self,
group_id: str,
user_id: str,
no_cache: bool = True,
) -> Optional[NapCatPayloadDict]:
"""获取群成员信息。
Args:
group_id: 群号。
user_id: 用户号。
no_cache: 是否禁用缓存。
Returns:
Optional[NapCatPayloadDict]: 群成员信息字典;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data(
"get_group_member_info",
{"group_id": group_id, "user_id": user_id, "no_cache": bool(no_cache)},
)
return response_data if isinstance(response_data, dict) else None
async def get_group_member_list(self, group_id: str, no_cache: bool = False) -> Optional[NapCatPayloadList]:
"""获取群成员列表。
Args:
group_id: 群号。
no_cache: 是否禁用缓存。
Returns:
Optional[NapCatPayloadList]: 群成员信息列表;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data(
"get_group_member_list",
{"group_id": group_id, "no_cache": bool(no_cache)},
)
return self._normalize_payload_list(response_data, action_name="get_group_member_list")
async def get_message_detail(self, message_id: str) -> Optional[NapCatPayloadDict]:
"""获取消息详情。
Args:
message_id: 消息 ID。
Returns:
Optional[NapCatPayloadDict]: 消息详情字典;失败时返回 ``None``。
"""
response_data = await self._safe_call_action_data("get_msg", {"message_id": message_id})
return response_data if isinstance(response_data, dict) else None
async def get_friend_message_history(
self,
user_id: str,
*,
message_seq: int | None = None,
count: int = 20,
reverse_order: bool = False,
) -> Optional[NapCatPayloadList]:
"""获取私聊历史消息列表。"""
params: NapCatActionResponse = {
"user_id": user_id,
"count": max(1, int(count)),
"reverse_order": bool(reverse_order),
}
if message_seq is not None:
params["message_seq"] = int(message_seq)
response_data = await self._safe_call_action_data("get_friend_msg_history", params)
return self._normalize_payload_list(response_data, action_name="get_friend_msg_history")
async def get_group_message_history(
self,
group_id: str,
*,
message_seq: int | None = None,
count: int = 20,
reverse_order: bool = False,
) -> Optional[NapCatPayloadList]:
"""获取群聊历史消息列表。"""
params: NapCatActionResponse = {
"group_id": group_id,
"count": max(1, int(count)),
"reverse_order": bool(reverse_order),
}
if message_seq is not None:
params["message_seq"] = int(message_seq)
response_data = await self._safe_call_action_data("get_group_msg_history", params)
return self._normalize_payload_list(response_data, action_name="get_group_msg_history")
async def get_forward_message(
self,
message_id: Optional[str] = None,
forward_id: Optional[str] = None,
) -> Optional[NapCatPayloadDict]:
"""获取合并转发消息详情。
Args:
message_id: 转发消息 ID。
forward_id: NapCat 官方文档中的兼容字段 ``id``。
Returns:
Optional[NapCatPayloadDict]: 合并转发消息详情;失败时返回 ``None``。
"""
params: NapCatActionResponse = {}
if message_id:
params["message_id"] = message_id
if forward_id:
params["id"] = forward_id
if not params:
raise ValueError("message_id 或 id 至少提供一个")
response_data = await self._safe_call_action_data("get_forward_msg", params)
return self._normalize_forward_payload(response_data)
async def get_record_detail(
self,
file_name: Optional[str] = None,
file_id: Optional[str] = None,
out_format: str = "wav",
) -> Optional[NapCatPayloadDict]:
"""获取语音文件详情。
Args:
file_name: 语音文件名。
file_id: 可选文件 ID。
out_format: 输出格式。
Returns:
Optional[NapCatPayloadDict]: 语音详情字典;失败时返回 ``None``。
"""
params: NapCatActionResponse = {}
if file_name:
params["file"] = file_name
if file_id:
params["file_id"] = file_id
if out_format:
params["out_format"] = out_format
if not params.get("file") and not params.get("file_id"):
raise ValueError("file 或 file_id 至少提供一个")
response_data = await self._safe_call_action_data("get_record", params)
return response_data if isinstance(response_data, dict) else None
async def set_group_ban(self, group_id: int, user_id: int, duration: int) -> NapCatActionResponse:
"""设置群成员禁言。
Args:
group_id: 群号。
user_id: 用户号。
duration: 禁言秒数。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_group_ban",
{"group_id": group_id, "user_id": user_id, "duration": duration},
)
async def set_group_whole_ban(self, group_id: int, enable: bool) -> NapCatActionResponse:
"""设置群全体禁言。
Args:
group_id: 群号。
enable: 是否开启全体禁言。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_group_whole_ban",
{"group_id": group_id, "enable": bool(enable)},
)
async def set_group_kick(
self,
group_id: int,
user_id: int,
reject_add_request: bool = False,
) -> NapCatActionResponse:
"""踢出群成员。
Args:
group_id: 群号。
user_id: 用户号。
reject_add_request: 是否拒绝再次加群。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_group_kick",
{
"group_id": group_id,
"user_id": user_id,
"reject_add_request": bool(reject_add_request),
},
)
async def set_group_kick_members(
self,
group_id: int,
user_ids: List[int],
reject_add_request: bool = False,
) -> NapCatActionResponse:
"""批量踢出群成员。
Args:
group_id: 群号。
user_ids: 用户号列表。
reject_add_request: 是否拒绝再次加群。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_group_kick_members",
{
"group_id": group_id,
"user_id": user_ids,
"reject_add_request": bool(reject_add_request),
},
)
async def send_poke(
self,
user_id: int,
group_id: Optional[int] = None,
target_id: Optional[int] = None,
) -> NapCatActionResponse:
"""发送戳一戳。
Args:
user_id: 目标用户号。
group_id: 可选群号;私聊时为空。
target_id: NapCat 官方 ``send_poke`` 动作支持的目标 ID。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
params: NapCatActionResponse = {"user_id": user_id}
if group_id is not None:
params["group_id"] = group_id
if target_id is not None:
params["target_id"] = target_id
return await self.call_action("send_poke", params)
async def delete_message(self, message_id: int) -> NapCatActionResponse:
"""撤回消息。
Args:
message_id: 消息 ID。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action("delete_msg", {"message_id": message_id})
async def send_group_ai_record(self, group_id: int, character: str, text: str) -> NapCatActionResponse:
"""发送群 AI 语音。
Args:
group_id: 群号。
character: 角色标识。
text: 语音文本。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"send_group_ai_record",
{"group_id": group_id, "character": character, "text": text},
)
async def set_message_emoji_like(
self,
message_id: int,
emoji_id: int,
set_like: bool = True,
) -> NapCatActionResponse:
"""给消息贴表情或取消表情。
Args:
message_id: 消息 ID。
emoji_id: 表情 ID。
set_like: 是否设置为已贴表情。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_msg_emoji_like",
{"message_id": message_id, "emoji_id": emoji_id, "set": bool(set_like)},
)
async def set_group_name(self, group_id: int, group_name: str) -> NapCatActionResponse:
"""设置群名称。
Args:
group_id: 群号。
group_name: 新群名称。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
return await self.call_action(
"set_group_name",
{"group_id": group_id, "group_name": group_name},
)
async def set_qq_profile(
self,
nickname: str,
personal_note: str = "",
sex: str = "",
) -> NapCatActionResponse:
"""设置 QQ 账号资料。
Args:
nickname: 新昵称。
personal_note: 个性签名。
sex: 性别,支持 ``male``、``female``、``unknown``。
Returns:
NapCatActionResponse: NapCat 原始响应字典。
"""
params: NapCatActionResponse = {"nickname": nickname}
if personal_note:
params["personal_note"] = personal_note
if sex:
params["sex"] = sex
return await self.call_action("set_qq_profile", params)
async def download_binary(self, url: str) -> Optional[bytes]:
"""下载远程二进制资源。
Args:
url: 资源 URL。
Returns:
Optional[bytes]: 下载到的二进制内容;失败时返回 ``None``。
"""
return await self._action_service.download_binary(url)
async def _safe_call_action_data(self, action_name: str, params: NapCatActionParams) -> Any:
"""安全调用 OneBot 动作并返回 ``data`` 字段。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Any: 响应中的 ``data`` 字段;失败时返回 ``None``。
"""
return await self._action_service.safe_call_action_data(action_name, params)
def _normalize_payload_list(self, response_data: Any, action_name: str) -> Optional[NapCatPayloadList]:
"""将列表类响应归一化为字典列表。
NapCat 在不同版本或不同动作下,``data`` 可能直接返回列表,
也可能再包一层字典,例如 ``{\"members\": [...]}``。
Args:
response_data: 原始 ``data`` 字段。
action_name: 当前动作名称。
Returns:
Optional[NapCatPayloadList]: 归一化后的列表;无法识别时返回 ``None``。
"""
if isinstance(response_data, list):
return [dict(item) for item in response_data if isinstance(item, Mapping)]
if not isinstance(response_data, Mapping):
self._logger.warning(
"NapCat 列表接口返回了无法识别的数据类型: action=%s type=%s payload=%r",
action_name,
type(response_data).__name__,
response_data,
)
return None
for key in (
"list",
"items",
"members",
"member_list",
"group_list",
"friend_list",
"friends",
"records",
"rows",
"data",
):
candidate = response_data.get(key)
if isinstance(candidate, list):
return [dict(item) for item in candidate if isinstance(item, Mapping)]
for candidate in response_data.values():
if isinstance(candidate, list):
return [dict(item) for item in candidate if isinstance(item, Mapping)]
self._logger.warning(
"NapCat 列表接口返回了无法归一化的字典结构: action=%s payload=%r",
action_name,
response_data,
)
return None
def _normalize_forward_payload(self, response_data: Any) -> Optional[NapCatPayloadDict]:
"""将合并转发响应归一化为统一字典结构。
NapCat 的 ``get_forward_msg`` 在不同版本下,``data`` 可能直接返回节点列表,
也可能返回 ``{\"messages\": [...]}``,甚至包在 ``content`` 字段中。
Args:
response_data: ``get_forward_msg`` 的原始 ``data`` 字段。
Returns:
Optional[NapCatPayloadDict]: 归一化后的转发消息详情;失败时返回 ``None``。
"""
if isinstance(response_data, list):
return {"messages": [dict(item) for item in response_data if isinstance(item, Mapping)]}
if not isinstance(response_data, Mapping):
self._logger.warning(
"NapCat 转发接口返回了无法识别的数据类型: type=%s payload=%r",
type(response_data).__name__,
response_data,
)
return None
direct_messages = response_data.get("messages")
if isinstance(direct_messages, list):
return dict(response_data)
direct_content = response_data.get("content")
if isinstance(direct_content, list):
return {"messages": [dict(item) for item in direct_content if isinstance(item, Mapping)]}
nested_data = response_data.get("data")
if isinstance(nested_data, Mapping):
nested_messages = nested_data.get("messages")
if isinstance(nested_messages, list):
return {"messages": [dict(item) for item in nested_messages if isinstance(item, Mapping)]}
nested_content = nested_data.get("content")
if isinstance(nested_content, list):
return {"messages": [dict(item) for item in nested_content if isinstance(item, Mapping)]}
self._logger.warning("NapCat 转发接口未返回可识别的转发节点列表: payload=%r", response_data)
return None

View File

@@ -0,0 +1,449 @@
"""NapCat 正向 WebSocket 传输层。"""
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, Optional, Set, cast
from uuid import uuid4
import asyncio
import contextlib
import json
from .config import NapCatServerConfig
if TYPE_CHECKING:
from aiohttp import ClientWebSocketResponse as AiohttpClientWebSocketResponse
try:
from aiohttp import ClientSession, ClientTimeout, WSMsgType
AIOHTTP_AVAILABLE = True
except ImportError:
ClientSession = cast(Any, None)
ClientTimeout = cast(Any, None)
WSMsgType = cast(Any, None)
AIOHTTP_AVAILABLE = False
if not TYPE_CHECKING:
AiohttpClientWebSocketResponse = Any
class NapCatTransportClient:
"""NapCat 正向 WebSocket 客户端。"""
def __init__(
self,
logger: Any,
on_connection_opened: Callable[[], Coroutine[Any, Any, None]],
on_connection_closed: Callable[[], Coroutine[Any, Any, None]],
on_payload: Callable[[Dict[str, Any]], Coroutine[Any, Any, None]],
) -> None:
"""初始化传输层客户端。
Args:
logger: 插件日志对象。
on_connection_opened: 连接建立后的异步回调。
on_connection_closed: 连接断开后的异步回调。
on_payload: 收到非 echo 载荷后的异步回调。
"""
self._logger = logger
self._on_connection_opened = on_connection_opened
self._on_connection_closed = on_connection_closed
self._on_payload = on_payload
self._server_config: Optional[NapCatServerConfig] = None
self._connection_task: Optional[asyncio.Task[None]] = None
self._pending_actions: Dict[str, asyncio.Future[Dict[str, Any]]] = {}
self._background_tasks: Set[asyncio.Task[Any]] = set()
self._send_lock = asyncio.Lock()
self._ws: Optional[AiohttpClientWebSocketResponse] = None
self._stop_requested: bool = False
self._connection_active: bool = False
self._warned_missing_token_for_ws_url: Optional[str] = None
@classmethod
def is_available(cls) -> bool:
"""判断当前环境是否安装了传输层依赖。
Returns:
bool: 若已安装 ``aiohttp``,则返回 ``True``。
"""
return AIOHTTP_AVAILABLE
def configure(self, server_config: NapCatServerConfig) -> None:
"""更新当前传输层使用的 NapCat 服务端配置。
Args:
server_config: 最新生效的 NapCat 服务端配置。
"""
self._server_config = server_config
self._warned_missing_token_for_ws_url = None
async def start(self) -> None:
"""启动 NapCat 正向 WebSocket 连接循环。
Raises:
RuntimeError: 当缺少配置或依赖时抛出。
"""
if not self.is_available():
raise RuntimeError("NapCat 适配器依赖 aiohttp但当前环境未安装该依赖")
if self._server_config is None:
raise RuntimeError("NapCat 适配器尚未配置 napcat_server")
if self._connection_task is not None and not self._connection_task.done():
return
self._stop_requested = False
self._connection_task = asyncio.create_task(self._connection_loop(), name="napcat_adapter.connection")
async def stop(self) -> None:
"""停止当前连接并清理所有后台任务。"""
self._stop_requested = True
connection_task = self._connection_task
self._connection_task = None
ws = self._ws
if ws is not None and not ws.closed:
with contextlib.suppress(Exception):
await ws.close()
self._ws = None
if connection_task is not None:
connection_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await connection_task
await self._cancel_background_tasks()
await self._notify_connection_closed()
self._fail_pending_actions("NapCat connection closed")
async def call_action(self, action_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""发送 OneBot 动作并等待对应的 echo 响应。
Args:
action_name: OneBot 动作名称。
params: 动作参数。
Returns:
Dict[str, Any]: NapCat 返回的原始响应字典。
Raises:
RuntimeError: 当连接不可用时抛出。
"""
ws = self._ws
server_config = self._server_config
if ws is None or ws.closed or server_config is None:
raise RuntimeError("NapCat is not connected")
echo_id = uuid4().hex
loop = asyncio.get_running_loop()
response_future: asyncio.Future[Dict[str, Any]] = loop.create_future()
self._pending_actions[echo_id] = response_future
request_payload = {"action": action_name, "params": params, "echo": echo_id}
try:
async with self._send_lock:
await ws.send_str(json.dumps(request_payload, ensure_ascii=False))
return await asyncio.wait_for(response_future, timeout=server_config.action_timeout_sec)
finally:
self._pending_actions.pop(echo_id, None)
async def _connection_loop(self) -> None:
"""维护单个 WebSocket 连接,并在断开后按配置重连。"""
assert ClientSession is not None
assert ClientTimeout is not None
while not self._stop_requested:
server_config = self._server_config
if server_config is None:
return
ws_url = server_config.build_ws_url()
timeout = ClientTimeout(total=None, connect=10)
self._log_connection_attempt(ws_url, server_config)
try:
async with ClientSession(headers=self._build_headers(server_config), timeout=timeout) as session:
async with session.ws_connect(ws_url, heartbeat=server_config.heartbeat_interval or None) as ws:
self._ws = ws
self._logger.info(f"NapCat 适配器已连接: {ws_url}")
disconnect_reason = await self._receive_loop(ws)
self._log_connection_closed(ws_url, server_config, disconnect_reason)
except asyncio.CancelledError:
raise
except Exception as exc:
self._logger.warning(
f"NapCat 适配器连接失败: {exc}"
f"{self._build_missing_token_hint(server_config)}"
f"{self._build_reconnect_hint(server_config)}"
)
finally:
self._ws = None
await self._notify_connection_closed()
self._fail_pending_actions("NapCat connection interrupted")
if self._stop_requested:
break
await asyncio.sleep(server_config.reconnect_delay_sec)
async def _receive_loop(self, ws: AiohttpClientWebSocketResponse) -> str:
"""持续消费 WebSocket 消息并分发处理。
Args:
ws: 当前活跃的 WebSocket 连接对象。
Returns:
str: 当前连接结束时的简要原因描述。
"""
assert WSMsgType is not None
disconnect_reason = "未收到更多 WebSocket 消息,连接已结束"
bootstrap_task = self._create_background_task(
self._notify_connection_opened(),
"napcat_adapter.bootstrap",
)
try:
async for ws_message in ws:
if ws_message.type != WSMsgType.TEXT:
if ws_message.type == WSMsgType.CLOSE:
disconnect_reason = self._describe_terminal_ws_message(
ws=ws,
ws_message=ws_message,
message_label="收到服务端 CLOSE 帧",
)
break
if ws_message.type == WSMsgType.CLOSED:
disconnect_reason = self._describe_terminal_ws_message(
ws=ws,
ws_message=ws_message,
message_label="WebSocket 已关闭",
)
break
if ws_message.type == WSMsgType.ERROR:
disconnect_reason = self._describe_terminal_ws_message(
ws=ws,
ws_message=ws_message,
message_label="WebSocket 进入错误状态",
)
break
continue
payload = self._parse_json_message(ws_message.data)
if payload is None:
continue
if echo_id := str(payload.get("echo") or "").strip():
self._resolve_pending_action(echo_id, payload)
continue
self._create_background_task(self._on_payload(payload), "napcat_adapter.payload")
finally:
if bootstrap_task is not None and not bootstrap_task.done():
bootstrap_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await bootstrap_task
return disconnect_reason
def _create_background_task(self, coroutine: Coroutine[Any, Any, Any], name: str) -> asyncio.Task[Any]:
"""创建并跟踪一个后台任务。
Args:
coroutine: 待执行的协程对象。
name: 任务名。
Returns:
asyncio.Task[Any]: 已创建的后台任务。
"""
task = asyncio.create_task(coroutine, name=name)
self._background_tasks.add(task)
task.add_done_callback(self._handle_background_task_completion)
return task
def _handle_background_task_completion(self, task: asyncio.Task[Any]) -> None:
"""处理后台任务结束后的清理与异常记录。
Args:
task: 已结束的后台任务。
"""
self._background_tasks.discard(task)
if task.cancelled():
return
exception = task.exception()
if exception is not None:
self._logger.error(f"NapCat 适配器后台任务异常: {exception}", exc_info=True)
async def _cancel_background_tasks(self) -> None:
"""取消所有仍在运行的后台任务。"""
background_tasks = list(self._background_tasks)
for task in background_tasks:
task.cancel()
if background_tasks:
with contextlib.suppress(Exception):
await asyncio.gather(*background_tasks, return_exceptions=True)
self._background_tasks.clear()
async def _notify_connection_opened(self) -> None:
"""在连接建立后触发上层回调。"""
if self._connection_active:
return
self._connection_active = True
try:
await self._on_connection_opened()
except Exception as exc:
self._logger.warning(f"NapCat 适配器连接建立回调失败: {exc}")
async def _notify_connection_closed(self) -> None:
"""在连接断开后触发上层回调。"""
if not self._connection_active:
return
self._connection_active = False
try:
await self._on_connection_closed()
except Exception as exc:
self._logger.warning(f"NapCat 适配器断连回调失败: {exc}")
def _resolve_pending_action(self, echo_id: str, payload: Dict[str, Any]) -> None:
"""解析等待中的动作响应。
Args:
echo_id: 动作请求对应的 echo 标识。
payload: NapCat 返回的响应载荷。
"""
response_future = self._pending_actions.get(echo_id)
if response_future is None or response_future.done():
return
response_future.set_result(payload)
def _fail_pending_actions(self, error_message: str) -> None:
"""让所有等待中的动作以异常方式结束。
Args:
error_message: 写入异常中的错误信息。
"""
for response_future in self._pending_actions.values():
if not response_future.done():
response_future.set_exception(RuntimeError(error_message))
self._pending_actions.clear()
def _build_headers(self, server_config: NapCatServerConfig) -> Dict[str, str]:
"""构造连接 NapCat 所需的请求头。
Args:
server_config: 当前生效的 NapCat 服务端配置。
Returns:
Dict[str, str]: WebSocket 握手请求头。
"""
return {"Authorization": f"Bearer {server_config.token}"} if server_config.token else {}
def _log_connection_attempt(self, ws_url: str, server_config: NapCatServerConfig) -> None:
"""记录一次连接尝试的诊断信息。
Args:
ws_url: 即将连接的 WebSocket 地址。
server_config: 当前生效的 NapCat 服务端配置。
"""
auth_mode = "已配置 token" if server_config.token else "未配置 token"
self._logger.debug(f"NapCat 适配器开始连接: {ws_url}(鉴权: {auth_mode}")
if not server_config.token and self._warned_missing_token_for_ws_url != ws_url:
self._logger.warning(
"NapCat 适配器当前未配置 napcat_server.token"
"若 NapCat 开启了访问令牌校验,连接可能会被服务端立即断开"
)
self._warned_missing_token_for_ws_url = ws_url
def _log_connection_closed(self, ws_url: str, server_config: NapCatServerConfig, reason: str) -> None:
"""记录连接结束与重连计划。
Args:
ws_url: 当前连接对应的 WebSocket 地址。
server_config: 当前生效的 NapCat 服务端配置。
reason: 当前连接结束原因。
"""
self._logger.warning(
f"NapCat 适配器连接已断开: {ws_url}{reason}"
f"{self._build_missing_token_hint(server_config)}"
f"{self._build_reconnect_hint(server_config)}"
)
def _build_missing_token_hint(self, server_config: NapCatServerConfig) -> str:
"""构造缺失 token 时的附加提示。
Args:
server_config: 当前生效的 NapCat 服务端配置。
Returns:
str: 缺失 token 时的提示文案;无需提示时返回空字符串。
"""
if server_config.token:
return ""
return ";当前未配置 napcat_server.token若服务端开启了访问令牌校验请补全 token"
def _build_reconnect_hint(self, server_config: NapCatServerConfig) -> str:
"""构造连接结束后的重连提示。
Args:
server_config: 当前生效的 NapCat 服务端配置。
Returns:
str: 自动重连提示;当停止请求已发出时返回空字符串。
"""
if self._stop_requested:
return ""
return f";将在 {server_config.reconnect_delay_sec:g} 秒后重连"
def _describe_terminal_ws_message(
self,
ws: AiohttpClientWebSocketResponse,
ws_message: Any,
message_label: str,
) -> str:
"""描述导致连接结束的终止类 WebSocket 消息。
Args:
ws: 当前活跃的 WebSocket 连接对象。
ws_message: aiohttp 返回的终止消息。
message_label: 当前终止消息的人类可读标签。
Returns:
str: 汇总后的终止原因描述。
"""
details = []
close_code = getattr(ws, "close_code", None)
if close_code not in (None, 0):
details.append(f"close_code={close_code}")
message_data = getattr(ws_message, "data", None)
if message_data not in (None, "", 0, close_code):
details.append(f"data={message_data}")
message_extra = str(getattr(ws_message, "extra", "") or "").strip()
if message_extra:
details.append(f"extra={message_extra}")
ws_exception = ws.exception()
if ws_exception is not None:
details.append(f"exception={ws_exception}")
if not details:
return message_label
return f"{message_label}{', '.join(str(item) for item in details)}"
def _parse_json_message(self, data: Any) -> Optional[Dict[str, Any]]:
"""解析 WebSocket 文本消息中的 JSON 数据。
Args:
data: WebSocket 收到的原始文本数据。
Returns:
Optional[Dict[str, Any]]: 成功时返回字典,失败时返回 ``None``。
"""
try:
payload = json.loads(str(data))
except Exception as exc:
self._logger.warning(f"NapCat 适配器解析 JSON 载荷失败: {exc}")
return None
return payload if isinstance(payload, dict) else None

View File

@@ -0,0 +1,37 @@
"""NapCat 适配器内部共享类型。"""
from __future__ import annotations
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, TypeAlias
from typing_extensions import NotRequired, TypedDict
class NapCatIncomingSegment(TypedDict):
"""NapCat / OneBot 入站消息段结构。"""
type: str
data: Mapping[str, Any]
class NapCatHostMessageSegment(TypedDict):
"""适配器转换后写入 Host 的消息段结构。"""
type: str
data: Any
hash: NotRequired[str]
binary_data_base64: NotRequired[str]
NapCatActionParams: TypeAlias = Mapping[str, Any]
NapCatActionParamsInput: TypeAlias = Optional[Mapping[str, Any]]
NapCatActionResponse: TypeAlias = Dict[str, Any]
NapCatIdInput: TypeAlias = int | str
NapCatMutablePayload: TypeAlias = MutableMapping[str, Any]
NapCatOptionalIdInput: TypeAlias = int | str | None
NapCatPayload: TypeAlias = Mapping[str, Any]
NapCatPayloadDict: TypeAlias = Dict[str, Any]
NapCatPayloadList: TypeAlias = List[Dict[str, Any]]
NapCatIncomingSegments: TypeAlias = List[NapCatIncomingSegment]
NapCatSegment: TypeAlias = NapCatHostMessageSegment
NapCatSegments: TypeAlias = List[NapCatHostMessageSegment]