chore: import private baseline from gitea state
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
54
plugin-templates/MaiBot-Napcat-Adapter/.github/workflows/docker-image.yml
vendored
Normal file
54
plugin-templates/MaiBot-Napcat-Adapter/.github/workflows/docker-image.yml
vendored
Normal 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 }}
|
||||
1
plugin-templates/MaiBot-Napcat-Adapter/__init__.py
Normal file
1
plugin-templates/MaiBot-Napcat-Adapter/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""NapCat 内置适配器插件包。"""
|
||||
35
plugin-templates/MaiBot-Napcat-Adapter/_manifest.json
Normal file
35
plugin-templates/MaiBot-Napcat-Adapter/_manifest.json
Normal 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"
|
||||
}
|
||||
18
plugin-templates/MaiBot-Napcat-Adapter/apis/__init__.py
Normal file
18
plugin-templates/MaiBot-Napcat-Adapter/apis/__init__.py
Normal 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",
|
||||
]
|
||||
366
plugin-templates/MaiBot-Napcat-Adapter/apis/account.py
Normal file
366
plugin-templates/MaiBot-Napcat-Adapter/apis/account.py
Normal 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)
|
||||
535
plugin-templates/MaiBot-Napcat-Adapter/apis/file.py
Normal file
535
plugin-templates/MaiBot-Napcat-Adapter/apis/file.py
Normal 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)
|
||||
593
plugin-templates/MaiBot-Napcat-Adapter/apis/group.py
Normal file
593
plugin-templates/MaiBot-Napcat-Adapter/apis/group.py
Normal 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)
|
||||
431
plugin-templates/MaiBot-Napcat-Adapter/apis/message.py
Normal file
431
plugin-templates/MaiBot-Napcat-Adapter/apis/message.py
Normal 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)
|
||||
@@ -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
|
||||
275
plugin-templates/MaiBot-Napcat-Adapter/apis/support.py
Normal file
275
plugin-templates/MaiBot-Napcat-Adapter/apis/support.py
Normal 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)
|
||||
290
plugin-templates/MaiBot-Napcat-Adapter/apis/system.py
Normal file
290
plugin-templates/MaiBot-Napcat-Adapter/apis/system.py
Normal 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)
|
||||
@@ -0,0 +1 @@
|
||||
"""NapCat 编解码组件导出。"""
|
||||
@@ -0,0 +1,5 @@
|
||||
"""NapCat 入站编解码导出。"""
|
||||
|
||||
from .message_codec import NapCatInboundCodec
|
||||
|
||||
__all__ = ["NapCatInboundCodec"]
|
||||
545
plugin-templates/MaiBot-Napcat-Adapter/codecs/inbound/cards.py
Normal file
545
plugin-templates/MaiBot-Napcat-Adapter/codecs/inbound/cards.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
"""NapCat 通知编解码导出。"""
|
||||
|
||||
from .message_codec import NapCatNoticeCodec
|
||||
|
||||
__all__ = ["NapCatNoticeCodec"]
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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} 状态异常")
|
||||
@@ -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(".")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""NapCat 出站编解码导出。"""
|
||||
|
||||
from .message_codec import NapCatOutboundCodec
|
||||
|
||||
__all__ = ["NapCatOutboundCodec"]
|
||||
@@ -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}
|
||||
@@ -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'}]"}}
|
||||
631
plugin-templates/MaiBot-Napcat-Adapter/config.py
Normal file
631
plugin-templates/MaiBot-Napcat-Adapter/config.py
Normal 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
|
||||
13
plugin-templates/MaiBot-Napcat-Adapter/constants.py
Normal file
13
plugin-templates/MaiBot-Napcat-Adapter/constants.py
Normal 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
|
||||
90
plugin-templates/MaiBot-Napcat-Adapter/docs/README.md
Normal file
90
plugin-templates/MaiBot-Napcat-Adapter/docs/README.md
Normal 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 | 文件、群文件、在线文件、相册、流式传输。 |
|
||||
59
plugin-templates/MaiBot-Napcat-Adapter/docs/account-api.md
Normal file
59
plugin-templates/MaiBot-Napcat-Adapter/docs/account-api.md
Normal 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"},
|
||||
)
|
||||
```
|
||||
83
plugin-templates/MaiBot-Napcat-Adapter/docs/file-api.md
Normal file
83
plugin-templates/MaiBot-Napcat-Adapter/docs/file-api.md
Normal 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",
|
||||
},
|
||||
)
|
||||
```
|
||||
70
plugin-templates/MaiBot-Napcat-Adapter/docs/group-api.md
Normal file
70
plugin-templates/MaiBot-Napcat-Adapter/docs/group-api.md
Normal 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": "你好"}}],
|
||||
},
|
||||
)
|
||||
```
|
||||
61
plugin-templates/MaiBot-Napcat-Adapter/docs/message-api.md
Normal file
61
plugin-templates/MaiBot-Napcat-Adapter/docs/message-api.md
Normal 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"}}],
|
||||
},
|
||||
)
|
||||
```
|
||||
54
plugin-templates/MaiBot-Napcat-Adapter/docs/system-api.md
Normal file
54
plugin-templates/MaiBot-Napcat-Adapter/docs/system-api.md
Normal 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},
|
||||
)
|
||||
```
|
||||
83
plugin-templates/MaiBot-Napcat-Adapter/docs/typed-api.md
Normal file
83
plugin-templates/MaiBot-Napcat-Adapter/docs/typed-api.md
Normal 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,
|
||||
)
|
||||
```
|
||||
55
plugin-templates/MaiBot-Napcat-Adapter/docs/verification.md
Normal file
55
plugin-templates/MaiBot-Napcat-Adapter/docs/verification.md
Normal 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"` 默认值。
|
||||
82
plugin-templates/MaiBot-Napcat-Adapter/filters.py
Normal file
82
plugin-templates/MaiBot-Napcat-Adapter/filters.py
Normal 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
|
||||
148
plugin-templates/MaiBot-Napcat-Adapter/heartbeat_monitor.py
Normal file
148
plugin-templates/MaiBot-Napcat-Adapter/heartbeat_monitor.py
Normal 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}")
|
||||
244
plugin-templates/MaiBot-Napcat-Adapter/plugin.py
Normal file
244
plugin-templates/MaiBot-Napcat-Adapter/plugin.py
Normal 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()
|
||||
226
plugin-templates/MaiBot-Napcat-Adapter/qq_emoji_list.py
Normal file
226
plugin-templates/MaiBot-Napcat-Adapter/qq_emoji_list.py
Normal 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": "[表情:瞪眼]",
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"""NapCat 运行时组件导出。"""
|
||||
|
||||
from .builder import NapCatRuntimeBuilder
|
||||
from .bundle import NapCatRuntimeBundle
|
||||
from .router import NapCatEventRouter
|
||||
|
||||
__all__ = ["NapCatEventRouter", "NapCatRuntimeBuilder", "NapCatRuntimeBundle"]
|
||||
105
plugin-templates/MaiBot-Napcat-Adapter/runtime/builder.py
Normal file
105
plugin-templates/MaiBot-Napcat-Adapter/runtime/builder.py
Normal 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,
|
||||
)
|
||||
40
plugin-templates/MaiBot-Napcat-Adapter/runtime/bundle.py
Normal file
40
plugin-templates/MaiBot-Napcat-Adapter/runtime/bundle.py
Normal 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
|
||||
611
plugin-templates/MaiBot-Napcat-Adapter/runtime/router.py
Normal file
611
plugin-templates/MaiBot-Napcat-Adapter/runtime/router.py
Normal 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
|
||||
118
plugin-templates/MaiBot-Napcat-Adapter/runtime_state.py
Normal file
118
plugin-templates/MaiBot-Napcat-Adapter/runtime_state.py
Normal 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
|
||||
19
plugin-templates/MaiBot-Napcat-Adapter/services/__init__.py
Normal file
19
plugin-templates/MaiBot-Napcat-Adapter/services/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
176
plugin-templates/MaiBot-Napcat-Adapter/services/ban_tracker.py
Normal file
176
plugin-templates/MaiBot-Napcat-Adapter/services/ban_tracker.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
585
plugin-templates/MaiBot-Napcat-Adapter/services/query_service.py
Normal file
585
plugin-templates/MaiBot-Napcat-Adapter/services/query_service.py
Normal 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
|
||||
449
plugin-templates/MaiBot-Napcat-Adapter/transport.py
Normal file
449
plugin-templates/MaiBot-Napcat-Adapter/transport.py
Normal 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
|
||||
37
plugin-templates/MaiBot-Napcat-Adapter/types.py
Normal file
37
plugin-templates/MaiBot-Napcat-Adapter/types.py
Normal 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]
|
||||
Reference in New Issue
Block a user