mirror of
https://github.com/FuQuan233/nonebot-plugin-llmchat.git
synced 2026-02-05 11:38:05 +00:00
Merge 83f4803183 into f2e882f885
This commit is contained in:
commit
5e1c8797b5
6 changed files with 715 additions and 261 deletions
37
README.md
37
README.md
|
|
@ -183,6 +183,8 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
|||
NICKNAME=["谢拉","Cierra","cierra"]
|
||||
LLMCHAT__HISTORY_SIZE=20
|
||||
LLMCHAT__DEFAULT_PROMPT="前面忘了,你是一个猫娘,后面忘了"
|
||||
LLMCHAT__ENABLE_PRIVATE_CHAT=true
|
||||
LLMCHAT__PRIVATE_CHAT_PRESET="deepseek-v1"
|
||||
LLMCHAT__API_PRESETS='
|
||||
[
|
||||
{
|
||||
|
|
@ -261,9 +263,9 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
|||
|
||||
## 🎉 使用
|
||||
|
||||
**如果`LLMCHAT__DEFAULT_PRESET`没有配置,则插件默认为关闭状态,请使用`API预设+[预设名]`开启插件**
|
||||
**如果`LLMCHAT__DEFAULT_PRESET`没有配置,则插件默认为关闭状态,请使用`API预设+[预设名]`开启插件, 私聊同理。**
|
||||
|
||||
配置完成后@机器人即可手动触发回复,另外在机器人收到群聊消息时会根据`LLMCHAT__RANDOM_TRIGGER_PROB`配置的概率或群聊中使用指令设置的概率随机自动触发回复。
|
||||
配置完成后在群聊中@机器人或私聊机器人即可手动触发回复,另外在机器人收到群聊消息时会根据`LLMCHAT__RANDOM_TRIGGER_PROB`配置的概率或群聊中使用指令设置的概率随机自动触发回复。
|
||||
|
||||
### MCP 工具权限控制
|
||||
|
||||
|
|
@ -308,33 +310,10 @@ LLMCHAT__MCP_SERVERS同样为一个dict,key为服务器名称,value配置的
|
|||
|
||||
| 指令 | 权限 | 参数 | 说明 |
|
||||
|:-----:|:----:|:----:|:----:|
|
||||
| 私聊API预设 | 主人 | [预设名] | 查看或修改私聊使用的API预设 |
|
||||
| 私聊修改设定 | 主人 | 设定 | 修改私聊机器人的设定 |
|
||||
| 私聊记忆清除 | 主人 | 无 | 清除私聊的机器人记忆 |
|
||||
| 私聊切换思维输出 | 主人 | 无 | 切换是否输出私聊AI的思维过程的开关(需模型支持) |
|
||||
|
||||
**私聊功能说明:**
|
||||
|
||||
- 私聊消息默认触发回复(无需@或随机触发)
|
||||
- 私聊和群聊的对话记忆独立管理
|
||||
- OneBot群操作工具(如禁言、撤回等)在私聊中不可用
|
||||
|
||||
## 📝 私聊功能启用示例
|
||||
|
||||
在 `.env` 文件中添加以下配置以启用私聊功能:
|
||||
|
||||
```bash
|
||||
LLMCHAT__ENABLE_PRIVATE_CHAT=true
|
||||
LLMCHAT__PRIVATE_CHAT_PRESET="deepseek-v1"
|
||||
```
|
||||
|
||||
然后你可以在私聊中与机器人交互。使用以下命令管理私聊:
|
||||
|
||||
- 切换预设:`私聊API预设 aliyun-deepseek-v3`
|
||||
- 清除记忆:`私聊记忆清除`
|
||||
- 修改设定:`私聊修改设定 你是一个有趣的AI助手`
|
||||
| 切换思维输出 | 管理 | 否 | 群聊 | 无 | 切换是否输出AI的思维过程的开关(需模型支持) |
|
||||
| 设置主动回复概率 | 管理 | 否 | 群聊 | 主动回复概率 | 主动回复概率需为 [0, 1] 的浮点数,0为完全关闭主动回复 |
|
||||
| API预设 | 主人 | [预设名] | 查看或修改私聊使用的API预设 |
|
||||
| 修改设定 | 所有人 | 设定 | 修改私聊机器人的设定 |
|
||||
| 记忆清除 | 所有人 | 无 | 清除私聊的机器人记忆 |
|
||||
| 切换思维输出 | 所有人 | 无 | 切换是否输出私聊AI的思维过程的开关(需模型支持) |
|
||||
|
||||
### 效果图
|
||||

|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from nonebot import (
|
|||
require,
|
||||
)
|
||||
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.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
|
@ -38,12 +38,22 @@ import nonebot_plugin_localstore as store
|
|||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
require("nonebot_plugin_tortoise_orm")
|
||||
# 必须在 require 之后导入模型,才能正确注册到 Tortoise ORM
|
||||
from . import models # noqa: F401
|
||||
|
||||
require("nonebot_plugin_tortoise_orm")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai.types.chat import (
|
||||
ChatCompletionContentPartParam,
|
||||
ChatCompletionMessageParam,
|
||||
)
|
||||
|
||||
from .db_manager import DatabaseManager
|
||||
from .models import ChatHistory, ChatMessage, GroupChatState, PrivateChatState
|
||||
from .migration import migrate_from_json_to_db
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="llmchat",
|
||||
description="支持多API预设、MCP协议、联网搜索、视觉模型、Nano Banana(生图模型)的AI群聊插件",
|
||||
|
|
@ -375,13 +385,21 @@ async def process_messages(context_id: int, is_group: bool = True):
|
|||
"- 不要使用markdown或者html,聊天软件不支持解析,换行请用换行符。",
|
||||
"- 你应该以普通人的方式发送消息,每条消息字数要尽量少一些,但是不建议超过两条。",
|
||||
"- 代码则不需要分段,用单独的一条消息发送。",
|
||||
"- 请使用发送者的昵称称呼发送者,你可以礼貌地问候发送者,但只需要在第一次回答这位发送者的问题时问候他。",
|
||||
"- 你有at群成员的能力,只需要在某条消息中插入[CQ:at,qq=(QQ号)],"
|
||||
"也就是CQ码。at发送者是非必要的,如果不是必要,请不要at别人。",
|
||||
"- 请使用发送者的昵称称呼发送者,你可以礼貌地问候发送者,但只需要在"
|
||||
"第一次回答这位发送者的问题时问候他。",
|
||||
"- 你有引用某条消息的能力,使用[CQ:reply,id=(消息id)]来引用。",
|
||||
"- 如果有多条消息,你应该优先回复提到你的,一段时间之前的就不要回复了,也可以直接选择不回复。",
|
||||
"- 如果你选择完全不回复,你只需要直接输出一个<botbr>。",
|
||||
"- 如果你需要思考的话,你应该尽量少思考,以节省时间。",
|
||||
]
|
||||
|
||||
if is_group:
|
||||
system_lines += [
|
||||
"- 你有at群成员的能力,只需要在某条消息中插入[CQ:at,qq=(QQ号)],"
|
||||
"也就是CQ码。at发送者是非必要的,你可以根据你自己的想法at某个人。",
|
||||
]
|
||||
|
||||
system_lines += [
|
||||
"下面是关于你性格的设定,如果设定中提到让你扮演某个人,或者设定中有提到名字,则优先使用设定中的名字。",
|
||||
default_prompt,
|
||||
]
|
||||
|
|
@ -438,7 +456,7 @@ async def process_messages(context_id: int, is_group: bool = True):
|
|||
}
|
||||
|
||||
if preset.support_mcp:
|
||||
available_tools = await mcp_client.get_available_tools()
|
||||
available_tools = await mcp_client.get_available_tools(is_group)
|
||||
client_config["tools"] = available_tools
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
|
|
@ -470,27 +488,12 @@ async def process_messages(context_id: int, is_group: bool = True):
|
|||
# 发送工具调用提示
|
||||
await handler.send(Message(f"正在使用{mcp_client.get_friendly_name(tool_name)}"))
|
||||
|
||||
# 执行工具调用,传递群组和机器人信息用于QQ工具
|
||||
if is_group:
|
||||
result = await mcp_client.call_tool(
|
||||
tool_name,
|
||||
tool_args,
|
||||
group_id=event.group_id,
|
||||
bot_id=str(event.self_id),
|
||||
user_id=event.user_id
|
||||
)
|
||||
else:
|
||||
# 私聊时某些工具不可用(如群操作工具),跳过这些工具
|
||||
if tool_name.startswith("ob__"):
|
||||
result = f"私聊不支持{mcp_client.get_friendly_name(tool_name)}工具"
|
||||
else:
|
||||
result = await mcp_client.call_tool(
|
||||
tool_name,
|
||||
tool_args,
|
||||
group_id=None,
|
||||
bot_id=str(event.self_id),
|
||||
user_id=event.user_id
|
||||
)
|
||||
result = await mcp_client.call_tool(
|
||||
tool_name,
|
||||
tool_args,
|
||||
group_id=event.group_id,
|
||||
bot_id=str(event.self_id)
|
||||
)
|
||||
|
||||
new_messages.append({
|
||||
"role": "tool",
|
||||
|
|
@ -568,6 +571,28 @@ async def process_messages(context_id: int, is_group: bool = True):
|
|||
finally:
|
||||
state.processing = False
|
||||
state.queue.task_done()
|
||||
|
||||
# 实时保存状态到数据库
|
||||
try:
|
||||
if is_group:
|
||||
await DatabaseManager.save_group_state(
|
||||
group_id=context_id,
|
||||
preset_name=state.preset_name,
|
||||
history=state.history,
|
||||
group_prompt=state.group_prompt,
|
||||
output_reasoning_content=state.output_reasoning_content,
|
||||
random_trigger_prob=state.random_trigger_prob,
|
||||
)
|
||||
else:
|
||||
await DatabaseManager.save_private_state(
|
||||
user_id=context_id,
|
||||
preset_name=state.preset_name,
|
||||
history=state.history,
|
||||
user_prompt=state.group_prompt,
|
||||
output_reasoning_content=state.output_reasoning_content,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"实时保存状态失败 {'群号' if is_group else '用户'}:{context_id}, 错误:{e}")
|
||||
# 不再需要每次都清理MCPClient,因为它现在是单例
|
||||
# await mcp_client.cleanup()
|
||||
|
||||
|
|
@ -577,22 +602,33 @@ preset_handler = on_command("API预设", priority=1, block=True, permission=SUPE
|
|||
|
||||
|
||||
@preset_handler.handle()
|
||||
async def handle_preset(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||
group_id = event.group_id
|
||||
async def handle_preset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||
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]
|
||||
|
||||
preset_name = args.extract_plain_text().strip()
|
||||
|
||||
if preset_name == "off":
|
||||
group_states[group_id].preset_name = preset_name
|
||||
await preset_handler.finish("已关闭llmchat")
|
||||
state.preset_name = preset_name
|
||||
if isinstance(event, GroupMessageEvent):
|
||||
await preset_handler.finish("已关闭llmchat群聊功能")
|
||||
else:
|
||||
await preset_handler.finish("已关闭llmchat私聊功能")
|
||||
|
||||
available_presets = {p.name for p in plugin_config.api_presets}
|
||||
if preset_name not in available_presets:
|
||||
available_presets_str = "\n- ".join(available_presets)
|
||||
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
|
||||
await preset_handler.finish(f"已切换至API预设:{preset_name}")
|
||||
|
||||
|
||||
|
|
@ -600,16 +636,23 @@ edit_preset_handler = on_command(
|
|||
"修改设定",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||
)
|
||||
|
||||
|
||||
@edit_preset_handler.handle()
|
||||
async def handle_edit_preset(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||
group_id = event.group_id
|
||||
group_prompt = args.extract_plain_text().strip()
|
||||
async def handle_edit_preset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||
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].group_prompt = group_prompt
|
||||
group_prompt = args.extract_plain_text().strip()
|
||||
state.group_prompt = group_prompt
|
||||
await edit_preset_handler.finish("修改成功")
|
||||
|
||||
|
||||
|
|
@ -617,16 +660,25 @@ reset_handler = on_command(
|
|||
"记忆清除",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||
)
|
||||
|
||||
|
||||
@reset_handler.handle()
|
||||
async def handle_reset(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||
group_id = event.group_id
|
||||
async def handle_reset(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if isinstance(event, GroupMessageEvent):
|
||||
context_id = event.group_id
|
||||
state = group_states[context_id]
|
||||
await DatabaseManager.clear_group_history(context_id)
|
||||
else: # PrivateMessageEvent
|
||||
if not plugin_config.enable_private_chat:
|
||||
return
|
||||
context_id = event.user_id
|
||||
state = private_chat_states[context_id]
|
||||
await DatabaseManager.clear_private_history(context_id)
|
||||
|
||||
group_states[group_id].past_events.clear()
|
||||
group_states[group_id].history.clear()
|
||||
state.past_events.clear()
|
||||
state.history.clear()
|
||||
await reset_handler.finish("记忆已清空")
|
||||
|
||||
|
||||
|
|
@ -640,32 +692,39 @@ set_prob_handler = on_command(
|
|||
|
||||
@set_prob_handler.handle()
|
||||
async def handle_set_prob(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||
group_id = event.group_id
|
||||
prob = 0
|
||||
context_id = event.group_id
|
||||
state = group_states[context_id]
|
||||
|
||||
try:
|
||||
prob = float(args.extract_plain_text().strip())
|
||||
if prob < 0 or prob > 1:
|
||||
raise ValueError
|
||||
except Exception as e:
|
||||
await reset_handler.finish(f"输入有误,请使用 [0,1] 的浮点数\n{e!s}")
|
||||
raise ValueError("概率值必须在0-1之间")
|
||||
except ValueError as e:
|
||||
await set_prob_handler.finish(f"输入有误,请使用 [0,1] 的浮点数\n{e!s}")
|
||||
return
|
||||
|
||||
group_states[group_id].random_trigger_prob = prob
|
||||
await reset_handler.finish(f"主动回复概率已设为 {prob}")
|
||||
state.random_trigger_prob = prob
|
||||
await set_prob_handler.finish(f"主动回复概率已设为 {prob}")
|
||||
|
||||
|
||||
# 预设切换命令
|
||||
# 思维输出切换命令
|
||||
think_handler = on_command(
|
||||
"切换思维输出",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER),
|
||||
permission=(SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE),
|
||||
)
|
||||
|
||||
|
||||
@think_handler.handle()
|
||||
async def handle_think(event: GroupMessageEvent, args: Message = CommandArg()):
|
||||
state = group_states[event.group_id]
|
||||
async def handle_think(event: GroupMessageEvent | PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if isinstance(event, GroupMessageEvent):
|
||||
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
|
||||
|
||||
await think_handler.finish(
|
||||
|
|
@ -673,198 +732,82 @@ async def handle_think(event: GroupMessageEvent, args: Message = CommandArg()):
|
|||
)
|
||||
|
||||
|
||||
# region 私聊相关指令
|
||||
|
||||
# 私聊预设切换命令
|
||||
private_preset_handler = on_command(
|
||||
"私聊API预设",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=SUPERUSER,
|
||||
)
|
||||
|
||||
|
||||
@private_preset_handler.handle()
|
||||
async def handle_private_preset(event: PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if not plugin_config.enable_private_chat:
|
||||
await private_preset_handler.finish("私聊功能未启用")
|
||||
|
||||
user_id = event.user_id
|
||||
preset_name = args.extract_plain_text().strip()
|
||||
|
||||
if preset_name == "off":
|
||||
private_chat_states[user_id].preset_name = preset_name
|
||||
await private_preset_handler.finish("已关闭llmchat私聊功能")
|
||||
|
||||
available_presets = {p.name for p in plugin_config.api_presets}
|
||||
if preset_name not in available_presets:
|
||||
available_presets_str = "\n- ".join(available_presets)
|
||||
await private_preset_handler.finish(
|
||||
f"当前API预设:{private_chat_states[user_id].preset_name}\n可用API预设:\n- {available_presets_str}"
|
||||
)
|
||||
|
||||
private_chat_states[user_id].preset_name = preset_name
|
||||
await private_preset_handler.finish(f"已切换至API预设:{preset_name}")
|
||||
|
||||
|
||||
# 私聊设定修改命令
|
||||
private_edit_preset_handler = on_command(
|
||||
"私聊修改设定",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=SUPERUSER,
|
||||
)
|
||||
|
||||
|
||||
@private_edit_preset_handler.handle()
|
||||
async def handle_private_edit_preset(event: PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if not plugin_config.enable_private_chat:
|
||||
await private_edit_preset_handler.finish("私聊功能未启用")
|
||||
|
||||
user_id = event.user_id
|
||||
user_prompt = args.extract_plain_text().strip()
|
||||
|
||||
private_chat_states[user_id].group_prompt = user_prompt
|
||||
await private_edit_preset_handler.finish("修改成功")
|
||||
|
||||
|
||||
# 私聊记忆清除命令
|
||||
private_reset_handler = on_command(
|
||||
"私聊记忆清除",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=SUPERUSER,
|
||||
)
|
||||
|
||||
|
||||
@private_reset_handler.handle()
|
||||
async def handle_private_reset(event: PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if not plugin_config.enable_private_chat:
|
||||
await private_reset_handler.finish("私聊功能未启用")
|
||||
|
||||
user_id = event.user_id
|
||||
|
||||
private_chat_states[user_id].past_events.clear()
|
||||
private_chat_states[user_id].history.clear()
|
||||
await private_reset_handler.finish("记忆已清空")
|
||||
|
||||
|
||||
# 私聊思维输出切换命令
|
||||
private_think_handler = on_command(
|
||||
"私聊切换思维输出",
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=SUPERUSER,
|
||||
)
|
||||
|
||||
|
||||
@private_think_handler.handle()
|
||||
async def handle_private_think(event: PrivateMessageEvent, args: Message = CommandArg()):
|
||||
if not plugin_config.enable_private_chat:
|
||||
await private_think_handler.finish("私聊功能未启用")
|
||||
|
||||
state = private_chat_states[event.user_id]
|
||||
state.output_reasoning_content = not state.output_reasoning_content
|
||||
|
||||
await private_think_handler.finish(
|
||||
f"已{(state.output_reasoning_content and '开启') or '关闭'}思维输出"
|
||||
)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region 持久化与定时任务
|
||||
|
||||
# 获取插件数据目录
|
||||
data_dir = store.get_plugin_data_dir()
|
||||
# 获取插件数据文件
|
||||
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():
|
||||
"""保存群组状态到文件"""
|
||||
logger.info(f"开始保存群组状态到文件:{data_file}")
|
||||
data = {
|
||||
gid: {
|
||||
"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,
|
||||
"random_trigger_prob": state.random_trigger_prob,
|
||||
}
|
||||
for gid, state in group_states.items()
|
||||
}
|
||||
|
||||
os.makedirs(os.path.dirname(data_file), exist_ok=True)
|
||||
async with aiofiles.open(data_file, "w", encoding="utf8") as f:
|
||||
await f.write(json.dumps(data, ensure_ascii=False))
|
||||
|
||||
"""保存所有群组和私聊状态到数据库"""
|
||||
logger.info("开始保存所有状态到数据库")
|
||||
|
||||
# 保存群组状态
|
||||
for gid, state in group_states.items():
|
||||
await DatabaseManager.save_group_state(
|
||||
group_id=gid,
|
||||
preset_name=state.preset_name,
|
||||
history=state.history,
|
||||
group_prompt=state.group_prompt,
|
||||
output_reasoning_content=state.output_reasoning_content,
|
||||
random_trigger_prob=state.random_trigger_prob,
|
||||
)
|
||||
|
||||
# 保存私聊状态
|
||||
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))
|
||||
for uid, state in private_chat_states.items():
|
||||
await DatabaseManager.save_private_state(
|
||||
user_id=uid,
|
||||
preset_name=state.preset_name,
|
||||
history=state.history,
|
||||
user_prompt=state.group_prompt, # 注意:这里应该是 group_prompt 但是在 PrivateChatState 中叫 group_prompt
|
||||
output_reasoning_content=state.output_reasoning_content,
|
||||
)
|
||||
|
||||
logger.info("所有状态保存完成")
|
||||
|
||||
|
||||
async def load_state():
|
||||
"""从文件加载群组状态"""
|
||||
logger.info(f"从文件加载群组状态:{data_file}")
|
||||
if not os.path.exists(data_file):
|
||||
return
|
||||
|
||||
async with aiofiles.open(data_file, encoding="utf8") as f:
|
||||
data = json.loads(await f.read())
|
||||
for gid, state_data in data.items():
|
||||
state = GroupState()
|
||||
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"]
|
||||
state.random_trigger_prob = state_data.get("random_trigger_prob", plugin_config.random_trigger_prob)
|
||||
group_states[int(gid)] = state
|
||||
|
||||
"""从数据库加载所有状态"""
|
||||
logger.info("从数据库加载所有状态")
|
||||
|
||||
history_maxlen = plugin_config.history_size * 2
|
||||
|
||||
# 加载群组状态
|
||||
group_data = await DatabaseManager.load_all_group_states(history_maxlen)
|
||||
for gid, state_data in group_data.items():
|
||||
state = GroupState()
|
||||
state.preset_name = state_data["preset_name"]
|
||||
state.history = state_data["history"]
|
||||
state.group_prompt = state_data["group_prompt"]
|
||||
state.output_reasoning_content = state_data["output_reasoning_content"]
|
||||
state.random_trigger_prob = state_data["random_trigger_prob"]
|
||||
state.last_active = state_data["last_active"]
|
||||
group_states[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
|
||||
private_data = await DatabaseManager.load_all_private_states(history_maxlen)
|
||||
for uid, state_data in private_data.items():
|
||||
state = PrivateChatState()
|
||||
state.preset_name = state_data["preset_name"]
|
||||
state.history = state_data["history"]
|
||||
state.group_prompt = state_data["user_prompt"] # 注意:从 user_prompt 恢复到 group_prompt
|
||||
state.output_reasoning_content = state_data["output_reasoning_content"]
|
||||
state.last_active = state_data["last_active"]
|
||||
private_chat_states[uid] = state
|
||||
|
||||
logger.info(f"已加载 {len(group_states)} 个群组和 {len(private_chat_states)} 个私聊的状态")
|
||||
|
||||
|
||||
# 注册生命周期事件
|
||||
@driver.on_startup
|
||||
async def init_plugin():
|
||||
logger.info("插件启动初始化")
|
||||
# 首先进行数据迁移(如果存在 JSON 文件)
|
||||
logger.info("检查是否需要进行 JSON 数据迁移...")
|
||||
await migrate_from_json_to_db(Config(llmchat=plugin_config))
|
||||
await load_state()
|
||||
# 每5分钟保存状态
|
||||
scheduler.add_job(save_state, "interval", minutes=5)
|
||||
# 每30分钟保存一次状态作为备份(已开启实时保存)
|
||||
scheduler.add_job(save_state, "interval", minutes=30)
|
||||
logger.info("插件初始化完成(已启用实时保存功能)")
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
|
|
|
|||
269
nonebot_plugin_llmchat/db_manager.py
Normal file
269
nonebot_plugin_llmchat/db_manager.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
"""
|
||||
数据库操作层
|
||||
处理聊天历史和状态的持久化
|
||||
"""
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from nonebot import logger
|
||||
|
||||
from .models import ChatHistory, ChatMessage, GroupChatState, PrivateChatState
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
"""数据库管理器"""
|
||||
|
||||
@staticmethod
|
||||
async def save_group_state(
|
||||
group_id: int,
|
||||
preset_name: str,
|
||||
history: deque,
|
||||
group_prompt: Optional[str],
|
||||
output_reasoning_content: bool,
|
||||
random_trigger_prob: float,
|
||||
):
|
||||
"""保存群组状态"""
|
||||
try:
|
||||
# 保存或更新群组状态
|
||||
state, _ = await GroupChatState.get_or_create(
|
||||
group_id=group_id,
|
||||
defaults={
|
||||
"preset_name": preset_name,
|
||||
"group_prompt": group_prompt,
|
||||
"output_reasoning_content": output_reasoning_content,
|
||||
"random_trigger_prob": random_trigger_prob,
|
||||
},
|
||||
)
|
||||
if _: # 如果是新创建的
|
||||
logger.debug(f"创建群组状态记录: {group_id}")
|
||||
else:
|
||||
# 更新现有记录
|
||||
state.preset_name = preset_name
|
||||
state.group_prompt = group_prompt
|
||||
state.output_reasoning_content = output_reasoning_content
|
||||
state.random_trigger_prob = random_trigger_prob
|
||||
await state.save()
|
||||
logger.debug(f"更新群组状态记录: {group_id}")
|
||||
|
||||
# 保存历史快照
|
||||
messages_list = list(history)
|
||||
history_record, _ = await ChatHistory.get_or_create(
|
||||
group_id=group_id,
|
||||
is_private=False,
|
||||
defaults={"messages_json": ChatHistory.serialize_messages(messages_list)},
|
||||
)
|
||||
if not _:
|
||||
history_record.messages_json = ChatHistory.serialize_messages(messages_list)
|
||||
await history_record.save()
|
||||
|
||||
logger.debug(f"已保存群组 {group_id} 的历史记录({len(messages_list)} 条消息)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存群组状态失败 群号: {group_id}, 错误: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def save_private_state(
|
||||
user_id: int,
|
||||
preset_name: str,
|
||||
history: deque,
|
||||
user_prompt: Optional[str],
|
||||
output_reasoning_content: bool,
|
||||
):
|
||||
"""保存私聊状态"""
|
||||
try:
|
||||
# 保存或更新私聊状态
|
||||
state, _ = await PrivateChatState.get_or_create(
|
||||
user_id=user_id,
|
||||
defaults={
|
||||
"preset_name": preset_name,
|
||||
"user_prompt": user_prompt,
|
||||
"output_reasoning_content": output_reasoning_content,
|
||||
},
|
||||
)
|
||||
if _: # 如果是新创建的
|
||||
logger.debug(f"创建私聊状态记录: {user_id}")
|
||||
else:
|
||||
# 更新现有记录
|
||||
state.preset_name = preset_name
|
||||
state.user_prompt = user_prompt
|
||||
state.output_reasoning_content = output_reasoning_content
|
||||
await state.save()
|
||||
logger.debug(f"更新私聊状态记录: {user_id}")
|
||||
|
||||
# 保存历史快照
|
||||
messages_list = list(history)
|
||||
history_record, _ = await ChatHistory.get_or_create(
|
||||
user_id=user_id,
|
||||
is_private=True,
|
||||
defaults={"messages_json": ChatHistory.serialize_messages(messages_list)},
|
||||
)
|
||||
if not _:
|
||||
history_record.messages_json = ChatHistory.serialize_messages(messages_list)
|
||||
await history_record.save()
|
||||
|
||||
logger.debug(f"已保存用户 {user_id} 的历史记录({len(messages_list)} 条消息)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存私聊状态失败 用户: {user_id}, 错误: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def load_group_state(group_id: int, history_maxlen: int) -> Optional[dict]:
|
||||
"""从数据库加载群组状态"""
|
||||
try:
|
||||
state = await GroupChatState.get_or_none(group_id=group_id)
|
||||
if not state:
|
||||
logger.debug(f"未找到群组 {group_id} 的状态记录,返回默认值")
|
||||
return None
|
||||
|
||||
# 加载历史
|
||||
history_record = await ChatHistory.get_or_none(
|
||||
group_id=group_id, is_private=False
|
||||
)
|
||||
history = deque(
|
||||
ChatHistory.deserialize_messages(history_record.messages_json)
|
||||
if history_record
|
||||
else [],
|
||||
maxlen=history_maxlen,
|
||||
)
|
||||
|
||||
logger.debug(f"已加载群组 {group_id} 的状态({len(history)} 条历史)")
|
||||
|
||||
return {
|
||||
"preset_name": state.preset_name,
|
||||
"history": history,
|
||||
"group_prompt": state.group_prompt,
|
||||
"output_reasoning_content": state.output_reasoning_content,
|
||||
"random_trigger_prob": state.random_trigger_prob,
|
||||
"last_active": state.last_active.timestamp(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载群组状态失败 群号: {group_id}, 错误: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def load_private_state(user_id: int, history_maxlen: int) -> Optional[dict]:
|
||||
"""从数据库加载私聊状态"""
|
||||
try:
|
||||
state = await PrivateChatState.get_or_none(user_id=user_id)
|
||||
if not state:
|
||||
logger.debug(f"未找到用户 {user_id} 的状态记录,返回默认值")
|
||||
return None
|
||||
|
||||
# 加载历史
|
||||
history_record = await ChatHistory.get_or_none(
|
||||
user_id=user_id, is_private=True
|
||||
)
|
||||
history = deque(
|
||||
ChatHistory.deserialize_messages(history_record.messages_json)
|
||||
if history_record
|
||||
else [],
|
||||
maxlen=history_maxlen,
|
||||
)
|
||||
|
||||
logger.debug(f"已加载用户 {user_id} 的状态({len(history)} 条历史)")
|
||||
|
||||
return {
|
||||
"preset_name": state.preset_name,
|
||||
"history": history,
|
||||
"user_prompt": state.user_prompt,
|
||||
"output_reasoning_content": state.output_reasoning_content,
|
||||
"last_active": state.last_active.timestamp(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载私聊状态失败 用户: {user_id}, 错误: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def load_all_group_states(history_maxlen: int) -> dict:
|
||||
"""加载所有群组状态"""
|
||||
try:
|
||||
states = await GroupChatState.all()
|
||||
result = {}
|
||||
|
||||
for state in states:
|
||||
history_record = await ChatHistory.get_or_none(
|
||||
group_id=state.group_id, is_private=False
|
||||
)
|
||||
history = deque(
|
||||
ChatHistory.deserialize_messages(history_record.messages_json)
|
||||
if history_record
|
||||
else [],
|
||||
maxlen=history_maxlen,
|
||||
)
|
||||
|
||||
result[state.group_id] = {
|
||||
"preset_name": state.preset_name,
|
||||
"history": history,
|
||||
"group_prompt": state.group_prompt,
|
||||
"output_reasoning_content": state.output_reasoning_content,
|
||||
"random_trigger_prob": state.random_trigger_prob,
|
||||
"last_active": state.last_active.timestamp(),
|
||||
}
|
||||
|
||||
logger.info(f"已加载 {len(result)} 个群组的状态")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载所有群组状态失败, 错误: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
async def load_all_private_states(history_maxlen: int) -> dict:
|
||||
"""加载所有私聊状态"""
|
||||
try:
|
||||
states = await PrivateChatState.all()
|
||||
result = {}
|
||||
|
||||
for state in states:
|
||||
history_record = await ChatHistory.get_or_none(
|
||||
user_id=state.user_id, is_private=True
|
||||
)
|
||||
history = deque(
|
||||
ChatHistory.deserialize_messages(history_record.messages_json)
|
||||
if history_record
|
||||
else [],
|
||||
maxlen=history_maxlen,
|
||||
)
|
||||
|
||||
result[state.user_id] = {
|
||||
"preset_name": state.preset_name,
|
||||
"history": history,
|
||||
"user_prompt": state.user_prompt,
|
||||
"output_reasoning_content": state.output_reasoning_content,
|
||||
"last_active": state.last_active.timestamp(),
|
||||
}
|
||||
|
||||
logger.info(f"已加载 {len(result)} 个用户的私聊状态")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载所有私聊状态失败, 错误: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
async def clear_group_history(group_id: int):
|
||||
"""清空群组历史"""
|
||||
try:
|
||||
await ChatHistory.filter(group_id=group_id, is_private=False).delete()
|
||||
state = await GroupChatState.get_or_none(group_id=group_id)
|
||||
if state:
|
||||
await state.delete()
|
||||
logger.info(f"已清空群组 {group_id} 的历史记录")
|
||||
except Exception as e:
|
||||
logger.error(f"清空群组历史失败 群号: {group_id}, 错误: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def clear_private_history(user_id: int):
|
||||
"""清空私聊历史"""
|
||||
try:
|
||||
await ChatHistory.filter(user_id=user_id, is_private=True).delete()
|
||||
state = await PrivateChatState.get_or_none(user_id=user_id)
|
||||
if state:
|
||||
await state.delete()
|
||||
logger.info(f"已清空用户 {user_id} 的历史记录")
|
||||
except Exception as e:
|
||||
logger.error(f"清空私聊历史失败 用户: {user_id}, 错误: {e}")
|
||||
|
|
@ -108,7 +108,7 @@ class MCPClient:
|
|||
|
||||
return SessionContext()
|
||||
|
||||
async def get_available_tools(self):
|
||||
async def get_available_tools(self, is_group: bool):
|
||||
"""获取可用工具列表,使用缓存机制"""
|
||||
if self._tools_cache is not None:
|
||||
logger.debug("返回缓存的工具列表")
|
||||
|
|
@ -117,10 +117,11 @@ 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内置工具")
|
||||
if is_group:
|
||||
# 添加OneBot内置工具,仅在群聊中可用
|
||||
onebot_tools = self.onebot_tools.get_available_tools()
|
||||
available_tools.extend(onebot_tools)
|
||||
logger.debug(f"添加了{len(onebot_tools)}个OneBot内置工具")
|
||||
|
||||
# 添加自定义工具(可选)
|
||||
# if hasattr(self, 'custom_tools'):
|
||||
|
|
|
|||
162
nonebot_plugin_llmchat/migration.py
Normal file
162
nonebot_plugin_llmchat/migration.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""
|
||||
数据迁移脚本
|
||||
将聊天数据从 JSON 文件迁移到数据库
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot import logger
|
||||
|
||||
from .config import Config
|
||||
from .db_manager import DatabaseManager
|
||||
from .models import ChatHistory, GroupChatState, PrivateChatState
|
||||
|
||||
# 获取插件数据目录
|
||||
try:
|
||||
import nonebot_plugin_localstore as store
|
||||
data_dir = store.get_plugin_data_dir()
|
||||
data_file = store.get_plugin_data_file("llmchat_state.json")
|
||||
private_data_file = store.get_plugin_data_file("llmchat_private_state.json")
|
||||
except ImportError:
|
||||
logger.warning("无法找到 nonebot_plugin_localstore,迁移可能失败")
|
||||
data_dir = None
|
||||
data_file = None
|
||||
private_data_file = None
|
||||
|
||||
|
||||
async def migrate_from_json_to_db(plugin_config: Config):
|
||||
"""从 JSON 文件迁移到数据库"""
|
||||
logger.info("开始从 JSON 文件迁移到数据库")
|
||||
|
||||
if not data_file or not os.path.exists(data_file):
|
||||
logger.info("未找到群组状态 JSON 文件,跳过迁移")
|
||||
return
|
||||
|
||||
total_migrated_groups = 0
|
||||
total_migrated_users = 0
|
||||
|
||||
try:
|
||||
# 迁移群组状态
|
||||
logger.info(f"正在迁移群组状态数据: {data_file}")
|
||||
with open(data_file, "r", encoding="utf8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
migrated_groups = 0
|
||||
for gid_str, state_data in data.items():
|
||||
try:
|
||||
gid = int(gid_str)
|
||||
# 检查是否已存在
|
||||
existing = await GroupChatState.get_or_none(group_id=gid)
|
||||
if existing:
|
||||
logger.debug(f"群组 {gid} 已存在于数据库,跳过迁移")
|
||||
continue
|
||||
|
||||
# 创建新的状态记录
|
||||
await GroupChatState.create(
|
||||
group_id=gid,
|
||||
preset_name=state_data.get("preset", "off"),
|
||||
group_prompt=state_data.get("group_prompt"),
|
||||
output_reasoning_content=state_data.get("output_reasoning_content", False),
|
||||
random_trigger_prob=state_data.get("random_trigger_prob", 0.05),
|
||||
last_active=datetime.fromtimestamp(state_data.get("last_active", datetime.now().timestamp())),
|
||||
)
|
||||
|
||||
# 创建历史记录
|
||||
messages = state_data.get("history", [])
|
||||
if messages:
|
||||
await ChatHistory.create(
|
||||
group_id=gid,
|
||||
is_private=False,
|
||||
messages_json=ChatHistory.serialize_messages(messages),
|
||||
)
|
||||
|
||||
migrated_groups += 1
|
||||
logger.debug(f"已迁移群组 {gid}({len(messages)} 条消息)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移群组 {gid_str} 失败: {e}")
|
||||
|
||||
logger.info(f"成功迁移 {migrated_groups} 个群组的状态")
|
||||
total_migrated_groups = migrated_groups
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移群组状态失败: {e}")
|
||||
|
||||
# 迁移私聊状态
|
||||
if plugin_config.llmchat.enable_private_chat and private_data_file and os.path.exists(private_data_file):
|
||||
try:
|
||||
logger.info(f"正在迁移私聊状态数据: {private_data_file}")
|
||||
with open(private_data_file, "r", encoding="utf8") as f:
|
||||
private_data = json.load(f)
|
||||
|
||||
migrated_users = 0
|
||||
for uid_str, state_data in private_data.items():
|
||||
try:
|
||||
uid = int(uid_str)
|
||||
# 检查是否已存在
|
||||
existing = await PrivateChatState.get_or_none(user_id=uid)
|
||||
if existing:
|
||||
logger.debug(f"用户 {uid} 已存在于数据库,跳过迁移")
|
||||
continue
|
||||
|
||||
# 创建新的状态记录
|
||||
await PrivateChatState.create(
|
||||
user_id=uid,
|
||||
preset_name=state_data.get("preset", "off"),
|
||||
user_prompt=state_data.get("group_prompt"), # JSON 中存的是 group_prompt
|
||||
output_reasoning_content=state_data.get("output_reasoning_content", False),
|
||||
last_active=datetime.fromtimestamp(state_data.get("last_active", datetime.now().timestamp())),
|
||||
)
|
||||
|
||||
# 创建历史记录
|
||||
messages = state_data.get("history", [])
|
||||
if messages:
|
||||
await ChatHistory.create(
|
||||
user_id=uid,
|
||||
is_private=True,
|
||||
messages_json=ChatHistory.serialize_messages(messages),
|
||||
)
|
||||
|
||||
migrated_users += 1
|
||||
logger.debug(f"已迁移用户 {uid}({len(messages)} 条消息)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移用户 {uid_str} 失败: {e}")
|
||||
|
||||
logger.info(f"成功迁移 {migrated_users} 个用户的私聊状态")
|
||||
total_migrated_users = migrated_users
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移私聊状态失败: {e}")
|
||||
|
||||
# 迁移成功后,重命名 JSON 文件为 .migrated
|
||||
if total_migrated_groups > 0 or total_migrated_users > 0:
|
||||
logger.info("迁移成功,开始重命名 JSON 文件...")
|
||||
rename_json_files_to_migrated()
|
||||
|
||||
logger.info(f"JSON 迁移完成(群组: {total_migrated_groups},用户: {total_migrated_users})")
|
||||
|
||||
|
||||
def rename_json_files_to_migrated():
|
||||
"""将已迁移的 JSON 文件重命名为 .migrated"""
|
||||
if not data_file:
|
||||
return
|
||||
|
||||
if os.path.exists(data_file):
|
||||
migrated_file = f"{data_file}.migrated"
|
||||
try:
|
||||
os.rename(data_file, migrated_file)
|
||||
logger.info(f"已将群组状态文件重命名为: {migrated_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"重命名文件失败: {e}")
|
||||
|
||||
if private_data_file and os.path.exists(private_data_file):
|
||||
migrated_file = f"{private_data_file}.migrated"
|
||||
try:
|
||||
os.rename(private_data_file, migrated_file)
|
||||
logger.info(f"已将私聊状态文件重命名为: {migrated_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"重命名文件失败: {e}")
|
||||
100
nonebot_plugin_llmchat/models.py
Normal file
100
nonebot_plugin_llmchat/models.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Tortoise ORM 模型定义
|
||||
用于存储聊天历史和群组/私聊状态
|
||||
"""
|
||||
import json
|
||||
|
||||
from nonebot_plugin_tortoise_orm import add_model
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
# 注册模型到 Tortoise ORM
|
||||
add_model(__name__)
|
||||
|
||||
|
||||
class GroupChatState(Model):
|
||||
"""群组聊天状态"""
|
||||
|
||||
id = fields.IntField(pk=True)
|
||||
group_id = fields.BigIntField(unique=True, description="群号")
|
||||
preset_name = fields.CharField(max_length=50, description="当前使用的 API 预设名")
|
||||
group_prompt = fields.TextField(null=True, description="群组自定义提示词")
|
||||
output_reasoning_content = fields.BooleanField(default=False, description="是否输出推理内容")
|
||||
random_trigger_prob = fields.FloatField(default=0.05, description="随机触发概率")
|
||||
last_active = fields.DatetimeField(auto_now=True, description="最后活跃时间")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
|
||||
class Meta:
|
||||
table = "llmchat_group_state"
|
||||
table_description = "群组聊天状态表"
|
||||
|
||||
|
||||
class PrivateChatState(Model):
|
||||
"""私聊状态"""
|
||||
|
||||
id = fields.IntField(pk=True)
|
||||
user_id = fields.BigIntField(unique=True, description="用户 QQ")
|
||||
preset_name = fields.CharField(max_length=50, description="当前使用的 API 预设名")
|
||||
user_prompt = fields.TextField(null=True, description="用户自定义提示词")
|
||||
output_reasoning_content = fields.BooleanField(default=False, description="是否输出推理内容")
|
||||
last_active = fields.DatetimeField(auto_now=True, description="最后活跃时间")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
|
||||
class Meta:
|
||||
table = "llmchat_private_state"
|
||||
table_description = "私聊状态表"
|
||||
|
||||
|
||||
class ChatMessage(Model):
|
||||
"""聊天消息历史"""
|
||||
|
||||
id = fields.IntField(pk=True)
|
||||
group_id = fields.BigIntField(null=True, description="群号(私聊时为 NULL)")
|
||||
user_id = fields.BigIntField(null=True, description="用户 QQ(私聊时有值)")
|
||||
is_private = fields.BooleanField(default=False, description="是否为私聊")
|
||||
role = fields.CharField(
|
||||
max_length=20,
|
||||
description="消息角色: user/assistant/system/tool",
|
||||
)
|
||||
content = fields.TextField(description="消息内容(JSON 序列化)")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="消息时间")
|
||||
|
||||
class Meta:
|
||||
table = "llmchat_message"
|
||||
table_description = "聊天消息历史表"
|
||||
|
||||
@staticmethod
|
||||
def serialize_content(content) -> str:
|
||||
"""将内容序列化为 JSON 字符串"""
|
||||
return json.dumps(content, ensure_ascii=False)
|
||||
|
||||
@staticmethod
|
||||
def deserialize_content(content_str: str):
|
||||
"""从 JSON 字符串反序列化内容"""
|
||||
return json.loads(content_str)
|
||||
|
||||
|
||||
class ChatHistory(Model):
|
||||
"""聊天历史快照(用于快速加载)"""
|
||||
|
||||
id = fields.IntField(pk=True)
|
||||
group_id = fields.BigIntField(null=True, unique=True, description="群号(私聊时为 NULL)")
|
||||
user_id = fields.BigIntField(null=True, unique=True, description="用户 QQ(私聊时有值)")
|
||||
is_private = fields.BooleanField(default=False, description="是否为私聊")
|
||||
# 存储最近 history_size*2 条消息的 JSON 数组
|
||||
messages_json = fields.TextField(description="消息历史(JSON 数组)")
|
||||
last_update = fields.DatetimeField(auto_now=True, description="最后更新时间")
|
||||
|
||||
class Meta:
|
||||
table = "llmchat_history"
|
||||
table_description = "聊天历史快照表(用于快速加载)"
|
||||
|
||||
@staticmethod
|
||||
def serialize_messages(messages_list) -> str:
|
||||
"""将消息列表序列化为 JSON 字符串"""
|
||||
return json.dumps(messages_list, ensure_ascii=False)
|
||||
|
||||
@staticmethod
|
||||
def deserialize_messages(messages_json: str):
|
||||
"""从 JSON 字符串反序列化消息列表"""
|
||||
return json.loads(messages_json)
|
||||
Loading…
Add table
Add a link
Reference in a new issue