新增内置OneBot工具支持,包括禁言、获取群信息等功能,并优化工具调用逻辑

This commit is contained in:
FuQuan233 2025-10-31 17:07:10 +08:00
parent b4f7b2797c
commit 63e446d5e4
4 changed files with 267 additions and 4 deletions

View file

@ -8,7 +8,7 @@
# nonebot-plugin-llmchat
_✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件 ✨_
_✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型的AI群聊插件 ✨_
<a href="./LICENSE">
@ -33,6 +33,11 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
- 通过连接一些搜索MCP服务器可以实现在线搜索
- 兼容 Claude.app 的配置格式
1. **内置工具**
- 内置OneBot群操作工具LLM可直接进行群管理操作需模型支持tool_call
- 支持禁言用户、获取群信息、查看群成员等功能
- 支持戳一戳、撤回消息等互动功能
1. **多API预设支持**
- 可配置多个LLM服务预设如不同模型/API密钥
- 支持运行时通过`API预设`命令热切换API配置
@ -116,6 +121,22 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
| LLMCHAT__IGNORE_PREFIXES | 否 | [] | 需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理 |
| LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置具体见下表 |
### 内置OneBot工具
插件内置了以下工具LLM可以直接调用这些工具进行群操作需模型支持tool_call这些工具不需要额外配置
| 工具名称 | 说明 | 权限要求 |
|:-----:|:----:|:----:|
| ob__mute_user | 禁言指定用户 | 机器人需要管理员权限 |
| ob__get_group_info | 获取群信息 | 无 |
| ob__get_group_member_info | 获取指定群成员信息 | 无 |
| ob__get_group_member_list | 获取群成员列表 | 无 |
| ob__poke_user | 戳一戳指定用户 | 无 |
| ob__recall_message | 撤回指定消息 | 机器人需要管理员权限或为消息发送者 |
### MCP服务器配置
其中LLMCHAT__API_PRESETS为一个列表每项配置有以下的配置项
| 配置项 | 必填 | 默认值 | 说明 |
|:-----:|:----:|:----:|:----:|

View file

@ -381,8 +381,13 @@ async def process_messages(group_id: int):
# 发送工具调用提示
await handler.send(Message(f"正在使用{mcp_client.get_friendly_name(tool_name)}"))
# 执行工具调用
result = await mcp_client.call_tool(tool_name, tool_args)
# 执行工具调用传递群组和机器人信息用于QQ工具
result = await mcp_client.call_tool(
tool_name,
tool_args,
group_id=event.group_id,
bot_id=str(event.self_id)
)
new_messages.append({
"role": "tool",

View file

@ -7,6 +7,7 @@ from mcp.client.stdio import stdio_client
from nonebot import logger
from .config import MCPServerConfig
from .onebottools import OneBotTools
class MCPClient:
@ -32,6 +33,8 @@ class MCPClient:
# 添加工具列表缓存
self._tools_cache: list | None = None
self._cache_initialized = False
# 初始化OneBot工具
self.onebot_tools = OneBotTools()
self._initialized = True
logger.debug("MCPClient单例初始化成功")
@ -112,6 +115,12 @@ class MCPClient:
logger.info(f"初始化工具列表缓存,需要连接{len(self.server_config)}个服务器")
available_tools = []
# 添加OneBot内置工具
onebot_tools = self.onebot_tools.get_available_tools()
available_tools.extend(onebot_tools)
logger.debug(f"添加了{len(onebot_tools)}个OneBot内置工具")
# 添加MCP服务器工具
for server_name in self.server_config.keys():
logger.debug(f"正在从服务器[{server_name}]获取工具列表")
async with self._create_session_context(server_name) as session:
@ -137,8 +146,16 @@ class MCPClient:
logger.info(f"工具列表缓存完成,共缓存{len(available_tools)}个工具")
return available_tools
async def call_tool(self, tool_name: str, tool_args: dict):
async def call_tool(self, tool_name: str, tool_args: dict, group_id: int | None = None, bot_id: str | None = None):
"""按需连接调用工具,调用后立即断开"""
# 检查是否是QQ工具
if tool_name.startswith("ob__"):
if group_id is None or bot_id is None:
return "QQ工具需要提供group_id和bot_id参数"
logger.info(f"调用OneBot工具[{tool_name}]")
return await self.onebot_tools.call_tool(tool_name, tool_args, group_id, bot_id)
# MCP工具处理
server_name, real_tool_name = tool_name.split("___")
logger.info(f"按需连接到服务器[{server_name}]调用工具[{real_tool_name}]")
@ -153,6 +170,11 @@ class MCPClient:
def get_friendly_name(self, tool_name: str):
logger.debug(tool_name)
# 检查是否是OneBot工具
if tool_name.startswith("ob__"):
return self.onebot_tools.get_friendly_name(tool_name)
# MCP工具处理
server_name, real_tool_name = tool_name.split("___")
return (self.server_config[server_name].friendly_name or server_name) + " - " + real_tool_name

View file

@ -0,0 +1,215 @@
import json
import time
from typing import Any, cast
from nonebot import get_bot, logger
from nonebot.adapters.onebot.v11 import Bot
class OneBotTools:
"""内置的OneBot群操作工具类"""
def __init__(self):
self.tools = [
{
"type": "function",
"function": {
"name": "ob__mute_user",
"description": "禁言指定用户一段时间。需要机器人有管理员权限。不要随便禁言别人,你应该只听群主或者管理员你的话。",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "要禁言的用户QQ号"},
"duration": {
"type": "integer",
"description": "禁言时长0表示解除禁言最大259200030天",
"minimum": 0,
"maximum": 2592000,
},
},
"required": ["user_id", "duration"],
},
},
},
{
"type": "function",
"function": {
"name": "ob__get_group_info",
"description": "获取群信息,包括群成员数量、群名称等。",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "ob__get_group_member_info",
"description": "获取指定群成员的信息。",
"parameters": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "要查询的用户QQ号"}},
"required": ["user_id"],
},
},
},
{
"type": "function",
"function": {
"name": "ob__get_group_member_list",
"description": "获取群成员列表。",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "ob__poke_user",
"description": "戳一戳指定用户。",
"parameters": {
"type": "object",
"properties": {"user_id": {"type": "string", "description": "要戳一戳的用户QQ号"}},
"required": ["user_id"],
},
},
},
{
"type": "function",
"function": {
"name": "ob__recall_message",
"description": "撤回指定消息。需要机器人有管理员权限或者是消息发送者。",
"parameters": {
"type": "object",
"properties": {"message_id": {"type": "integer", "description": "要撤回的消息ID"}},
"required": ["message_id"],
},
},
},
]
def get_friendly_name(self, tool_name: str) -> str:
"""获取工具的友好名称"""
friendly_names = {
"ob__mute_user": "OneBot - 禁言用户",
"ob__get_group_info": "OneBot - 获取群信息",
"ob__get_group_member_info": "OneBot - 获取成员信息",
"ob__get_group_member_list": "OneBot - 获取成员列表",
"ob__poke_user": "OneBot - 戳一戳用户",
"ob__recall_message": "OneBot - 撤回消息",
}
return friendly_names.get(tool_name, tool_name)
def get_available_tools(self) -> list[dict[str, Any]]:
"""获取可用的工具列表"""
return self.tools
async def call_tool(self, tool_name: str, tool_args: dict[str, Any], group_id: int, bot_id: str) -> str:
"""调用指定的工具"""
try:
bot = cast(Bot, get_bot(bot_id))
if tool_name == "ob__mute_user":
return await self._mute_user(bot, group_id, tool_args)
elif tool_name == "ob__get_group_info":
return await self._get_group_info(bot, group_id, tool_args)
elif tool_name == "ob__get_group_member_info":
return await self._get_group_member_info(bot, group_id, tool_args)
elif tool_name == "ob__get_group_member_list":
return await self._get_group_member_list(bot, group_id, tool_args)
elif tool_name == "ob__poke_user":
return await self._poke_user(bot, group_id, tool_args)
elif tool_name == "ob__recall_message":
return await self._recall_message(bot, group_id, tool_args)
else:
return f"未知的工具: {tool_name}"
except Exception as e:
logger.error(f"调用OneBot工具 {tool_name} 时出错: {e}")
return f"执行失败: {e!s}"
async def _mute_user(self, bot: Bot, group_id: int, args: dict[str, Any]) -> str:
"""禁言用户"""
user_id = int(args["user_id"])
duration = args["duration"]
try:
await bot.set_group_ban(group_id=group_id, user_id=user_id, duration=duration)
if duration > 0:
return f"成功禁言用户 {user_id},时长 {duration}"
else:
return f"成功解除用户 {user_id} 的禁言"
except Exception as e:
return f"禁言操作失败: {e!s}"
async def _get_group_info(self, bot: Bot, group_id: int, _args: dict[str, Any]) -> str:
"""获取群信息"""
try:
group_info = await bot.get_group_info(group_id=group_id)
info = {
"群号": group_info["group_id"],
"群名称": group_info["group_name"],
"群成员数": group_info["member_count"],
"群上限": group_info["max_member_count"],
}
return json.dumps(info, ensure_ascii=False, indent=2)
except Exception as e:
return f"获取群信息失败: {e!s}"
async def _get_group_member_info(self, bot: Bot, group_id: int, args: dict[str, Any]) -> str:
"""获取群成员信息"""
user_id = int(args["user_id"])
try:
member_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id)
info = {
"用户QQ": member_info["user_id"],
"昵称": member_info["nickname"],
"群名片": member_info["card"],
"性别": member_info["sex"],
"年龄": member_info["age"],
"地区": member_info["area"],
"加群时间": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(member_info["join_time"])),
"最后发言时间": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(member_info["last_sent_time"])),
"群内等级": member_info["level"],
"角色": member_info["role"],
"专属头衔": member_info["title"],
}
return json.dumps(info, ensure_ascii=False, indent=2)
except Exception as e:
return f"获取成员信息失败: {e!s}"
async def _get_group_member_list(self, bot: Bot, group_id: int, _args: dict[str, Any]) -> str:
"""获取群成员列表"""
try:
member_list = await bot.get_group_member_list(group_id=group_id)
members = []
for member in member_list:
members.append(
{"QQ": member["user_id"], "昵称": member["nickname"], "群名片": member["card"], "角色": member["role"]}
)
result = {"群成员总数": len(members), "成员列表": members}
return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
return f"获取群成员列表失败: {e!s}"
async def _poke_user(self, bot: Bot, group_id: int, args: dict[str, Any]) -> str:
"""戳一戳用户"""
user_id = int(args["user_id"])
try:
# 使用OneBot的戳一戳API
await bot.call_api("group_poke", group_id=group_id, user_id=user_id)
return f"成功戳了戳用户 {user_id}"
except Exception as e:
return f"戳一戳失败: {e!s}"
async def _recall_message(self, bot: Bot, group_id: int, args: dict[str, Any]) -> str:
"""撤回消息"""
message_id = int(args["message_id"])
try:
await bot.delete_msg(message_id=message_id)
return f"成功撤回消息 {message_id}"
except Exception as e:
return f"撤回消息失败: {e!s}"