mirror of
https://github.com/FuQuan233/nonebot-plugin-llmchat.git
synced 2026-05-12 19:42:50 +00:00
Compare commits
4 commits
7c7e270851
...
885c87160b
| Author | SHA1 | Date | |
|---|---|---|---|
| 885c87160b | |||
| e234048bff | |||
| d7d6c14b3f | |||
| c2a53175d5 |
5 changed files with 75 additions and 11 deletions
14
README.md
14
README.md
|
|
@ -157,6 +157,8 @@ _✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型
|
||||||
| proxy | 否 | 无 | 请求API时使用的HTTP代理 |
|
| proxy | 否 | 无 | 请求API时使用的HTTP代理 |
|
||||||
| support_mcp | 否 | False | 是否支持MCP协议 |
|
| support_mcp | 否 | False | 是否支持MCP协议 |
|
||||||
| support_image | 否 | False | 是否支持图片输入 |
|
| support_image | 否 | False | 是否支持图片输入 |
|
||||||
|
| extra_body | 否 | {} | 额外的请求体字段,用于兼容不同API的特殊参数 |
|
||||||
|
| request_with_reasoning_content | 否 | false | 请求中是否包含推理过程内容(部分模型要求进行了工具调用后,必须完整回传推理过程给API) |
|
||||||
|
|
||||||
|
|
||||||
LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的格式基本兼容 Claude.app 的配置格式,具体支持如下
|
LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的格式基本兼容 Claude.app 的配置格式,具体支持如下
|
||||||
|
|
@ -184,6 +186,18 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
||||||
LLMCHAT__PRIVATE_CHAT_PRESET="deepseek-v1"
|
LLMCHAT__PRIVATE_CHAT_PRESET="deepseek-v1"
|
||||||
LLMCHAT__API_PRESETS='
|
LLMCHAT__API_PRESETS='
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "deepseek-v4-pro",
|
||||||
|
"api_key": "sk-your-api-key",
|
||||||
|
"model_name": "deepseek-v4-pro",
|
||||||
|
"api_base": "https://api.deepseek.com",
|
||||||
|
"support_mcp": true,
|
||||||
|
"support_image": false,
|
||||||
|
"extra_body": {
|
||||||
|
"thinking": {"type": "enabled"}
|
||||||
|
},
|
||||||
|
"request_with_reasoning_content": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "aliyun-deepseek-v3",
|
"name": "aliyun-deepseek-v3",
|
||||||
"api_key": "sk-your-api-key",
|
"api_key": "sk-your-api-key",
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@ class PrivateChatState:
|
||||||
self.last_active = time.time()
|
self.last_active = time.time()
|
||||||
self.past_events = deque(maxlen=plugin_config.past_events_size)
|
self.past_events = deque(maxlen=plugin_config.past_events_size)
|
||||||
self.group_prompt: str | None = None
|
self.group_prompt: str | None = None
|
||||||
self.user_prompt: str | None = None
|
|
||||||
self.output_reasoning_content = False
|
self.output_reasoning_content = False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -364,7 +363,7 @@ async def process_messages(context_id: int, is_group: bool = True):
|
||||||
# 构建系统提示,分成多行以满足行长限制
|
# 构建系统提示,分成多行以满足行长限制
|
||||||
chat_type = "群聊" if is_group else "私聊"
|
chat_type = "群聊" if is_group else "私聊"
|
||||||
bot_names = "、".join(list(driver.config.nickname))
|
bot_names = "、".join(list(driver.config.nickname))
|
||||||
default_prompt = (state.group_prompt if is_group else state.user_prompt) or plugin_config.default_prompt
|
default_prompt = (state.group_prompt) or plugin_config.default_prompt
|
||||||
|
|
||||||
system_lines = [
|
system_lines = [
|
||||||
f"我想要你帮我在{chat_type}中闲聊,大家一般叫你{bot_names}。",
|
f"我想要你帮我在{chat_type}中闲聊,大家一般叫你{bot_names}。",
|
||||||
|
|
@ -443,6 +442,7 @@ async def process_messages(context_id: int, is_group: bool = True):
|
||||||
"max_tokens": preset.max_tokens,
|
"max_tokens": preset.max_tokens,
|
||||||
"temperature": preset.temperature,
|
"temperature": preset.temperature,
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
|
"extra_body": preset.extra_body,
|
||||||
}
|
}
|
||||||
|
|
||||||
if preset.support_mcp:
|
if preset.support_mcp:
|
||||||
|
|
@ -461,10 +461,14 @@ async def process_messages(context_id: int, is_group: bool = True):
|
||||||
|
|
||||||
# 处理响应并处理工具调用
|
# 处理响应并处理工具调用
|
||||||
while preset.support_mcp and message and message.tool_calls:
|
while preset.support_mcp and message and message.tool_calls:
|
||||||
new_messages.append({
|
llm_reply: ChatCompletionMessageParam = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
|
"content": message.content,
|
||||||
"tool_calls": [tool_call.model_dump() for tool_call in message.tool_calls]
|
"tool_calls": [tool_call.model_dump() for tool_call in message.tool_calls]
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if preset.request_with_reasoning_content:
|
||||||
|
llm_reply["reasoning_content"] = message.reasoning_content# pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
# 发送LLM调用工具时的回复,一般没有
|
# 发送LLM调用工具时的回复,一般没有
|
||||||
if message.content:
|
if message.content:
|
||||||
|
|
@ -531,6 +535,9 @@ async def process_messages(context_id: int, is_group: bool = True):
|
||||||
# openai的sdk里的assistant消息暂时没有images字段,需要单独处理
|
# openai的sdk里的assistant消息暂时没有images字段,需要单独处理
|
||||||
llm_reply["images"] = reply_images # pyright: ignore[reportGeneralTypeIssues]
|
llm_reply["images"] = reply_images # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
|
if preset.request_with_reasoning_content:
|
||||||
|
llm_reply["reasoning_content"] = reasoning_content# pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
new_messages.append(llm_reply)
|
new_messages.append(llm_reply)
|
||||||
|
|
||||||
# 请求成功后再保存历史记录,保证user和assistant穿插,防止R1模型报错
|
# 请求成功后再保存历史记录,保证user和assistant穿插,防止R1模型报错
|
||||||
|
|
@ -540,12 +547,20 @@ async def process_messages(context_id: int, is_group: bool = True):
|
||||||
if state.output_reasoning_content and reasoning_content:
|
if state.output_reasoning_content and reasoning_content:
|
||||||
try:
|
try:
|
||||||
bot = get_bot(str(event.self_id))
|
bot = get_bot(str(event.self_id))
|
||||||
await bot.send_group_forward_msg(
|
if is_group:
|
||||||
group_id=group_id,
|
await bot.send_group_forward_msg(
|
||||||
messages=build_reasoning_forward_nodes(
|
group_id=group_id,
|
||||||
bot.self_id, reasoning_content
|
messages=build_reasoning_forward_nodes(
|
||||||
),
|
bot.self_id, reasoning_content
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await bot.send_private_forward_msg(
|
||||||
|
user_id=context_id,
|
||||||
|
messages=build_reasoning_forward_nodes(
|
||||||
|
bot.self_id, reasoning_content
|
||||||
|
),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"合并转发消息发送失败:\n{e!s}\n")
|
logger.error(f"合并转发消息发送失败:\n{e!s}\n")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ class PresetConfig(BaseModel):
|
||||||
proxy: str = Field("", description="HTTP代理服务器")
|
proxy: str = Field("", description="HTTP代理服务器")
|
||||||
support_mcp: bool = Field(False, description="是否支持MCP")
|
support_mcp: bool = Field(False, description="是否支持MCP")
|
||||||
support_image: bool = Field(False, description="是否支持图片输入")
|
support_image: bool = Field(False, description="是否支持图片输入")
|
||||||
|
extra_body: dict = Field({}, description="额外的请求体字段,用于兼容不同API的特殊参数")
|
||||||
|
request_with_reasoning_content: bool = Field(
|
||||||
|
False,
|
||||||
|
description="请求中是否包含推理过程内容(部分模型要求进行了工具调用后,必须完整回传推理过程给API)"
|
||||||
|
)
|
||||||
|
|
||||||
class MCPServerConfig(BaseModel):
|
class MCPServerConfig(BaseModel):
|
||||||
"""MCP服务器配置"""
|
"""MCP服务器配置"""
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,21 @@ class OneBotTools:
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "ob__set_group_card",
|
||||||
|
"description": "修改群成员的群名片。需要机器人有管理员权限。",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user_id": {"type": "string", "description": "要修改名片的用户QQ号"},
|
||||||
|
"card": {"type": "string", "description": "新的群名片内容,留空则清除群名片"},
|
||||||
|
},
|
||||||
|
"required": ["user_id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_friendly_name(self, tool_name: str) -> str:
|
def get_friendly_name(self, tool_name: str) -> str:
|
||||||
|
|
@ -94,6 +109,7 @@ class OneBotTools:
|
||||||
"ob__get_group_member_list": "OneBot - 获取成员列表",
|
"ob__get_group_member_list": "OneBot - 获取成员列表",
|
||||||
"ob__poke_user": "OneBot - 戳一戳用户",
|
"ob__poke_user": "OneBot - 戳一戳用户",
|
||||||
"ob__recall_message": "OneBot - 撤回消息",
|
"ob__recall_message": "OneBot - 撤回消息",
|
||||||
|
"ob__set_group_card": "OneBot - 修改群名片",
|
||||||
}
|
}
|
||||||
return friendly_names.get(tool_name, tool_name)
|
return friendly_names.get(tool_name, tool_name)
|
||||||
|
|
||||||
|
|
@ -118,6 +134,8 @@ class OneBotTools:
|
||||||
return await self._poke_user(bot, group_id, tool_args)
|
return await self._poke_user(bot, group_id, tool_args)
|
||||||
elif tool_name == "ob__recall_message":
|
elif tool_name == "ob__recall_message":
|
||||||
return await self._recall_message(bot, group_id, tool_args)
|
return await self._recall_message(bot, group_id, tool_args)
|
||||||
|
elif tool_name == "ob__set_group_card":
|
||||||
|
return await self._set_group_card(bot, group_id, tool_args)
|
||||||
else:
|
else:
|
||||||
return f"未知的工具: {tool_name}"
|
return f"未知的工具: {tool_name}"
|
||||||
|
|
||||||
|
|
@ -212,4 +230,16 @@ class OneBotTools:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"撤回消息失败: {e!s}"
|
return f"撤回消息失败: {e!s}"
|
||||||
|
|
||||||
|
async def _set_group_card(self, bot: Bot, group_id: int, args: dict[str, Any]) -> str:
|
||||||
|
"""修改群成员名片"""
|
||||||
|
user_id = int(args["user_id"])
|
||||||
|
card = args.get("card", "") # 如果没有提供card参数,默认为空字符串(清除名片)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.set_group_card(group_id=group_id, user_id=user_id, card=card)
|
||||||
|
if card:
|
||||||
|
return f"成功修改用户 {user_id} 的群名片为: {card}"
|
||||||
|
else:
|
||||||
|
return f"成功清除用户 {user_id} 的群名片"
|
||||||
|
except Exception as e:
|
||||||
|
return f"修改群名片失败: {e!s}"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nonebot-plugin-llmchat"
|
name = "nonebot-plugin-llmchat"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
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