mirror of
https://github.com/FuQuan233/nonebot-plugin-llmchat.git
synced 2026-02-05 03:28:05 +00:00
Compare commits
No commits in common. "21421c4754d69ccfe856f6597035ff4d1efb00d0" and "b4f7b2797ca7596d5095fab29dc0a7d0f66fbc93" have entirely different histories.
21421c4754
...
b4f7b2797c
5 changed files with 5 additions and 268 deletions
23
README.md
23
README.md
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
# nonebot-plugin-llmchat
|
||||
|
||||
_✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型的AI群聊插件 ✨_
|
||||
_✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件 ✨_
|
||||
|
||||
|
||||
<a href="./LICENSE">
|
||||
|
|
@ -33,11 +33,6 @@ _✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型
|
|||
- 通过连接一些搜索MCP服务器可以实现在线搜索
|
||||
- 兼容 Claude.app 的配置格式
|
||||
|
||||
1. **内置工具**
|
||||
- 内置OneBot群操作工具,LLM可直接进行群管理操作(需模型支持tool_call)
|
||||
- 支持禁言用户、获取群信息、查看群成员等功能
|
||||
- 支持戳一戳、撤回消息等互动功能
|
||||
|
||||
1. **多API预设支持**
|
||||
- 可配置多个LLM服务预设(如不同模型/API密钥)
|
||||
- 支持运行时通过`API预设`命令热切换API配置
|
||||
|
|
@ -121,22 +116,6 @@ _✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型
|
|||
| 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为一个列表,每项配置有以下的配置项
|
||||
| 配置项 | 必填 | 默认值 | 说明 |
|
||||
|:-----:|:----:|:----:|:----:|
|
||||
|
|
|
|||
|
|
@ -381,13 +381,8 @@ async def process_messages(group_id: int):
|
|||
# 发送工具调用提示
|
||||
await handler.send(Message(f"正在使用{mcp_client.get_friendly_name(tool_name)}"))
|
||||
|
||||
# 执行工具调用,传递群组和机器人信息用于QQ工具
|
||||
result = await mcp_client.call_tool(
|
||||
tool_name,
|
||||
tool_args,
|
||||
group_id=event.group_id,
|
||||
bot_id=str(event.self_id)
|
||||
)
|
||||
# 执行工具调用
|
||||
result = await mcp_client.call_tool(tool_name, tool_args)
|
||||
|
||||
new_messages.append({
|
||||
"role": "tool",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from mcp.client.stdio import stdio_client
|
|||
from nonebot import logger
|
||||
|
||||
from .config import MCPServerConfig
|
||||
from .onebottools import OneBotTools
|
||||
|
||||
|
||||
class MCPClient:
|
||||
|
|
@ -33,8 +32,6 @@ class MCPClient:
|
|||
# 添加工具列表缓存
|
||||
self._tools_cache: list | None = None
|
||||
self._cache_initialized = False
|
||||
# 初始化OneBot工具
|
||||
self.onebot_tools = OneBotTools()
|
||||
self._initialized = True
|
||||
logger.debug("MCPClient单例初始化成功")
|
||||
|
||||
|
|
@ -115,12 +112,6 @@ 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:
|
||||
|
|
@ -146,16 +137,8 @@ class MCPClient:
|
|||
logger.info(f"工具列表缓存完成,共缓存{len(available_tools)}个工具")
|
||||
return available_tools
|
||||
|
||||
async def call_tool(self, tool_name: str, tool_args: dict, group_id: int | None = None, bot_id: str | None = None):
|
||||
async def call_tool(self, tool_name: str, tool_args: dict):
|
||||
"""按需连接调用工具,调用后立即断开"""
|
||||
# 检查是否是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}]")
|
||||
|
||||
|
|
@ -170,11 +153,6 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
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}"
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "nonebot-plugin-llmchat"
|
||||
version = "0.4.0"
|
||||
version = "0.3.1"
|
||||
description = "Nonebot AI group chat plugin supporting multiple API preset configurations"
|
||||
license = "GPL"
|
||||
authors = ["FuQuan i@fuquan.moe"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue