mirror of
https://github.com/FuQuan233/nonebot-plugin-llmchat.git
synced 2026-02-05 11:38:05 +00:00
Compare commits
25 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c7e270851 | |||
| 162afd426c | |||
| 2f78d4023a | |||
| a47ae8ef16 | |||
|
|
e082f7db9f | ||
|
|
b8afa12c9f | ||
|
|
fe39e2aba4 | ||
|
|
e542deabdb | ||
|
|
1f41ed084e | ||
|
|
53f3f185e7 | ||
|
|
3089bb51ae | ||
|
|
e293b05fa1 | ||
|
|
a8d3213e48 | ||
|
|
7ea7a26681 | ||
|
|
2c04afc86a | ||
|
|
2fecb746b3 | ||
|
|
36a47fa5e2 | ||
| 2f62365460 | |||
| 8f7adbd176 | |||
| 0943b7077f | |||
| ca1b5e75ec | |||
| 21421c4754 | |||
| 5bd92dfda6 | |||
| 63e446d5e4 | |||
| b4f7b2797c |
8 changed files with 854 additions and 187 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -174,3 +174,4 @@ pyrightconfig.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
!.vscode/*.code-snippets
|
!.vscode/*.code-snippets
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
88
README.md
88
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
# nonebot-plugin-llmchat
|
# nonebot-plugin-llmchat
|
||||||
|
|
||||||
_✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插件 ✨_
|
_✨ 支持多API预设、MCP协议、内置工具、联网搜索、视觉模型、群聊&私聊的AI对话插件 ✨_
|
||||||
|
|
||||||
|
|
||||||
<a href="./LICENSE">
|
<a href="./LICENSE">
|
||||||
|
|
@ -33,6 +33,11 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
|
||||||
- 通过连接一些搜索MCP服务器可以实现在线搜索
|
- 通过连接一些搜索MCP服务器可以实现在线搜索
|
||||||
- 兼容 Claude.app 的配置格式
|
- 兼容 Claude.app 的配置格式
|
||||||
|
|
||||||
|
1. **内置工具**
|
||||||
|
- 内置OneBot群操作工具,LLM可直接进行群管理操作(需模型支持tool_call)
|
||||||
|
- 支持禁言用户、获取群信息、查看群成员等功能
|
||||||
|
- 支持戳一戳、撤回消息等互动功能
|
||||||
|
|
||||||
1. **多API预设支持**
|
1. **多API预设支持**
|
||||||
- 可配置多个LLM服务预设(如不同模型/API密钥)
|
- 可配置多个LLM服务预设(如不同模型/API密钥)
|
||||||
- 支持运行时通过`API预设`命令热切换API配置
|
- 支持运行时通过`API预设`命令热切换API配置
|
||||||
|
|
@ -43,6 +48,12 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
|
||||||
- 支持处理回复消息
|
- 支持处理回复消息
|
||||||
- 群聊消息顺序处理,防止消息错乱
|
- 群聊消息顺序处理,防止消息错乱
|
||||||
|
|
||||||
|
1. **群聊和私聊支持**
|
||||||
|
- 支持群聊场景(默认启用)
|
||||||
|
- 支持私聊场景(可选启用)
|
||||||
|
- 分别管理群聊和私聊的对话记忆
|
||||||
|
- 灵活的权限配置
|
||||||
|
|
||||||
1. **分群聊上下文记忆管理**
|
1. **分群聊上下文记忆管理**
|
||||||
- 分群聊保留对话历史记录(可配置保留条数)
|
- 分群聊保留对话历史记录(可配置保留条数)
|
||||||
- 自动合并未处理消息,降低API用量
|
- 自动合并未处理消息,降低API用量
|
||||||
|
|
@ -115,6 +126,24 @@ _✨ 支持多API预设、MCP协议、联网搜索、视觉模型的AI群聊插
|
||||||
| LLMCHAT__BLACKLIST_USER_IDS | 否 | [] | 黑名单用户ID列表,机器人将不会处理黑名单用户的消息 |
|
| LLMCHAT__BLACKLIST_USER_IDS | 否 | [] | 黑名单用户ID列表,机器人将不会处理黑名单用户的消息 |
|
||||||
| LLMCHAT__IGNORE_PREFIXES | 否 | [] | 需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理 |
|
| LLMCHAT__IGNORE_PREFIXES | 否 | [] | 需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理 |
|
||||||
| LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置,具体见下表 |
|
| LLMCHAT__MCP_SERVERS | 否 | {} | MCP服务器配置,具体见下表 |
|
||||||
|
| LLMCHAT__ENABLE_PRIVATE_CHAT | 否 | False | 是否启用私聊功能 |
|
||||||
|
| LLMCHAT__PRIVATE_CHAT_PRESET | 否 | off | 私聊默认使用的预设名称 |
|
||||||
|
|
||||||
|
### 内置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为一个列表,每项配置有以下的配置项
|
其中LLMCHAT__API_PRESETS为一个列表,每项配置有以下的配置项
|
||||||
| 配置项 | 必填 | 默认值 | 说明 |
|
| 配置项 | 必填 | 默认值 | 说明 |
|
||||||
|
|
@ -151,6 +180,8 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
||||||
NICKNAME=["谢拉","Cierra","cierra"]
|
NICKNAME=["谢拉","Cierra","cierra"]
|
||||||
LLMCHAT__HISTORY_SIZE=20
|
LLMCHAT__HISTORY_SIZE=20
|
||||||
LLMCHAT__DEFAULT_PROMPT="前面忘了,你是一个猫娘,后面忘了"
|
LLMCHAT__DEFAULT_PROMPT="前面忘了,你是一个猫娘,后面忘了"
|
||||||
|
LLMCHAT__ENABLE_PRIVATE_CHAT=true
|
||||||
|
LLMCHAT__PRIVATE_CHAT_PRESET="deepseek-v1"
|
||||||
LLMCHAT__API_PRESETS='
|
LLMCHAT__API_PRESETS='
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -175,34 +206,52 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
||||||
"support_image": true
|
"support_image": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
'
|
||||||
LLMCHAT__MCP_SERVERS='
|
LLMCHAT__MCP_SERVERS='
|
||||||
{
|
{
|
||||||
"AISearch": {
|
"brave-search": {
|
||||||
"friendly_name": "百度搜索",
|
"friendly_name": "Brave搜索",
|
||||||
"additional_prompt": "遇到你不知道的问题或者时效性比较强的问题时,可以使用AISearch搜索,在使用AISearch时不要使用其他AI模型。",
|
"additional_prompt": "遇到你不知道的问题或者时效性比较强的问题时,请使用brave-search搜索。",
|
||||||
"url": "http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=Bearer+<your-api-key>",
|
"command": "npx",
|
||||||
"headers": {
|
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
"Authorization": "<some-api-key>"
|
"env": {
|
||||||
|
"BRAVE_API_KEY": "<your-api-key>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fetch": {
|
"fetch": {
|
||||||
"friendly_name": "网页浏览",
|
"friendly_name": "浏览网页",
|
||||||
|
"additional_prompt": "搜索到的链接可以通过fetch打开进一步了解。",
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["mcp-server-fetch"]
|
"args": ["mcp-server-fetch", "--ignore-robots-txt", "--user-agent=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36\""]
|
||||||
|
},
|
||||||
|
"hefeng-weather": {
|
||||||
|
"friendly_name": "和风天气",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["hefeng-mcp-weather@latest", "--apiKey=<your-api-key>"]
|
||||||
|
},
|
||||||
|
"mcp-server-code-runner": {
|
||||||
|
"friendly_name": "代码运行器",
|
||||||
|
"additional_prompt": "在使用的时候你需要将你需要的结果输出出来,用户看不到你的代码,如果你需要给用户展示,你需要将代码以文字的形式发送出来。",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"-i",
|
||||||
|
"formulahendry/mcp-server-code-runner"
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
'
|
|
||||||
'
|
'
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 🎉 使用
|
## 🎉 使用
|
||||||
|
|
||||||
**如果`LLMCHAT__DEFAULT_PRESET`没有配置,则插件默认为关闭状态,请使用`API预设+[预设名]`开启插件**
|
**如果`LLMCHAT__DEFAULT_PRESET`没有配置,则插件默认为关闭状态,请使用`API预设+[预设名]`开启插件, 私聊同理。**
|
||||||
|
|
||||||
配置完成后@机器人即可手动触发回复,另外在机器人收到群聊消息时会根据`LLMCHAT__RANDOM_TRIGGER_PROB`配置的概率或群聊中使用指令设置的概率随机自动触发回复。
|
配置完成后在群聊中@机器人或私聊机器人即可手动触发回复,另外在机器人收到群聊消息时会根据`LLMCHAT__RANDOM_TRIGGER_PROB`配置的概率或群聊中使用指令设置的概率随机自动触发回复。
|
||||||
|
|
||||||
### 指令表
|
### 群聊指令表
|
||||||
|
|
||||||
以下指令均仅对发送的群聊生效,不同群聊配置不互通。
|
以下指令均仅对发送的群聊生效,不同群聊配置不互通。
|
||||||
|
|
||||||
|
|
@ -214,6 +263,17 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
||||||
| 切换思维输出 | 管理 | 否 | 群聊 | 无 | 切换是否输出AI的思维过程的开关(需模型支持) |
|
| 切换思维输出 | 管理 | 否 | 群聊 | 无 | 切换是否输出AI的思维过程的开关(需模型支持) |
|
||||||
| 设置主动回复概率 | 管理 | 否 | 群聊 | 主动回复概率 | 主动回复概率需为 [0, 1] 的浮点数,0为完全关闭主动回复 |
|
| 设置主动回复概率 | 管理 | 否 | 群聊 | 主动回复概率 | 主动回复概率需为 [0, 1] 的浮点数,0为完全关闭主动回复 |
|
||||||
|
|
||||||
|
### 私聊指令表
|
||||||
|
|
||||||
|
以下指令仅在启用私聊功能(`LLMCHAT__ENABLE_PRIVATE_CHAT=true`)后可用,这些指令均只对发送者的私聊生效。
|
||||||
|
|
||||||
|
| 指令 | 权限 | 参数 | 说明 |
|
||||||
|
|:-----:|:----:|:----:|:----:|
|
||||||
|
| API预设 | 主人 | [QQ号\|群号] [预设名] | 查看或修改使用的API预设,缺省[QQ号\|群号]则对当前聊天生效 |
|
||||||
|
| 修改设定 | 所有人 | 设定 | 修改私聊机器人的设定 |
|
||||||
|
| 记忆清除 | 所有人 | 无 | 清除私聊的机器人记忆 |
|
||||||
|
| 切换思维输出 | 所有人 | 无 | 切换是否输出私聊AI的思维过程的开关(需模型支持) |
|
||||||
|
|
||||||
### 效果图
|
### 效果图
|
||||||

|

|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ from nonebot import (
|
||||||
on_message,
|
on_message,
|
||||||
require,
|
require,
|
||||||
)
|
)
|
||||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegment
|
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegment, PrivateMessageEvent
|
||||||
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
|
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE
|
||||||
from nonebot.params import CommandArg
|
from nonebot.params import CommandArg
|
||||||
from nonebot.permission import SUPERUSER
|
from nonebot.permission import SUPERUSER
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
@ -86,16 +86,36 @@ class GroupState:
|
||||||
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
|
||||||
self.random_trigger_prob = plugin_config.random_trigger_prob
|
self.random_trigger_prob = plugin_config.random_trigger_prob
|
||||||
|
|
||||||
|
|
||||||
|
# 初始化私聊状态
|
||||||
|
class PrivateChatState:
|
||||||
|
def __init__(self):
|
||||||
|
self.preset_name = plugin_config.private_chat_preset
|
||||||
|
self.history = deque(maxlen=plugin_config.history_size * 2)
|
||||||
|
self.queue = asyncio.Queue()
|
||||||
|
self.processing = False
|
||||||
|
self.last_active = time.time()
|
||||||
|
self.past_events = deque(maxlen=plugin_config.past_events_size)
|
||||||
|
self.group_prompt: str | None = None
|
||||||
|
self.user_prompt: str | None = None
|
||||||
|
self.output_reasoning_content = False
|
||||||
|
|
||||||
|
|
||||||
group_states: dict[int, GroupState] = defaultdict(GroupState)
|
group_states: dict[int, GroupState] = defaultdict(GroupState)
|
||||||
|
private_chat_states: dict[int, PrivateChatState] = defaultdict(PrivateChatState)
|
||||||
|
|
||||||
|
|
||||||
# 获取当前预设配置
|
# 获取当前预设配置
|
||||||
def get_preset(group_id: int) -> PresetConfig:
|
def get_preset(context_id: int, is_group: bool = True) -> PresetConfig:
|
||||||
state = group_states[group_id]
|
if is_group:
|
||||||
|
state = group_states[context_id]
|
||||||
|
else:
|
||||||
|
state = private_chat_states[context_id]
|
||||||
|
|
||||||
for preset in plugin_config.api_presets:
|
for preset in plugin_config.api_presets:
|
||||||
if preset.name == state.preset_name:
|
if preset.name == state.preset_name:
|
||||||
return preset
|
return preset
|
||||||
|
|
@ -103,12 +123,12 @@ def get_preset(group_id: int) -> PresetConfig:
|
||||||
|
|
||||||
|
|
||||||
# 消息格式转换
|
# 消息格式转换
|
||||||
def format_message(event: GroupMessageEvent) -> str:
|
def format_message(event: GroupMessageEvent | PrivateMessageEvent) -> str:
|
||||||
text_message = ""
|
text_message = ""
|
||||||
if event.reply is not None:
|
if isinstance(event, GroupMessageEvent) and event.reply is not None:
|
||||||
text_message += f"[回复 {event.reply.sender.nickname} 的消息 {event.reply.message.extract_plain_text()}]\n"
|
text_message += f"[回复 {event.reply.sender.nickname} 的消息 {event.reply.message.extract_plain_text()}]\n"
|
||||||
|
|
||||||
if event.is_tome():
|
if isinstance(event, GroupMessageEvent) and event.is_tome():
|
||||||
text_message += f"@{next(iter(driver.config.nickname))} "
|
text_message += f"@{next(iter(driver.config.nickname))} "
|
||||||
|
|
||||||
for msgseg in event.get_message():
|
for msgseg in event.get_message():
|
||||||
|
|
@ -123,6 +143,7 @@ def format_message(event: GroupMessageEvent) -> str:
|
||||||
elif msgseg.type == "text":
|
elif msgseg.type == "text":
|
||||||
text_message += msgseg.data.get("text", "")
|
text_message += msgseg.data.get("text", "")
|
||||||
|
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
message = {
|
message = {
|
||||||
"SenderNickname": str(event.sender.card or event.sender.nickname),
|
"SenderNickname": str(event.sender.card or event.sender.nickname),
|
||||||
"SenderUserId": str(event.user_id),
|
"SenderUserId": str(event.user_id),
|
||||||
|
|
@ -130,6 +151,14 @@ def format_message(event: GroupMessageEvent) -> str:
|
||||||
"MessageID": event.message_id,
|
"MessageID": event.message_id,
|
||||||
"SendTime": datetime.fromtimestamp(event.time).isoformat(),
|
"SendTime": datetime.fromtimestamp(event.time).isoformat(),
|
||||||
}
|
}
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
message = {
|
||||||
|
"SenderNickname": str(event.sender.nickname),
|
||||||
|
"SenderUserId": str(event.user_id),
|
||||||
|
"Message": text_message,
|
||||||
|
"MessageID": event.message_id,
|
||||||
|
"SendTime": datetime.fromtimestamp(event.time).isoformat(),
|
||||||
|
}
|
||||||
return json.dumps(message, ensure_ascii=False)
|
return json.dumps(message, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,9 +186,10 @@ def build_reasoning_forward_nodes(self_id: str, reasoning_content: str):
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
async def is_triggered(event: GroupMessageEvent) -> bool:
|
async def is_triggered(event: GroupMessageEvent | PrivateMessageEvent) -> bool:
|
||||||
"""扩展后的消息处理规则"""
|
"""扩展后的消息处理规则"""
|
||||||
|
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
state = group_states[event.group_id]
|
state = group_states[event.group_id]
|
||||||
|
|
||||||
if state.preset_name == "off":
|
if state.preset_name == "off":
|
||||||
|
|
@ -187,6 +217,33 @@ async def is_triggered(event: GroupMessageEvent) -> bool:
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
elif isinstance(event, PrivateMessageEvent):
|
||||||
|
# 检查私聊功能是否启用
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return False
|
||||||
|
|
||||||
|
state = private_chat_states[event.user_id]
|
||||||
|
|
||||||
|
if state.preset_name == "off":
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 黑名单用户
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 私聊默认触发
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# 消息处理器
|
# 消息处理器
|
||||||
handler = on_message(
|
handler = on_message(
|
||||||
|
|
@ -197,22 +254,31 @@ handler = on_message(
|
||||||
|
|
||||||
|
|
||||||
@handler.handle()
|
@handler.handle()
|
||||||
async def handle_message(event: GroupMessageEvent):
|
async def handle_message(event: GroupMessageEvent | PrivateMessageEvent):
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
group_id = event.group_id
|
group_id = event.group_id
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"收到群聊消息 群号:{group_id} 用户:{event.user_id} 内容:{event.get_plaintext()}"
|
f"收到群聊消息 群号:{group_id} 用户:{event.user_id} 内容:{event.get_plaintext()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
state = group_states[group_id]
|
state = group_states[group_id]
|
||||||
|
context_id = group_id
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
user_id = event.user_id
|
||||||
|
logger.debug(
|
||||||
|
f"收到私聊消息 用户:{user_id} 内容:{event.get_plaintext()}"
|
||||||
|
)
|
||||||
|
state = private_chat_states[user_id]
|
||||||
|
context_id = user_id
|
||||||
|
|
||||||
await state.queue.put(event)
|
await state.queue.put(event)
|
||||||
if not state.processing:
|
if not state.processing:
|
||||||
state.processing = True
|
state.processing = True
|
||||||
task = asyncio.create_task(process_messages(group_id))
|
is_group = isinstance(event, GroupMessageEvent)
|
||||||
|
task = asyncio.create_task(process_messages(context_id, is_group))
|
||||||
task.add_done_callback(tasks.discard)
|
task.add_done_callback(tasks.discard)
|
||||||
tasks.add(task)
|
tasks.add(task)
|
||||||
|
|
||||||
async def process_images(event: GroupMessageEvent) -> list[str]:
|
async def process_images(event: GroupMessageEvent | PrivateMessageEvent) -> list[str]:
|
||||||
base64_images = []
|
base64_images = []
|
||||||
for segement in event.get_message():
|
for segement in event.get_message():
|
||||||
if segement.type == "image":
|
if segement.type == "image":
|
||||||
|
|
@ -253,9 +319,16 @@ async def send_split_messages(message_handler, content: str):
|
||||||
logger.debug(f"发送消息分段 内容:{segment[:50]}...") # 只记录前50个字符避免日志过大
|
logger.debug(f"发送消息分段 内容:{segment[:50]}...") # 只记录前50个字符避免日志过大
|
||||||
await message_handler.send(Message(segment))
|
await message_handler.send(Message(segment))
|
||||||
|
|
||||||
async def process_messages(group_id: int):
|
async def process_messages(context_id: int, is_group: bool = True):
|
||||||
|
if is_group:
|
||||||
|
group_id = context_id
|
||||||
state = group_states[group_id]
|
state = group_states[group_id]
|
||||||
preset = get_preset(group_id)
|
else:
|
||||||
|
user_id = context_id
|
||||||
|
state = private_chat_states[user_id]
|
||||||
|
group_id = None
|
||||||
|
|
||||||
|
preset = get_preset(context_id, is_group)
|
||||||
|
|
||||||
# 初始化OpenAI客户端
|
# 初始化OpenAI客户端
|
||||||
if preset.proxy != "":
|
if preset.proxy != "":
|
||||||
|
|
@ -272,32 +345,56 @@ async def process_messages(group_id: int):
|
||||||
timeout=plugin_config.request_timeout,
|
timeout=plugin_config.request_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
chat_type = "群聊" if is_group else "私聊"
|
||||||
|
context_type = "群号" if is_group else "用户"
|
||||||
logger.info(
|
logger.info(
|
||||||
f"开始处理群聊消息 群号:{group_id} 当前队列长度:{state.queue.qsize()}"
|
f"开始处理{chat_type}消息 {context_type}:{context_id} 当前队列长度:{state.queue.qsize()}"
|
||||||
)
|
)
|
||||||
while not state.queue.empty():
|
while not state.queue.empty():
|
||||||
event = await state.queue.get()
|
event = await state.queue.get()
|
||||||
logger.debug(f"从队列获取消息 群号:{group_id} 消息ID:{event.message_id}")
|
if is_group:
|
||||||
|
logger.debug(f"从队列获取消息 群号:{context_id} 消息ID:{event.message_id}")
|
||||||
|
group_id = context_id
|
||||||
|
else:
|
||||||
|
logger.debug(f"从队列获取消息 用户:{context_id} 消息ID:{event.message_id}")
|
||||||
|
group_id = None
|
||||||
past_events_snapshot = []
|
past_events_snapshot = []
|
||||||
mcp_client = MCPClient(plugin_config.mcp_servers)
|
mcp_client = MCPClient.get_instance(plugin_config.mcp_servers)
|
||||||
try:
|
try:
|
||||||
systemPrompt = f"""
|
# 构建系统提示,分成多行以满足行长限制
|
||||||
我想要你帮我在群聊中闲聊,大家一般叫你{"、".join(list(driver.config.nickname))},我将会在后面的信息中告诉你每条群聊信息的发送者和发送时间,你可以直接称呼发送者为他对应的昵称。
|
chat_type = "群聊" if is_group else "私聊"
|
||||||
你的回复需要遵守以下几点规则:
|
bot_names = "、".join(list(driver.config.nickname))
|
||||||
- 你可以使用多条消息回复,每两条消息之间使用<botbr>分隔,<botbr>前后不需要包含额外的换行和空格。
|
default_prompt = (state.group_prompt if is_group else state.user_prompt) or plugin_config.default_prompt
|
||||||
- 除<botbr>外,消息中不应该包含其他类似的标记。
|
|
||||||
- 不要使用markdown或者html,聊天软件不支持解析,换行请用换行符。
|
system_lines = [
|
||||||
- 你应该以普通人的方式发送消息,每条消息字数要尽量少一些,应该倾向于使用更多条的消息回复。
|
f"我想要你帮我在{chat_type}中闲聊,大家一般叫你{bot_names}。",
|
||||||
- 代码则不需要分段,用单独的一条消息发送。
|
"我将会在后面的信息中告诉你每条信息的发送者和发送时间,你可以直接称呼发送者为他对应的昵称。",
|
||||||
- 请使用发送者的昵称称呼发送者,你可以礼貌地问候发送者,但只需要在第一次回答这位发送者的问题时问候他。
|
"你的回复需要遵守以下几点规则:",
|
||||||
- 你有at群成员的能力,只需要在某条消息中插入[CQ:at,qq=(QQ号)],也就是CQ码。at发送者是非必要的,你可以根据你自己的想法at某个人。
|
"- 你可以使用多条消息回复,每两条消息之间使用<botbr>分隔,<botbr>前后不需要包含额外的换行和空格。",
|
||||||
- 你有引用某条消息的能力,使用[CQ:reply,id=(消息id)]来引用。
|
"- 除<botbr>外,消息中不应该包含其他类似的标记。",
|
||||||
- 如果有多条消息,你应该优先回复提到你的,一段时间之前的就不要回复了,也可以直接选择不回复。
|
"- 不要使用markdown或者html,聊天软件不支持解析,换行请用换行符。",
|
||||||
- 如果你选择完全不回复,你只需要直接输出一个<botbr>。
|
"- 你应该以普通人的方式发送消息,每条消息字数要尽量少一些,应该倾向于使用更多条的消息回复。",
|
||||||
- 如果你需要思考的话,你应该思考尽量少,以节省时间。
|
"- 代码则不需要分段,用单独的一条消息发送。",
|
||||||
下面是关于你性格的设定,如果设定中提到让你扮演某个人,或者设定中有提到名字,则优先使用设定中的名字。
|
"- 请使用发送者的昵称称呼发送者,你可以礼貌地问候发送者,但只需要在"
|
||||||
{state.group_prompt or plugin_config.default_prompt}
|
"第一次回答这位发送者的问题时问候他。",
|
||||||
"""
|
"- 你有引用某条消息的能力,使用[CQ:reply,id=(消息id)]来引用。",
|
||||||
|
"- 如果有多条消息,你应该优先回复提到你的,一段时间之前的就不要回复了,也可以直接选择不回复。",
|
||||||
|
"- 如果你选择完全不回复,你只需要直接输出一个<botbr>。",
|
||||||
|
"- 如果你需要思考的话,你应该尽量少思考,以节省时间。",
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_group:
|
||||||
|
system_lines += [
|
||||||
|
"- 你有at群成员的能力,只需要在某条消息中插入[CQ:at,qq=(QQ号)],"
|
||||||
|
"也就是CQ码。at发送者是非必要的,你可以根据你自己的想法at某个人。",
|
||||||
|
]
|
||||||
|
|
||||||
|
system_lines += [
|
||||||
|
"下面是关于你性格的设定,如果设定中提到让你扮演某个人,或者设定中有提到名字,则优先使用设定中的名字。",
|
||||||
|
default_prompt,
|
||||||
|
]
|
||||||
|
|
||||||
|
systemPrompt = "\n".join(system_lines)
|
||||||
if preset.support_mcp:
|
if preset.support_mcp:
|
||||||
systemPrompt += "你也可以使用一些工具,下面是关于这些工具的额外说明:\n"
|
systemPrompt += "你也可以使用一些工具,下面是关于这些工具的额外说明:\n"
|
||||||
for mcp_name, mcp_config in plugin_config.mcp_servers.items():
|
for mcp_name, mcp_config in plugin_config.mcp_servers.items():
|
||||||
|
|
@ -349,8 +446,7 @@ async def process_messages(group_id: int):
|
||||||
}
|
}
|
||||||
|
|
||||||
if preset.support_mcp:
|
if preset.support_mcp:
|
||||||
await mcp_client.connect_to_servers()
|
available_tools = await mcp_client.get_available_tools(is_group)
|
||||||
available_tools = await mcp_client.get_available_tools()
|
|
||||||
client_config["tools"] = available_tools
|
client_config["tools"] = available_tools
|
||||||
|
|
||||||
response = await client.chat.completions.create(
|
response = await client.chat.completions.create(
|
||||||
|
|
@ -364,7 +460,7 @@ async def process_messages(group_id: int):
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
|
|
||||||
# 处理响应并处理工具调用
|
# 处理响应并处理工具调用
|
||||||
while preset.support_mcp and message.tool_calls:
|
while preset.support_mcp and message and message.tool_calls:
|
||||||
new_messages.append({
|
new_messages.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"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]
|
||||||
|
|
@ -382,8 +478,19 @@ async def process_messages(group_id: int):
|
||||||
# 发送工具调用提示
|
# 发送工具调用提示
|
||||||
await handler.send(Message(f"正在使用{mcp_client.get_friendly_name(tool_name)}"))
|
await handler.send(Message(f"正在使用{mcp_client.get_friendly_name(tool_name)}"))
|
||||||
|
|
||||||
# 执行工具调用
|
if is_group:
|
||||||
result = await mcp_client.call_tool(tool_name, tool_args)
|
result = await mcp_client.call_tool(
|
||||||
|
tool_name,
|
||||||
|
tool_args,
|
||||||
|
group_id=event.group_id,
|
||||||
|
bot_id=str(event.self_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = await mcp_client.call_tool(
|
||||||
|
tool_name,
|
||||||
|
tool_args,
|
||||||
|
bot_id=str(event.self_id)
|
||||||
|
)
|
||||||
|
|
||||||
new_messages.append({
|
new_messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
|
@ -399,11 +506,17 @@ async def process_messages(group_id: int):
|
||||||
|
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
|
|
||||||
|
# 安全检查:确保 message 不为 None
|
||||||
|
if not message:
|
||||||
|
logger.error("API 响应中的 message 为 None")
|
||||||
|
await handler.send(Message("服务暂时不可用,请稍后再试"))
|
||||||
|
return
|
||||||
|
|
||||||
reply, matched_reasoning_content = pop_reasoning_content(
|
reply, matched_reasoning_content = pop_reasoning_content(
|
||||||
response.choices[0].message.content
|
message.content
|
||||||
)
|
)
|
||||||
reasoning_content: str | None = (
|
reasoning_content: str | None = (
|
||||||
getattr(response.choices[0].message, "reasoning_content", None)
|
getattr(message, "reasoning_content", None)
|
||||||
or matched_reasoning_content
|
or matched_reasoning_content
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -412,7 +525,7 @@ async def process_messages(group_id: int):
|
||||||
"content": reply,
|
"content": reply,
|
||||||
}
|
}
|
||||||
|
|
||||||
reply_images = getattr(response.choices[0].message, "images", None)
|
reply_images = getattr(message, "images", None)
|
||||||
|
|
||||||
if reply_images:
|
if reply_images:
|
||||||
# openai的sdk里的assistant消息暂时没有images字段,需要单独处理
|
# openai的sdk里的assistant消息暂时没有images字段,需要单独处理
|
||||||
|
|
@ -448,14 +561,15 @@ async def process_messages(group_id: int):
|
||||||
await handler.send(image_msg)
|
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请求失败 {'群号' if is_group else '用户'}:{context_id}")
|
||||||
# 如果在处理过程中出现异常,恢复未处理的消息到state中
|
# 如果在处理过程中出现异常,恢复未处理的消息到state中
|
||||||
state.past_events.extendleft(reversed(past_events_snapshot))
|
state.past_events.extendleft(reversed(past_events_snapshot))
|
||||||
await handler.send(Message(f"服务暂时不可用,请稍后再试\n{e!s}"))
|
await handler.send(Message(f"服务暂时不可用,请稍后再试\n{e!s}"))
|
||||||
finally:
|
finally:
|
||||||
state.processing = False
|
state.processing = False
|
||||||
state.queue.task_done()
|
state.queue.task_done()
|
||||||
await mcp_client.cleanup()
|
# 不再需要每次都清理MCPClient,因为它现在是单例
|
||||||
|
# await mcp_client.cleanup()
|
||||||
|
|
||||||
|
|
||||||
# 预设切换命令
|
# 预设切换命令
|
||||||
|
|
@ -463,22 +577,94 @@ preset_handler = on_command("API预设", priority=1, block=True, permission=SUPE
|
||||||
|
|
||||||
|
|
||||||
@preset_handler.handle()
|
@preset_handler.handle()
|
||||||
async def handle_preset(event: GroupMessageEvent, args: Message = CommandArg()):
|
async def handle_preset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||||
group_id = event.group_id
|
# 解析命令参数
|
||||||
preset_name = args.extract_plain_text().strip()
|
args_text = args.extract_plain_text().strip()
|
||||||
|
args_parts = args_text.split(maxsplit=1)
|
||||||
|
|
||||||
if preset_name == "off":
|
target_id = None
|
||||||
group_states[group_id].preset_name = preset_name
|
preset_name = None
|
||||||
await preset_handler.finish("已关闭llmchat")
|
|
||||||
|
|
||||||
|
# 可用预设列表
|
||||||
available_presets = {p.name for p in plugin_config.api_presets}
|
available_presets = {p.name for p in plugin_config.api_presets}
|
||||||
|
|
||||||
|
# 只在私聊中允许 SUPERUSER 修改他人预设
|
||||||
|
if isinstance(event, PrivateMessageEvent) and args_parts and args_parts[0].isdigit():
|
||||||
|
# 第一个参数是纯数字,且不是预设名
|
||||||
|
if args_parts[0] not in available_presets:
|
||||||
|
target_id = int(args_parts[0])
|
||||||
|
|
||||||
|
# 判断目标是群聊还是私聊
|
||||||
|
if target_id in group_states:
|
||||||
|
state = group_states[target_id]
|
||||||
|
is_group_target = True
|
||||||
|
elif target_id in private_chat_states:
|
||||||
|
state = private_chat_states[target_id]
|
||||||
|
is_group_target = False
|
||||||
|
else:
|
||||||
|
# 默认创建私聊状态
|
||||||
|
state = private_chat_states[target_id]
|
||||||
|
is_group_target = False
|
||||||
|
|
||||||
|
# 如果只有目标 ID,没有预设名,返回当前预设
|
||||||
|
if len(args_parts) == 1:
|
||||||
|
context_type = "群聊" if is_group_target else "私聊"
|
||||||
|
available_presets_str = "\n- ".join(available_presets)
|
||||||
|
await preset_handler.finish(
|
||||||
|
f"{context_type} {target_id} 当前API预设:{state.preset_name}\n可用API预设:\n- {available_presets_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 有预设名,进行修改
|
||||||
|
preset_name = args_parts[1]
|
||||||
|
context_id = target_id
|
||||||
|
else:
|
||||||
|
# 第一个参数虽然是数字但也是预设名,按普通流程处理
|
||||||
|
target_id = None
|
||||||
|
preset_name = args_text
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return
|
||||||
|
context_id = event.user_id
|
||||||
|
state = private_chat_states[context_id]
|
||||||
|
is_group_target = False
|
||||||
|
else:
|
||||||
|
# 普通情况:修改自己的预设
|
||||||
|
preset_name = args_text
|
||||||
|
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
|
context_id = event.group_id
|
||||||
|
state = group_states[context_id]
|
||||||
|
is_group_target = True
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return
|
||||||
|
context_id = event.user_id
|
||||||
|
state = private_chat_states[context_id]
|
||||||
|
is_group_target = False
|
||||||
|
|
||||||
|
# 处理关闭功能
|
||||||
|
if preset_name == "off":
|
||||||
|
state.preset_name = preset_name
|
||||||
|
if target_id:
|
||||||
|
context_type = "群聊" if is_group_target else "私聊"
|
||||||
|
await preset_handler.finish(f"已关闭 {context_type} {context_id} 的llmchat功能")
|
||||||
|
elif isinstance(event, GroupMessageEvent):
|
||||||
|
await preset_handler.finish("已关闭llmchat群聊功能")
|
||||||
|
else:
|
||||||
|
await preset_handler.finish("已关闭llmchat私聊功能")
|
||||||
|
|
||||||
|
# 检查预设是否存在
|
||||||
if preset_name not in available_presets:
|
if preset_name not in available_presets:
|
||||||
available_presets_str = "\n- ".join(available_presets)
|
available_presets_str = "\n- ".join(available_presets)
|
||||||
await preset_handler.finish(
|
await preset_handler.finish(
|
||||||
f"当前API预设:{group_states[group_id].preset_name}\n可用API预设:\n- {available_presets_str}"
|
f"当前API预设:{state.preset_name}\n可用API预设:\n- {available_presets_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
group_states[group_id].preset_name = preset_name
|
# 切换预设
|
||||||
|
state.preset_name = preset_name
|
||||||
|
if target_id:
|
||||||
|
context_type = "群聊" if is_group_target else "私聊"
|
||||||
|
await preset_handler.finish(f"已将 {context_type} {context_id} 切换至API预设:{preset_name}")
|
||||||
|
else:
|
||||||
await preset_handler.finish(f"已切换至API预设:{preset_name}")
|
await preset_handler.finish(f"已切换至API预设:{preset_name}")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -486,16 +672,23 @@ edit_preset_handler = on_command(
|
||||||
"修改设定",
|
"修改设定",
|
||||||
priority=1,
|
priority=1,
|
||||||
block=True,
|
block=True,
|
||||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@edit_preset_handler.handle()
|
@edit_preset_handler.handle()
|
||||||
async def handle_edit_preset(event: GroupMessageEvent, args: Message = CommandArg()):
|
async def handle_edit_preset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||||
group_id = event.group_id
|
if isinstance(event, GroupMessageEvent):
|
||||||
group_prompt = args.extract_plain_text().strip()
|
context_id = event.group_id
|
||||||
|
state = group_states[context_id]
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return
|
||||||
|
context_id = event.user_id
|
||||||
|
state = private_chat_states[context_id]
|
||||||
|
|
||||||
group_states[group_id].group_prompt = group_prompt
|
group_prompt = args.extract_plain_text().strip()
|
||||||
|
state.group_prompt = group_prompt
|
||||||
await edit_preset_handler.finish("修改成功")
|
await edit_preset_handler.finish("修改成功")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -503,16 +696,23 @@ reset_handler = on_command(
|
||||||
"记忆清除",
|
"记忆清除",
|
||||||
priority=1,
|
priority=1,
|
||||||
block=True,
|
block=True,
|
||||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@reset_handler.handle()
|
@reset_handler.handle()
|
||||||
async def handle_reset(event: GroupMessageEvent, args: Message = CommandArg()):
|
async def handle_reset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||||
group_id = event.group_id
|
if isinstance(event, GroupMessageEvent):
|
||||||
|
context_id = event.group_id
|
||||||
|
state = group_states[context_id]
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return
|
||||||
|
context_id = event.user_id
|
||||||
|
state = private_chat_states[context_id]
|
||||||
|
|
||||||
group_states[group_id].past_events.clear()
|
state.past_events.clear()
|
||||||
group_states[group_id].history.clear()
|
state.history.clear()
|
||||||
await reset_handler.finish("记忆已清空")
|
await reset_handler.finish("记忆已清空")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -526,32 +726,39 @@ set_prob_handler = on_command(
|
||||||
|
|
||||||
@set_prob_handler.handle()
|
@set_prob_handler.handle()
|
||||||
async def handle_set_prob(event: GroupMessageEvent, args: Message = CommandArg()):
|
async def handle_set_prob(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||||
group_id = event.group_id
|
context_id = event.group_id
|
||||||
prob = 0
|
state = group_states[context_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prob = float(args.extract_plain_text().strip())
|
prob = float(args.extract_plain_text().strip())
|
||||||
if prob < 0 or prob > 1:
|
if prob < 0 or prob > 1:
|
||||||
raise ValueError
|
raise ValueError("概率值必须在0-1之间")
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
await reset_handler.finish(f"输入有误,请使用 [0,1] 的浮点数\n{e!s}")
|
await set_prob_handler.finish(f"输入有误,请使用 [0,1] 的浮点数\n{e!s}")
|
||||||
|
return
|
||||||
|
|
||||||
group_states[group_id].random_trigger_prob = prob
|
state.random_trigger_prob = prob
|
||||||
await reset_handler.finish(f"主动回复概率已设为 {prob}")
|
await set_prob_handler.finish(f"主动回复概率已设为 {prob}")
|
||||||
|
|
||||||
|
|
||||||
# 预设切换命令
|
# 思维输出切换命令
|
||||||
think_handler = on_command(
|
think_handler = on_command(
|
||||||
"切换思维输出",
|
"切换思维输出",
|
||||||
priority=1,
|
priority=1,
|
||||||
block=True,
|
block=True,
|
||||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@think_handler.handle()
|
@think_handler.handle()
|
||||||
async def handle_think(event: GroupMessageEvent, args: Message = CommandArg()):
|
async def handle_think(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
state = group_states[event.group_id]
|
state = group_states[event.group_id]
|
||||||
|
else: # PrivateMessageEvent
|
||||||
|
if not plugin_config.enable_private_chat:
|
||||||
|
return
|
||||||
|
state = private_chat_states[event.user_id]
|
||||||
|
|
||||||
state.output_reasoning_content = not state.output_reasoning_content
|
state.output_reasoning_content = not state.output_reasoning_content
|
||||||
|
|
||||||
await think_handler.finish(
|
await think_handler.finish(
|
||||||
|
|
@ -565,6 +772,7 @@ async def handle_think(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||||
data_dir = store.get_plugin_data_dir()
|
data_dir = store.get_plugin_data_dir()
|
||||||
# 获取插件数据文件
|
# 获取插件数据文件
|
||||||
data_file = store.get_plugin_data_file("llmchat_state.json")
|
data_file = store.get_plugin_data_file("llmchat_state.json")
|
||||||
|
private_data_file = store.get_plugin_data_file("llmchat_private_state.json")
|
||||||
|
|
||||||
|
|
||||||
async def save_state():
|
async def save_state():
|
||||||
|
|
@ -586,6 +794,24 @@ async def save_state():
|
||||||
async with aiofiles.open(data_file, "w", encoding="utf8") as f:
|
async with aiofiles.open(data_file, "w", encoding="utf8") as f:
|
||||||
await f.write(json.dumps(data, ensure_ascii=False))
|
await f.write(json.dumps(data, ensure_ascii=False))
|
||||||
|
|
||||||
|
# 保存私聊状态
|
||||||
|
if plugin_config.enable_private_chat:
|
||||||
|
logger.info(f"开始保存私聊状态到文件:{private_data_file}")
|
||||||
|
private_data = {
|
||||||
|
uid: {
|
||||||
|
"preset": state.preset_name,
|
||||||
|
"history": list(state.history),
|
||||||
|
"last_active": state.last_active,
|
||||||
|
"group_prompt": state.group_prompt,
|
||||||
|
"output_reasoning_content": state.output_reasoning_content,
|
||||||
|
}
|
||||||
|
for uid, state in private_chat_states.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(private_data_file), exist_ok=True)
|
||||||
|
async with aiofiles.open(private_data_file, "w", encoding="utf8") as f:
|
||||||
|
await f.write(json.dumps(private_data, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
async def load_state():
|
async def load_state():
|
||||||
"""从文件加载群组状态"""
|
"""从文件加载群组状态"""
|
||||||
|
|
@ -607,6 +833,23 @@ async def load_state():
|
||||||
state.random_trigger_prob = state_data.get("random_trigger_prob", plugin_config.random_trigger_prob)
|
state.random_trigger_prob = state_data.get("random_trigger_prob", plugin_config.random_trigger_prob)
|
||||||
group_states[int(gid)] = state
|
group_states[int(gid)] = state
|
||||||
|
|
||||||
|
# 加载私聊状态
|
||||||
|
if plugin_config.enable_private_chat:
|
||||||
|
logger.info(f"从文件加载私聊状态:{private_data_file}")
|
||||||
|
if os.path.exists(private_data_file):
|
||||||
|
async with aiofiles.open(private_data_file, encoding="utf8") as f:
|
||||||
|
private_data = json.loads(await f.read())
|
||||||
|
for uid, state_data in private_data.items():
|
||||||
|
state = PrivateChatState()
|
||||||
|
state.preset_name = state_data["preset"]
|
||||||
|
state.history = deque(
|
||||||
|
state_data["history"], maxlen=plugin_config.history_size * 2
|
||||||
|
)
|
||||||
|
state.last_active = state_data["last_active"]
|
||||||
|
state.group_prompt = state_data["group_prompt"]
|
||||||
|
state.output_reasoning_content = state_data["output_reasoning_content"]
|
||||||
|
private_chat_states[int(uid)] = state
|
||||||
|
|
||||||
|
|
||||||
# 注册生命周期事件
|
# 注册生命周期事件
|
||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
|
|
@ -621,3 +864,5 @@ async def init_plugin():
|
||||||
async def cleanup_plugin():
|
async def cleanup_plugin():
|
||||||
logger.info("插件关闭清理")
|
logger.info("插件关闭清理")
|
||||||
await save_state()
|
await save_state()
|
||||||
|
# 销毁MCPClient单例
|
||||||
|
await MCPClient.destroy_instance()
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ class ScopedConfig(BaseModel):
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理"
|
description="需要忽略的消息前缀列表,匹配到这些前缀的消息不会处理"
|
||||||
)
|
)
|
||||||
|
enable_private_chat: bool = Field(False, description="是否启用私聊功能")
|
||||||
|
private_chat_preset: str = Field("off", description="私聊默认使用的预设名称")
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,52 @@ from mcp.client.stdio import stdio_client
|
||||||
from nonebot import logger
|
from nonebot import logger
|
||||||
|
|
||||||
from .config import MCPServerConfig
|
from .config import MCPServerConfig
|
||||||
|
from .onebottools import OneBotTools
|
||||||
|
|
||||||
|
|
||||||
class MCPClient:
|
class MCPClient:
|
||||||
def __init__(self, server_config: dict[str, MCPServerConfig]):
|
_instance = None
|
||||||
logger.info(f"正在初始化MCPClient,共有{len(server_config)}个服务器配置")
|
_initialized = False
|
||||||
|
|
||||||
|
def __new__(cls, server_config: dict[str, MCPServerConfig] | None = None):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, server_config: dict[str, MCPServerConfig] | None = None):
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
if server_config is None:
|
||||||
|
raise ValueError("server_config must be provided for first initialization")
|
||||||
|
|
||||||
|
logger.info(f"正在初始化MCPClient单例,共有{len(server_config)}个服务器配置")
|
||||||
self.server_config = server_config
|
self.server_config = server_config
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
self.exit_stack = AsyncExitStack()
|
self.exit_stack = AsyncExitStack()
|
||||||
logger.debug("MCPClient初始化成功")
|
# 添加工具列表缓存
|
||||||
|
self._tools_cache: list | None = None
|
||||||
|
self._cache_initialized = False
|
||||||
|
# 初始化OneBot工具
|
||||||
|
self.onebot_tools = OneBotTools()
|
||||||
|
self._initialized = True
|
||||||
|
logger.debug("MCPClient单例初始化成功")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls, server_config: dict[str, MCPServerConfig] | None = None):
|
||||||
|
"""获取MCPClient实例"""
|
||||||
|
if cls._instance is None:
|
||||||
|
if server_config is None:
|
||||||
|
raise ValueError("server_config must be provided for first initialization")
|
||||||
|
cls._instance = cls(server_config)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def instance(cls):
|
||||||
|
"""快速获取已初始化的MCPClient实例,如果未初始化则抛出异常"""
|
||||||
|
if cls._instance is None:
|
||||||
|
raise RuntimeError("MCPClient has not been initialized. Call get_instance() first.")
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
async def connect_to_servers(self):
|
async def connect_to_servers(self):
|
||||||
logger.info(f"开始连接{len(self.server_config)}个MCP服务器")
|
logger.info(f"开始连接{len(self.server_config)}个MCP服务器")
|
||||||
|
|
@ -38,12 +75,45 @@ class MCPClient:
|
||||||
|
|
||||||
logger.info(f"已成功连接到MCP服务器[{server_name}]")
|
logger.info(f"已成功连接到MCP服务器[{server_name}]")
|
||||||
|
|
||||||
async def get_available_tools(self):
|
def _create_session_context(self, server_name: str):
|
||||||
logger.info(f"正在从{len(self.sessions)}个已连接的服务器获取可用工具")
|
"""创建临时会话的异步上下文管理器"""
|
||||||
available_tools = []
|
config = self.server_config[server_name]
|
||||||
|
|
||||||
for server_name, session in self.sessions.items():
|
class SessionContext:
|
||||||
logger.debug(f"正在列出服务器[{server_name}]中的工具")
|
def __init__(self):
|
||||||
|
self.session = None
|
||||||
|
self.exit_stack = AsyncExitStack()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
if config.url:
|
||||||
|
transport = await self.exit_stack.enter_async_context(
|
||||||
|
sse_client(url=config.url, headers=config.headers)
|
||||||
|
)
|
||||||
|
elif config.command:
|
||||||
|
transport = await self.exit_stack.enter_async_context(
|
||||||
|
stdio_client(StdioServerParameters(**config.model_dump()))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Server config must have either url or command")
|
||||||
|
|
||||||
|
read, write = transport
|
||||||
|
self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
||||||
|
await self.session.initialize()
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.exit_stack.aclose()
|
||||||
|
|
||||||
|
return SessionContext()
|
||||||
|
|
||||||
|
async def init_tools_cache(self):
|
||||||
|
"""初始化工具列表缓存"""
|
||||||
|
if not self._cache_initialized:
|
||||||
|
available_tools = []
|
||||||
|
logger.info(f"初始化工具列表缓存,需要连接{len(self.server_config)}个服务器")
|
||||||
|
for server_name in self.server_config.keys():
|
||||||
|
logger.debug(f"正在从服务器[{server_name}]获取工具列表")
|
||||||
|
async with self._create_session_context(server_name) as session:
|
||||||
response = await session.list_tools()
|
response = await session.list_tools()
|
||||||
tools = response.tools
|
tools = response.tools
|
||||||
logger.debug(f"在服务器[{server_name}]中找到{len(tools)}个工具")
|
logger.debug(f"在服务器[{server_name}]中找到{len(tools)}个工具")
|
||||||
|
|
@ -52,33 +122,107 @@ class MCPClient:
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": f"{server_name}___{tool.name}",
|
"name": f"mcp__{server_name}__{tool.name}",
|
||||||
"description": tool.description,
|
"description": tool.description,
|
||||||
"parameters": tool.inputSchema,
|
"parameters": tool.inputSchema,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for tool in tools
|
for tool in tools
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 缓存工具列表
|
||||||
|
self._tools_cache = available_tools
|
||||||
|
self._cache_initialized = True
|
||||||
|
|
||||||
|
logger.info(f"工具列表缓存完成,共缓存{len(available_tools)}个工具")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def get_available_tools(self, is_group: bool):
|
||||||
|
"""获取可用工具列表,使用缓存机制"""
|
||||||
|
await self.init_tools_cache()
|
||||||
|
available_tools = self._tools_cache.copy() if self._tools_cache else []
|
||||||
|
if is_group:
|
||||||
|
# 群聊场景,包含OneBot工具和MCP工具
|
||||||
|
available_tools.extend(self.onebot_tools.get_available_tools())
|
||||||
|
logger.debug(f"获取可用工具列表,共{len(available_tools)}个工具")
|
||||||
return 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):
|
||||||
server_name, real_tool_name = tool_name.split("___")
|
"""按需连接调用工具,调用后立即断开"""
|
||||||
logger.info(f"正在服务器[{server_name}]上调用工具[{real_tool_name}]")
|
# 检查是否是OneBot内置工具
|
||||||
session = self.sessions[server_name]
|
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工具
|
||||||
|
if tool_name.startswith("mcp__"):
|
||||||
|
# MCP工具处理:mcp__server_name__tool_name
|
||||||
|
parts = tool_name.split("__")
|
||||||
|
if len(parts) != 3 or parts[0] != "mcp":
|
||||||
|
return f"MCP工具名称格式错误: {tool_name}"
|
||||||
|
|
||||||
|
server_name = parts[1]
|
||||||
|
real_tool_name = parts[2]
|
||||||
|
logger.info(f"按需连接到服务器[{server_name}]调用工具[{real_tool_name}]")
|
||||||
|
|
||||||
|
async with self._create_session_context(server_name) as session:
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(session.call_tool(real_tool_name, tool_args), timeout=30)
|
response = await asyncio.wait_for(session.call_tool(real_tool_name, tool_args), timeout=30)
|
||||||
|
logger.debug(f"工具[{real_tool_name}]调用完成,响应: {response}")
|
||||||
|
return response.content
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.error(f"调用工具[{real_tool_name}]超时")
|
logger.error(f"调用工具[{real_tool_name}]超时")
|
||||||
return f"调用工具[{real_tool_name}]超时"
|
return f"调用工具[{real_tool_name}]超时"
|
||||||
logger.debug(f"工具[{real_tool_name}]调用完成,响应: {response}")
|
|
||||||
return response.content
|
# 未知工具类型
|
||||||
|
return f"未知的工具类型: {tool_name}"
|
||||||
|
|
||||||
def get_friendly_name(self, tool_name: str):
|
def get_friendly_name(self, tool_name: str):
|
||||||
logger.debug(tool_name)
|
logger.debug(tool_name)
|
||||||
server_name, real_tool_name = tool_name.split("___")
|
# 检查是否是OneBot内置工具
|
||||||
|
if tool_name.startswith("ob__"):
|
||||||
|
return self.onebot_tools.get_friendly_name(tool_name)
|
||||||
|
|
||||||
|
# 检查是否是MCP工具
|
||||||
|
if tool_name.startswith("mcp__"):
|
||||||
|
# MCP工具处理:mcp__server_name__tool_name
|
||||||
|
parts = tool_name.split("__")
|
||||||
|
if len(parts) != 3 or parts[0] != "mcp":
|
||||||
|
return tool_name # 格式错误时返回原名称
|
||||||
|
|
||||||
|
server_name = parts[1]
|
||||||
|
real_tool_name = parts[2]
|
||||||
return (self.server_config[server_name].friendly_name or server_name) + " - " + real_tool_name
|
return (self.server_config[server_name].friendly_name or server_name) + " - " + real_tool_name
|
||||||
|
|
||||||
|
# 未知工具类型,返回原名称
|
||||||
|
return tool_name
|
||||||
|
|
||||||
|
def clear_tools_cache(self):
|
||||||
|
"""清除工具列表缓存"""
|
||||||
|
logger.info("清除工具列表缓存")
|
||||||
|
self._tools_cache = None
|
||||||
|
self._cache_initialized = False
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
|
"""清理资源(不销毁单例)"""
|
||||||
logger.debug("正在清理MCPClient资源")
|
logger.debug("正在清理MCPClient资源")
|
||||||
|
# 只清除缓存,不销毁单例
|
||||||
|
# self.clear_tools_cache() # 保留缓存,避免重复获取工具列表
|
||||||
await self.exit_stack.aclose()
|
await self.exit_stack.aclose()
|
||||||
|
# 重新初始化exit_stack以便后续使用
|
||||||
|
self.exit_stack = AsyncExitStack()
|
||||||
logger.debug("MCPClient资源清理完成")
|
logger.debug("MCPClient资源清理完成")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def destroy_instance(cls):
|
||||||
|
"""完全销毁单例实例(仅在应用关闭时使用)"""
|
||||||
|
if cls._instance is not None:
|
||||||
|
logger.info("销毁MCPClient单例")
|
||||||
|
await cls._instance.cleanup()
|
||||||
|
cls._instance.clear_tools_cache()
|
||||||
|
cls._instance = None
|
||||||
|
cls._initialized = False
|
||||||
|
logger.debug("MCPClient单例已销毁")
|
||||||
|
|
|
||||||
215
nonebot_plugin_llmchat/onebottools.py
Normal file
215
nonebot_plugin_llmchat/onebottools.py
Normal file
|
|
@ -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}"
|
||||||
|
|
||||||
|
|
||||||
18
poetry.lock
generated
18
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
|
|
@ -662,14 +662,14 @@ typing-extensions = ">=4.0.0,<5.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonebot2"
|
name = "nonebot2"
|
||||||
version = "2.4.1"
|
version = "2.4.4"
|
||||||
description = "An asynchronous python bot framework."
|
description = "An asynchronous python bot framework."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.9"
|
python-versions = "<4.0,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "nonebot2-2.4.1-py3-none-any.whl", hash = "sha256:fec95f075efc89dbe9ce148618b413b02f46ba284200367749b035e794695111"},
|
{file = "nonebot2-2.4.4-py3-none-any.whl", hash = "sha256:8885d02906f1def83c138f298a7aa99ca1975351f44d8d290ea0eeec5aec1f0b"},
|
||||||
{file = "nonebot2-2.4.1.tar.gz", hash = "sha256:8fea364318501ed79721403a8ecd76587bc884d58c356260f691a8bbda9b05e6"},
|
{file = "nonebot2-2.4.4.tar.gz", hash = "sha256:b367c17f31ae0d548e374bb80b719ed12885620f29f3cbc305a5a88a6175f4e3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -679,17 +679,17 @@ loguru = ">=0.6.0,<1.0.0"
|
||||||
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<2.10.0 || >2.10.0,<2.10.1 || >2.10.1,<3.0.0"
|
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<2.10.0 || >2.10.0,<2.10.1 || >2.10.1,<3.0.0"
|
||||||
pygtrie = ">=2.4.1,<3.0.0"
|
pygtrie = ">=2.4.1,<3.0.0"
|
||||||
python-dotenv = ">=0.21.0,<2.0.0"
|
python-dotenv = ">=0.21.0,<2.0.0"
|
||||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_full_version < \"3.11.0\""}
|
||||||
typing-extensions = ">=4.4.0,<5.0.0"
|
typing-extensions = ">=4.6.0,<5.0.0"
|
||||||
yarl = ">=1.7.2,<2.0.0"
|
yarl = ">=1.7.2,<2.0.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
aiohttp = ["aiohttp[speedups] (>=3.11.0,<4.0.0)"]
|
aiohttp = ["aiohttp[speedups] (>=3.11.0,<4.0.0)"]
|
||||||
all = ["Quart (>=0.18.0,<1.0.0)", "aiohttp[speedups] (>=3.11.0,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.26.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=10.0)"]
|
all = ["aiohttp[speedups] (>=3.11.0,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.26.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=15.0)"]
|
||||||
fastapi = ["fastapi (>=0.93.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"]
|
fastapi = ["fastapi (>=0.93.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"]
|
||||||
httpx = ["httpx[http2] (>=0.26.0,<1.0.0)"]
|
httpx = ["httpx[http2] (>=0.26.0,<1.0.0)"]
|
||||||
quart = ["Quart (>=0.18.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"]
|
quart = ["quart (>=0.18.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"]
|
||||||
websockets = ["websockets (>=10.0)"]
|
websockets = ["websockets (>=15.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonemoji"
|
name = "nonemoji"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nonebot-plugin-llmchat"
|
name = "nonebot-plugin-llmchat"
|
||||||
version = "0.3.1"
|
version = "0.5.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