mirror of
https://github.com/FuQuan233/nonebot-plugin-llmchat.git
synced 2025-09-09 05:10:51 +00:00
Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
4bfcaf3a61 | |||
909026bf6b | |||
14fbe3cb3c | |||
efb25f0727 | |||
d640f16abe | |||
1600cba172 | |||
9f81a38d5b | |||
53d57beba3 | |||
ea635fd147 | |||
5014d3014b | |||
89baec6abc | |||
19ff0026c0 |
5 changed files with 54 additions and 15 deletions
16
README.md
16
README.md
|
@ -18,37 +18,42 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
|
||||||
<img src="https://img.shields.io/pypi/v/nonebot-plugin-llmchat.svg" alt="pypi">
|
<img src="https://img.shields.io/pypi/v/nonebot-plugin-llmchat.svg" alt="pypi">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
|
<a href="https://deepwiki.com/FuQuan233/nonebot-plugin-llmchat"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📖 介绍
|
## 📖 介绍
|
||||||
|
|
||||||
|
1. **支持LLM回复图片**
|
||||||
|
- 支持最新 Gemini 2.5 Flash Image (Nano Banana) 的图片回复
|
||||||
|
- 支持图片上下文修改
|
||||||
|
|
||||||
1. **支持MCP协议**
|
1. **支持MCP协议**
|
||||||
- 可以连接各种支持MCP协议的LLM工具
|
- 可以连接各种支持MCP协议的LLM工具
|
||||||
- 通过连接一些搜索MCP服务器可以实现在线搜索
|
- 通过连接一些搜索MCP服务器可以实现在线搜索
|
||||||
- 兼容 Claude.app 的配置格式
|
- 兼容 Claude.app 的配置格式
|
||||||
|
|
||||||
2. **多API预设支持**
|
1. **多API预设支持**
|
||||||
- 可配置多个LLM服务预设(如不同模型/API密钥)
|
- 可配置多个LLM服务预设(如不同模型/API密钥)
|
||||||
- 支持运行时通过`API预设`命令热切换API配置
|
- 支持运行时通过`API预设`命令热切换API配置
|
||||||
- 内置服务开关功能(预设名为`off`时停用)
|
- 内置服务开关功能(预设名为`off`时停用)
|
||||||
|
|
||||||
3. **多种回复触发方式**
|
1. **多种回复触发方式**
|
||||||
- @触发 + 随机概率触发
|
- @触发 + 随机概率触发
|
||||||
- 支持处理回复消息
|
- 支持处理回复消息
|
||||||
- 群聊消息顺序处理,防止消息错乱
|
- 群聊消息顺序处理,防止消息错乱
|
||||||
|
|
||||||
4. **分群聊上下文记忆管理**
|
1. **分群聊上下文记忆管理**
|
||||||
- 分群聊保留对话历史记录(可配置保留条数)
|
- 分群聊保留对话历史记录(可配置保留条数)
|
||||||
- 自动合并未处理消息,降低API用量
|
- 自动合并未处理消息,降低API用量
|
||||||
- 支持`记忆清除`命令手动重置对话上下文
|
- 支持`记忆清除`命令手动重置对话上下文
|
||||||
|
|
||||||
5. **分段回复支持**
|
1. **分段回复支持**
|
||||||
- 支持多段式回复(由LLM决定如何回复)
|
- 支持多段式回复(由LLM决定如何回复)
|
||||||
- 可@群成员(由LLM插入)
|
- 可@群成员(由LLM插入)
|
||||||
- 可选输出AI的思维过程(需模型支持)
|
- 可选输出AI的思维过程(需模型支持)
|
||||||
|
|
||||||
6. **可自定义性格**
|
1. **可自定义性格**
|
||||||
- 可动态修改群组专属系统提示词(`/修改设定`)
|
- 可动态修改群组专属系统提示词(`/修改设定`)
|
||||||
- 支持自定义默认提示词
|
- 支持自定义默认提示词
|
||||||
|
|
||||||
|
@ -108,6 +113,7 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
|
||||||
| LLMCHAT__RANDOM_TRIGGER_PROB | 否 | 0.05 | 默认随机触发概率 [0, 1] |
|
| LLMCHAT__RANDOM_TRIGGER_PROB | 否 | 0.05 | 默认随机触发概率 [0, 1] |
|
||||||
| LLMCHAT__DEFAULT_PROMPT | 否 | 你的回答应该尽量简洁、幽默、可以使用一些语气词、颜文字。你应该拒绝回答任何政治相关的问题。 | 默认提示词 |
|
| LLMCHAT__DEFAULT_PROMPT | 否 | 你的回答应该尽量简洁、幽默、可以使用一些语气词、颜文字。你应该拒绝回答任何政治相关的问题。 | 默认提示词 |
|
||||||
| LLMCHAT__BLACKLIST_USER_IDS | 否 | [] | 黑名单用户ID列表,机器人将不会处理黑名单用户的消息 |
|
| LLMCHAT__BLACKLIST_USER_IDS | 否 | [] | 黑名单用户ID列表,机器人将不会处理黑名单用户的消息 |
|
||||||
|
| LLMCHAT__IGNORE_PREFIXES | 否 | [] | 需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理 |
|
||||||
| LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置,具体见下表 |
|
| LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置,具体见下表 |
|
||||||
|
|
||||||
其中LLMCHAT__API_PRESETS为一个列表,每项配置有以下的配置项
|
其中LLMCHAT__API_PRESETS为一个列表,每项配置有以下的配置项
|
||||||
|
|
|
@ -21,7 +21,7 @@ from nonebot import (
|
||||||
on_message,
|
on_message,
|
||||||
require,
|
require,
|
||||||
)
|
)
|
||||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message
|
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegment
|
||||||
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
|
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
|
||||||
from nonebot.params import CommandArg
|
from nonebot.params import CommandArg
|
||||||
from nonebot.permission import SUPERUSER
|
from nonebot.permission import SUPERUSER
|
||||||
|
@ -46,7 +46,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="llmchat",
|
name="llmchat",
|
||||||
description="支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件",
|
description="支持多API预设、MCP协议、联网搜索、视觉模型、Nano Banana(生图模型)的AI群聊插件",
|
||||||
usage="""@机器人 + 消息 开启对话""",
|
usage="""@机器人 + 消息 开启对话""",
|
||||||
type="application",
|
type="application",
|
||||||
homepage="https://github.com/FuQuan233/nonebot-plugin-llmchat",
|
homepage="https://github.com/FuQuan233/nonebot-plugin-llmchat",
|
||||||
|
@ -169,6 +169,12 @@ async def is_triggered(event: GroupMessageEvent) -> bool:
|
||||||
if event.user_id in plugin_config.blacklist_user_ids:
|
if event.user_id in plugin_config.blacklist_user_ids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 忽略特定前缀的消息
|
||||||
|
msg_text = event.get_plaintext().strip()
|
||||||
|
for prefix in plugin_config.ignore_prefixes:
|
||||||
|
if msg_text.startswith(prefix):
|
||||||
|
return False
|
||||||
|
|
||||||
state.past_events.append(event)
|
state.past_events.append(event)
|
||||||
|
|
||||||
# 原有@触发条件
|
# 原有@触发条件
|
||||||
|
@ -313,8 +319,10 @@ async def process_messages(group_id: int):
|
||||||
content: list[ChatCompletionContentPartParam] = []
|
content: list[ChatCompletionContentPartParam] = []
|
||||||
|
|
||||||
# 将机器人错过的消息推送给LLM
|
# 将机器人错过的消息推送给LLM
|
||||||
for ev in state.past_events:
|
past_events_snapshot = list(state.past_events)
|
||||||
content.append({"type": "text", "text": format_message(ev)})
|
for ev in past_events_snapshot:
|
||||||
|
text_content = format_message(ev)
|
||||||
|
content.append({"type": "text", "text": text_content})
|
||||||
|
|
||||||
# 将消息中的图片转成 base64
|
# 将消息中的图片转成 base64
|
||||||
if preset.support_image:
|
if preset.support_image:
|
||||||
|
@ -378,7 +386,7 @@ async def process_messages(group_id: int):
|
||||||
new_messages.append({
|
new_messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": tool_call.id,
|
"tool_call_id": tool_call.id,
|
||||||
"content": str(result.content)
|
"content": str(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
# 将工具调用的结果交给 LLM
|
# 将工具调用的结果交给 LLM
|
||||||
|
@ -399,10 +407,18 @@ async def process_messages(group_id: int):
|
||||||
or matched_reasoning_content
|
or matched_reasoning_content
|
||||||
)
|
)
|
||||||
|
|
||||||
new_messages.append({
|
llm_reply: ChatCompletionMessageParam = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": reply,
|
"content": reply,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
reply_images = getattr(response.choices[0].message, "images", None)
|
||||||
|
|
||||||
|
if reply_images:
|
||||||
|
# openai的sdk里的assistant消息暂时没有images字段,需要单独处理
|
||||||
|
llm_reply["images"] = reply_images # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
|
new_messages.append(llm_reply)
|
||||||
|
|
||||||
# 请求成功后再保存历史记录,保证user和assistant穿插,防止R1模型报错
|
# 请求成功后再保存历史记录,保证user和assistant穿插,防止R1模型报错
|
||||||
for message in new_messages:
|
for message in new_messages:
|
||||||
|
@ -424,6 +440,14 @@ async def process_messages(group_id: int):
|
||||||
assert reply is not None
|
assert reply is not None
|
||||||
await send_split_messages(handler, reply)
|
await send_split_messages(handler, reply)
|
||||||
|
|
||||||
|
if reply_images:
|
||||||
|
logger.debug(f"API响应 图片数:{len(reply_images)}")
|
||||||
|
for i, image in enumerate(reply_images, start=1):
|
||||||
|
logger.debug(f"正在发送第{i}张图片")
|
||||||
|
image_base64 = image["image_url"]["url"].removeprefix("data:image/png;base64,")
|
||||||
|
image_msg = MessageSegment.image(base64.b64decode(image_base64))
|
||||||
|
await handler.send(image_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(exception=e).error(f"API请求失败 群号:{group_id}")
|
logger.opt(exception=e).error(f"API请求失败 群号:{group_id}")
|
||||||
await handler.send(Message(f"服务暂时不可用,请稍后再试\n{e!s}"))
|
await handler.send(Message(f"服务暂时不可用,请稍后再试\n{e!s}"))
|
||||||
|
|
|
@ -44,6 +44,10 @@ class ScopedConfig(BaseModel):
|
||||||
)
|
)
|
||||||
mcp_servers: dict[str, MCPServerConfig] = Field({}, description="MCP服务器配置")
|
mcp_servers: dict[str, MCPServerConfig] = Field({}, description="MCP服务器配置")
|
||||||
blacklist_user_ids: set[int] = Field(set(), description="黑名单用户ID列表")
|
blacklist_user_ids: set[int] = Field(set(), description="黑名单用户ID列表")
|
||||||
|
ignore_prefixes: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
|
|
||||||
from mcp import ClientSession, StdioServerParameters
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
@ -64,9 +65,13 @@ class MCPClient:
|
||||||
server_name, real_tool_name = tool_name.split("___")
|
server_name, real_tool_name = tool_name.split("___")
|
||||||
logger.info(f"正在服务器[{server_name}]上调用工具[{real_tool_name}]")
|
logger.info(f"正在服务器[{server_name}]上调用工具[{real_tool_name}]")
|
||||||
session = self.sessions[server_name]
|
session = self.sessions[server_name]
|
||||||
response = await session.call_tool(real_tool_name, tool_args)
|
try:
|
||||||
|
response = await asyncio.wait_for(session.call_tool(real_tool_name, tool_args), timeout=30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"调用工具[{real_tool_name}]超时")
|
||||||
|
return f"调用工具[{real_tool_name}]超时"
|
||||||
logger.debug(f"工具[{real_tool_name}]调用完成,响应: {response}")
|
logger.debug(f"工具[{real_tool_name}]调用完成,响应: {response}")
|
||||||
return response
|
return response.content
|
||||||
|
|
||||||
def get_friendly_name(self, tool_name: str):
|
def get_friendly_name(self, tool_name: str):
|
||||||
server_name, real_tool_name = tool_name.split("___")
|
server_name, real_tool_name = tool_name.split("___")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nonebot-plugin-llmchat"
|
name = "nonebot-plugin-llmchat"
|
||||||
version = "0.2.3"
|
version = "0.3.0"
|
||||||
description = "Nonebot AI group chat plugin supporting multiple API preset configurations"
|
description = "Nonebot AI group chat plugin supporting multiple API preset configurations"
|
||||||
license = "GPL"
|
license = "GPL"
|
||||||
authors = ["FuQuan i@fuquan.moe"]
|
authors = ["FuQuan i@fuquan.moe"]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue