diff --git a/README.md b/README.md index f945db3..21ebf63 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # nonebot-plugin-llmchat -_✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件 ✨_ +_✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型的AI群聊插件 ✨_ @@ -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为一个列表,每项配置有以下的配置项 | 配置项 | 必填 | 默认值 | 说明 | |:-----:|:----:|:----:|:----:| diff --git a/nonebot_plugin_llmchat/__init__.py b/nonebot_plugin_llmchat/__init__.py index ab07224..acfa403 100755 --- a/nonebot_plugin_llmchat/__init__.py +++ b/nonebot_plugin_llmchat/__init__.py @@ -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", diff --git a/nonebot_plugin_llmchat/mcpclient.py b/nonebot_plugin_llmchat/mcpclient.py index d0bc80e..09f5b73 100644 --- a/nonebot_plugin_llmchat/mcpclient.py +++ b/nonebot_plugin_llmchat/mcpclient.py @@ -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 diff --git a/nonebot_plugin_llmchat/onebottools.py b/nonebot_plugin_llmchat/onebottools.py new file mode 100644 index 0000000..ff19fa2 --- /dev/null +++ b/nonebot_plugin_llmchat/onebottools.py @@ -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表示解除禁言,最大2592000(30天)", + "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}" + +