diff --git a/README.md b/README.md index 356effc..2f41e31 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插 pypi python -Ask DeepWiki @@ -109,7 +108,6 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插 | LLMCHAT__RANDOM_TRIGGER_PROB | 否 | 0.05 | 默认随机触发概率 [0, 1] | | LLMCHAT__DEFAULT_PROMPT | 否 | 你的回答应该尽量简洁、幽默、可以使用一些语气词、颜文字。你应该拒绝回答任何政治相关的问题。 | 默认提示词 | | LLMCHAT__BLACKLIST_USER_IDS | 否 | [] | 黑名单用户ID列表,机器人将不会处理黑名单用户的消息 | -| LLMCHAT__IGNORE_PREFIXES | 否 | [] | 需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理 | | LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置,具体见下表 | 其中LLMCHAT__API_PRESETS为一个列表,每项配置有以下的配置项 diff --git a/nonebot_plugin_llmchat/__init__.py b/nonebot_plugin_llmchat/__init__.py index d3c6605..b2021c2 100755 --- a/nonebot_plugin_llmchat/__init__.py +++ b/nonebot_plugin_llmchat/__init__.py @@ -40,13 +40,14 @@ from nonebot_plugin_apscheduler import scheduler if TYPE_CHECKING: from openai.types.chat import ( - ChatCompletionContentPartParam, + ChatCompletionContentPartImageParam, + ChatCompletionContentPartTextParam, ChatCompletionMessageParam, ) __plugin_meta__ = PluginMetadata( name="llmchat", - description="支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件", + description="支持多API预设、MCP协议、联网搜索的AI群聊插件", usage="""@机器人 + 消息 开启对话""", type="application", homepage="https://github.com/FuQuan233/nonebot-plugin-llmchat", @@ -169,12 +170,6 @@ async def is_triggered(event: GroupMessageEvent) -> bool: if event.user_id in plugin_config.blacklist_user_ids: 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) # 原有@触发条件 @@ -191,7 +186,7 @@ async def is_triggered(event: GroupMessageEvent) -> bool: # 消息处理器 handler = on_message( rule=Rule(is_triggered), - priority=99, + priority=10, block=False, ) @@ -216,7 +211,7 @@ async def process_images(event: GroupMessageEvent) -> list[str]: base64_images = [] for segement in event.get_message(): if segement.type == "image": - image_url = segement.data.get("url") or segement.data.get("file") + image_url = segement.data.get("url") if image_url: try: # 处理高版本 httpx 的 [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] 报错 @@ -239,20 +234,6 @@ async def process_images(event: GroupMessageEvent) -> list[str]: logger.debug(f"共处理 {len(base64_images)} 张图片") return base64_images -async def send_split_messages(message_handler, content: str): - """ - 将消息按分隔符分段并发送 - """ - logger.info(f"准备发送分段消息,分段数:{len(content.split(''))}") - for segment in content.split(""): - # 跳过空消息 - if not segment.strip(): - continue - segment = segment.strip() # 删除前后多余的换行和空格 - await asyncio.sleep(2) # 避免发送过快 - logger.debug(f"发送消息分段 内容:{segment[:50]}...") # 只记录前50个字符避免日志过大 - await message_handler.send(Message(segment)) - async def process_messages(group_id: int): state = group_states[group_id] preset = get_preset(group_id) @@ -316,19 +297,18 @@ async def process_messages(group_id: int): if state.past_events.__len__() < 1: break - content: list[ChatCompletionContentPartParam] = [] + # 将消息中的图片转成 base64 + base64_images = [] + if preset.support_image: + base64_images = await process_images(event) # 将机器人错过的消息推送给LLM - past_events_snapshot = list(state.past_events) - for ev in past_events_snapshot: - text_content = format_message(ev) - content.append({"type": "text", "text": text_content}) - - # 将消息中的图片转成 base64 - if preset.support_image: - base64_images = await process_images(ev) - for base64_image in base64_images: - content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}) + text_content = ",".join([format_message(ev) for ev in state.past_events]) + content: list[ChatCompletionContentPartTextParam | ChatCompletionContentPartImageParam] = [ + {"type": "text", "text": text_content} + ] + for base64_image in base64_images: + content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}) new_messages: list[ChatCompletionMessageParam] = [ {"role": "user", "content": content} @@ -370,7 +350,7 @@ async def process_messages(group_id: int): # 发送LLM调用工具时的回复,一般没有 if message.content: - await send_split_messages(handler, message.content) + await handler.send(Message(message.content)) # 处理每个工具调用 for tool_call in message.tool_calls: @@ -386,7 +366,7 @@ async def process_messages(group_id: int): new_messages.append({ "role": "tool", "tool_call_id": tool_call.id, - "content": str(result) + "content": str(result.content) }) # 将工具调用的结果交给 LLM @@ -430,7 +410,20 @@ async def process_messages(group_id: int): logger.error(f"合并转发消息发送失败:\n{e!s}\n") assert reply is not None - await send_split_messages(handler, reply) + logger.info( + f"准备发送回复消息 群号:{group_id} 消息分段数:{len(reply.split(''))}" + ) + for r in reply.split(""): + # 似乎会有空消息的情况导致string index out of range异常 + if len(r) == 0 or r.isspace(): + continue + # 删除前后多余的换行和空格 + r = r.strip() + await asyncio.sleep(2) + logger.debug( + f"发送消息分段 内容:{r[:50]}..." + ) # 只记录前50个字符避免日志过大 + await handler.send(Message(r)) except Exception as e: logger.opt(exception=e).error(f"API请求失败 群号:{group_id}") @@ -467,7 +460,7 @@ async def handle_preset(event: GroupMessageEvent, args: Message = CommandArg()): edit_preset_handler = on_command( "修改设定", - priority=1, + priority=99, block=True, permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER), ) @@ -484,7 +477,7 @@ async def handle_edit_preset(event: GroupMessageEvent, args: Message = CommandAr reset_handler = on_command( "记忆清除", - priority=1, + priority=99, block=True, permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER), ) @@ -501,7 +494,7 @@ async def handle_reset(event: GroupMessageEvent, args: Message = CommandArg()): set_prob_handler = on_command( "设置主动回复概率", - priority=1, + priority=99, block=True, permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER), ) diff --git a/nonebot_plugin_llmchat/config.py b/nonebot_plugin_llmchat/config.py index d658875..fa873d5 100755 --- a/nonebot_plugin_llmchat/config.py +++ b/nonebot_plugin_llmchat/config.py @@ -44,10 +44,6 @@ class ScopedConfig(BaseModel): ) mcp_servers: dict[str, MCPServerConfig] = Field({}, description="MCP服务器配置") blacklist_user_ids: set[int] = Field(set(), description="黑名单用户ID列表") - ignore_prefixes: list[str] = Field( - default_factory=list, - description="需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理" - ) class Config(BaseModel): diff --git a/nonebot_plugin_llmchat/mcpclient.py b/nonebot_plugin_llmchat/mcpclient.py index 55e1b44..7031d34 100644 --- a/nonebot_plugin_llmchat/mcpclient.py +++ b/nonebot_plugin_llmchat/mcpclient.py @@ -1,4 +1,3 @@ -import asyncio from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters @@ -65,13 +64,9 @@ class MCPClient: server_name, real_tool_name = tool_name.split("___") logger.info(f"正在服务器[{server_name}]上调用工具[{real_tool_name}]") session = self.sessions[server_name] - 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}]超时" + response = await session.call_tool(real_tool_name, tool_args) logger.debug(f"工具[{real_tool_name}]调用完成,响应: {response}") - return response.content + return response def get_friendly_name(self, tool_name: str): server_name, real_tool_name = tool_name.split("___") diff --git a/pyproject.toml b/pyproject.toml index 7c17df2..b396caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-plugin-llmchat" -version = "0.2.5" +version = "0.2.2" description = "Nonebot AI group chat plugin supporting multiple API preset configurations" license = "GPL" authors = ["FuQuan i@fuquan.moe"]