From fe3684736a026fc83eb281bb836ec895157b598e Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Sun, 9 Mar 2025 10:27:54 +0800 Subject: [PATCH 001/105] =?UTF-8?q?feat:=20=E8=B6=85=E5=A4=A7=E5=9E=8B?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 6 +- src/plugins/chat/cq_code.py | 278 +++++++-------- src/plugins/chat/emoji_manager.py | 104 ++++-- src/plugins/chat/llm_generator.py | 2 +- src/plugins/chat/message.py | 469 ++++++++++++++++--------- src/plugins/chat/message_base.py | 158 +++++++++ src/plugins/chat/message_cq.py | 188 ++++++++++ src/plugins/chat/message_sender.py | 2 +- src/plugins/chat/storage.py | 2 +- src/plugins/chat/utils.py | 23 +- src/plugins/chat/utils_image.py | 543 ++++++++++++++++++----------- 11 files changed, 1209 insertions(+), 566 deletions(-) create mode 100644 src/plugins/chat/message_base.py create mode 100644 src/plugins/chat/message_cq.py diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a02c4a059..c9eea3ed9 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -7,10 +7,10 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent from ..memory_system.memory import hippocampus from ..moods.moods import MoodManager # 导入情绪管理器 from .config import global_config -from .cq_code import CQCode # 导入CQCode模块 +from .cq_code import CQCode,cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator -from .message import ( +from .message_cq import ( Message, Message_Sending, Message_Thinking, # 导入 Message_Thinking 类 @@ -180,7 +180,7 @@ class ChatBot: if emoji_raw != None: emoji_path,discription = emoji_raw - emoji_cq = CQCode.create_emoji_cq(emoji_path) + emoji_cq = cq_code_tool.create_emoji_cq(emoji_path) if random() < 0.5: bot_response_time = tinking_time_point - 1 diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index 4a295e3d5..e908219b5 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -3,7 +3,7 @@ import html import os import time from dataclasses import dataclass -from typing import Dict, Optional +from typing import Dict, Optional, List, Union import requests @@ -12,12 +12,14 @@ import requests import urllib3 from nonebot import get_driver from urllib3.util import create_urllib3_context +from loguru import logger from ..models.utils_model import LLM_request from .config import global_config from .mapper import emojimapper -from .utils_image import storage_emoji, storage_image +from .utils_image import image_manager from .utils_user import get_user_nickname +from .message_base import Seg driver = get_driver() config = driver.config @@ -48,16 +50,15 @@ class CQCode: type: CQ码类型(如'image', 'at', 'face'等) params: CQ码的参数字典 raw_code: 原始CQ码字符串 - translated_plain_text: 经过处理(如AI翻译)后的文本表示 + translated_segments: 经过处理后的Seg对象列表 """ type: str params: Dict[str, str] - # raw_code: str group_id: int user_id: int group_name: str = "" user_nickname: str = "" - translated_plain_text: Optional[str] = None + translated_segments: Optional[Union[Seg, List[Seg]]] = None reply_message: Dict = None # 存储回复消息 image_base64: Optional[str] = None _llm: Optional[LLM_request] = None @@ -66,31 +67,72 @@ class CQCode: """初始化LLM实例""" self._llm = LLM_request(model=global_config.vlm, temperature=0.4, max_tokens=300) - async def translate(self): - """根据CQ码类型进行相应的翻译处理""" + def translate(self): + """根据CQ码类型进行相应的翻译处理,转换为Seg对象""" if self.type == 'text': - self.translated_plain_text = self.params.get('text', '') + self.translated_segments = Seg( + type='text', + data=self.params.get('text', '') + ) elif self.type == 'image': - if self.params.get('sub_type') == '0': - self.translated_plain_text = await self.translate_image() + base64_data = self.translate_image() + if base64_data: + if self.params.get('sub_type') == '0': + self.translated_segments = Seg( + type='image', + data=base64_data + ) + else: + self.translated_segments = Seg( + type='emoji', + data=base64_data + ) else: - self.translated_plain_text = await self.translate_emoji() + self.translated_segments = Seg( + type='text', + data='[图片]' + ) elif self.type == 'at': user_nickname = get_user_nickname(self.params.get('qq', '')) - if user_nickname: - self.translated_plain_text = f"[@{user_nickname}]" - else: - self.translated_plain_text = "@某人" + self.translated_segments = Seg( + type='text', + data=f"[@{user_nickname or '某人'}]" + ) elif self.type == 'reply': - self.translated_plain_text = await self.translate_reply() + reply_segments = self.translate_reply() + if reply_segments: + self.translated_segments = Seg( + type='seglist', + data=reply_segments + ) + else: + self.translated_segments = Seg( + type='text', + data='[回复某人消息]' + ) elif self.type == 'face': face_id = self.params.get('id', '') - # self.translated_plain_text = f"[表情{face_id}]" - self.translated_plain_text = f"[{emojimapper.get(int(face_id), '表情')}]" + self.translated_segments = Seg( + type='text', + data=f"[{emojimapper.get(int(face_id), '表情')}]" + ) elif self.type == 'forward': - self.translated_plain_text = await self.translate_forward() + forward_segments = self.translate_forward() + if forward_segments: + self.translated_segments = Seg( + type='seglist', + data=forward_segments + ) + else: + self.translated_segments = Seg( + type='text', + data='[转发消息]' + ) else: - self.translated_plain_text = f"[{self.type}]" + self.translated_segments = Seg( + type='text', + data=f"[{self.type}]" + ) def get_img(self): ''' @@ -160,155 +202,101 @@ class CQCode: return None - async def translate_emoji(self) -> str: - """处理表情包类型的CQ码""" + + def translate_image(self) -> Optional[str]: + """处理图片类型的CQ码,返回base64字符串""" if 'url' not in self.params: - return '[表情包]' - base64_str = self.get_img() - if base64_str: - # 将 base64 字符串转换为字节类型 - image_bytes = base64.b64decode(base64_str) - storage_emoji(image_bytes) - return await self.get_emoji_description(base64_str) - else: - return '[表情包]' + return None + return self.get_img() - async def translate_image(self) -> str: - """处理图片类型的CQ码,区分普通图片和表情包""" - # 没有url,直接返回默认文本 - if 'url' not in self.params: - return '[图片]' - base64_str = self.get_img() - if base64_str: - image_bytes = base64.b64decode(base64_str) - storage_image(image_bytes) - return await self.get_image_description(base64_str) - else: - return '[图片]' - - async def get_emoji_description(self, image_base64: str) -> str: - """调用AI接口获取表情包描述""" - try: - prompt = "这是一个表情包,请用简短的中文描述这个表情包传达的情感和含义。最多20个字。" - # description, _ = self._llm.generate_response_for_image_sync(prompt, image_base64) - description, _ = await self._llm.generate_response_for_image(prompt, image_base64) - return f"[表情包:{description}]" - except Exception as e: - print(f"\033[1;31m[错误]\033[0m AI接口调用失败: {str(e)}") - return "[表情包]" - - async def get_image_description(self, image_base64: str) -> str: - """调用AI接口获取普通图片描述""" - try: - prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" - # description, _ = self._llm.generate_response_for_image_sync(prompt, image_base64) - description, _ = await self._llm.generate_response_for_image(prompt, image_base64) - return f"[图片:{description}]" - except Exception as e: - print(f"\033[1;31m[错误]\033[0m AI接口调用失败: {str(e)}") - return "[图片]" - - async def translate_forward(self) -> str: - """处理转发消息""" + def translate_forward(self) -> Optional[List[Seg]]: + """处理转发消息,返回Seg列表""" try: if 'content' not in self.params: - return '[转发消息]' + return None - # 解析content内容(需要先反转义) content = self.unescape(self.params['content']) - # print(f"\033[1;34m[调试信息]\033[0m 转发消息内容: {content}") - # 将字符串形式的列表转换为Python对象 import ast try: messages = ast.literal_eval(content) except ValueError as e: - print(f"\033[1;31m[错误]\033[0m 解析转发消息内容失败: {str(e)}") - return '[转发消息]' + logger.error(f"解析转发消息内容失败: {str(e)}") + return None - # 处理每条消息 - formatted_messages = [] + formatted_segments = [] for msg in messages: sender = msg.get('sender', {}) nickname = sender.get('card') or sender.get('nickname', '未知用户') - - # 获取消息内容并使用Message类处理 raw_message = msg.get('raw_message', '') message_array = msg.get('message', []) if message_array and isinstance(message_array, list): - # 检查是否包含嵌套的转发消息 for message_part in message_array: if message_part.get('type') == 'forward': - content = '[转发消息]' + content_seg = Seg(type='text', data='[转发消息]') break - else: - # 处理普通消息 - if raw_message: - from .message import Message - message_obj = Message( - user_id=msg.get('user_id', 0), - message_id=msg.get('message_id', 0), - raw_message=raw_message, - plain_text=raw_message, - group_id=msg.get('group_id', 0) - ) - await message_obj.initialize() - content = message_obj.processed_plain_text else: - content = '[空消息]' + if raw_message: + from .message_cq import MessageRecvCQ + message_obj = MessageRecvCQ( + user_id=msg.get('user_id', 0), + message_id=msg.get('message_id', 0), + raw_message=raw_message, + plain_text=raw_message, + group_id=msg.get('group_id', 0) + ) + content_seg = Seg(type='seglist', data=message_obj.message_segments) + else: + content_seg = Seg(type='text', data='[空消息]') else: - # 处理普通消息 if raw_message: - from .message import Message - message_obj = Message( + from .message_cq import MessageRecvCQ + message_obj = MessageRecvCQ( user_id=msg.get('user_id', 0), message_id=msg.get('message_id', 0), raw_message=raw_message, plain_text=raw_message, group_id=msg.get('group_id', 0) ) - await message_obj.initialize() - content = message_obj.processed_plain_text + content_seg = Seg(type='seglist', data=message_obj.message_segments) else: - content = '[空消息]' + content_seg = Seg(type='text', data='[空消息]') - formatted_msg = f"{nickname}: {content}" - formatted_messages.append(formatted_msg) + formatted_segments.append(Seg(type='text', data=f"{nickname}: ")) + formatted_segments.append(content_seg) + formatted_segments.append(Seg(type='text', data='\n')) - # 合并所有消息 - combined_messages = '\n'.join(formatted_messages) - print(f"\033[1;34m[调试信息]\033[0m 合并后的转发消息: {combined_messages}") - return f"[转发消息:\n{combined_messages}]" + return formatted_segments except Exception as e: - print(f"\033[1;31m[错误]\033[0m 处理转发消息失败: {str(e)}") - return '[转发消息]' + logger.error(f"处理转发消息失败: {str(e)}") + return None - async def translate_reply(self) -> str: - """处理回复类型的CQ码""" - - # 创建Message对象 - from .message import Message - if self.reply_message == None: - # print(f"\033[1;31m[错误]\033[0m 回复消息为空") - return '[回复某人消息]' + def translate_reply(self) -> Optional[List[Seg]]: + """处理回复类型的CQ码,返回Seg列表""" + from .message_cq import MessageRecvCQ + if self.reply_message is None: + return None if self.reply_message.sender.user_id: - message_obj = Message( + message_obj = MessageRecvCQ( user_id=self.reply_message.sender.user_id, message_id=self.reply_message.message_id, raw_message=str(self.reply_message.message), group_id=self.group_id ) - await message_obj.initialize() + + segments = [] if message_obj.user_id == global_config.BOT_QQ: - return f"[回复 {global_config.BOT_NICKNAME} 的消息: {message_obj.processed_plain_text}]" + segments.append(Seg(type='text', data=f"[回复 {global_config.BOT_NICKNAME} 的消息: ")) else: - return f"[回复 {self.reply_message.sender.nickname} 的消息: {message_obj.processed_plain_text}]" - + segments.append(Seg(type='text', data=f"[回复 {self.reply_message.sender.nickname} 的消息: ")) + + segments.append(Seg(type='seglist', data=message_obj.message_segments)) + segments.append(Seg(type='text', data="]")) + return segments else: - print("\033[1;31m[错误]\033[0m 回复消息的sender.user_id为空") - return '[回复某人消息]' + return None @staticmethod def unescape(text: str) -> str: @@ -318,29 +306,12 @@ class CQCode: .replace(']', ']') \ .replace('&', '&') - @staticmethod - def create_emoji_cq(file_path: str) -> str: - """ - 创建表情包CQ码 - Args: - file_path: 本地表情包文件路径 - Returns: - 表情包CQ码字符串 - """ - # 确保使用绝对路径 - abs_path = os.path.abspath(file_path) - # 转义特殊字符 - escaped_path = abs_path.replace('&', '&') \ - .replace('[', '[') \ - .replace(']', ']') \ - .replace(',', ',') - # 生成CQ码,设置sub_type=1表示这是表情包 - return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" + class CQCode_tool: @staticmethod - async def cq_from_dict_to_class(cq_code: Dict, reply: Optional[Dict] = None) -> CQCode: + def cq_from_dict_to_class(cq_code: Dict, reply: Optional[Dict] = None) -> CQCode: """ 将CQ码字典转换为CQCode对象 @@ -369,7 +340,7 @@ class CQCode_tool: ) # 进行翻译处理 - await instance.translate() + instance.translate() return instance @staticmethod @@ -382,6 +353,27 @@ class CQCode_tool: 回复CQ码字符串 """ return f"[CQ:reply,id={message_id}]" + + @staticmethod + def create_emoji_cq(file_path: str) -> str: + """ + 创建表情包CQ码 + Args: + file_path: 本地表情包文件路径 + Returns: + 表情包CQ码字符串 + """ + # 确保使用绝对路径 + abs_path = os.path.abspath(file_path) + # 转义特殊字符 + escaped_path = abs_path.replace('&', '&') \ + .replace('[', '[') \ + .replace(']', ']') \ + .replace(',', ',') + # 生成CQ码,设置sub_type=1表示这是表情包 + return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" + + cq_code_tool = CQCode_tool() diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 4f2637738..ff59220dc 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -4,6 +4,8 @@ import random import time import traceback from typing import Optional +import base64 +import hashlib from loguru import logger from nonebot import get_driver @@ -13,9 +15,11 @@ from ..chat.config import global_config from ..chat.utils import get_embedding from ..chat.utils_image import image_path_to_base64 from ..models.utils_model import LLM_request +from ..chat.utils_image import ImageManager driver = get_driver() config = driver.config +image_manager = ImageManager() class EmojiManager: @@ -142,14 +146,14 @@ class EmojiManager: emoji_similarities.sort(key=lambda x: x[1], reverse=True) # 获取前3个最相似的表情包 - top_3_emojis = emoji_similarities[:3] + top_10_emojis = emoji_similarities[:10 if len(emoji_similarities) > 10 else len(emoji_similarities)] - if not top_3_emojis: + if not top_10_emojis: logger.warning("未找到匹配的表情包") return None # 从前3个中随机选择一个 - selected_emoji, similarity = random.choice(top_3_emojis) + selected_emoji, similarity = random.choice(top_10_emojis) if selected_emoji and 'path' in selected_emoji: # 更新使用次数 @@ -172,13 +176,13 @@ class EmojiManager: return None async def _get_emoji_discription(self, image_base64: str) -> str: - """获取表情包的标签""" + """获取表情包的标签,使用image_manager的描述生成功能""" try: - prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感' - - content, _ = await self.vlm.generate_response_for_image(prompt, image_base64) - logger.debug(f"输出描述: {content}") - return content + # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀 + description = await image_manager.get_emoji_description(image_base64) + # 去掉[表情包:xxx]的格式,只保留描述内容 + description = description.strip('[]').replace('表情包:', '') + return description except Exception as e: logger.error(f"获取标签失败: {str(e)}") @@ -220,42 +224,94 @@ class EmojiManager: for filename in files_to_process: image_path = os.path.join(emoji_dir, filename) - # 检查是否已经注册过 - existing_emoji = self.db.db['emoji'].find_one({'filename': filename}) - if existing_emoji: - continue - - # 压缩图片并获取base64编码 + # 获取图片的base64编码和哈希值 image_base64 = image_path_to_base64(image_path) if image_base64 is None: os.remove(image_path) continue - # 获取表情包的描述 - discription = await self._get_emoji_discription(image_base64) + image_bytes = base64.b64decode(image_base64) + image_hash = hashlib.md5(image_bytes).hexdigest() + + # 检查是否已经注册过 + existing_emoji = self.db.db['emoji'].find_one({'filename': filename}) + description = None + + if existing_emoji: + # 即使表情包已存在,也检查是否需要同步到images集合 + description = existing_emoji.get('discription') + # 检查是否在images集合中存在 + existing_image = await image_manager.db.db.images.find_one({'hash': image_hash}) + if not existing_image: + # 同步到images集合 + image_doc = { + 'hash': image_hash, + 'path': image_path, + 'type': 'emoji', + 'description': description, + 'timestamp': int(time.time()) + } + await image_manager.db.db.images.update_one( + {'hash': image_hash}, + {'$set': image_doc}, + upsert=True + ) + # 保存描述到image_descriptions集合 + await image_manager._save_description_to_db(image_hash, description, 'emoji') + logger.success(f"同步已存在的表情包到images集合: {filename}") + continue + + # 检查是否在images集合中已有描述 + existing_description = await image_manager._get_description_from_db(image_hash, 'emoji') + + if existing_description: + description = existing_description + else: + # 获取表情包的描述 + description = await self._get_emoji_discription(image_base64) + if global_config.EMOJI_CHECK: check = await self._check_emoji(image_base64) if '是' not in check: os.remove(image_path) - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") logger.info(f"其不满足过滤规则,被剔除 {check}") continue logger.info(f"check通过 {check}") - embedding = await get_embedding(discription) - if discription is not None: + + if description is not None: + embedding = await get_embedding(description) # 准备数据库记录 emoji_record = { 'filename': filename, 'path': image_path, - 'embedding':embedding, - 'discription': discription, + 'embedding': embedding, + 'discription': description, + 'hash': image_hash, 'timestamp': int(time.time()) } - # 保存到数据库 + # 保存到emoji数据库 self.db.db['emoji'].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") + + # 保存到images数据库 + image_doc = { + 'hash': image_hash, + 'path': image_path, + 'type': 'emoji', + 'description': description, + 'timestamp': int(time.time()) + } + await image_manager.db.db.images.update_one( + {'hash': image_hash}, + {'$set': image_doc}, + upsert=True + ) + # 保存描述到image_descriptions集合 + await image_manager._save_description_to_db(image_hash, description, 'emoji') + logger.success(f"同步保存到images集合: {filename}") else: logger.warning(f"跳过表情包: {filename}") diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 1ac421e6b..2803e5a14 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -7,7 +7,7 @@ from nonebot import get_driver from ...common.database import Database from ..models.utils_model import LLM_request from .config import global_config -from .message import Message +from .message_cq import Message from .prompt_builder import prompt_builder from .relationship_manager import relationship_manager from .utils import process_llm_response diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index f1fc5569d..a561e6490 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,231 +1,360 @@ import time from dataclasses import dataclass -from typing import Dict, ForwardRef, List, Optional +from typing import Dict, ForwardRef, List, Optional, Union import urllib3 +from loguru import logger from .cq_code import CQCode, cq_code_tool from .utils_cq import parse_cq_code from .utils_user import get_groupname, get_user_cardname, get_user_nickname - -Message = ForwardRef('Message') # 添加这行 +from .utils_image import image_manager +from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) #这个类是消息数据类,用于存储和管理消息数据。 #它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 #它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 + +@dataclass +class MessageRecv(MessageBase): + """接收消息类,用于处理从MessageCQ序列化的消息""" + + def __init__(self, message_dict: Dict): + """从MessageCQ的字典初始化 + + Args: + message_dict: MessageCQ序列化后的字典 + """ + message_info = BaseMessageInfo(**message_dict.get('message_info', {})) + message_segment = Seg(**message_dict.get('message_segment', {})) + raw_message = message_dict.get('raw_message') + + super().__init__( + message_info=message_info, + message_segment=message_segment, + raw_message=raw_message + ) + + # 处理消息内容 + self.processed_plain_text = "" # 初始化为空字符串 + self.detailed_plain_text = "" # 初始化为空字符串 + async def process(self) -> None: + """处理消息内容,生成纯文本和详细文本 + + 这个方法必须在创建实例后显式调用,因为它包含异步操作。 + """ + self.processed_plain_text = await self._process_message_segments(self.message_segment) + self.detailed_plain_text = self._generate_detailed_text() + async def _process_message_segments(self, segment: Seg) -> str: + """递归处理消息段,转换为文字描述 + + Args: + segment: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + if segment.type == 'seglist': + # 处理消息段列表 + segments_text = [] + for seg in segment.data: + processed = await self._process_message_segments(seg) + if processed: + segments_text.append(processed) + return ' '.join(segments_text) + else: + # 处理单个消息段 + return await self._process_single_segment(segment) + + async def _process_single_segment(self, seg: Seg) -> str: + """处理单个消息段 + + Args: + seg: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + try: + if seg.type == 'text': + return seg.data + elif seg.type == 'image': + # 如果是base64图片数据 + if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + return await image_manager.get_image_description(seg.data) + return '[图片]' + elif seg.type == 'emoji': + if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + return await image_manager.get_emoji_description(seg.data) + return '[表情]' + else: + return f"[{seg.type}:{str(seg.data)}]" + except Exception as e: + logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") + return f"[处理失败的{seg.type}消息]" + + def _generate_detailed_text(self) -> str: + """生成详细文本,包含时间和用户信息""" + time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time)) + user_info = self.message_info.user_info + name = ( + f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})" + if user_info.user_cardname!='' + else f"{user_info.user_nickname}(ta的id:{user_info.user_id})" + ) + return f"[{time_str}] {name}: {self.processed_plain_text}\n" @dataclass -class Message: - """消息数据类""" - message_id: int = None - time: float = None - - group_id: int = None - group_name: str = None # 群名称 - - user_id: int = None - user_nickname: str = None # 用户昵称 - user_cardname: str = None # 用户群昵称 - - raw_message: str = None # 原始消息,包含未解析的cq码 - plain_text: str = None # 纯文本 - - reply_message: Dict = None # 存储 回复的 源消息 - - # 延迟初始化字段 - _initialized: bool = False - message_segments: List[Dict] = None # 存储解析后的消息片段 - processed_plain_text: str = None # 用于存储处理后的plain_text - detailed_plain_text: str = None # 用于存储详细可读文本 - - # 状态标志 - is_emoji: bool = False - has_emoji: bool = False - translate_cq: bool = True - - async def initialize(self): - """显式异步初始化方法(必须调用)""" - if self._initialized: - return - - # 异步获取补充信息 - self.group_name = self.group_name or get_groupname(self.group_id) - self.user_nickname = self.user_nickname or get_user_nickname(self.user_id) - self.user_cardname = self.user_cardname or get_user_cardname(self.user_id) - - # 消息解析 - if self.raw_message: - if not isinstance(self,Message_Sending): - self.message_segments = await self.parse_message_segments(self.raw_message) - self.processed_plain_text = ' '.join( - seg.translated_plain_text - for seg in self.message_segments - ) - - # 构建详细文本 - if self.time is None: - self.time = int(time.time()) - time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.time)) - name = ( - f"{self.user_nickname}(ta的昵称:{self.user_cardname},ta的id:{self.user_id})" - if self.user_cardname - else f"{self.user_nickname or f'用户{self.user_id}'}" - ) - if isinstance(self,Message_Sending) and self.is_emoji: - self.detailed_plain_text = f"[{time_str}] {name}: {self.detailed_plain_text}\n" - else: - self.detailed_plain_text = f"[{time_str}] {name}: {self.processed_plain_text}\n" - - self._initialized = True +class MessageProcessBase(MessageBase): + """消息处理基类,用于处理中和发送中的消息""" - async def parse_message_segments(self, message: str) -> List[CQCode]: - """ - 将消息解析为片段列表,包括纯文本和CQ码 - 返回的列表中每个元素都是字典,包含: - - cq_code_list:分割出的聊天对象,包括文本和CQ码 - - trans_list:翻译后的对象列表 - """ - # print(f"\033[1;34m[调试信息]\033[0m 正在处理消息: {message}") - cq_code_dict_list = [] - trans_list = [] - - start = 0 - while True: - # 查找下一个CQ码的开始位置 - cq_start = message.find('[CQ:', start) - #如果没有cq码,直接返回文本内容 - if cq_start == -1: - # 如果没有找到更多CQ码,添加剩余文本 - if start < len(message): - text = message[start:].strip() - if text: # 只添加非空文本 - cq_code_dict_list.append(parse_cq_code(text)) - break - # 添加CQ码前的文本 - if cq_start > start: - text = message[start:cq_start].strip() - if text: # 只添加非空文本 - cq_code_dict_list.append(parse_cq_code(text)) - # 查找CQ码的结束位置 - cq_end = message.find(']', cq_start) - if cq_end == -1: - # CQ码未闭合,作为普通文本处理 - text = message[cq_start:].strip() - if text: - cq_code_dict_list.append(parse_cq_code(text)) - break - cq_code = message[cq_start:cq_end + 1] - - #将cq_code解析成字典 - cq_code_dict_list.append(parse_cq_code(cq_code)) - # 更新start位置到当前CQ码之后 - start = cq_end + 1 - - # print(f"\033[1;34m[调试信息]\033[0m 提取的消息对象:列表: {cq_code_dict_list}") - - #判定是否是表情包消息,以及是否含有表情包 - if len(cq_code_dict_list) == 1 and cq_code_dict_list[0]['type'] == 'image': - self.is_emoji = True - self.has_emoji_emoji = True - else: - for segment in cq_code_dict_list: - if segment['type'] == 'image' and segment['data'].get('sub_type') == '1': - self.has_emoji_emoji = True - break - - - #翻译作为字典的CQ码 - for _code_item in cq_code_dict_list: - message_obj = await cq_code_tool.cq_from_dict_to_class(_code_item,reply = self.reply_message) - trans_list.append(message_obj) - return trans_list + def __init__( + self, + message_id: str, + user_id: int, + group_id: Optional[int] = None, + platform: str = "qq", + message_segment: Optional[Seg] = None, + reply: Optional['MessageRecv'] = None + ): + # 构造用户信息 + user_info = UserInfo( + platform=platform, + user_id=user_id, + user_nickname=get_user_nickname(user_id), + user_cardname=get_user_cardname(user_id) if group_id else None + ) -class Message_Thinking: - """消息思考类""" - def __init__(self, message: Message,message_id: str): - # 复制原始消息的基本属性 - self.group_id = message.group_id - self.user_id = message.user_id - self.user_nickname = message.user_nickname - self.user_cardname = message.user_cardname - self.group_name = message.group_name - - self.message_id = message_id - - # 思考状态相关属性 + # 构造群组信息(如果有) + group_info = None + if group_id: + group_info = GroupInfo( + platform=platform, + group_id=group_id, + group_name=get_groupname(group_id) + ) + + # 构造基础消息信息 + message_info = BaseMessageInfo( + platform=platform, + message_id=message_id, + time=int(time.time()), + group_info=group_info, + user_info=user_info + ) + + # 调用父类初始化 + super().__init__( + message_info=message_info, + message_segment=message_segment, + raw_message=None + ) + + # 处理状态相关属性 self.thinking_start_time = int(time.time()) self.thinking_time = 0 - self.interupt=False - - def update_thinking_time(self): - self.thinking_time = round(time.time(), 2) - self.thinking_start_time - + + # 文本处理相关属性 + self.processed_plain_text = "" + self.detailed_plain_text = "" + + # 回复消息 + self.reply = reply -@dataclass -class Message_Sending(Message): - """发送中的消息类""" - thinking_start_time: float = None # 思考开始时间 - thinking_time: float = None # 思考时间 - - reply_message_id: int = None # 存储 回复的 源消息ID - - is_head: bool = False # 是否是头部消息 - - def update_thinking_time(self): - self.thinking_time = round(time.time(), 2) - self.thinking_start_time + def update_thinking_time(self) -> float: + """更新思考时间""" + self.thinking_time = round(time.time() - self.thinking_start_time, 2) return self.thinking_time + async def _process_message_segments(self, segment: Seg) -> str: + """递归处理消息段,转换为文字描述 + + Args: + segment: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + if segment.type == 'seglist': + # 处理消息段列表 + segments_text = [] + for seg in segment.data: + processed = await self._process_message_segments(seg) + if processed: + segments_text.append(processed) + return ' '.join(segments_text) + else: + # 处理单个消息段 + return await self._process_single_segment(segment) - + async def _process_single_segment(self, seg: Seg) -> str: + """处理单个消息段 + + Args: + seg: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + try: + if seg.type == 'text': + return seg.data + elif seg.type == 'image': + # 如果是base64图片数据 + if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + return await image_manager.get_image_description(seg.data) + return '[图片]' + elif seg.type == 'emoji': + if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + return await image_manager.get_emoji_description(seg.data) + return '[表情]' + elif seg.type == 'at': + return f"[@{seg.data}]" + elif seg.type == 'reply': + if self.reply and hasattr(self.reply, 'processed_plain_text'): + return f"[回复:{self.reply.processed_plain_text}]" + else: + return f"[{seg.type}:{str(seg.data)}]" + except Exception as e: + logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") + return f"[处理失败的{seg.type}消息]" + + def _generate_detailed_text(self) -> str: + """生成详细文本,包含时间和用户信息""" + time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time)) + user_info = self.message_info.user_info + name = ( + f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})" + if user_info.user_cardname != '' + else f"{user_info.user_nickname}(ta的id:{user_info.user_id})" + ) + return f"[{time_str}] {name}: {self.processed_plain_text}\n" + +@dataclass +class MessageThinking(MessageProcessBase): + """思考状态的消息类""" + + def __init__( + self, + message_id: str, + user_id: int, + group_id: Optional[int] = None, + platform: str = "qq", + reply: Optional['MessageRecv'] = None + ): + # 调用父类初始化 + super().__init__( + message_id=message_id, + user_id=user_id, + group_id=group_id, + platform=platform, + message_segment=None, # 思考状态不需要消息段 + reply=reply + ) + + # 思考状态特有属性 + self.interrupt = False + +@dataclass +class MessageSending(MessageProcessBase): + """发送状态的消息类""" + + def __init__( + self, + message_id: str, + user_id: int, + message_segment: Seg, + group_id: Optional[int] = None, + reply: Optional['MessageRecv'] = None, + platform: str = "qq", + is_head: bool = False + ): + # 调用父类初始化 + super().__init__( + message_id=message_id, + user_id=user_id, + group_id=group_id, + platform=platform, + message_segment=message_segment, + reply=reply + ) + + # 发送状态特有属性 + self.reply_to_message_id = reply.message_info.message_id if reply else None + self.is_head = is_head + + async def process(self) -> None: + """处理消息内容,生成纯文本和详细文本""" + if self.message_segment: + self.processed_plain_text = await self._process_message_segments(self.message_segment) + self.detailed_plain_text = self._generate_detailed_text() + + @classmethod + def from_thinking( + cls, + thinking: MessageThinking, + message_segment: Seg, + reply: Optional['MessageRecv'] = None, + is_head: bool = False + ) -> 'MessageSending': + """从思考状态消息创建发送状态消息""" + return cls( + message_id=thinking.message_info.message_id, + user_id=thinking.message_info.user_info.user_id, + message_segment=message_segment, + group_id=thinking.message_info.group_info.group_id if thinking.message_info.group_info else None, + reply=reply or thinking.reply, + platform=thinking.message_info.platform, + is_head=is_head + ) + +@dataclass class MessageSet: """消息集合类,可以存储多个发送消息""" def __init__(self, group_id: int, user_id: int, message_id: str): self.group_id = group_id self.user_id = user_id self.message_id = message_id - self.messages: List[Message_Sending] = [] # 修改类型标注 + self.messages: List[MessageSending] = [] self.time = round(time.time(), 2) - def add_message(self, message: Message_Sending) -> None: - """添加消息到集合,只接受Message_Sending类型""" - if not isinstance(message, Message_Sending): - raise TypeError("MessageSet只能添加Message_Sending类型的消息") + def add_message(self, message: MessageSending) -> None: + """添加消息到集合""" + if not isinstance(message, MessageSending): + raise TypeError("MessageSet只能添加MessageSending类型的消息") self.messages.append(message) - # 按时间排序 - self.messages.sort(key=lambda x: x.time) + self.messages.sort(key=lambda x: x.message_info.time) - def get_message_by_index(self, index: int) -> Optional[Message_Sending]: + def get_message_by_index(self, index: int) -> Optional[MessageSending]: """通过索引获取消息""" if 0 <= index < len(self.messages): return self.messages[index] return None - def get_message_by_time(self, target_time: float) -> Optional[Message_Sending]: + def get_message_by_time(self, target_time: float) -> Optional[MessageSending]: """获取最接近指定时间的消息""" if not self.messages: return None - # 使用二分查找找到最接近的消息 left, right = 0, len(self.messages) - 1 while left < right: mid = (left + right) // 2 - if self.messages[mid].time < target_time: + if self.messages[mid].message_info.time < target_time: left = mid + 1 else: right = mid return self.messages[left] - def clear_messages(self) -> None: """清空所有消息""" self.messages.clear() - def remove_message(self, message: Message_Sending) -> bool: + def remove_message(self, message: MessageSending) -> bool: """移除指定消息""" if message in self.messages: self.messages.remove(message) diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py new file mode 100644 index 000000000..77694ad8c --- /dev/null +++ b/src/plugins/chat/message_base.py @@ -0,0 +1,158 @@ +from dataclasses import dataclass, asdict +from typing import List, Optional, Union, Any, Dict + +@dataclass +class Seg(dict): + """消息片段类,用于表示消息的不同部分 + + Attributes: + type: 片段类型,可以是 'text'、'image'、'seglist' 等 + data: 片段的具体内容 + - 对于 text 类型,data 是字符串 + - 对于 image 类型,data 是 base64 字符串 + - 对于 seglist 类型,data 是 Seg 列表 + translated_data: 经过翻译处理的数据(可选) + """ + type: str + data: Union[str, List['Seg']] + translated_data: Optional[str] = None + + def __init__(self, type: str, data: Union[str, List['Seg']], translated_data: Optional[str] = None): + """初始化实例,确保字典和属性同步""" + # 先初始化字典 + super().__init__(type=type, data=data) + if translated_data is not None: + self['translated_data'] = translated_data + + # 再初始化属性 + object.__setattr__(self, 'type', type) + object.__setattr__(self, 'data', data) + object.__setattr__(self, 'translated_data', translated_data) + + # 验证数据类型 + self._validate_data() + + def _validate_data(self) -> None: + """验证数据类型的正确性""" + if self.type == 'seglist' and not isinstance(self.data, list): + raise ValueError("seglist类型的data必须是列表") + elif self.type == 'text' and not isinstance(self.data, str): + raise ValueError("text类型的data必须是字符串") + elif self.type == 'image' and not isinstance(self.data, str): + raise ValueError("image类型的data必须是字符串") + + def __setattr__(self, name: str, value: Any) -> None: + """重写属性设置,同时更新字典值""" + # 更新属性 + object.__setattr__(self, name, value) + # 同步更新字典 + if name in ['type', 'data', 'translated_data']: + self[name] = value + + def __setitem__(self, key: str, value: Any) -> None: + """重写字典值设置,同时更新属性""" + # 更新字典 + super().__setitem__(key, value) + # 同步更新属性 + if key in ['type', 'data', 'translated_data']: + object.__setattr__(self, key, value) + + def to_dict(self) -> Dict: + """转换为字典格式""" + result = {'type': self.type} + if self.type == 'seglist': + result['data'] = [seg.to_dict() for seg in self.data] + else: + result['data'] = self.data + if self.translated_data is not None: + result['translated_data'] = self.translated_data + return result + +@dataclass +class GroupInfo: + """群组信息类""" + platform: Optional[str] = None + group_id: Optional[int] = None + group_name: Optional[str] = None # 群名称 + + def to_dict(self) -> Dict: + """转换为字典格式""" + return {k: v for k, v in asdict(self).items() if v is not None} + +@dataclass +class UserInfo: + """用户信息类""" + platform: Optional[str] = None + user_id: Optional[int] = None + user_nickname: Optional[str] = None # 用户昵称 + user_cardname: Optional[str] = None # 用户群昵称 + + def to_dict(self) -> Dict: + """转换为字典格式""" + return {k: v for k, v in asdict(self).items() if v is not None} + +@dataclass +class BaseMessageInfo: + """消息信息类""" + platform: Optional[str] = None + message_id: Optional[int,str] = None + time: Optional[int] = None + group_info: Optional[GroupInfo] = None + user_info: Optional[UserInfo] = None + + def to_dict(self) -> Dict: + """转换为字典格式""" + result = {} + for field, value in asdict(self).items(): + if value is not None: + if isinstance(value, (GroupInfo, UserInfo)): + result[field] = value.to_dict() + else: + result[field] = value + return result + +@dataclass +class MessageBase: + """消息类""" + message_info: BaseMessageInfo + message_segment: Seg + raw_message: Optional[str] = None # 原始消息,包含未解析的cq码 + + def to_dict(self) -> Dict: + """转换为字典格式 + + Returns: + Dict: 包含所有非None字段的字典,其中: + - message_info: 转换为字典格式 + - message_segment: 转换为字典格式 + - raw_message: 如果存在则包含 + """ + result = { + 'message_info': self.message_info.to_dict(), + 'message_segment': self.message_segment.to_dict() + } + if self.raw_message is not None: + result['raw_message'] = self.raw_message + return result + + @classmethod + def from_dict(cls, data: Dict) -> 'MessageBase': + """从字典创建MessageBase实例 + + Args: + data: 包含必要字段的字典 + + Returns: + MessageBase: 新的实例 + """ + message_info = BaseMessageInfo(**data.get('message_info', {})) + message_segment = Seg(**data.get('message_segment', {})) + raw_message = data.get('raw_message') + return cls( + message_info=message_info, + message_segment=message_segment, + raw_message=raw_message + ) + + + diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py new file mode 100644 index 000000000..80df4e340 --- /dev/null +++ b/src/plugins/chat/message_cq.py @@ -0,0 +1,188 @@ +import time +from dataclasses import dataclass +from typing import Dict, ForwardRef, List, Optional, Union + +import urllib3 + +from .cq_code import CQCode, cq_code_tool +from .utils_cq import parse_cq_code +from .utils_user import get_groupname, get_user_cardname, get_user_nickname +from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase +# 禁用SSL警告 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +#这个类是消息数据类,用于存储和管理消息数据。 +#它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 +#它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 + +@dataclass +class MessageCQ(MessageBase): + """QQ消息基类,继承自MessageBase + + 最小必要参数: + - message_id: 消息ID + - user_id: 发送者/接收者ID + - platform: 平台标识(默认为"qq") + """ + def __init__( + self, + message_id: int, + user_id: int, + group_id: Optional[int] = None, + platform: str = "qq" + ): + # 构造用户信息 + user_info = UserInfo( + platform=platform, + user_id=user_id, + user_nickname=get_user_nickname(user_id), + user_cardname=get_user_cardname(user_id) if group_id else None + ) + + # 构造群组信息(如果有) + group_info = None + if group_id: + group_info = GroupInfo( + platform=platform, + group_id=group_id, + group_name=get_groupname(group_id) + ) + + # 构造基础消息信息 + message_info = BaseMessageInfo( + platform=platform, + message_id=message_id, + time=int(time.time()), + group_info=group_info, + user_info=user_info + ) + + # 调用父类初始化,message_segment 由子类设置 + super().__init__( + message_info=message_info, + message_segment=None, + raw_message=None + ) + +@dataclass +class MessageRecvCQ(MessageCQ): + """QQ接收消息类,用于解析raw_message到Seg对象""" + + def __init__( + self, + message_id: int, + user_id: int, + raw_message: str, + group_id: Optional[int] = None, + reply_message: Optional[Dict] = None, + platform: str = "qq" + ): + # 调用父类初始化 + super().__init__(message_id, user_id, group_id, platform) + + # 解析消息段 + self.message_segment = self._parse_message(raw_message, reply_message) + self.raw_message = raw_message + + def _parse_message(self, message: str, reply_message: Optional[Dict] = None) -> Seg: + """解析消息内容为Seg对象""" + cq_code_dict_list = [] + segments = [] + + start = 0 + while True: + cq_start = message.find('[CQ:', start) + if cq_start == -1: + if start < len(message): + text = message[start:].strip() + if text: + cq_code_dict_list.append(parse_cq_code(text)) + break + + if cq_start > start: + text = message[start:cq_start].strip() + if text: + cq_code_dict_list.append(parse_cq_code(text)) + + cq_end = message.find(']', cq_start) + if cq_end == -1: + text = message[cq_start:].strip() + if text: + cq_code_dict_list.append(parse_cq_code(text)) + break + + cq_code = message[cq_start:cq_end + 1] + cq_code_dict_list.append(parse_cq_code(cq_code)) + start = cq_end + 1 + + # 转换CQ码为Seg对象 + for code_item in cq_code_dict_list: + message_obj = cq_code_tool.cq_from_dict_to_class(code_item, reply=reply_message) + if message_obj.translated_segments: + segments.append(message_obj.translated_segments) + + # 如果只有一个segment,直接返回 + if len(segments) == 1: + return segments[0] + + # 否则返回seglist类型的Seg + return Seg(type='seglist', data=segments) + + def to_dict(self) -> Dict: + """转换为字典格式,包含所有必要信息""" + base_dict = super().to_dict() + return base_dict + +@dataclass +class MessageSendCQ(MessageCQ): + """QQ发送消息类,用于将Seg对象转换为raw_message""" + + def __init__( + self, + message_id: int, + user_id: int, + message_segment: Seg, + group_id: Optional[int] = None, + reply_to_message_id: Optional[int] = None, + platform: str = "qq" + ): + # 调用父类初始化 + super().__init__(message_id, user_id, group_id, platform) + + self.message_segment = message_segment + self.raw_message = self._generate_raw_message(reply_to_message_id) + + def _generate_raw_message(self, reply_to_message_id: Optional[int] = None) -> str: + """将Seg对象转换为raw_message""" + segments = [] + + # 添加回复消息 + if reply_to_message_id: + segments.append(cq_code_tool.create_reply_cq(reply_to_message_id)) + + # 处理消息段 + if self.message_segment.type == 'seglist': + for seg in self.message_segment.data: + segments.append(self._seg_to_cq_code(seg)) + else: + segments.append(self._seg_to_cq_code(self.message_segment)) + + return ''.join(segments) + + def _seg_to_cq_code(self, seg: Seg) -> str: + """将单个Seg对象转换为CQ码字符串""" + if seg.type == 'text': + return str(seg.data) + elif seg.type == 'image': + # 如果是base64图片数据 + if seg.data.startswith(('data:', 'base64:')): + return f"[CQ:image,file=base64://{seg.data}]" + # 如果是表情包(本地文件) + return cq_code_tool.create_emoji_cq(seg.data) + elif seg.type == 'at': + return f"[CQ:at,qq={seg.data}]" + elif seg.type == 'reply': + return cq_code_tool.create_reply_cq(int(seg.data)) + else: + return f"[{seg.data}]" + diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 050c59d74..8ed30d69c 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union from nonebot.adapters.onebot.v11 import Bot from .cq_code import cq_code_tool -from .message import Message, Message_Sending, Message_Thinking, MessageSet +from .message_cq import Message, Message_Sending, Message_Thinking, MessageSet from .storage import MessageStorage from .utils import calculate_typing_time from .config import global_config diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 6a87480b7..4b1bf0ca4 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -1,7 +1,7 @@ from typing import Optional from ...common.database import Database -from .message import Message +from .message_cq import Message class MessageStorage: diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index b2583e86f..07c3e2a6f 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -11,31 +11,12 @@ from nonebot import get_driver from ..models.utils_model import LLM_request from ..utils.typo_generator import ChineseTypoGenerator from .config import global_config -from .message import Message +from .message_cq import Message driver = get_driver() config = driver.config -def combine_messages(messages: List[Message]) -> str: - """将消息列表组合成格式化的字符串 - - Args: - messages: Message对象列表 - - Returns: - str: 格式化后的消息字符串 - """ - result = "" - for message in messages: - time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message.time)) - name = message.user_nickname or f"用户{message.user_id}" - content = message.processed_plain_text or message.plain_text - - result += f"[{time_str}] {name}: {content}\n" - - return result - def db_message_to_str(message_dict: Dict) -> str: print(f"message_dict: {message_dict}") @@ -159,7 +140,7 @@ async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: return [] # 转换为 Message对象列表 - from .message import Message + from .message_cq import Message message_objects = [] for msg_data in recent_messages: try: diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 8a8b3ce5a..aba09714c 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -2,7 +2,11 @@ import base64 import io import os import time -import zlib # 用于 CRC32 +import zlib +import aiohttp +import hashlib +from typing import Optional, Tuple, Union +from urllib.parse import urlparse from loguru import logger from nonebot import get_driver @@ -10,213 +14,348 @@ from PIL import Image from ...common.database import Database from ..chat.config import global_config - +from ..models.utils_model import LLM_request driver = get_driver() config = driver.config - - -def storage_compress_image(base64_data: str, max_size: int = 200) -> str: - """ - 压缩base64格式的图片到指定大小(单位:KB)并在数据库中记录图片信息 - Args: - base64_data: base64编码的图片数据 - max_size: 最大文件大小(KB) - Returns: - str: 压缩后的base64图片数据 - """ - try: - # 将base64转换为字节数据 - image_data = base64.b64decode(base64_data) - - # 使用 CRC32 计算哈希值 - hash_value = format(zlib.crc32(image_data) & 0xFFFFFFFF, 'x') - - # 确保图片目录存在 - images_dir = "data/images" - os.makedirs(images_dir, exist_ok=True) - - # 连接数据库 - db = Database( - host=config.mongodb_host, - port=int(config.mongodb_port), - db_name=config.database_name, - username=config.mongodb_username, - password=config.mongodb_password, - auth_source=config.mongodb_auth_source - ) - - # 检查是否已存在相同哈希值的图片 - collection = db.db['images'] - existing_image = collection.find_one({'hash': hash_value}) - - if existing_image: - print(f"\033[1;33m[提示]\033[0m 发现重复图片,使用已存在的文件: {existing_image['path']}") - return base64_data - - # 将字节数据转换为图片对象 - img = Image.open(io.BytesIO(image_data)) - - # 如果是动图,直接返回原图 - if getattr(img, 'is_animated', False): - return base64_data - - # 计算当前大小(KB) - current_size = len(image_data) / 1024 - - # 如果已经小于目标大小,直接使用原图 - if current_size <= max_size: - compressed_data = image_data - else: - # 压缩逻辑 - # 先缩放到50% - new_width = int(img.width * 0.5) - new_height = int(img.height * 0.5) - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # 如果缩放后的最大边长仍然大于400,继续缩放 - max_dimension = 400 - max_current = max(new_width, new_height) - if max_current > max_dimension: - ratio = max_dimension / max_current - new_width = int(new_width * ratio) - new_height = int(new_height * ratio) - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # 转换为RGB模式(去除透明通道) - if img.mode in ('RGBA', 'P'): - img = img.convert('RGB') - - # 使用固定质量参数压缩 - output = io.BytesIO() - img.save(output, format='JPEG', quality=85, optimize=True) - compressed_data = output.getvalue() - - # 生成文件名(使用时间戳和哈希值确保唯一性) - timestamp = int(time.time()) - filename = f"{timestamp}_{hash_value}.jpg" - image_path = os.path.join(images_dir, filename) - - # 保存文件 - with open(image_path, "wb") as f: - f.write(compressed_data) - - print(f"\033[1;32m[成功]\033[0m 保存图片到: {image_path}") - - try: - # 准备数据库记录 - image_record = { - 'filename': filename, - 'path': image_path, - 'size': len(compressed_data) / 1024, - 'timestamp': timestamp, - 'width': img.width, - 'height': img.height, - 'description': '', - 'tags': [], - 'type': 'image', - 'hash': hash_value - } - - # 保存记录 - collection.insert_one(image_record) - print("\033[1;32m[成功]\033[0m 保存图片记录到数据库") - - except Exception as db_error: - print(f"\033[1;31m[错误]\033[0m 数据库操作失败: {str(db_error)}") - - # 将压缩后的数据转换为base64 - compressed_base64 = base64.b64encode(compressed_data).decode('utf-8') - return compressed_base64 - - except Exception as e: - print(f"\033[1;31m[错误]\033[0m 压缩图片失败: {str(e)}") - import traceback - print(traceback.format_exc()) - return base64_data - -def storage_emoji(image_data: bytes) -> bytes: - """ - 存储表情包到本地文件夹 - Args: - image_data: 图片字节数据 - group_id: 群组ID(仅用于日志) - user_id: 用户ID(仅用于日志) - Returns: - bytes: 原始图片数据 - """ - if not global_config.EMOJI_SAVE: - return image_data - try: - # 使用 CRC32 计算哈希值 - hash_value = format(zlib.crc32(image_data) & 0xFFFFFFFF, 'x') - - # 确保表情包目录存在 - emoji_dir = "data/emoji" - os.makedirs(emoji_dir, exist_ok=True) - - # 检查是否已存在相同哈希值的文件 - for filename in os.listdir(emoji_dir): - if hash_value in filename: - # print(f"\033[1;33m[提示]\033[0m 发现重复表情包: {filename}") - return image_data - - # 生成文件名 - timestamp = int(time.time()) - filename = f"{timestamp}_{hash_value}.jpg" - emoji_path = os.path.join(emoji_dir, filename) - - # 直接保存原始文件 - with open(emoji_path, "wb") as f: - f.write(image_data) - - print(f"\033[1;32m[成功]\033[0m 保存表情包到: {emoji_path}") - return image_data - - except Exception as e: - print(f"\033[1;31m[错误]\033[0m 保存表情包失败: {str(e)}") - return image_data +class ImageManager: + _instance = None + IMAGE_DIR = "data" # 图像存储根目录 - -def storage_image(image_data: bytes) -> bytes: - """ - 存储图片到本地文件夹 - Args: - image_data: 图片字节数据 - group_id: 群组ID(仅用于日志) - user_id: 用户ID(仅用于日志) - Returns: - bytes: 原始图片数据 - """ - try: - # 使用 CRC32 计算哈希值 - hash_value = format(zlib.crc32(image_data) & 0xFFFFFFFF, 'x') - - # 确保表情包目录存在 - image_dir = "data/image" - os.makedirs(image_dir, exist_ok=True) - - # 检查是否已存在相同哈希值的文件 - for filename in os.listdir(image_dir): - if hash_value in filename: - # print(f"\033[1;33m[提示]\033[0m 发现重复表情包: {filename}") - return image_data - - # 生成文件名 - timestamp = int(time.time()) - filename = f"{timestamp}_{hash_value}.jpg" - image_path = os.path.join(image_dir, filename) - - # 直接保存原始文件 - with open(image_path, "wb") as f: - f.write(image_data) + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.db = None + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if not self._initialized: + self.db = Database.get_instance() + self._ensure_image_collection() + self._ensure_description_collection() + self._ensure_image_dir() + self._initialized = True + self._llm = LLM_request(model=global_config.vlm, temperature=0.4, max_tokens=300) - print(f"\033[1;32m[成功]\033[0m 保存图片到: {image_path}") - return image_data + def _ensure_image_dir(self): + """确保图像存储目录存在""" + os.makedirs(self.IMAGE_DIR, exist_ok=True) - except Exception as e: - print(f"\033[1;31m[错误]\033[0m 保存图片失败: {str(e)}") - return image_data + def _ensure_image_collection(self): + """确保images集合存在并创建索引""" + if 'images' not in self.db.db.list_collection_names(): + self.db.db.create_collection('images') + # 创建索引 + self.db.db.images.create_index([('hash', 1)], unique=True) + self.db.db.images.create_index([('url', 1)]) + self.db.db.images.create_index([('path', 1)]) + + def _ensure_description_collection(self): + """确保image_descriptions集合存在并创建索引""" + if 'image_descriptions' not in self.db.db.list_collection_names(): + self.db.db.create_collection('image_descriptions') + # 创建索引 + self.db.db.image_descriptions.create_index([('hash', 1)], unique=True) + self.db.db.image_descriptions.create_index([('type', 1)]) + + async def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]: + """从数据库获取图片描述 + + Args: + image_hash: 图片哈希值 + description_type: 描述类型 ('emoji' 或 'image') + + Returns: + Optional[str]: 描述文本,如果不存在则返回None + """ + result = await self.db.db.image_descriptions.find_one({ + 'hash': image_hash, + 'type': description_type + }) + return result['description'] if result else None + + async def _save_description_to_db(self, image_hash: str, description: str, description_type: str) -> None: + """保存图片描述到数据库 + + Args: + image_hash: 图片哈希值 + description: 描述文本 + description_type: 描述类型 ('emoji' 或 'image') + """ + await self.db.db.image_descriptions.update_one( + {'hash': image_hash, 'type': description_type}, + { + '$set': { + 'description': description, + 'timestamp': int(time.time()) + } + }, + upsert=True + ) + + async def save_image(self, + image_data: Union[str, bytes], + url: str = None, + description: str = None, + is_base64: bool = False) -> Optional[str]: + """保存图像 + Args: + image_data: 图像数据(base64字符串或字节) + url: 图像URL + description: 图像描述 + is_base64: image_data是否为base64格式 + Returns: + str: 保存后的文件路径,失败返回None + """ + try: + # 转换为字节格式 + if is_base64: + if isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + return None + else: + if isinstance(image_data, bytes): + image_bytes = image_data + else: + return None + + # 计算哈希值 + image_hash = hashlib.md5(image_bytes).hexdigest() + + # 查重 + existing = self.db.db.images.find_one({'hash': image_hash}) + if existing: + return existing['path'] + + # 生成文件名和路径 + timestamp = int(time.time()) + filename = f"{timestamp}_{image_hash[:8]}.jpg" + file_path = os.path.join(self.IMAGE_DIR, filename) + + # 保存文件 + with open(file_path, "wb") as f: + f.write(image_bytes) + + # 保存到数据库 + image_doc = { + 'hash': image_hash, + 'path': file_path, + 'url': url, + 'description': description, + 'timestamp': timestamp + } + self.db.db.images.insert_one(image_doc) + + return file_path + + except Exception as e: + logger.error(f"保存图像失败: {str(e)}") + return None + + async def get_image_by_url(self, url: str) -> Optional[str]: + """根据URL获取图像路径(带查重) + Args: + url: 图像URL + Returns: + str: 本地文件路径,不存在返回None + """ + try: + # 先查找是否已存在 + existing = self.db.db.images.find_one({'url': url}) + if existing: + return existing['path'] + + # 下载图像 + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status == 200: + image_bytes = await resp.read() + return await self.save_image(image_bytes, url=url) + return None + + except Exception as e: + logger.error(f"获取图像失败: {str(e)}") + return None + + async def get_base64_by_url(self, url: str) -> Optional[str]: + """根据URL获取base64(带查重) + Args: + url: 图像URL + Returns: + str: base64字符串,失败返回None + """ + try: + image_path = await self.get_image_by_url(url) + if not image_path: + return None + + with open(image_path, 'rb') as f: + image_bytes = f.read() + return base64.b64encode(image_bytes).decode('utf-8') + + except Exception as e: + logger.error(f"获取base64失败: {str(e)}") + return None + + async def save_base64_image(self, base64_str: str, description: str = None) -> Optional[str]: + """保存base64图像(带查重) + Args: + base64_str: base64字符串 + description: 图像描述 + Returns: + str: 保存路径,失败返回None + """ + return await self.save_image(base64_str, description=description, is_base64=True) + + def check_url_exists(self, url: str) -> bool: + """检查URL是否已存在 + Args: + url: 图像URL + Returns: + bool: 是否存在 + """ + return self.db.db.images.find_one({'url': url}) is not None + + def check_hash_exists(self, image_data: Union[str, bytes], is_base64: bool = False) -> bool: + """检查图像是否已存在 + Args: + image_data: 图像数据(base64或字节) + is_base64: 是否为base64格式 + Returns: + bool: 是否存在 + """ + try: + if is_base64: + if isinstance(image_data, str): + image_bytes = base64.b64decode(image_data) + else: + return False + else: + if isinstance(image_data, bytes): + image_bytes = image_data + else: + return False + + image_hash = hashlib.md5(image_bytes).hexdigest() + return self.db.db.images.find_one({'hash': image_hash}) is not None + + except Exception as e: + logger.error(f"检查哈希失败: {str(e)}") + return False + + async def get_emoji_description(self, image_base64: str) -> str: + """获取表情包描述,带查重和保存功能""" + try: + # 计算图片哈希 + image_bytes = base64.b64decode(image_base64) + image_hash = hashlib.md5(image_bytes).hexdigest() + + # 查询缓存的描述 + cached_description = await self._get_description_from_db(image_hash, 'emoji') + if cached_description: + return f"[表情包:{cached_description}]" + + # 调用AI获取描述 + prompt = "这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感" + description, _ = await self._llm.generate_response_for_image(prompt, image_base64) + + # 根据配置决定是否保存图片 + if global_config.EMOJI_SAVE: + # 生成文件名和路径 + timestamp = int(time.time()) + filename = f"emoji_{timestamp}_{image_hash[:8]}.jpg" + file_path = os.path.join(self.IMAGE_DIR, filename) + + try: + # 保存文件 + with open(file_path, "wb") as f: + f.write(image_bytes) + + # 保存到数据库 + image_doc = { + 'hash': image_hash, + 'path': file_path, + 'type': 'emoji', + 'description': description, + 'timestamp': timestamp + } + await self.db.db.images.update_one( + {'hash': image_hash}, + {'$set': image_doc}, + upsert=True + ) + logger.success(f"保存表情包: {file_path}") + except Exception as e: + logger.error(f"保存表情包文件失败: {str(e)}") + + # 保存描述到数据库 + await self._save_description_to_db(image_hash, description, 'emoji') + + return f"[表情包:{description}]" + except Exception as e: + logger.error(f"获取表情包描述失败: {str(e)}") + return "[表情包]" + + async def get_image_description(self, image_base64: str) -> str: + """获取普通图片描述,带查重和保存功能""" + try: + # 计算图片哈希 + image_bytes = base64.b64decode(image_base64) + image_hash = hashlib.md5(image_bytes).hexdigest() + + # 查询缓存的描述 + cached_description = await self._get_description_from_db(image_hash, 'image') + if cached_description: + return f"[图片:{cached_description}]" + + # 调用AI获取描述 + prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" + description, _ = await self._llm.generate_response_for_image(prompt, image_base64) + + # 根据配置决定是否保存图片 + if global_config.EMOJI_SAVE: + # 生成文件名和路径 + timestamp = int(time.time()) + filename = f"image_{timestamp}_{image_hash[:8]}.jpg" + file_path = os.path.join(self.IMAGE_DIR, filename) + + try: + # 保存文件 + with open(file_path, "wb") as f: + f.write(image_bytes) + + # 保存到数据库 + image_doc = { + 'hash': image_hash, + 'path': file_path, + 'type': 'image', + 'description': description, + 'timestamp': timestamp + } + await self.db.db.images.update_one( + {'hash': image_hash}, + {'$set': image_doc}, + upsert=True + ) + logger.success(f"保存图片: {file_path}") + except Exception as e: + logger.error(f"保存图片文件失败: {str(e)}") + + # 保存描述到数据库 + await self._save_description_to_db(image_hash, description, 'image') + + return f"[图片:{description}]" + except Exception as e: + logger.error(f"获取图片描述失败: {str(e)}") + return "[图片]" + + + +# 创建全局单例 +image_manager = ImageManager() + def compress_base64_image_by_scale(base64_data: str, target_size: int = 0.8 * 1024 * 1024) -> str: """压缩base64格式的图片到指定大小 From 826daa518e600b0a34eb453eab94b4feaf74567b Mon Sep 17 00:00:00 2001 From: jiajiu123 <1771663559@qq.com> Date: Sun, 9 Mar 2025 20:02:59 +0800 Subject: [PATCH 002/105] =?UTF-8?q?fix:=20=E5=BD=93=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=AD=98=E5=9C=A8=E6=97=B6=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.bat | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/run.bat b/run.bat index c0fd81324..659a7545a 100644 --- a/run.bat +++ b/run.bat @@ -1,6 +1,10 @@ @ECHO OFF chcp 65001 -python -m venv venv -call venv\Scripts\activate.bat -pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt +if not exist "venv" ( + python -m venv venv + call venv\Scripts\activate.bat + pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt + ) else ( + call venv\Scripts\activate.bat +) python run.py \ No newline at end of file From f327f45aa389ef5c64de3cee834368dca9235418 Mon Sep 17 00:00:00 2001 From: AL76 Date: Sun, 9 Mar 2025 20:29:33 +0800 Subject: [PATCH 003/105] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96src/plugins/ch?= =?UTF-8?q?at/=5F=5Finit=5F=5F.py=E7=9A=84import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 0bffaed19..4411b3ce3 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -1,6 +1,4 @@ import asyncio -import os -import random import time from loguru import logger From 0de9ebac6a1dc9c5dd9df2ea0f9378cbfb67e0ae Mon Sep 17 00:00:00 2001 From: Rikki Date: Sun, 9 Mar 2025 20:41:50 +0800 Subject: [PATCH 004/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=9B=B4=E6=96=B0=E8=B4=A1=E7=8C=AE=E8=80=85=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c915a9f79..575dc8232 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包 感谢各位大佬! - + From 411ff1a65f571ff39c2ce276fc7250848aef17d3 Mon Sep 17 00:00:00 2001 From: jiajiu123 <1771663559@qq.com> Date: Sun, 9 Mar 2025 20:58:07 +0800 Subject: [PATCH 005/105] =?UTF-8?q?feat:=20=E5=AE=89=E8=A3=85=20MongoDB=20?= =?UTF-8?q?Compass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/run.py b/run.py index 549d15e9c..46b53318a 100644 --- a/run.py +++ b/run.py @@ -37,7 +37,7 @@ def extract_files(zip_path, target_dir): f.write(zip_ref.read(file)) -def run_cmd(command: str, open_new_window: bool = False): +def run_cmd(command: str, open_new_window: bool = True): """ 运行 cmd 命令 @@ -61,10 +61,9 @@ def run_cmd(command: str, open_new_window: bool = False): def run_maimbot(): run_cmd(r"napcat\NapCatWinBootMain.exe 10001", False) run_cmd( - r"mongodb\bin\mongod.exe --dbpath=" + os.getcwd() + r"\mongodb\db --port 27017", - True, + r"mongodb\bin\mongod.exe --dbpath=" + os.getcwd() + r"\mongodb\db --port 27017" ) - run_cmd("nb run", True) + run_cmd("nb run") def install_mongodb(): @@ -90,10 +89,23 @@ def install_mongodb(): extract_files("mongodb.zip", "mongodb") print("MongoDB 下载完成") os.remove("mongodb.zip") + choice = input( + "是否安装 MongoDB Compass?此软件可以以可视化的方式修改数据库,建议安装(Y/n)" + ).upper() + if choice == "Y" or choice == "": + install_mongodb_compass() + + +def install_mongodb_compass(): + run_cmd( + r"powershell Start-Process powershell -Verb runAs 'Set-ExecutionPolicy RemoteSigned'") + input("请在弹出的用户账户控制中点击“是”后按任意键继续安装") + run_cmd(r"powershell mongodb\bin\Install-Compass.ps1") + input("Compass 安装完成后请按任意键继续后续安装") def install_napcat(): - run_cmd("start https://github.com/NapNeko/NapCatQQ/releases", True) + run_cmd("start https://github.com/NapNeko/NapCatQQ/releases") print("请检查弹出的浏览器窗口,点击**第一个**蓝色的“Win64无头” 下载 napcat") napcat_filename = input( "下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell:" @@ -124,4 +136,4 @@ if __name__ == "__main__": run_maimbot() elif choice == "3": run_maimbot() - run_cmd("python src/gui/reasoning_gui.py", True) + run_cmd("python src/gui/reasoning_gui.py") \ No newline at end of file From 6a669334520e7fc70c753b37187fef990d976525 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:05:35 +0800 Subject: [PATCH 006/105] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=8E=AF=E5=A2=83.env.dev=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index d43d5e607..4a463f5db 100644 --- a/bot.py +++ b/bot.py @@ -1,5 +1,7 @@ import os import shutil +import sys + import nonebot import time from dotenv import load_dotenv @@ -22,11 +24,12 @@ def easter_egg(): rainbow_text += rainbow_colors[i % len(rainbow_colors)] + char print(rainbow_text) + def init_config(): # 初次启动检测 if not os.path.exists("config/bot_config.toml"): logger.warning("检测到bot_config.toml不存在,正在从模板复制") - + # 检查config目录是否存在 if not os.path.exists("config"): os.makedirs("config") @@ -35,6 +38,7 @@ def init_config(): shutil.copy("template/bot_config_template.toml", "config/bot_config.toml") logger.info("复制完成,请修改config/bot_config.toml和.env.prod中的配置后重新启动") + def init_env(): # 初始化.env 默认ENVIRONMENT=prod if not os.path.exists(".env"): @@ -46,11 +50,17 @@ def init_env(): logger.error("检测到.env.prod文件不存在") shutil.copy("template.env", "./.env.prod") + # 检测.env.dev文件是否存在,不存在的话直接复制生产环境配置 + if not os.path.exists(".env.dev"): + logger.error("检测到.env.dev文件不存在") + shutil.copy(".env.prod", "./.env.dev") + # 首先加载基础环境变量.env if os.path.exists(".env"): load_dotenv(".env") logger.success("成功加载基础环境变量配置") + def load_env(): # 使用闭包实现对加载器的横向扩展,避免大量重复判断 def prod(): @@ -70,7 +80,7 @@ def load_env(): logger.info(f"[load_env] 当前的 ENVIRONMENT 变量值:{env}") if env in fn_map: - fn_map[env]() # 根据映射执行闭包函数 + fn_map[env]() # 根据映射执行闭包函数 elif os.path.exists(f".env.{env}"): logger.success(f"加载{env}环境变量配置") @@ -115,6 +125,7 @@ def scan_provider(env_config: dict): ) raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量") + if __name__ == "__main__": # 利用 TZ 环境变量设定程序工作的时区 # 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 From 5566f178d0f090158c18b7888e9bf7fff5ef00e1 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Sun, 9 Mar 2025 22:12:10 +0800 Subject: [PATCH 007/105] =?UTF-8?q?refractor:=20=E5=87=A0=E4=B9=8E?= =?UTF-8?q?=E5=86=99=E5=AE=8C=E4=BA=86=EF=BC=8C=E8=BF=9B=E5=85=A5=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=98=B6=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 162 +++++++++-------- src/plugins/chat/chat_stream.py | 209 ++++++++++++++++++++++ src/plugins/chat/cq_code.py | 18 ++ src/plugins/chat/emoji_manager.py | 4 +- src/plugins/chat/message.py | 82 ++++----- src/plugins/chat/message_cq.py | 2 +- src/plugins/chat/message_sender.py | 167 +++++++++--------- src/plugins/chat/relationship_manager.py | 210 ++++++++++++++++++----- src/plugins/chat/storage.py | 43 ++--- src/plugins/chat/willing_manager.py | 84 +++++---- 10 files changed, 662 insertions(+), 319 deletions(-) create mode 100644 src/plugins/chat/chat_stream.py diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c9eea3ed9..b5096b7a9 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -10,18 +10,19 @@ from .config import global_config from .cq_code import CQCode,cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator +from .message import MessageSending, MessageRecv, MessageThinking, MessageSet from .message_cq import ( - Message, - Message_Sending, - Message_Thinking, # 导入 Message_Thinking 类 - MessageSet, + MessageRecvCQ, + MessageSendCQ, ) +from .chat_stream import chat_manager from .message_sender import message_manager # 导入新的消息管理器 from .relationship_manager import relationship_manager from .storage import MessageStorage from .utils import calculate_typing_time, is_mentioned_bot_in_txt +from .utils_image import image_path_to_base64 from .willing_manager import willing_manager # 导入意愿管理器 - +from .message_base import UserInfo, GroupInfo, Seg class ChatBot: def __init__(self): @@ -43,12 +44,9 @@ class ChatBot: async def handle_message(self, event: GroupMessageEvent, bot: Bot) -> None: """处理收到的群消息""" - if event.group_id not in global_config.talk_allowed_groups: - return self.bot = bot # 更新 bot 实例 - if event.user_id in global_config.ban_user_id: - return + group_info = await bot.get_group_info(group_id=event.group_id) sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) @@ -56,25 +54,42 @@ class ChatBot: await relationship_manager.update_relationship(user_id = event.user_id, data = sender_info) await relationship_manager.update_relationship_value(user_id = event.user_id, relationship_value = 0.5) - message = Message( - group_id=event.group_id, - user_id=event.user_id, + message_cq=MessageRecvCQ( message_id=event.message_id, - user_cardname=sender_info['card'], - raw_message=str(event.original_message), - plain_text=event.get_plaintext(), + user_id=event.user_id, + raw_message=str(event.original_message), + group_id=event.group_id, reply_message=event.reply, + platform='qq' ) - await message.initialize() - + message_json=message_cq.to_dict() + message=MessageRecv(**message_json) + await message.process() + groupinfo=message.message_info.group_info + userinfo=message.message_info.user_info + messageinfo=message.message_info + chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) + if groupinfo: + if groupinfo.group_id not in global_config.talk_allowed_groups: + return + else: + if userinfo: + if userinfo.user_id in []: + pass + else: + return + else: + return + if userinfo.user_id in global_config.ban_user_id: + return # 过滤词 for word in global_config.ban_words: - if word in message.detailed_plain_text: - logger.info(f"\033[1;32m[{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}") + if word in message.processed_plain_text: + logger.info(f"\033[1;32m[{groupinfo.group_name}]{userinfo.user_nickname}:\033[0m {message.processed_plain_text}") logger.info(f"\033[1;32m[过滤词识别]\033[0m 消息中含有{word},filtered") return - current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.time)) + current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) @@ -88,44 +103,58 @@ class ChatBot: await self.storage.store_message(message, topic[0] if topic else None) is_mentioned = is_mentioned_bot_in_txt(message.processed_plain_text) - reply_probability = willing_manager.change_reply_willing_received( - event.group_id, - topic[0] if topic else None, - is_mentioned, - global_config, - event.user_id, - message.is_emoji, - interested_rate + reply_probability = await willing_manager.change_reply_willing_received( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo, + topic=topic[0] if topic else None, + is_mentioned_bot=is_mentioned, + config=global_config, + is_emoji=message.is_emoji, + interested_rate=interested_rate + ) + current_willing = willing_manager.get_willing( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo ) - current_willing = willing_manager.get_willing(event.group_id) - - print(f"\033[1;32m[{current_time}][{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") + print(f"\033[1;32m[{current_time}][{groupinfo.group_name}]{userinfo.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") response = "" if random() < reply_probability: - - + bot_user_info=UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform + ) tinking_time_point = round(time.time(), 2) think_id = 'mt' + str(tinking_time_point) - thinking_message = Message_Thinking(message=message,message_id=think_id) + thinking_message = MessageThinking.from_chat_stream( + chat_stream=chat, + message_id=think_id, + reply=message + ) message_manager.add_message(thinking_message) - willing_manager.change_reply_willing_sent(thinking_message.group_id) + willing_manager.change_reply_willing_sent( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo + ) response,raw_content = await self.gpt.generate_response(message) if response: - container = message_manager.get_container(event.group_id) + container = message_manager.get_container(chat.stream_id) thinking_message = None # 找到message,删除 for msg in container.messages: - if isinstance(msg, Message_Thinking) and msg.message_id == think_id: + if isinstance(msg, MessageThinking) and msg.message_id == think_id: thinking_message = msg container.messages.remove(msg) - # print(f"\033[1;32m[思考消息删除]\033[0m 已找到思考消息对象,开始删除") break # 如果找不到思考消息,直接返回 @@ -135,11 +164,10 @@ class ChatBot: #记录开始思考的时间,避免从思考到回复的时间太久 thinking_start_time = thinking_message.thinking_start_time - message_set = MessageSet(event.group_id, global_config.BOT_QQ, think_id) # 发送消息的id和产生发送消息的message_thinking是一致的 + message_set = MessageSet(chat, think_id) #计算打字时间,1是为了模拟打字,2是避免多条回复乱序 accu_typing_time = 0 - # print(f"\033[1;32m[开始回复]\033[0m 开始将回复1载入发送容器") mark_head = False for msg in response: # print(f"\033[1;32m[回复内容]\033[0m {msg}") @@ -148,22 +176,16 @@ class ChatBot: accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - bot_message = Message_Sending( - group_id=event.group_id, - user_id=global_config.BOT_QQ, + message_segment = Seg(type='text', data=msg) + bot_message = MessageSending( message_id=think_id, - raw_message=msg, - plain_text=msg, - processed_plain_text=msg, - user_nickname=global_config.BOT_NICKNAME, - group_name=message.group_name, - time=timepoint, #记录了回复生成的时间 - thinking_start_time=thinking_start_time, #记录了思考开始的时间 - reply_message_id=message.message_id + chat_stream=chat, + message_segment=message_segment, + reply=message, + is_head=not mark_head, + is_emoji=False ) - await bot_message.initialize() if not mark_head: - bot_message.is_head = True mark_head = True message_set.add_message(bot_message) @@ -180,30 +202,22 @@ class ChatBot: if emoji_raw != None: emoji_path,discription = emoji_raw - emoji_cq = cq_code_tool.create_emoji_cq(emoji_path) + emoji_cq = image_path_to_base64(emoji_path) if random() < 0.5: bot_response_time = tinking_time_point - 1 else: bot_response_time = bot_response_time + 1 - bot_message = Message_Sending( - group_id=event.group_id, - user_id=global_config.BOT_QQ, - message_id=0, - raw_message=emoji_cq, - plain_text=emoji_cq, - processed_plain_text=emoji_cq, - detailed_plain_text=discription, - user_nickname=global_config.BOT_NICKNAME, - group_name=message.group_name, - time=bot_response_time, - is_emoji=True, - translate_cq=False, - thinking_start_time=thinking_start_time, - # reply_message_id=message.message_id + message_segment = Seg(type='emoji', data=emoji_cq) + bot_message = MessageSending( + message_id=think_id, + chat_stream=chat, + message_segment=message_segment, + reply=message, + is_head=False, + is_emoji=True ) - await bot_message.initialize() message_manager.add_message(bot_message) emotion = await self.gpt._get_emotion_tags(raw_content) print(f"为 '{response}' 获取到的情感标签为:{emotion}") @@ -219,8 +233,12 @@ class ChatBot: await relationship_manager.update_relationship_value(message.user_id, relationship_value=valuedict[emotion[0]]) # 使用情绪管理器更新情绪 self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) - - # willing_manager.change_reply_willing_after_sent(event.group_id) + + willing_manager.change_reply_willing_after_sent( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo + ) # 创建全局ChatBot实例 chat_bot = ChatBot() \ No newline at end of file diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py new file mode 100644 index 000000000..e617054ec --- /dev/null +++ b/src/plugins/chat/chat_stream.py @@ -0,0 +1,209 @@ +import time +import asyncio +from typing import Optional, Dict, Tuple +import hashlib + +from loguru import logger +from ...common.database import Database +from .message_base import UserInfo, GroupInfo + + +class ChatStream: + """聊天流对象,存储一个完整的聊天上下文""" + def __init__(self, + stream_id: str, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None, + data: dict = None): + self.stream_id = stream_id + self.platform = platform + self.user_info = user_info + self.group_info = group_info + self.create_time = data.get('create_time', int(time.time())) if data else int(time.time()) + self.last_active_time = data.get('last_active_time', self.create_time) if data else self.create_time + self.saved = False + + def to_dict(self) -> dict: + """转换为字典格式""" + result = { + 'stream_id': self.stream_id, + 'platform': self.platform, + 'user_info': self.user_info.to_dict() if self.user_info else None, + 'group_info': self.group_info.to_dict() if self.group_info else None, + 'create_time': self.create_time, + 'last_active_time': self.last_active_time + } + return result + + @classmethod + def from_dict(cls, data: dict) -> 'ChatStream': + """从字典创建实例""" + user_info = UserInfo(**data.get('user_info', {})) if data.get('user_info') else None + group_info = GroupInfo(**data.get('group_info', {})) if data.get('group_info') else None + + return cls( + stream_id=data['stream_id'], + platform=data['platform'], + user_info=user_info, + group_info=group_info, + data=data + ) + + def update_active_time(self): + """更新最后活跃时间""" + self.last_active_time = int(time.time()) + self.saved = False + + +class ChatManager: + """聊天管理器,管理所有聊天流""" + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + self.streams: Dict[str, ChatStream] = {} # stream_id -> ChatStream + self.db = Database.get_instance() + self._ensure_collection() + self._initialized = True + # 在事件循环中启动初始化 + asyncio.create_task(self._initialize()) + # 启动自动保存任务 + asyncio.create_task(self._auto_save_task()) + + async def _initialize(self): + """异步初始化""" + try: + await self.load_all_streams() + logger.success(f"聊天管理器已启动,已加载 {len(self.streams)} 个聊天流") + except Exception as e: + logger.error(f"聊天管理器启动失败: {str(e)}") + + async def _auto_save_task(self): + """定期自动保存所有聊天流""" + while True: + await asyncio.sleep(300) # 每5分钟保存一次 + try: + await self._save_all_streams() + logger.info("聊天流自动保存完成") + except Exception as e: + logger.error(f"聊天流自动保存失败: {str(e)}") + + def _ensure_collection(self): + """确保数据库集合存在并创建索引""" + if 'chat_streams' not in self.db.db.list_collection_names(): + self.db.db.create_collection('chat_streams') + # 创建索引 + self.db.db.chat_streams.create_index([('stream_id', 1)], unique=True) + self.db.db.chat_streams.create_index([ + ('platform', 1), + ('user_info.user_id', 1), + ('group_info.group_id', 1) + ]) + + def _generate_stream_id(self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None) -> str: + """生成聊天流唯一ID""" + # 组合关键信息 + components = [ + platform, + str(user_info.user_id), + str(group_info.group_id) if group_info else 'private' + ] + + # 使用MD5生成唯一ID + key = '_'.join(components) + return hashlib.md5(key.encode()).hexdigest() + + async def get_or_create_stream(self, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None) -> ChatStream: + """获取或创建聊天流 + + Args: + platform: 平台标识 + user_info: 用户信息 + group_info: 群组信息(可选) + + Returns: + ChatStream: 聊天流对象 + """ + # 生成stream_id + stream_id = self._generate_stream_id(platform, user_info, group_info) + + # 检查内存中是否存在 + if stream_id in self.streams: + stream = self.streams[stream_id] + # 更新用户信息和群组信息 + stream.user_info = user_info + if group_info: + stream.group_info = group_info + stream.update_active_time() + return stream + + # 检查数据库中是否存在 + data = self.db.db.chat_streams.find_one({'stream_id': stream_id}) + if data: + stream = ChatStream.from_dict(data) + # 更新用户信息和群组信息 + stream.user_info = user_info + if group_info: + stream.group_info = group_info + stream.update_active_time() + else: + # 创建新的聊天流 + stream = ChatStream( + stream_id=stream_id, + platform=platform, + user_info=user_info, + group_info=group_info + ) + + # 保存到内存和数据库 + self.streams[stream_id] = stream + await self._save_stream(stream) + return stream + + def get_stream(self, stream_id: str) -> Optional[ChatStream]: + """通过stream_id获取聊天流""" + return self.streams.get(stream_id) + + def get_stream_by_info(self, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None) -> Optional[ChatStream]: + """通过信息获取聊天流""" + stream_id = self._generate_stream_id(platform, user_info, group_info) + return self.streams.get(stream_id) + + async def _save_stream(self, stream: ChatStream): + """保存聊天流到数据库""" + if not stream.saved: + self.db.db.chat_streams.update_one( + {'stream_id': stream.stream_id}, + {'$set': stream.to_dict()}, + upsert=True + ) + stream.saved = True + + async def _save_all_streams(self): + """保存所有聊天流""" + for stream in self.streams.values(): + await self._save_stream(stream) + + async def load_all_streams(self): + """从数据库加载所有聊天流""" + all_streams = self.db.db.chat_streams.find({}) + for data in all_streams: + stream = ChatStream.from_dict(data) + self.streams[stream.stream_id] = stream + + +# 创建全局单例 +chat_manager = ChatManager() diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index e908219b5..b29e25b4c 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -373,6 +373,24 @@ class CQCode_tool: # 生成CQ码,设置sub_type=1表示这是表情包 return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" + @staticmethod + def create_emoji_cq_base64(base64_data: str) -> str: + """ + 创建表情包CQ码 + Args: + base64_data: base64编码的表情包数据 + Returns: + 表情包CQ码字符串 + """ + # 转义base64数据 + escaped_base64 = base64_data.replace('&', '&') \ + .replace('[', '[') \ + .replace(']', ']') \ + .replace(',', ',') + # 生成CQ码,设置sub_type=1表示这是表情包 + return f"[CQ:image,file=base64://{escaped_base64},sub_type=1]" + + diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index ff59220dc..3432f011c 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -3,7 +3,7 @@ import os import random import time import traceback -from typing import Optional +from typing import Optional, Tuple import base64 import hashlib @@ -92,7 +92,7 @@ class EmojiManager: except Exception as e: logger.error(f"记录表情使用失败: {str(e)}") - async def get_emoji_for_text(self, text: str) -> Optional[str]: + async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str,str]]: """根据文本内容获取相关表情包 Args: text: 输入文本 diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index a561e6490..949f45596 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -5,11 +5,10 @@ from typing import Dict, ForwardRef, List, Optional, Union import urllib3 from loguru import logger -from .cq_code import CQCode, cq_code_tool -from .utils_cq import parse_cq_code from .utils_user import get_groupname, get_user_cardname, get_user_nickname from .utils_image import image_manager from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase +from .chat_stream import ChatStream # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -115,36 +114,17 @@ class MessageProcessBase(MessageBase): def __init__( self, message_id: str, - user_id: int, - group_id: Optional[int] = None, - platform: str = "qq", + chat_stream: ChatStream, message_segment: Optional[Seg] = None, reply: Optional['MessageRecv'] = None ): - # 构造用户信息 - user_info = UserInfo( - platform=platform, - user_id=user_id, - user_nickname=get_user_nickname(user_id), - user_cardname=get_user_cardname(user_id) if group_id else None - ) - - # 构造群组信息(如果有) - group_info = None - if group_id: - group_info = GroupInfo( - platform=platform, - group_id=group_id, - group_name=get_groupname(group_id) - ) - # 构造基础消息信息 message_info = BaseMessageInfo( - platform=platform, + platform=chat_stream.platform, message_id=message_id, time=int(time.time()), - group_info=group_info, - user_info=user_info + group_info=chat_stream.group_info, + user_info=chat_stream.user_info ) # 调用父类初始化 @@ -241,17 +221,13 @@ class MessageThinking(MessageProcessBase): def __init__( self, message_id: str, - user_id: int, - group_id: Optional[int] = None, - platform: str = "qq", + chat_stream: ChatStream, reply: Optional['MessageRecv'] = None ): # 调用父类初始化 super().__init__( message_id=message_id, - user_id=user_id, - group_id=group_id, - platform=platform, + chat_stream=chat_stream, message_segment=None, # 思考状态不需要消息段 reply=reply ) @@ -259,6 +235,15 @@ class MessageThinking(MessageProcessBase): # 思考状态特有属性 self.interrupt = False + @classmethod + def from_chat_stream(cls, chat_stream: ChatStream, message_id: str, reply: Optional['MessageRecv'] = None) -> 'MessageThinking': + """从聊天流创建思考状态消息""" + return cls( + message_id=message_id, + chat_stream=chat_stream, + reply=reply + ) + @dataclass class MessageSending(MessageProcessBase): """发送状态的消息类""" @@ -266,19 +251,16 @@ class MessageSending(MessageProcessBase): def __init__( self, message_id: str, - user_id: int, + chat_stream: ChatStream, message_segment: Seg, - group_id: Optional[int] = None, reply: Optional['MessageRecv'] = None, - platform: str = "qq", - is_head: bool = False + is_head: bool = False, + is_emoji: bool = False ): # 调用父类初始化 super().__init__( message_id=message_id, - user_id=user_id, - group_id=group_id, - platform=platform, + chat_stream=chat_stream, message_segment=message_segment, reply=reply ) @@ -286,6 +268,12 @@ class MessageSending(MessageProcessBase): # 发送状态特有属性 self.reply_to_message_id = reply.message_info.message_id if reply else None self.is_head = is_head + self.is_emoji = is_emoji + if is_head: + self.message_segment = Seg(type='seglist', data=[ + Seg(type='reply', data=reply.message_info.message_id), + self.message_segment + ]) async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" @@ -298,26 +286,24 @@ class MessageSending(MessageProcessBase): cls, thinking: MessageThinking, message_segment: Seg, - reply: Optional['MessageRecv'] = None, - is_head: bool = False + is_head: bool = False, + is_emoji: bool = False ) -> 'MessageSending': """从思考状态消息创建发送状态消息""" return cls( message_id=thinking.message_info.message_id, - user_id=thinking.message_info.user_info.user_id, + chat_stream=thinking.chat_stream, message_segment=message_segment, - group_id=thinking.message_info.group_info.group_id if thinking.message_info.group_info else None, - reply=reply or thinking.reply, - platform=thinking.message_info.platform, - is_head=is_head + reply=thinking.reply, + is_head=is_head, + is_emoji=is_emoji ) @dataclass class MessageSet: """消息集合类,可以存储多个发送消息""" - def __init__(self, group_id: int, user_id: int, message_id: str): - self.group_id = group_id - self.user_id = user_id + def __init__(self, chat_stream: ChatStream, message_id: str): + self.chat_stream = chat_stream self.message_id = message_id self.messages: List[MessageSending] = [] self.time = round(time.time(), 2) diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 80df4e340..7d9c6216d 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -176,7 +176,7 @@ class MessageSendCQ(MessageCQ): elif seg.type == 'image': # 如果是base64图片数据 if seg.data.startswith(('data:', 'base64:')): - return f"[CQ:image,file=base64://{seg.data}]" + return cq_code_tool.create_emoji_cq_base64(seg.data) # 如果是表情包(本地文件) return cq_code_tool.create_emoji_cq(seg.data) elif seg.type == 'at': diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8ed30d69c..9b1ab66be 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -5,10 +5,11 @@ from typing import Dict, List, Optional, Union from nonebot.adapters.onebot.v11 import Bot from .cq_code import cq_code_tool -from .message_cq import Message, Message_Sending, Message_Thinking, MessageSet +from .message_cq import MessageSendCQ +from .message import MessageSending, MessageThinking, MessageRecv,MessageSet from .storage import MessageStorage -from .utils import calculate_typing_time from .config import global_config +from .chat_stream import chat_manager class Message_Sender: @@ -21,66 +22,59 @@ class Message_Sender: def set_bot(self, bot: Bot): """设置当前bot实例""" self._current_bot = bot - - async def send_group_message( - self, - group_id: int, - send_text: str, - auto_escape: bool = False, - reply_message_id: int = None, - at_user_id: int = None - ) -> None: - if not self._current_bot: - raise RuntimeError("Bot未设置,请先调用set_bot方法设置bot实例") - - message = send_text - - # 如果需要回复 - if reply_message_id: - reply_cq = cq_code_tool.create_reply_cq(reply_message_id) - message = reply_cq + message - - # 如果需要at - # if at_user_id: - # at_cq = cq_code_tool.create_at_cq(at_user_id) - # message = at_cq + " " + message - - - typing_time = calculate_typing_time(message) - if typing_time > 10: - typing_time = 10 - await asyncio.sleep(typing_time) - - # 发送消息 - try: - await self._current_bot.send_group_msg( - group_id=group_id, - message=message, - auto_escape=auto_escape + async def send_message( + self, + message: MessageSending, + ) -> None: + """发送消息""" + if isinstance(message, MessageSending): + message_send=MessageSendCQ( + message_id=message.message_id, + user_id=message.message_info.user_info.user_id, + message_segment=message.message_segment, + reply=message.reply ) - print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") - except Exception as e: - print(f"发生错误 {e}") - print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + if message.message_info.group_info: + try: + await self._current_bot.send_group_msg( + group_id=message.message_info.group_info.group_id, + message=message_send.raw_message, + auto_escape=False + ) + print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + except Exception as e: + print(f"发生错误 {e}") + print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + else: + try: + await self._current_bot.send_private_msg( + user_id=message.message_info.user_info.user_id, + message=message_send.raw_message, + auto_escape=False + ) + print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + except Exception as e: + print(f"发生错误 {e}") + print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") class MessageContainer: - """单个群的发送/思考消息容器""" - def __init__(self, group_id: int, max_size: int = 100): - self.group_id = group_id + """单个聊天流的发送/思考消息容器""" + def __init__(self, chat_id: str, max_size: int = 100): + self.chat_id = chat_id self.max_size = max_size self.messages = [] self.last_send_time = 0 self.thinking_timeout = 20 # 思考超时时间(秒) - def get_timeout_messages(self) -> List[Message_Sending]: + def get_timeout_messages(self) -> List[MessageSending]: """获取所有超时的Message_Sending对象(思考时间超过30秒),按thinking_start_time排序""" current_time = time.time() timeout_messages = [] for msg in self.messages: - if isinstance(msg, Message_Sending): + if isinstance(msg, MessageSending): if current_time - msg.thinking_start_time > self.thinking_timeout: timeout_messages.append(msg) @@ -89,7 +83,7 @@ class MessageContainer: return timeout_messages - def get_earliest_message(self) -> Optional[Union[Message_Thinking, Message_Sending]]: + def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: """获取thinking_start_time最早的消息对象""" if not self.messages: return None @@ -102,16 +96,15 @@ class MessageContainer: earliest_message = msg return earliest_message - def add_message(self, message: Union[Message_Thinking, Message_Sending]) -> None: + def add_message(self, message: Union[MessageThinking, MessageSending]) -> None: """添加消息到队列""" - # print(f"\033[1;32m[添加消息]\033[0m 添加消息到对应群") if isinstance(message, MessageSet): for single_message in message.messages: self.messages.append(single_message) else: self.messages.append(message) - def remove_message(self, message: Union[Message_Thinking, Message_Sending]) -> bool: + def remove_message(self, message: Union[MessageThinking, MessageSending]) -> bool: """移除消息,如果消息存在则返回True,否则返回False""" try: if message in self.messages: @@ -126,40 +119,42 @@ class MessageContainer: """检查是否有待发送的消息""" return bool(self.messages) - def get_all_messages(self) -> List[Union[Message, Message_Thinking]]: + def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]: """获取所有消息""" return list(self.messages) class MessageManager: - """管理所有群的消息容器""" + """管理所有聊天流的消息容器""" def __init__(self): - self.containers: Dict[int, MessageContainer] = {} + self.containers: Dict[str, MessageContainer] = {} # chat_id -> MessageContainer self.storage = MessageStorage() self._running = True - def get_container(self, group_id: int) -> MessageContainer: - """获取或创建群的消息容器""" - if group_id not in self.containers: - self.containers[group_id] = MessageContainer(group_id) - return self.containers[group_id] + def get_container(self, chat_id: str) -> MessageContainer: + """获取或创建聊天流的消息容器""" + if chat_id not in self.containers: + self.containers[chat_id] = MessageContainer(chat_id) + return self.containers[chat_id] - def add_message(self, message: Union[Message_Thinking, Message_Sending, MessageSet]) -> None: - container = self.get_container(message.group_id) + def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None: + chat_stream = chat_manager.get_stream_by_info( + platform=message.message_info.platform, + user_info=message.message_info.user_info, + group_info=message.message_info.group_info + ) + if not chat_stream: + raise ValueError("无法找到对应的聊天流") + container = self.get_container(chat_stream.stream_id) container.add_message(message) - async def process_group_messages(self, group_id: int): - """处理群消息""" - # if int(time.time() / 3) == time.time() / 3: - # print(f"\033[1;34m[调试]\033[0m 开始处理群{group_id}的消息") - container = self.get_container(group_id) + async def process_chat_messages(self, chat_id: str): + """处理聊天流消息""" + container = self.get_container(chat_id) if container.has_messages(): - #最早的对象,可能是思考消息,也可能是发送消息 - message_earliest = container.get_earliest_message() #一个message_thinking or message_sending + message_earliest = container.get_earliest_message() - #如果是思考消息 - if isinstance(message_earliest, Message_Thinking): - #优先等待这条消息 + if isinstance(message_earliest, MessageThinking): message_earliest.update_thinking_time() thinking_time = message_earliest.thinking_time print(f"\033[1;34m[调试]\033[0m 消息正在思考中,已思考{int(thinking_time)}秒\033[K\r", end='', flush=True) @@ -168,42 +163,36 @@ class MessageManager: if thinking_time > global_config.thinking_timeout: print(f"\033[1;33m[警告]\033[0m 消息思考超时({thinking_time}秒),移除该消息") container.remove_message(message_earliest) - else:# 如果不是message_thinking就只能是message_sending + else: print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") - #直接发,等什么呢 - if message_earliest.is_head and message_earliest.update_thinking_time() >30: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False, reply_message_id=message_earliest.reply_message_id) + if message_earliest.is_head and message_earliest.update_thinking_time() > 30: + await message_sender.send_message(message_earliest) else: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False) - #移除消息 + await message_sender.send_message(message_earliest) + if message_earliest.is_emoji: message_earliest.processed_plain_text = "[表情包]" await self.storage.store_message(message_earliest, None) container.remove_message(message_earliest) - #获取并处理超时消息 - message_timeout = container.get_timeout_messages() #也许是一堆message_sending + message_timeout = container.get_timeout_messages() if message_timeout: print(f"\033[1;34m[调试]\033[0m 发现{len(message_timeout)}条超时消息") for msg in message_timeout: if msg == message_earliest: - continue # 跳过已经处理过的消息 + continue try: - #发送 - if msg.is_head and msg.update_thinking_time() >30: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) + if msg.is_head and msg.update_thinking_time() > 30: + await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) else: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False) - + await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False) - #如果是表情包,则替换为"[表情包]" if msg.is_emoji: msg.processed_plain_text = "[表情包]" await self.storage.store_message(msg, None) - # 安全地移除消息 if not container.remove_message(msg): print("\033[1;33m[警告]\033[0m 尝试删除不存在的消息") except Exception as e: @@ -215,8 +204,8 @@ class MessageManager: while self._running: await asyncio.sleep(1) tasks = [] - for group_id in self.containers.keys(): - tasks.append(self.process_group_messages(group_id)) + for chat_id in self.containers.keys(): + tasks.append(self.process_chat_messages(chat_id)) await asyncio.gather(*tasks) diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 4ed7a2f11..b69f2a638 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,7 +1,8 @@ import asyncio -from typing import Optional +from typing import Optional, Union from ...common.database import Database +from .message_base import UserInfo class Impression: @@ -13,27 +14,36 @@ class Impression: class Relationship: user_id: int = None - # impression: Impression = None - # group_id: int = None - # group_name: str = None + platform: str = None gender: str = None age: int = None nickname: str = None relationship_value: float = None saved = False - def __init__(self, user_id: int, data=None, **kwargs): + def __init__(self, user_id: int = None, data: dict = None, user_info: UserInfo = None, **kwargs): if isinstance(data, dict): # 如果输入是字典,使用字典解析 self.user_id = data.get('user_id') + self.platform = data.get('platform', 'qq') self.gender = data.get('gender') self.age = data.get('age') self.nickname = data.get('nickname') self.relationship_value = data.get('relationship_value', 0.0) self.saved = data.get('saved', False) + elif user_info is not None: + # 如果输入是UserInfo对象 + self.user_id = user_info.user_id + self.platform = user_info.platform or 'qq' + self.nickname = user_info.user_nickname or user_info.user_cardname or "某人" + self.relationship_value = kwargs.get('relationship_value', 0.0) + self.gender = kwargs.get('gender') + self.age = kwargs.get('age') + self.saved = kwargs.get('saved', False) else: # 如果是直接传入属性值 self.user_id = kwargs.get('user_id') + self.platform = kwargs.get('platform', 'qq') self.gender = kwargs.get('gender') self.age = kwargs.get('age') self.nickname = kwargs.get('nickname') @@ -41,32 +51,63 @@ class Relationship: self.saved = kwargs.get('saved', False) - - class RelationshipManager: def __init__(self): - self.relationships: dict[int, Relationship] = {} + self.relationships: dict[tuple[int, str], Relationship] = {} # 修改为使用(user_id, platform)作为键 - async def update_relationship(self, user_id: int, data=None, **kwargs): + async def update_relationship(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None, + data: dict = None, + **kwargs) -> Optional[Relationship]: + """更新或创建关系 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + data: 字典格式的数据(可选) + **kwargs: 其他参数 + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + # 使用(user_id, platform)作为键 + key = (user_id, platform) + # 检查是否在内存中已存在 - relationship = self.relationships.get(user_id) + relationship = self.relationships.get(key) if relationship: # 如果存在,更新现有对象 if isinstance(data, dict): - for key, value in data.items(): - if hasattr(relationship, key) and value is not None: - setattr(relationship, key, value) + for k, value in data.items(): + if hasattr(relationship, k) and value is not None: + setattr(relationship, k, value) else: - for key, value in kwargs.items(): - if hasattr(relationship, key) and value is not None: - setattr(relationship, key, value) + for k, value in kwargs.items(): + if hasattr(relationship, k) and value is not None: + setattr(relationship, k, value) else: # 如果不存在,创建新对象 - relationship = Relationship(user_id, data=data) if isinstance(data, dict) else Relationship(user_id, **kwargs) - self.relationships[user_id] = relationship - - # 更新 id_name_nickname_table - # self.id_name_nickname_table[user_id] = [relationship.nickname] # 别称设置为空列表 + if user_info is not None: + relationship = Relationship(user_info=user_info, **kwargs) + elif isinstance(data, dict): + data['platform'] = platform + relationship = Relationship(user_id=user_id, data=data) + else: + kwargs['platform'] = platform + kwargs['user_id'] = user_id + relationship = Relationship(**kwargs) + self.relationships[key] = relationship # 保存到数据库 await self.storage_relationship(relationship) @@ -74,33 +115,87 @@ class RelationshipManager: return relationship - async def update_relationship_value(self, user_id: int, **kwargs): + async def update_relationship_value(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None, + **kwargs) -> Optional[Relationship]: + """更新关系值 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + **kwargs: 其他参数 + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + # 使用(user_id, platform)作为键 + key = (user_id, platform) + # 检查是否在内存中已存在 - relationship = self.relationships.get(user_id) + relationship = self.relationships.get(key) if relationship: - for key, value in kwargs.items(): - if key == 'relationship_value': + for k, value in kwargs.items(): + if k == 'relationship_value': relationship.relationship_value += value await self.storage_relationship(relationship) relationship.saved = True return relationship else: - print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id} 不存在,无法更新") + # 如果不存在且提供了user_info,则创建新的关系 + if user_info is not None: + return await self.update_relationship(user_info=user_info, **kwargs) + print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id}({platform}) 不存在,无法更新") return None - - def get_relationship(self, user_id: int) -> Optional[Relationship]: - """获取用户关系对象""" - if user_id in self.relationships: - return self.relationships[user_id] + def get_relationship(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None) -> Optional[Relationship]: + """获取用户关系对象 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + key = (user_id, platform) + if key in self.relationships: + return self.relationships[key] else: return 0 async def load_relationship(self, data: dict) -> Relationship: - """从数据库加载或创建新的关系对象""" - rela = Relationship(user_id=data['user_id'], data=data) + """从数据库加载或创建新的关系对象""" + # 确保data中有platform字段,如果没有则默认为'qq' + if 'platform' not in data: + data['platform'] = 'qq' + + rela = Relationship(data=data) rela.saved = True - self.relationships[rela.user_id] = rela + key = (rela.user_id, rela.platform) + self.relationships[key] = rela return rela async def load_all_relationships(self): @@ -117,9 +212,7 @@ class RelationshipManager: all_relationships = db.db.relationships.find({}) # 依次加载每条记录 for data in all_relationships: - user_id = data['user_id'] - relationship = await self.load_relationship(data) - self.relationships[user_id] = relationship + await self.load_relationship(data) print(f"\033[1;32m[关系管理]\033[0m 已加载 {len(self.relationships)} 条关系记录") while True: @@ -130,16 +223,15 @@ class RelationshipManager: async def _save_all_relationships(self): """将所有关系数据保存到数据库""" # 保存所有关系数据 - for userid, relationship in self.relationships.items(): + for (userid, platform), relationship in self.relationships.items(): if not relationship.saved: relationship.saved = True await self.storage_relationship(relationship) - async def storage_relationship(self,relationship: Relationship): - """ - 将关系记录存储到数据库中 - """ + async def storage_relationship(self, relationship: Relationship): + """将关系记录存储到数据库中""" user_id = relationship.user_id + platform = relationship.platform nickname = relationship.nickname relationship_value = relationship.relationship_value gender = relationship.gender @@ -148,8 +240,9 @@ class RelationshipManager: db = Database.get_instance() db.db.relationships.update_one( - {'user_id': user_id}, + {'user_id': user_id, 'platform': platform}, {'$set': { + 'platform': platform, 'nickname': nickname, 'relationship_value': relationship_value, 'gender': gender, @@ -159,12 +252,35 @@ class RelationshipManager: upsert=True ) - def get_name(self, user_id: int) -> str: + def get_name(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None) -> str: + """获取用户昵称 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + Returns: + str: 用户昵称 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + # 确保user_id是整数类型 user_id = int(user_id) - if user_id in self.relationships: - - return self.relationships[user_id].nickname + key = (user_id, platform) + if key in self.relationships: + return self.relationships[key].nickname + elif user_info is not None: + return user_info.user_nickname or user_info.user_cardname or "某人" else: return "某人" diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 4b1bf0ca4..2c1f4071d 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -1,47 +1,28 @@ -from typing import Optional +from typing import Optional, Union from ...common.database import Database -from .message_cq import Message +from .message_base import MessageBase +from .message import MessageSending, MessageRecv class MessageStorage: def __init__(self): self.db = Database.get_instance() - async def store_message(self, message: Message, topic: Optional[str] = None) -> None: + async def store_message(self, message: Union[MessageSending, MessageRecv], topic: Optional[str] = None) -> None: """存储消息到数据库""" try: - if not message.is_emoji: - message_data = { - "group_id": message.group_id, - "user_id": message.user_id, - "message_id": message.message_id, - "raw_message": message.raw_message, - "plain_text": message.plain_text, + message_data = { + "message_id": message.message_info.message_id, + "time": message.message_info.time, + "group_id": message.message_info.group_info.group_id, + "group_name": message.message_info.group_info.group_name, + "user_id": message.message_info.user_info.user_id, + "user_nickname": message.message_info.user_info.user_nickname, + "detailed_plain_text": message.detailed_plain_text, "processed_plain_text": message.processed_plain_text, - "time": message.time, - "user_nickname": message.user_nickname, - "user_cardname": message.user_cardname, - "group_name": message.group_name, "topic": topic, - "detailed_plain_text": message.detailed_plain_text, } - else: - message_data = { - "group_id": message.group_id, - "user_id": message.user_id, - "message_id": message.message_id, - "raw_message": message.raw_message, - "plain_text": message.plain_text, - "processed_plain_text": '[表情包]', - "time": message.time, - "user_nickname": message.user_nickname, - "user_cardname": message.user_cardname, - "group_name": message.group_name, - "topic": topic, - "detailed_plain_text": message.detailed_plain_text, - } - self.db.db.messages.insert_one(message_data) except Exception as e: print(f"\033[1;31m[错误]\033[0m 存储消息失败: {e}") diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 001b66207..d73071ba4 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -1,10 +1,15 @@ import asyncio +from typing import Dict +from loguru import logger + from .config import global_config +from .message_base import UserInfo, GroupInfo +from .chat_stream import chat_manager class WillingManager: def __init__(self): - self.group_reply_willing = {} # 存储每个群的回复意愿 + self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿 self._decay_task = None self._started = False @@ -12,20 +17,35 @@ class WillingManager: """定期衰减回复意愿""" while True: await asyncio.sleep(5) - for group_id in self.group_reply_willing: - self.group_reply_willing[group_id] = max(0, self.group_reply_willing[group_id] * 0.6) + for chat_id in self.chat_reply_willing: + self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.6) - def get_willing(self, group_id: int) -> float: - """获取指定群组的回复意愿""" - return self.group_reply_willing.get(group_id, 0) + def get_willing(self, platform: str, user_info: UserInfo, group_info: GroupInfo = None) -> float: + """获取指定聊天流的回复意愿""" + stream = chat_manager.get_stream_by_info(platform, user_info, group_info) + if stream: + return self.chat_reply_willing.get(stream.stream_id, 0) + return 0 - def set_willing(self, group_id: int, willing: float): - """设置指定群组的回复意愿""" - self.group_reply_willing[group_id] = willing + def set_willing(self, chat_id: str, willing: float): + """设置指定聊天流的回复意愿""" + self.chat_reply_willing[chat_id] = willing - def change_reply_willing_received(self, group_id: int, topic: str, is_mentioned_bot: bool, config, user_id: int = None, is_emoji: bool = False, interested_rate: float = 0) -> float: - """改变指定群组的回复意愿并返回回复概率""" - current_willing = self.group_reply_willing.get(group_id, 0) + async def change_reply_willing_received(self, + platform: str, + user_info: UserInfo, + group_info: GroupInfo = None, + topic: str = None, + is_mentioned_bot: bool = False, + config = None, + is_emoji: bool = False, + interested_rate: float = 0) -> float: + """改变指定聊天流的回复意愿并返回回复概率""" + # 获取或创建聊天流 + stream = await chat_manager.get_or_create_stream(platform, user_info, group_info) + chat_id = stream.stream_id + + current_willing = self.chat_reply_willing.get(chat_id, 0) # print(f"初始意愿: {current_willing}") if is_mentioned_bot and current_willing < 1.0: @@ -49,31 +69,37 @@ class WillingManager: # print(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") reply_probability = max((current_willing - 0.45) * 2, 0) - if group_id not in config.talk_allowed_groups: - current_willing = 0 - reply_probability = 0 - - if group_id in config.talk_frequency_down_groups: - reply_probability = reply_probability / global_config.down_frequency_rate + + # 检查群组权限(如果是群聊) + if group_info: + if group_info.group_id not in config.talk_allowed_groups: + current_willing = 0 + reply_probability = 0 + + if group_info.group_id in config.talk_frequency_down_groups: + reply_probability = reply_probability / global_config.down_frequency_rate reply_probability = min(reply_probability, 1) if reply_probability < 0: reply_probability = 0 - - self.group_reply_willing[group_id] = min(current_willing, 3.0) + self.chat_reply_willing[chat_id] = min(current_willing, 3.0) return reply_probability - def change_reply_willing_sent(self, group_id: int): - """开始思考后降低群组的回复意愿""" - current_willing = self.group_reply_willing.get(group_id, 0) - self.group_reply_willing[group_id] = max(0, current_willing - 2) + def change_reply_willing_sent(self, platform: str, user_info: UserInfo, group_info: GroupInfo = None): + """开始思考后降低聊天流的回复意愿""" + stream = chat_manager.get_stream_by_info(platform, user_info, group_info) + if stream: + current_willing = self.chat_reply_willing.get(stream.stream_id, 0) + self.chat_reply_willing[stream.stream_id] = max(0, current_willing - 2) - def change_reply_willing_after_sent(self, group_id: int): - """发送消息后提高群组的回复意愿""" - current_willing = self.group_reply_willing.get(group_id, 0) - if current_willing < 1: - self.group_reply_willing[group_id] = min(1, current_willing + 0.2) + def change_reply_willing_after_sent(self, platform: str, user_info: UserInfo, group_info: GroupInfo = None): + """发送消息后提高聊天流的回复意愿""" + stream = chat_manager.get_stream_by_info(platform, user_info, group_info) + if stream: + current_willing = self.chat_reply_willing.get(stream.stream_id, 0) + if current_willing < 1: + self.chat_reply_willing[stream.stream_id] = min(1, current_willing + 0.2) async def ensure_started(self): """确保衰减任务已启动""" From 8baef077204065bcd50a9bc4a8ac6b632976c0a8 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:22:58 +0800 Subject: [PATCH 008/105] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8?= =?UTF-8?q?=E5=B1=80logger=E5=88=9D=E5=A7=8B=E5=8C=96=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bot.py b/bot.py index 4a463f5db..fbe300a6b 100644 --- a/bot.py +++ b/bot.py @@ -12,6 +12,7 @@ import platform # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} + def easter_egg(): # 彩蛋 from colorama import init, Fore @@ -91,6 +92,17 @@ def load_env(): RuntimeError(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在") +def load_logger(): + logger.remove() # 移除默认配置 + logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", + colorize=True, + level=os.getenv("LOG_LEVEL", "INFO") # 根据环境设置日志级别,默认为INFO + ) + def scan_provider(env_config: dict): provider = {} @@ -136,6 +148,7 @@ if __name__ == "__main__": init_config() init_env() load_env() + load_logger() env_config = {key: os.getenv(key) for key in os.environ} scan_provider(env_config) From 31a5514a0f3e88a3e67f750d6e8ffae8841a14eb Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:30:41 +0800 Subject: [PATCH 009/105] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E5=85=A8?= =?UTF-8?q?=E5=B1=80logger=E5=8A=A0=E8=BD=BD=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index fbe300a6b..3defe3fed 100644 --- a/bot.py +++ b/bot.py @@ -145,10 +145,11 @@ if __name__ == "__main__": time.tzset() easter_egg() + load_logger() init_config() init_env() load_env() - load_logger() + env_config = {key: os.getenv(key) for key in os.environ} scan_provider(env_config) From 980cde7230376f77b134213de4c1434687171839 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:40:32 +0800 Subject: [PATCH 010/105] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96scheduler=5Fge?= =?UTF-8?q?nerator=E6=97=A5=E5=BF=97&=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/schedule/schedule_generator.py | 102 +++++++++++---------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 8a036152c..ffe99a2da 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -13,21 +13,21 @@ from ..models.utils_model import LLM_request driver = get_driver() config = driver.config - Database.initialize( - host= config.MONGODB_HOST, - port= int(config.MONGODB_PORT), - db_name= config.DATABASE_NAME, - username= config.MONGODB_USERNAME, - password= config.MONGODB_PASSWORD, - auth_source=config.MONGODB_AUTH_SOURCE - ) + host=config.MONGODB_HOST, + port=int(config.MONGODB_PORT), + db_name=config.DATABASE_NAME, + username=config.MONGODB_USERNAME, + password=config.MONGODB_PASSWORD, + auth_source=config.MONGODB_AUTH_SOURCE +) + class ScheduleGenerator: def __init__(self): - #根据global_config.llm_normal这一字典配置指定模型 + # 根据global_config.llm_normal这一字典配置指定模型 # self.llm_scheduler = LLMModel(model = global_config.llm_normal,temperature=0.9) - self.llm_scheduler = LLM_request(model = global_config.llm_normal,temperature=0.9) + self.llm_scheduler = LLM_request(model=global_config.llm_normal, temperature=0.9) self.db = Database.get_instance() self.today_schedule_text = "" self.today_schedule = {} @@ -35,39 +35,41 @@ class ScheduleGenerator: self.tomorrow_schedule = {} self.yesterday_schedule_text = "" self.yesterday_schedule = {} - + async def initialize(self): today = datetime.datetime.now() tomorrow = datetime.datetime.now() + datetime.timedelta(days=1) yesterday = datetime.datetime.now() - datetime.timedelta(days=1) - + self.today_schedule_text, self.today_schedule = await self.generate_daily_schedule(target_date=today) - self.tomorrow_schedule_text, self.tomorrow_schedule = await self.generate_daily_schedule(target_date=tomorrow,read_only=True) - self.yesterday_schedule_text, self.yesterday_schedule = await self.generate_daily_schedule(target_date=yesterday,read_only=True) - - async def generate_daily_schedule(self, target_date: datetime.datetime = None,read_only:bool = False) -> Dict[str, str]: - + self.tomorrow_schedule_text, self.tomorrow_schedule = await self.generate_daily_schedule(target_date=tomorrow, + read_only=True) + self.yesterday_schedule_text, self.yesterday_schedule = await self.generate_daily_schedule( + target_date=yesterday, read_only=True) + + async def generate_daily_schedule(self, target_date: datetime.datetime = None, read_only: bool = False) -> Dict[ + str, str]: + date_str = target_date.strftime("%Y-%m-%d") weekday = target_date.strftime("%A") - schedule_text = str - + existing_schedule = self.db.db.schedule.find_one({"date": date_str}) if existing_schedule: - print(f"{date_str}的日程已存在:") + logger.info(f"{date_str}的日程已存在:") schedule_text = existing_schedule["schedule"] # print(self.schedule_text) elif read_only == False: - print(f"{date_str}的日程不存在,准备生成新的日程。") - prompt = f"""我是{global_config.BOT_NICKNAME},{global_config.PROMPT_SCHEDULE_GEN},请为我生成{date_str}({weekday})的日程安排,包括:"""+\ - """ + logger.info(f"{date_str}的日程不存在,准备生成新的日程。") + prompt = f"""我是{global_config.BOT_NICKNAME},{global_config.PROMPT_SCHEDULE_GEN},请为我生成{date_str}({weekday})的日程安排,包括:""" + \ + """ 1. 早上的学习和工作安排 2. 下午的活动和任务 3. 晚上的计划和休息时间 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表,仅返回内容,不要返回注释,时间采用24小时制,格式为{"时间": "活动","时间": "活动",...}。""" - + try: schedule_text, _ = await self.llm_scheduler.generate_response(prompt) self.db.db.schedule.insert_one({"date": date_str, "schedule": schedule_text}) @@ -76,36 +78,35 @@ class ScheduleGenerator: schedule_text = "生成日程时出错了" # print(self.schedule_text) else: - print(f"{date_str}的日程不存在。") + logger.info(f"{date_str}的日程不存在。") schedule_text = "忘了" - return schedule_text,None - + return schedule_text, None + schedule_form = self._parse_schedule(schedule_text) - return schedule_text,schedule_form - + return schedule_text, schedule_form + def _parse_schedule(self, schedule_text: str) -> Union[bool, Dict[str, str]]: """解析日程文本,转换为时间和活动的字典""" - try: + try: schedule_dict = json.loads(schedule_text) return schedule_dict except json.JSONDecodeError as e: - print(schedule_text) - print(f"解析日程失败: {str(e)}") + logger.exception("解析日程失败: {}".format(schedule_text)) return False - + def _parse_time(self, time_str: str) -> str: """解析时间字符串,转换为时间""" return datetime.datetime.strptime(time_str, "%H:%M") - + def get_current_task(self) -> str: """获取当前时间应该进行的任务""" current_time = datetime.datetime.now().strftime("%H:%M") - + # 找到最接近当前时间的任务 closest_time = None min_diff = float('inf') - + # 检查今天的日程 if not self.today_schedule: return "摸鱼" @@ -114,7 +115,7 @@ class ScheduleGenerator: if closest_time is None or diff < min_diff: closest_time = time_str min_diff = diff - + # 检查昨天的日程中的晚间任务 if self.yesterday_schedule: for time_str in self.yesterday_schedule.keys(): @@ -125,17 +126,17 @@ class ScheduleGenerator: closest_time = time_str min_diff = diff return closest_time, self.yesterday_schedule[closest_time] - + if closest_time: return closest_time, self.today_schedule[closest_time] return "摸鱼" - + def _time_diff(self, time1: str, time2: str) -> int: """计算两个时间字符串之间的分钟差""" - if time1=="24:00": - time1="23:59" - if time2=="24:00": - time2="23:59" + if time1 == "24:00": + time1 = "23:59" + if time2 == "24:00": + time2 = "23:59" t1 = datetime.datetime.strptime(time1, "%H:%M") t2 = datetime.datetime.strptime(time2, "%H:%M") diff = int((t2 - t1).total_seconds() / 60) @@ -146,17 +147,18 @@ class ScheduleGenerator: diff -= 1440 # 减一天的分钟 # print(f"时间1[{time1}]: 时间2[{time2}],差值[{diff}]分钟") return diff - + def print_schedule(self): """打印完整的日程安排""" if not self._parse_schedule(self.today_schedule_text): - print("今日日程有误,将在下次运行时重新生成") + logger.warning("今日日程有误,将在下次运行时重新生成") self.db.db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) else: - print("\n=== 今日日程安排 ===") + logger.info("\n=== 今日日程安排 ===") for time_str, activity in self.today_schedule.items(): - print(f"时间[{time_str}]: 活动[{activity}]") - print("==================\n") + logger.info(f"时间[{time_str}]: 活动[{activity}]") + logger.info("==================\n") + # def main(): # # 使用示例 @@ -165,7 +167,7 @@ class ScheduleGenerator: # scheduler.print_schedule() # print("\n当前任务:") # print(scheduler.get_current_task()) - + # print("昨天日程:") # print(scheduler.yesterday_schedule) # print("今天日程:") @@ -175,5 +177,5 @@ class ScheduleGenerator: # if __name__ == "__main__": # main() - + bot_schedule = ScheduleGenerator() From 41aa974d2e9653f00024981e28189af50718b6ed Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:41:27 +0800 Subject: [PATCH 011/105] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96chat/config.py?= =?UTF-8?q?=E7=9A=84=E6=97=A5=E5=BF=97&=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/config.py | 163 +++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index c027753c0..332c28401 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -6,45 +6,46 @@ import tomli from loguru import logger from packaging import version from packaging.version import Version, InvalidVersion -from packaging.specifiers import SpecifierSet,InvalidSpecifier +from packaging.specifiers import SpecifierSet, InvalidSpecifier + @dataclass class BotConfig: - """机器人配置类""" + """机器人配置类""" INNER_VERSION: Version = None BOT_QQ: Optional[int] = 1 BOT_NICKNAME: Optional[str] = None - + # 消息处理相关配置 MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数 emoji_chance: float = 0.2 # 发送表情包的基础概率 - + ENABLE_PIC_TRANSLATE: bool = True # 是否启用图片翻译 - + talk_allowed_groups = set() talk_frequency_down_groups = set() thinking_timeout: int = 100 # 思考时间 - + response_willing_amplifier: float = 1.0 # 回复意愿放大系数 response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数 down_frequency_rate: float = 3.5 # 降低回复频率的群组回复意愿降低系数 - + ban_user_id = set() - + build_memory_interval: int = 30 # 记忆构建间隔(秒) forget_memory_interval: int = 300 # 记忆遗忘间隔(秒) EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟) EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟) EMOJI_SAVE: bool = True # 偷表情包 - EMOJI_CHECK: bool = False #是否开启过滤 - EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 + EMOJI_CHECK: bool = False # 是否开启过滤 + EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 ban_words = set() max_response_length: int = 1024 # 最大回复长度 - + # 模型配置 llm_reasoning: Dict[str, str] = field(default_factory=lambda: {}) llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {}) @@ -60,34 +61,34 @@ class BotConfig: MODEL_R1_PROBABILITY: float = 0.8 # R1模型概率 MODEL_V3_PROBABILITY: float = 0.1 # V3模型概率 MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率 - + enable_advance_output: bool = False # 是否启用高级输出 - enable_kuuki_read: bool = True # 是否启用读空气功能 - - mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 - mood_decay_rate: float = 0.95 # 情绪衰减率 - mood_intensity_factor: float = 0.7 # 情绪强度因子 + enable_kuuki_read: bool = True # 是否启用读空气功能 - keywords_reaction_rules = [] # 关键词回复规则 + mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 + mood_decay_rate: float = 0.95 # 情绪衰减率 + mood_intensity_factor: float = 0.7 # 情绪强度因子 - chinese_typo_enable=True # 是否启用中文错别字生成器 - chinese_typo_error_rate=0.03 # 单字替换概率 - chinese_typo_min_freq=7 # 最小字频阈值 - chinese_typo_tone_error_rate=0.2 # 声调错误概率 - chinese_typo_word_replace_rate=0.02 # 整词替换概率 + keywords_reaction_rules = [] # 关键词回复规则 + + chinese_typo_enable = True # 是否启用中文错别字生成器 + chinese_typo_error_rate = 0.03 # 单字替换概率 + chinese_typo_min_freq = 7 # 最小字频阈值 + chinese_typo_tone_error_rate = 0.2 # 声调错误概率 + chinese_typo_word_replace_rate = 0.02 # 整词替换概率 # 默认人设 - PROMPT_PERSONALITY=[ + PROMPT_PERSONALITY = [ "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", "是一个女大学生,你有黑色头发,你会刷小红书", "是一个女大学生,你会刷b站,对ACG文化感兴趣" ] - PROMPT_SCHEDULE_GEN="一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" - - PERSONALITY_1: float = 0.6 # 第一种人格概率 - PERSONALITY_2: float = 0.3 # 第二种人格概率 - PERSONALITY_3: float = 0.1 # 第三种人格概率 - + PROMPT_SCHEDULE_GEN = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" + + PERSONALITY_1: float = 0.6 # 第一种人格概率 + PERSONALITY_2: float = 0.3 # 第二种人格概率 + PERSONALITY_3: float = 0.1 # 第三种人格概率 + @staticmethod def get_config_dir() -> str: """获取配置文件目录""" @@ -97,7 +98,7 @@ class BotConfig: if not os.path.exists(config_dir): os.makedirs(config_dir) return config_dir - + @classmethod def convert_to_specifierset(cls, value: str) -> SpecifierSet: """将 字符串 版本表达式转换成 SpecifierSet @@ -117,7 +118,7 @@ class BotConfig: exit(1) return converted - + @classmethod def get_config_version(cls, toml: dict) -> Version: """提取配置文件的 SpecifierSet 版本数据 @@ -129,14 +130,14 @@ class BotConfig: if 'inner' in toml: try: - config_version : str = toml["inner"]["version"] + config_version: str = toml["inner"]["version"] except KeyError as e: logger.error(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") else: - toml["inner"] = { "version": "0.0.0" } + toml["inner"] = {"version": "0.0.0"} config_version = toml["inner"]["version"] - + try: ver = version.parse(config_version) except InvalidVersion as e: @@ -148,38 +149,38 @@ class BotConfig: raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") return ver - + @classmethod def load_config(cls, config_path: str = None) -> "BotConfig": """从TOML配置文件加载配置""" config = cls() def personality(parent: dict): - personality_config=parent['personality'] - personality=personality_config.get('prompt_personality') + personality_config = parent['personality'] + personality = personality_config.get('prompt_personality') if len(personality) >= 2: logger.info(f"载入自定义人格:{personality}") - config.PROMPT_PERSONALITY=personality_config.get('prompt_personality',config.PROMPT_PERSONALITY) - logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule',config.PROMPT_SCHEDULE_GEN)}") - config.PROMPT_SCHEDULE_GEN=personality_config.get('prompt_schedule',config.PROMPT_SCHEDULE_GEN) - + config.PROMPT_PERSONALITY = personality_config.get('prompt_personality', config.PROMPT_PERSONALITY) + logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") + config.PROMPT_SCHEDULE_GEN = personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN) + if config.INNER_VERSION in SpecifierSet(">=0.0.2"): - config.PERSONALITY_1=personality_config.get('personality_1_probability',config.PERSONALITY_1) - config.PERSONALITY_2=personality_config.get('personality_2_probability',config.PERSONALITY_2) - config.PERSONALITY_3=personality_config.get('personality_3_probability',config.PERSONALITY_3) + config.PERSONALITY_1 = personality_config.get('personality_1_probability', config.PERSONALITY_1) + config.PERSONALITY_2 = personality_config.get('personality_2_probability', config.PERSONALITY_2) + config.PERSONALITY_3 = personality_config.get('personality_3_probability', config.PERSONALITY_3) def emoji(parent: dict): emoji_config = parent["emoji"] config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) - config.EMOJI_CHECK_PROMPT = emoji_config.get('check_prompt',config.EMOJI_CHECK_PROMPT) - config.EMOJI_SAVE = emoji_config.get('auto_save',config.EMOJI_SAVE) - config.EMOJI_CHECK = emoji_config.get('enable_check',config.EMOJI_CHECK) - + config.EMOJI_CHECK_PROMPT = emoji_config.get('check_prompt', config.EMOJI_CHECK_PROMPT) + config.EMOJI_SAVE = emoji_config.get('auto_save', config.EMOJI_SAVE) + config.EMOJI_CHECK = emoji_config.get('enable_check', config.EMOJI_CHECK) + def cq_code(parent: dict): cq_code_config = parent["cq_code"] config.ENABLE_PIC_TRANSLATE = cq_code_config.get("enable_pic_translate", config.ENABLE_PIC_TRANSLATE) - + def bot(parent: dict): # 机器人基础配置 bot_config = parent["bot"] @@ -191,12 +192,13 @@ class BotConfig: response_config = parent["response"] config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY) config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY) - config.MODEL_R1_DISTILL_PROBABILITY = response_config.get("model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY) + config.MODEL_R1_DISTILL_PROBABILITY = response_config.get("model_r1_distill_probability", + config.MODEL_R1_DISTILL_PROBABILITY) config.max_response_length = response_config.get("max_response_length", config.max_response_length) - + def model(parent: dict): # 加载模型配置 - model_config:dict = parent["model"] + model_config: dict = parent["model"] config_list = [ "llm_reasoning", @@ -213,24 +215,24 @@ class BotConfig: for item in config_list: if item in model_config: - cfg_item:dict = model_config[item] + cfg_item: dict = model_config[item] # base_url 的例子: SILICONFLOW_BASE_URL # key 的例子: SILICONFLOW_KEY cfg_target = { - "name" : "", - "base_url" : "", - "key" : "", - "pri_in" : 0, - "pri_out" : 0 + "name": "", + "base_url": "", + "key": "", + "pri_in": 0, + "pri_out": 0 } if config.INNER_VERSION in SpecifierSet("<=0.0.0"): cfg_target = cfg_item elif config.INNER_VERSION in SpecifierSet(">=0.0.1"): - stable_item = ["name","pri_in","pri_out"] - pricing_item = ["pri_in","pri_out"] + stable_item = ["name", "pri_in", "pri_out"] + pricing_item = ["pri_in", "pri_out"] # 从配置中原始拷贝稳定字段 for i in stable_item: # 如果 字段 属于计费项 且获取不到,那默认值是 0 @@ -244,18 +246,16 @@ class BotConfig: logger.error(f"{item} 中的必要字段 {e} 不存在,请检查") raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") - provider = cfg_item.get("provider") if provider == None: logger.error(f"provider 字段在模型配置 {item} 中不存在,请检查") raise KeyError(f"provider 字段在模型配置 {item} 中不存在,请检查") - + cfg_target["base_url"] = f"{provider}_BASE_URL" cfg_target["key"] = f"{provider}_KEY" - # 如果 列表中的项目在 model_config 中,利用反射来设置对应项目 - setattr(config,item,cfg_target) + setattr(config, item, cfg_target) else: logger.error(f"模型 {item} 在config中不存在,请检查") raise KeyError(f"模型 {item} 在config中不存在,请检查") @@ -265,12 +265,14 @@ class BotConfig: config.MIN_TEXT_LENGTH = msg_config.get("min_text_length", config.MIN_TEXT_LENGTH) config.MAX_CONTEXT_SIZE = msg_config.get("max_context_size", config.MAX_CONTEXT_SIZE) config.emoji_chance = msg_config.get("emoji_chance", config.emoji_chance) - config.ban_words=msg_config.get("ban_words",config.ban_words) + config.ban_words = msg_config.get("ban_words", config.ban_words) if config.INNER_VERSION in SpecifierSet(">=0.0.2"): config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout) - config.response_willing_amplifier = msg_config.get("response_willing_amplifier", config.response_willing_amplifier) - config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", config.response_interested_rate_amplifier) + config.response_willing_amplifier = msg_config.get("response_willing_amplifier", + config.response_willing_amplifier) + config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", + config.response_interested_rate_amplifier) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) def memory(parent: dict): @@ -294,8 +296,10 @@ class BotConfig: config.chinese_typo_enable = chinese_typo_config.get("enable", config.chinese_typo_enable) config.chinese_typo_error_rate = chinese_typo_config.get("error_rate", config.chinese_typo_error_rate) config.chinese_typo_min_freq = chinese_typo_config.get("min_freq", config.chinese_typo_min_freq) - config.chinese_typo_tone_error_rate = chinese_typo_config.get("tone_error_rate", config.chinese_typo_tone_error_rate) - config.chinese_typo_word_replace_rate = chinese_typo_config.get("word_replace_rate", config.chinese_typo_word_replace_rate) + config.chinese_typo_tone_error_rate = chinese_typo_config.get("tone_error_rate", + config.chinese_typo_tone_error_rate) + config.chinese_typo_word_replace_rate = chinese_typo_config.get("word_replace_rate", + config.chinese_typo_word_replace_rate) def groups(parent: dict): groups_config = parent["groups"] @@ -382,7 +386,7 @@ class BotConfig: except(tomli.TOMLDecodeError) as e: logger.critical(f"配置文件bot_config.toml填写有误,请检查第{e.lineno}行第{e.colno}处:{e.msg}") exit(1) - + # 获取配置文件版本 config.INNER_VERSION = cls.get_config_version(toml_dict) @@ -406,31 +410,32 @@ class BotConfig: f"当前程序仅支持以下版本范围: {group_specifierset}" ) raise InvalidVersion(f"当前程序仅支持以下版本范围: {group_specifierset}") - + # 如果 necessary 项目存在,而且显式声明是 False,进入特殊处理 elif "necessary" in include_configs[key] and include_configs[key].get("necessary") == False: # 通过 pass 处理的项虽然直接忽略也是可以的,但是为了不增加理解困难,依然需要在这里显式处理 if key == "keywords_reaction": pass - + else: # 如果用户根本没有需要的配置项,提示缺少配置 logger.error(f"配置文件中缺少必需的字段: '{key}'") raise KeyError(f"配置文件中缺少必需的字段: '{key}'") logger.success(f"成功加载配置文件: {config_path}") - - return config - + + return config + + # 获取配置文件路径 bot_config_floder_path = BotConfig.get_config_dir() -print(f"正在品鉴配置文件目录: {bot_config_floder_path}") +logger.debug(f"正在品鉴配置文件目录: {bot_config_floder_path}") bot_config_path = os.path.join(bot_config_floder_path, "bot_config.toml") if os.path.exists(bot_config_path): # 如果开发环境配置文件不存在,则使用默认配置文件 - print(f"异常的新鲜,异常的美味: {bot_config_path}") + logger.debug(f"异常的新鲜,异常的美味: {bot_config_path}") logger.info("使用bot配置文件") else: # 配置文件不存在 @@ -439,8 +444,6 @@ else: global_config = BotConfig.load_config(config_path=bot_config_path) - if not global_config.enable_advance_output: logger.remove() pass - From 1b5344f32fe7122842b5564248805428af1009fc Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 22:42:04 +0800 Subject: [PATCH 012/105] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96bot=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E7=9A=84=E6=97=A5=E5=BF=97&=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 - src/plugins/chat/__init__.py | 50 ++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/bot.py b/bot.py index 3defe3fed..84ce5067b 100644 --- a/bot.py +++ b/bot.py @@ -150,7 +150,6 @@ if __name__ == "__main__": init_env() load_env() - env_config = {key: os.getenv(key) for key in os.environ} scan_provider(env_config) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 4411b3ce3..485d9d759 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -28,16 +28,15 @@ driver = get_driver() config = driver.config Database.initialize( - host= config.MONGODB_HOST, - port= int(config.MONGODB_PORT), - db_name= config.DATABASE_NAME, - username= config.MONGODB_USERNAME, - password= config.MONGODB_PASSWORD, - auth_source= config.MONGODB_AUTH_SOURCE + host=config.MONGODB_HOST, + port=int(config.MONGODB_PORT), + db_name=config.DATABASE_NAME, + username=config.MONGODB_USERNAME, + password=config.MONGODB_PASSWORD, + auth_source=config.MONGODB_AUTH_SOURCE ) print("\033[1;32m[初始化数据库完成]\033[0m") - # 导入其他模块 from ..memory_system.memory import hippocampus, memory_graph from .bot import ChatBot @@ -57,24 +56,24 @@ group_msg = on_message(priority=5) scheduler = require("nonebot_plugin_apscheduler").scheduler - @driver.on_startup async def start_background_tasks(): """启动后台任务""" # 启动LLM统计 llm_stats.start() - print("\033[1;32m[初始化]\033[0m LLM统计功能已启动") - + logger.success("[初始化]LLM统计功能已启动") + # 初始化并启动情绪管理器 mood_manager = MoodManager.get_instance() mood_manager.start_mood_update(update_interval=global_config.mood_update_interval) - print("\033[1;32m[初始化]\033[0m 情绪管理器已启动") - + logger.success("[初始化]情绪管理器已启动") + # 只启动表情包管理任务 asyncio.create_task(emoji_manager.start_periodic_check(interval_MINS=global_config.EMOJI_CHECK_INTERVAL)) await bot_schedule.initialize() bot_schedule.print_schedule() - + + @driver.on_startup async def init_relationships(): """在 NoneBot2 启动时初始化关系管理器""" @@ -82,45 +81,52 @@ async def init_relationships(): await relationship_manager.load_all_relationships() asyncio.create_task(relationship_manager._start_relationship_manager()) + @driver.on_bot_connect async def _(bot: Bot): """Bot连接成功时的处理""" global _message_manager_started print(f"\033[1;38;5;208m-----------{global_config.BOT_NICKNAME}成功连接!-----------\033[0m") await willing_manager.ensure_started() - + message_sender.set_bot(bot) print("\033[1;38;5;208m-----------消息发送器已启动!-----------\033[0m") - + if not _message_manager_started: asyncio.create_task(message_manager.start_processor()) _message_manager_started = True print("\033[1;38;5;208m-----------消息处理器已启动!-----------\033[0m") - + asyncio.create_task(emoji_manager._periodic_scan(interval_MINS=global_config.EMOJI_REGISTER_INTERVAL)) print("\033[1;38;5;208m-----------开始偷表情包!-----------\033[0m") - + + @group_msg.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): await chat_bot.handle_message(event, bot) + # 添加build_memory定时任务 @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval, id="build_memory") async def build_memory_task(): """每build_memory_interval秒执行一次记忆构建""" - print("\033[1;32m[记忆构建]\033[0m -------------------------------------------开始构建记忆-------------------------------------------") + print( + "\033[1;32m[记忆构建]\033[0m -------------------------------------------开始构建记忆-------------------------------------------") start_time = time.time() await hippocampus.operation_build_memory(chat_size=20) end_time = time.time() - print(f"\033[1;32m[记忆构建]\033[0m -------------------------------------------记忆构建完成:耗时: {end_time - start_time:.2f} 秒-------------------------------------------") - -@scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") + print( + f"\033[1;32m[记忆构建]\033[0m -------------------------------------------记忆构建完成:耗时: {end_time - start_time:.2f} 秒-------------------------------------------") + + +@scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") async def forget_memory_task(): """每30秒执行一次记忆构建""" # print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") # await hippocampus.operation_forget_topic(percentage=0.1) # print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") + @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval + 10, id="merge_memory") async def merge_memory_task(): """每30秒执行一次记忆构建""" @@ -128,9 +134,9 @@ async def merge_memory_task(): # await hippocampus.operation_merge_memory(percentage=0.1) # print("\033[1;32m[记忆整合]\033[0m 记忆整合完成") + @scheduler.scheduled_job("interval", seconds=30, id="print_mood") async def print_mood_task(): """每30秒打印一次情绪状态""" mood_manager = MoodManager.get_instance() mood_manager.print_mood_status() - From 6a71ea4f2e82d4afa37f51ce55f3f6cb1f4d4814 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 9 Mar 2025 22:45:32 +0800 Subject: [PATCH 013/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E6=97=B6=E9=97=B4bug,config=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E8=AE=B0=E5=BF=86=E5=B1=8F=E8=94=BD=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 6 + changelog_config.md | 6 + src/plugins/chat/config.py | 9 +- src/plugins/memory_system/memory.py | 100 ++++-- .../memory_system/memory_manual_build.py | 307 +++++++++++++++--- template/auto_format.py | 48 --- template/bot_config_template.toml | 21 +- 7 files changed, 367 insertions(+), 130 deletions(-) create mode 100644 changelog.md create mode 100644 changelog_config.md delete mode 100644 template/auto_format.py diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..c68a16ad9 --- /dev/null +++ b/changelog.md @@ -0,0 +1,6 @@ +# Changelog + +## [0.5.12] - 2025-3-9 +### Added +- 新增了 我是测试 + diff --git a/changelog_config.md b/changelog_config.md new file mode 100644 index 000000000..7101fe828 --- /dev/null +++ b/changelog_config.md @@ -0,0 +1,6 @@ +# Changelog + +## [0.0.4] - 2025-3-9 +### Added +- 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 + diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index c027753c0..f4c44a72e 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -88,6 +88,8 @@ class BotConfig: PERSONALITY_2: float = 0.3 # 第二种人格概率 PERSONALITY_3: float = 0.1 # 第三种人格概率 + memory_ban_words: list = field(default_factory=lambda: ['表情包', '图片', '回复', '聊天记录']) # 添加新的配置项默认值 + @staticmethod def get_config_dir() -> str: """获取配置文件目录""" @@ -277,6 +279,10 @@ class BotConfig: memory_config = parent["memory"] config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) config.forget_memory_interval = memory_config.get("forget_memory_interval", config.forget_memory_interval) + + # 在版本 >= 0.0.4 时才处理新增的配置项 + if config.INNER_VERSION in SpecifierSet(">=0.0.4"): + config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) def mood(parent: dict): mood_config = parent["mood"] @@ -344,7 +350,8 @@ class BotConfig: }, "memory": { "func": memory, - "support": ">=0.0.0" + "support": ">=0.0.0", + "necessary": False }, "mood": { "func": mood, diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index f88888aa4..08fc6d30c 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -157,46 +157,88 @@ class Hippocampus: nodes = sorted([source, target]) return hash(f"{nodes[0]}:{nodes[1]}") - def get_memory_sample(self,chat_size=20,time_frequency:dict={'near':2,'mid':4,'far':3}): + def get_memory_sample(self, chat_size=20, time_frequency:dict={'near':2,'mid':4,'far':3}): + """获取记忆样本 + + Returns: + list: 消息记录列表,每个元素是一个消息记录字典列表 + """ current_timestamp = datetime.datetime.now().timestamp() - chat_text = [] - #短期:1h 中期:4h 长期:24h - for _ in range(time_frequency.get('near')): # 循环10次 - random_time = current_timestamp - random.randint(1, 3600) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - for _ in range(time_frequency.get('mid')): # 循环10次 - random_time = current_timestamp - random.randint(3600, 3600*4) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - for _ in range(time_frequency.get('far')): # 循环10次 - random_time = current_timestamp - random.randint(3600*4, 3600*24) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - return [text for text in chat_text if text] - - async def memory_compress(self, input_text, compress_rate=0.1): + chat_samples = [] + + # 短期:1h 中期:4h 长期:24h + for _ in range(time_frequency.get('near')): + random_time = current_timestamp - random.randint(1, 3600) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('mid')): + random_time = current_timestamp - random.randint(3600, 3600*4) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('far')): + random_time = current_timestamp - random.randint(3600*4, 3600*24) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + return chat_samples + + async def memory_compress(self, messages: list, compress_rate=0.1): + """压缩消息记录为记忆 + + Args: + messages: 消息记录字典列表,每个字典包含text和time字段 + compress_rate: 压缩率 + + Returns: + set: (话题, 记忆) 元组集合 + """ + if not messages: + return set() + + # 合并消息文本,同时保留时间信息 + input_text = "" + time_info = "" + # 计算最早和最晚时间 + earliest_time = min(msg['time'] for msg in messages) + latest_time = max(msg['time'] for msg in messages) + + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) + latest_dt = datetime.datetime.fromtimestamp(latest_time) + + # 如果是同一年 + if earliest_dt.year == latest_dt.year: + earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%m-%d %H:%M:%S") + time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" + else: + earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" + + for msg in messages: + input_text += f"{msg['text']}\n" + print(input_text) - #获取topics topic_num = self.calculate_topic_num(input_text, compress_rate) topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(input_text, topic_num)) - # 修改话题处理逻辑 - # 定义需要过滤的关键词 - filter_keywords = ['表情包', '图片', '回复', '聊天记录'] # 过滤topics + filter_keywords = global_config.memory_ban_words topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] - # print(f"原始话题: {topics}") print(f"过滤后话题: {filtered_topics}") - # 使用过滤后的话题继续处理 + # 创建所有话题的请求任务 tasks = [] for topic in filtered_topics: - topic_what_prompt = self.topic_what(input_text, topic) - # 创建异步任务 + topic_what_prompt = self.topic_what(input_text, topic, time_info) task = self.llm_summary_by_topic.generate_response_async(topic_what_prompt) tasks.append((topic.strip(), task)) @@ -440,7 +482,7 @@ class Hippocampus: print(f"选择的记忆:\n{merged_text}") # 使用memory_compress生成新的压缩记忆 - compressed_memories = await self.memory_compress(merged_text, 0.1) + compressed_memories = await self.memory_compress(selected_memories, 0.1) # 从原记忆列表中移除被选中的记忆 for memory in selected_memories: @@ -494,8 +536,8 @@ class Hippocampus: prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' return prompt - def topic_what(self,text, topic): - prompt = f'这是一段文字:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' + def topic_what(self,text, topic, time_info): + prompt = f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' return prompt async def _identify_topics(self, text: str) -> list: diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 3124bc8e4..2e1151f93 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -13,6 +13,7 @@ import networkx as nx import pymongo from dotenv import load_dotenv from loguru import logger +import jieba # from chat.config import global_config sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 @@ -86,23 +87,26 @@ def calculate_information_content(text): return entropy def get_cloest_chat_from_db(db, length: int, timestamp: str): - """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数""" - chat_text = '' + """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 + + Returns: + list: 消息记录字典列表,每个字典包含消息内容和时间信息 + """ + chat_records = [] closest_record = db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) if closest_record and closest_record.get('memorized', 0) < 4: closest_time = closest_record['time'] - group_id = closest_record['group_id'] # 获取groupid + group_id = closest_record['group_id'] # 获取该时间戳之后的length条消息,且groupid相同 - chat_records = list(db.db.messages.find( + records = list(db.db.messages.find( {"time": {"$gt": closest_time}, "group_id": group_id} ).sort('time', 1).limit(length)) # 更新每条消息的memorized属性 - for record in chat_records: - # 检查当前记录的memorized值 + for record in records: current_memorized = record.get('memorized', 0) - if current_memorized > 3: + if current_memorized > 3: print("消息已读取3次,跳过") return '' @@ -112,11 +116,14 @@ def get_cloest_chat_from_db(db, length: int, timestamp: str): {"$set": {"memorized": current_memorized + 1}} ) - chat_text += record["detailed_plain_text"] + # 添加到记录列表中 + chat_records.append({ + 'text': record["detailed_plain_text"], + 'time': record["time"], + 'group_id': record["group_id"] + }) - return chat_text - print("消息已读取3次,跳过") - return '' + return chat_records class Memory_graph: def __init__(self): @@ -205,22 +212,34 @@ class Hippocampus: self.llm_model_summary = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct") def get_memory_sample(self, chat_size=20, time_frequency:dict={'near':2,'mid':4,'far':3}): + """获取记忆样本 + + Returns: + list: 消息记录列表,每个元素是一个消息记录字典列表 + """ current_timestamp = datetime.datetime.now().timestamp() - chat_text = [] - #短期:1h 中期:4h 长期:24h - for _ in range(time_frequency.get('near')): # 循环10次 - random_time = current_timestamp - random.randint(1, 3600*4) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - for _ in range(time_frequency.get('mid')): # 循环10次 - random_time = current_timestamp - random.randint(3600*4, 3600*24) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - for _ in range(time_frequency.get('far')): # 循环10次 - random_time = current_timestamp - random.randint(3600*24, 3600*24*7) # 随机时间 - chat_ = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) - chat_text.append(chat_) - return [chat for chat in chat_text if chat] + chat_samples = [] + + # 短期:1h 中期:4h 长期:24h + for _ in range(time_frequency.get('near')): + random_time = current_timestamp - random.randint(1, 3600*4) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('mid')): + random_time = current_timestamp - random.randint(3600*4, 3600*24) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('far')): + random_time = current_timestamp - random.randint(3600*24, 3600*24*7) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + return chat_samples def calculate_topic_num(self,text, compress_rate): """计算文本的话题数量""" @@ -231,16 +250,49 @@ class Hippocampus: print(f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") return topic_num - async def memory_compress(self, input_text, compress_rate=0.1): + async def memory_compress(self, messages: list, compress_rate=0.1): + """压缩消息记录为记忆 + + Args: + messages: 消息记录字典列表,每个字典包含text和time字段 + compress_rate: 压缩率 + + Returns: + set: (话题, 记忆) 元组集合 + """ + if not messages: + return set() + + # 合并消息文本,同时保留时间信息 + input_text = "" + time_info = "" + # 计算最早和最晚时间 + earliest_time = min(msg['time'] for msg in messages) + latest_time = max(msg['time'] for msg in messages) + + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) + latest_dt = datetime.datetime.fromtimestamp(latest_time) + + # 如果是同一年 + if earliest_dt.year == latest_dt.year: + earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%m-%d %H:%M:%S") + time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" + else: + earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" + + for msg in messages: + input_text += f"{msg['text']}\n" + print(input_text) topic_num = self.calculate_topic_num(input_text, compress_rate) topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(input_text, topic_num)) - # 修改话题处理逻辑 - # 定义需要过滤的关键词 - filter_keywords = ['表情包', '图片', '回复', '聊天记录'] # 过滤topics + filter_keywords = ['表情包', '图片', '回复', '聊天记录'] topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] @@ -250,7 +302,7 @@ class Hippocampus: # 创建所有话题的请求任务 tasks = [] for topic in filtered_topics: - topic_what_prompt = self.topic_what(input_text, topic) + topic_what_prompt = self.topic_what(input_text, topic , time_info) # 创建异步任务 task = self.llm_model_small.generate_response_async(topic_what_prompt) tasks.append((topic.strip(), task)) @@ -267,37 +319,35 @@ class Hippocampus: async def operation_build_memory(self, chat_size=12): # 最近消息获取频率 time_frequency = {'near': 3, 'mid': 8, 'far': 5} - memory_sample = self.get_memory_sample(chat_size, time_frequency) + memory_samples = self.get_memory_sample(chat_size, time_frequency) all_topics = [] # 用于存储所有话题 - for i, input_text in enumerate(memory_sample, 1): + for i, messages in enumerate(memory_samples, 1): # 加载进度可视化 all_topics = [] - progress = (i / len(memory_sample)) * 100 + progress = (i / len(memory_samples)) * 100 bar_length = 30 - filled_length = int(bar_length * i // len(memory_sample)) + filled_length = int(bar_length * i // len(memory_samples)) bar = '█' * filled_length + '-' * (bar_length - filled_length) - print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") + print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - # 生成压缩后记忆 ,表现为 (话题,记忆) 的元组 - compressed_memory = set() + # 生成压缩后记忆 compress_rate = 0.1 - compressed_memory = await self.memory_compress(input_text, compress_rate) + compressed_memory = await self.memory_compress(messages, compress_rate) print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)}") # 将记忆加入到图谱中 for topic, memory in compressed_memory: print(f"\033[1;32m添加节点\033[0m: {topic}") self.memory_graph.add_dot(topic, memory) - all_topics.append(topic) # 收集所有话题 + all_topics.append(topic) + + # 连接相关话题 for i in range(len(all_topics)): for j in range(i + 1, len(all_topics)): print(f"\033[1;32m连接节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") self.memory_graph.connect_dot(all_topics[i], all_topics[j]) - - - self.sync_memory_to_db() @@ -375,7 +425,7 @@ class Hippocampus: if concept not in db_nodes_dict: # 数据库中缺少的节点,添加 - logger.info(f"添加新节点: {concept}") + # logger.info(f"添加新节点: {concept}") node_data = { 'concept': concept, 'memory_items': memory_items, @@ -389,7 +439,7 @@ class Hippocampus: # 如果特征值不同,则更新节点 if db_hash != memory_hash: - logger.info(f"更新节点内容: {concept}") + # logger.info(f"更新节点内容: {concept}") self.memory_graph.db.db.graph_data.nodes.update_one( {'concept': concept}, {'$set': { @@ -402,7 +452,7 @@ class Hippocampus: memory_concepts = set(node[0] for node in memory_nodes) for db_node in db_nodes: if db_node['concept'] not in memory_concepts: - logger.info(f"删除多余节点: {db_node['concept']}") + # logger.info(f"删除多余节点: {db_node['concept']}") self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) # 处理边的信息 @@ -460,9 +510,10 @@ class Hippocampus: prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' return prompt - def topic_what(self,text, topic): + def topic_what(self,text, topic, time_info): # prompt = f'这是一段文字:{text}。我想知道这段文字里有什么关于{topic}的话题,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' - prompt = f'这是一段文字:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' + # 获取当前时间 + prompt = f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' return prompt def remove_node_from_db(self, topic): @@ -597,7 +648,7 @@ class Hippocampus: print(f"选择的记忆:\n{merged_text}") # 使用memory_compress生成新的压缩记忆 - compressed_memories = await self.memory_compress(merged_text, 0.1) + compressed_memories = await self.memory_compress(selected_memories, 0.1) # 从原记忆列表中移除被选中的记忆 for memory in selected_memories: @@ -647,6 +698,164 @@ class Hippocampus: else: print("\n本次检查没有需要合并的节点") + async def _identify_topics(self, text: str) -> list: + """从文本中识别可能的主题""" + topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(text, 5)) + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + return topics + + def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: + """查找与给定主题相似的记忆主题""" + all_memory_topics = list(self.memory_graph.G.nodes()) + all_similar_topics = [] + + for topic in topics: + if debug_info: + pass + + topic_vector = text_to_vector(topic) + has_similar_topic = False + + for memory_topic in all_memory_topics: + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + similarity = cosine_similarity(v1, v2) + + if similarity >= similarity_threshold: + has_similar_topic = True + all_similar_topics.append((memory_topic, similarity)) + + return all_similar_topics + + def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: + """获取相似度最高的主题""" + seen_topics = set() + top_topics = [] + + for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): + if topic not in seen_topics and len(top_topics) < max_topics: + seen_topics.add(topic) + top_topics.append((topic, score)) + + return top_topics + + async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: + """计算输入文本对记忆的激活程度""" + print(f"\033[1;32m[记忆激活]\033[0m 识别主题: {await self._identify_topics(text)}") + + identified_topics = await self._identify_topics(text) + if not identified_topics: + return 0 + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆激活" + ) + + if not all_similar_topics: + return 0 + + top_topics = self._get_top_topics(all_similar_topics, max_topics) + + if len(top_topics) == 1: + topic, score = top_topics[0] + memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + activation = int(score * 50 * penalty) + print(f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") + return activation + + matched_topics = set() + topic_similarities = {} + + for memory_topic, similarity in top_topics: + memory_items = self.memory_graph.G.nodes[memory_topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + for input_topic in identified_topics: + topic_vector = text_to_vector(input_topic) + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + sim = cosine_similarity(v1, v2) + if sim >= similarity_threshold: + matched_topics.add(input_topic) + adjusted_sim = sim * penalty + topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) + print(f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") + + topic_match = len(matched_topics) / len(identified_topics) + average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 + + activation = int((topic_match + average_similarities) / 2 * 100) + print(f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") + + return activation + + async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5) -> list: + """根据输入文本获取相关的记忆内容""" + identified_topics = await self._identify_topics(text) + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆检索" + ) + + relevant_topics = self._get_top_topics(all_similar_topics, max_topics) + + relevant_memories = [] + for topic, score in relevant_topics: + first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) + if first_layer: + if len(first_layer) > max_memory_num/2: + first_layer = random.sample(first_layer, max_memory_num//2) + for memory in first_layer: + relevant_memories.append({ + 'topic': topic, + 'similarity': score, + 'content': memory + }) + + relevant_memories.sort(key=lambda x: x['similarity'], reverse=True) + + if len(relevant_memories) > max_memory_num: + relevant_memories = random.sample(relevant_memories, max_memory_num) + + return relevant_memories + +def segment_text(text): + """使用jieba进行文本分词""" + seg_text = list(jieba.cut(text)) + return seg_text + +def text_to_vector(text): + """将文本转换为词频向量""" + words = segment_text(text) + vector = {} + for word in words: + vector[word] = vector.get(word, 0) + 1 + return vector + +def cosine_similarity(v1, v2): + """计算两个向量的余弦相似度""" + dot_product = sum(a * b for a, b in zip(v1, v2)) + norm1 = math.sqrt(sum(a * a for a in v1)) + norm2 = math.sqrt(sum(b * b for b in v2)) + if norm1 == 0 or norm2 == 0: + return 0 + return dot_product / (norm1 * norm2) def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): # 设置中文字体 diff --git a/template/auto_format.py b/template/auto_format.py deleted file mode 100644 index d99e29e34..000000000 --- a/template/auto_format.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import sys -from pathlib import Path - -import tomli -import tomli_w - - -def sync_configs(): - # 读取两个配置文件 - try: - with open('bot_config_dev.toml', 'rb') as f: # tomli需要使用二进制模式读取 - dev_config = tomli.load(f) - - with open('bot_config.toml', 'rb') as f: - prod_config = tomli.load(f) - except FileNotFoundError as e: - print(f"错误:找不到配置文件 - {e}") - sys.exit(1) - except tomli.TOMLDecodeError as e: - print(f"错误:TOML格式解析失败 - {e}") - sys.exit(1) - - # 递归合并配置 - def merge_configs(source, target): - for key, value in source.items(): - if key not in target: - target[key] = value - elif isinstance(value, dict) and isinstance(target[key], dict): - merge_configs(value, target[key]) - - # 将dev配置的新属性合并到prod配置中 - merge_configs(dev_config, prod_config) - - # 保存更新后的配置 - try: - with open('bot_config.toml', 'wb') as f: # tomli_w需要使用二进制模式写入 - tomli_w.dump(prod_config, f) - print("配置文件同步完成!") - except Exception as e: - print(f"错误:保存配置文件失败 - {e}") - sys.exit(1) - -if __name__ == '__main__': - # 确保在正确的目录下运行 - script_dir = Path(__file__).parent - os.chdir(script_dir) - sync_configs() diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d7c66d3f4..bff64d05f 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,16 @@ [inner] -version = "0.0.3" +version = "0.0.4" + +#如果你想要修改配置文件,请在修改后将version的值进行变更 +#如果新增项目,请在BotConfig类下新增相应的变量 +#1.如果你修改的是[]层级项目,例如你新增了 [memory],那么请在config.py的 load_config函数中的include_configs字典中新增"内容":{ +#"func":memory, +#"support":">=0.0.0", #新的版本号 +#"necessary":False #是否必须 +#} +#2.如果你修改的是[]下的项目,例如你新增了[memory]下的 memory_ban_words ,那么请在config.py的 load_config函数中的 memory函数下新增版本判断: + # if config.INNER_VERSION in SpecifierSet(">=0.0.2"): + # config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) [bot] qq = 123 @@ -49,6 +60,10 @@ max_response_length = 1024 # 麦麦回答的最大token数 build_memory_interval = 300 # 记忆构建间隔 单位秒 forget_memory_interval = 300 # 记忆遗忘间隔 单位秒 +memory_ban_words = [ #不希望记忆的词 + # "403","张三" +] + [mood] mood_update_interval = 1.0 # 情绪更新间隔 单位秒 mood_decay_rate = 0.95 # 情绪衰减率 @@ -69,10 +84,10 @@ reaction = "回答“测试成功”" [chinese_typo] enable = true # 是否启用中文错别字生成器 -error_rate=0.03 # 单字替换概率 +error_rate=0.006 # 单字替换概率 min_freq=7 # 最小字频阈值 tone_error_rate=0.2 # 声调错误概率 -word_replace_rate=0.02 # 整词替换概率 +word_replace_rate=0.006 # 整词替换概率 [others] enable_advance_output = true # 是否启用高级输出 From c4b09177fad08a4cb04b815387640c2aae796c23 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 9 Mar 2025 22:56:13 +0800 Subject: [PATCH 014/105] =?UTF-8?q?=E4=B8=80=E4=B8=AA=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=E5=B0=8F=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1 --- run_memory_vis.bat | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 run_memory_vis.bat diff --git a/run_memory_vis.bat b/run_memory_vis.bat new file mode 100644 index 000000000..14b9c766f --- /dev/null +++ b/run_memory_vis.bat @@ -0,0 +1,18 @@ +@echo on +chcp 65001 > nul +set /p CONDA_ENV="请输入要激活的 conda 环境名称: " +call conda activate %CONDA_ENV% +if errorlevel 1 ( + echo 激活 conda 环境失败 + pause + exit /b 1 +) +echo Conda 环境 "%CONDA_ENV%" 激活成功 +python src/plugins/memory_system/memory_manual_build.py +if errorlevel 1 ( + echo 命令执行失败,错误代码 %errorlevel% + pause + exit /b 1 +) +echo 脚本成功完成 +pause \ No newline at end of file From 089d6a6c796ffa4987476c6fc5624e795ce70eeb Mon Sep 17 00:00:00 2001 From: HYY Date: Sun, 9 Mar 2025 23:00:09 +0800 Subject: [PATCH 015/105] =?UTF-8?q?feat:=20=E9=92=88=E5=AF=B9=E7=A1=85?= =?UTF-8?q?=E5=9F=BA=E6=B5=81=E5=8A=A8=E7=9A=84Pro=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E8=87=AA=E5=8A=A8=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e890b4c80..bd06fd6dd 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -182,13 +182,26 @@ class LLM_request: continue elif response.status in policy["abort_codes"]: logger.error(f"错误码: {response.status} - {error_code_mapping.get(response.status)}") - if response.status == 403 : - if global_config.llm_normal == "Pro/deepseek-ai/DeepSeek-V3": - logger.error("可能是没有给硅基流动充钱,普通模型自动退化至非Pro模型,反应速度可能会变慢") - global_config.llm_normal = "deepseek-ai/DeepSeek-V3" - if global_config.llm_reasoning == "Pro/deepseek-ai/DeepSeek-R1": - logger.error("可能是没有给硅基流动充钱,推理模型自动退化至非Pro模型,反应速度可能会变慢") - global_config.llm_reasoning = "deepseek-ai/DeepSeek-R1" + if response.status == 403: + # 尝试降级Pro模型 + if self.model_name.startswith("Pro/") and self.base_url == "https://api.siliconflow.cn/v1/": + old_model_name = self.model_name + self.model_name = self.model_name[4:] # 移除"Pro/"前缀 + logger.warning(f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}") + + # 对全局配置进行更新 + if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get('name') == old_model_name: + global_config.llm_normal['name'] = self.model_name + logger.warning(f"已将全局配置中的 llm_normal 模型降级") + + # 更新payload中的模型名 + if payload and 'model' in payload: + payload['model'] = self.model_name + + # 重新尝试请求 + retry -= 1 # 不计入重试次数 + continue + raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(response.status)}") response.raise_for_status() From 3149dd3301481b621c4836dda5f75917a83d93ac Mon Sep 17 00:00:00 2001 From: jiajiu123 <1771663559@qq.com> Date: Sun, 9 Mar 2025 23:00:31 +0800 Subject: [PATCH 016/105] =?UTF-8?q?fix:=20mongodb.zip=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=A7=A3=E5=8E=8B=20fix:=E6=9B=B4=E6=8D=A2=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E6=96=B9=E6=B3=95=20fix:=E5=BD=93?= =?UTF-8?q?=20db=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=20feat:=20=E4=B8=80=E9=94=AE=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=AE=8C=E6=88=90=E5=90=8E=E5=90=AF=E5=8A=A8=E9=BA=A6?= =?UTF-8?q?=E9=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.py | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/run.py b/run.py index 46b53318a..35f8653b8 100644 --- a/run.py +++ b/run.py @@ -45,21 +45,15 @@ def run_cmd(command: str, open_new_window: bool = True): command (str): 指定要运行的命令 open_new_window (bool): 指定是否新建一个 cmd 窗口运行 """ - creationflags = 0 if open_new_window: - creationflags = subprocess.CREATE_NEW_CONSOLE - subprocess.Popen( - [ - "cmd.exe", - "/c", - command, - ], - creationflags=creationflags, - ) + command = "start " + command + subprocess.Popen(command, shell=True) def run_maimbot(): run_cmd(r"napcat\NapCatWinBootMain.exe 10001", False) + if not os.path.exists(r"mongodb\db"): + os.makedirs(r"mongodb\db") run_cmd( r"mongodb\bin\mongod.exe --dbpath=" + os.getcwd() + r"\mongodb\db --port 27017" ) @@ -86,26 +80,29 @@ def install_mongodb(): for data in resp.iter_content(chunk_size=1024): size = file.write(data) bar.update(size) - extract_files("mongodb.zip", "mongodb") - print("MongoDB 下载完成") - os.remove("mongodb.zip") - choice = input( - "是否安装 MongoDB Compass?此软件可以以可视化的方式修改数据库,建议安装(Y/n)" - ).upper() - if choice == "Y" or choice == "": - install_mongodb_compass() + extract_files("mongodb.zip", "mongodb") + print("MongoDB 下载完成") + os.remove("mongodb.zip") + choice = input( + "是否安装 MongoDB Compass?此软件可以以可视化的方式修改数据库,建议安装(Y/n)" + ).upper() + if choice == "Y" or choice == "": + install_mongodb_compass() def install_mongodb_compass(): run_cmd( - r"powershell Start-Process powershell -Verb runAs 'Set-ExecutionPolicy RemoteSigned'") + r"powershell Start-Process powershell -Verb runAs 'Set-ExecutionPolicy RemoteSigned'" + ) input("请在弹出的用户账户控制中点击“是”后按任意键继续安装") run_cmd(r"powershell mongodb\bin\Install-Compass.ps1") - input("Compass 安装完成后请按任意键继续后续安装") + input("按任意键启动麦麦") + input("如不需要启动此窗口可直接关闭,无需等待 Compass 安装完成") + install_mongodb_compass() def install_napcat(): - run_cmd("start https://github.com/NapNeko/NapCatQQ/releases") + run_cmd("start https://github.com/NapNeko/NapCatQQ/releases", False) print("请检查弹出的浏览器窗口,点击**第一个**蓝色的“Win64无头” 下载 napcat") napcat_filename = input( "下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell:" @@ -136,4 +133,4 @@ if __name__ == "__main__": run_maimbot() elif choice == "3": run_maimbot() - run_cmd("python src/gui/reasoning_gui.py") \ No newline at end of file + run_cmd(r"python src\gui\reasoning_gui.py") From 3223153e3ecde6410c0e3d3a36e320f893bd1b6a Mon Sep 17 00:00:00 2001 From: jiajiu123 <1771663559@qq.com> Date: Sun, 9 Mar 2025 23:09:02 +0800 Subject: [PATCH 017/105] =?UTF-8?q?feat:=20=E4=B8=80=E9=94=AE=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=96=B0=E5=A2=9E=E8=AE=B0=E5=BF=86=E5=8F=AF=E8=A7=86?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/run.py b/run.py index 35f8653b8..5546d1faa 100644 --- a/run.py +++ b/run.py @@ -123,7 +123,6 @@ if __name__ == "__main__": "请输入要进行的操作:\n" "1.首次安装\n" "2.运行麦麦\n" - "3.运行麦麦并启动可视化推理界面\n" ) os.system("cls") if choice == "1": @@ -131,6 +130,9 @@ if __name__ == "__main__": install_mongodb() elif choice == "2": run_maimbot() - elif choice == "3": - run_maimbot() - run_cmd(r"python src\gui\reasoning_gui.py") + choice = input("是否启动推理可视化?(y/N)").upper() + if choice == "Y": + run_cmd(r"python src\gui\reasoning_gui.py") + choice = input("是否启动记忆可视化?(y/N)").upper() + if choice == "Y": + run_cmd(r"python src/plugins/memory_system/memory_manual_build.py") From 6c35704e70bcd1f2861f68b9e87f4309f67be22c Mon Sep 17 00:00:00 2001 From: jiajiu123 <1771663559@qq.com> Date: Sun, 9 Mar 2025 23:12:48 +0800 Subject: [PATCH 018/105] =?UTF-8?q?fix:=20=E8=B0=83=E7=94=A8=E4=BA=86?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=9A=84=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 5546d1faa..baea4d13c 100644 --- a/run.py +++ b/run.py @@ -98,7 +98,7 @@ def install_mongodb_compass(): run_cmd(r"powershell mongodb\bin\Install-Compass.ps1") input("按任意键启动麦麦") input("如不需要启动此窗口可直接关闭,无需等待 Compass 安装完成") - install_mongodb_compass() + run_maimbot() def install_napcat(): From 665c4593e12007bde95c7633c1e5fbfa8931372d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 9 Mar 2025 23:19:12 +0800 Subject: [PATCH 019/105] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E4=BA=86=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1 --- run_memory_vis.bat | 13 ++- src/plugins/memory_system/draw_memory.py | 79 +++---------------- .../memory_system/memory_manual_build.py | 2 +- 3 files changed, 23 insertions(+), 71 deletions(-) diff --git a/run_memory_vis.bat b/run_memory_vis.bat index 14b9c766f..b1feb0cb2 100644 --- a/run_memory_vis.bat +++ b/run_memory_vis.bat @@ -8,7 +8,18 @@ if errorlevel 1 ( exit /b 1 ) echo Conda 环境 "%CONDA_ENV%" 激活成功 -python src/plugins/memory_system/memory_manual_build.py + +set /p OPTION="请选择运行选项 (1: 运行全部绘制, 2: 运行简单绘制): " +if "%OPTION%"=="1" ( + python src/plugins/memory_system/memory_manual_build.py +) else if "%OPTION%"=="2" ( + python src/plugins/memory_system/draw_memory.py +) else ( + echo 无效的选项 + pause + exit /b 1 +) + if errorlevel 1 ( echo 命令执行失败,错误代码 %errorlevel% pause diff --git a/src/plugins/memory_system/draw_memory.py b/src/plugins/memory_system/draw_memory.py index 006991bcb..c2d04064d 100644 --- a/src/plugins/memory_system/draw_memory.py +++ b/src/plugins/memory_system/draw_memory.py @@ -201,67 +201,6 @@ def topic_what(text, topic): prompt = f'这是一段文字:{text}。我想知道这记忆里有什么关于{topic}的话题,帮我总结成一句自然的话,可以包含时间和人物。只输出这句话就好' return prompt -def visualize_graph(memory_graph: Memory_graph, color_by_memory: bool = False): - # 设置中文字体 - plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 - plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 - - G = memory_graph.G - - # 保存图到本地 - nx.write_gml(G, "memory_graph.gml") # 保存为 GML 格式 - - # 根据连接条数或记忆数量设置节点颜色 - node_colors = [] - nodes = list(G.nodes()) # 获取图中实际的节点列表 - - if color_by_memory: - # 计算每个节点的记忆数量 - memory_counts = [] - for node in nodes: - memory_items = G.nodes[node].get('memory_items', []) - if isinstance(memory_items, list): - count = len(memory_items) - else: - count = 1 if memory_items else 0 - memory_counts.append(count) - max_memories = max(memory_counts) if memory_counts else 1 - - for count in memory_counts: - # 使用不同的颜色方案:红色表示记忆多,蓝色表示记忆少 - if max_memories > 0: - intensity = min(1.0, count / max_memories) - color = (intensity, 0, 1.0 - intensity) # 从蓝色渐变到红色 - else: - color = (0, 0, 1) # 如果没有记忆,则为蓝色 - node_colors.append(color) - else: - # 使用原来的连接数量着色方案 - max_degree = max(G.degree(), key=lambda x: x[1])[1] if G.degree() else 1 - for node in nodes: - degree = G.degree(node) - if max_degree > 0: - red = min(1.0, degree / max_degree) - blue = 1.0 - red - color = (red, 0, blue) - else: - color = (0, 0, 1) - node_colors.append(color) - - # 绘制图形 - plt.figure(figsize=(12, 8)) - pos = nx.spring_layout(G, k=1, iterations=50) - nx.draw(G, pos, - with_labels=True, - node_color=node_colors, - node_size=200, - font_size=10, - font_family='SimHei', - font_weight='bold') - - title = '记忆图谱可视化 - ' + ('按记忆数量着色' if color_by_memory else '按连接数量着色') - plt.title(title, fontsize=16, fontfamily='SimHei') - plt.show() def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): @@ -280,7 +219,7 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal memory_items = H.nodes[node].get('memory_items', []) memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) degree = H.degree(node) - if memory_count < 5 or degree < 2: # 改为小于2而不是小于等于2 + if memory_count < 3 or degree < 2: # 改为小于2而不是小于等于2 nodes_to_remove.append(node) H.remove_nodes_from(nodes_to_remove) @@ -291,7 +230,7 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal return # 保存图到本地 - nx.write_gml(H, "memory_graph.gml") # 保存为 GML 格式 + # nx.write_gml(H, "memory_graph.gml") # 保存为 GML 格式 # 计算节点大小和颜色 node_colors = [] @@ -315,21 +254,23 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) # 使用指数函数使变化更明显 ratio = memory_count / max_memories - size = 500 + 5000 * (ratio ** 2) # 使用平方函数使差异更明显 + size = 500 + 5000 * (ratio ) # 使用1.5次方函数使差异不那么明显 node_sizes.append(size) # 计算节点颜色(基于连接数) degree = H.degree(node) # 红色分量随着度数增加而增加 - red = min(1.0, degree / max_degree) + r = (degree / max_degree) ** 0.3 + red = min(1.0, r) # 蓝色分量随着度数减少而增加 - blue = 1.0 - red - color = (red, 0, blue) + blue = max(0.0, 1 - red) + # blue = 1 + color = (red, 0.1, blue) node_colors.append(color) # 绘制图形 plt.figure(figsize=(12, 8)) - pos = nx.spring_layout(H, k=1.5, iterations=50) # 增加k值使节点分布更开 + pos = nx.spring_layout(H, k=1, iterations=50) # 增加k值使节点分布更开 nx.draw(H, pos, with_labels=True, node_color=node_colors, @@ -339,7 +280,7 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal font_weight='bold', edge_color='gray', width=0.5, - alpha=0.7) + alpha=0.9) title = '记忆图谱可视化 - 节点大小表示记忆数量,颜色表示连接数' plt.title(title, fontsize=16, fontfamily='SimHei') diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 2e1151f93..db88febf2 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -944,7 +944,7 @@ async def main(): db = Database.get_instance() start_time = time.time() - test_pare = {'do_build_memory':True,'do_forget_topic':False,'do_visualize_graph':True,'do_query':False,'do_merge_memory':False} + test_pare = {'do_build_memory':False,'do_forget_topic':False,'do_visualize_graph':True,'do_query':False,'do_merge_memory':False} # 创建记忆图 memory_graph = Memory_graph() From 8428a066642a4249bacb1c7c4aad235544f7fb5d Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Sun, 9 Mar 2025 23:39:44 +0800 Subject: [PATCH 020/105] fix: memory logger optimization (CR comment) --- src/plugins/memory_system/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/plugins/memory_system/__init__.py diff --git a/src/plugins/memory_system/__init__.py b/src/plugins/memory_system/__init__.py new file mode 100644 index 000000000..e69de29bb From 288dbb68b5d8820c8507151d565fa803aebbbaba Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 00:27:42 +0800 Subject: [PATCH 021/105] refactor: logger in src\plugins\chat\__init__.py --- src/plugins/chat/__init__.py | 39 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 485d9d759..795658800 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -2,9 +2,8 @@ import asyncio import time from loguru import logger -from nonebot import get_driver, on_command, on_message, require +from nonebot import get_driver, on_message, require from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageSegment -from nonebot.rule import to_me from nonebot.typing import T_State from ...common.database import Database @@ -16,6 +15,10 @@ from .config import global_config from .emoji_manager import emoji_manager from .relationship_manager import relationship_manager from .willing_manager import willing_manager +from ..memory_system.memory import hippocampus, memory_graph +from .bot import ChatBot +from .message_sender import message_manager, message_sender + # 创建LLM统计实例 llm_stats = LLMStatistics("llm_statistics.txt") @@ -35,19 +38,13 @@ Database.initialize( password=config.MONGODB_PASSWORD, auth_source=config.MONGODB_AUTH_SOURCE ) -print("\033[1;32m[初始化数据库完成]\033[0m") +logger.success("初始化数据库成功") -# 导入其他模块 -from ..memory_system.memory import hippocampus, memory_graph -from .bot import ChatBot - -# from .message_send_control import message_sender -from .message_sender import message_manager, message_sender # 初始化表情管理器 emoji_manager.initialize() -print(f"\033[1;32m正在唤醒{global_config.BOT_NICKNAME}......\033[0m") +logger.debug(f"正在唤醒{global_config.BOT_NICKNAME}......") # 创建机器人实例 chat_bot = ChatBot() # 注册群消息处理器 @@ -61,7 +58,7 @@ async def start_background_tasks(): """启动后台任务""" # 启动LLM统计 llm_stats.start() - logger.success("[初始化]LLM统计功能已启动") + logger.success("LLM统计功能启动成功") # 初始化并启动情绪管理器 mood_manager = MoodManager.get_instance() @@ -77,7 +74,7 @@ async def start_background_tasks(): @driver.on_startup async def init_relationships(): """在 NoneBot2 启动时初始化关系管理器""" - print("\033[1;32m[初始化]\033[0m 正在加载用户关系数据...") + logger.debug("正在加载用户关系数据...") await relationship_manager.load_all_relationships() asyncio.create_task(relationship_manager._start_relationship_manager()) @@ -86,19 +83,19 @@ async def init_relationships(): async def _(bot: Bot): """Bot连接成功时的处理""" global _message_manager_started - print(f"\033[1;38;5;208m-----------{global_config.BOT_NICKNAME}成功连接!-----------\033[0m") + logger.debug(f"-----------{global_config.BOT_NICKNAME}成功连接!-----------") await willing_manager.ensure_started() message_sender.set_bot(bot) - print("\033[1;38;5;208m-----------消息发送器已启动!-----------\033[0m") + logger.success("-----------消息发送器已启动!-----------") if not _message_manager_started: asyncio.create_task(message_manager.start_processor()) _message_manager_started = True - print("\033[1;38;5;208m-----------消息处理器已启动!-----------\033[0m") + logger.success("-----------消息处理器已启动!-----------") asyncio.create_task(emoji_manager._periodic_scan(interval_MINS=global_config.EMOJI_REGISTER_INTERVAL)) - print("\033[1;38;5;208m-----------开始偷表情包!-----------\033[0m") + logger.success("-----------开始偷表情包!-----------") @group_msg.handle() @@ -110,13 +107,15 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval, id="build_memory") async def build_memory_task(): """每build_memory_interval秒执行一次记忆构建""" - print( - "\033[1;32m[记忆构建]\033[0m -------------------------------------------开始构建记忆-------------------------------------------") + logger.debug( + "[记忆构建]" + "------------------------------------开始构建记忆--------------------------------------") start_time = time.time() await hippocampus.operation_build_memory(chat_size=20) end_time = time.time() - print( - f"\033[1;32m[记忆构建]\033[0m -------------------------------------------记忆构建完成:耗时: {end_time - start_time:.2f} 秒-------------------------------------------") + logger.success( + f"[记忆构建]--------------------------记忆构建完成:耗时: {end_time - start_time:.2f} " + "秒-------------------------------------------") @scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") From 5746afaa2aeeb77b4c3bc3ca2f65e02b821acf89 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 00:33:13 +0800 Subject: [PATCH 022/105] refactor: logger in src\plugins\chat\bot.py --- src/plugins/chat/bot.py | 103 +++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a02c4a059..a1d710122 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -31,10 +31,10 @@ class ChatBot: self._started = False self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 self.mood_manager.start_mood_update() # 启动情绪更新 - + self.emoji_chance = 0.2 # 发送表情包的基础概率 # self.message_streams = MessageStreamContainer() - + async def _ensure_started(self): """确保所有任务已启动""" if not self._started: @@ -42,26 +42,26 @@ class ChatBot: async def handle_message(self, event: GroupMessageEvent, bot: Bot) -> None: """处理收到的群消息""" - + if event.group_id not in global_config.talk_allowed_groups: return self.bot = bot # 更新 bot 实例 - + if event.user_id in global_config.ban_user_id: return group_info = await bot.get_group_info(group_id=event.group_id) sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) - - await relationship_manager.update_relationship(user_id = event.user_id, data = sender_info) - await relationship_manager.update_relationship_value(user_id = event.user_id, relationship_value = 0.5) - + + await relationship_manager.update_relationship(user_id=event.user_id, data=sender_info) + await relationship_manager.update_relationship_value(user_id=event.user_id, relationship_value=0.5) + message = Message( group_id=event.group_id, user_id=event.user_id, message_id=event.message_id, user_cardname=sender_info['card'], - raw_message=str(event.original_message), + raw_message=str(event.original_message), plain_text=event.get_plaintext(), reply_message=event.reply, ) @@ -70,26 +70,26 @@ class ChatBot: # 过滤词 for word in global_config.ban_words: if word in message.detailed_plain_text: - logger.info(f"\033[1;32m[{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}") + logger.info( + f"\033[1;32m[{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}") logger.info(f"\033[1;32m[过滤词识别]\033[0m 消息中含有{word},filtered") return - + current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.time)) - - # topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) topic = '' interested_rate = 0 - interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text)/100 - print(f"\033[1;32m[记忆激活]\033[0m 对{message.processed_plain_text}的激活度:---------------------------------------{interested_rate}\n") + interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 + logger.debug(f"\033[1;32m[记忆激活]\033[0m 对{message.processed_plain_text}" + "的激活度:---------------------------------------{interested_rate}\n") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") - + await self.storage.store_message(message, topic[0] if topic else None) is_mentioned = is_mentioned_bot_in_txt(message.processed_plain_text) reply_probability = willing_manager.change_reply_willing_received( - event.group_id, + event.group_id, topic[0] if topic else None, is_mentioned, global_config, @@ -98,25 +98,25 @@ class ChatBot: interested_rate ) current_willing = willing_manager.get_willing(event.group_id) - - - print(f"\033[1;32m[{current_time}][{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") + + logger.debug( + f"\033[1;32m[{current_time}][{message.group_name}]{message.user_nickname}:\033[0m " + "{message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * " + "100:.1f}%]\033[0m") response = "" - + if random() < reply_probability: - - tinking_time_point = round(time.time(), 2) think_id = 'mt' + str(tinking_time_point) - thinking_message = Message_Thinking(message=message,message_id=think_id) - + thinking_message = Message_Thinking(message=message, message_id=think_id) + message_manager.add_message(thinking_message) willing_manager.change_reply_willing_sent(thinking_message.group_id) - - response,raw_content = await self.gpt.generate_response(message) - + + response, raw_content = await self.gpt.generate_response(message) + if response: container = message_manager.get_container(event.group_id) thinking_message = None @@ -127,27 +127,28 @@ class ChatBot: container.messages.remove(msg) # print(f"\033[1;32m[思考消息删除]\033[0m 已找到思考消息对象,开始删除") break - + # 如果找不到思考消息,直接返回 if not thinking_message: print(f"\033[1;33m[警告]\033[0m 未找到对应的思考消息,可能已超时被移除") return - - #记录开始思考的时间,避免从思考到回复的时间太久 + + # 记录开始思考的时间,避免从思考到回复的时间太久 thinking_start_time = thinking_message.thinking_start_time - message_set = MessageSet(event.group_id, global_config.BOT_QQ, think_id) # 发送消息的id和产生发送消息的message_thinking是一致的 - #计算打字时间,1是为了模拟打字,2是避免多条回复乱序 + message_set = MessageSet(event.group_id, global_config.BOT_QQ, + think_id) # 发送消息的id和产生发送消息的message_thinking是一致的 + # 计算打字时间,1是为了模拟打字,2是避免多条回复乱序 accu_typing_time = 0 - + # print(f"\033[1;32m[开始回复]\033[0m 开始将回复1载入发送容器") mark_head = False for msg in response: # print(f"\033[1;32m[回复内容]\033[0m {msg}") - #通过时间改变时间戳 + # 通过时间改变时间戳 typing_time = calculate_typing_time(msg) accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - + bot_message = Message_Sending( group_id=event.group_id, user_id=global_config.BOT_QQ, @@ -157,8 +158,8 @@ class ChatBot: processed_plain_text=msg, user_nickname=global_config.BOT_NICKNAME, group_name=message.group_name, - time=timepoint, #记录了回复生成的时间 - thinking_start_time=thinking_start_time, #记录了思考开始的时间 + time=timepoint, # 记录了回复生成的时间 + thinking_start_time=thinking_start_time, # 记录了思考开始的时间 reply_message_id=message.message_id ) await bot_message.initialize() @@ -166,27 +167,27 @@ class ChatBot: bot_message.is_head = True mark_head = True message_set.add_message(bot_message) - - #message_set 可以直接加入 message_manager + + # message_set 可以直接加入 message_manager # print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") message_manager.add_message(message_set) - + bot_response_time = tinking_time_point if random() < global_config.emoji_chance: emoji_raw = await emoji_manager.get_emoji_for_text(response) - + # 检查是否 <没有找到> emoji if emoji_raw != None: - emoji_path,discription = emoji_raw + emoji_path, discription = emoji_raw emoji_cq = CQCode.create_emoji_cq(emoji_path) - + if random() < 0.5: bot_response_time = tinking_time_point - 1 else: bot_response_time = bot_response_time + 1 - + bot_message = Message_Sending( group_id=event.group_id, user_id=global_config.BOT_QQ, @@ -206,8 +207,8 @@ class ChatBot: await bot_message.initialize() message_manager.add_message(bot_message) emotion = await self.gpt._get_emotion_tags(raw_content) - print(f"为 '{response}' 获取到的情感标签为:{emotion}") - valuedict={ + logger.debug(f"为 '{response}' 获取到的情感标签为:{emotion}") + valuedict = { 'happy': 0.5, 'angry': -1, 'sad': -0.5, @@ -216,11 +217,13 @@ class ChatBot: 'fearful': -0.7, 'neutral': 0.1 } - await relationship_manager.update_relationship_value(message.user_id, relationship_value=valuedict[emotion[0]]) + await relationship_manager.update_relationship_value(message.user_id, + relationship_value=valuedict[emotion[0]]) # 使用情绪管理器更新情绪 self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) - + # willing_manager.change_reply_willing_after_sent(event.group_id) + # 创建全局ChatBot实例 -chat_bot = ChatBot() \ No newline at end of file +chat_bot = ChatBot() From 8661d949767de50664bde15f6736bfa1a5546cc2 Mon Sep 17 00:00:00 2001 From: CabLate Date: Mon, 10 Mar 2025 00:46:34 +0800 Subject: [PATCH 023/105] doc: README.md - telegram version information --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 575dc8232..0c02d1cba 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ **交流群**: 571780722 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 **交流群**: 1035228475 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +**其他平台版本** + +- (由 [CabLate](https://github.com/cablate) 贡献) [Telegram 与其他平台(未来可能会有)的版本](https://github.com/cablate/MaiMBot/tree/telegram) - [集中讨论串](https://github.com/SengokuCola/MaiMBot/discussions/149) + ##

📚 文档 ⬇️ 快速开始使用麦麦 ⬇️

From 052802c85161ee7bf149b1126c16dcae1bdc9169 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 00:52:04 +0800 Subject: [PATCH 024/105] refactor: logger promotion --- src/plugins/chat/__init__.py | 2 +- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/chat/relationship_manager.py | 54 +++++++++++----------- src/plugins/schedule/schedule_generator.py | 12 ++--- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 795658800..bd71be019 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -63,7 +63,7 @@ async def start_background_tasks(): # 初始化并启动情绪管理器 mood_manager = MoodManager.get_instance() mood_manager.start_mood_update(update_interval=global_config.mood_update_interval) - logger.success("[初始化]情绪管理器已启动") + logger.success("情绪管理器启动成功") # 只启动表情包管理任务 asyncio.create_task(emoji_manager.start_periodic_check(interval_MINS=global_config.EMOJI_CHECK_INTERVAL)) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 4f2637738..05c5aeada 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -302,7 +302,7 @@ class EmojiManager: # 从数据库中删除记录 result = self.db.db.emoji.delete_one({'_id': emoji['_id']}) if result.deleted_count > 0: - logger.success(f"成功删除数据库记录: {emoji['_id']}") + logger.debug(f"成功删除数据库记录: {emoji['_id']}") removed_count += 1 else: logger.error(f"删除数据库记录失败: {emoji['_id']}") diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 4ed7a2f11..4d82184c8 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,4 +1,5 @@ import asyncio +from loguru import logger from typing import Optional from ...common.database import Database @@ -8,9 +9,10 @@ class Impression: traits: str = None called: str = None know_time: float = None - + relationship_value: float = None + class Relationship: user_id: int = None # impression: Impression = None @@ -21,7 +23,7 @@ class Relationship: nickname: str = None relationship_value: float = None saved = False - + def __init__(self, user_id: int, data=None, **kwargs): if isinstance(data, dict): # 如果输入是字典,使用字典解析 @@ -39,14 +41,12 @@ class Relationship: self.nickname = kwargs.get('nickname') self.relationship_value = kwargs.get('relationship_value', 0.0) self.saved = kwargs.get('saved', False) - - class RelationshipManager: def __init__(self): - self.relationships: dict[int, Relationship] = {} - + self.relationships: dict[int, Relationship] = {} + async def update_relationship(self, user_id: int, data=None, **kwargs): # 检查是否在内存中已存在 relationship = self.relationships.get(user_id) @@ -62,7 +62,8 @@ class RelationshipManager: setattr(relationship, key, value) else: # 如果不存在,创建新对象 - relationship = Relationship(user_id, data=data) if isinstance(data, dict) else Relationship(user_id, **kwargs) + relationship = Relationship(user_id, data=data) if isinstance(data, dict) else Relationship(user_id, + **kwargs) self.relationships[user_id] = relationship # 更新 id_name_nickname_table @@ -71,9 +72,9 @@ class RelationshipManager: # 保存到数据库 await self.storage_relationship(relationship) relationship.saved = True - + return relationship - + async def update_relationship_value(self, user_id: int, **kwargs): # 检查是否在内存中已存在 relationship = self.relationships.get(user_id) @@ -85,31 +86,30 @@ class RelationshipManager: relationship.saved = True return relationship else: - print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id} 不存在,无法更新") + logger.warning(f"用户 {user_id} 不存在,无法更新") return None - - + def get_relationship(self, user_id: int) -> Optional[Relationship]: """获取用户关系对象""" if user_id in self.relationships: return self.relationships[user_id] else: return 0 - + async def load_relationship(self, data: dict) -> Relationship: - """从数据库加载或创建新的关系对象""" + """从数据库加载或创建新的关系对象""" rela = Relationship(user_id=data['user_id'], data=data) rela.saved = True self.relationships[rela.user_id] = rela return rela - + async def load_all_relationships(self): """加载所有关系对象""" db = Database.get_instance() all_relationships = db.db.relationships.find({}) for data in all_relationships: await self.load_relationship(data) - + async def _start_relationship_manager(self): """每5分钟自动保存一次关系数据""" db = Database.get_instance() @@ -119,23 +119,23 @@ class RelationshipManager: for data in all_relationships: user_id = data['user_id'] relationship = await self.load_relationship(data) - self.relationships[user_id] = relationship - print(f"\033[1;32m[关系管理]\033[0m 已加载 {len(self.relationships)} 条关系记录") - + self.relationships[user_id] = relationship + logger.debug(f"已加载 {len(self.relationships)} 条关系记录") + while True: - print("\033[1;32m[关系管理]\033[0m 正在自动保存关系") + logger.debug("正在自动保存关系") await asyncio.sleep(300) # 等待300秒(5分钟) await self._save_all_relationships() - + async def _save_all_relationships(self): - """将所有关系数据保存到数据库""" + """将所有关系数据保存到数据库""" # 保存所有关系数据 for userid, relationship in self.relationships.items(): if not relationship.saved: relationship.saved = True await self.storage_relationship(relationship) - - async def storage_relationship(self,relationship: Relationship): + + async def storage_relationship(self, relationship: Relationship): """ 将关系记录存储到数据库中 """ @@ -145,7 +145,7 @@ class RelationshipManager: gender = relationship.gender age = relationship.age saved = relationship.saved - + db = Database.get_instance() db.db.relationships.update_one( {'user_id': user_id}, @@ -158,7 +158,7 @@ class RelationshipManager: }}, upsert=True ) - + def get_name(self, user_id: int) -> str: # 确保user_id是整数类型 user_id = int(user_id) @@ -169,4 +169,4 @@ class RelationshipManager: return "某人" -relationship_manager = RelationshipManager() \ No newline at end of file +relationship_manager = RelationshipManager() diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index ffe99a2da..fc07a152d 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -57,12 +57,12 @@ class ScheduleGenerator: existing_schedule = self.db.db.schedule.find_one({"date": date_str}) if existing_schedule: - logger.info(f"{date_str}的日程已存在:") + logger.debug(f"{date_str}的日程已存在:") schedule_text = existing_schedule["schedule"] # print(self.schedule_text) - elif read_only == False: - logger.info(f"{date_str}的日程不存在,准备生成新的日程。") + elif not read_only: + logger.debug(f"{date_str}的日程不存在,准备生成新的日程。") prompt = f"""我是{global_config.BOT_NICKNAME},{global_config.PROMPT_SCHEDULE_GEN},请为我生成{date_str}({weekday})的日程安排,包括:""" + \ """ 1. 早上的学习和工作安排 @@ -78,7 +78,7 @@ class ScheduleGenerator: schedule_text = "生成日程时出错了" # print(self.schedule_text) else: - logger.info(f"{date_str}的日程不存在。") + logger.debug(f"{date_str}的日程不存在。") schedule_text = "忘了" return schedule_text, None @@ -154,10 +154,10 @@ class ScheduleGenerator: logger.warning("今日日程有误,将在下次运行时重新生成") self.db.db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) else: - logger.info("\n=== 今日日程安排 ===") + logger.info("=== 今日日程安排 ===") for time_str, activity in self.today_schedule.items(): logger.info(f"时间[{time_str}]: 活动[{activity}]") - logger.info("==================\n") + logger.info("==================") # def main(): From 8d99592b32f834afd109d62b88fe1f0809d113c9 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 01:15:31 +0800 Subject: [PATCH 025/105] =?UTF-8?q?fix:=20logger=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 + src/plugins/chat/config.py | 2 +- src/plugins/memory_system/memory.py | 272 ++++++++++++++-------------- 3 files changed, 142 insertions(+), 133 deletions(-) diff --git a/bot.py b/bot.py index 84ce5067b..c9e397724 100644 --- a/bot.py +++ b/bot.py @@ -149,6 +149,7 @@ if __name__ == "__main__": init_config() init_env() load_env() + load_logger() env_config = {key: os.getenv(key) for key in os.environ} scan_provider(env_config) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 49963ad3b..02fccc863 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -162,7 +162,7 @@ class BotConfig: personality_config = parent['personality'] personality = personality_config.get('prompt_personality') if len(personality) >= 2: - logger.info(f"载入自定义人格:{personality}") + logger.debug(f"载入自定义人格:{personality}") config.PROMPT_PERSONALITY = personality_config.get('prompt_personality', config.PROMPT_PERSONALITY) logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") config.PROMPT_SCHEDULE_GEN = personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN) diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 08fc6d30c..8e917caf4 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -7,6 +7,7 @@ import time import jieba import networkx as nx +from loguru import logger from ...common.database import Database # 使用正确的导入语法 from ..chat.config import global_config from ..chat.utils import ( @@ -22,7 +23,7 @@ class Memory_graph: def __init__(self): self.G = nx.Graph() # 使用 networkx 的图结构 self.db = Database.get_instance() - + def connect_dot(self, concept1, concept2): # 如果边已存在,增加 strength if self.G.has_edge(concept1, concept2): @@ -30,7 +31,7 @@ class Memory_graph: else: # 如果是新边,初始化 strength 为 1 self.G.add_edge(concept1, concept2, strength=1) - + def add_dot(self, concept, memory): if concept in self.G: # 如果节点已存在,将新记忆添加到现有列表中 @@ -44,7 +45,7 @@ class Memory_graph: else: # 如果是新节点,创建新的记忆列表 self.G.add_node(concept, memory_items=[memory]) - + def get_dot(self, concept): # 检查节点是否存在于图中 if concept in self.G: @@ -56,13 +57,13 @@ class Memory_graph: def get_related_item(self, topic, depth=1): if topic not in self.G: return [], [] - + first_layer_items = [] second_layer_items = [] - + # 获取相邻节点 neighbors = list(self.G.neighbors(topic)) - + # 获取当前节点的记忆项 node_data = self.get_dot(topic) if node_data: @@ -73,7 +74,7 @@ class Memory_graph: first_layer_items.extend(memory_items) else: first_layer_items.append(memory_items) - + # 只在depth=2时获取第二层记忆 if depth >= 2: # 获取相邻节点的记忆项 @@ -87,9 +88,9 @@ class Memory_graph: second_layer_items.extend(memory_items) else: second_layer_items.append(memory_items) - + return first_layer_items, second_layer_items - + @property def dots(self): # 返回所有节点对应的 Memory_dot 对象 @@ -99,43 +100,43 @@ class Memory_graph: """随机删除指定话题中的一条记忆,如果话题没有记忆则移除该话题节点""" if topic not in self.G: return None - + # 获取话题节点数据 node_data = self.G.nodes[topic] - + # 如果节点存在memory_items if 'memory_items' in node_data: memory_items = node_data['memory_items'] - + # 确保memory_items是列表 if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] - + # 如果有记忆项可以删除 if memory_items: # 随机选择一个记忆项删除 removed_item = random.choice(memory_items) memory_items.remove(removed_item) - + # 更新节点的记忆项 if memory_items: self.G.nodes[topic]['memory_items'] = memory_items else: # 如果没有记忆项了,删除整个节点 self.G.remove_node(topic) - + return removed_item - + return None # 海马体 class Hippocampus: - def __init__(self,memory_graph:Memory_graph): + def __init__(self, memory_graph: Memory_graph): self.memory_graph = memory_graph - self.llm_topic_judge = LLM_request(model = global_config.llm_topic_judge,temperature=0.5) - self.llm_summary_by_topic = LLM_request(model = global_config.llm_summary_by_topic,temperature=0.5) - + self.llm_topic_judge = LLM_request(model=global_config.llm_topic_judge, temperature=0.5) + self.llm_summary_by_topic = LLM_request(model=global_config.llm_summary_by_topic, temperature=0.5) + def get_all_node_names(self) -> list: """获取记忆图中所有节点的名字列表 @@ -156,8 +157,8 @@ class Hippocampus: """计算边的特征值""" nodes = sorted([source, target]) return hash(f"{nodes[0]}:{nodes[1]}") - - def get_memory_sample(self, chat_size=20, time_frequency:dict={'near':2,'mid':4,'far':3}): + + def get_memory_sample(self, chat_size=20, time_frequency: dict = {'near': 2, 'mid': 4, 'far': 3}): """获取记忆样本 Returns: @@ -165,26 +166,26 @@ class Hippocampus: """ current_timestamp = datetime.datetime.now().timestamp() chat_samples = [] - + # 短期:1h 中期:4h 长期:24h for _ in range(time_frequency.get('near')): random_time = current_timestamp - random.randint(1, 3600) messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) if messages: chat_samples.append(messages) - + for _ in range(time_frequency.get('mid')): - random_time = current_timestamp - random.randint(3600, 3600*4) + random_time = current_timestamp - random.randint(3600, 3600 * 4) messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) if messages: chat_samples.append(messages) - + for _ in range(time_frequency.get('far')): - random_time = current_timestamp - random.randint(3600*4, 3600*24) + random_time = current_timestamp - random.randint(3600 * 4, 3600 * 24) messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) if messages: chat_samples.append(messages) - + return chat_samples async def memory_compress(self, messages: list, compress_rate=0.1): @@ -199,17 +200,17 @@ class Hippocampus: """ if not messages: return set() - + # 合并消息文本,同时保留时间信息 input_text = "" time_info = "" # 计算最早和最晚时间 earliest_time = min(msg['time'] for msg in messages) latest_time = max(msg['time'] for msg in messages) - + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) latest_dt = datetime.datetime.fromtimestamp(latest_time) - + # 如果是同一年 if earliest_dt.year == latest_dt.year: earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") @@ -217,54 +218,56 @@ class Hippocampus: time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" else: earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") - latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" - + for msg in messages: input_text += f"{msg['text']}\n" - + print(input_text) - + topic_num = self.calculate_topic_num(input_text, compress_rate) topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(input_text, topic_num)) - + # 过滤topics filter_keywords = global_config.memory_ban_words - topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + topics = [topic.strip() for topic in + topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] - + print(f"过滤后话题: {filtered_topics}") - + # 创建所有话题的请求任务 tasks = [] for topic in filtered_topics: topic_what_prompt = self.topic_what(input_text, topic, time_info) task = self.llm_summary_by_topic.generate_response_async(topic_what_prompt) tasks.append((topic.strip(), task)) - + # 等待所有任务完成 compressed_memory = set() for topic, task in tasks: response = await task if response: compressed_memory.add((topic, response[0])) - + return compressed_memory - def calculate_topic_num(self,text, compress_rate): + def calculate_topic_num(self, text, compress_rate): """计算文本的话题数量""" information_content = calculate_information_content(text) - topic_by_length = text.count('\n')*compress_rate - topic_by_information_content = max(1, min(5, int((information_content-3) * 2))) - topic_num = int((topic_by_length + topic_by_information_content)/2) - print(f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") + topic_by_length = text.count('\n') * compress_rate + topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) + topic_num = int((topic_by_length + topic_by_information_content) / 2) + print( + f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") return topic_num - async def operation_build_memory(self,chat_size=20): + async def operation_build_memory(self, chat_size=20): # 最近消息获取频率 - time_frequency = {'near':2,'mid':4,'far':2} - memory_sample = self.get_memory_sample(chat_size,time_frequency) - + time_frequency = {'near': 2, 'mid': 4, 'far': 2} + memory_sample = self.get_memory_sample(chat_size, time_frequency) + for i, input_text in enumerate(memory_sample, 1): # 加载进度可视化 all_topics = [] @@ -279,7 +282,7 @@ class Hippocampus: compress_rate = 0.1 compressed_memory = await self.memory_compress(input_text, compress_rate) print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)}") - + # 将记忆加入到图谱中 for topic, memory in compressed_memory: print(f"\033[1;32m添加节点\033[0m: {topic}") @@ -289,7 +292,7 @@ class Hippocampus: for j in range(i + 1, len(all_topics)): print(f"\033[1;32m连接节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") self.memory_graph.connect_dot(all_topics[i], all_topics[j]) - + self.sync_memory_to_db() def sync_memory_to_db(self): @@ -297,19 +300,19 @@ class Hippocampus: # 获取数据库中所有节点和内存中所有节点 db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) memory_nodes = list(self.memory_graph.G.nodes(data=True)) - + # 转换数据库节点为字典格式,方便查找 db_nodes_dict = {node['concept']: node for node in db_nodes} - + # 检查并更新节点 for concept, data in memory_nodes: memory_items = data.get('memory_items', []) if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] - + # 计算内存中节点的特征值 memory_hash = self.calculate_node_hash(concept, memory_items) - + if concept not in db_nodes_dict: # 数据库中缺少的节点,添加 node_data = { @@ -322,7 +325,7 @@ class Hippocampus: # 获取数据库中节点的特征值 db_node = db_nodes_dict[concept] db_hash = db_node.get('hash', None) - + # 如果特征值不同,则更新节点 if db_hash != memory_hash: self.memory_graph.db.db.graph_data.nodes.update_one( @@ -332,17 +335,17 @@ class Hippocampus: 'hash': memory_hash }} ) - + # 检查并删除数据库中多余的节点 memory_concepts = set(node[0] for node in memory_nodes) for db_node in db_nodes: if db_node['concept'] not in memory_concepts: self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) - + # 处理边的信息 db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) memory_edges = list(self.memory_graph.G.edges()) - + # 创建边的哈希值字典 db_edge_dict = {} for edge in db_edges: @@ -351,13 +354,13 @@ class Hippocampus: 'hash': edge_hash, 'strength': edge.get('strength', 1) } - + # 检查并更新边 for source, target in memory_edges: edge_hash = self.calculate_edge_hash(source, target) edge_key = (source, target) strength = self.memory_graph.G[source][target].get('strength', 1) - + if edge_key not in db_edge_dict: # 添加新边 edge_data = { @@ -377,7 +380,7 @@ class Hippocampus: 'strength': strength }} ) - + # 删除多余的边 memory_edge_set = set(memory_edges) for edge_key in db_edge_dict: @@ -392,7 +395,7 @@ class Hippocampus: """从数据库同步数据到内存中的图结构""" # 清空当前图 self.memory_graph.G.clear() - + # 从数据库加载所有节点 nodes = self.memory_graph.db.db.graph_data.nodes.find() for node in nodes: @@ -403,7 +406,7 @@ class Hippocampus: memory_items = [memory_items] if memory_items else [] # 添加节点到图中 self.memory_graph.G.add_node(concept, memory_items=memory_items) - + # 从数据库加载所有边 edges = self.memory_graph.db.db.graph_data.edges.find() for edge in edges: @@ -413,7 +416,7 @@ class Hippocampus: # 只有当源节点和目标节点都存在时才添加边 if source in self.memory_graph.G and target in self.memory_graph.G: self.memory_graph.G.add_edge(source, target, strength=strength) - + async def operation_forget_topic(self, percentage=0.1): """随机选择图中一定比例的节点进行检查,根据条件决定是否遗忘""" # 获取所有节点 @@ -422,18 +425,18 @@ class Hippocampus: check_count = max(1, int(len(all_nodes) * percentage)) # 随机选择节点 nodes_to_check = random.sample(all_nodes, check_count) - + forgotten_nodes = [] for node in nodes_to_check: # 获取节点的连接数 connections = self.memory_graph.G.degree(node) - + # 获取节点的内容条数 memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] content_count = len(memory_items) - + # 检查连接强度 weak_connections = True if connections > 1: # 只有当连接数大于1时才检查强度 @@ -442,14 +445,14 @@ class Hippocampus: if strength > 2: weak_connections = False break - + # 如果满足遗忘条件 if (connections <= 1 and weak_connections) or content_count <= 2: removed_item = self.memory_graph.forget_topic(node) if removed_item: forgotten_nodes.append((node, removed_item)) print(f"遗忘节点 {node} 的记忆: {removed_item}") - + # 同步到数据库 if forgotten_nodes: self.sync_memory_to_db() @@ -468,35 +471,35 @@ class Hippocampus: memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] - + # 如果记忆项不足,直接返回 if len(memory_items) < 10: return - + # 随机选择10条记忆 selected_memories = random.sample(memory_items, 10) - + # 拼接成文本 merged_text = "\n".join(selected_memories) print(f"\n[合并记忆] 话题: {topic}") print(f"选择的记忆:\n{merged_text}") - + # 使用memory_compress生成新的压缩记忆 compressed_memories = await self.memory_compress(selected_memories, 0.1) - + # 从原记忆列表中移除被选中的记忆 for memory in selected_memories: memory_items.remove(memory) - + # 添加新的压缩记忆 for _, compressed_memory in compressed_memories: memory_items.append(compressed_memory) print(f"添加压缩记忆: {compressed_memory}") - + # 更新节点的记忆项 self.memory_graph.G.nodes[topic]['memory_items'] = memory_items print(f"完成记忆合并,当前记忆数量: {len(memory_items)}") - + async def operation_merge_memory(self, percentage=0.1): """ 随机检查一定比例的节点,对内容数量超过100的节点进行记忆合并 @@ -510,7 +513,7 @@ class Hippocampus: check_count = max(1, int(len(all_nodes) * percentage)) # 随机选择节点 nodes_to_check = random.sample(all_nodes, check_count) - + merged_nodes = [] for node in nodes_to_check: # 获取节点的内容条数 @@ -518,13 +521,13 @@ class Hippocampus: if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] content_count = len(memory_items) - + # 如果内容数量超过100,进行合并 if content_count > 100: print(f"\n检查节点: {node}, 当前记忆数量: {content_count}") await self.merge_memory(node) merged_nodes.append(node) - + # 同步到数据库 if merged_nodes: self.sync_memory_to_db() @@ -532,11 +535,11 @@ class Hippocampus: else: print("\n本次检查没有需要合并的节点") - def find_topic_llm(self,text, topic_num): + def find_topic_llm(self, text, topic_num): prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' return prompt - def topic_what(self,text, topic, time_info): + def topic_what(self, text, topic, time_info): prompt = f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' return prompt @@ -551,11 +554,12 @@ class Hippocampus: """ topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, 5)) # print(f"话题: {topics_response[0]}") - topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + topics = [topic.strip() for topic in + topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] # print(f"话题: {topics}") - + return topics - + def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: """查找与给定主题相似的记忆主题 @@ -569,16 +573,16 @@ class Hippocampus: """ all_memory_topics = self.get_all_node_names() all_similar_topics = [] - + # 计算每个识别出的主题与记忆主题的相似度 for topic in topics: if debug_info: # print(f"\033[1;32m[{debug_info}]\033[0m 正在思考有没有见过: {topic}") pass - + topic_vector = text_to_vector(topic) has_similar_topic = False - + for memory_topic in all_memory_topics: memory_vector = text_to_vector(memory_topic) # 获取所有唯一词 @@ -588,20 +592,20 @@ class Hippocampus: v2 = [memory_vector.get(word, 0) for word in all_words] # 计算相似度 similarity = cosine_similarity(v1, v2) - + if similarity >= similarity_threshold: has_similar_topic = True if debug_info: # print(f"\033[1;32m[{debug_info}]\033[0m 找到相似主题: {topic} -> {memory_topic} (相似度: {similarity:.2f})") pass all_similar_topics.append((memory_topic, similarity)) - + if not has_similar_topic and debug_info: # print(f"\033[1;31m[{debug_info}]\033[0m 没有见过: {topic} ,呃呃") pass - + return all_similar_topics - + def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: """获取相似度最高的主题 @@ -614,36 +618,36 @@ class Hippocampus: """ seen_topics = set() top_topics = [] - + for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): if topic not in seen_topics and len(top_topics) < max_topics: seen_topics.add(topic) top_topics.append((topic, score)) - + return top_topics async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: """计算输入文本对记忆的激活程度""" print(f"\033[1;32m[记忆激活]\033[0m 识别主题: {await self._identify_topics(text)}") - + # 识别主题 identified_topics = await self._identify_topics(text) if not identified_topics: return 0 - + # 查找相似主题 all_similar_topics = self._find_similar_topics( - identified_topics, + identified_topics, similarity_threshold=similarity_threshold, debug_info="记忆激活" ) - + if not all_similar_topics: return 0 - + # 获取最相关的主题 top_topics = self._get_top_topics(all_similar_topics, max_topics) - + # 如果只找到一个主题,进行惩罚 if len(top_topics) == 1: topic, score = top_topics[0] @@ -653,15 +657,16 @@ class Hippocampus: memory_items = [memory_items] if memory_items else [] content_count = len(memory_items) penalty = 1.0 / (1 + math.log(content_count + 1)) - + activation = int(score * 50 * penalty) - print(f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") + print( + f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") return activation - + # 计算关键词匹配率,同时考虑内容数量 matched_topics = set() topic_similarities = {} - + for memory_topic, similarity in top_topics: # 计算内容数量惩罚 memory_items = self.memory_graph.G.nodes[memory_topic].get('memory_items', []) @@ -669,7 +674,7 @@ class Hippocampus: memory_items = [memory_items] if memory_items else [] content_count = len(memory_items) penalty = 1.0 / (1 + math.log(content_count + 1)) - + # 对每个记忆主题,检查它与哪些输入主题相似 for input_topic in identified_topics: topic_vector = text_to_vector(input_topic) @@ -682,33 +687,36 @@ class Hippocampus: matched_topics.add(input_topic) adjusted_sim = sim * penalty topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) - print(f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") - + print( + f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") + # 计算主题匹配率和平均相似度 topic_match = len(matched_topics) / len(identified_topics) average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 - + # 计算最终激活值 activation = int((topic_match + average_similarities) / 2 * 100) - print(f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") - + print( + f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") + return activation - async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5) -> list: + async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, + max_memory_num: int = 5) -> list: """根据输入文本获取相关的记忆内容""" # 识别主题 identified_topics = await self._identify_topics(text) - + # 查找相似主题 all_similar_topics = self._find_similar_topics( - identified_topics, + identified_topics, similarity_threshold=similarity_threshold, debug_info="记忆检索" ) - + # 获取最相关的主题 relevant_topics = self._get_top_topics(all_similar_topics, max_topics) - + # 获取相关记忆内容 relevant_memories = [] for topic, score in relevant_topics: @@ -716,8 +724,8 @@ class Hippocampus: first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) if first_layer: # 如果记忆条数超过限制,随机选择指定数量的记忆 - if len(first_layer) > max_memory_num/2: - first_layer = random.sample(first_layer, max_memory_num//2) + if len(first_layer) > max_memory_num / 2: + first_layer = random.sample(first_layer, max_memory_num // 2) # 为每条记忆添加来源主题和相似度信息 for memory in first_layer: relevant_memories.append({ @@ -725,20 +733,20 @@ class Hippocampus: 'similarity': score, 'content': memory }) - + # 如果记忆数量超过5个,随机选择5个 # 按相似度排序 relevant_memories.sort(key=lambda x: x['similarity'], reverse=True) - + if len(relevant_memories) > max_memory_num: relevant_memories = random.sample(relevant_memories, max_memory_num) - + return relevant_memories def segment_text(text): seg_text = list(jieba.cut(text)) - return seg_text + return seg_text from nonebot import get_driver @@ -749,19 +757,19 @@ config = driver.config start_time = time.time() Database.initialize( - host= config.MONGODB_HOST, - port= config.MONGODB_PORT, - db_name= config.DATABASE_NAME, - username= config.MONGODB_USERNAME, - password= config.MONGODB_PASSWORD, + host=config.MONGODB_HOST, + port=config.MONGODB_PORT, + db_name=config.DATABASE_NAME, + username=config.MONGODB_USERNAME, + password=config.MONGODB_PASSWORD, auth_source=config.MONGODB_AUTH_SOURCE ) -#创建记忆图 +# 创建记忆图 memory_graph = Memory_graph() -#创建海马体 +# 创建海马体 hippocampus = Hippocampus(memory_graph) -#从数据库加载记忆图 +# 从数据库加载记忆图 hippocampus.sync_memory_from_db() end_time = time.time() -print(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") \ No newline at end of file +logger.success(f"加载海马体耗时: {end_time - start_time:.2f} 秒") From 536bb1d9238db77e79446b3a8f1684663679f53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Mon, 10 Mar 2025 02:26:13 +0900 Subject: [PATCH 026/105] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=83=85?= =?UTF-8?q?=E6=84=9F=E5=88=A4=E6=96=AD=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 4f2637738..614bfe025 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -33,7 +33,7 @@ class EmojiManager: self.db = Database.get_instance() self._scan_task = None self.vlm = LLM_request(model=global_config.vlm, temperature=0.3, max_tokens=1000) - self.llm_emotion_judge = LLM_request(model=global_config.llm_normal_minor, max_tokens=60,temperature=0.8) #更高的温度,更少的token(后续可以根据情绪来调整温度) + self.llm_emotion_judge = LLM_request(model=global_config.llm_emotion_judge, max_tokens=60,temperature=0.8) #更高的温度,更少的token(后续可以根据情绪来调整温度) def _ensure_emoji_dir(self): """确保表情存储目录存在""" @@ -330,4 +330,4 @@ class EmojiManager: # 创建全局单例 -emoji_manager = EmojiManager() \ No newline at end of file +emoji_manager = EmojiManager() From a7f9d05c010689c5137a0a5007a6b112ad4a4056 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 10 Mar 2025 01:40:06 +0800 Subject: [PATCH 027/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BC=A0=E5=85=A5=E6=A0=BC=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils.py | 40 ++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index d166bcd27..f8331a148 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -98,37 +98,43 @@ def calculate_information_content(text): def get_cloest_chat_from_db(db, length: int, timestamp: str): - """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数""" - chat_text = '' + """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 + + Returns: + list: 消息记录字典列表,每个字典包含消息内容和时间信息 + """ + chat_records = [] closest_record = db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) - - if closest_record and closest_record.get('memorized', 0) < 4: + + if closest_record and closest_record.get('memorized', 0) < 4: closest_time = closest_record['time'] - group_id = closest_record['group_id'] # 获取groupid + group_id = closest_record['group_id'] # 获取该时间戳之后的length条消息,且groupid相同 - chat_records = list(db.db.messages.find( + records = list(db.db.messages.find( {"time": {"$gt": closest_time}, "group_id": group_id} ).sort('time', 1).limit(length)) - + # 更新每条消息的memorized属性 - for record in chat_records: - # 检查当前记录的memorized值 + for record in records: current_memorized = record.get('memorized', 0) if current_memorized > 3: - # print(f"消息已读取3次,跳过") + print("消息已读取3次,跳过") return '' - + # 更新memorized值 db.db.messages.update_one( {"_id": record["_id"]}, {"$set": {"memorized": current_memorized + 1}} ) - - chat_text += record["detailed_plain_text"] - - return chat_text - # print(f"消息已读取3次,跳过") - return '' + + # 添加到记录列表中 + chat_records.append({ + 'text': record["detailed_plain_text"], + 'time': record["time"], + 'group_id': record["group_id"] + }) + + return chat_records async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: From 052e67b5762e6b6db6ab99dec98caf12379ff5e6 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 02:25:03 +0800 Subject: [PATCH 028/105] =?UTF-8?q?refactor:=20=E6=97=A5=E5=BF=97=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E4=BC=98=E5=8C=96=EF=BC=88=E7=BB=88=E4=BA=8E=E6=94=B9?= =?UTF-8?q?=E5=AE=8C=E4=BA=86=EF=BC=8C=E7=88=BD=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 2 +- src/plugins/chat/bot.py | 17 +- src/plugins/chat/cq_code.py | 17 +- src/plugins/chat/emoji_manager.py | 114 +++++----- src/plugins/chat/llm_generator.py | 15 +- src/plugins/chat/message_sender.py | 142 ++++++------ src/plugins/chat/prompt_builder.py | 207 +++++++++--------- src/plugins/chat/topic_identifier.py | 19 +- src/plugins/chat/utils.py | 27 ++- src/plugins/chat/willing_manager.py | 9 +- src/plugins/memory_system/memory.py | 51 ++--- .../memory_system/memory_manual_build.py | 2 +- src/plugins/models/utils_model.py | 68 +++--- src/plugins/moods/moods.py | 4 +- src/test/typo.py | 149 +++++++------ 15 files changed, 431 insertions(+), 412 deletions(-) diff --git a/bot.py b/bot.py index c9e397724..c2ed3dfdf 100644 --- a/bot.py +++ b/bot.py @@ -100,7 +100,7 @@ def load_logger(): "#777777>| {name:.<8}:{function:.<8}:{line: >4} - {message}", colorize=True, - level=os.getenv("LOG_LEVEL", "INFO") # 根据环境设置日志级别,默认为INFO + level=os.getenv("LOG_LEVEL", "DEBUG") # 根据环境设置日志级别,默认为INFO ) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a1d710122..c510fe4bf 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -71,8 +71,8 @@ class ChatBot: for word in global_config.ban_words: if word in message.detailed_plain_text: logger.info( - f"\033[1;32m[{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}") - logger.info(f"\033[1;32m[过滤词识别]\033[0m 消息中含有{word},filtered") + f"[{message.group_name}]{message.user_nickname}:{message.processed_plain_text}") + logger.info(f"[过滤词识别]消息中含有{word},filtered") return current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.time)) @@ -81,8 +81,8 @@ class ChatBot: topic = '' interested_rate = 0 interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 - logger.debug(f"\033[1;32m[记忆激活]\033[0m 对{message.processed_plain_text}" - "的激活度:---------------------------------------{interested_rate}\n") + logger.debug(f"对{message.processed_plain_text}" + f"的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") await self.storage.store_message(message, topic[0] if topic else None) @@ -99,10 +99,9 @@ class ChatBot: ) current_willing = willing_manager.get_willing(event.group_id) - logger.debug( - f"\033[1;32m[{current_time}][{message.group_name}]{message.user_nickname}:\033[0m " - "{message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * " - "100:.1f}%]\033[0m") + logger.info( + f"[{current_time}][{message.group_name}]{message.user_nickname}:" + f"{message.processed_plain_text}[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]") response = "" @@ -130,7 +129,7 @@ class ChatBot: # 如果找不到思考消息,直接返回 if not thinking_message: - print(f"\033[1;33m[警告]\033[0m 未找到对应的思考消息,可能已超时被移除") + logger.warning(f"未找到对应的思考消息,可能已超时被移除") return # 记录开始思考的时间,避免从思考到回复的时间太久 diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index 4a295e3d5..b13e33e48 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -4,6 +4,7 @@ import os import time from dataclasses import dataclass from typing import Dict, Optional +from loguru import logger import requests @@ -151,11 +152,11 @@ class CQCode: except (requests.exceptions.SSLError, requests.exceptions.HTTPError) as e: if retry == max_retries - 1: - print(f"\033[1;31m[致命错误]\033[0m 最终请求失败: {str(e)}") + logger.error(f"最终请求失败: {str(e)}") time.sleep(1.5 ** retry) # 指数退避 except Exception as e: - print(f"\033[1;33m[未知错误]\033[0m {str(e)}") + logger.exception(f"[未知错误]") return None return None @@ -194,7 +195,7 @@ class CQCode: description, _ = await self._llm.generate_response_for_image(prompt, image_base64) return f"[表情包:{description}]" except Exception as e: - print(f"\033[1;31m[错误]\033[0m AI接口调用失败: {str(e)}") + logger.exception(f"AI接口调用失败: {str(e)}") return "[表情包]" async def get_image_description(self, image_base64: str) -> str: @@ -205,7 +206,7 @@ class CQCode: description, _ = await self._llm.generate_response_for_image(prompt, image_base64) return f"[图片:{description}]" except Exception as e: - print(f"\033[1;31m[错误]\033[0m AI接口调用失败: {str(e)}") + logger.exception(f"AI接口调用失败: {str(e)}") return "[图片]" async def translate_forward(self) -> str: @@ -222,7 +223,7 @@ class CQCode: try: messages = ast.literal_eval(content) except ValueError as e: - print(f"\033[1;31m[错误]\033[0m 解析转发消息内容失败: {str(e)}") + logger.error(f"解析转发消息内容失败: {str(e)}") return '[转发消息]' # 处理每条消息 @@ -277,11 +278,11 @@ class CQCode: # 合并所有消息 combined_messages = '\n'.join(formatted_messages) - print(f"\033[1;34m[调试信息]\033[0m 合并后的转发消息: {combined_messages}") + logger.debug(f"合并后的转发消息: {combined_messages}") return f"[转发消息:\n{combined_messages}]" except Exception as e: - print(f"\033[1;31m[错误]\033[0m 处理转发消息失败: {str(e)}") + logger.exception("处理转发消息失败") return '[转发消息]' async def translate_reply(self) -> str: @@ -307,7 +308,7 @@ class CQCode: return f"[回复 {self.reply_message.sender.nickname} 的消息: {message_obj.processed_plain_text}]" else: - print("\033[1;31m[错误]\033[0m 回复消息的sender.user_id为空") + logger.error("回复消息的sender.user_id为空") return '[回复某人消息]' @staticmethod diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 05c5aeada..eb1ff281a 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -21,24 +21,25 @@ config = driver.config class EmojiManager: _instance = None EMOJI_DIR = "data/emoji" # 表情包存储目录 - + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.db = None cls._instance._initialized = False return cls._instance - + def __init__(self): self.db = Database.get_instance() self._scan_task = None self.vlm = LLM_request(model=global_config.vlm, temperature=0.3, max_tokens=1000) - self.llm_emotion_judge = LLM_request(model=global_config.llm_normal_minor, max_tokens=60,temperature=0.8) #更高的温度,更少的token(后续可以根据情绪来调整温度) - + self.llm_emotion_judge = LLM_request(model=global_config.llm_normal_minor, max_tokens=60, + temperature=0.8) # 更高的温度,更少的token(后续可以根据情绪来调整温度) + def _ensure_emoji_dir(self): """确保表情存储目录存在""" os.makedirs(self.EMOJI_DIR, exist_ok=True) - + def initialize(self): """初始化数据库连接和表情目录""" if not self._initialized: @@ -50,15 +51,15 @@ class EmojiManager: # 启动时执行一次完整性检查 self.check_emoji_file_integrity() except Exception as e: - logger.error(f"初始化表情管理器失败: {str(e)}") - + logger.exception(f"初始化表情管理器失败") + def _ensure_db(self): """确保数据库已初始化""" if not self._initialized: self.initialize() if not self._initialized: raise RuntimeError("EmojiManager not initialized") - + def _ensure_emoji_collection(self): """确保emoji集合存在并创建索引 @@ -76,7 +77,7 @@ class EmojiManager: self.db.db.emoji.create_index([('embedding', '2dsphere')]) self.db.db.emoji.create_index([('tags', 1)]) self.db.db.emoji.create_index([('filename', 1)], unique=True) - + def record_usage(self, emoji_id: str): """记录表情使用次数""" try: @@ -86,8 +87,8 @@ class EmojiManager: {'$inc': {'usage_count': 1}} ) except Exception as e: - logger.error(f"记录表情使用失败: {str(e)}") - + logger.exception(f"记录表情使用失败") + async def get_emoji_for_text(self, text: str) -> Optional[str]: """根据文本内容获取相关表情包 Args: @@ -102,9 +103,9 @@ class EmojiManager: """ try: self._ensure_db() - + # 获取文本的embedding - text_for_search= await self._get_kimoji_for_text(text) + text_for_search = await self._get_kimoji_for_text(text) if not text_for_search: logger.error("无法获取文本的情绪") return None @@ -112,15 +113,15 @@ class EmojiManager: if not text_embedding: logger.error("无法获取文本的embedding") return None - + try: # 获取所有表情包 all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'discription': 1})) - + if not all_emojis: logger.warning("数据库中没有任何表情包") return None - + # 计算余弦相似度并排序 def cosine_similarity(v1, v2): if not v1 or not v2: @@ -131,42 +132,43 @@ class EmojiManager: if norm_v1 == 0 or norm_v2 == 0: return 0 return dot_product / (norm_v1 * norm_v2) - + # 计算所有表情包与输入文本的相似度 emoji_similarities = [ (emoji, cosine_similarity(text_embedding, emoji.get('embedding', []))) for emoji in all_emojis ] - + # 按相似度降序排序 emoji_similarities.sort(key=lambda x: x[1], reverse=True) - + # 获取前3个最相似的表情包 top_3_emojis = emoji_similarities[:3] - + if not top_3_emojis: logger.warning("未找到匹配的表情包") return None - + # 从前3个中随机选择一个 selected_emoji, similarity = random.choice(top_3_emojis) - + if selected_emoji and 'path' in selected_emoji: # 更新使用次数 self.db.db.emoji.update_one( {'_id': selected_emoji['_id']}, {'$inc': {'usage_count': 1}} ) - logger.success(f"找到匹配的表情包: {selected_emoji.get('discription', '无描述')} (相似度: {similarity:.4f})") + logger.success( + f"找到匹配的表情包: {selected_emoji.get('discription', '无描述')} (相似度: {similarity:.4f})") # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 - return selected_emoji['path'],"[ %s ]" % selected_emoji.get('discription', '无描述') - + return selected_emoji['path'], "[ %s ]" % selected_emoji.get('discription', '无描述') + except Exception as search_error: logger.error(f"搜索表情包失败: {str(search_error)}") return None - + return None - + except Exception as e: logger.error(f"获取表情包失败: {str(e)}") return None @@ -175,39 +177,39 @@ class EmojiManager: """获取表情包的标签""" try: prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感' - + content, _ = await self.vlm.generate_response_for_image(prompt, image_base64) logger.debug(f"输出描述: {content}") return content - + except Exception as e: logger.error(f"获取标签失败: {str(e)}") return None - + async def _check_emoji(self, image_base64: str) -> str: try: prompt = f'这是一个表情包,请回答这个表情包是否满足\"{global_config.EMOJI_CHECK_PROMPT}\"的要求,是则回答是,否则回答否,不要出现任何其他内容' - + content, _ = await self.vlm.generate_response_for_image(prompt, image_base64) logger.debug(f"输出描述: {content}") return content - + except Exception as e: logger.error(f"获取标签失败: {str(e)}") return None - - async def _get_kimoji_for_text(self, text:str): + + async def _get_kimoji_for_text(self, text: str): try: prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。' - + content, _ = await self.llm_emotion_judge.generate_response_async(prompt) logger.info(f"输出描述: {content}") return content - + except Exception as e: logger.error(f"获取标签失败: {str(e)}") return None - + async def scan_new_emojis(self): """扫描新的表情包""" try: @@ -215,22 +217,23 @@ class EmojiManager: os.makedirs(emoji_dir, exist_ok=True) # 获取所有支持的图片文件 - files_to_process = [f for f in os.listdir(emoji_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] - + files_to_process = [f for f in os.listdir(emoji_dir) if + f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] + for filename in files_to_process: image_path = os.path.join(emoji_dir, filename) - + # 检查是否已经注册过 existing_emoji = self.db.db['emoji'].find_one({'filename': filename}) if existing_emoji: continue - + # 压缩图片并获取base64编码 image_base64 = image_path_to_base64(image_path) if image_base64 is None: os.remove(image_path) continue - + # 获取表情包的描述 discription = await self._get_emoji_discription(image_base64) if global_config.EMOJI_CHECK: @@ -247,30 +250,28 @@ class EmojiManager: emoji_record = { 'filename': filename, 'path': image_path, - 'embedding':embedding, + 'embedding': embedding, 'discription': discription, 'timestamp': int(time.time()) } - + # 保存到数据库 self.db.db['emoji'].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") logger.info(f"描述: {discription}") else: logger.warning(f"跳过表情包: {filename}") - + except Exception as e: - logger.error(f"扫描表情包失败: {str(e)}") - logger.error(traceback.format_exc()) - + logger.exception(f"扫描表情包失败") + async def _periodic_scan(self, interval_MINS: int = 10): """定期扫描新表情包""" while True: - print("\033[1;36m[表情包]\033[0m 开始扫描新表情包...") + logger.info("开始扫描新表情包...") await self.scan_new_emojis() await asyncio.sleep(interval_MINS * 60) # 每600秒扫描一次 - def check_emoji_file_integrity(self): """检查表情包文件完整性 如果文件已被删除,则从数据库中移除对应记录 @@ -281,7 +282,7 @@ class EmojiManager: all_emojis = list(self.db.db.emoji.find()) removed_count = 0 total_count = len(all_emojis) - + for emoji in all_emojis: try: if 'path' not in emoji: @@ -289,13 +290,13 @@ class EmojiManager: self.db.db.emoji.delete_one({'_id': emoji['_id']}) removed_count += 1 continue - + if 'embedding' not in emoji: logger.warning(f"发现过时记录(缺少embedding字段),ID: {emoji.get('_id', 'unknown')}") self.db.db.emoji.delete_one({'_id': emoji['_id']}) removed_count += 1 continue - + # 检查文件是否存在 if not os.path.exists(emoji['path']): logger.warning(f"表情包文件已被删除: {emoji['path']}") @@ -309,7 +310,7 @@ class EmojiManager: except Exception as item_error: logger.error(f"处理表情包记录时出错: {str(item_error)}") continue - + # 验证清理结果 remaining_count = self.db.db.emoji.count_documents({}) if removed_count > 0: @@ -317,7 +318,7 @@ class EmojiManager: logger.info(f"清理前总数: {total_count} | 清理后总数: {remaining_count}") else: logger.info(f"已检查 {total_count} 个表情包记录") - + except Exception as e: logger.error(f"检查表情包完整性失败: {str(e)}") logger.error(traceback.format_exc()) @@ -328,6 +329,5 @@ class EmojiManager: await asyncio.sleep(interval_MINS * 60) - # 创建全局单例 -emoji_manager = EmojiManager() \ No newline at end of file +emoji_manager = EmojiManager() diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 1ac421e6b..1236bb2e0 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -3,6 +3,7 @@ import time from typing import List, Optional, Tuple, Union from nonebot import get_driver +from loguru import logger from ...common.database import Database from ..models.utils_model import LLM_request @@ -39,13 +40,13 @@ class ResponseGenerator: self.current_model_type = 'r1_distill' current_model = self.model_r1_distill - print(f"+++++++++++++++++{global_config.BOT_NICKNAME}{self.current_model_type}思考中+++++++++++++++++") + logger.info(f"{global_config.BOT_NICKNAME}{self.current_model_type}思考中") model_response = await self._generate_response_with_model(message, current_model) raw_content=model_response if model_response: - print(f'{global_config.BOT_NICKNAME}的回复是:{model_response}') + logger.info(f'{global_config.BOT_NICKNAME}的回复是:{model_response}') model_response = await self._process_response(model_response) if model_response: @@ -93,7 +94,7 @@ class ResponseGenerator: try: content, reasoning_content = await model.generate_response(prompt) except Exception as e: - print(f"生成回复时出错: {e}") + logger.exception(f"生成回复时出错: {e}") return None # 保存到数据库 @@ -145,7 +146,7 @@ class ResponseGenerator: return ["neutral"] except Exception as e: - print(f"获取情感标签时出错: {e}") + logger.exception(f"获取情感标签时出错: {e}") return ["neutral"] async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: @@ -172,7 +173,7 @@ class InitiativeMessageGenerate: prompt_builder._build_initiative_prompt_select(message.group_id) ) content_select, reasoning = self.model_v3.generate_response(topic_select_prompt) - print(f"[DEBUG] {content_select} {reasoning}") + logger.debug(f"{content_select} {reasoning}") topics_list = [dot[0] for dot in dots_for_select] if content_select: if content_select in topics_list: @@ -185,12 +186,12 @@ class InitiativeMessageGenerate: select_dot[1], prompt_template ) content_check, reasoning_check = self.model_v3.generate_response(prompt_check) - print(f"[DEBUG] {content_check} {reasoning_check}") + logger.info(f"{content_check} {reasoning_check}") if "yes" not in content_check.lower(): return None prompt = prompt_builder._build_initiative_prompt( select_dot, prompt_template, memory ) content, reasoning = self.model_r1.generate_response_async(prompt) - print(f"[DEBUG] {content} {reasoning}") + logger.debug(f"[DEBUG] {content} {reasoning}") return content diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 050c59d74..b9506b455 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -2,6 +2,7 @@ import asyncio import time from typing import Dict, List, Optional, Union +from loguru import logger from nonebot.adapters.onebot.v11 import Bot from .cq_code import cq_code_tool @@ -13,45 +14,45 @@ from .config import global_config class Message_Sender: """发送器""" + def __init__(self): self.message_interval = (0.5, 1) # 消息间隔时间范围(秒) self.last_send_time = 0 self._current_bot = None - + def set_bot(self, bot: Bot): """设置当前bot实例""" self._current_bot = bot - + async def send_group_message( - self, - group_id: int, - send_text: str, - auto_escape: bool = False, - reply_message_id: int = None, - at_user_id: int = None + self, + group_id: int, + send_text: str, + auto_escape: bool = False, + reply_message_id: int = None, + at_user_id: int = None ) -> None: if not self._current_bot: raise RuntimeError("Bot未设置,请先调用set_bot方法设置bot实例") - + message = send_text - + # 如果需要回复 if reply_message_id: reply_cq = cq_code_tool.create_reply_cq(reply_message_id) message = reply_cq + message - + # 如果需要at # if at_user_id: # at_cq = cq_code_tool.create_at_cq(at_user_id) # message = at_cq + " " + message - - + typing_time = calculate_typing_time(message) if typing_time > 10: typing_time = 10 await asyncio.sleep(typing_time) - + # 发送消息 try: await self._current_bot.send_group_msg( @@ -59,49 +60,49 @@ class Message_Sender: message=message, auto_escape=auto_escape ) - print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + logger.debug(f"发送消息{message}成功") except Exception as e: - print(f"发生错误 {e}") - print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + logger.exception(f"发送消息{message}失败") class MessageContainer: """单个群的发送/思考消息容器""" + def __init__(self, group_id: int, max_size: int = 100): self.group_id = group_id self.max_size = max_size self.messages = [] self.last_send_time = 0 self.thinking_timeout = 20 # 思考超时时间(秒) - + def get_timeout_messages(self) -> List[Message_Sending]: """获取所有超时的Message_Sending对象(思考时间超过30秒),按thinking_start_time排序""" current_time = time.time() timeout_messages = [] - + for msg in self.messages: if isinstance(msg, Message_Sending): if current_time - msg.thinking_start_time > self.thinking_timeout: timeout_messages.append(msg) - + # 按thinking_start_time排序,时间早的在前面 timeout_messages.sort(key=lambda x: x.thinking_start_time) - + return timeout_messages - + def get_earliest_message(self) -> Optional[Union[Message_Thinking, Message_Sending]]: """获取thinking_start_time最早的消息对象""" if not self.messages: return None earliest_time = float('inf') earliest_message = None - for msg in self.messages: + for msg in self.messages: msg_time = msg.thinking_start_time if msg_time < earliest_time: earliest_time = msg_time - earliest_message = msg + earliest_message = msg return earliest_message - + def add_message(self, message: Union[Message_Thinking, Message_Sending]) -> None: """添加消息到队列""" # print(f"\033[1;32m[添加消息]\033[0m 添加消息到对应群") @@ -110,7 +111,7 @@ class MessageContainer: self.messages.append(single_message) else: self.messages.append(message) - + def remove_message(self, message: Union[Message_Thinking, Message_Sending]) -> bool: """移除消息,如果消息存在则返回True,否则返回False""" try: @@ -119,97 +120,103 @@ class MessageContainer: return True return False except Exception as e: - print(f"\033[1;31m[错误]\033[0m 移除消息时发生错误: {e}") + logger.exception(f"移除消息时发生错误: {e}") return False - + def has_messages(self) -> bool: """检查是否有待发送的消息""" return bool(self.messages) - + def get_all_messages(self) -> List[Union[Message, Message_Thinking]]: """获取所有消息""" return list(self.messages) - + class MessageManager: """管理所有群的消息容器""" + def __init__(self): self.containers: Dict[int, MessageContainer] = {} self.storage = MessageStorage() self._running = True - + def get_container(self, group_id: int) -> MessageContainer: """获取或创建群的消息容器""" if group_id not in self.containers: self.containers[group_id] = MessageContainer(group_id) return self.containers[group_id] - + def add_message(self, message: Union[Message_Thinking, Message_Sending, MessageSet]) -> None: container = self.get_container(message.group_id) container.add_message(message) - + async def process_group_messages(self, group_id: int): """处理群消息""" # if int(time.time() / 3) == time.time() / 3: - # print(f"\033[1;34m[调试]\033[0m 开始处理群{group_id}的消息") + # print(f"\033[1;34m[调试]\033[0m 开始处理群{group_id}的消息") container = self.get_container(group_id) if container.has_messages(): - #最早的对象,可能是思考消息,也可能是发送消息 - message_earliest = container.get_earliest_message() #一个message_thinking or message_sending - - #如果是思考消息 + # 最早的对象,可能是思考消息,也可能是发送消息 + message_earliest = container.get_earliest_message() # 一个message_thinking or message_sending + + # 如果是思考消息 if isinstance(message_earliest, Message_Thinking): - #优先等待这条消息 + # 优先等待这条消息 message_earliest.update_thinking_time() thinking_time = message_earliest.thinking_time - print(f"\033[1;34m[调试]\033[0m 消息正在思考中,已思考{int(thinking_time)}秒\033[K\r", end='', flush=True) - + print(f"消息正在思考中,已思考{int(thinking_time)}秒\r", end='', flush=True) + # 检查是否超时 if thinking_time > global_config.thinking_timeout: - print(f"\033[1;33m[警告]\033[0m 消息思考超时({thinking_time}秒),移除该消息") + logger.warning(f"消息思考超时({thinking_time}秒),移除该消息") container.remove_message(message_earliest) - else:# 如果不是message_thinking就只能是message_sending - print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") - #直接发,等什么呢 - if message_earliest.is_head and message_earliest.update_thinking_time() >30: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False, reply_message_id=message_earliest.reply_message_id) + else: # 如果不是message_thinking就只能是message_sending + logger.debug(f"消息'{message_earliest.processed_plain_text}'正在发送中") + # 直接发,等什么呢 + if message_earliest.is_head and message_earliest.update_thinking_time() > 30: + await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, + auto_escape=False, + reply_message_id=message_earliest.reply_message_id) else: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False) - #移除消息 + await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, + auto_escape=False) + # 移除消息 if message_earliest.is_emoji: message_earliest.processed_plain_text = "[表情包]" await self.storage.store_message(message_earliest, None) - + container.remove_message(message_earliest) - - #获取并处理超时消息 - message_timeout = container.get_timeout_messages() #也许是一堆message_sending + + # 获取并处理超时消息 + message_timeout = container.get_timeout_messages() # 也许是一堆message_sending if message_timeout: - print(f"\033[1;34m[调试]\033[0m 发现{len(message_timeout)}条超时消息") + logger.warning(f"发现{len(message_timeout)}条超时消息") for msg in message_timeout: if msg == message_earliest: continue # 跳过已经处理过的消息 - + try: - #发送 - if msg.is_head and msg.update_thinking_time() >30: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) + # 发送 + if msg.is_head and msg.update_thinking_time() > 30: + await message_sender.send_group_message(group_id, msg.processed_plain_text, + auto_escape=False, + reply_message_id=msg.reply_message_id) else: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False) - - - #如果是表情包,则替换为"[表情包]" + await message_sender.send_group_message(group_id, msg.processed_plain_text, + auto_escape=False) + + # 如果是表情包,则替换为"[表情包]" if msg.is_emoji: msg.processed_plain_text = "[表情包]" await self.storage.store_message(msg, None) - + # 安全地移除消息 if not container.remove_message(msg): - print("\033[1;33m[警告]\033[0m 尝试删除不存在的消息") + logger.warning("尝试删除不存在的消息") except Exception as e: - print(f"\033[1;31m[错误]\033[0m 处理超时消息时发生错误: {e}") + logger.exception(f"处理超时消息时发生错误: {e}") continue - + async def start_processor(self): """启动消息处理器""" while self._running: @@ -217,9 +224,10 @@ class MessageManager: tasks = [] for group_id in self.containers.keys(): tasks.append(self.process_group_messages(group_id)) - + await asyncio.gather(*tasks) + # 创建全局消息管理器实例 message_manager = MessageManager() # 创建全局发送器实例 diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index fdb887af5..4cf21af19 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -1,6 +1,7 @@ import random import time from typing import Optional +from loguru import logger from ...common.database import Database from ..memory_system.memory import hippocampus, memory_graph @@ -16,13 +17,11 @@ class PromptBuilder: self.activate_messages = '' self.db = Database.get_instance() - - - async def _build_prompt(self, - message_txt: str, - sender_name: str = "某人", - relationship_value: float = 0.0, - group_id: Optional[int] = None) -> tuple[str, str]: + async def _build_prompt(self, + message_txt: str, + sender_name: str = "某人", + relationship_value: float = 0.0, + group_id: Optional[int] = None) -> tuple[str, str]: """构建prompt Args: @@ -33,57 +32,56 @@ class PromptBuilder: Returns: str: 构建好的prompt - """ - #先禁用关系 + """ + # 先禁用关系 if 0 > 30: relation_prompt = "关系特别特别好,你很喜欢喜欢他" relation_prompt_2 = "热情发言或者回复" - elif 0 <-20: + elif 0 < -20: relation_prompt = "关系很差,你很讨厌他" relation_prompt_2 = "骂他" else: relation_prompt = "关系一般" relation_prompt_2 = "发言或者回复" - - #开始构建prompt - - - #心情 + + # 开始构建prompt + + # 心情 mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_prompt() - - - #日程构建 + + # 日程构建 current_date = time.strftime("%Y-%m-%d", time.localtime()) current_time = time.strftime("%H:%M:%S", time.localtime()) - bot_schedule_now_time,bot_schedule_now_activity = bot_schedule.get_current_task() + bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task() prompt_date = f'''今天是{current_date},现在是{current_time},你今天的日程是:\n{bot_schedule.today_schedule}\n你现在正在{bot_schedule_now_activity}\n''' - #知识构建 + # 知识构建 start_time = time.time() - + prompt_info = '' promt_info_prompt = '' - prompt_info = await self.get_prompt_info(message_txt,threshold=0.5) + prompt_info = await self.get_prompt_info(message_txt, threshold=0.5) if prompt_info: - prompt_info = f'''\n----------------------------------------------------\n你有以下这些[知识]:\n{prompt_info}\n请你记住上面的[知识],之后可能会用到\n----------------------------------------------------\n''' - + prompt_info = f'''你有以下这些[知识]:{prompt_info}请你记住上面的[ + 知识],之后可能会用到-''' + end_time = time.time() - print(f"\033[1;32m[知识检索]\033[0m 耗时: {(end_time - start_time):.3f}秒") - + logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒") + # 获取聊天上下文 chat_talking_prompt = '' if group_id: - chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, limit=global_config.MAX_CONTEXT_SIZE,combine = True) - + chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, + limit=global_config.MAX_CONTEXT_SIZE, + combine=True) + chat_talking_prompt = f"以下是群里正在聊天的内容:\n{chat_talking_prompt}" - - - + # 使用新的记忆获取方法 memory_prompt = '' start_time = time.time() - + # 调用 hippocampus 的 get_relevant_memories 方法 relevant_memories = await hippocampus.get_relevant_memories( text=message_txt, @@ -91,30 +89,28 @@ class PromptBuilder: similarity_threshold=0.4, max_memory_num=5 ) - + if relevant_memories: # 格式化记忆内容 memory_items = [] for memory in relevant_memories: memory_items.append(f"关于「{memory['topic']}」的记忆:{memory['content']}") - + memory_prompt = "看到这些聊天,你想起来:\n" + "\n".join(memory_items) + "\n" - + # 打印调试信息 - print("\n\033[1;32m[记忆检索]\033[0m 找到以下相关记忆:") + logger.debug("[记忆检索]找到以下相关记忆:") for memory in relevant_memories: - print(f"- 主题「{memory['topic']}」[相似度: {memory['similarity']:.2f}]: {memory['content']}") - + logger.debug(f"- 主题「{memory['topic']}」[相似度: {memory['similarity']:.2f}]: {memory['content']}") + end_time = time.time() - print(f"\033[1;32m[回忆耗时]\033[0m 耗时: {(end_time - start_time):.3f}秒") - - - - #激活prompt构建 + logger.info(f"回忆耗时: {(end_time - start_time):.3f}秒") + + # 激活prompt构建 activate_prompt = '' - activate_prompt = f"以上是群里正在进行的聊天,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},{mood_prompt},你想要{relation_prompt_2}。" - - #检测机器人相关词汇,改为关键词检测与反应功能了,提取到全局配置中 + activate_prompt = f"以上是群里正在进行的聊天,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},{mood_prompt},你想要{relation_prompt_2}。" + + # 检测机器人相关词汇,改为关键词检测与反应功能了,提取到全局配置中 # bot_keywords = ['人机', 'bot', '机器', '入机', 'robot', '机器人'] # is_bot = any(keyword in message_txt.lower() for keyword in bot_keywords) # if is_bot: @@ -127,12 +123,11 @@ class PromptBuilder: for rule in global_config.keywords_reaction_rules: if rule.get("enable", False): if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])): - print(f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}") + logger.info(f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}") keywords_reaction_prompt += rule.get("reaction", "") + ',' - - #人格选择 - personality=global_config.PROMPT_PERSONALITY + # 人格选择 + personality = global_config.PROMPT_PERSONALITY probability_1 = global_config.PERSONALITY_1 probability_2 = global_config.PERSONALITY_2 probability_3 = global_config.PERSONALITY_3 @@ -150,8 +145,8 @@ class PromptBuilder: prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 请你表达自己的见解和观点。可以有个性。''' - - #中文高手(新加的好玩功能) + + # 中文高手(新加的好玩功能) prompt_ger = '' if random.random() < 0.04: prompt_ger += '你喜欢用倒装句' @@ -159,23 +154,23 @@ class PromptBuilder: prompt_ger += '你喜欢用反问句' if random.random() < 0.01: prompt_ger += '你喜欢用文言文' - - #额外信息要求 - extra_info = '''但是记得回复平淡一些,简短一些,尤其注意在没明确提到时不要过多提及自身的背景, 不要直接回复别人发的表情包,记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只需要输出回复内容就好,不要输出其他任何内容''' - - #合并prompt + + # 额外信息要求 + extra_info = '''但是记得回复平淡一些,简短一些,尤其注意在没明确提到时不要过多提及自身的背景, 不要直接回复别人发的表情包,记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只需要输出回复内容就好,不要输出其他任何内容''' + + # 合并prompt prompt = "" prompt += f"{prompt_info}\n" prompt += f"{prompt_date}\n" - prompt += f"{chat_talking_prompt}\n" + prompt += f"{chat_talking_prompt}\n" prompt += f"{prompt_personality}\n" prompt += f"{prompt_ger}\n" - prompt += f"{extra_info}\n" - - '''读空气prompt处理''' - activate_prompt_check=f"以上是群里正在进行的聊天,昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},你想要{relation_prompt_2},但是这不一定是合适的时机,请你决定是否要回应这条消息。" + prompt += f"{extra_info}\n" + + '''读空气prompt处理''' + activate_prompt_check = f"以上是群里正在进行的聊天,昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},你想要{relation_prompt_2},但是这不一定是合适的时机,请你决定是否要回应这条消息。" prompt_personality_check = '' - extra_check_info=f"请注意把握群里的聊天内容的基础上,综合群内的氛围,例如,和{global_config.BOT_NICKNAME}相关的话题要积极回复,如果是at自己的消息一定要回复,如果自己正在和别人聊天一定要回复,其他话题如果合适搭话也可以回复,如果认为应该回复请输出yes,否则输出no,请注意是决定是否需要回复,而不是编写回复内容,除了yes和no不要输出任何回复内容。" + extra_check_info = f"请注意把握群里的聊天内容的基础上,综合群内的氛围,例如,和{global_config.BOT_NICKNAME}相关的话题要积极回复,如果是at自己的消息一定要回复,如果自己正在和别人聊天一定要回复,其他话题如果合适搭话也可以回复,如果认为应该回复请输出yes,否则输出no,请注意是决定是否需要回复,而不是编写回复内容,除了yes和no不要输出任何回复内容。" if personality_choice < probability_1: # 第一种人格 prompt_personality_check = f'''你的网名叫{global_config.BOT_NICKNAME},{personality[0]}, 你正在浏览qq群,{promt_info_prompt} {activate_prompt_check} {extra_check_info}''' elif personality_choice < probability_1 + probability_2: # 第二种人格 @@ -183,34 +178,36 @@ class PromptBuilder: else: # 第三种人格 prompt_personality_check = f'''你的网名叫{global_config.BOT_NICKNAME},{personality[2]}, 你正在浏览qq群,{promt_info_prompt} {activate_prompt_check} {extra_check_info}''' - prompt_check_if_response=f"{prompt_info}\n{prompt_date}\n{chat_talking_prompt}\n{prompt_personality_check}" - - return prompt,prompt_check_if_response - - def _build_initiative_prompt_select(self,group_id): + prompt_check_if_response = f"{prompt_info}\n{prompt_date}\n{chat_talking_prompt}\n{prompt_personality_check}" + + return prompt, prompt_check_if_response + + def _build_initiative_prompt_select(self, group_id, probability_1=0.8, probability_2=0.1): current_date = time.strftime("%Y-%m-%d", time.localtime()) current_time = time.strftime("%H:%M:%S", time.localtime()) - bot_schedule_now_time,bot_schedule_now_activity = bot_schedule.get_current_task() + bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task() prompt_date = f'''今天是{current_date},现在是{current_time},你今天的日程是:\n{bot_schedule.today_schedule}\n你现在正在{bot_schedule_now_activity}\n''' chat_talking_prompt = '' if group_id: - chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, limit=global_config.MAX_CONTEXT_SIZE,combine = True) - + chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, + limit=global_config.MAX_CONTEXT_SIZE, + combine=True) + chat_talking_prompt = f"以下是群里正在聊天的内容:\n{chat_talking_prompt}" - # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") + # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") # 获取主动发言的话题 - all_nodes=memory_graph.dots - all_nodes=filter(lambda dot:len(dot[1]['memory_items'])>3,all_nodes) - nodes_for_select=random.sample(all_nodes,5) - topics=[info[0] for info in nodes_for_select] - infos=[info[1] for info in nodes_for_select] + all_nodes = memory_graph.dots + all_nodes = filter(lambda dot: len(dot[1]['memory_items']) > 3, all_nodes) + nodes_for_select = random.sample(all_nodes, 5) + topics = [info[0] for info in nodes_for_select] + infos = [info[1] for info in nodes_for_select] - #激活prompt构建 + # 激活prompt构建 activate_prompt = '' activate_prompt = "以上是群里正在进行的聊天。" - personality=global_config.PROMPT_PERSONALITY + personality = global_config.PROMPT_PERSONALITY prompt_personality = '' personality_choice = random.random() if personality_choice < probability_1: # 第一种人格 @@ -219,32 +216,31 @@ class PromptBuilder: prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[1]}''' else: # 第三种人格 prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]}''' - - topics_str=','.join(f"\"{topics}\"") - prompt_for_select=f"你现在想在群里发言,回忆了一下,想到几个话题,分别是{topics_str},综合当前状态以及群内气氛,请你在其中选择一个合适的话题,注意只需要输出话题,除了话题什么也不要输出(双引号也不要输出)" - - prompt_initiative_select=f"{prompt_date}\n{prompt_personality}\n{prompt_for_select}" - prompt_regular=f"{prompt_date}\n{prompt_personality}" - return prompt_initiative_select,nodes_for_select,prompt_regular - - def _build_initiative_prompt_check(self,selected_node,prompt_regular): - memory=random.sample(selected_node['memory_items'],3) - memory='\n'.join(memory) - prompt_for_check=f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']},关于这个话题的记忆有\n{memory}\n,以这个作为主题发言合适吗?请在把握群里的聊天内容的基础上,综合群内的氛围,如果认为应该发言请输出yes,否则输出no,请注意是决定是否需要发言,而不是编写回复内容,除了yes和no不要输出任何回复内容。" - return prompt_for_check,memory - - def _build_initiative_prompt(self,selected_node,prompt_regular,memory): - prompt_for_initiative=f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']},关于这个话题的记忆有\n{memory}\n,请在把握群里的聊天内容的基础上,综合群内的氛围,以日常且口语化的口吻,简短且随意一点进行发言,不要说的太有条理,可以有个性。记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情等)" + topics_str = ','.join(f"\"{topics}\"") + prompt_for_select = f"你现在想在群里发言,回忆了一下,想到几个话题,分别是{topics_str},综合当前状态以及群内气氛,请你在其中选择一个合适的话题,注意只需要输出话题,除了话题什么也不要输出(双引号也不要输出)" + + prompt_initiative_select = f"{prompt_date}\n{prompt_personality}\n{prompt_for_select}" + prompt_regular = f"{prompt_date}\n{prompt_personality}" + + return prompt_initiative_select, nodes_for_select, prompt_regular + + def _build_initiative_prompt_check(self, selected_node, prompt_regular): + memory = random.sample(selected_node['memory_items'], 3) + memory = '\n'.join(memory) + prompt_for_check = f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']},关于这个话题的记忆有\n{memory}\n,以这个作为主题发言合适吗?请在把握群里的聊天内容的基础上,综合群内的氛围,如果认为应该发言请输出yes,否则输出no,请注意是决定是否需要发言,而不是编写回复内容,除了yes和no不要输出任何回复内容。" + return prompt_for_check, memory + + def _build_initiative_prompt(self, selected_node, prompt_regular, memory): + prompt_for_initiative = f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']},关于这个话题的记忆有\n{memory}\n,请在把握群里的聊天内容的基础上,综合群内的氛围,以日常且口语化的口吻,简短且随意一点进行发言,不要说的太有条理,可以有个性。记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情等)" return prompt_for_initiative - - async def get_prompt_info(self,message:str,threshold:float): + async def get_prompt_info(self, message: str, threshold: float): related_info = '' - print(f"\033[1;34m[调试]\033[0m 获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") + logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") embedding = await get_embedding(message) - related_info += self.get_info_from_db(embedding,threshold=threshold) - + related_info += self.get_info_from_db(embedding, threshold=threshold) + return related_info def get_info_from_db(self, query_embedding: list, limit: int = 1, threshold: float = 0.5) -> str: @@ -305,14 +301,15 @@ class PromptBuilder: {"$limit": limit}, {"$project": {"content": 1, "similarity": 1}} ] - + results = list(self.db.db.knowledges.aggregate(pipeline)) # print(f"\033[1;34m[调试]\033[0m获取知识库内容结果: {results}") - + if not results: return '' - + # 返回所有找到的内容,用换行分隔 return '\n'.join(str(result['content']) for result in results) - -prompt_builder = PromptBuilder() \ No newline at end of file + + +prompt_builder = PromptBuilder() diff --git a/src/plugins/chat/topic_identifier.py b/src/plugins/chat/topic_identifier.py index 3296d0895..a0c5bae30 100644 --- a/src/plugins/chat/topic_identifier.py +++ b/src/plugins/chat/topic_identifier.py @@ -4,9 +4,11 @@ from nonebot import get_driver from ..models.utils_model import LLM_request from .config import global_config +from loguru import logger driver = get_driver() -config = driver.config +config = driver.config + class TopicIdentifier: def __init__(self): @@ -23,19 +25,20 @@ class TopicIdentifier: # 使用 LLM_request 类进行请求 topic, _ = await self.llm_topic_judge.generate_response(prompt) - + if not topic: - print("\033[1;31m[错误]\033[0m LLM API 返回为空") + logger.error("LLM API 返回为空") return None - + # 直接在这里处理主题解析 if not topic or topic == "无主题": return None - + # 解析主题字符串为列表 topic_list = [t.strip() for t in topic.split(",") if t.strip()] - - print(f"\033[1;32m[主题识别]\033[0m 主题: {topic_list}") + + logger.info(f"主题: {topic_list}") return topic_list if topic_list else None -topic_identifier = TopicIdentifier() \ No newline at end of file + +topic_identifier = TopicIdentifier() diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index d166bcd27..e5ebad59d 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -7,6 +7,7 @@ from typing import Dict, List import jieba import numpy as np from nonebot import get_driver +from loguru import logger from ..models.utils_model import LLM_request from ..utils.typo_generator import ChineseTypoGenerator @@ -39,16 +40,16 @@ def combine_messages(messages: List[Message]) -> str: def db_message_to_str(message_dict: Dict) -> str: - print(f"message_dict: {message_dict}") + logger.debug(f"message_dict: {message_dict}") time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"])) try: name = "[(%s)%s]%s" % ( - message_dict['user_id'], message_dict.get("user_nickname", ""), message_dict.get("user_cardname", "")) + message_dict['user_id'], message_dict.get("user_nickname", ""), message_dict.get("user_cardname", "")) except: name = message_dict.get("user_nickname", "") or f"用户{message_dict['user_id']}" content = message_dict.get("processed_plain_text", "") result = f"[{time_str}] {name}: {content}\n" - print(f"result: {result}") + logger.debug(f"result: {result}") return result @@ -176,7 +177,7 @@ async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: await msg.initialize() message_objects.append(msg) except KeyError: - print("[WARNING] 数据库中存在无效的消息") + logger.warning("数据库中存在无效的消息") continue # 按时间正序排列 @@ -292,11 +293,10 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: sentence = sentence.replace(',', ' ').replace(',', ' ') sentences_done.append(sentence) - print(f"处理后的句子: {sentences_done}") + logger.info(f"处理后的句子: {sentences_done}") return sentences_done - def random_remove_punctuation(text: str) -> str: """随机处理标点符号,模拟人类打字习惯 @@ -324,11 +324,10 @@ def random_remove_punctuation(text: str) -> str: return result - def process_llm_response(text: str) -> List[str]: # processed_response = process_text_with_typos(content) if len(text) > 200: - print(f"回复过长 ({len(text)} 字符),返回默认回复") + logger.warning(f"回复过长 ({len(text)} 字符),返回默认回复") return ['懒得说'] # 处理长消息 typo_generator = ChineseTypoGenerator( @@ -348,9 +347,9 @@ def process_llm_response(text: str) -> List[str]: else: sentences.append(sentence) # 检查分割后的消息数量是否过多(超过3条) - + if len(sentences) > 5: - print(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") + logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") return [f'{global_config.BOT_NICKNAME}不知道哦'] return sentences @@ -372,15 +371,15 @@ def calculate_typing_time(input_string: str, chinese_time: float = 0.4, english_ mood_arousal = mood_manager.current_mood.arousal # 映射到0.5到2倍的速度系数 typing_speed_multiplier = 1.5 ** mood_arousal # 唤醒度为1时速度翻倍,为-1时速度减半 - chinese_time *= 1/typing_speed_multiplier - english_time *= 1/typing_speed_multiplier + chinese_time *= 1 / typing_speed_multiplier + english_time *= 1 / typing_speed_multiplier # 计算中文字符数 chinese_chars = sum(1 for char in input_string if '\u4e00' <= char <= '\u9fff') - + # 如果只有一个中文字符,使用3倍时间 if chinese_chars == 1 and len(input_string.strip()) == 1: return chinese_time * 3 + 0.3 # 加上回车时间 - + # 正常计算所有字符的输入时间 total_time = 0.0 for char in input_string: diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 001b66207..4f8bec7dc 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -1,5 +1,6 @@ import asyncio from .config import global_config +from loguru import logger class WillingManager: @@ -30,16 +31,16 @@ class WillingManager: # print(f"初始意愿: {current_willing}") if is_mentioned_bot and current_willing < 1.0: current_willing += 0.9 - print(f"被提及, 当前意愿: {current_willing}") + logger.info(f"被提及, 当前意愿: {current_willing}") elif is_mentioned_bot: current_willing += 0.05 - print(f"被重复提及, 当前意愿: {current_willing}") + logger.info(f"被重复提及, 当前意愿: {current_willing}") if is_emoji: current_willing *= 0.1 - print(f"表情包, 当前意愿: {current_willing}") + logger.info(f"表情包, 当前意愿: {current_willing}") - print(f"放大系数_interested_rate: {global_config.response_interested_rate_amplifier}") + logger.debug(f"放大系数_interested_rate: {global_config.response_interested_rate_amplifier}") interested_rate *= global_config.response_interested_rate_amplifier #放大回复兴趣度 if interested_rate > 0.4: # print(f"兴趣度: {interested_rate}, 当前意愿: {current_willing}") diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 8e917caf4..9b325b36d 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -224,7 +224,7 @@ class Hippocampus: for msg in messages: input_text += f"{msg['text']}\n" - print(input_text) + logger.debug(input_text) topic_num = self.calculate_topic_num(input_text, compress_rate) topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(input_text, topic_num)) @@ -235,7 +235,7 @@ class Hippocampus: topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] - print(f"过滤后话题: {filtered_topics}") + logger.info(f"过滤后话题: {filtered_topics}") # 创建所有话题的请求任务 tasks = [] @@ -259,8 +259,9 @@ class Hippocampus: topic_by_length = text.count('\n') * compress_rate topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) topic_num = int((topic_by_length + topic_by_information_content) / 2) - print( - f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") + logger.debug( + f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, " + f"topic_num: {topic_num}") return topic_num async def operation_build_memory(self, chat_size=20): @@ -275,22 +276,22 @@ class Hippocampus: bar_length = 30 filled_length = int(bar_length * i // len(memory_sample)) bar = '█' * filled_length + '-' * (bar_length - filled_length) - print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") + logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") # 生成压缩后记忆 ,表现为 (话题,记忆) 的元组 compressed_memory = set() compress_rate = 0.1 compressed_memory = await self.memory_compress(input_text, compress_rate) - print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)}") + logger.info(f"压缩后记忆数量: {len(compressed_memory)}") # 将记忆加入到图谱中 for topic, memory in compressed_memory: - print(f"\033[1;32m添加节点\033[0m: {topic}") + logger.info(f"添加节点: {topic}") self.memory_graph.add_dot(topic, memory) all_topics.append(topic) # 收集所有话题 for i in range(len(all_topics)): for j in range(i + 1, len(all_topics)): - print(f"\033[1;32m连接节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") + logger.info(f"连接节点: {all_topics[i]} 和 {all_topics[j]}") self.memory_graph.connect_dot(all_topics[i], all_topics[j]) self.sync_memory_to_db() @@ -451,14 +452,14 @@ class Hippocampus: removed_item = self.memory_graph.forget_topic(node) if removed_item: forgotten_nodes.append((node, removed_item)) - print(f"遗忘节点 {node} 的记忆: {removed_item}") + logger.debug(f"遗忘节点 {node} 的记忆: {removed_item}") # 同步到数据库 if forgotten_nodes: self.sync_memory_to_db() - print(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") + logger.debug(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") else: - print("本次检查没有节点满足遗忘条件") + logger.debug("本次检查没有节点满足遗忘条件") async def merge_memory(self, topic): """ @@ -481,8 +482,8 @@ class Hippocampus: # 拼接成文本 merged_text = "\n".join(selected_memories) - print(f"\n[合并记忆] 话题: {topic}") - print(f"选择的记忆:\n{merged_text}") + logger.debug(f"\n[合并记忆] 话题: {topic}") + logger.debug(f"选择的记忆:\n{merged_text}") # 使用memory_compress生成新的压缩记忆 compressed_memories = await self.memory_compress(selected_memories, 0.1) @@ -494,11 +495,11 @@ class Hippocampus: # 添加新的压缩记忆 for _, compressed_memory in compressed_memories: memory_items.append(compressed_memory) - print(f"添加压缩记忆: {compressed_memory}") + logger.info(f"添加压缩记忆: {compressed_memory}") # 更新节点的记忆项 self.memory_graph.G.nodes[topic]['memory_items'] = memory_items - print(f"完成记忆合并,当前记忆数量: {len(memory_items)}") + logger.debug(f"完成记忆合并,当前记忆数量: {len(memory_items)}") async def operation_merge_memory(self, percentage=0.1): """ @@ -524,16 +525,16 @@ class Hippocampus: # 如果内容数量超过100,进行合并 if content_count > 100: - print(f"\n检查节点: {node}, 当前记忆数量: {content_count}") + logger.debug(f"检查节点: {node}, 当前记忆数量: {content_count}") await self.merge_memory(node) merged_nodes.append(node) # 同步到数据库 if merged_nodes: self.sync_memory_to_db() - print(f"\n完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") + logger.debug(f"完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") else: - print("\n本次检查没有需要合并的节点") + logger.debug("本次检查没有需要合并的节点") def find_topic_llm(self, text, topic_num): prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' @@ -628,7 +629,7 @@ class Hippocampus: async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: """计算输入文本对记忆的激活程度""" - print(f"\033[1;32m[记忆激活]\033[0m 识别主题: {await self._identify_topics(text)}") + logger.info(f"识别主题: {await self._identify_topics(text)}") # 识别主题 identified_topics = await self._identify_topics(text) @@ -659,8 +660,8 @@ class Hippocampus: penalty = 1.0 / (1 + math.log(content_count + 1)) activation = int(score * 50 * penalty) - print( - f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") + logger.info( + f"[记忆激活]单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") return activation # 计算关键词匹配率,同时考虑内容数量 @@ -687,8 +688,8 @@ class Hippocampus: matched_topics.add(input_topic) adjusted_sim = sim * penalty topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) - print( - f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") + logger.info( + f"[记忆激活]主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") # 计算主题匹配率和平均相似度 topic_match = len(matched_topics) / len(identified_topics) @@ -696,8 +697,8 @@ class Hippocampus: # 计算最终激活值 activation = int((topic_match + average_similarities) / 2 * 100) - print( - f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") + logger.info( + f"[记忆激活]匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") return activation diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 2e1151f93..012e5ecbb 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -743,7 +743,7 @@ class Hippocampus: async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: """计算输入文本对记忆的激活程度""" - print(f"\033[1;32m[记忆激活]\033[0m 识别主题: {await self._identify_topics(text)}") + logger.info(f"[记忆激活]识别主题: {await self._identify_topics(text)}") identified_topics = await self._identify_topics(text) if not identified_topics: diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index bd06fd6dd..f4100e0bb 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -28,10 +28,10 @@ class LLM_request: raise ValueError(f"配置错误:找不到对应的配置项 - {str(e)}") from e self.model_name = model["name"] self.params = kwargs - + self.pri_in = model.get("pri_in", 0) self.pri_out = model.get("pri_out", 0) - + # 获取数据库实例 self.db = Database.get_instance() self._init_database() @@ -47,9 +47,9 @@ class LLM_request: except Exception as e: logger.error(f"创建数据库索引失败: {e}") - def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, - user_id: str = "system", request_type: str = "chat", - endpoint: str = "/chat/completions"): + def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, + user_id: str = "system", request_type: str = "chat", + endpoint: str = "/chat/completions"): """记录模型使用情况到数据库 Args: prompt_tokens: 输入token数 @@ -140,12 +140,12 @@ class LLM_request: } api_url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" - #判断是否为流式 + # 判断是否为流式 stream_mode = self.params.get("stream", False) if self.params.get("stream", False) is True: - logger.info(f"进入流式输出模式,发送请求到URL: {api_url}") + logger.debug(f"进入流式输出模式,发送请求到URL: {api_url}") else: - logger.info(f"发送请求到URL: {api_url}") + logger.debug(f"发送请求到URL: {api_url}") logger.info(f"使用模型: {self.model_name}") # 构建请求体 @@ -158,7 +158,7 @@ class LLM_request: try: # 使用上下文管理器处理会话 headers = await self._build_headers() - #似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响 + # 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响 if stream_mode: headers["Accept"] = "text/event-stream" @@ -184,29 +184,31 @@ class LLM_request: logger.error(f"错误码: {response.status} - {error_code_mapping.get(response.status)}") if response.status == 403: # 尝试降级Pro模型 - if self.model_name.startswith("Pro/") and self.base_url == "https://api.siliconflow.cn/v1/": + if self.model_name.startswith( + "Pro/") and self.base_url == "https://api.siliconflow.cn/v1/": old_model_name = self.model_name self.model_name = self.model_name[4:] # 移除"Pro/"前缀 logger.warning(f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}") - + # 对全局配置进行更新 - if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get('name') == old_model_name: + if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( + 'name') == old_model_name: global_config.llm_normal['name'] = self.model_name logger.warning(f"已将全局配置中的 llm_normal 模型降级") - + # 更新payload中的模型名 if payload and 'model' in payload: payload['model'] = self.model_name - + # 重新尝试请求 retry -= 1 # 不计入重试次数 continue - + raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(response.status)}") - + response.raise_for_status() - - #将流式输出转化为非流式输出 + + # 将流式输出转化为非流式输出 if stream_mode: accumulated_content = "" async for line_bytes in response.content: @@ -233,12 +235,15 @@ class LLM_request: reasoning_content = think_match.group(1).strip() content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip() # 构造一个伪result以便调用自定义响应处理器或默认处理器 - result = {"choices": [{"message": {"content": content, "reasoning_content": reasoning_content}}]} - return response_handler(result) if response_handler else self._default_response_handler(result, user_id, request_type, endpoint) + result = { + "choices": [{"message": {"content": content, "reasoning_content": reasoning_content}}]} + return response_handler(result) if response_handler else self._default_response_handler( + result, user_id, request_type, endpoint) else: result = await response.json() # 使用自定义处理器或默认处理 - return response_handler(result) if response_handler else self._default_response_handler(result, user_id, request_type, endpoint) + return response_handler(result) if response_handler else self._default_response_handler( + result, user_id, request_type, endpoint) except Exception as e: if retry < policy["max_retries"] - 1: @@ -252,8 +257,8 @@ class LLM_request: logger.error("达到最大重试次数,请求仍然失败") raise RuntimeError("达到最大重试次数,API请求仍然失败") - - async def _transform_parameters(self, params: dict) ->dict: + + async def _transform_parameters(self, params: dict) -> dict: """ 根据模型名称转换参数: - 对于需要转换的OpenAI CoT系列模型(例如 "o3-mini"),删除 'temprature' 参数, @@ -262,7 +267,8 @@ class LLM_request: # 复制一份参数,避免直接修改原始数据 new_params = dict(params) # 定义需要转换的模型列表 - models_needing_transformation = ["o3-mini", "o1-mini", "o1-preview", "o1-2024-12-17", "o1-preview-2024-09-12", "o3-mini-2025-01-31", "o1-mini-2024-09-12"] + models_needing_transformation = ["o3-mini", "o1-mini", "o1-preview", "o1-2024-12-17", "o1-preview-2024-09-12", + "o3-mini-2025-01-31", "o1-mini-2024-09-12"] if self.model_name.lower() in models_needing_transformation: # 删除 'temprature' 参数(如果存在) new_params.pop("temperature", None) @@ -298,13 +304,13 @@ class LLM_request: **params_copy } # 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查 - if self.model_name.lower() in ["o3-mini", "o1-mini", "o1-preview", "o1-2024-12-17", "o1-preview-2024-09-12", "o3-mini-2025-01-31", "o1-mini-2024-09-12"] and "max_tokens" in payload: + if self.model_name.lower() in ["o3-mini", "o1-mini", "o1-preview", "o1-2024-12-17", "o1-preview-2024-09-12", + "o3-mini-2025-01-31", "o1-mini-2024-09-12"] and "max_tokens" in payload: payload["max_completion_tokens"] = payload.pop("max_tokens") return payload - - def _default_response_handler(self, result: dict, user_id: str = "system", - request_type: str = "chat", endpoint: str = "/chat/completions") -> Tuple: + def _default_response_handler(self, result: dict, user_id: str = "system", + request_type: str = "chat", endpoint: str = "/chat/completions") -> Tuple: """默认响应解析""" if "choices" in result and result["choices"]: message = result["choices"][0]["message"] @@ -356,8 +362,8 @@ class LLM_request: return { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" - } - # 防止小朋友们截图自己的key + } + # 防止小朋友们截图自己的key async def generate_response(self, prompt: str) -> Tuple[str, str]: """根据输入的提示生成模型的异步响应""" @@ -404,6 +410,7 @@ class LLM_request: Returns: list: embedding向量,如果失败则返回None """ + def embedding_handler(result): """处理响应""" if "data" in result and len(result["data"]) > 0: @@ -425,4 +432,3 @@ class LLM_request: response_handler=embedding_handler ) return embedding - diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index c35779f84..c37bfc81d 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -4,7 +4,7 @@ import time from dataclasses import dataclass from ..chat.config import global_config - +from loguru import logger @dataclass class MoodState: @@ -210,7 +210,7 @@ class MoodManager: def print_mood_status(self) -> None: """打印当前情绪状态""" - print(f"\033[1;35m[情绪状态]\033[0m 愉悦度: {self.current_mood.valence:.2f}, " + logger.info(f"[情绪状态]愉悦度: {self.current_mood.valence:.2f}, " f"唤醒度: {self.current_mood.arousal:.2f}, " f"心情: {self.current_mood.text}") diff --git a/src/test/typo.py b/src/test/typo.py index 16834200f..1378eae7d 100644 --- a/src/test/typo.py +++ b/src/test/typo.py @@ -11,12 +11,14 @@ from pathlib import Path import random import math import time +from loguru import logger + class ChineseTypoGenerator: - def __init__(self, - error_rate=0.3, - min_freq=5, - tone_error_rate=0.2, + def __init__(self, + error_rate=0.3, + min_freq=5, + tone_error_rate=0.2, word_replace_rate=0.3, max_freq_diff=200): """ @@ -34,27 +36,27 @@ class ChineseTypoGenerator: self.tone_error_rate = tone_error_rate self.word_replace_rate = word_replace_rate self.max_freq_diff = max_freq_diff - + # 加载数据 - print("正在加载汉字数据库,请稍候...") + logger.debug("正在加载汉字数据库,请稍候...") self.pinyin_dict = self._create_pinyin_dict() self.char_frequency = self._load_or_create_char_frequency() - + def _load_or_create_char_frequency(self): """ 加载或创建汉字频率字典 """ cache_file = Path("char_frequency.json") - + # 如果缓存文件存在,直接加载 if cache_file.exists(): with open(cache_file, 'r', encoding='utf-8') as f: return json.load(f) - + # 使用内置的词频文件 char_freq = defaultdict(int) dict_path = os.path.join(os.path.dirname(jieba.__file__), 'dict.txt') - + # 读取jieba的词典文件 with open(dict_path, 'r', encoding='utf-8') as f: for line in f: @@ -63,15 +65,15 @@ class ChineseTypoGenerator: for char in word: if self._is_chinese_char(char): char_freq[char] += int(freq) - + # 归一化频率值 max_freq = max(char_freq.values()) - normalized_freq = {char: freq/max_freq * 1000 for char, freq in char_freq.items()} - + normalized_freq = {char: freq / max_freq * 1000 for char, freq in char_freq.items()} + # 保存到缓存文件 with open(cache_file, 'w', encoding='utf-8') as f: json.dump(normalized_freq, f, ensure_ascii=False, indent=2) - + return normalized_freq def _create_pinyin_dict(self): @@ -81,7 +83,7 @@ class ChineseTypoGenerator: # 常用汉字范围 chars = [chr(i) for i in range(0x4e00, 0x9fff)] pinyin_dict = defaultdict(list) - + # 为每个汉字建立拼音映射 for char in chars: try: @@ -89,7 +91,7 @@ class ChineseTypoGenerator: pinyin_dict[py].append(char) except Exception: continue - + return pinyin_dict def _is_chinese_char(self, char): @@ -107,7 +109,7 @@ class ChineseTypoGenerator: """ # 将句子拆分成单个字符 characters = list(sentence) - + # 获取每个字符的拼音 result = [] for char in characters: @@ -117,7 +119,7 @@ class ChineseTypoGenerator: # 获取拼音(数字声调) py = pinyin(char, style=Style.TONE3)[0][0] result.append((char, py)) - + return result def _get_similar_tone_pinyin(self, py): @@ -127,19 +129,19 @@ class ChineseTypoGenerator: # 检查拼音是否为空或无效 if not py or len(py) < 1: return py - + # 如果最后一个字符不是数字,说明可能是轻声或其他特殊情况 if not py[-1].isdigit(): # 为非数字结尾的拼音添加数字声调1 return py + '1' - + base = py[:-1] # 去掉声调 tone = int(py[-1]) # 获取声调 - + # 处理轻声(通常用5表示)或无效声调 if tone not in [1, 2, 3, 4]: return base + str(random.choice([1, 2, 3, 4])) - + # 正常处理声调 possible_tones = [1, 2, 3, 4] possible_tones.remove(tone) # 移除原声调 @@ -152,11 +154,11 @@ class ChineseTypoGenerator: """ if target_freq > orig_freq: return 1.0 # 如果替换字频率更高,保持原有概率 - + freq_diff = orig_freq - target_freq if freq_diff > self.max_freq_diff: return 0.0 # 频率差太大,不替换 - + # 使用指数衰减函数计算概率 # 频率差为0时概率为1,频率差为max_freq_diff时概率接近0 return math.exp(-3 * freq_diff / self.max_freq_diff) @@ -166,42 +168,42 @@ class ChineseTypoGenerator: 获取与给定字频率相近的同音字,可能包含声调错误 """ homophones = [] - + # 有一定概率使用错误声调 if random.random() < self.tone_error_rate: wrong_tone_py = self._get_similar_tone_pinyin(py) homophones.extend(self.pinyin_dict[wrong_tone_py]) - + # 添加正确声调的同音字 homophones.extend(self.pinyin_dict[py]) - + if not homophones: return None - + # 获取原字的频率 orig_freq = self.char_frequency.get(char, 0) - + # 计算所有同音字与原字的频率差,并过滤掉低频字 - freq_diff = [(h, self.char_frequency.get(h, 0)) - for h in homophones - if h != char and self.char_frequency.get(h, 0) >= self.min_freq] - + freq_diff = [(h, self.char_frequency.get(h, 0)) + for h in homophones + if h != char and self.char_frequency.get(h, 0) >= self.min_freq] + if not freq_diff: return None - + # 计算每个候选字的替换概率 candidates_with_prob = [] for h, freq in freq_diff: prob = self._calculate_replacement_probability(orig_freq, freq) if prob > 0: # 只保留有效概率的候选字 candidates_with_prob.append((h, prob)) - + if not candidates_with_prob: return None - + # 根据概率排序 candidates_with_prob.sort(key=lambda x: x[1], reverse=True) - + # 返回概率最高的几个字 return [char for char, _ in candidates_with_prob[:num_candidates]] @@ -223,10 +225,10 @@ class ChineseTypoGenerator: """ if len(word) == 1: return [] - + # 获取词的拼音 word_pinyin = self._get_word_pinyin(word) - + # 遍历所有可能的同音字组合 candidates = [] for py in word_pinyin: @@ -234,11 +236,11 @@ class ChineseTypoGenerator: if not chars: return [] candidates.append(chars) - + # 生成所有可能的组合 import itertools all_combinations = itertools.product(*candidates) - + # 获取jieba词典和词频信息 dict_path = os.path.join(os.path.dirname(jieba.__file__), 'dict.txt') valid_words = {} # 改用字典存储词语及其频率 @@ -249,11 +251,11 @@ class ChineseTypoGenerator: word_text = parts[0] word_freq = float(parts[1]) # 获取词频 valid_words[word_text] = word_freq - + # 获取原词的词频作为参考 original_word_freq = valid_words.get(word, 0) min_word_freq = original_word_freq * 0.1 # 设置最小词频为原词频的10% - + # 过滤和计算频率 homophones = [] for combo in all_combinations: @@ -268,7 +270,7 @@ class ChineseTypoGenerator: combined_score = (new_word_freq * 0.7 + char_avg_freq * 0.3) if combined_score >= self.min_freq: homophones.append((new_word, combined_score)) - + # 按综合分数排序并限制返回数量 sorted_homophones = sorted(homophones, key=lambda x: x[1], reverse=True) return [word for word, _ in sorted_homophones[:5]] # 限制返回前5个结果 @@ -286,19 +288,19 @@ class ChineseTypoGenerator: """ result = [] typo_info = [] - + # 分词 words = self._segment_sentence(sentence) - + for word in words: # 如果是标点符号或空格,直接添加 if all(not self._is_chinese_char(c) for c in word): result.append(word) continue - + # 获取词语的拼音 word_pinyin = self._get_word_pinyin(word) - + # 尝试整词替换 if len(word) > 1 and random.random() < self.word_replace_rate: word_homophones = self._get_word_homophones(word) @@ -307,15 +309,15 @@ class ChineseTypoGenerator: # 计算词的平均频率 orig_freq = sum(self.char_frequency.get(c, 0) for c in word) / len(word) typo_freq = sum(self.char_frequency.get(c, 0) for c in typo_word) / len(typo_word) - + # 添加到结果中 result.append(typo_word) - typo_info.append((word, typo_word, - ' '.join(word_pinyin), - ' '.join(self._get_word_pinyin(typo_word)), - orig_freq, typo_freq)) + typo_info.append((word, typo_word, + ' '.join(word_pinyin), + ' '.join(self._get_word_pinyin(typo_word)), + orig_freq, typo_freq)) continue - + # 如果不进行整词替换,则进行单字替换 if len(word) == 1: char = word @@ -339,7 +341,7 @@ class ChineseTypoGenerator: for i, (char, py) in enumerate(zip(word, word_pinyin)): # 词中的字替换概率降低 word_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) - + if random.random() < word_error_rate: similar_chars = self._get_similar_frequency_chars(char, py) if similar_chars: @@ -354,7 +356,7 @@ class ChineseTypoGenerator: continue word_result.append(char) result.append(''.join(word_result)) - + return ''.join(result), typo_info def format_typo_info(self, typo_info): @@ -369,7 +371,7 @@ class ChineseTypoGenerator: """ if not typo_info: return "未生成错别字" - + result = [] for orig, typo, orig_py, typo_py, orig_freq, typo_freq in typo_info: # 判断是否为词语替换 @@ -379,12 +381,12 @@ class ChineseTypoGenerator: else: tone_error = orig_py[:-1] == typo_py[:-1] and orig_py[-1] != typo_py[-1] error_type = "声调错误" if tone_error else "同音字替换" - + result.append(f"原文:{orig}({orig_py}) [频率:{orig_freq:.2f}] -> " - f"替换:{typo}({typo_py}) [频率:{typo_freq:.2f}] [{error_type}]") - + f"替换:{typo}({typo_py}) [频率:{typo_freq:.2f}] [{error_type}]") + return "\n".join(result) - + def set_params(self, **kwargs): """ 设置参数 @@ -399,9 +401,10 @@ class ChineseTypoGenerator: for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) - print(f"参数 {key} 已设置为 {value}") + logger.debug(f"参数 {key} 已设置为 {value}") else: - print(f"警告: 参数 {key} 不存在") + logger.warning(f"警告: 参数 {key} 不存在") + def main(): # 创建错别字生成器实例 @@ -411,27 +414,27 @@ def main(): tone_error_rate=0.02, word_replace_rate=0.3 ) - + # 获取用户输入 sentence = input("请输入中文句子:") - + # 创建包含错别字的句子 start_time = time.time() typo_sentence, typo_info = typo_generator.create_typo_sentence(sentence) - + # 打印结果 - print("\n原句:", sentence) - print("错字版:", typo_sentence) - + logger.debug("原句:", sentence) + logger.debug("错字版:", typo_sentence) + # 打印错别字信息 if typo_info: - print("\n错别字信息:") - print(typo_generator.format_typo_info(typo_info)) - + logger.debug(f"错别字信息:{typo_generator.format_typo_info(typo_info)})") + # 计算并打印总耗时 end_time = time.time() total_time = end_time - start_time - print(f"\n总耗时:{total_time:.2f}秒") + logger.debug(f"总耗时:{total_time:.2f}秒") + if __name__ == "__main__": main() From 6b4130efcd16da9f2be472f36825c8392c092eb6 Mon Sep 17 00:00:00 2001 From: Rikki Date: Mon, 10 Mar 2025 04:09:44 +0800 Subject: [PATCH 029/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0stable-dev?= =?UTF-8?q?=E5=88=86=E6=94=AF=E7=9A=84=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2a5f497fd..b729a14ef 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,6 +5,7 @@ on: branches: - main - debug # 新增 debug 分支触发 + - stable-dev tags: - 'v*' workflow_dispatch: From 8b7876c14f8031070faec023935a6a02fb92a97e Mon Sep 17 00:00:00 2001 From: Rikki Date: Mon, 10 Mar 2025 04:20:07 +0800 Subject: [PATCH 030/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E4=B8=8A=E4=BC=A0tag=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b729a14ef..5b09b8cda 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -35,6 +35,8 @@ jobs: echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" == "refs/heads/debug" ]; then echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:debug" >> $GITHUB_OUTPUT + elif [ "${{ github.ref }}" == "refs/heads/stable-dev" ]; then + echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:stable-dev" >> $GITHUB_OUTPUT fi - name: Build and Push Docker Image From fddb64151ea456843a655032dd2117e810fad98e Mon Sep 17 00:00:00 2001 From: Rikki Date: Mon, 10 Mar 2025 04:56:40 +0800 Subject: [PATCH 031/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E7=A9=BA=E5=80=BC=E6=A3=80=E6=B5=8B=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 614bfe025..708454a1e 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -241,8 +241,9 @@ class EmojiManager: logger.info(f"其不满足过滤规则,被剔除 {check}") continue logger.info(f"check通过 {check}") - embedding = await get_embedding(discription) + if discription is not None: + embedding = await get_embedding(discription) # 准备数据库记录 emoji_record = { 'filename': filename, From a43f9495eaf2ac82bfc19db8babe50ec9d745252 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 11:46:59 +0800 Subject: [PATCH 032/105] fix: remove duplicate message(CR comments) --- src/gui/reasoning_gui.py | 169 +++++++++++------------ src/plugins/chat/config.py | 6 +- src/plugins/chat/llm_generator.py | 8 +- src/plugins/chat/message_sender.py | 8 +- src/plugins/chat/storage.py | 13 +- src/plugins/memory_system/draw_memory.py | 119 ++++++++-------- src/plugins/models/utils_model.py | 10 +- src/plugins/utils/statistic.py | 5 +- 8 files changed, 171 insertions(+), 167 deletions(-) diff --git a/src/gui/reasoning_gui.py b/src/gui/reasoning_gui.py index 572e4ece9..514a95dfb 100644 --- a/src/gui/reasoning_gui.py +++ b/src/gui/reasoning_gui.py @@ -5,6 +5,9 @@ import threading import time from datetime import datetime from typing import Dict, List +from loguru import logger +from typing import Optional +from pymongo import MongoClient import customtkinter as ctk from dotenv import load_dotenv @@ -17,23 +20,20 @@ root_dir = os.path.abspath(os.path.join(current_dir, '..', '..')) # 加载环境变量 if os.path.exists(os.path.join(root_dir, '.env.dev')): load_dotenv(os.path.join(root_dir, '.env.dev')) - print("成功加载开发环境配置") + logger.info("成功加载开发环境配置") elif os.path.exists(os.path.join(root_dir, '.env.prod')): load_dotenv(os.path.join(root_dir, '.env.prod')) - print("成功加载生产环境配置") + logger.info("成功加载生产环境配置") else: - print("未找到环境配置文件") + logger.error("未找到环境配置文件") sys.exit(1) -from typing import Optional - -from pymongo import MongoClient - class Database: _instance: Optional["Database"] = None - - def __init__(self, host: str, port: int, db_name: str, username: str = None, password: str = None, auth_source: str = None): + + def __init__(self, host: str, port: int, db_name: str, username: str = None, password: str = None, + auth_source: str = None): if username and password: self.client = MongoClient( host=host, @@ -45,96 +45,96 @@ class Database: else: self.client = MongoClient(host, port) self.db = self.client[db_name] - + @classmethod - def initialize(cls, host: str, port: int, db_name: str, username: str = None, password: str = None, auth_source: str = None) -> "Database": + def initialize(cls, host: str, port: int, db_name: str, username: str = None, password: str = None, + auth_source: str = None) -> "Database": if cls._instance is None: cls._instance = cls(host, port, db_name, username, password, auth_source) return cls._instance - + @classmethod def get_instance(cls) -> "Database": if cls._instance is None: raise RuntimeError("Database not initialized") - return cls._instance - + return cls._instance class ReasoningGUI: def __init__(self): # 记录启动时间戳,转换为Unix时间戳 self.start_timestamp = datetime.now().timestamp() - print(f"程序启动时间戳: {self.start_timestamp}") - + logger.info(f"程序启动时间戳: {self.start_timestamp}") + # 设置主题 ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") - + # 创建主窗口 self.root = ctk.CTk() self.root.title('麦麦推理') self.root.geometry('800x600') self.root.protocol("WM_DELETE_WINDOW", self._on_closing) - + # 初始化数据库连接 try: self.db = Database.get_instance().db - print("数据库连接成功") + logger.success("数据库连接成功") except RuntimeError: - print("数据库未初始化,正在尝试初始化...") + logger.warning("数据库未初始化,正在尝试初始化...") try: Database.initialize("127.0.0.1", 27017, "maimai_bot") self.db = Database.get_instance().db - print("数据库初始化成功") - except Exception as e: - print(f"数据库初始化失败: {e}") + logger.success("数据库初始化成功") + except Exception: + logger.exception(f"数据库初始化失败") sys.exit(1) - + # 存储群组数据 self.group_data: Dict[str, List[dict]] = {} - + # 创建更新队列 self.update_queue = queue.Queue() - + # 创建主框架 self.frame = ctk.CTkFrame(self.root) self.frame.pack(pady=20, padx=20, fill="both", expand=True) - + # 添加标题 self.title = ctk.CTkLabel(self.frame, text="麦麦的脑内所想", font=("Arial", 24)) self.title.pack(pady=10, padx=10) - + # 创建左右分栏 self.paned = ctk.CTkFrame(self.frame) self.paned.pack(fill="both", expand=True, padx=10, pady=10) - + # 左侧群组列表 self.left_frame = ctk.CTkFrame(self.paned, width=200) self.left_frame.pack(side="left", fill="y", padx=5, pady=5) - + self.group_label = ctk.CTkLabel(self.left_frame, text="群组列表", font=("Arial", 16)) self.group_label.pack(pady=5) - + # 创建可滚动框架来容纳群组按钮 self.group_scroll_frame = ctk.CTkScrollableFrame(self.left_frame, width=180, height=400) self.group_scroll_frame.pack(pady=5, padx=5, fill="both", expand=True) - + # 存储群组按钮的字典 self.group_buttons: Dict[str, ctk.CTkButton] = {} # 当前选中的群组ID self.selected_group_id: Optional[str] = None - + # 右侧内容显示 self.right_frame = ctk.CTkFrame(self.paned) self.right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5) - + self.content_label = ctk.CTkLabel(self.right_frame, text="推理内容", font=("Arial", 16)) self.content_label.pack(pady=5) - + # 创建富文本显示框 self.content_text = ctk.CTkTextbox(self.right_frame, width=500, height=400) self.content_text.pack(pady=5, padx=5, fill="both", expand=True) - + # 配置文本标签 - 只使用颜色 self.content_text.tag_config("timestamp", foreground="#888888") # 时间戳使用灰色 self.content_text.tag_config("user", foreground="#4CAF50") # 用户名使用绿色 @@ -144,11 +144,11 @@ class ReasoningGUI: self.content_text.tag_config("reasoning", foreground="#FF9800") # 推理过程使用橙色 self.content_text.tag_config("response", foreground="#E91E63") # 回复使用粉色 self.content_text.tag_config("separator", foreground="#666666") # 分隔符使用深灰色 - + # 底部控制栏 self.control_frame = ctk.CTkFrame(self.frame) self.control_frame.pack(fill="x", padx=10, pady=5) - + self.clear_button = ctk.CTkButton( self.control_frame, text="清除显示", @@ -156,19 +156,19 @@ class ReasoningGUI: width=120 ) self.clear_button.pack(side="left", padx=5) - + # 启动自动更新线程 self.update_thread = threading.Thread(target=self._auto_update, daemon=True) self.update_thread.start() - + # 启动GUI更新检查 self.root.after(100, self._process_queue) - + def _on_closing(self): """处理窗口关闭事件""" self.root.quit() sys.exit(0) - + def _process_queue(self): """处理更新队列中的任务""" try: @@ -183,14 +183,14 @@ class ReasoningGUI: finally: # 继续检查队列 self.root.after(100, self._process_queue) - + def _update_group_list_gui(self): """在主线程中更新群组列表""" # 清除现有按钮 for button in self.group_buttons.values(): button.destroy() self.group_buttons.clear() - + # 创建新的群组按钮 for group_id in self.group_data.keys(): button = ctk.CTkButton( @@ -203,16 +203,16 @@ class ReasoningGUI: ) button.pack(pady=2, padx=5) self.group_buttons[group_id] = button - + # 如果有选中的群组,保持其高亮状态 if self.selected_group_id and self.selected_group_id in self.group_buttons: self._highlight_selected_group(self.selected_group_id) - + def _on_group_select(self, group_id: str): """处理群组选择事件""" self._highlight_selected_group(group_id) self._update_display_gui(group_id) - + def _highlight_selected_group(self, group_id: str): """高亮显示选中的群组按钮""" # 重置所有按钮的颜色 @@ -223,9 +223,9 @@ class ReasoningGUI: else: # 恢复其他按钮的默认颜色 button.configure(fg_color="#2B2B2B", hover_color="#404040") - + self.selected_group_id = group_id - + def _update_display_gui(self, group_id: str): """在主线程中更新显示内容""" if group_id in self.group_data: @@ -234,19 +234,19 @@ class ReasoningGUI: # 时间戳 time_str = item['time'].strftime("%Y-%m-%d %H:%M:%S") self.content_text.insert("end", f"[{time_str}]\n", "timestamp") - + # 用户信息 self.content_text.insert("end", "用户: ", "timestamp") self.content_text.insert("end", f"{item.get('user', '未知')}\n", "user") - + # 消息内容 self.content_text.insert("end", "消息: ", "timestamp") self.content_text.insert("end", f"{item.get('message', '')}\n", "message") - + # 模型信息 self.content_text.insert("end", "模型: ", "timestamp") self.content_text.insert("end", f"{item.get('model', '')}\n", "model") - + # Prompt内容 self.content_text.insert("end", "Prompt内容:\n", "timestamp") prompt_text = item.get('prompt', '') @@ -257,7 +257,7 @@ class ReasoningGUI: self.content_text.insert("end", " " + line + "\n", "prompt") else: self.content_text.insert("end", " 无Prompt内容\n", "prompt") - + # 推理过程 self.content_text.insert("end", "推理过程:\n", "timestamp") reasoning_text = item.get('reasoning', '') @@ -268,53 +268,53 @@ class ReasoningGUI: self.content_text.insert("end", " " + line + "\n", "reasoning") else: self.content_text.insert("end", " 无推理过程\n", "reasoning") - + # 回复内容 self.content_text.insert("end", "回复: ", "timestamp") self.content_text.insert("end", f"{item.get('response', '')}\n", "response") - + # 分隔符 - self.content_text.insert("end", f"\n{'='*50}\n\n", "separator") - + self.content_text.insert("end", f"\n{'=' * 50}\n\n", "separator") + # 滚动到顶部 self.content_text.see("1.0") - + def _auto_update(self): """自动更新函数""" while True: try: # 从数据库获取最新数据,只获取启动时间之后的记录 query = {"time": {"$gt": self.start_timestamp}} - print(f"查询条件: {query}") - + logger.debug(f"查询条件: {query}") + # 先获取一条记录检查时间格式 sample = self.db.reasoning_logs.find_one() if sample: - print(f"样本记录时间格式: {type(sample['time'])} 值: {sample['time']}") - + logger.debug(f"样本记录时间格式: {type(sample['time'])} 值: {sample['time']}") + cursor = self.db.reasoning_logs.find(query).sort("time", -1) new_data = {} total_count = 0 - + for item in cursor: # 调试输出 if total_count == 0: - print(f"记录时间: {item['time']}, 类型: {type(item['time'])}") - + logger.debug(f"记录时间: {item['time']}, 类型: {type(item['time'])}") + total_count += 1 group_id = str(item.get('group_id', 'unknown')) if group_id not in new_data: new_data[group_id] = [] - + # 转换时间戳为datetime对象 if isinstance(item['time'], (int, float)): time_obj = datetime.fromtimestamp(item['time']) elif isinstance(item['time'], datetime): time_obj = item['time'] else: - print(f"未知的时间格式: {type(item['time'])}") + logger.warning(f"未知的时间格式: {type(item['time'])}") time_obj = datetime.now() # 使用当前时间作为后备 - + new_data[group_id].append({ 'time': time_obj, 'user': item.get('user', '未知'), @@ -324,13 +324,13 @@ class ReasoningGUI: 'response': item.get('response', ''), 'prompt': item.get('prompt', '') # 添加prompt字段 }) - - print(f"从数据库加载了 {total_count} 条记录,分布在 {len(new_data)} 个群组中") - + + logger.info(f"从数据库加载了 {total_count} 条记录,分布在 {len(new_data)} 个群组中") + # 更新数据 if new_data != self.group_data: self.group_data = new_data - print("数据已更新,正在刷新显示...") + logger.info("数据已更新,正在刷新显示...") # 将更新任务添加到队列 self.update_queue.put({'type': 'update_group_list'}) if self.group_data: @@ -341,16 +341,16 @@ class ReasoningGUI: 'type': 'update_display', 'group_id': self.selected_group_id }) - except Exception as e: - print(f"自动更新出错: {e}") - + except Exception: + logger.exception(f"自动更新出错") + # 每5秒更新一次 time.sleep(5) - + def clear_display(self): """清除显示内容""" self.content_text.delete("1.0", "end") - + def run(self): """运行GUI""" self.root.mainloop() @@ -359,18 +359,17 @@ class ReasoningGUI: def main(): """主函数""" Database.initialize( - host= os.getenv("MONGODB_HOST"), - port= int(os.getenv("MONGODB_PORT")), - db_name= os.getenv("DATABASE_NAME"), - username= os.getenv("MONGODB_USERNAME"), - password= os.getenv("MONGODB_PASSWORD"), + host=os.getenv("MONGODB_HOST"), + port=int(os.getenv("MONGODB_PORT")), + db_name=os.getenv("DATABASE_NAME"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), auth_source=os.getenv("MONGODB_AUTH_SOURCE") ) - + app = ReasoningGUI() app.run() - if __name__ == "__main__": main() diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 02fccc863..c37d23a46 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -135,7 +135,7 @@ class BotConfig: try: config_version: str = toml["inner"]["version"] except KeyError as e: - logger.error(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") + logger.error(f"配置文件中 inner 段 不存在, 这是错误的配置文件") raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") else: toml["inner"] = {"version": "0.0.0"} @@ -246,11 +246,11 @@ class BotConfig: try: cfg_target[i] = cfg_item[i] except KeyError as e: - logger.error(f"{item} 中的必要字段 {e} 不存在,请检查") + logger.error(f"{item} 中的必要字段不存在,请检查") raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") provider = cfg_item.get("provider") - if provider == None: + if provider is None: logger.error(f"provider 字段在模型配置 {item} 中不存在,请检查") raise KeyError(f"provider 字段在模型配置 {item} 中不存在,请检查") diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 1236bb2e0..f40c9a441 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -93,8 +93,8 @@ class ResponseGenerator: # 生成回复 try: content, reasoning_content = await model.generate_response(prompt) - except Exception as e: - logger.exception(f"生成回复时出错: {e}") + except Exception: + logger.exception(f"生成回复时出错") return None # 保存到数据库 @@ -145,8 +145,8 @@ class ResponseGenerator: else: return ["neutral"] - except Exception as e: - logger.exception(f"获取情感标签时出错: {e}") + except Exception: + logger.exception(f"获取情感标签时出错") return ["neutral"] async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index b9506b455..4abbd3b3f 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -119,8 +119,8 @@ class MessageContainer: self.messages.remove(message) return True return False - except Exception as e: - logger.exception(f"移除消息时发生错误: {e}") + except Exception: + logger.exception(f"移除消息时发生错误") return False def has_messages(self) -> bool: @@ -213,8 +213,8 @@ class MessageManager: # 安全地移除消息 if not container.remove_message(msg): logger.warning("尝试删除不存在的消息") - except Exception as e: - logger.exception(f"处理超时消息时发生错误: {e}") + except Exception: + logger.exception(f"处理超时消息时发生错误") continue async def start_processor(self): diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 6a87480b7..1c2d05071 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -2,12 +2,13 @@ from typing import Optional from ...common.database import Database from .message import Message +from loguru import logger class MessageStorage: def __init__(self): self.db = Database.get_instance() - + async def store_message(self, message: Message, topic: Optional[str] = None) -> None: """存储消息到数据库""" try: @@ -41,9 +42,9 @@ class MessageStorage: "topic": topic, "detailed_plain_text": message.detailed_plain_text, } - - self.db.db.messages.insert_one(message_data) - except Exception as e: - print(f"\033[1;31m[错误]\033[0m 存储消息失败: {e}") -# 如果需要其他存储相关的函数,可以在这里添加 \ No newline at end of file + self.db.db.messages.insert_one(message_data) + except Exception: + logger.exception(f"存储消息失败") + +# 如果需要其他存储相关的函数,可以在这里添加 diff --git a/src/plugins/memory_system/draw_memory.py b/src/plugins/memory_system/draw_memory.py index c2d04064d..6da330d95 100644 --- a/src/plugins/memory_system/draw_memory.py +++ b/src/plugins/memory_system/draw_memory.py @@ -7,6 +7,7 @@ import jieba import matplotlib.pyplot as plt import networkx as nx from dotenv import load_dotenv +from loguru import logger sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 from src.common.database import Database # 使用正确的导入语法 @@ -15,15 +16,15 @@ from src.common.database import Database # 使用正确的导入语法 env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), '.env.dev') load_dotenv(env_path) - + class Memory_graph: def __init__(self): self.G = nx.Graph() # 使用 networkx 的图结构 self.db = Database.get_instance() - + def connect_dot(self, concept1, concept2): self.G.add_edge(concept1, concept2) - + def add_dot(self, concept, memory): if concept in self.G: # 如果节点已存在,将新记忆添加到现有列表中 @@ -37,7 +38,7 @@ class Memory_graph: else: # 如果是新节点,创建新的记忆列表 self.G.add_node(concept, memory_items=[memory]) - + def get_dot(self, concept): # 检查节点是否存在于图中 if concept in self.G: @@ -45,20 +46,20 @@ class Memory_graph: node_data = self.G.nodes[concept] # print(node_data) # 创建新的Memory_dot对象 - return concept,node_data + return concept, node_data return None def get_related_item(self, topic, depth=1): if topic not in self.G: return [], [] - + first_layer_items = [] second_layer_items = [] - + # 获取相邻节点 neighbors = list(self.G.neighbors(topic)) # print(f"第一层: {topic}") - + # 获取当前节点的记忆项 node_data = self.get_dot(topic) if node_data: @@ -69,7 +70,7 @@ class Memory_graph: first_layer_items.extend(memory_items) else: first_layer_items.append(memory_items) - + # 只在depth=2时获取第二层记忆 if depth >= 2: # 获取相邻节点的记忆项 @@ -84,42 +85,44 @@ class Memory_graph: second_layer_items.extend(memory_items) else: second_layer_items.append(memory_items) - + return first_layer_items, second_layer_items - + def store_memory(self): for node in self.G.nodes(): dot_data = { "concept": node } self.db.db.store_memory_dots.insert_one(dot_data) - + @property def dots(self): # 返回所有节点对应的 Memory_dot 对象 return [self.get_dot(node) for node in self.G.nodes()] - - + def get_random_chat_from_db(self, length: int, timestamp: str): # 从数据库中根据时间戳获取离其最近的聊天记录 chat_text = '' closest_record = self.db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) # 调试输出 - print(f"距离time最近的消息时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(closest_record['time'])))}") - + logger.info( + f"距离time最近的消息时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(closest_record['time'])))}") + if closest_record: closest_time = closest_record['time'] group_id = closest_record['group_id'] # 获取groupid # 获取该时间戳之后的length条消息,且groupid相同 - chat_record = list(self.db.db.messages.find({"time": {"$gt": closest_time}, "group_id": group_id}).sort('time', 1).limit(length)) + chat_record = list( + self.db.db.messages.find({"time": {"$gt": closest_time}, "group_id": group_id}).sort('time', 1).limit( + length)) for record in chat_record: time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(record['time']))) try: - displayname="[(%s)%s]%s" % (record["user_id"],record["user_nickname"],record["user_cardname"]) + displayname = "[(%s)%s]%s" % (record["user_id"], record["user_nickname"], record["user_cardname"]) except: - displayname=record["user_nickname"] or "用户" + str(record["user_id"]) + displayname = record["user_nickname"] or "用户" + str(record["user_id"]) chat_text += f'[{time_str}] {displayname}: {record["processed_plain_text"]}\n' # 添加发送者和时间信息 return chat_text - + return [] # 如果没有找到记录,返回空列表 def save_graph_to_db(self): @@ -166,53 +169,54 @@ def main(): password=os.getenv("MONGODB_PASSWORD", ""), auth_source=os.getenv("MONGODB_AUTH_SOURCE", "") ) - + memory_graph = Memory_graph() memory_graph.load_graph_from_db() - + # 只显示一次优化后的图形 visualize_graph_lite(memory_graph) - + while True: query = input("请输入新的查询概念(输入'退出'以结束):") if query.lower() == '退出': break first_layer_items, second_layer_items = memory_graph.get_related_item(query) if first_layer_items or second_layer_items: - print("\n第一层记忆:") + logger.debug("第一层记忆:") for item in first_layer_items: - print(item) - print("\n第二层记忆:") + logger.debug(item) + logger.debug("第二层记忆:") for item in second_layer_items: - print(item) + logger.debug(item) else: - print("未找到相关记忆。") - + logger.debug("未找到相关记忆。") + def segment_text(text): seg_text = list(jieba.cut(text)) - return seg_text + return seg_text + def find_topic(text, topic_num): prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个话题,帮我列出来,用逗号隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要告诉我其他内容。' return prompt + def topic_what(text, topic): prompt = f'这是一段文字:{text}。我想知道这记忆里有什么关于{topic}的话题,帮我总结成一句自然的话,可以包含时间和人物。只输出这句话就好' return prompt - def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 - + G = memory_graph.G - + # 创建一个新图用于可视化 H = G.copy() - + # 移除只有一条记忆的节点和连接数少于3的节点 nodes_to_remove = [] for node in H.nodes(): @@ -221,14 +225,14 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal degree = H.degree(node) if memory_count < 3 or degree < 2: # 改为小于2而不是小于等于2 nodes_to_remove.append(node) - + H.remove_nodes_from(nodes_to_remove) - + # 如果过滤后没有节点,则返回 if len(H.nodes()) == 0: - print("过滤后没有符合条件的节点可显示") + logger.debug("过滤后没有符合条件的节点可显示") return - + # 保存图到本地 # nx.write_gml(H, "memory_graph.gml") # 保存为 GML 格式 @@ -236,7 +240,7 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal node_colors = [] node_sizes = [] nodes = list(H.nodes()) - + # 获取最大记忆数和最大度数用于归一化 max_memories = 1 max_degree = 1 @@ -246,7 +250,7 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal degree = H.degree(node) max_memories = max(max_memories, memory_count) max_degree = max(max_degree, degree) - + # 计算每个节点的大小和颜色 for node in nodes: # 计算节点大小(基于记忆数量) @@ -254,9 +258,9 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) # 使用指数函数使变化更明显 ratio = memory_count / max_memories - size = 500 + 5000 * (ratio ) # 使用1.5次方函数使差异不那么明显 + size = 500 + 5000 * (ratio) # 使用1.5次方函数使差异不那么明显 node_sizes.append(size) - + # 计算节点颜色(基于连接数) degree = H.degree(node) # 红色分量随着度数增加而增加 @@ -267,26 +271,25 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal # blue = 1 color = (red, 0.1, blue) node_colors.append(color) - + # 绘制图形 plt.figure(figsize=(12, 8)) pos = nx.spring_layout(H, k=1, iterations=50) # 增加k值使节点分布更开 - nx.draw(H, pos, - with_labels=True, - node_color=node_colors, - node_size=node_sizes, - font_size=10, - font_family='SimHei', - font_weight='bold', - edge_color='gray', - width=0.5, - alpha=0.9) - + nx.draw(H, pos, + with_labels=True, + node_color=node_colors, + node_size=node_sizes, + font_size=10, + font_family='SimHei', + font_weight='bold', + edge_color='gray', + width=0.5, + alpha=0.9) + title = '记忆图谱可视化 - 节点大小表示记忆数量,颜色表示连接数' plt.title(title, fontsize=16, fontfamily='SimHei') plt.show() - - - + + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index f4100e0bb..fe8e1a100 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -45,7 +45,7 @@ class LLM_request: self.db.db.llm_usage.create_index([("user_id", 1)]) self.db.db.llm_usage.create_index([("request_type", 1)]) except Exception as e: - logger.error(f"创建数据库索引失败: {e}") + logger.error(f"创建数据库索引失败") def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, user_id: str = "system", request_type: str = "chat", @@ -79,8 +79,8 @@ class LLM_request: f"提示词: {prompt_tokens}, 完成: {completion_tokens}, " f"总计: {total_tokens}" ) - except Exception as e: - logger.error(f"记录token使用情况失败: {e}") + except Exception: + logger.error(f"记录token使用情况失败") def _calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float: """计算API调用成本 @@ -226,8 +226,8 @@ class LLM_request: if delta_content is None: delta_content = "" accumulated_content += delta_content - except Exception as e: - logger.error(f"解析流式输出错误: {e}") + except Exception: + logger.exception(f"解析流式输出错") content = accumulated_content reasoning_content = "" think_match = re.search(r'(.*?)', content, re.DOTALL) diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index d7248e869..a071355a3 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -3,6 +3,7 @@ import time from collections import defaultdict from datetime import datetime, timedelta from typing import Any, Dict +from loguru import logger from ...common.database import Database @@ -153,8 +154,8 @@ class LLMStatistics: try: all_stats = self._collect_all_statistics() self._save_statistics(all_stats) - except Exception as e: - print(f"\033[1;31m[错误]\033[0m 统计数据处理失败: {e}") + except Exception: + logger.exception(f"统计数据处理失败") # 等待1分钟 for _ in range(60): From e1b484ae61249febf66692cc5c709f5bba9c689c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 13:53:17 +0900 Subject: [PATCH 033/105] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0CLAUDE.md?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=8C=87=E5=8D=97=E6=96=87=E4=BB=B6=EF=BC=88?= =?UTF-8?q?=E7=94=A8=E4=BA=8EClaude=20Code=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..d30b0e651 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,48 @@ +# MaiMBot 开发指南 + +## 🛠️ 常用命令 + +- **运行机器人**: `python run.py` 或 `python bot.py` +- **安装依赖**: `pip install --upgrade -r requirements.txt` +- **Docker 部署**: `docker-compose up` +- **代码检查**: `ruff check .` +- **代码格式化**: `ruff format .` +- **内存可视化**: `run_memory_vis.bat` 或 `python -m src.plugins.memory_system.draw_memory` +- **推理过程可视化**: `script/run_thingking.bat` + +## 🔧 脚本工具 + +- **运行MongoDB**: `script/run_db.bat` - 在端口27017启动MongoDB +- **Windows完整启动**: `script/run_windows.bat` - 检查Python版本、设置虚拟环境、安装依赖并运行机器人 +- **快速启动**: `script/run_maimai.bat` - 设置UTF-8编码并执行"nb run"命令 + +## 📝 代码风格 + +- **Python版本**: 3.9+ +- **行长度限制**: 88字符 +- **命名规范**: + - `snake_case` 用于函数和变量 + - `PascalCase` 用于类 + - `_prefix` 用于私有成员 +- **导入顺序**: 标准库 → 第三方库 → 本地模块 +- **异步编程**: 对I/O操作使用async/await +- **日志记录**: 使用loguru进行一致的日志记录 +- **错误处理**: 使用带有具体异常的try/except +- **文档**: 为类和公共函数编写docstrings + +## 🧩 系统架构 + +- **框架**: NoneBot2框架与插件架构 +- **数据库**: MongoDB持久化存储 +- **设计模式**: 工厂模式和单例管理器 +- **配置管理**: 使用环境变量和TOML文件 +- **内存系统**: 基于图的记忆结构,支持记忆构建、压缩、检索和遗忘 +- **情绪系统**: 情绪模拟与概率权重 +- **LLM集成**: 支持多个LLM服务提供商(ChatAnywhere, SiliconFlow, DeepSeek) + +## ⚙️ 环境配置 + +- 使用`template.env`作为环境变量模板 +- 使用`template/bot_config_template.toml`作为机器人配置模板 +- MongoDB配置: 主机、端口、数据库名 +- API密钥配置: 各LLM提供商的API密钥 From c9f12446c0a24a8bc3eaea9f88157389c1360cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 13:54:15 +0900 Subject: [PATCH 034/105] =?UTF-8?q?docs:=20=E6=94=B9=E8=BF=9BREADME.md?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=A0=BC=E5=BC=8F=E5=92=8C=E6=8E=92=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一空行格式 - 修复标题标记一致性 - 优化列表格式和标题间距 - 添加缺失的章节分隔符 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0c02d1cba..55f8e9f82 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# 麦麦!MaiMBot (编辑中) - +# 麦麦!MaiMBot (编辑中)
@@ -30,6 +29,7 @@
> ⚠️ **注意事项** +> > - 项目处于活跃开发阶段,代码可能随时更改 > - 文档未完善,有问题可以提交 Issue 或者 Discussion > - QQ机器人存在被限制风险,请自行了解,谨慎使用 @@ -44,7 +44,6 @@ - (由 [CabLate](https://github.com/cablate) 贡献) [Telegram 与其他平台(未来可能会有)的版本](https://github.com/cablate/MaiMBot/tree/telegram) - [集中讨论串](https://github.com/SengokuCola/MaiMBot/discussions/149) -##

📚 文档 ⬇️ 快速开始使用麦麦 ⬇️

@@ -55,15 +54,14 @@ - [🐳 Docker部署指南](docs/docker_deploy.md) - - [📦 手动部署指南 Windows](docs/manual_deploy_windows.md) - - [📦 手动部署指南 Linux](docs/manual_deploy_linux.md) -- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 +- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 ### 配置说明 + - [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘 - [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户 @@ -76,6 +74,7 @@ ## 🎯 功能介绍 ### 💬 聊天功能 + - 支持关键词检索主动发言:对消息的话题topic进行识别,如果检测到麦麦存储过的话题就会主动进行发言 - 支持bot名字呼唤发言:检测到"麦麦"会主动发言,可配置 - 支持多模型,多厂商自定义配置 @@ -84,31 +83,33 @@ - 错别字和多条回复功能:麦麦可以随机生成错别字,会多条发送回复以及对消息进行reply ### 😊 表情包功能 + - 支持根据发言内容发送对应情绪的表情包 - 会自动偷群友的表情包 ### 📅 日程功能 + - 麦麦会自动生成一天的日程,实现更拟人的回复 ### 🧠 记忆功能 + - 对聊天记录进行概括存储,在需要时调用,待完善 ### 📚 知识库功能 + - 基于embedding模型的知识库,手动放入txt会自动识别,写完了,暂时禁用 ### 👥 关系功能 + - 针对每个用户创建"关系",可以对不同用户进行个性化回复,目前只有极其简单的好感度(WIP) - 针对每个群创建"群印象",可以对不同群进行个性化回复(WIP) - - ## 开发计划TODO:LIST 规划主线 0.6.0:记忆系统更新 0.7.0: 麦麦RunTime - - 人格功能:WIP - 群氛围功能:WIP - 图片发送,转发功能:WIP @@ -128,7 +129,6 @@ - 采用截断生成加快麦麦的反应速度 - 改进发送消息的触发 - ## 设计理念 - **千石可乐说:** @@ -138,13 +138,14 @@ - 如果人类真的需要一个AI来陪伴自己,并不是所有人都需要一个完美的,能解决所有问题的helpful assistant,而是一个会犯错的,拥有自己感知和想法的"生命形式"。 - 代码会保持开源和开放,但个人希望MaiMbot的运行时数据保持封闭,尽量避免以显式命令来对其进行控制和调试.我认为一个你无法完全掌控的个体才更能让你感觉到它的自主性,而视其成为一个对话机器. - ## 📌 注意事项 + SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 > ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 ## 致谢 + [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架 [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现 @@ -156,6 +157,6 @@ SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包 - ## Stargazers over time + [![Stargazers over time](https://starchart.cc/SengokuCola/MaiMBot.svg?variant=adaptive)](https://starchart.cc/SengokuCola/MaiMBot) From 8a32d184605ec256541fc8d9b3bcee2dae2967ff Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Mon, 10 Mar 2025 13:09:01 +0800 Subject: [PATCH 035/105] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96willing=5Fman?= =?UTF-8?q?ager=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E4=BF=9D=E5=BA=95=E6=A6=82=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/willing_manager.py | 111 +++++++++++++++++----------- 1 file changed, 69 insertions(+), 42 deletions(-) diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 4f8bec7dc..116ee3f87 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -1,6 +1,6 @@ import asyncio -from .config import global_config from loguru import logger +from .config import global_config class WillingManager: @@ -8,74 +8,100 @@ class WillingManager: self.group_reply_willing = {} # 存储每个群的回复意愿 self._decay_task = None self._started = False - + self.min_reply_willing = 0.01 + self.attenuation_coefficient = 0.75 + async def _decay_reply_willing(self): """定期衰减回复意愿""" while True: await asyncio.sleep(5) for group_id in self.group_reply_willing: - self.group_reply_willing[group_id] = max(0, self.group_reply_willing[group_id] * 0.6) - + self.group_reply_willing[group_id] = max( + self.min_reply_willing, + self.group_reply_willing[group_id] * self.attenuation_coefficient + ) + def get_willing(self, group_id: int) -> float: """获取指定群组的回复意愿""" return self.group_reply_willing.get(group_id, 0) - + def set_willing(self, group_id: int, willing: float): """设置指定群组的回复意愿""" self.group_reply_willing[group_id] = willing - - def change_reply_willing_received(self, group_id: int, topic: str, is_mentioned_bot: bool, config, user_id: int = None, is_emoji: bool = False, interested_rate: float = 0) -> float: - """改变指定群组的回复意愿并返回回复概率""" + + def change_reply_willing_received(self, group_id: int, topic: str, is_mentioned_bot: bool, config, + user_id: int = None, is_emoji: bool = False, interested_rate: float = 0) -> float: + + # 若非目标回复群组,则直接return + if group_id not in config.talk_allowed_groups: + reply_probability = 0 + return reply_probability + current_willing = self.group_reply_willing.get(group_id, 0) - - # print(f"初始意愿: {current_willing}") - if is_mentioned_bot and current_willing < 1.0: - current_willing += 0.9 - logger.info(f"被提及, 当前意愿: {current_willing}") - elif is_mentioned_bot: - current_willing += 0.05 - logger.info(f"被重复提及, 当前意愿: {current_willing}") - + + logger.debug(f"[{group_id}]的初始回复意愿: {current_willing}") + + # 根据消息类型(被cue/表情包)调控 + if is_mentioned_bot: + current_willing = min( + 3.0, + current_willing + 0.9 + ) + logger.debug(f"被提及, 当前意愿: {current_willing}") + if is_emoji: current_willing *= 0.1 - logger.info(f"表情包, 当前意愿: {current_willing}") - - logger.debug(f"放大系数_interested_rate: {global_config.response_interested_rate_amplifier}") - interested_rate *= global_config.response_interested_rate_amplifier #放大回复兴趣度 - if interested_rate > 0.4: - # print(f"兴趣度: {interested_rate}, 当前意愿: {current_willing}") - current_willing += interested_rate-0.4 - - current_willing *= global_config.response_willing_amplifier #放大回复意愿 - # print(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") - - reply_probability = max((current_willing - 0.45) * 2, 0) - if group_id not in config.talk_allowed_groups: - current_willing = 0 - reply_probability = 0 - + logger.debug(f"表情包, 当前意愿: {current_willing}") + + # 兴趣放大系数,若兴趣 > 0.4则增加回复概率 + interested_rate_amplifier = global_config.response_interested_rate_amplifier + logger.debug(f"放大系数_interested_rate: {interested_rate_amplifier}") + interested_rate *= interested_rate_amplifier + + current_willing += max( + 0.0, + interested_rate - 0.4 + ) + + # 回复意愿系数调控,独立乘区 + willing_amplifier = max( + global_config.response_willing_amplifier, + self.min_reply_willing + ) + current_willing *= willing_amplifier + logger.debug(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") + + # 回复概率迭代,保底0.01回复概率 + reply_probability = max( + (current_willing - 0.45) * 2, + self.min_reply_willing + ) + + # 降低目标低频群组回复概率 + down_frequency_rate = max( + 1.0, + global_config.down_frequency_rate + ) if group_id in config.talk_frequency_down_groups: - reply_probability = reply_probability / global_config.down_frequency_rate + reply_probability = reply_probability / down_frequency_rate reply_probability = min(reply_probability, 1) - if reply_probability < 0: - reply_probability = 0 - - + self.group_reply_willing[group_id] = min(current_willing, 3.0) + logger.debug(f"当前群组{group_id}回复概率:{reply_probability}") return reply_probability - + def change_reply_willing_sent(self, group_id: int): """开始思考后降低群组的回复意愿""" current_willing = self.group_reply_willing.get(group_id, 0) self.group_reply_willing[group_id] = max(0, current_willing - 2) - + def change_reply_willing_after_sent(self, group_id: int): """发送消息后提高群组的回复意愿""" current_willing = self.group_reply_willing.get(group_id, 0) if current_willing < 1: self.group_reply_willing[group_id] = min(1, current_willing + 0.2) - + async def ensure_started(self): """确保衰减任务已启动""" if not self._started: @@ -83,5 +109,6 @@ class WillingManager: self._decay_task = asyncio.create_task(self._decay_reply_willing()) self._started = True + # 创建全局实例 -willing_manager = WillingManager() +willing_manager = WillingManager() From 4baa6c6f0aa2986a8fa6a7f7425a2dcb315d6478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 14:48:43 +0900 Subject: [PATCH 036/105] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0MongoDB=20URI?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E8=BF=9E=E6=8E=A5=EF=BC=8C=E5=B9=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=9E=E6=8E=A5=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/database.py | 36 ++++++-- src/gui/reasoning_gui.py | 53 ++++-------- src/plugins/chat/__init__.py | 1 + src/plugins/chat/utils_image.py | 9 +- src/plugins/knowledege/knowledge_library.py | 5 +- src/plugins/memory_system/draw_memory.py | 7 +- src/plugins/memory_system/memory.py | 8 +- .../memory_system/memory_manual_build.py | 82 ++++++------------- src/plugins/schedule/schedule_generator.py | 2 +- 9 files changed, 82 insertions(+), 121 deletions(-) diff --git a/src/common/database.py b/src/common/database.py index 45ac05dac..f0954b07c 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -6,20 +6,44 @@ from pymongo import MongoClient class Database: _instance: Optional["Database"] = None - def __init__(self, host: str, port: int, db_name: str, username: Optional[str] = None, password: Optional[str] = None, auth_source: Optional[str] = None): - if username and password: + def __init__( + self, + host: str, + port: int, + db_name: str, + username: Optional[str] = None, + password: Optional[str] = None, + auth_source: Optional[str] = None, + uri: Optional[str] = None, + ): + if uri and uri.startswith("mongodb://"): + # 优先使用URI连接 + self.client = MongoClient(uri) + elif username and password: # 如果有用户名和密码,使用认证连接 - # TODO: 复杂情况直接支持URI吧 - self.client = MongoClient(host, port, username=username, password=password, authSource=auth_source) + self.client = MongoClient( + host, port, username=username, password=password, authSource=auth_source + ) else: # 否则使用无认证连接 self.client = MongoClient(host, port) self.db = self.client[db_name] @classmethod - def initialize(cls, host: str, port: int, db_name: str, username: Optional[str] = None, password: Optional[str] = None, auth_source: Optional[str] = None) -> "Database": + def initialize( + cls, + host: str, + port: int, + db_name: str, + username: Optional[str] = None, + password: Optional[str] = None, + auth_source: Optional[str] = None, + uri: Optional[str] = None, + ) -> "Database": if cls._instance is None: - cls._instance = cls(host, port, db_name, username, password, auth_source) + cls._instance = cls( + host, port, db_name, username, password, auth_source, uri + ) return cls._instance @classmethod diff --git a/src/gui/reasoning_gui.py b/src/gui/reasoning_gui.py index 514a95dfb..dd62e0634 100644 --- a/src/gui/reasoning_gui.py +++ b/src/gui/reasoning_gui.py @@ -7,7 +7,7 @@ from datetime import datetime from typing import Dict, List from loguru import logger from typing import Optional -from pymongo import MongoClient +from ..common.database import Database import customtkinter as ctk from dotenv import load_dotenv @@ -28,38 +28,6 @@ else: logger.error("未找到环境配置文件") sys.exit(1) - -class Database: - _instance: Optional["Database"] = None - - def __init__(self, host: str, port: int, db_name: str, username: str = None, password: str = None, - auth_source: str = None): - if username and password: - self.client = MongoClient( - host=host, - port=port, - username=username, - password=password, - authSource=auth_source or 'admin' - ) - else: - self.client = MongoClient(host, port) - self.db = self.client[db_name] - - @classmethod - def initialize(cls, host: str, port: int, db_name: str, username: str = None, password: str = None, - auth_source: str = None) -> "Database": - if cls._instance is None: - cls._instance = cls(host, port, db_name, username, password, auth_source) - return cls._instance - - @classmethod - def get_instance(cls) -> "Database": - if cls._instance is None: - raise RuntimeError("Database not initialized") - return cls._instance - - class ReasoningGUI: def __init__(self): # 记录启动时间戳,转换为Unix时间戳 @@ -83,7 +51,15 @@ class ReasoningGUI: except RuntimeError: logger.warning("数据库未初始化,正在尝试初始化...") try: - Database.initialize("127.0.0.1", 27017, "maimai_bot") + Database.initialize( + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), + ) self.db = Database.get_instance().db logger.success("数据库初始化成功") except Exception: @@ -359,12 +335,13 @@ class ReasoningGUI: def main(): """主函数""" Database.initialize( - host=os.getenv("MONGODB_HOST"), - port=int(os.getenv("MONGODB_PORT")), - db_name=os.getenv("DATABASE_NAME"), + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), username=os.getenv("MONGODB_USERNAME"), password=os.getenv("MONGODB_PASSWORD"), - auth_source=os.getenv("MONGODB_AUTH_SOURCE") + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) app = ReasoningGUI() diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index bd71be019..36d558d10 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -31,6 +31,7 @@ driver = get_driver() config = driver.config Database.initialize( + uri=config.MONGODB_URI, host=config.MONGODB_HOST, port=int(config.MONGODB_PORT), db_name=config.DATABASE_NAME, diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 8a8b3ce5a..7e57560c9 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -37,14 +37,7 @@ def storage_compress_image(base64_data: str, max_size: int = 200) -> str: os.makedirs(images_dir, exist_ok=True) # 连接数据库 - db = Database( - host=config.mongodb_host, - port=int(config.mongodb_port), - db_name=config.database_name, - username=config.mongodb_username, - password=config.mongodb_password, - auth_source=config.mongodb_auth_source - ) + db = Database.get_instance() # 检查是否已存在相同哈希值的图片 collection = db.db['images'] diff --git a/src/plugins/knowledege/knowledge_library.py b/src/plugins/knowledege/knowledge_library.py index 481076961..99e2f842a 100644 --- a/src/plugins/knowledege/knowledge_library.py +++ b/src/plugins/knowledege/knowledge_library.py @@ -19,12 +19,13 @@ from src.common.database import Database # 从环境变量获取配置 Database.initialize( + uri=os.getenv("MONGODB_URI"), host=os.getenv("MONGODB_HOST", "127.0.0.1"), port=int(os.getenv("MONGODB_PORT", "27017")), - db_name=os.getenv("DATABASE_NAME", "maimai"), + db_name=os.getenv("DATABASE_NAME", "MegBot"), username=os.getenv("MONGODB_USERNAME"), password=os.getenv("MONGODB_PASSWORD"), - auth_source=os.getenv("MONGODB_AUTH_SOURCE", "admin") + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) class KnowledgeLibrary: diff --git a/src/plugins/memory_system/draw_memory.py b/src/plugins/memory_system/draw_memory.py index 6da330d95..ffe2ba42c 100644 --- a/src/plugins/memory_system/draw_memory.py +++ b/src/plugins/memory_system/draw_memory.py @@ -162,12 +162,13 @@ class Memory_graph: def main(): # 初始化数据库 Database.initialize( + uri=os.getenv("MONGODB_URI"), host=os.getenv("MONGODB_HOST", "127.0.0.1"), port=int(os.getenv("MONGODB_PORT", "27017")), db_name=os.getenv("DATABASE_NAME", "MegBot"), - username=os.getenv("MONGODB_USERNAME", ""), - password=os.getenv("MONGODB_PASSWORD", ""), - auth_source=os.getenv("MONGODB_AUTH_SOURCE", "") + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) memory_graph = Memory_graph() diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 9b325b36d..b894aa6ff 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -8,6 +8,7 @@ import jieba import networkx as nx from loguru import logger +from nonebot import get_driver from ...common.database import Database # 使用正确的导入语法 from ..chat.config import global_config from ..chat.utils import ( @@ -18,7 +19,6 @@ from ..chat.utils import ( ) from ..models.utils_model import LLM_request - class Memory_graph: def __init__(self): self.G = nx.Graph() # 使用 networkx 的图结构 @@ -130,7 +130,7 @@ class Memory_graph: return None -# 海马体 +# 海马体 class Hippocampus: def __init__(self, memory_graph: Memory_graph): self.memory_graph = memory_graph @@ -749,15 +749,13 @@ def segment_text(text): seg_text = list(jieba.cut(text)) return seg_text - -from nonebot import get_driver - driver = get_driver() config = driver.config start_time = time.time() Database.initialize( + uri=config.MONGODB_URI, host=config.MONGODB_HOST, port=config.MONGODB_PORT, db_name=config.DATABASE_NAME, diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 3c120f21b..3a2961b69 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -35,45 +35,6 @@ else: logger.warning(f"未找到环境变量文件: {env_path}") logger.info("将使用默认配置") -class Database: - _instance = None - db = None - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = cls() - return cls._instance - - def __init__(self): - if not Database.db: - Database.initialize( - host=os.getenv("MONGODB_HOST"), - port=int(os.getenv("MONGODB_PORT")), - db_name=os.getenv("DATABASE_NAME"), - username=os.getenv("MONGODB_USERNAME"), - password=os.getenv("MONGODB_PASSWORD"), - auth_source=os.getenv("MONGODB_AUTH_SOURCE") - ) - - @classmethod - def initialize(cls, host, port, db_name, username=None, password=None, auth_source="admin"): - try: - if username and password: - uri = f"mongodb://{username}:{password}@{host}:{port}/{db_name}?authSource={auth_source}" - else: - uri = f"mongodb://{host}:{port}" - - client = pymongo.MongoClient(uri) - cls.db = client[db_name] - # 测试连接 - client.server_info() - logger.success("MongoDB连接成功!") - - except Exception as e: - logger.error(f"初始化MongoDB失败: {str(e)}") - raise - def calculate_information_content(text): """计算文本的信息量(熵)""" char_count = Counter(text) @@ -202,7 +163,7 @@ class Memory_graph: # 返回所有节点对应的 Memory_dot 对象 return [self.get_dot(node) for node in self.G.nodes()] -# 海马体 +# 海马体 class Hippocampus: def __init__(self, memory_graph: Memory_graph): self.memory_graph = memory_graph @@ -941,59 +902,67 @@ def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = Fal async def main(): # 初始化数据库 logger.info("正在初始化数据库连接...") - db = Database.get_instance() + Database.initialize( + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), + ) start_time = time.time() - + test_pare = {'do_build_memory':False,'do_forget_topic':False,'do_visualize_graph':True,'do_query':False,'do_merge_memory':False} - + # 创建记忆图 memory_graph = Memory_graph() - + # 创建海马体 hippocampus = Hippocampus(memory_graph) - + # 从数据库同步数据 hippocampus.sync_memory_from_db() - + end_time = time.time() logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") - + # 构建记忆 if test_pare['do_build_memory']: logger.info("开始构建记忆...") chat_size = 20 await hippocampus.operation_build_memory(chat_size=chat_size) - + end_time = time.time() logger.info(f"\033[32m[构建记忆耗时: {end_time - start_time:.2f} 秒,chat_size={chat_size},chat_count = 16]\033[0m") - + if test_pare['do_forget_topic']: logger.info("开始遗忘记忆...") await hippocampus.operation_forget_topic(percentage=0.1) - + end_time = time.time() logger.info(f"\033[32m[遗忘记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") - + if test_pare['do_merge_memory']: logger.info("开始合并记忆...") await hippocampus.operation_merge_memory(percentage=0.1) - + end_time = time.time() logger.info(f"\033[32m[合并记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") - + if test_pare['do_visualize_graph']: # 展示优化后的图形 logger.info("生成记忆图谱可视化...") print("\n生成优化后的记忆图谱:") visualize_graph_lite(memory_graph) - + if test_pare['do_query']: # 交互式查询 while True: query = input("\n请输入新的查询概念(输入'退出'以结束):") if query.lower() == '退出': break - + items_list = memory_graph.get_related_item(query) if items_list: first_layer, second_layer = items_list @@ -1008,9 +977,6 @@ async def main(): else: print("未找到相关记忆。") - if __name__ == "__main__": import asyncio asyncio.run(main()) - - diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index fc07a152d..b968d43ca 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -14,6 +14,7 @@ driver = get_driver() config = driver.config Database.initialize( + uri=config.MONGODB_URI, host=config.MONGODB_HOST, port=int(config.MONGODB_PORT), db_name=config.DATABASE_NAME, @@ -22,7 +23,6 @@ Database.initialize( auth_source=config.MONGODB_AUTH_SOURCE ) - class ScheduleGenerator: def __init__(self): # 根据global_config.llm_normal这一字典配置指定模型 From 6e36a56ceecaf5ab8e7a175a6855e4b13fbc0b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 14:50:05 +0900 Subject: [PATCH 037/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20MONGODB=5F?= =?UTF-8?q?URI=20=E7=9A=84=E9=85=8D=E7=BD=AE=E9=A1=B9=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=B0=86=E6=89=80=E6=9C=89env=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=8D=95=E7=8B=AC=E6=94=BE=E5=9C=A8=E4=B8=80?= =?UTF-8?q?=E8=A1=8C=EF=BC=88python=E7=9A=84dotenv=E6=9C=89=E6=97=B6?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=AD=A3=E7=A1=AE=E5=A4=84=E7=90=86=E8=A1=8C?= =?UTF-8?q?=E5=86=85=E6=B3=A8=E9=87=8A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/installation_cute.md | 38 +++++++++++++++++++++++++---------- docs/installation_standard.md | 28 ++++++++++++++++++-------- template.env | 15 +++++++++----- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/docs/installation_cute.md b/docs/installation_cute.md index e7541f7d3..d79b28839 100644 --- a/docs/installation_cute.md +++ b/docs/installation_cute.md @@ -1,8 +1,9 @@ # 🔧 配置指南 喵~ -## 👋 你好呀! +## 👋 你好呀 让咱来告诉你我们要做什么喵: + 1. 我们要一起设置一个可爱的AI机器人 2. 这个机器人可以在QQ上陪你聊天玩耍哦 3. 需要设置两个文件才能让机器人工作呢 @@ -10,16 +11,19 @@ ## 📝 需要设置的文件喵 要设置这两个文件才能让机器人跑起来哦: + 1. `.env.prod` - 这个文件告诉机器人要用哪些AI服务呢 2. `bot_config.toml` - 这个文件教机器人怎么和你聊天喵 ## 🔑 密钥和域名的对应关系 想象一下,你要进入一个游乐园,需要: + 1. 知道游乐园的地址(这就是域名 base_url) 2. 有入场的门票(这就是密钥 key) 在 `.env.prod` 文件里,我们定义了三个游乐园的地址和门票喵: + ```ini # 硅基流动游乐园 SILICONFLOW_KEY=your_key # 硅基流动的门票 @@ -35,6 +39,7 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere的地 ``` 然后在 `bot_config.toml` 里,机器人会用这些门票和地址去游乐园玩耍: + ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" @@ -47,9 +52,10 @@ base_url = "SILICONFLOW_BASE_URL" # 还是去硅基流动游乐园 key = "SILICONFLOW_KEY" # 用同一张门票就可以啦 ``` -### 🎪 举个例子喵: +### 🎪 举个例子喵 如果你想用DeepSeek官方的服务,就要这样改: + ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" @@ -62,7 +68,8 @@ base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园 key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票 ``` -### 🎯 简单来说: +### 🎯 简单来说 + - `.env.prod` 文件就像是你的票夹,存放着各个游乐园的门票和地址 - `bot_config.toml` 就是告诉机器人:用哪张票去哪个游乐园玩 - 所有模型都可以用同一个游乐园的票,也可以去不同的游乐园玩耍 @@ -88,19 +95,25 @@ CHAT_ANY_WHERE_KEY=your_key CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # 如果你不知道这是什么,那么下面这些不用改,保持原样就好啦 -HOST=127.0.0.1 # 如果使用Docker部署,需要改成0.0.0.0喵,不然听不见群友讲话了喵 +# 如果使用Docker部署,需要改成0.0.0.0喵,不然听不见群友讲话了喵 +HOST=127.0.0.1 PORT=8080 # 这些是数据库设置,一般也不用改呢 -MONGODB_HOST=127.0.0.1 # 如果使用Docker部署,需要改成数据库容器的名字喵,默认是mongodb喵 +# 如果使用Docker部署,需要把MONGODB_HOST改成数据库容器的名字喵,默认是mongodb喵 +MONGODB_HOST=127.0.0.1 MONGODB_PORT=27017 DATABASE_NAME=MegBot -MONGODB_USERNAME = "" # 如果数据库需要用户名,就在这里填写喵 -MONGODB_PASSWORD = "" # 如果数据库需要密码,就在这里填写呢 -MONGODB_AUTH_SOURCE = "" # 数据库认证源,一般不用改哦 +# 数据库认证信息,如果需要认证就取消注释并填写下面三行喵 +# MONGODB_USERNAME = "" +# MONGODB_PASSWORD = "" +# MONGODB_AUTH_SOURCE = "" -# 插件设置喵 -PLUGINS=["src2.plugins.chat"] # 这里是机器人的插件列表呢 +# 也可以使用URI连接数据库,取消注释填写在下面这行喵(URI的优先级比上面的高) +# MONGODB_URI=mongodb://127.0.0.1:27017/MegBot + +# 这里是机器人的插件列表呢 +PLUGINS=["src2.plugins.chat"] ``` ### 第二个文件:机器人配置 (bot_config.toml) @@ -198,10 +211,12 @@ key = "SILICONFLOW_KEY" - `topic`: 负责理解对话主题的能力呢 ## 🌟 小提示 + - 如果你刚开始使用,建议保持默认配置呢 - 不同的模型有不同的特长,可以根据需要调整它们的使用比例哦 ## 🌟 小贴士喵 + - 记得要好好保管密钥(key)哦,不要告诉别人呢 - 配置文件要小心修改,改错了机器人可能就不能和你玩了喵 - 如果想让机器人更聪明,可以调整 personality 里的设置呢 @@ -209,7 +224,8 @@ key = "SILICONFLOW_KEY" - QQ群号和QQ号都要用数字填写,不要加引号哦(除了机器人自己的QQ号) ## ⚠️ 注意事项 + - 这个机器人还在测试中呢,可能会有一些小问题喵 - 如果不知道怎么改某个设置,就保持原样不要动它哦~ - 记得要先有AI服务的密钥,不然机器人就不能和你说话了呢 -- 修改完配置后要重启机器人才能生效喵~ \ No newline at end of file +- 修改完配置后要重启机器人才能生效喵~ diff --git a/docs/installation_standard.md b/docs/installation_standard.md index 5f52676d1..cb6c5dff9 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -3,6 +3,7 @@ ## 简介 本项目需要配置两个主要文件: + 1. `.env.prod` - 配置API服务和系统环境 2. `bot_config.toml` - 配置机器人行为和模型 @@ -10,7 +11,8 @@ `.env.prod`和`bot_config.toml`中的API配置关系如下: -### 在.env.prod中定义API凭证: +### 在.env.prod中定义API凭证 + ```ini # API凭证配置 SILICONFLOW_KEY=your_key # 硅基流动API密钥 @@ -23,7 +25,8 @@ CHAT_ANY_WHERE_KEY=your_key # ChatAnyWhere API密钥 CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere API地址 ``` -### 在bot_config.toml中引用API凭证: +### 在bot_config.toml中引用API凭证 + ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" @@ -32,6 +35,7 @@ key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 ``` 如需切换到其他API服务,只需修改引用: + ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" @@ -42,6 +46,7 @@ key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 ## 配置文件详解 ### 环境配置文件 (.env.prod) + ```ini # API配置 SILICONFLOW_KEY=your_key @@ -52,22 +57,29 @@ CHAT_ANY_WHERE_KEY=your_key CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # 服务配置 -HOST=127.0.0.1 # 如果使用Docker部署,需要改成0.0.0.0,否则QQ消息无法传入 + # 如果使用Docker部署,需要改成0.0.0.0,否则QQ消息无法传入 +HOST=127.0.0.1 PORT=8080 # 数据库配置 -MONGODB_HOST=127.0.0.1 # 如果使用Docker部署,需要改成数据库容器的名字,默认是mongodb +# 如果使用Docker部署,需要改成数据库容器的名字,默认是mongodb +MONGODB_HOST=127.0.0.1 MONGODB_PORT=27017 DATABASE_NAME=MegBot -MONGODB_USERNAME = "" # 数据库用户名 -MONGODB_PASSWORD = "" # 数据库密码 -MONGODB_AUTH_SOURCE = "" # 认证数据库 +# 数据库认证信息,如果需要认证就取消注释并填写下面三行 +# MONGODB_USERNAME = "" +# MONGODB_PASSWORD = "" +# MONGODB_AUTH_SOURCE = "" + +# 也可以使用URI连接数据库,取消注释填写在下面这行(URI的优先级比上面的高) +# MONGODB_URI=mongodb://127.0.0.1:27017/MegBot # 插件配置 PLUGINS=["src2.plugins.chat"] ``` ### 机器人配置文件 (bot_config.toml) + ```toml [bot] qq = "机器人QQ号" # 必填 @@ -151,4 +163,4 @@ key = "SILICONFLOW_KEY" 3. 其他说明: - 项目处于测试阶段,可能存在未知问题 - - 建议初次使用保持默认配置 \ No newline at end of file + - 建议初次使用保持默认配置 diff --git a/template.env b/template.env index 09fe63597..d2a763112 100644 --- a/template.env +++ b/template.env @@ -5,13 +5,18 @@ PORT=8080 PLUGINS=["src2.plugins.chat"] # 默认配置 -MONGODB_HOST=127.0.0.1 # 如果工作在Docker下,请改成 MONGODB_HOST=mongodb +# 如果工作在Docker下,请改成 MONGODB_HOST=mongodb +MONGODB_HOST=127.0.0.1 MONGODB_PORT=27017 DATABASE_NAME=MegBot -MONGODB_USERNAME = "" # 默认空值 -MONGODB_PASSWORD = "" # 默认空值 -MONGODB_AUTH_SOURCE = "" # 默认空值 +# 也可以使用 URI 连接数据库(优先级比上面的高) +# MONGODB_URI=mongodb://127.0.0.1:27017/MegBot + +# MongoDB 认证信息,若需要认证,请取消注释以下三行并填写正确的信息 +# MONGODB_USERNAME=user +# MONGODB_PASSWORD=password +# MONGODB_AUTH_SOURCE=admin #key and url CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 @@ -21,4 +26,4 @@ DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 #定义你要用的api的base_url DEEP_SEEK_KEY= CHAT_ANY_WHERE_KEY= -SILICONFLOW_KEY= \ No newline at end of file +SILICONFLOW_KEY= From 8ff7bb6f88b400f972641b3c5c6499b48d020eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 14:50:15 +0900 Subject: [PATCH 038/105] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81=E7=9A=84=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/doc1.md | 48 ++++++++++++++++++++--------------- docs/docker_deploy.md | 21 ++++++++------- docs/manual_deploy_linux.md | 22 +++++++++++++--- docs/manual_deploy_windows.md | 14 ++++++++-- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/docs/doc1.md b/docs/doc1.md index 158136b9c..e8aa0f0d6 100644 --- a/docs/doc1.md +++ b/docs/doc1.md @@ -1,6 +1,7 @@ # 📂 文件及功能介绍 (2025年更新) ## 根目录 + - **README.md**: 项目的概述和使用说明。 - **requirements.txt**: 项目所需的Python依赖包列表。 - **bot.py**: 主启动文件,负责环境配置加载和NoneBot初始化。 @@ -10,6 +11,7 @@ - **run_*.bat**: 各种启动脚本,包括数据库、maimai和thinking功能。 ## `src/` 目录结构 + - **`plugins/` 目录**: 存放不同功能模块的插件。 - **chat/**: 处理聊天相关的功能,如消息发送和接收。 - **memory_system/**: 处理机器人的记忆功能。 @@ -22,94 +24,96 @@ - **`common/` 目录**: 存放通用的工具和库。 - **database.py**: 处理与数据库的交互,负责数据的存储和检索。 - - **__init__.py**: 初始化模块。 + - ****init**.py**: 初始化模块。 ## `config/` 目录 + - **bot_config_template.toml**: 机器人配置模板。 - **auto_format.py**: 自动格式化工具。 ### `src/plugins/chat/` 目录文件详细介绍 -1. **`__init__.py`**: +1. **`__init__.py`**: - 初始化 `chat` 模块,使其可以作为一个包被导入。 -2. **`bot.py`**: +2. **`bot.py`**: - 主要的聊天机器人逻辑实现,处理消息的接收、思考和回复。 - 包含 `ChatBot` 类,负责消息处理流程控制。 - 集成记忆系统和意愿管理。 -3. **`config.py`**: +3. **`config.py`**: - 配置文件,定义了聊天机器人的各种参数和设置。 - 包含 `BotConfig` 和全局配置对象 `global_config`。 -4. **`cq_code.py`**: +4. **`cq_code.py`**: - 处理 CQ 码(CoolQ 码),用于发送和接收特定格式的消息。 -5. **`emoji_manager.py`**: +5. **`emoji_manager.py`**: - 管理表情包的发送和接收,根据情感选择合适的表情。 - 提供根据情绪获取表情的方法。 -6. **`llm_generator.py`**: +6. **`llm_generator.py`**: - 生成基于大语言模型的回复,处理用户输入并生成相应的文本。 - 通过 `ResponseGenerator` 类实现回复生成。 -7. **`message.py`**: +7. **`message.py`**: - 定义消息的结构和处理逻辑,包含多种消息类型: - `Message`: 基础消息类 - `MessageSet`: 消息集合 - `Message_Sending`: 发送中的消息 - `Message_Thinking`: 思考状态的消息 -8. **`message_sender.py`**: +8. **`message_sender.py`**: - 控制消息的发送逻辑,确保消息按照特定规则发送。 - 包含 `message_manager` 对象,用于管理消息队列。 -9. **`prompt_builder.py`**: +9. **`prompt_builder.py`**: - 构建用于生成回复的提示,优化机器人的响应质量。 -10. **`relationship_manager.py`**: +10. **`relationship_manager.py`**: - 管理用户之间的关系,记录用户的互动和偏好。 - 提供更新关系和关系值的方法。 -11. **`Segment_builder.py`**: +11. **`Segment_builder.py`**: - 构建消息片段的工具。 -12. **`storage.py`**: +12. **`storage.py`**: - 处理数据存储,负责将聊天记录和用户信息保存到数据库。 - 实现 `MessageStorage` 类管理消息存储。 -13. **`thinking_idea.py`**: +13. **`thinking_idea.py`**: - 实现机器人的思考机制。 -14. **`topic_identifier.py`**: +14. **`topic_identifier.py`**: - 识别消息中的主题,帮助机器人理解用户的意图。 -15. **`utils.py`** 和 **`utils_*.py`** 系列文件: +15. **`utils.py`** 和 **`utils_*.py`** 系列文件: - 存放各种工具函数,提供辅助功能以支持其他模块。 - 包括 `utils_cq.py`、`utils_image.py`、`utils_user.py` 等专门工具。 -16. **`willing_manager.py`**: +16. **`willing_manager.py`**: - 管理机器人的回复意愿,动态调整回复概率。 - 通过多种因素(如被提及、话题兴趣度)影响回复决策。 ### `src/plugins/memory_system/` 目录文件介绍 -1. **`memory.py`**: +1. **`memory.py`**: - 实现记忆管理核心功能,包含 `memory_graph` 对象。 - 提供相关项目检索,支持多层次记忆关联。 -2. **`draw_memory.py`**: +2. **`draw_memory.py`**: - 记忆可视化工具。 -3. **`memory_manual_build.py`**: +3. **`memory_manual_build.py`**: - 手动构建记忆的工具。 -4. **`offline_llm.py`**: +4. **`offline_llm.py`**: - 离线大语言模型处理功能。 ## 消息处理流程 ### 1. 消息接收与预处理 + - 通过 `ChatBot.handle_message()` 接收群消息。 - 进行用户和群组的权限检查。 - 更新用户关系信息。 @@ -117,12 +121,14 @@ - 对消息进行过滤和敏感词检测。 ### 2. 主题识别与决策 + - 使用 `topic_identifier` 识别消息主题。 - 通过记忆系统检查对主题的兴趣度。 - `willing_manager` 动态计算回复概率。 - 根据概率决定是否回复消息。 ### 3. 回复生成与发送 + - 如需回复,首先创建 `Message_Thinking` 对象表示思考状态。 - 调用 `ResponseGenerator.generate_response()` 生成回复内容和情感状态。 - 删除思考消息,创建 `MessageSet` 准备发送回复。 diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index 3958d2fc4..e33cc6144 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -2,8 +2,7 @@ ## 部署步骤(推荐,但不一定是最新) - -### 1. 获取Docker配置文件: +### 1. 获取Docker配置文件 ```bash wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml @@ -12,56 +11,56 @@ wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.y - 若需要启用MongoDB数据库的用户名和密码,可进入docker-compose.yml,取消MongoDB处的注释并修改变量`=`后方的值为你的用户名和密码\ 修改后请注意在之后配置`.env.prod`文件时指定MongoDB数据库的用户名密码 - -### 2. 启动服务: +### 2. 启动服务 - **!!! 请在第一次启动前确保当前工作目录下`.env.prod`与`bot_config.toml`文件存在 !!!**\ 由于Docker文件映射行为的特殊性,若宿主机的映射路径不存在,可能导致意外的目录创建,而不会创建文件,由于此处需要文件映射到文件,需提前确保文件存在且路径正确,可使用如下命令: + ```bash touch .env.prod touch bot_config.toml ``` - 启动Docker容器: + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d ``` - 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 - -### 3. 修改配置并重启Docker: +### 3. 修改配置并重启Docker - 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\ **需要注意`.env.prod`中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: + ```bash docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名臣 ``` - 下方命令可以但不推荐,只是同时重启NapCat、MongoDB、MaiMBot三个服务 + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart ``` - 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 - ### 4. 登入NapCat管理页添加反向WebSocket - 在浏览器地址栏输入`http://<宿主机IP>:6099/`进入NapCat的管理Web页,添加一个Websocket客户端 + > 网络配置 -> 新建 -> Websocket客户端 - Websocket客户端的名称自定,URL栏填入`ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ (若修改过容器名称则替换maimbot为你自定的名称) - -### 5. 愉快地和麦麦对话吧! - +### 5. 愉快地和麦麦对话吧 ## ⚠️ 注意事项 - 目前部署方案仍在测试中,可能存在未知问题 - 配置文件中的API密钥请妥善保管,不要泄露 -- 建议先在测试环境中运行,确认无误后再部署到生产环境 \ No newline at end of file +- 建议先在测试环境中运行,确认无误后再部署到生产环境 diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index d310ffc59..a859a30e7 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -1,6 +1,7 @@ # 📦 Linux系统如何手动部署MaiMbot麦麦? ## 准备工作 + - 一台联网的Linux设备(本教程以Ubuntu/Debian系为例) - QQ小号(QQ框架的使用可能导致qq被风控,严重(小概率)可能会导致账号封禁,强烈不推荐使用大号) - 可用的大模型API @@ -20,6 +21,7 @@ - 数据库是什么?如何安装并启动MongoDB - 如何运行一个QQ机器人,以及NapCat框架是什么 + --- ## 环境配置 @@ -33,7 +35,9 @@ python --version # 或 python3 --version ``` + 如果版本低于3.9,请更新Python版本。 + ```bash # Ubuntu/Debian sudo apt update @@ -45,6 +49,7 @@ sudo update-alternatives --config python3 ``` ### 2️⃣ **创建虚拟环境** + ```bash # 方法1:使用venv(推荐) python3 -m venv maimbot @@ -65,32 +70,39 @@ pip install -r requirements.txt --- ## 数据库配置 + ### 3️⃣ **安装并启动MongoDB** + - 安装与启动:Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/),Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) - 默认连接本地27017端口 + --- ## NapCat配置 + ### 4️⃣ **安装NapCat框架** - 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装 -- 使用QQ小号登录,添加反向WS地址: +- 使用QQ小号登录,添加反向WS地址: `ws://127.0.0.1:8080/onebot/v11/ws` --- ## 配置文件设置 + ### 5️⃣ **配置文件设置,让麦麦Bot正常工作** + - 修改环境配置文件:`.env.prod` - 修改机器人配置文件:`bot_config.toml` - --- ## 启动机器人 + ### 6️⃣ **启动麦麦机器人** + ```bash # 在项目目录下操作 nb run @@ -101,16 +113,18 @@ python3 bot.py --- ## **其他组件(可选)** -- 直接运行 knowledge.py生成知识库 +- 直接运行 knowledge.py生成知识库 --- ## 常见问题 + 🔧 权限问题:在命令前加`sudo` 🔌 端口占用:使用`sudo lsof -i :8080`查看端口占用 🛡️ 防火墙:确保8080/27017端口开放 + ```bash sudo ufw allow 8080/tcp sudo ufw allow 27017/tcp -``` \ No newline at end of file +``` diff --git a/docs/manual_deploy_windows.md b/docs/manual_deploy_windows.md index 86238bcd4..bca46962a 100644 --- a/docs/manual_deploy_windows.md +++ b/docs/manual_deploy_windows.md @@ -30,12 +30,13 @@ 在创建虚拟环境之前,请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装: -1. 访问Python官网下载页面:https://www.python.org/downloads/release/python-3913/ +1. 访问Python官网下载页面: 2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe` 3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项 4. 点击"Install Now"开始安装 或者使用PowerShell自动下载安装(需要管理员权限): + ```powershell # 下载并安装Python 3.9.13 $pythonUrl = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe" @@ -46,7 +47,7 @@ Start-Process -Wait -FilePath $pythonInstaller -ArgumentList "/quiet", "InstallA ### 2️⃣ **创建Python虚拟环境来运行程序** - 你可以选择使用以下两种方法之一来创建Python环境: +> 你可以选择使用以下两种方法之一来创建Python环境: ```bash # ---方法1:使用venv(Python自带) @@ -60,6 +61,7 @@ maimbot\\Scripts\\activate # 安装依赖 pip install -r requirements.txt ``` + ```bash # ---方法2:使用conda # 创建一个新的conda环境(环境名为maimbot) @@ -74,27 +76,35 @@ pip install -r requirements.txt ``` ### 2️⃣ **然后你需要启动MongoDB数据库,来存储信息** + - 安装并启动MongoDB服务 - 默认连接本地27017端口 ### 3️⃣ **配置NapCat,让麦麦bot与qq取得联系** + - 安装并登录NapCat(用你的qq小号) - 添加反向WS:`ws://127.0.0.1:8080/onebot/v11/ws` ### 4️⃣ **配置文件设置,让麦麦Bot正常工作** + - 修改环境配置文件:`.env.prod` - 修改机器人配置文件:`bot_config.toml` ### 5️⃣ **启动麦麦机器人** + - 打开命令行,cd到对应路径 + ```bash nb run ``` + - 或者cd到对应路径后 + ```bash python bot.py ``` ### 6️⃣ **其他组件(可选)** + - `run_thingking.bat`: 启动可视化推理界面(未完善) - 直接运行 knowledge.py生成知识库 From ed505a4c11e066a9ae003d09a86ad1061763c18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Mon, 10 Mar 2025 15:51:39 +0900 Subject: [PATCH 039/105] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=B7=AF=E5=BE=84=E6=9B=BF=E6=8D=A2=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E9=A1=B9=E7=9B=AE=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将内存系统相关文件中硬编码的项目路径(C:/GitHub/MaiMBot)替换为动态计算的相对路径, 提高代码的可移植性和兼容性。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/plugins/memory_system/draw_memory.py | 5 ++++- src/plugins/memory_system/memory_manual_build.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/memory_system/draw_memory.py b/src/plugins/memory_system/draw_memory.py index ffe2ba42c..9f15164f1 100644 --- a/src/plugins/memory_system/draw_memory.py +++ b/src/plugins/memory_system/draw_memory.py @@ -9,7 +9,10 @@ import networkx as nx from dotenv import load_dotenv from loguru import logger -sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 +# 添加项目根目录到 Python 路径 +root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +sys.path.append(root_path) + from src.common.database import Database # 使用正确的导入语法 # 加载.env.dev文件 diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 3a2961b69..9c1d43ce9 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -16,7 +16,10 @@ from loguru import logger import jieba # from chat.config import global_config -sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 +# 添加项目根目录到 Python 路径 +root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +sys.path.append(root_path) + from src.common.database import Database from src.plugins.memory_system.offline_llm import LLMModel From 68b3f578c4331298e69c43e5840b87689dad75c2 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Mon, 10 Mar 2025 20:36:15 +0800 Subject: [PATCH 040/105] Minor Doc Update --- README.md | 15 +++++++-------- docs/installation_cute.md | 4 ++-- docs/installation_standard.md | 2 +- run.bat | 2 +- run.py | 2 ++ 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0c02d1cba..29e25b2b9 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,17 @@ ### 部署方式 -如果你不知道Docker是什么,建议寻找相关教程或使用手动部署(现在不建议使用docker,更新慢,可能不适配) +- 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 + +- [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md) + +- [📦 Linux 手动部署指南 ](docs/manual_deploy_linux.md) + +如果你不知道Docker是什么,建议寻找相关教程或使用手动部署 **(现在不建议使用docker,更新慢,可能不适配)** - [🐳 Docker部署指南](docs/docker_deploy.md) -- [📦 手动部署指南 Windows](docs/manual_deploy_windows.md) - - -- [📦 手动部署指南 Linux](docs/manual_deploy_linux.md) - -- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 - ### 配置说明 - [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘 - [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户 diff --git a/docs/installation_cute.md b/docs/installation_cute.md index e7541f7d3..3a63988f1 100644 --- a/docs/installation_cute.md +++ b/docs/installation_cute.md @@ -52,12 +52,12 @@ key = "SILICONFLOW_KEY" # 用同一张门票就可以啦 如果你想用DeepSeek官方的服务,就要这样改: ```toml [model.llm_reasoning] -name = "Pro/deepseek-ai/DeepSeek-R1" +name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园 key = "DEEP_SEEK_KEY" # 用DeepSeek的门票 [model.llm_normal] -name = "Pro/deepseek-ai/DeepSeek-V3" +name = "deepseek-chat" # 改成对应的模型名称,这里为DeepseekV3 base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园 key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票 ``` diff --git a/docs/installation_standard.md b/docs/installation_standard.md index 5f52676d1..71bfbac77 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -34,7 +34,7 @@ key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 如需切换到其他API服务,只需修改引用: ```toml [model.llm_reasoning] -name = "Pro/deepseek-ai/DeepSeek-R1" +name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务 key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 ``` diff --git a/run.bat b/run.bat index 659a7545a..91904bc34 100644 --- a/run.bat +++ b/run.bat @@ -3,7 +3,7 @@ chcp 65001 if not exist "venv" ( python -m venv venv call venv\Scripts\activate.bat - pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt + pip install -i https://mirrors.aliyun.com/pypi/simple --upgrade -r requirements.txt ) else ( call venv\Scripts\activate.bat ) diff --git a/run.py b/run.py index baea4d13c..50e312c37 100644 --- a/run.py +++ b/run.py @@ -107,6 +107,8 @@ def install_napcat(): napcat_filename = input( "下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell:" ) + if(napcat_filename[-4:] == ".zip"): + napcat_filename = napcat_filename[:-4] extract_files(napcat_filename + ".zip", "napcat") print("NapCat 安装完成") os.remove(napcat_filename + ".zip") From e68040515a97549f879a81b57763d76f515f2aa2 Mon Sep 17 00:00:00 2001 From: Naptie Date: Mon, 10 Mar 2025 20:43:17 +0800 Subject: [PATCH 041/105] fix: typo 'discription' --- src/plugins/chat/bot.py | 4 ++-- src/plugins/chat/emoji_manager.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c510fe4bf..415bdd6bb 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -178,7 +178,7 @@ class ChatBot: # 检查是否 <没有找到> emoji if emoji_raw != None: - emoji_path, discription = emoji_raw + emoji_path, description = emoji_raw emoji_cq = CQCode.create_emoji_cq(emoji_path) @@ -194,7 +194,7 @@ class ChatBot: raw_message=emoji_cq, plain_text=emoji_cq, processed_plain_text=emoji_cq, - detailed_plain_text=discription, + detailed_plain_text=description, user_nickname=global_config.BOT_NICKNAME, group_name=message.group_name, time=bot_response_time, diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 973bcad2d..dcb702c03 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -117,7 +117,7 @@ class EmojiManager: try: # 获取所有表情包 - all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'discription': 1})) + all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1})) if not all_emojis: logger.warning("数据库中没有任何表情包") @@ -160,9 +160,9 @@ class EmojiManager: {'$inc': {'usage_count': 1}} ) logger.success( - f"找到匹配的表情包: {selected_emoji.get('discription', '无描述')} (相似度: {similarity:.4f})") + f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})") # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 - return selected_emoji['path'], "[ %s ]" % selected_emoji.get('discription', '无描述') + return selected_emoji['path'], "[ %s ]" % selected_emoji.get('description', '无描述') except Exception as search_error: logger.error(f"搜索表情包失败: {str(search_error)}") @@ -174,7 +174,7 @@ class EmojiManager: logger.error(f"获取表情包失败: {str(e)}") return None - async def _get_emoji_discription(self, image_base64: str) -> str: + async def _get_emoji_description(self, image_base64: str) -> str: """获取表情包的标签""" try: prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感' @@ -236,31 +236,31 @@ class EmojiManager: continue # 获取表情包的描述 - discription = await self._get_emoji_discription(image_base64) + description = await self._get_emoji_description(image_base64) if global_config.EMOJI_CHECK: check = await self._check_emoji(image_base64) if '是' not in check: os.remove(image_path) - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") logger.info(f"其不满足过滤规则,被剔除 {check}") continue logger.info(f"check通过 {check}") - if discription is not None: - embedding = await get_embedding(discription) + if description is not None: + embedding = await get_embedding(description) # 准备数据库记录 emoji_record = { 'filename': filename, 'path': image_path, 'embedding': embedding, - 'discription': discription, + 'description': description, 'timestamp': int(time.time()) } # 保存到数据库 self.db.db['emoji'].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") else: logger.warning(f"跳过表情包: {filename}") From 2ffdfef02e7e87357b472c1b9c080316f9e44607 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Mon, 10 Mar 2025 20:45:12 +0800 Subject: [PATCH 042/105] More --- docs/installation_standard.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation_standard.md b/docs/installation_standard.md index 71bfbac77..cf8e7eb9b 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -53,11 +53,11 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # 服务配置 HOST=127.0.0.1 # 如果使用Docker部署,需要改成0.0.0.0,否则QQ消息无法传入 -PORT=8080 +PORT=8080 # 与反向端口相同 # 数据库配置 MONGODB_HOST=127.0.0.1 # 如果使用Docker部署,需要改成数据库容器的名字,默认是mongodb -MONGODB_PORT=27017 +MONGODB_PORT=27017 # MongoDB端口 DATABASE_NAME=MegBot MONGODB_USERNAME = "" # 数据库用户名 MONGODB_PASSWORD = "" # 数据库密码 From 6e2ea8261be3a8a36a9d93b1a20e03bae7b9562d Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Sun, 9 Mar 2025 22:12:10 +0800 Subject: [PATCH 043/105] =?UTF-8?q?refractor:=20=E5=87=A0=E4=B9=8E?= =?UTF-8?q?=E5=86=99=E5=AE=8C=E4=BA=86=EF=BC=8C=E8=BF=9B=E5=85=A5=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=98=B6=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 162 +++++++++-------- src/plugins/chat/chat_stream.py | 209 +++++++++++++++++++++ src/plugins/chat/cq_code.py | 18 ++ src/plugins/chat/emoji_manager.py | 4 +- src/plugins/chat/message.py | 84 ++++----- src/plugins/chat/message_cq.py | 2 +- src/plugins/chat/message_sender.py | 167 ++++++++--------- src/plugins/chat/relationship_manager.py | 221 ++++++++++++++++------- src/plugins/chat/storage.py | 43 ++--- src/plugins/chat/willing_manager.py | 82 ++++++--- 10 files changed, 654 insertions(+), 338 deletions(-) create mode 100644 src/plugins/chat/chat_stream.py diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c9eea3ed9..5385f3afb 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -10,18 +10,19 @@ from .config import global_config from .cq_code import CQCode,cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator +from .message import MessageSending, MessageRecv, MessageThinking, MessageSet from .message_cq import ( - Message, - Message_Sending, - Message_Thinking, # 导入 Message_Thinking 类 - MessageSet, + MessageRecvCQ, + MessageSendCQ, ) +from .chat_stream import chat_manager from .message_sender import message_manager # 导入新的消息管理器 from .relationship_manager import relationship_manager from .storage import MessageStorage from .utils import calculate_typing_time, is_mentioned_bot_in_txt +from .utils_image import image_path_to_base64 from .willing_manager import willing_manager # 导入意愿管理器 - +from .message_base import UserInfo, GroupInfo, Seg class ChatBot: def __init__(self): @@ -43,12 +44,9 @@ class ChatBot: async def handle_message(self, event: GroupMessageEvent, bot: Bot) -> None: """处理收到的群消息""" - if event.group_id not in global_config.talk_allowed_groups: - return self.bot = bot # 更新 bot 实例 - if event.user_id in global_config.ban_user_id: - return + group_info = await bot.get_group_info(group_id=event.group_id) sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) @@ -56,25 +54,46 @@ class ChatBot: await relationship_manager.update_relationship(user_id = event.user_id, data = sender_info) await relationship_manager.update_relationship_value(user_id = event.user_id, relationship_value = 0.5) - message = Message( - group_id=event.group_id, - user_id=event.user_id, + message_cq=MessageRecvCQ( message_id=event.message_id, - user_cardname=sender_info['card'], - raw_message=str(event.original_message), - plain_text=event.get_plaintext(), + user_id=event.user_id, + raw_message=str(event.original_message), + group_id=event.group_id, reply_message=event.reply, + platform='qq' ) - await message.initialize() + message_json=message_cq.to_dict() + # 进入maimbot + message=MessageRecv(**message_json) + await message.process() + groupinfo=message.message_info.group_info + userinfo=message.message_info.user_info + messageinfo=message.message_info + chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) + + # 消息过滤,涉及到config有待更新 + if groupinfo: + if groupinfo.group_id not in global_config.talk_allowed_groups: + return + else: + if userinfo: + if userinfo.user_id in []: + pass + else: + return + else: + return + if userinfo.user_id in global_config.ban_user_id: + return # 过滤词 for word in global_config.ban_words: - if word in message.detailed_plain_text: - logger.info(f"\033[1;32m[{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}") + if word in message.processed_plain_text: + logger.info(f"\033[1;32m[{groupinfo.group_name}]{userinfo.user_nickname}:\033[0m {message.processed_plain_text}") logger.info(f"\033[1;32m[过滤词识别]\033[0m 消息中含有{word},filtered") return - current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.time)) + current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) @@ -85,47 +104,55 @@ class ChatBot: print(f"\033[1;32m[记忆激活]\033[0m 对{message.processed_plain_text}的激活度:---------------------------------------{interested_rate}\n") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") - await self.storage.store_message(message, topic[0] if topic else None) + await self.storage.store_message(message,chat, topic[0] if topic else None) is_mentioned = is_mentioned_bot_in_txt(message.processed_plain_text) - reply_probability = willing_manager.change_reply_willing_received( - event.group_id, - topic[0] if topic else None, - is_mentioned, - global_config, - event.user_id, - message.is_emoji, - interested_rate + reply_probability = await willing_manager.change_reply_willing_received( + chat_stream=chat, + topic=topic[0] if topic else None, + is_mentioned_bot=is_mentioned, + config=global_config, + is_emoji=message.is_emoji, + interested_rate=interested_rate + ) + current_willing = willing_manager.get_willing( + chat_stream=chat ) - current_willing = willing_manager.get_willing(event.group_id) - - print(f"\033[1;32m[{current_time}][{message.group_name}]{message.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") + print(f"\033[1;32m[{current_time}][{chat.group_info.group_name}]{chat.user_info.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") - response = "" + response = None if random() < reply_probability: - - + bot_user_info=UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform + ) tinking_time_point = round(time.time(), 2) think_id = 'mt' + str(tinking_time_point) - thinking_message = Message_Thinking(message=message,message_id=think_id) + thinking_message = MessageThinking.from_chat_stream( + chat_stream=chat, + message_id=think_id, + reply=message + ) message_manager.add_message(thinking_message) - willing_manager.change_reply_willing_sent(thinking_message.group_id) + willing_manager.change_reply_willing_sent( + chat_stream=chat + ) response,raw_content = await self.gpt.generate_response(message) if response: - container = message_manager.get_container(event.group_id) + container = message_manager.get_container(chat.stream_id) thinking_message = None # 找到message,删除 for msg in container.messages: - if isinstance(msg, Message_Thinking) and msg.message_id == think_id: + if isinstance(msg, MessageThinking) and msg.message_info.message_id == think_id: thinking_message = msg container.messages.remove(msg) - # print(f"\033[1;32m[思考消息删除]\033[0m 已找到思考消息对象,开始删除") break # 如果找不到思考消息,直接返回 @@ -135,11 +162,10 @@ class ChatBot: #记录开始思考的时间,避免从思考到回复的时间太久 thinking_start_time = thinking_message.thinking_start_time - message_set = MessageSet(event.group_id, global_config.BOT_QQ, think_id) # 发送消息的id和产生发送消息的message_thinking是一致的 + message_set = MessageSet(chat, think_id) #计算打字时间,1是为了模拟打字,2是避免多条回复乱序 accu_typing_time = 0 - # print(f"\033[1;32m[开始回复]\033[0m 开始将回复1载入发送容器") mark_head = False for msg in response: # print(f"\033[1;32m[回复内容]\033[0m {msg}") @@ -148,22 +174,16 @@ class ChatBot: accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - bot_message = Message_Sending( - group_id=event.group_id, - user_id=global_config.BOT_QQ, + message_segment = Seg(type='text', data=msg) + bot_message = MessageSending( message_id=think_id, - raw_message=msg, - plain_text=msg, - processed_plain_text=msg, - user_nickname=global_config.BOT_NICKNAME, - group_name=message.group_name, - time=timepoint, #记录了回复生成的时间 - thinking_start_time=thinking_start_time, #记录了思考开始的时间 - reply_message_id=message.message_id + chat_stream=chat, + message_segment=message_segment, + reply=message, + is_head=not mark_head, + is_emoji=False ) - await bot_message.initialize() if not mark_head: - bot_message.is_head = True mark_head = True message_set.add_message(bot_message) @@ -180,30 +200,22 @@ class ChatBot: if emoji_raw != None: emoji_path,discription = emoji_raw - emoji_cq = cq_code_tool.create_emoji_cq(emoji_path) + emoji_cq = image_path_to_base64(emoji_path) if random() < 0.5: bot_response_time = tinking_time_point - 1 else: bot_response_time = bot_response_time + 1 - bot_message = Message_Sending( - group_id=event.group_id, - user_id=global_config.BOT_QQ, - message_id=0, - raw_message=emoji_cq, - plain_text=emoji_cq, - processed_plain_text=emoji_cq, - detailed_plain_text=discription, - user_nickname=global_config.BOT_NICKNAME, - group_name=message.group_name, - time=bot_response_time, - is_emoji=True, - translate_cq=False, - thinking_start_time=thinking_start_time, - # reply_message_id=message.message_id + message_segment = Seg(type='emoji', data=emoji_cq) + bot_message = MessageSending( + message_id=think_id, + chat_stream=chat, + message_segment=message_segment, + reply=message, + is_head=False, + is_emoji=True ) - await bot_message.initialize() message_manager.add_message(bot_message) emotion = await self.gpt._get_emotion_tags(raw_content) print(f"为 '{response}' 获取到的情感标签为:{emotion}") @@ -219,8 +231,12 @@ class ChatBot: await relationship_manager.update_relationship_value(message.user_id, relationship_value=valuedict[emotion[0]]) # 使用情绪管理器更新情绪 self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) - - # willing_manager.change_reply_willing_after_sent(event.group_id) + + willing_manager.change_reply_willing_after_sent( + platform=messageinfo.platform, + user_info=userinfo, + group_info=groupinfo + ) # 创建全局ChatBot实例 chat_bot = ChatBot() \ No newline at end of file diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py new file mode 100644 index 000000000..e617054ec --- /dev/null +++ b/src/plugins/chat/chat_stream.py @@ -0,0 +1,209 @@ +import time +import asyncio +from typing import Optional, Dict, Tuple +import hashlib + +from loguru import logger +from ...common.database import Database +from .message_base import UserInfo, GroupInfo + + +class ChatStream: + """聊天流对象,存储一个完整的聊天上下文""" + def __init__(self, + stream_id: str, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None, + data: dict = None): + self.stream_id = stream_id + self.platform = platform + self.user_info = user_info + self.group_info = group_info + self.create_time = data.get('create_time', int(time.time())) if data else int(time.time()) + self.last_active_time = data.get('last_active_time', self.create_time) if data else self.create_time + self.saved = False + + def to_dict(self) -> dict: + """转换为字典格式""" + result = { + 'stream_id': self.stream_id, + 'platform': self.platform, + 'user_info': self.user_info.to_dict() if self.user_info else None, + 'group_info': self.group_info.to_dict() if self.group_info else None, + 'create_time': self.create_time, + 'last_active_time': self.last_active_time + } + return result + + @classmethod + def from_dict(cls, data: dict) -> 'ChatStream': + """从字典创建实例""" + user_info = UserInfo(**data.get('user_info', {})) if data.get('user_info') else None + group_info = GroupInfo(**data.get('group_info', {})) if data.get('group_info') else None + + return cls( + stream_id=data['stream_id'], + platform=data['platform'], + user_info=user_info, + group_info=group_info, + data=data + ) + + def update_active_time(self): + """更新最后活跃时间""" + self.last_active_time = int(time.time()) + self.saved = False + + +class ChatManager: + """聊天管理器,管理所有聊天流""" + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + self.streams: Dict[str, ChatStream] = {} # stream_id -> ChatStream + self.db = Database.get_instance() + self._ensure_collection() + self._initialized = True + # 在事件循环中启动初始化 + asyncio.create_task(self._initialize()) + # 启动自动保存任务 + asyncio.create_task(self._auto_save_task()) + + async def _initialize(self): + """异步初始化""" + try: + await self.load_all_streams() + logger.success(f"聊天管理器已启动,已加载 {len(self.streams)} 个聊天流") + except Exception as e: + logger.error(f"聊天管理器启动失败: {str(e)}") + + async def _auto_save_task(self): + """定期自动保存所有聊天流""" + while True: + await asyncio.sleep(300) # 每5分钟保存一次 + try: + await self._save_all_streams() + logger.info("聊天流自动保存完成") + except Exception as e: + logger.error(f"聊天流自动保存失败: {str(e)}") + + def _ensure_collection(self): + """确保数据库集合存在并创建索引""" + if 'chat_streams' not in self.db.db.list_collection_names(): + self.db.db.create_collection('chat_streams') + # 创建索引 + self.db.db.chat_streams.create_index([('stream_id', 1)], unique=True) + self.db.db.chat_streams.create_index([ + ('platform', 1), + ('user_info.user_id', 1), + ('group_info.group_id', 1) + ]) + + def _generate_stream_id(self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None) -> str: + """生成聊天流唯一ID""" + # 组合关键信息 + components = [ + platform, + str(user_info.user_id), + str(group_info.group_id) if group_info else 'private' + ] + + # 使用MD5生成唯一ID + key = '_'.join(components) + return hashlib.md5(key.encode()).hexdigest() + + async def get_or_create_stream(self, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None) -> ChatStream: + """获取或创建聊天流 + + Args: + platform: 平台标识 + user_info: 用户信息 + group_info: 群组信息(可选) + + Returns: + ChatStream: 聊天流对象 + """ + # 生成stream_id + stream_id = self._generate_stream_id(platform, user_info, group_info) + + # 检查内存中是否存在 + if stream_id in self.streams: + stream = self.streams[stream_id] + # 更新用户信息和群组信息 + stream.user_info = user_info + if group_info: + stream.group_info = group_info + stream.update_active_time() + return stream + + # 检查数据库中是否存在 + data = self.db.db.chat_streams.find_one({'stream_id': stream_id}) + if data: + stream = ChatStream.from_dict(data) + # 更新用户信息和群组信息 + stream.user_info = user_info + if group_info: + stream.group_info = group_info + stream.update_active_time() + else: + # 创建新的聊天流 + stream = ChatStream( + stream_id=stream_id, + platform=platform, + user_info=user_info, + group_info=group_info + ) + + # 保存到内存和数据库 + self.streams[stream_id] = stream + await self._save_stream(stream) + return stream + + def get_stream(self, stream_id: str) -> Optional[ChatStream]: + """通过stream_id获取聊天流""" + return self.streams.get(stream_id) + + def get_stream_by_info(self, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None) -> Optional[ChatStream]: + """通过信息获取聊天流""" + stream_id = self._generate_stream_id(platform, user_info, group_info) + return self.streams.get(stream_id) + + async def _save_stream(self, stream: ChatStream): + """保存聊天流到数据库""" + if not stream.saved: + self.db.db.chat_streams.update_one( + {'stream_id': stream.stream_id}, + {'$set': stream.to_dict()}, + upsert=True + ) + stream.saved = True + + async def _save_all_streams(self): + """保存所有聊天流""" + for stream in self.streams.values(): + await self._save_stream(stream) + + async def load_all_streams(self): + """从数据库加载所有聊天流""" + all_streams = self.db.db.chat_streams.find({}) + for data in all_streams: + stream = ChatStream.from_dict(data) + self.streams[stream.stream_id] = stream + + +# 创建全局单例 +chat_manager = ChatManager() diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index e908219b5..b29e25b4c 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -373,6 +373,24 @@ class CQCode_tool: # 生成CQ码,设置sub_type=1表示这是表情包 return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" + @staticmethod + def create_emoji_cq_base64(base64_data: str) -> str: + """ + 创建表情包CQ码 + Args: + base64_data: base64编码的表情包数据 + Returns: + 表情包CQ码字符串 + """ + # 转义base64数据 + escaped_base64 = base64_data.replace('&', '&') \ + .replace('[', '[') \ + .replace(']', ']') \ + .replace(',', ',') + # 生成CQ码,设置sub_type=1表示这是表情包 + return f"[CQ:image,file=base64://{escaped_base64},sub_type=1]" + + diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index ff59220dc..3432f011c 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -3,7 +3,7 @@ import os import random import time import traceback -from typing import Optional +from typing import Optional, Tuple import base64 import hashlib @@ -92,7 +92,7 @@ class EmojiManager: except Exception as e: logger.error(f"记录表情使用失败: {str(e)}") - async def get_emoji_for_text(self, text: str) -> Optional[str]: + async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str,str]]: """根据文本内容获取相关表情包 Args: text: 输入文本 diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index a561e6490..070241ac1 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -5,11 +5,10 @@ from typing import Dict, ForwardRef, List, Optional, Union import urllib3 from loguru import logger -from .cq_code import CQCode, cq_code_tool -from .utils_cq import parse_cq_code from .utils_user import get_groupname, get_user_cardname, get_user_nickname from .utils_image import image_manager from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase +from .chat_stream import ChatStream # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -40,6 +39,7 @@ class MessageRecv(MessageBase): # 处理消息内容 self.processed_plain_text = "" # 初始化为空字符串 self.detailed_plain_text = "" # 初始化为空字符串 + self.is_emoji=False async def process(self) -> None: """处理消息内容,生成纯文本和详细文本 @@ -88,6 +88,7 @@ class MessageRecv(MessageBase): return await image_manager.get_image_description(seg.data) return '[图片]' elif seg.type == 'emoji': + self.is_emoji=True if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): return await image_manager.get_emoji_description(seg.data) return '[表情]' @@ -115,36 +116,17 @@ class MessageProcessBase(MessageBase): def __init__( self, message_id: str, - user_id: int, - group_id: Optional[int] = None, - platform: str = "qq", + chat_stream: ChatStream, message_segment: Optional[Seg] = None, reply: Optional['MessageRecv'] = None ): - # 构造用户信息 - user_info = UserInfo( - platform=platform, - user_id=user_id, - user_nickname=get_user_nickname(user_id), - user_cardname=get_user_cardname(user_id) if group_id else None - ) - - # 构造群组信息(如果有) - group_info = None - if group_id: - group_info = GroupInfo( - platform=platform, - group_id=group_id, - group_name=get_groupname(group_id) - ) - # 构造基础消息信息 message_info = BaseMessageInfo( - platform=platform, + platform=chat_stream.platform, message_id=message_id, time=int(time.time()), - group_info=group_info, - user_info=user_info + group_info=chat_stream.group_info, + user_info=chat_stream.user_info ) # 调用父类初始化 @@ -241,17 +223,13 @@ class MessageThinking(MessageProcessBase): def __init__( self, message_id: str, - user_id: int, - group_id: Optional[int] = None, - platform: str = "qq", + chat_stream: ChatStream, reply: Optional['MessageRecv'] = None ): # 调用父类初始化 super().__init__( message_id=message_id, - user_id=user_id, - group_id=group_id, - platform=platform, + chat_stream=chat_stream, message_segment=None, # 思考状态不需要消息段 reply=reply ) @@ -259,6 +237,15 @@ class MessageThinking(MessageProcessBase): # 思考状态特有属性 self.interrupt = False + @classmethod + def from_chat_stream(cls, chat_stream: ChatStream, message_id: str, reply: Optional['MessageRecv'] = None) -> 'MessageThinking': + """从聊天流创建思考状态消息""" + return cls( + message_id=message_id, + chat_stream=chat_stream, + reply=reply + ) + @dataclass class MessageSending(MessageProcessBase): """发送状态的消息类""" @@ -266,19 +253,16 @@ class MessageSending(MessageProcessBase): def __init__( self, message_id: str, - user_id: int, + chat_stream: ChatStream, message_segment: Seg, - group_id: Optional[int] = None, reply: Optional['MessageRecv'] = None, - platform: str = "qq", - is_head: bool = False + is_head: bool = False, + is_emoji: bool = False ): # 调用父类初始化 super().__init__( message_id=message_id, - user_id=user_id, - group_id=group_id, - platform=platform, + chat_stream=chat_stream, message_segment=message_segment, reply=reply ) @@ -286,6 +270,12 @@ class MessageSending(MessageProcessBase): # 发送状态特有属性 self.reply_to_message_id = reply.message_info.message_id if reply else None self.is_head = is_head + self.is_emoji = is_emoji + if is_head: + self.message_segment = Seg(type='seglist', data=[ + Seg(type='reply', data=reply.message_info.message_id), + self.message_segment + ]) async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" @@ -298,26 +288,24 @@ class MessageSending(MessageProcessBase): cls, thinking: MessageThinking, message_segment: Seg, - reply: Optional['MessageRecv'] = None, - is_head: bool = False + is_head: bool = False, + is_emoji: bool = False ) -> 'MessageSending': """从思考状态消息创建发送状态消息""" return cls( message_id=thinking.message_info.message_id, - user_id=thinking.message_info.user_info.user_id, + chat_stream=thinking.chat_stream, message_segment=message_segment, - group_id=thinking.message_info.group_info.group_id if thinking.message_info.group_info else None, - reply=reply or thinking.reply, - platform=thinking.message_info.platform, - is_head=is_head + reply=thinking.reply, + is_head=is_head, + is_emoji=is_emoji ) @dataclass class MessageSet: """消息集合类,可以存储多个发送消息""" - def __init__(self, group_id: int, user_id: int, message_id: str): - self.group_id = group_id - self.user_id = user_id + def __init__(self, chat_stream: ChatStream, message_id: str): + self.chat_stream = chat_stream self.message_id = message_id self.messages: List[MessageSending] = [] self.time = round(time.time(), 2) diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 80df4e340..7d9c6216d 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -176,7 +176,7 @@ class MessageSendCQ(MessageCQ): elif seg.type == 'image': # 如果是base64图片数据 if seg.data.startswith(('data:', 'base64:')): - return f"[CQ:image,file=base64://{seg.data}]" + return cq_code_tool.create_emoji_cq_base64(seg.data) # 如果是表情包(本地文件) return cq_code_tool.create_emoji_cq(seg.data) elif seg.type == 'at': diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8ed30d69c..9b1ab66be 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -5,10 +5,11 @@ from typing import Dict, List, Optional, Union from nonebot.adapters.onebot.v11 import Bot from .cq_code import cq_code_tool -from .message_cq import Message, Message_Sending, Message_Thinking, MessageSet +from .message_cq import MessageSendCQ +from .message import MessageSending, MessageThinking, MessageRecv,MessageSet from .storage import MessageStorage -from .utils import calculate_typing_time from .config import global_config +from .chat_stream import chat_manager class Message_Sender: @@ -21,66 +22,59 @@ class Message_Sender: def set_bot(self, bot: Bot): """设置当前bot实例""" self._current_bot = bot - - async def send_group_message( - self, - group_id: int, - send_text: str, - auto_escape: bool = False, - reply_message_id: int = None, - at_user_id: int = None - ) -> None: - if not self._current_bot: - raise RuntimeError("Bot未设置,请先调用set_bot方法设置bot实例") - - message = send_text - - # 如果需要回复 - if reply_message_id: - reply_cq = cq_code_tool.create_reply_cq(reply_message_id) - message = reply_cq + message - - # 如果需要at - # if at_user_id: - # at_cq = cq_code_tool.create_at_cq(at_user_id) - # message = at_cq + " " + message - - - typing_time = calculate_typing_time(message) - if typing_time > 10: - typing_time = 10 - await asyncio.sleep(typing_time) - - # 发送消息 - try: - await self._current_bot.send_group_msg( - group_id=group_id, - message=message, - auto_escape=auto_escape + async def send_message( + self, + message: MessageSending, + ) -> None: + """发送消息""" + if isinstance(message, MessageSending): + message_send=MessageSendCQ( + message_id=message.message_id, + user_id=message.message_info.user_info.user_id, + message_segment=message.message_segment, + reply=message.reply ) - print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") - except Exception as e: - print(f"发生错误 {e}") - print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + if message.message_info.group_info: + try: + await self._current_bot.send_group_msg( + group_id=message.message_info.group_info.group_id, + message=message_send.raw_message, + auto_escape=False + ) + print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + except Exception as e: + print(f"发生错误 {e}") + print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + else: + try: + await self._current_bot.send_private_msg( + user_id=message.message_info.user_info.user_id, + message=message_send.raw_message, + auto_escape=False + ) + print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + except Exception as e: + print(f"发生错误 {e}") + print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") class MessageContainer: - """单个群的发送/思考消息容器""" - def __init__(self, group_id: int, max_size: int = 100): - self.group_id = group_id + """单个聊天流的发送/思考消息容器""" + def __init__(self, chat_id: str, max_size: int = 100): + self.chat_id = chat_id self.max_size = max_size self.messages = [] self.last_send_time = 0 self.thinking_timeout = 20 # 思考超时时间(秒) - def get_timeout_messages(self) -> List[Message_Sending]: + def get_timeout_messages(self) -> List[MessageSending]: """获取所有超时的Message_Sending对象(思考时间超过30秒),按thinking_start_time排序""" current_time = time.time() timeout_messages = [] for msg in self.messages: - if isinstance(msg, Message_Sending): + if isinstance(msg, MessageSending): if current_time - msg.thinking_start_time > self.thinking_timeout: timeout_messages.append(msg) @@ -89,7 +83,7 @@ class MessageContainer: return timeout_messages - def get_earliest_message(self) -> Optional[Union[Message_Thinking, Message_Sending]]: + def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: """获取thinking_start_time最早的消息对象""" if not self.messages: return None @@ -102,16 +96,15 @@ class MessageContainer: earliest_message = msg return earliest_message - def add_message(self, message: Union[Message_Thinking, Message_Sending]) -> None: + def add_message(self, message: Union[MessageThinking, MessageSending]) -> None: """添加消息到队列""" - # print(f"\033[1;32m[添加消息]\033[0m 添加消息到对应群") if isinstance(message, MessageSet): for single_message in message.messages: self.messages.append(single_message) else: self.messages.append(message) - def remove_message(self, message: Union[Message_Thinking, Message_Sending]) -> bool: + def remove_message(self, message: Union[MessageThinking, MessageSending]) -> bool: """移除消息,如果消息存在则返回True,否则返回False""" try: if message in self.messages: @@ -126,40 +119,42 @@ class MessageContainer: """检查是否有待发送的消息""" return bool(self.messages) - def get_all_messages(self) -> List[Union[Message, Message_Thinking]]: + def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]: """获取所有消息""" return list(self.messages) class MessageManager: - """管理所有群的消息容器""" + """管理所有聊天流的消息容器""" def __init__(self): - self.containers: Dict[int, MessageContainer] = {} + self.containers: Dict[str, MessageContainer] = {} # chat_id -> MessageContainer self.storage = MessageStorage() self._running = True - def get_container(self, group_id: int) -> MessageContainer: - """获取或创建群的消息容器""" - if group_id not in self.containers: - self.containers[group_id] = MessageContainer(group_id) - return self.containers[group_id] + def get_container(self, chat_id: str) -> MessageContainer: + """获取或创建聊天流的消息容器""" + if chat_id not in self.containers: + self.containers[chat_id] = MessageContainer(chat_id) + return self.containers[chat_id] - def add_message(self, message: Union[Message_Thinking, Message_Sending, MessageSet]) -> None: - container = self.get_container(message.group_id) + def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None: + chat_stream = chat_manager.get_stream_by_info( + platform=message.message_info.platform, + user_info=message.message_info.user_info, + group_info=message.message_info.group_info + ) + if not chat_stream: + raise ValueError("无法找到对应的聊天流") + container = self.get_container(chat_stream.stream_id) container.add_message(message) - async def process_group_messages(self, group_id: int): - """处理群消息""" - # if int(time.time() / 3) == time.time() / 3: - # print(f"\033[1;34m[调试]\033[0m 开始处理群{group_id}的消息") - container = self.get_container(group_id) + async def process_chat_messages(self, chat_id: str): + """处理聊天流消息""" + container = self.get_container(chat_id) if container.has_messages(): - #最早的对象,可能是思考消息,也可能是发送消息 - message_earliest = container.get_earliest_message() #一个message_thinking or message_sending + message_earliest = container.get_earliest_message() - #如果是思考消息 - if isinstance(message_earliest, Message_Thinking): - #优先等待这条消息 + if isinstance(message_earliest, MessageThinking): message_earliest.update_thinking_time() thinking_time = message_earliest.thinking_time print(f"\033[1;34m[调试]\033[0m 消息正在思考中,已思考{int(thinking_time)}秒\033[K\r", end='', flush=True) @@ -168,42 +163,36 @@ class MessageManager: if thinking_time > global_config.thinking_timeout: print(f"\033[1;33m[警告]\033[0m 消息思考超时({thinking_time}秒),移除该消息") container.remove_message(message_earliest) - else:# 如果不是message_thinking就只能是message_sending + else: print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") - #直接发,等什么呢 - if message_earliest.is_head and message_earliest.update_thinking_time() >30: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False, reply_message_id=message_earliest.reply_message_id) + if message_earliest.is_head and message_earliest.update_thinking_time() > 30: + await message_sender.send_message(message_earliest) else: - await message_sender.send_group_message(group_id, message_earliest.processed_plain_text, auto_escape=False) - #移除消息 + await message_sender.send_message(message_earliest) + if message_earliest.is_emoji: message_earliest.processed_plain_text = "[表情包]" await self.storage.store_message(message_earliest, None) container.remove_message(message_earliest) - #获取并处理超时消息 - message_timeout = container.get_timeout_messages() #也许是一堆message_sending + message_timeout = container.get_timeout_messages() if message_timeout: print(f"\033[1;34m[调试]\033[0m 发现{len(message_timeout)}条超时消息") for msg in message_timeout: if msg == message_earliest: - continue # 跳过已经处理过的消息 + continue try: - #发送 - if msg.is_head and msg.update_thinking_time() >30: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) + if msg.is_head and msg.update_thinking_time() > 30: + await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) else: - await message_sender.send_group_message(group_id, msg.processed_plain_text, auto_escape=False) - + await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False) - #如果是表情包,则替换为"[表情包]" if msg.is_emoji: msg.processed_plain_text = "[表情包]" await self.storage.store_message(msg, None) - # 安全地移除消息 if not container.remove_message(msg): print("\033[1;33m[警告]\033[0m 尝试删除不存在的消息") except Exception as e: @@ -215,8 +204,8 @@ class MessageManager: while self._running: await asyncio.sleep(1) tasks = [] - for group_id in self.containers.keys(): - tasks.append(self.process_group_messages(group_id)) + for chat_id in self.containers.keys(): + tasks.append(self.process_chat_messages(chat_id)) await asyncio.gather(*tasks) diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 4ed7a2f11..b56cdc6e5 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,8 +1,9 @@ import asyncio -from typing import Optional +from typing import Optional, Union from ...common.database import Database - +from .message_base import UserInfo +from .chat_stream import ChatStream class Impression: traits: str = None @@ -13,60 +14,77 @@ class Impression: class Relationship: user_id: int = None - # impression: Impression = None - # group_id: int = None - # group_name: str = None + platform: str = None gender: str = None age: int = None nickname: str = None relationship_value: float = None saved = False - def __init__(self, user_id: int, data=None, **kwargs): - if isinstance(data, dict): - # 如果输入是字典,使用字典解析 - self.user_id = data.get('user_id') - self.gender = data.get('gender') - self.age = data.get('age') - self.nickname = data.get('nickname') - self.relationship_value = data.get('relationship_value', 0.0) - self.saved = data.get('saved', False) - else: - # 如果是直接传入属性值 - self.user_id = kwargs.get('user_id') - self.gender = kwargs.get('gender') - self.age = kwargs.get('age') - self.nickname = kwargs.get('nickname') - self.relationship_value = kwargs.get('relationship_value', 0.0) - self.saved = kwargs.get('saved', False) + def __init__(self, chat:ChatStream,data:dict): + self.user_id=chat.user_info.user_id + self.platform=chat.platform + self.nickname=chat.user_info.user_nickname + self.relationship_value=data.get('relationship_value',0) + self.age=data.get('age',0) + self.gender=data.get('gender','') - - class RelationshipManager: def __init__(self): - self.relationships: dict[int, Relationship] = {} + self.relationships: dict[tuple[int, str], Relationship] = {} # 修改为使用(user_id, platform)作为键 - async def update_relationship(self, user_id: int, data=None, **kwargs): + async def update_relationship(self, + chat_stream:ChatStream, + data: dict = None, + **kwargs) -> Optional[Relationship]: + """更新或创建关系 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + data: 字典格式的数据(可选) + **kwargs: 其他参数 + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if chat_stream.user_info is not None: + user_id = chat_stream.user_info.user_id + platform = chat_stream.user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + # 使用(user_id, platform)作为键 + key = (user_id, platform) + # 检查是否在内存中已存在 - relationship = self.relationships.get(user_id) + relationship = self.relationships.get(key) if relationship: # 如果存在,更新现有对象 if isinstance(data, dict): - for key, value in data.items(): - if hasattr(relationship, key) and value is not None: - setattr(relationship, key, value) + for k, value in data.items(): + if hasattr(relationship, k) and value is not None: + setattr(relationship, k, value) else: - for key, value in kwargs.items(): - if hasattr(relationship, key) and value is not None: - setattr(relationship, key, value) + for k, value in kwargs.items(): + if hasattr(relationship, k) and value is not None: + setattr(relationship, k, value) else: # 如果不存在,创建新对象 - relationship = Relationship(user_id, data=data) if isinstance(data, dict) else Relationship(user_id, **kwargs) - self.relationships[user_id] = relationship - - # 更新 id_name_nickname_table - # self.id_name_nickname_table[user_id] = [relationship.nickname] # 别称设置为空列表 + if user_info is not None: + relationship = Relationship(user_info=user_info, **kwargs) + elif isinstance(data, dict): + data['platform'] = platform + relationship = Relationship(user_id=user_id, data=data) + else: + kwargs['platform'] = platform + kwargs['user_id'] = user_id + relationship = Relationship(**kwargs) + self.relationships[key] = relationship # 保存到数据库 await self.storage_relationship(relationship) @@ -74,33 +92,87 @@ class RelationshipManager: return relationship - async def update_relationship_value(self, user_id: int, **kwargs): + async def update_relationship_value(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None, + **kwargs) -> Optional[Relationship]: + """更新关系值 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + **kwargs: 其他参数 + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + # 使用(user_id, platform)作为键 + key = (user_id, platform) + # 检查是否在内存中已存在 - relationship = self.relationships.get(user_id) + relationship = self.relationships.get(key) if relationship: - for key, value in kwargs.items(): - if key == 'relationship_value': + for k, value in kwargs.items(): + if k == 'relationship_value': relationship.relationship_value += value await self.storage_relationship(relationship) relationship.saved = True return relationship else: - print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id} 不存在,无法更新") + # 如果不存在且提供了user_info,则创建新的关系 + if user_info is not None: + return await self.update_relationship(user_info=user_info, **kwargs) + print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id}({platform}) 不存在,无法更新") return None - - def get_relationship(self, user_id: int) -> Optional[Relationship]: - """获取用户关系对象""" - if user_id in self.relationships: - return self.relationships[user_id] + def get_relationship(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None) -> Optional[Relationship]: + """获取用户关系对象 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + Returns: + Relationship: 关系对象 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + + key = (user_id, platform) + if key in self.relationships: + return self.relationships[key] else: return 0 async def load_relationship(self, data: dict) -> Relationship: - """从数据库加载或创建新的关系对象""" - rela = Relationship(user_id=data['user_id'], data=data) + """从数据库加载或创建新的关系对象""" + # 确保data中有platform字段,如果没有则默认为'qq' + if 'platform' not in data: + data['platform'] = 'qq' + + rela = Relationship(data=data) rela.saved = True - self.relationships[rela.user_id] = rela + key = (rela.user_id, rela.platform) + self.relationships[key] = rela return rela async def load_all_relationships(self): @@ -117,9 +189,7 @@ class RelationshipManager: all_relationships = db.db.relationships.find({}) # 依次加载每条记录 for data in all_relationships: - user_id = data['user_id'] - relationship = await self.load_relationship(data) - self.relationships[user_id] = relationship + await self.load_relationship(data) print(f"\033[1;32m[关系管理]\033[0m 已加载 {len(self.relationships)} 条关系记录") while True: @@ -130,16 +200,15 @@ class RelationshipManager: async def _save_all_relationships(self): """将所有关系数据保存到数据库""" # 保存所有关系数据 - for userid, relationship in self.relationships.items(): + for (userid, platform), relationship in self.relationships.items(): if not relationship.saved: relationship.saved = True await self.storage_relationship(relationship) - async def storage_relationship(self,relationship: Relationship): - """ - 将关系记录存储到数据库中 - """ + async def storage_relationship(self, relationship: Relationship): + """将关系记录存储到数据库中""" user_id = relationship.user_id + platform = relationship.platform nickname = relationship.nickname relationship_value = relationship.relationship_value gender = relationship.gender @@ -148,8 +217,9 @@ class RelationshipManager: db = Database.get_instance() db.db.relationships.update_one( - {'user_id': user_id}, + {'user_id': user_id, 'platform': platform}, {'$set': { + 'platform': platform, 'nickname': nickname, 'relationship_value': relationship_value, 'gender': gender, @@ -159,12 +229,35 @@ class RelationshipManager: upsert=True ) - def get_name(self, user_id: int) -> str: + def get_name(self, + user_id: int = None, + platform: str = None, + user_info: UserInfo = None) -> str: + """获取用户昵称 + Args: + user_id: 用户ID(可选,如果提供user_info则不需要) + platform: 平台(可选,如果提供user_info则不需要) + user_info: 用户信息对象(可选) + Returns: + str: 用户昵称 + """ + # 确定user_id和platform + if user_info is not None: + user_id = user_info.user_id + platform = user_info.platform or 'qq' + else: + platform = platform or 'qq' + + if user_id is None: + raise ValueError("必须提供user_id或user_info") + # 确保user_id是整数类型 user_id = int(user_id) - if user_id in self.relationships: - - return self.relationships[user_id].nickname + key = (user_id, platform) + if key in self.relationships: + return self.relationships[key].nickname + elif user_info is not None: + return user_info.user_nickname or user_info.user_cardname or "某人" else: return "某人" diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 4b1bf0ca4..170b677dc 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -1,47 +1,26 @@ -from typing import Optional +from typing import Optional, Union from ...common.database import Database -from .message_cq import Message - +from .message_base import MessageBase +from .message import MessageSending, MessageRecv +from .chat_stream import ChatStream class MessageStorage: def __init__(self): self.db = Database.get_instance() - async def store_message(self, message: Message, topic: Optional[str] = None) -> None: + async def store_message(self, message: Union[MessageSending, MessageRecv],chat_stream:ChatStream, topic: Optional[str] = None) -> None: """存储消息到数据库""" try: - if not message.is_emoji: - message_data = { - "group_id": message.group_id, - "user_id": message.user_id, - "message_id": message.message_id, - "raw_message": message.raw_message, - "plain_text": message.plain_text, + message_data = { + "message_id": message.message_info.message_id, + "time": message.message_info.time, + "chat_id":chat_stream.stream_id, + "chat_info": chat_stream.to_dict(), + "detailed_plain_text": message.detailed_plain_text, "processed_plain_text": message.processed_plain_text, - "time": message.time, - "user_nickname": message.user_nickname, - "user_cardname": message.user_cardname, - "group_name": message.group_name, "topic": topic, - "detailed_plain_text": message.detailed_plain_text, } - else: - message_data = { - "group_id": message.group_id, - "user_id": message.user_id, - "message_id": message.message_id, - "raw_message": message.raw_message, - "plain_text": message.plain_text, - "processed_plain_text": '[表情包]', - "time": message.time, - "user_nickname": message.user_nickname, - "user_cardname": message.user_cardname, - "group_name": message.group_name, - "topic": topic, - "detailed_plain_text": message.detailed_plain_text, - } - self.db.db.messages.insert_one(message_data) except Exception as e: print(f"\033[1;31m[错误]\033[0m 存储消息失败: {e}") diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 001b66207..e3d928c10 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -1,10 +1,15 @@ import asyncio +from typing import Dict +from loguru import logger + from .config import global_config +from .message_base import UserInfo, GroupInfo +from .chat_stream import chat_manager,ChatStream class WillingManager: def __init__(self): - self.group_reply_willing = {} # 存储每个群的回复意愿 + self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿 self._decay_task = None self._started = False @@ -12,20 +17,33 @@ class WillingManager: """定期衰减回复意愿""" while True: await asyncio.sleep(5) - for group_id in self.group_reply_willing: - self.group_reply_willing[group_id] = max(0, self.group_reply_willing[group_id] * 0.6) + for chat_id in self.chat_reply_willing: + self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.6) - def get_willing(self, group_id: int) -> float: - """获取指定群组的回复意愿""" - return self.group_reply_willing.get(group_id, 0) + def get_willing(self,chat_stream:ChatStream) -> float: + """获取指定聊天流的回复意愿""" + stream = chat_stream + if stream: + return self.chat_reply_willing.get(stream.stream_id, 0) + return 0 - def set_willing(self, group_id: int, willing: float): - """设置指定群组的回复意愿""" - self.group_reply_willing[group_id] = willing + def set_willing(self, chat_id: str, willing: float): + """设置指定聊天流的回复意愿""" + self.chat_reply_willing[chat_id] = willing - def change_reply_willing_received(self, group_id: int, topic: str, is_mentioned_bot: bool, config, user_id: int = None, is_emoji: bool = False, interested_rate: float = 0) -> float: - """改变指定群组的回复意愿并返回回复概率""" - current_willing = self.group_reply_willing.get(group_id, 0) + async def change_reply_willing_received(self, + chat_stream:ChatStream, + topic: str = None, + is_mentioned_bot: bool = False, + config = None, + is_emoji: bool = False, + interested_rate: float = 0) -> float: + """改变指定聊天流的回复意愿并返回回复概率""" + # 获取或创建聊天流 + stream = chat_stream + chat_id = stream.stream_id + + current_willing = self.chat_reply_willing.get(chat_id, 0) # print(f"初始意愿: {current_willing}") if is_mentioned_bot and current_willing < 1.0: @@ -49,31 +67,37 @@ class WillingManager: # print(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") reply_probability = max((current_willing - 0.45) * 2, 0) - if group_id not in config.talk_allowed_groups: - current_willing = 0 - reply_probability = 0 - - if group_id in config.talk_frequency_down_groups: - reply_probability = reply_probability / global_config.down_frequency_rate + + # 检查群组权限(如果是群聊) + if chat_stream.group_info: + if chat_stream.group_info.group_id not in config.talk_allowed_groups: + current_willing = 0 + reply_probability = 0 + + if chat_stream.group_info.group_id in config.talk_frequency_down_groups: + reply_probability = reply_probability / global_config.down_frequency_rate reply_probability = min(reply_probability, 1) if reply_probability < 0: reply_probability = 0 - - self.group_reply_willing[group_id] = min(current_willing, 3.0) + self.chat_reply_willing[chat_id] = min(current_willing, 3.0) return reply_probability - def change_reply_willing_sent(self, group_id: int): - """开始思考后降低群组的回复意愿""" - current_willing = self.group_reply_willing.get(group_id, 0) - self.group_reply_willing[group_id] = max(0, current_willing - 2) + def change_reply_willing_sent(self, chat_stream:ChatStream): + """开始思考后降低聊天流的回复意愿""" + stream = chat_stream + if stream: + current_willing = self.chat_reply_willing.get(stream.stream_id, 0) + self.chat_reply_willing[stream.stream_id] = max(0, current_willing - 2) - def change_reply_willing_after_sent(self, group_id: int): - """发送消息后提高群组的回复意愿""" - current_willing = self.group_reply_willing.get(group_id, 0) - if current_willing < 1: - self.group_reply_willing[group_id] = min(1, current_willing + 0.2) + def change_reply_willing_after_sent(self,chat_stream:ChatStream): + """发送消息后提高聊天流的回复意愿""" + stream = chat_stream + if stream: + current_willing = self.chat_reply_willing.get(stream.stream_id, 0) + if current_willing < 1: + self.chat_reply_willing[stream.stream_id] = min(1, current_willing + 0.2) async def ensure_started(self): """确保衰减任务已启动""" From 1b611ecce5178d71963403b8b1032d1bfcdc161f Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Mon, 10 Mar 2025 22:52:14 +0800 Subject: [PATCH 044/105] =?UTF-8?q?resolve=20SengokuCola/MaiMBot#167=20?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=AD=A3=E5=88=99=E8=A1=A8=E8=BE=BE=E5=BC=8F?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 9 +++++++++ src/plugins/chat/config.py | 4 ++++ template/bot_config_template.toml | 8 +++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c510fe4bf..c72ff2380 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -1,3 +1,4 @@ +import re import time from random import random @@ -75,6 +76,14 @@ class ChatBot: logger.info(f"[过滤词识别]消息中含有{word},filtered") return + # 正则表达式过滤 + for pattern in global_config.ban_words_regex: + if re.search(pattern, message.detailed_plain_text): + logger.info( + f"[{message.group_name}]{message.user_nickname}:{message.processed_plain_text}") + logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") + return + current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.time)) # topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index c37d23a46..7a7e4ebc9 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -43,6 +43,7 @@ class BotConfig: EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 ban_words = set() + ban_words_regex = set() max_response_length: int = 1024 # 最大回复长度 @@ -277,6 +278,9 @@ class BotConfig: config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", config.response_interested_rate_amplifier) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) + + if config.INNER_VERSION in SpecifierSet(">=0.0.5"): + config.ban_words_regex = msg_config.get("ban_words_regex", config.ban_words_regex) def memory(parent: dict): memory_config = parent["memory"] diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index bff64d05f..44c80fd2f 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "0.0.4" +version = "0.0.5" #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 @@ -40,6 +40,12 @@ ban_words = [ # "403","张三" ] +ban_words_regex = [ + # 需要过滤的消息匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 + #"https?://[^\\s]+", # 匹配https链接 + #"\\d{4}-\\d{2}-\\d{2}", # 匹配日期 +] + [emoji] check_interval = 120 # 检查表情包的时间间隔 register_interval = 10 # 注册表情包的时间间隔 From ee579bcf27a6d25dd5f4e0376ec14a591bee7320 Mon Sep 17 00:00:00 2001 From: Ziphyrien <111620796+Ziphyrien@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:54:13 +0800 Subject: [PATCH 045/105] Update README.md --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0c02d1cba..c9e1275cd 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,17 @@
-> ⚠️ **注意事项** +> [!WARNING] > - 项目处于活跃开发阶段,代码可能随时更改 > - 文档未完善,有问题可以提交 Issue 或者 Discussion > - QQ机器人存在被限制风险,请自行了解,谨慎使用 > - 由于持续迭代,可能存在一些已知或未知的bug > - 由于开发中,可能消耗较多token -**交流群**: 766798517 一群人较多,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -**交流群**: 571780722 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -**交流群**: 1035228475 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +## 💬交流群 +- [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 ,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [三群](https://qm.qq.com/q/wlH5eT8OmQ)1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 **其他平台版本** @@ -141,8 +142,8 @@ ## 📌 注意事项 SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 - -> ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 +> [!WARNING] +> 本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 ## 致谢 [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架 From 72a674939671ff7e10473c1c998255e23c8971cc Mon Sep 17 00:00:00 2001 From: Rikki Date: Mon, 10 Mar 2025 23:04:12 +0800 Subject: [PATCH 046/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Ddocker?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=97=B6=E5=8C=BA=E6=8C=87=E5=AE=9A=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 512558558..e61763398 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: mongodb: container_name: mongodb environment: - - tz=Asia/Shanghai + - TZ=Asia/Shanghai # - MONGO_INITDB_ROOT_USERNAME=your_username # - MONGO_INITDB_ROOT_PASSWORD=your_password expose: From 2b2b3429472fbacd11481769318a7a28cf05e30c Mon Sep 17 00:00:00 2001 From: Rikki Date: Mon, 10 Mar 2025 23:04:37 +0800 Subject: [PATCH 047/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20ruff=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 54737d640..3586857f0 100644 --- a/flake.nix +++ b/flake.nix @@ -22,6 +22,7 @@ pythonEnv = pkgs.python3.withPackages ( ps: with ps; [ + ruff pymongo python-dotenv pydantic From a03b49084daead610f3aeca9aede0bdbb9d522d9 Mon Sep 17 00:00:00 2001 From: Ziphyrien <111620796+Ziphyrien@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:05:10 +0800 Subject: [PATCH 048/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9e1275cd..7a23b62b5 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ## 💬交流群 - [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 ,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -- [三群](https://qm.qq.com/q/wlH5eT8OmQ)1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 **其他平台版本** From 204744cd7121cb4b7528739cea69355e4519564b Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Tue, 11 Mar 2025 00:12:19 +0800 Subject: [PATCH 049/105] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=90=8D=E4=B8=8E=E4=BF=AE=E6=94=B9=E8=BF=87=E6=BB=A4=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E4=B8=BAraw=5Fmessage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 4 ++-- src/plugins/chat/config.py | 4 ++-- template/bot_config_template.toml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c72ff2380..18652cfd4 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -77,10 +77,10 @@ class ChatBot: return # 正则表达式过滤 - for pattern in global_config.ban_words_regex: + for pattern in global_config.ban_msgs_regex: if re.search(pattern, message.detailed_plain_text): logger.info( - f"[{message.group_name}]{message.user_nickname}:{message.processed_plain_text}") + f"[{message.group_name}]{message.user_nickname}:{message.raw_message}") logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 7a7e4ebc9..888e33a7f 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -43,7 +43,7 @@ class BotConfig: EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 ban_words = set() - ban_words_regex = set() + ban_msgs_regex = set() max_response_length: int = 1024 # 最大回复长度 @@ -280,7 +280,7 @@ class BotConfig: config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) if config.INNER_VERSION in SpecifierSet(">=0.0.5"): - config.ban_words_regex = msg_config.get("ban_words_regex", config.ban_words_regex) + config.ban_msgs_regex = msg_config.get("ban_msgs_regex", config.ban_msgs_regex) def memory(parent: dict): memory_config = parent["memory"] diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 44c80fd2f..7dc15eb27 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -40,8 +40,8 @@ ban_words = [ # "403","张三" ] -ban_words_regex = [ - # 需要过滤的消息匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 +ban_msgs_regex = [ + # 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 #"https?://[^\\s]+", # 匹配https链接 #"\\d{4}-\\d{2}-\\d{2}", # 匹配日期 ] From 84495f83291608d40d2543ee8167ddb2907428e5 Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Tue, 11 Mar 2025 00:20:55 +0800 Subject: [PATCH 050/105] fix --- src/plugins/chat/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 18652cfd4..86d0b6944 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -78,7 +78,7 @@ class ChatBot: # 正则表达式过滤 for pattern in global_config.ban_msgs_regex: - if re.search(pattern, message.detailed_plain_text): + if re.search(pattern, message.raw_message): logger.info( f"[{message.group_name}]{message.user_nickname}:{message.raw_message}") logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") From cd96644a4c387b77e7b157ace73ea3446b661512 Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Tue, 11 Mar 2025 00:33:53 +0800 Subject: [PATCH 051/105] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 7dc15eb27..ff39c9a69 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -41,9 +41,10 @@ ban_words = [ ] ban_msgs_regex = [ - # 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 + # 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤(支持CQ码),若不了解正则表达式请勿修改 #"https?://[^\\s]+", # 匹配https链接 #"\\d{4}-\\d{2}-\\d{2}", # 匹配日期 + # "\\[CQ:at,qq=\\d+\\]" # 匹配@ ] [emoji] From 6cef8fd60b0efb98983090b4e4bb60519d08a08c Mon Sep 17 00:00:00 2001 From: Yan233_ Date: Tue, 11 Mar 2025 00:58:56 +0800 Subject: [PATCH 052/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=B6=E5=8C=BA?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E5=8E=BBnapcat=E7=94=A8=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=E7=9A=84=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 512558558..227df606b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,6 @@ services: - NAPCAT_UID=${NAPCAT_UID} - NAPCAT_GID=${NAPCAT_GID} # 让 NapCat 获取当前用户 GID,UID,防止权限问题 ports: - - 3000:3000 - - 3001:3001 - 6099:6099 restart: unless-stopped volumes: @@ -19,7 +17,7 @@ services: mongodb: container_name: mongodb environment: - - tz=Asia/Shanghai + - TZ=Asia/Shanghai # - MONGO_INITDB_ROOT_USERNAME=your_username # - MONGO_INITDB_ROOT_PASSWORD=your_password expose: From 354d6d0deb70cdf67ba8cfb508bacee78aff9839 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 01:13:17 +0800 Subject: [PATCH 053/105] =?UTF-8?q?=E8=AE=B0=E5=BF=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化了记忆的连接建立 重启了遗忘功能 --- README.md | 2 +- docs/Jonathan R.md | 20 + src/plugins/chat/__init__.py | 6 +- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/knowledege/knowledge_library.py | 2 +- src/plugins/memory_system/memory.py | 286 +++-- src/plugins/memory_system/memory_test1.py | 1208 +++++++++++++++++++ 7 files changed, 1422 insertions(+), 104 deletions(-) create mode 100644 docs/Jonathan R.md create mode 100644 src/plugins/memory_system/memory_test1.py diff --git a/README.md b/README.md index 29e25b2b9..5ae9c5f2c 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ ## 📌 注意事项 -SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 +SengokuCola已得到大脑升级 > ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 diff --git a/docs/Jonathan R.md b/docs/Jonathan R.md new file mode 100644 index 000000000..660caaeec --- /dev/null +++ b/docs/Jonathan R.md @@ -0,0 +1,20 @@ +Jonathan R. Wolpaw 在 “Memory in neuroscience: rhetoric versus reality.” 一文中提到,从神经科学的感觉运动假设出发,整个神经系统的功能是将经验与适当的行为联系起来,而不是单纯的信息存储。 +Jonathan R,Wolpaw. (2019). Memory in neuroscience: rhetoric versus reality.. Behavioral and cognitive neuroscience reviews(2). + +1. **单一过程理论** + - 单一过程理论认为,识别记忆主要是基于熟悉性这一单一因素的影响。熟悉性是指对刺激的一种自动的、无意识的感知,它可以使我们在没有回忆起具体细节的情况下,判断一个刺激是否曾经出现过。 + - 例如,在一些实验中,研究者发现被试可以在没有回忆起具体学习情境的情况下,对曾经出现过的刺激做出正确的判断,这被认为是熟悉性在起作用1。 +2. **双重过程理论** + - 双重过程理论则认为,识别记忆是基于两个过程:回忆和熟悉性。回忆是指对过去经验的有意识的回忆,它可以使我们回忆起具体的细节和情境;熟悉性则是一种自动的、无意识的感知。 + - 该理论认为,在识别记忆中,回忆和熟悉性共同作用,使我们能够判断一个刺激是否曾经出现过。例如,在 “记得 / 知道” 范式中,被试被要求判断他们对一个刺激的记忆是基于回忆还是熟悉性。研究发现,被试可以区分这两种不同的记忆过程,这为双重过程理论提供了支持1。 + + + +1. **神经元节点与连接**:借鉴神经网络原理,将每个记忆单元视为一个神经元节点。节点之间通过连接相互关联,连接的强度代表记忆之间的关联程度。在形态学联想记忆中,具有相似形态特征的记忆节点连接强度较高。例如,苹果和橘子的记忆节点,由于在形状、都是水果等形态语义特征上相似,它们之间的连接强度大于苹果与汽车记忆节点间的连接强度。 +2. **记忆聚类与层次结构**:依据形态特征的相似性对记忆进行聚类,形成不同的记忆簇。每个记忆簇内部的记忆具有较高的相似性,而不同记忆簇之间的记忆相似性较低。同时,构建记忆的层次结构,高层次的记忆节点代表更抽象、概括的概念,低层次的记忆节点对应具体的实例。比如,“水果” 作为高层次记忆节点,连接着 “苹果”“橘子”“香蕉” 等低层次具体水果的记忆节点。 +3. **网络的动态更新**:随着新记忆的不断加入,记忆网络动态调整。新记忆节点根据其形态特征与现有网络中的节点建立连接,同时影响相关连接的强度。若新记忆与某个记忆簇的特征高度相似,则被纳入该记忆簇;若具有独特特征,则可能引发新的记忆簇的形成。例如,当系统学习到一种新的水果 “番石榴”,它会根据番石榴的形态、语义等特征,在记忆网络中找到与之最相似的区域(如水果记忆簇),并建立相应连接,同时调整周围节点连接强度以适应这一新记忆。 + + + +- **相似性联想**:该理论认为,当两个或多个事物在形态上具有相似性时,它们在记忆中会形成关联。例如,梨和苹果在形状和都是水果这一属性上有相似性,所以当我们看到梨时,很容易通过形态学联想记忆联想到苹果。这种相似性联想有助于我们对新事物进行分类和理解,当遇到一个新的类似水果时,我们可以通过与已有的水果记忆进行相似性匹配,来推测它的一些特征。 +- **时空关联性联想**:除了相似性联想,MAM 还强调时空关联性联想。如果两个事物在时间或空间上经常同时出现,它们也会在记忆中形成关联。比如,每次在公园里看到花的时候,都能听到鸟儿的叫声,那么花和鸟儿叫声的形态特征(花的视觉形态和鸟叫的听觉形态)就会在记忆中形成关联,以后听到鸟叫可能就会联想到公园里的花。 \ No newline at end of file diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index bd71be019..c730466b3 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -121,9 +121,9 @@ async def build_memory_task(): @scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") async def forget_memory_task(): """每30秒执行一次记忆构建""" - # print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") - # await hippocampus.operation_forget_topic(percentage=0.1) - # print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") + print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") + await hippocampus.operation_forget_topic(percentage=0.1) + print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval + 10, id="merge_memory") diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 973bcad2d..740e9a677 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -203,7 +203,7 @@ class EmojiManager: try: prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。' - content, _ = await self.llm_emotion_judge.generate_response_async(prompt) + content, _ = await self.llm_emotion_judge.generate_response_async(prompt,temperature=1.5) logger.info(f"输出描述: {content}") return content diff --git a/src/plugins/knowledege/knowledge_library.py b/src/plugins/knowledege/knowledge_library.py index 481076961..4bf6227bb 100644 --- a/src/plugins/knowledege/knowledge_library.py +++ b/src/plugins/knowledege/knowledge_library.py @@ -79,7 +79,7 @@ class KnowledgeLibrary: content = f.read() # 按1024字符分段 - segments = [content[i:i+600] for i in range(0, len(content), 600)] + segments = [content[i:i+600] for i in range(0, len(content), 300)] # 处理每个分段 for segment in segments: diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 9b325b36d..0730f9e57 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -25,26 +25,46 @@ class Memory_graph: self.db = Database.get_instance() def connect_dot(self, concept1, concept2): - # 如果边已存在,增加 strength + # 避免自连接 + if concept1 == concept2: + return + + current_time = datetime.datetime.now().timestamp() + + # 如果边已存在,增加 strength if self.G.has_edge(concept1, concept2): self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1 + # 更新最后修改时间 + self.G[concept1][concept2]['last_modified'] = current_time else: - # 如果是新边,初始化 strength 为 1 - self.G.add_edge(concept1, concept2, strength=1) + # 如果是新边,初始化 strength 为 1 + self.G.add_edge(concept1, concept2, + strength=1, + created_time=current_time, # 添加创建时间 + last_modified=current_time) # 添加最后修改时间 def add_dot(self, concept, memory): + current_time = datetime.datetime.now().timestamp() + if concept in self.G: - # 如果节点已存在,将新记忆添加到现有列表中 if 'memory_items' in self.G.nodes[concept]: if not isinstance(self.G.nodes[concept]['memory_items'], list): - # 如果当前不是列表,将其转换为列表 self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']] self.G.nodes[concept]['memory_items'].append(memory) + # 更新最后修改时间 + self.G.nodes[concept]['last_modified'] = current_time else: self.G.nodes[concept]['memory_items'] = [memory] + # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time + if 'created_time' not in self.G.nodes[concept]: + self.G.nodes[concept]['created_time'] = current_time + self.G.nodes[concept]['last_modified'] = current_time else: - # 如果是新节点,创建新的记忆列表 - self.G.add_node(concept, memory_items=[memory]) + # 如果是新节点,创建新的记忆列表 + self.G.add_node(concept, + memory_items=[memory], + created_time=current_time, # 添加创建时间 + last_modified=current_time) # 添加最后修改时间 def get_dot(self, concept): # 检查节点是否存在于图中 @@ -191,15 +211,11 @@ class Hippocampus: async def memory_compress(self, messages: list, compress_rate=0.1): """压缩消息记录为记忆 - Args: - messages: 消息记录字典列表,每个字典包含text和time字段 - compress_rate: 压缩率 - Returns: - set: (话题, 记忆) 元组集合 + tuple: (压缩记忆集合, 相似主题字典) """ if not messages: - return set() + return set(), {} # 合并消息文本,同时保留时间信息 input_text = "" @@ -246,12 +262,33 @@ class Hippocampus: # 等待所有任务完成 compressed_memory = set() + similar_topics_dict = {} # 存储每个话题的相似主题列表 for topic, task in tasks: response = await task if response: compressed_memory.add((topic, response[0])) + # 为每个话题查找相似的已存在主题 + existing_topics = list(self.memory_graph.G.nodes()) + similar_topics = [] + + for existing_topic in existing_topics: + topic_words = set(jieba.cut(topic)) + existing_words = set(jieba.cut(existing_topic)) + + all_words = topic_words | existing_words + v1 = [1 if word in topic_words else 0 for word in all_words] + v2 = [1 if word in existing_words else 0 for word in all_words] + + similarity = cosine_similarity(v1, v2) + + if similarity >= 0.6: + similar_topics.append((existing_topic, similarity)) + + similar_topics.sort(key=lambda x: x[1], reverse=True) + similar_topics = similar_topics[:5] + similar_topics_dict[topic] = similar_topics - return compressed_memory + return compressed_memory, similar_topics_dict def calculate_topic_num(self, text, compress_rate): """计算文本的话题数量""" @@ -265,33 +302,40 @@ class Hippocampus: return topic_num async def operation_build_memory(self, chat_size=20): - # 最近消息获取频率 - time_frequency = {'near': 2, 'mid': 4, 'far': 2} - memory_sample = self.get_memory_sample(chat_size, time_frequency) - - for i, input_text in enumerate(memory_sample, 1): - # 加载进度可视化 + time_frequency = {'near': 3, 'mid': 8, 'far': 5} + memory_samples = self.get_memory_sample(chat_size, time_frequency) + + for i, messages in enumerate(memory_samples, 1): all_topics = [] - progress = (i / len(memory_sample)) * 100 + # 加载进度可视化 + progress = (i / len(memory_samples)) * 100 bar_length = 30 - filled_length = int(bar_length * i // len(memory_sample)) + filled_length = int(bar_length * i // len(memory_samples)) bar = '█' * filled_length + '-' * (bar_length - filled_length) - logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") + logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - # 生成压缩后记忆 ,表现为 (话题,记忆) 的元组 - compressed_memory = set() compress_rate = 0.1 - compressed_memory = await self.memory_compress(input_text, compress_rate) - logger.info(f"压缩后记忆数量: {len(compressed_memory)}") - - # 将记忆加入到图谱中 + compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) + logger.info(f"压缩后记忆数量: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") + for topic, memory in compressed_memory: logger.info(f"添加节点: {topic}") self.memory_graph.add_dot(topic, memory) - all_topics.append(topic) # 收集所有话题 + all_topics.append(topic) + + # 连接相似的已存在主题 + if topic in similar_topics_dict: + similar_topics = similar_topics_dict[topic] + for similar_topic, similarity in similar_topics: + if topic != similar_topic: + strength = int(similarity * 10) + logger.info(f"连接相似节点: {topic} 和 {similar_topic} (强度: {strength})") + self.memory_graph.G.add_edge(topic, similar_topic, strength=strength) + + # 连接同批次的相关话题 for i in range(len(all_topics)): for j in range(i + 1, len(all_topics)): - logger.info(f"连接节点: {all_topics[i]} 和 {all_topics[j]}") + logger.info(f"连接同批次节点: {all_topics[i]} 和 {all_topics[j]}") self.memory_graph.connect_dot(all_topics[i], all_topics[j]) self.sync_memory_to_db() @@ -302,7 +346,7 @@ class Hippocampus: db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) memory_nodes = list(self.memory_graph.G.nodes(data=True)) - # 转换数据库节点为字典格式,方便查找 + # 转换数据库节点为字典格式,方便查找 db_nodes_dict = {node['concept']: node for node in db_nodes} # 检查并更新节点 @@ -313,13 +357,19 @@ class Hippocampus: # 计算内存中节点的特征值 memory_hash = self.calculate_node_hash(concept, memory_items) + + # 获取时间信息 + created_time = data.get('created_time', datetime.datetime.now().timestamp()) + last_modified = data.get('last_modified', datetime.datetime.now().timestamp()) if concept not in db_nodes_dict: - # 数据库中缺少的节点,添加 + # 数据库中缺少的节点,添加 node_data = { 'concept': concept, 'memory_items': memory_items, - 'hash': memory_hash + 'hash': memory_hash, + 'created_time': created_time, + 'last_modified': last_modified } self.memory_graph.db.db.graph_data.nodes.insert_one(node_data) else: @@ -327,25 +377,21 @@ class Hippocampus: db_node = db_nodes_dict[concept] db_hash = db_node.get('hash', None) - # 如果特征值不同,则更新节点 + # 如果特征值不同,则更新节点 if db_hash != memory_hash: self.memory_graph.db.db.graph_data.nodes.update_one( {'concept': concept}, {'$set': { 'memory_items': memory_items, - 'hash': memory_hash + 'hash': memory_hash, + 'created_time': created_time, + 'last_modified': last_modified }} ) - # 检查并删除数据库中多余的节点 - memory_concepts = set(node[0] for node in memory_nodes) - for db_node in db_nodes: - if db_node['concept'] not in memory_concepts: - self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) - # 处理边的信息 db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) - memory_edges = list(self.memory_graph.G.edges()) + memory_edges = list(self.memory_graph.G.edges(data=True)) # 创建边的哈希值字典 db_edge_dict = {} @@ -357,10 +403,14 @@ class Hippocampus: } # 检查并更新边 - for source, target in memory_edges: + for source, target, data in memory_edges: edge_hash = self.calculate_edge_hash(source, target) edge_key = (source, target) - strength = self.memory_graph.G[source][target].get('strength', 1) + strength = data.get('strength', 1) + + # 获取边的时间信息 + created_time = data.get('created_time', datetime.datetime.now().timestamp()) + last_modified = data.get('last_modified', datetime.datetime.now().timestamp()) if edge_key not in db_edge_dict: # 添加新边 @@ -368,7 +418,9 @@ class Hippocampus: 'source': source, 'target': target, 'strength': strength, - 'hash': edge_hash + 'hash': edge_hash, + 'created_time': created_time, + 'last_modified': last_modified } self.memory_graph.db.db.graph_data.edges.insert_one(edge_data) else: @@ -378,20 +430,12 @@ class Hippocampus: {'source': source, 'target': target}, {'$set': { 'hash': edge_hash, - 'strength': strength + 'strength': strength, + 'created_time': created_time, + 'last_modified': last_modified }} ) - # 删除多余的边 - memory_edge_set = set(memory_edges) - for edge_key in db_edge_dict: - if edge_key not in memory_edge_set: - source, target = edge_key - self.memory_graph.db.db.graph_data.edges.delete_one({ - 'source': source, - 'target': target - }) - def sync_memory_from_db(self): """从数据库同步数据到内存中的图结构""" # 清空当前图 @@ -405,61 +449,107 @@ class Hippocampus: # 确保memory_items是列表 if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] + + # 获取时间信息 + created_time = node.get('created_time', datetime.datetime.now().timestamp()) + last_modified = node.get('last_modified', datetime.datetime.now().timestamp()) + # 添加节点到图中 - self.memory_graph.G.add_node(concept, memory_items=memory_items) + self.memory_graph.G.add_node(concept, + memory_items=memory_items, + created_time=created_time, + last_modified=last_modified) # 从数据库加载所有边 edges = self.memory_graph.db.db.graph_data.edges.find() for edge in edges: source = edge['source'] target = edge['target'] - strength = edge.get('strength', 1) # 获取 strength,默认为 1 + strength = edge.get('strength', 1) # 获取 strength,默认为 1 + + # 获取时间信息 + created_time = edge.get('created_time', datetime.datetime.now().timestamp()) + last_modified = edge.get('last_modified', datetime.datetime.now().timestamp()) + # 只有当源节点和目标节点都存在时才添加边 if source in self.memory_graph.G and target in self.memory_graph.G: - self.memory_graph.G.add_edge(source, target, strength=strength) + self.memory_graph.G.add_edge(source, target, + strength=strength, + created_time=created_time, + last_modified=last_modified) async def operation_forget_topic(self, percentage=0.1): - """随机选择图中一定比例的节点进行检查,根据条件决定是否遗忘""" - # 获取所有节点 + """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘""" all_nodes = list(self.memory_graph.G.nodes()) - # 计算要检查的节点数量 - check_count = max(1, int(len(all_nodes) * percentage)) - # 随机选择节点 - nodes_to_check = random.sample(all_nodes, check_count) - - forgotten_nodes = [] + all_edges = list(self.memory_graph.G.edges()) + + check_nodes_count = max(1, int(len(all_nodes) * percentage)) + check_edges_count = max(1, int(len(all_edges) * percentage)) + + nodes_to_check = random.sample(all_nodes, check_nodes_count) + edges_to_check = random.sample(all_edges, check_edges_count) + + edge_changes = {'weakened': 0, 'removed': 0} + node_changes = {'reduced': 0, 'removed': 0} + + current_time = datetime.datetime.now().timestamp() + + # 检查并遗忘连接 + logger.info("开始检查连接...") + for source, target in edges_to_check: + edge_data = self.memory_graph.G[source][target] + last_modified = edge_data.get('last_modified') + # print(source,target) + # print(f"float(last_modified):{float(last_modified)}" ) + # print(f"current_time:{current_time}") + # print(f"current_time - last_modified:{current_time - last_modified}") + if current_time - last_modified > 3600*24: # test + current_strength = edge_data.get('strength', 1) + new_strength = current_strength - 1 + + if new_strength <= 0: + self.memory_graph.G.remove_edge(source, target) + edge_changes['removed'] += 1 + logger.info(f"\033[1;31m[连接移除]\033[0m {source} - {target}") + else: + edge_data['strength'] = new_strength + edge_data['last_modified'] = current_time + edge_changes['weakened'] += 1 + logger.info(f"\033[1;34m[连接减弱]\033[0m {source} - {target} (强度: {current_strength} -> {new_strength})") + + # 检查并遗忘话题 + logger.info("开始检查节点...") for node in nodes_to_check: - # 获取节点的连接数 - connections = self.memory_graph.G.degree(node) - - # 获取节点的内容条数 - memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - - # 检查连接强度 - weak_connections = True - if connections > 1: # 只有当连接数大于1时才检查强度 - for neighbor in self.memory_graph.G.neighbors(node): - strength = self.memory_graph.G[node][neighbor].get('strength', 1) - if strength > 2: - weak_connections = False - break - - # 如果满足遗忘条件 - if (connections <= 1 and weak_connections) or content_count <= 2: - removed_item = self.memory_graph.forget_topic(node) - if removed_item: - forgotten_nodes.append((node, removed_item)) - logger.debug(f"遗忘节点 {node} 的记忆: {removed_item}") - - # 同步到数据库 - if forgotten_nodes: + node_data = self.memory_graph.G.nodes[node] + last_modified = node_data.get('last_modified', current_time) + + if current_time - last_modified > 3600*24: # test + memory_items = node_data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + current_count = len(memory_items) + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + if memory_items: + self.memory_graph.G.nodes[node]['memory_items'] = memory_items + self.memory_graph.G.nodes[node]['last_modified'] = current_time + node_changes['reduced'] += 1 + logger.info(f"\033[1;33m[记忆减少]\033[0m {node} (记忆数量: {current_count} -> {len(memory_items)})") + else: + self.memory_graph.G.remove_node(node) + node_changes['removed'] += 1 + logger.info(f"\033[1;31m[节点移除]\033[0m {node}") + + if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): self.sync_memory_to_db() - logger.debug(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") + logger.info("\n遗忘操作统计:") + logger.info(f"连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") + logger.info(f"节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") else: - logger.debug("本次检查没有节点满足遗忘条件") + logger.info("\n本次检查没有节点或连接满足遗忘条件") async def merge_memory(self, topic): """ @@ -486,7 +576,7 @@ class Hippocampus: logger.debug(f"选择的记忆:\n{merged_text}") # 使用memory_compress生成新的压缩记忆 - compressed_memories = await self.memory_compress(selected_memories, 0.1) + compressed_memories, _ = await self.memory_compress(selected_memories, 0.1) # 从原记忆列表中移除被选中的记忆 for memory in selected_memories: diff --git a/src/plugins/memory_system/memory_test1.py b/src/plugins/memory_system/memory_test1.py new file mode 100644 index 000000000..bbd734ec2 --- /dev/null +++ b/src/plugins/memory_system/memory_test1.py @@ -0,0 +1,1208 @@ +# -*- coding: utf-8 -*- +import datetime +import math +import os +import random +import sys +import time +from collections import Counter +from pathlib import Path + +import matplotlib.pyplot as plt +import networkx as nx +import pymongo +from dotenv import load_dotenv +from loguru import logger +import jieba + +''' +该理论认为,当两个或多个事物在形态上具有相似性时, +它们在记忆中会形成关联。 +例如,梨和苹果在形状和都是水果这一属性上有相似性, +所以当我们看到梨时,很容易通过形态学联想记忆联想到苹果。 +这种相似性联想有助于我们对新事物进行分类和理解, +当遇到一个新的类似水果时, +我们可以通过与已有的水果记忆进行相似性匹配, +来推测它的一些特征。 + + + +时空关联性联想: +除了相似性联想,MAM 还强调时空关联性联想。 +如果两个事物在时间或空间上经常同时出现,它们也会在记忆中形成关联。 +比如,每次在公园里看到花的时候,都能听到鸟儿的叫声, +那么花和鸟儿叫声的形态特征(花的视觉形态和鸟叫的听觉形态)就会在记忆中形成关联, +以后听到鸟叫可能就会联想到公园里的花。 + +''' + +# from chat.config import global_config +sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 +from src.common.database import Database +from src.plugins.memory_system.offline_llm import LLMModel + +# 获取当前文件的目录 +current_dir = Path(__file__).resolve().parent +# 获取项目根目录(上三层目录) +project_root = current_dir.parent.parent.parent +# env.dev文件路径 +env_path = project_root / ".env.dev" + +# 加载环境变量 +if env_path.exists(): + logger.info(f"从 {env_path} 加载环境变量") + load_dotenv(env_path) +else: + logger.warning(f"未找到环境变量文件: {env_path}") + logger.info("将使用默认配置") + +class Database: + _instance = None + db = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + if not Database.db: + Database.initialize( + host=os.getenv("MONGODB_HOST"), + port=int(os.getenv("MONGODB_PORT")), + db_name=os.getenv("DATABASE_NAME"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE") + ) + + @classmethod + def initialize(cls, host, port, db_name, username=None, password=None, auth_source="admin"): + try: + if username and password: + uri = f"mongodb://{username}:{password}@{host}:{port}/{db_name}?authSource={auth_source}" + else: + uri = f"mongodb://{host}:{port}" + + client = pymongo.MongoClient(uri) + cls.db = client[db_name] + # 测试连接 + client.server_info() + logger.success("MongoDB连接成功!") + + except Exception as e: + logger.error(f"初始化MongoDB失败: {str(e)}") + raise + +def calculate_information_content(text): + """计算文本的信息量(熵)""" + char_count = Counter(text) + total_chars = len(text) + + entropy = 0 + for count in char_count.values(): + probability = count / total_chars + entropy -= probability * math.log2(probability) + + return entropy + +def get_cloest_chat_from_db(db, length: int, timestamp: str): + """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 + + Returns: + list: 消息记录字典列表,每个字典包含消息内容和时间信息 + """ + chat_records = [] + closest_record = db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) + + if closest_record and closest_record.get('memorized', 0) < 4: + closest_time = closest_record['time'] + group_id = closest_record['group_id'] + # 获取该时间戳之后的length条消息,且groupid相同 + records = list(db.db.messages.find( + {"time": {"$gt": closest_time}, "group_id": group_id} + ).sort('time', 1).limit(length)) + + # 更新每条消息的memorized属性 + for record in records: + current_memorized = record.get('memorized', 0) + if current_memorized > 3: + print("消息已读取3次,跳过") + return '' + + # 更新memorized值 + db.db.messages.update_one( + {"_id": record["_id"]}, + {"$set": {"memorized": current_memorized + 1}} + ) + + # 添加到记录列表中 + chat_records.append({ + 'text': record["detailed_plain_text"], + 'time': record["time"], + 'group_id': record["group_id"] + }) + + return chat_records + +class Memory_cortex: + def __init__(self, memory_graph: 'Memory_graph'): + self.memory_graph = memory_graph + + def sync_memory_from_db(self): + """ + 从数据库同步数据到内存中的图结构 + 将清空当前内存中的图,并从数据库重新加载所有节点和边 + """ + # 清空当前图 + self.memory_graph.G.clear() + + # 获取当前时间作为默认时间 + default_time = datetime.datetime.now().timestamp() + + # 从数据库加载所有节点 + nodes = self.memory_graph.db.db.graph_data.nodes.find() + for node in nodes: + concept = node['concept'] + memory_items = node.get('memory_items', []) + # 确保memory_items是列表 + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 获取时间属性,如果不存在则使用默认时间 + created_time = node.get('created_time') + last_modified = node.get('last_modified') + + # 如果时间属性不存在,则更新数据库 + if created_time is None or last_modified is None: + created_time = default_time + last_modified = default_time + # 更新数据库中的节点 + self.memory_graph.db.db.graph_data.nodes.update_one( + {'concept': concept}, + {'$set': { + 'created_time': created_time, + 'last_modified': last_modified + }} + ) + logger.info(f"为节点 {concept} 添加默认时间属性") + + # 添加节点到图中,包含时间属性 + self.memory_graph.G.add_node(concept, + memory_items=memory_items, + created_time=created_time, + last_modified=last_modified) + + # 从数据库加载所有边 + edges = self.memory_graph.db.db.graph_data.edges.find() + for edge in edges: + source = edge['source'] + target = edge['target'] + + # 只有当源节点和目标节点都存在时才添加边 + if source in self.memory_graph.G and target in self.memory_graph.G: + # 获取时间属性,如果不存在则使用默认时间 + created_time = edge.get('created_time') + last_modified = edge.get('last_modified') + + # 如果时间属性不存在,则更新数据库 + if created_time is None or last_modified is None: + created_time = default_time + last_modified = default_time + # 更新数据库中的边 + self.memory_graph.db.db.graph_data.edges.update_one( + {'source': source, 'target': target}, + {'$set': { + 'created_time': created_time, + 'last_modified': last_modified + }} + ) + logger.info(f"为边 {source} - {target} 添加默认时间属性") + + self.memory_graph.G.add_edge(source, target, + strength=edge.get('strength', 1), + created_time=created_time, + last_modified=last_modified) + + logger.success("从数据库同步记忆图谱完成") + + def calculate_node_hash(self, concept, memory_items): + """ + 计算节点的特征值 + """ + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + # 将记忆项排序以确保相同内容生成相同的哈希值 + sorted_items = sorted(memory_items) + # 组合概念和记忆项生成特征值 + content = f"{concept}:{'|'.join(sorted_items)}" + return hash(content) + + def calculate_edge_hash(self, source, target): + """ + 计算边的特征值 + """ + # 对源节点和目标节点排序以确保相同的边生成相同的哈希值 + nodes = sorted([source, target]) + return hash(f"{nodes[0]}:{nodes[1]}") + + def sync_memory_to_db(self): + """ + 检查并同步内存中的图结构与数据库 + 使用特征值(哈希值)快速判断是否需要更新 + """ + current_time = datetime.datetime.now().timestamp() + + # 获取数据库中所有节点和内存中所有节点 + db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + + # 转换数据库节点为字典格式,方便查找 + db_nodes_dict = {node['concept']: node for node in db_nodes} + + # 检查并更新节点 + for concept, data in memory_nodes: + memory_items = data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 计算内存中节点的特征值 + memory_hash = self.calculate_node_hash(concept, memory_items) + + if concept not in db_nodes_dict: + # 数据库中缺少的节点,添加 + node_data = { + 'concept': concept, + 'memory_items': memory_items, + 'hash': memory_hash, + 'created_time': data.get('created_time', current_time), + 'last_modified': data.get('last_modified', current_time) + } + self.memory_graph.db.db.graph_data.nodes.insert_one(node_data) + else: + # 获取数据库中节点的特征值 + db_node = db_nodes_dict[concept] + db_hash = db_node.get('hash', None) + + # 如果特征值不同,则更新节点 + if db_hash != memory_hash: + self.memory_graph.db.db.graph_data.nodes.update_one( + {'concept': concept}, + {'$set': { + 'memory_items': memory_items, + 'hash': memory_hash, + 'last_modified': current_time + }} + ) + + # 检查并删除数据库中多余的节点 + memory_concepts = set(node[0] for node in memory_nodes) + for db_node in db_nodes: + if db_node['concept'] not in memory_concepts: + self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) + + # 处理边的信息 + db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 创建边的哈希值字典 + db_edge_dict = {} + for edge in db_edges: + edge_hash = self.calculate_edge_hash(edge['source'], edge['target']) + db_edge_dict[(edge['source'], edge['target'])] = { + 'hash': edge_hash, + 'strength': edge.get('strength', 1) + } + + # 检查并更新边 + for source, target, data in memory_edges: + edge_hash = self.calculate_edge_hash(source, target) + edge_key = (source, target) + strength = data.get('strength', 1) + + if edge_key not in db_edge_dict: + # 添加新边 + edge_data = { + 'source': source, + 'target': target, + 'strength': strength, + 'hash': edge_hash, + 'created_time': data.get('created_time', current_time), + 'last_modified': data.get('last_modified', current_time) + } + self.memory_graph.db.db.graph_data.edges.insert_one(edge_data) + else: + # 检查边的特征值是否变化 + if db_edge_dict[edge_key]['hash'] != edge_hash: + self.memory_graph.db.db.graph_data.edges.update_one( + {'source': source, 'target': target}, + {'$set': { + 'hash': edge_hash, + 'strength': strength, + 'last_modified': current_time + }} + ) + + # 删除多余的边 + memory_edge_set = set((source, target) for source, target, _ in memory_edges) + for edge_key in db_edge_dict: + if edge_key not in memory_edge_set: + source, target = edge_key + self.memory_graph.db.db.graph_data.edges.delete_one({ + 'source': source, + 'target': target + }) + + logger.success("完成记忆图谱与数据库的差异同步") + + def remove_node_from_db(self, topic): + """ + 从数据库中删除指定节点及其相关的边 + + Args: + topic: 要删除的节点概念 + """ + # 删除节点 + self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': topic}) + # 删除所有涉及该节点的边 + self.memory_graph.db.db.graph_data.edges.delete_many({ + '$or': [ + {'source': topic}, + {'target': topic} + ] + }) + +class Memory_graph: + def __init__(self): + self.G = nx.Graph() # 使用 networkx 的图结构 + self.db = Database.get_instance() + + def connect_dot(self, concept1, concept2): + # 避免自连接 + if concept1 == concept2: + return + + current_time = datetime.datetime.now().timestamp() + + # 如果边已存在,增加 strength + if self.G.has_edge(concept1, concept2): + self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1 + # 更新最后修改时间 + self.G[concept1][concept2]['last_modified'] = current_time + else: + # 如果是新边,初始化 strength 为 1 + self.G.add_edge(concept1, concept2, + strength=1, + created_time=current_time, + last_modified=current_time) + + def add_dot(self, concept, memory): + current_time = datetime.datetime.now().timestamp() + + if concept in self.G: + # 如果节点已存在,将新记忆添加到现有列表中 + if 'memory_items' in self.G.nodes[concept]: + if not isinstance(self.G.nodes[concept]['memory_items'], list): + # 如果当前不是列表,将其转换为列表 + self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']] + self.G.nodes[concept]['memory_items'].append(memory) + # 更新最后修改时间 + self.G.nodes[concept]['last_modified'] = current_time + else: + self.G.nodes[concept]['memory_items'] = [memory] + self.G.nodes[concept]['last_modified'] = current_time + else: + # 如果是新节点,创建新的记忆列表 + self.G.add_node(concept, + memory_items=[memory], + created_time=current_time, + last_modified=current_time) + + def get_dot(self, concept): + # 检查节点是否存在于图中 + if concept in self.G: + # 从图中获取节点数据 + node_data = self.G.nodes[concept] + return concept, node_data + return None + + def get_related_item(self, topic, depth=1): + if topic not in self.G: + return [], [] + + first_layer_items = [] + second_layer_items = [] + + # 获取相邻节点 + neighbors = list(self.G.neighbors(topic)) + + # 获取当前节点的记忆项 + node_data = self.get_dot(topic) + if node_data: + concept, data = node_data + if 'memory_items' in data: + memory_items = data['memory_items'] + if isinstance(memory_items, list): + first_layer_items.extend(memory_items) + else: + first_layer_items.append(memory_items) + + # 只在depth=2时获取第二层记忆 + if depth >= 2: + # 获取相邻节点的记忆项 + for neighbor in neighbors: + node_data = self.get_dot(neighbor) + if node_data: + concept, data = node_data + if 'memory_items' in data: + memory_items = data['memory_items'] + if isinstance(memory_items, list): + second_layer_items.extend(memory_items) + else: + second_layer_items.append(memory_items) + + return first_layer_items, second_layer_items + + @property + def dots(self): + # 返回所有节点对应的 Memory_dot 对象 + return [self.get_dot(node) for node in self.G.nodes()] + +# 海马体 +class Hippocampus: + def __init__(self, memory_graph: Memory_graph): + self.memory_graph = memory_graph + self.memory_cortex = Memory_cortex(memory_graph) + self.llm_model = LLMModel() + self.llm_model_small = LLMModel(model_name="deepseek-ai/DeepSeek-V2.5") + self.llm_model_get_topic = LLMModel(model_name="Pro/Qwen/Qwen2.5-7B-Instruct") + self.llm_model_summary = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct") + + def get_memory_sample(self, chat_size=20, time_frequency:dict={'near':2,'mid':4,'far':3}): + """获取记忆样本 + + Returns: + list: 消息记录列表,每个元素是一个消息记录字典列表 + """ + current_timestamp = datetime.datetime.now().timestamp() + chat_samples = [] + + # 短期:1h 中期:4h 长期:24h + for _ in range(time_frequency.get('near')): + random_time = current_timestamp - random.randint(1, 3600*4) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('mid')): + random_time = current_timestamp - random.randint(3600*4, 3600*24) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('far')): + random_time = current_timestamp - random.randint(3600*24, 3600*24*7) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + return chat_samples + + def calculate_topic_num(self,text, compress_rate): + """计算文本的话题数量""" + information_content = calculate_information_content(text) + topic_by_length = text.count('\n')*compress_rate + topic_by_information_content = max(1, min(5, int((information_content-3) * 2))) + topic_num = int((topic_by_length + topic_by_information_content)/2) + print(f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") + return topic_num + + async def memory_compress(self, messages: list, compress_rate=0.1): + """压缩消息记录为记忆 + + Args: + messages: 消息记录字典列表,每个字典包含text和time字段 + compress_rate: 压缩率 + + Returns: + tuple: (压缩记忆集合, 相似主题字典) + - 压缩记忆集合: set of (话题, 记忆) 元组 + - 相似主题字典: dict of {话题: [(相似主题, 相似度), ...]} + """ + if not messages: + return set(), {} + + # 合并消息文本,同时保留时间信息 + input_text = "" + time_info = "" + # 计算最早和最晚时间 + earliest_time = min(msg['time'] for msg in messages) + latest_time = max(msg['time'] for msg in messages) + + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) + latest_dt = datetime.datetime.fromtimestamp(latest_time) + + # 如果是同一年 + if earliest_dt.year == latest_dt.year: + earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%m-%d %H:%M:%S") + time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" + else: + earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" + + for msg in messages: + input_text += f"{msg['text']}\n" + + print(input_text) + + topic_num = self.calculate_topic_num(input_text, compress_rate) + topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(input_text, topic_num)) + + # 过滤topics + filter_keywords = ['表情包', '图片', '回复', '聊天记录'] + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] + + print(f"过滤后话题: {filtered_topics}") + + # 为每个话题查找相似的已存在主题 + print("\n检查相似主题:") + similar_topics_dict = {} # 存储每个话题的相似主题列表 + + for topic in filtered_topics: + # 获取所有现有节点 + existing_topics = list(self.memory_graph.G.nodes()) + similar_topics = [] + + # 对每个现有节点计算相似度 + for existing_topic in existing_topics: + # 使用jieba分词并计算余弦相似度 + topic_words = set(jieba.cut(topic)) + existing_words = set(jieba.cut(existing_topic)) + + # 计算词向量 + all_words = topic_words | existing_words + v1 = [1 if word in topic_words else 0 for word in all_words] + v2 = [1 if word in existing_words else 0 for word in all_words] + + # 计算余弦相似度 + similarity = cosine_similarity(v1, v2) + + # 如果相似度超过阈值,添加到结果中 + if similarity >= 0.6: # 设置相似度阈值 + similar_topics.append((existing_topic, similarity)) + + # 按相似度降序排序 + similar_topics.sort(key=lambda x: x[1], reverse=True) + # 只保留前5个最相似的主题 + similar_topics = similar_topics[:5] + + # 存储到字典中 + similar_topics_dict[topic] = similar_topics + + # 输出结果 + if similar_topics: + print(f"\n主题「{topic}」的相似主题:") + for similar_topic, score in similar_topics: + print(f"- {similar_topic} (相似度: {score:.3f})") + else: + print(f"\n主题「{topic}」没有找到相似主题") + + # 创建所有话题的请求任务 + tasks = [] + for topic in filtered_topics: + topic_what_prompt = self.topic_what(input_text, topic , time_info) + # 创建异步任务 + task = self.llm_model_small.generate_response_async(topic_what_prompt) + tasks.append((topic.strip(), task)) + + # 等待所有任务完成 + compressed_memory = set() + for topic, task in tasks: + response = await task + if response: + compressed_memory.add((topic, response[0])) + + return compressed_memory, similar_topics_dict + + async def operation_build_memory(self, chat_size=12): + # 最近消息获取频率 + time_frequency = {'near': 3, 'mid': 8, 'far': 5} + memory_samples = self.get_memory_sample(chat_size, time_frequency) + + all_topics = [] # 用于存储所有话题 + + for i, messages in enumerate(memory_samples, 1): + # 加载进度可视化 + all_topics = [] + progress = (i / len(memory_samples)) * 100 + bar_length = 30 + filled_length = int(bar_length * i // len(memory_samples)) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") + + # 生成压缩后记忆 + compress_rate = 0.1 + compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) + print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") + + # 将记忆加入到图谱中 + for topic, memory in compressed_memory: + print(f"\033[1;32m添加节点\033[0m: {topic}") + self.memory_graph.add_dot(topic, memory) + all_topics.append(topic) + + # 连接相似的已存在主题 + if topic in similar_topics_dict: + similar_topics = similar_topics_dict[topic] + for similar_topic, similarity in similar_topics: + # 避免自连接 + if topic != similar_topic: + # 根据相似度设置连接强度 + strength = int(similarity * 10) # 将0.3-1.0的相似度映射到3-10的强度 + print(f"\033[1;36m连接相似节点\033[0m: {topic} 和 {similar_topic} (强度: {strength})") + # 使用相似度作为初始连接强度 + self.memory_graph.G.add_edge(topic, similar_topic, strength=strength) + + # 连接同批次的相关话题 + for i in range(len(all_topics)): + for j in range(i + 1, len(all_topics)): + print(f"\033[1;32m连接同批次节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") + self.memory_graph.connect_dot(all_topics[i], all_topics[j]) + + self.memory_cortex.sync_memory_to_db() + + def forget_connection(self, source, target): + """ + 检查并可能遗忘一个连接 + + Args: + source: 连接的源节点 + target: 连接的目标节点 + + Returns: + tuple: (是否有变化, 变化类型, 变化详情) + 变化类型: 0-无变化, 1-强度减少, 2-连接移除 + """ + current_time = datetime.datetime.now().timestamp() + # 获取边的属性 + edge_data = self.memory_graph.G[source][target] + last_modified = edge_data.get('last_modified', current_time) + + # 如果连接超过7天未更新 + if current_time - last_modified > 6000: # test + # 获取当前强度 + current_strength = edge_data.get('strength', 1) + # 减少连接强度 + new_strength = current_strength - 1 + edge_data['strength'] = new_strength + edge_data['last_modified'] = current_time + + # 如果强度降为0,移除连接 + if new_strength <= 0: + self.memory_graph.G.remove_edge(source, target) + return True, 2, f"移除连接: {source} - {target} (强度降至0)" + else: + return True, 1, f"减弱连接: {source} - {target} (强度: {current_strength} -> {new_strength})" + + return False, 0, "" + + def forget_topic(self, topic): + """ + 检查并可能遗忘一个话题的记忆 + + Args: + topic: 要检查的话题 + + Returns: + tuple: (是否有变化, 变化类型, 变化详情) + 变化类型: 0-无变化, 1-记忆减少, 2-节点移除 + """ + current_time = datetime.datetime.now().timestamp() + # 获取节点的最后修改时间 + node_data = self.memory_graph.G.nodes[topic] + last_modified = node_data.get('last_modified', current_time) + + # 如果话题超过7天未更新 + if current_time - last_modified > 3000: # test + memory_items = node_data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + # 获取当前记忆数量 + current_count = len(memory_items) + # 随机选择一条记忆删除 + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + if memory_items: + # 更新节点的记忆项和最后修改时间 + self.memory_graph.G.nodes[topic]['memory_items'] = memory_items + self.memory_graph.G.nodes[topic]['last_modified'] = current_time + return True, 1, f"减少记忆: {topic} (记忆数量: {current_count} -> {len(memory_items)})\n被移除的记忆: {removed_item}" + else: + # 如果没有记忆了,删除节点及其所有连接 + self.memory_graph.G.remove_node(topic) + return True, 2, f"移除节点: {topic} (无剩余记忆)\n最后一条记忆: {removed_item}" + + return False, 0, "" + + async def operation_forget_topic(self, percentage=0.1): + """ + 随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘 + + Args: + percentage: 要检查的节点和边的比例,默认为0.1(10%) + """ + # 获取所有节点和边 + all_nodes = list(self.memory_graph.G.nodes()) + all_edges = list(self.memory_graph.G.edges()) + + # 计算要检查的数量 + check_nodes_count = max(1, int(len(all_nodes) * percentage)) + check_edges_count = max(1, int(len(all_edges) * percentage)) + + # 随机选择要检查的节点和边 + nodes_to_check = random.sample(all_nodes, check_nodes_count) + edges_to_check = random.sample(all_edges, check_edges_count) + + # 用于统计不同类型的变化 + edge_changes = {'weakened': 0, 'removed': 0} + node_changes = {'reduced': 0, 'removed': 0} + + # 检查并遗忘连接 + print("\n开始检查连接...") + for source, target in edges_to_check: + changed, change_type, details = self.forget_connection(source, target) + if changed: + if change_type == 1: + edge_changes['weakened'] += 1 + logger.info(f"\033[1;34m[连接减弱]\033[0m {details}") + elif change_type == 2: + edge_changes['removed'] += 1 + logger.info(f"\033[1;31m[连接移除]\033[0m {details}") + + # 检查并遗忘话题 + print("\n开始检查节点...") + for node in nodes_to_check: + changed, change_type, details = self.forget_topic(node) + if changed: + if change_type == 1: + node_changes['reduced'] += 1 + logger.info(f"\033[1;33m[记忆减少]\033[0m {details}") + elif change_type == 2: + node_changes['removed'] += 1 + logger.info(f"\033[1;31m[节点移除]\033[0m {details}") + + # 同步到数据库 + if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): + self.memory_cortex.sync_memory_to_db() + print("\n遗忘操作统计:") + print(f"连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") + print(f"节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") + else: + print("\n本次检查没有节点或连接满足遗忘条件") + + async def merge_memory(self, topic): + """ + 对指定话题的记忆进行合并压缩 + + Args: + topic: 要合并的话题节点 + """ + # 获取节点的记忆项 + memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 如果记忆项不足,直接返回 + if len(memory_items) < 10: + return + + # 随机选择10条记忆 + selected_memories = random.sample(memory_items, 10) + + # 拼接成文本 + merged_text = "\n".join(selected_memories) + print(f"\n[合并记忆] 话题: {topic}") + print(f"选择的记忆:\n{merged_text}") + + # 使用memory_compress生成新的压缩记忆 + compressed_memories, _ = await self.memory_compress(selected_memories, 0.1) + + # 从原记忆列表中移除被选中的记忆 + for memory in selected_memories: + memory_items.remove(memory) + + # 添加新的压缩记忆 + for _, compressed_memory in compressed_memories: + memory_items.append(compressed_memory) + print(f"添加压缩记忆: {compressed_memory}") + + # 更新节点的记忆项 + self.memory_graph.G.nodes[topic]['memory_items'] = memory_items + print(f"完成记忆合并,当前记忆数量: {len(memory_items)}") + + async def operation_merge_memory(self, percentage=0.1): + """ + 随机检查一定比例的节点,对内容数量超过100的节点进行记忆合并 + + Args: + percentage: 要检查的节点比例,默认为0.1(10%) + """ + # 获取所有节点 + all_nodes = list(self.memory_graph.G.nodes()) + # 计算要检查的节点数量 + check_count = max(1, int(len(all_nodes) * percentage)) + # 随机选择节点 + nodes_to_check = random.sample(all_nodes, check_count) + + merged_nodes = [] + for node in nodes_to_check: + # 获取节点的内容条数 + memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + + # 如果内容数量超过100,进行合并 + if content_count > 100: + print(f"\n检查节点: {node}, 当前记忆数量: {content_count}") + await self.merge_memory(node) + merged_nodes.append(node) + + # 同步到数据库 + if merged_nodes: + self.memory_cortex.sync_memory_to_db() + print(f"\n完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") + else: + print("\n本次检查没有需要合并的节点") + + async def _identify_topics(self, text: str) -> list: + """从文本中识别可能的主题""" + topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(text, 5)) + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + return topics + + def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: + """查找与给定主题相似的记忆主题""" + all_memory_topics = list(self.memory_graph.G.nodes()) + all_similar_topics = [] + + for topic in topics: + if debug_info: + pass + + topic_vector = text_to_vector(topic) + has_similar_topic = False + + for memory_topic in all_memory_topics: + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + similarity = cosine_similarity(v1, v2) + + if similarity >= similarity_threshold: + has_similar_topic = True + all_similar_topics.append((memory_topic, similarity)) + + return all_similar_topics + + def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: + """获取相似度最高的主题""" + seen_topics = set() + top_topics = [] + + for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): + if topic not in seen_topics and len(top_topics) < max_topics: + seen_topics.add(topic) + top_topics.append((topic, score)) + + return top_topics + + async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: + """计算输入文本对记忆的激活程度""" + logger.info(f"[记忆激活]识别主题: {await self._identify_topics(text)}") + + identified_topics = await self._identify_topics(text) + if not identified_topics: + return 0 + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆激活" + ) + + if not all_similar_topics: + return 0 + + top_topics = self._get_top_topics(all_similar_topics, max_topics) + + if len(top_topics) == 1: + topic, score = top_topics[0] + memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + activation = int(score * 50 * penalty) + print(f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") + return activation + + matched_topics = set() + topic_similarities = {} + + for memory_topic, similarity in top_topics: + memory_items = self.memory_graph.G.nodes[memory_topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + for input_topic in identified_topics: + topic_vector = text_to_vector(input_topic) + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + sim = cosine_similarity(v1, v2) + if sim >= similarity_threshold: + matched_topics.add(input_topic) + adjusted_sim = sim * penalty + topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) + print(f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") + + topic_match = len(matched_topics) / len(identified_topics) + average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 + + activation = int((topic_match + average_similarities) / 2 * 100) + print(f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") + + return activation + + async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5) -> list: + """根据输入文本获取相关的记忆内容""" + identified_topics = await self._identify_topics(text) + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆检索" + ) + + relevant_topics = self._get_top_topics(all_similar_topics, max_topics) + + relevant_memories = [] + for topic, score in relevant_topics: + first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) + if first_layer: + if len(first_layer) > max_memory_num/2: + first_layer = random.sample(first_layer, max_memory_num//2) + for memory in first_layer: + relevant_memories.append({ + 'topic': topic, + 'similarity': score, + 'content': memory + }) + + relevant_memories.sort(key=lambda x: x['similarity'], reverse=True) + + if len(relevant_memories) > max_memory_num: + relevant_memories = random.sample(relevant_memories, max_memory_num) + + return relevant_memories + + def find_topic_llm(self,text, topic_num): + prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' + return prompt + + def topic_what(self,text, topic, time_info): + prompt = f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' + return prompt + +def segment_text(text): + """使用jieba进行文本分词""" + seg_text = list(jieba.cut(text)) + return seg_text + +def text_to_vector(text): + """将文本转换为词频向量""" + words = segment_text(text) + vector = {} + for word in words: + vector[word] = vector.get(word, 0) + 1 + return vector + +def cosine_similarity(v1, v2): + """计算两个向量的余弦相似度""" + dot_product = sum(a * b for a, b in zip(v1, v2)) + norm1 = math.sqrt(sum(a * a for a in v1)) + norm2 = math.sqrt(sum(b * b for b in v2)) + if norm1 == 0 or norm2 == 0: + return 0 + return dot_product / (norm1 * norm2) + +def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): + # 设置中文字体 + plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 + plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 + + G = memory_graph.G + + # 创建一个新图用于可视化 + H = G.copy() + + # 过滤掉内容数量小于2的节点 + nodes_to_remove = [] + for node in H.nodes(): + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + if memory_count < 2: + nodes_to_remove.append(node) + + H.remove_nodes_from(nodes_to_remove) + + # 如果没有符合条件的节点,直接返回 + if len(H.nodes()) == 0: + print("没有找到内容数量大于等于2的节点") + return + + # 计算节点大小和颜色 + node_colors = [] + node_sizes = [] + nodes = list(H.nodes()) + + # 获取最大记忆数用于归一化节点大小 + max_memories = 1 + for node in nodes: + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + max_memories = max(max_memories, memory_count) + + # 计算每个节点的大小和颜色 + for node in nodes: + # 计算节点大小(基于记忆数量) + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + # 使用指数函数使变化更明显 + ratio = memory_count / max_memories + size = 400 + 2000 * (ratio ** 2) # 增大节点大小 + node_sizes.append(size) + + # 计算节点颜色(基于连接数) + degree = H.degree(node) + if degree >= 30: + node_colors.append((1.0, 0, 0)) # 亮红色 (#FF0000) + else: + # 将1-10映射到0-1的范围 + color_ratio = (degree - 1) / 29.0 if degree > 1 else 0 + # 使用蓝到红的渐变 + red = min(0.9, color_ratio) + blue = max(0.0, 1.0 - color_ratio) + node_colors.append((red, 0, blue)) + + # 绘制图形 + plt.figure(figsize=(16, 12)) # 减小图形尺寸 + pos = nx.spring_layout(H, + k=1, # 调整节点间斥力 + iterations=100, # 增加迭代次数 + scale=1.5, # 减小布局尺寸 + weight='strength') # 使用边的strength属性作为权重 + + nx.draw(H, pos, + with_labels=True, + node_color=node_colors, + node_size=node_sizes, + font_size=12, # 保持增大的字体大小 + font_family='SimHei', + font_weight='bold', + edge_color='gray', + width=1.5) # 统一的边宽度 + + title = '记忆图谱可视化(仅显示内容≥2的节点)\n节点大小表示记忆数量\n节点颜色:蓝(弱连接)到红(强连接)渐变,边的透明度表示连接强度\n连接强度越大的节点距离越近' + plt.title(title, fontsize=16, fontfamily='SimHei') + plt.show() + +async def main(): + # 初始化数据库 + logger.info("正在初始化数据库连接...") + db = Database.get_instance() + start_time = time.time() + + test_pare = {'do_build_memory':True,'do_forget_topic':False,'do_visualize_graph':True,'do_query':False,'do_merge_memory':False} + + # 创建记忆图 + memory_graph = Memory_graph() + + # 创建海马体 + hippocampus = Hippocampus(memory_graph) + + # 从数据库同步数据 + hippocampus.memory_cortex.sync_memory_from_db() + + end_time = time.time() + logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") + + # 构建记忆 + if test_pare['do_build_memory']: + logger.info("开始构建记忆...") + chat_size = 20 + await hippocampus.operation_build_memory(chat_size=chat_size) + + end_time = time.time() + logger.info(f"\033[32m[构建记忆耗时: {end_time - start_time:.2f} 秒,chat_size={chat_size},chat_count = 16]\033[0m") + + if test_pare['do_forget_topic']: + logger.info("开始遗忘记忆...") + await hippocampus.operation_forget_topic(percentage=0.01) + + end_time = time.time() + logger.info(f"\033[32m[遗忘记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") + + if test_pare['do_merge_memory']: + logger.info("开始合并记忆...") + await hippocampus.operation_merge_memory(percentage=0.1) + + end_time = time.time() + logger.info(f"\033[32m[合并记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") + + if test_pare['do_visualize_graph']: + # 展示优化后的图形 + logger.info("生成记忆图谱可视化...") + print("\n生成优化后的记忆图谱:") + visualize_graph_lite(memory_graph) + + if test_pare['do_query']: + # 交互式查询 + while True: + query = input("\n请输入新的查询概念(输入'退出'以结束):") + if query.lower() == '退出': + break + + items_list = memory_graph.get_related_item(query) + if items_list: + first_layer, second_layer = items_list + if first_layer: + print("\n直接相关的记忆:") + for item in first_layer: + print(f"- {item}") + if second_layer: + print("\n间接相关的记忆:") + for item in second_layer: + print(f"- {item}") + else: + print("未找到相关记忆。") + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) + + From 7899e67cb24d87638ea24e9573ff774c9f15fa86 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 01:15:32 +0800 Subject: [PATCH 054/105] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=BC=80=E5=A7=8B=E6=B5=8B=E8=AF=95debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 107 +++------- src/plugins/chat/chat_stream.py | 147 ++++++------- src/plugins/chat/cq_code.py | 251 +++++++++++------------ src/plugins/chat/emoji_manager.py | 8 +- src/plugins/chat/llm_generator.py | 153 +++++++++----- src/plugins/chat/message.py | 77 ++++--- src/plugins/chat/message_base.py | 33 ++- src/plugins/chat/message_cq.py | 23 +-- src/plugins/chat/message_sender.py | 9 +- src/plugins/chat/prompt_builder.py | 33 ++- src/plugins/chat/relationship_manager.py | 159 ++------------ src/plugins/chat/storage.py | 3 +- src/plugins/chat/utils.py | 55 ++--- 13 files changed, 486 insertions(+), 572 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 17aa0630d..a2fdab873 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -1,6 +1,5 @@ import time from random import random - from loguru import logger from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent @@ -11,25 +10,18 @@ from .cq_code import CQCode,cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator from .message import MessageSending, MessageRecv, MessageThinking, MessageSet -from .message import MessageSending, MessageRecv, MessageThinking, MessageSet from .message_cq import ( MessageRecvCQ, - MessageSendCQ, -) -from .chat_stream import chat_manager - MessageRecvCQ, - MessageSendCQ, ) from .chat_stream import chat_manager + from .message_sender import message_manager # 导入新的消息管理器 from .relationship_manager import relationship_manager from .storage import MessageStorage from .utils import calculate_typing_time, is_mentioned_bot_in_txt from .utils_image import image_path_to_base64 -from .utils_image import image_path_to_base64 from .willing_manager import willing_manager # 导入意愿管理器 from .message_base import UserInfo, GroupInfo, Seg -from .message_base import UserInfo, GroupInfo, Seg class ChatBot: def __init__(self): @@ -53,24 +45,21 @@ class ChatBot: self.bot = bot # 更新 bot 实例 - - - group_info = await bot.get_group_info(group_id=event.group_id) sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) - - await relationship_manager.update_relationship(user_id = event.user_id, data = sender_info) - await relationship_manager.update_relationship_value(user_id = event.user_id, relationship_value = 0.5) - - message_cq=MessageRecvCQ( + + # 白名单设定由nontbot侧完成 + if event.group_id: + if event.group_id not in global_config.talk_allowed_groups: + return + if event.user_id in global_config.ban_user_id: + return + message_cq=MessageRecvCQ( message_id=event.message_id, user_id=event.user_id, raw_message=str(event.original_message), group_id=event.group_id, - user_id=event.user_id, - raw_message=str(event.original_message), - group_id=event.group_id, reply_message=event.reply, platform='qq' ) @@ -78,37 +67,26 @@ class ChatBot: # 进入maimbot message=MessageRecv(**message_json) - await message.process() + groupinfo=message.message_info.group_info userinfo=message.message_info.user_info messageinfo=message.message_info - chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) # 消息过滤,涉及到config有待更新 - if groupinfo: - if groupinfo.group_id not in global_config.talk_allowed_groups: - return - else: - if userinfo: - if userinfo.user_id in []: - pass - else: - return - else: - return - if userinfo.user_id in global_config.ban_user_id: - return + + chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) + await relationship_manager.update_relationship(chat_stream=chat,) + await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value = 0.5) + + await message.process() # 过滤词 for word in global_config.ban_words: - if word in message.processed_plain_text: - logger.info(f"\033[1;32m[{groupinfo.group_name}]{userinfo.user_nickname}:\033[0m {message.processed_plain_text}") if word in message.processed_plain_text: logger.info(f"\033[1;32m[{groupinfo.group_name}]{userinfo.user_nickname}:\033[0m {message.processed_plain_text}") logger.info(f"\033[1;32m[过滤词识别]\033[0m 消息中含有{word},filtered") return current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) - current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) @@ -130,20 +108,13 @@ class ChatBot: is_emoji=message.is_emoji, interested_rate=interested_rate ) - current_willing = willing_manager.get_willing( - chat_stream=chat - ) + current_willing = willing_manager.get_willing(chat_stream=chat) print(f"\033[1;32m[{current_time}][{chat.group_info.group_name}]{chat.user_info.user_nickname}:\033[0m {message.processed_plain_text}\033[1;36m[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]\033[0m") response = None if random() < reply_probability: - bot_user_info=UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=messageinfo.platform - ) bot_user_info=UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, @@ -151,22 +122,16 @@ class ChatBot: ) tinking_time_point = round(time.time(), 2) think_id = 'mt' + str(tinking_time_point) - thinking_message = MessageThinking.from_chat_stream( - chat_stream=chat, + thinking_message = MessageThinking( message_id=think_id, - reply=message - ) - thinking_message = MessageThinking.from_chat_stream( chat_stream=chat, - message_id=think_id, + bot_user_info=bot_user_info, reply=message ) message_manager.add_message(thinking_message) - willing_manager.change_reply_willing_sent( - chat_stream=chat - ) + willing_manager.change_reply_willing_sent(chat) response,raw_content = await self.gpt.generate_response(message) @@ -201,18 +166,11 @@ class ChatBot: accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - message_segment = Seg(type='text', data=msg) - bot_message = MessageSending( message_segment = Seg(type='text', data=msg) bot_message = MessageSending( message_id=think_id, chat_stream=chat, - message_segment=message_segment, - reply=message, - is_head=not mark_head, - is_emoji=False - ) - chat_stream=chat, + bot_user_info=bot_user_info, message_segment=message_segment, reply=message, is_head=not mark_head, @@ -235,7 +193,6 @@ class ChatBot: if emoji_raw != None: emoji_path,discription = emoji_raw - emoji_cq = image_path_to_base64(emoji_path) emoji_cq = image_path_to_base64(emoji_path) if random() < 0.5: @@ -247,15 +204,7 @@ class ChatBot: bot_message = MessageSending( message_id=think_id, chat_stream=chat, - message_segment=message_segment, - reply=message, - is_head=False, - is_emoji=True - ) - message_segment = Seg(type='emoji', data=emoji_cq) - bot_message = MessageSending( - message_id=think_id, - chat_stream=chat, + bot_user_info=bot_user_info, message_segment=message_segment, reply=message, is_head=False, @@ -273,20 +222,12 @@ class ChatBot: 'fearful': -0.7, 'neutral': 0.1 } - await relationship_manager.update_relationship_value(message.user_id, relationship_value=valuedict[emotion[0]]) + await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value=valuedict[emotion[0]]) # 使用情绪管理器更新情绪 self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) willing_manager.change_reply_willing_after_sent( - platform=messageinfo.platform, - user_info=userinfo, - group_info=groupinfo - ) - - willing_manager.change_reply_willing_after_sent( - platform=messageinfo.platform, - user_info=userinfo, - group_info=groupinfo + chat_stream=chat ) # 创建全局ChatBot实例 diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index e617054ec..36c97bed0 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -1,55 +1,67 @@ -import time import asyncio -from typing import Optional, Dict, Tuple import hashlib +import time +from typing import Dict, Optional from loguru import logger + from ...common.database import Database -from .message_base import UserInfo, GroupInfo +from .message_base import GroupInfo, UserInfo class ChatStream: """聊天流对象,存储一个完整的聊天上下文""" - def __init__(self, - stream_id: str, - platform: str, - user_info: UserInfo, - group_info: Optional[GroupInfo] = None, - data: dict = None): + + def __init__( + self, + stream_id: str, + platform: str, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None, + data: dict = None, + ): self.stream_id = stream_id self.platform = platform self.user_info = user_info self.group_info = group_info - self.create_time = data.get('create_time', int(time.time())) if data else int(time.time()) - self.last_active_time = data.get('last_active_time', self.create_time) if data else self.create_time + self.create_time = ( + data.get("create_time", int(time.time())) if data else int(time.time()) + ) + self.last_active_time = ( + data.get("last_active_time", self.create_time) if data else self.create_time + ) self.saved = False - + def to_dict(self) -> dict: """转换为字典格式""" result = { - 'stream_id': self.stream_id, - 'platform': self.platform, - 'user_info': self.user_info.to_dict() if self.user_info else None, - 'group_info': self.group_info.to_dict() if self.group_info else None, - 'create_time': self.create_time, - 'last_active_time': self.last_active_time + "stream_id": self.stream_id, + "platform": self.platform, + "user_info": self.user_info.to_dict() if self.user_info else None, + "group_info": self.group_info.to_dict() if self.group_info else None, + "create_time": self.create_time, + "last_active_time": self.last_active_time, } return result - + @classmethod - def from_dict(cls, data: dict) -> 'ChatStream': + def from_dict(cls, data: dict) -> "ChatStream": """从字典创建实例""" - user_info = UserInfo(**data.get('user_info', {})) if data.get('user_info') else None - group_info = GroupInfo(**data.get('group_info', {})) if data.get('group_info') else None - + user_info = ( + UserInfo(**data.get("user_info", {})) if data.get("user_info") else None + ) + group_info = ( + GroupInfo(**data.get("group_info", {})) if data.get("group_info") else None + ) + return cls( - stream_id=data['stream_id'], - platform=data['platform'], + stream_id=data["stream_id"], + platform=data["platform"], user_info=user_info, group_info=group_info, - data=data + data=data, ) - + def update_active_time(self): """更新最后活跃时间""" self.last_active_time = int(time.time()) @@ -58,14 +70,15 @@ class ChatStream: class ChatManager: """聊天管理器,管理所有聊天流""" + _instance = None _initialized = False - + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance - + def __init__(self): if not self._initialized: self.streams: Dict[str, ChatStream] = {} # stream_id -> ChatStream @@ -76,7 +89,7 @@ class ChatManager: asyncio.create_task(self._initialize()) # 启动自动保存任务 asyncio.create_task(self._auto_save_task()) - + async def _initialize(self): """异步初始化""" try: @@ -84,7 +97,7 @@ class ChatManager: logger.success(f"聊天管理器已启动,已加载 {len(self.streams)} 个聊天流") except Exception as e: logger.error(f"聊天管理器启动失败: {str(e)}") - + async def _auto_save_task(self): """定期自动保存所有聊天流""" while True: @@ -94,49 +107,48 @@ class ChatManager: logger.info("聊天流自动保存完成") except Exception as e: logger.error(f"聊天流自动保存失败: {str(e)}") - + def _ensure_collection(self): """确保数据库集合存在并创建索引""" - if 'chat_streams' not in self.db.db.list_collection_names(): - self.db.db.create_collection('chat_streams') + if "chat_streams" not in self.db.db.list_collection_names(): + self.db.db.create_collection("chat_streams") # 创建索引 - self.db.db.chat_streams.create_index([('stream_id', 1)], unique=True) - self.db.db.chat_streams.create_index([ - ('platform', 1), - ('user_info.user_id', 1), - ('group_info.group_id', 1) - ]) - - def _generate_stream_id(self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None) -> str: + self.db.db.chat_streams.create_index([("stream_id", 1)], unique=True) + self.db.db.chat_streams.create_index( + [("platform", 1), ("user_info.user_id", 1), ("group_info.group_id", 1)] + ) + + def _generate_stream_id( + self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None + ) -> str: """生成聊天流唯一ID""" # 组合关键信息 components = [ platform, str(user_info.user_id), - str(group_info.group_id) if group_info else 'private' + str(group_info.group_id) if group_info else "private", ] - + # 使用MD5生成唯一ID - key = '_'.join(components) + key = "_".join(components) return hashlib.md5(key.encode()).hexdigest() - - async def get_or_create_stream(self, - platform: str, - user_info: UserInfo, - group_info: Optional[GroupInfo] = None) -> ChatStream: + + async def get_or_create_stream( + self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None + ) -> ChatStream: """获取或创建聊天流 - + Args: platform: 平台标识 user_info: 用户信息 group_info: 群组信息(可选) - + Returns: ChatStream: 聊天流对象 """ # 生成stream_id stream_id = self._generate_stream_id(platform, user_info, group_info) - + # 检查内存中是否存在 if stream_id in self.streams: stream = self.streams[stream_id] @@ -146,9 +158,9 @@ class ChatManager: stream.group_info = group_info stream.update_active_time() return stream - + # 检查数据库中是否存在 - data = self.db.db.chat_streams.find_one({'stream_id': stream_id}) + data = self.db.db.chat_streams.find_one({"stream_id": stream_id}) if data: stream = ChatStream.from_dict(data) # 更新用户信息和群组信息 @@ -162,41 +174,38 @@ class ChatManager: stream_id=stream_id, platform=platform, user_info=user_info, - group_info=group_info + group_info=group_info, ) - + # 保存到内存和数据库 self.streams[stream_id] = stream await self._save_stream(stream) return stream - + def get_stream(self, stream_id: str) -> Optional[ChatStream]: """通过stream_id获取聊天流""" return self.streams.get(stream_id) - - def get_stream_by_info(self, - platform: str, - user_info: UserInfo, - group_info: Optional[GroupInfo] = None) -> Optional[ChatStream]: + + def get_stream_by_info( + self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None + ) -> Optional[ChatStream]: """通过信息获取聊天流""" stream_id = self._generate_stream_id(platform, user_info, group_info) return self.streams.get(stream_id) - + async def _save_stream(self, stream: ChatStream): """保存聊天流到数据库""" if not stream.saved: self.db.db.chat_streams.update_one( - {'stream_id': stream.stream_id}, - {'$set': stream.to_dict()}, - upsert=True + {"stream_id": stream.stream_id}, {"$set": stream.to_dict()}, upsert=True ) stream.saved = True - + async def _save_all_streams(self): """保存所有聊天流""" for stream in self.streams.values(): await self._save_stream(stream) - + async def load_all_streams(self): """从数据库加载所有聊天流""" all_streams = self.db.db.chat_streams.find({}) diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index b29e25b4c..7581f8a33 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -3,23 +3,22 @@ import html import os import time from dataclasses import dataclass -from typing import Dict, Optional, List, Union +from typing import Dict, List, Optional, Union import requests # 解析各种CQ码 # 包含CQ码类 import urllib3 +from loguru import logger from nonebot import get_driver from urllib3.util import create_urllib3_context -from loguru import logger from ..models.utils_model import LLM_request from .config import global_config from .mapper import emojimapper -from .utils_image import image_manager -from .utils_user import get_user_nickname from .message_base import Seg +from .utils_user import get_user_nickname driver = get_driver() config = driver.config @@ -37,21 +36,25 @@ class TencentSSLAdapter(requests.adapters.HTTPAdapter): def init_poolmanager(self, connections, maxsize, block=False): self.poolmanager = urllib3.poolmanager.PoolManager( - num_pools=connections, maxsize=maxsize, - block=block, ssl_context=self.ssl_context) + num_pools=connections, + maxsize=maxsize, + block=block, + ssl_context=self.ssl_context, + ) @dataclass class CQCode: """ CQ码数据类,用于存储和处理CQ码 - + 属性: type: CQ码类型(如'image', 'at', 'face'等) params: CQ码的参数字典 raw_code: 原始CQ码字符串 translated_segments: 经过处理后的Seg对象列表 """ + type: str params: Dict[str, str] group_id: int @@ -65,77 +68,52 @@ class CQCode: def __post_init__(self): """初始化LLM实例""" - self._llm = LLM_request(model=global_config.vlm, temperature=0.4, max_tokens=300) + self._llm = LLM_request( + model=global_config.vlm, temperature=0.4, max_tokens=300 + ) def translate(self): """根据CQ码类型进行相应的翻译处理,转换为Seg对象""" - if self.type == 'text': + if self.type == "text": self.translated_segments = Seg( - type='text', - data=self.params.get('text', '') + type="text", data=self.params.get("text", "") ) - elif self.type == 'image': + elif self.type == "image": base64_data = self.translate_image() if base64_data: - if self.params.get('sub_type') == '0': - self.translated_segments = Seg( - type='image', - data=base64_data - ) + if self.params.get("sub_type") == "0": + self.translated_segments = Seg(type="image", data=base64_data) else: - self.translated_segments = Seg( - type='emoji', - data=base64_data - ) + self.translated_segments = Seg(type="emoji", data=base64_data) else: - self.translated_segments = Seg( - type='text', - data='[图片]' - ) - elif self.type == 'at': - user_nickname = get_user_nickname(self.params.get('qq', '')) + self.translated_segments = Seg(type="text", data="[图片]") + elif self.type == "at": + user_nickname = get_user_nickname(self.params.get("qq", "")) self.translated_segments = Seg( - type='text', - data=f"[@{user_nickname or '某人'}]" + type="text", data=f"[@{user_nickname or '某人'}]" ) - elif self.type == 'reply': + elif self.type == "reply": reply_segments = self.translate_reply() if reply_segments: - self.translated_segments = Seg( - type='seglist', - data=reply_segments - ) + self.translated_segments = Seg(type="seglist", data=reply_segments) else: - self.translated_segments = Seg( - type='text', - data='[回复某人消息]' - ) - elif self.type == 'face': - face_id = self.params.get('id', '') + self.translated_segments = Seg(type="text", data="[回复某人消息]") + elif self.type == "face": + face_id = self.params.get("id", "") self.translated_segments = Seg( - type='text', - data=f"[{emojimapper.get(int(face_id), '表情')}]" + type="text", data=f"[{emojimapper.get(int(face_id), '表情')}]" ) - elif self.type == 'forward': + elif self.type == "forward": forward_segments = self.translate_forward() if forward_segments: - self.translated_segments = Seg( - type='seglist', - data=forward_segments - ) + self.translated_segments = Seg(type="seglist", data=forward_segments) else: - self.translated_segments = Seg( - type='text', - data='[转发消息]' - ) + self.translated_segments = Seg(type="text", data="[转发消息]") else: - self.translated_segments = Seg( - type='text', - data=f"[{self.type}]" - ) + self.translated_segments = Seg(type="text", data=f"[{self.type}]") def get_img(self): - ''' + """ headers = { 'User-Agent': 'QQ/8.9.68.11565 CFNetwork/1220.1 Darwin/20.3.0', 'Accept': 'image/*;q=0.8', @@ -144,18 +122,18 @@ class CQCode: 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } - ''' + """ # 腾讯专用请求头配置 headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36', - 'Accept': 'text/html, application/xhtml xml, */*', - 'Accept-Encoding': 'gbk, GB2312', - 'Accept-Language': 'zh-cn', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Cache-Control': 'no-cache' + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36", + "Accept": "text/html, application/xhtml xml, */*", + "Accept-Encoding": "gbk, GB2312", + "Accept-Language": "zh-cn", + "Content-Type": "application/x-www-form-urlencoded", + "Cache-Control": "no-cache", } - url = html.unescape(self.params['url']) - if not url.startswith(('http://', 'https://')): + url = html.unescape(self.params["url"]) + if not url.startswith(("http://", "https://")): return None # 创建专用会话 @@ -171,30 +149,30 @@ class CQCode: headers=headers, timeout=15, allow_redirects=True, - stream=True # 流式传输避免大内存问题 + stream=True, # 流式传输避免大内存问题 ) # 腾讯服务器特殊状态码处理 - if response.status_code == 400 and 'multimedia.nt.qq.com.cn' in url: + if response.status_code == 400 and "multimedia.nt.qq.com.cn" in url: return None if response.status_code != 200: raise requests.exceptions.HTTPError(f"HTTP {response.status_code}") # 验证内容类型 - content_type = response.headers.get('Content-Type', '') - if not content_type.startswith('image/'): + content_type = response.headers.get("Content-Type", "") + if not content_type.startswith("image/"): raise ValueError(f"非图片内容类型: {content_type}") # 转换为Base64 - image_base64 = base64.b64encode(response.content).decode('utf-8') + image_base64 = base64.b64encode(response.content).decode("utf-8") self.image_base64 = image_base64 return image_base64 except (requests.exceptions.SSLError, requests.exceptions.HTTPError) as e: if retry == max_retries - 1: print(f"\033[1;31m[致命错误]\033[0m 最终请求失败: {str(e)}") - time.sleep(1.5 ** retry) # 指数退避 + time.sleep(1.5**retry) # 指数退避 except Exception as e: print(f"\033[1;33m[未知错误]\033[0m {str(e)}") @@ -202,21 +180,21 @@ class CQCode: return None - def translate_image(self) -> Optional[str]: """处理图片类型的CQ码,返回base64字符串""" - if 'url' not in self.params: + if "url" not in self.params: return None return self.get_img() def translate_forward(self) -> Optional[List[Seg]]: """处理转发消息,返回Seg列表""" try: - if 'content' not in self.params: + if "content" not in self.params: return None - content = self.unescape(self.params['content']) + content = self.unescape(self.params["content"]) import ast + try: messages = ast.literal_eval(content) except ValueError as e: @@ -225,46 +203,52 @@ class CQCode: formatted_segments = [] for msg in messages: - sender = msg.get('sender', {}) - nickname = sender.get('card') or sender.get('nickname', '未知用户') - raw_message = msg.get('raw_message', '') - message_array = msg.get('message', []) + sender = msg.get("sender", {}) + nickname = sender.get("card") or sender.get("nickname", "未知用户") + raw_message = msg.get("raw_message", "") + message_array = msg.get("message", []) if message_array and isinstance(message_array, list): for message_part in message_array: - if message_part.get('type') == 'forward': - content_seg = Seg(type='text', data='[转发消息]') + if message_part.get("type") == "forward": + content_seg = Seg(type="text", data="[转发消息]") break else: if raw_message: from .message_cq import MessageRecvCQ + message_obj = MessageRecvCQ( - user_id=msg.get('user_id', 0), - message_id=msg.get('message_id', 0), + user_id=msg.get("user_id", 0), + message_id=msg.get("message_id", 0), raw_message=raw_message, plain_text=raw_message, - group_id=msg.get('group_id', 0) + group_id=msg.get("group_id", 0), + ) + content_seg = Seg( + type="seglist", data=message_obj.message_segments ) - content_seg = Seg(type='seglist', data=message_obj.message_segments) else: - content_seg = Seg(type='text', data='[空消息]') + content_seg = Seg(type="text", data="[空消息]") else: if raw_message: from .message_cq import MessageRecvCQ + message_obj = MessageRecvCQ( - user_id=msg.get('user_id', 0), - message_id=msg.get('message_id', 0), + user_id=msg.get("user_id", 0), + message_id=msg.get("message_id", 0), raw_message=raw_message, plain_text=raw_message, - group_id=msg.get('group_id', 0) + group_id=msg.get("group_id", 0), + ) + content_seg = Seg( + type="seglist", data=message_obj.message_segments ) - content_seg = Seg(type='seglist', data=message_obj.message_segments) else: - content_seg = Seg(type='text', data='[空消息]') + content_seg = Seg(type="text", data="[空消息]") - formatted_segments.append(Seg(type='text', data=f"{nickname}: ")) + formatted_segments.append(Seg(type="text", data=f"{nickname}: ")) formatted_segments.append(content_seg) - formatted_segments.append(Seg(type='text', data='\n')) + formatted_segments.append(Seg(type="text", data="\n")) return formatted_segments @@ -275,6 +259,7 @@ class CQCode: def translate_reply(self) -> Optional[List[Seg]]: """处理回复类型的CQ码,返回Seg列表""" from .message_cq import MessageRecvCQ + if self.reply_message is None: return None @@ -283,17 +268,26 @@ class CQCode: user_id=self.reply_message.sender.user_id, message_id=self.reply_message.message_id, raw_message=str(self.reply_message.message), - group_id=self.group_id + group_id=self.group_id, ) - + segments = [] if message_obj.user_id == global_config.BOT_QQ: - segments.append(Seg(type='text', data=f"[回复 {global_config.BOT_NICKNAME} 的消息: ")) + segments.append( + Seg( + type="text", data=f"[回复 {global_config.BOT_NICKNAME} 的消息: " + ) + ) else: - segments.append(Seg(type='text', data=f"[回复 {self.reply_message.sender.nickname} 的消息: ")) - - segments.append(Seg(type='seglist', data=message_obj.message_segments)) - segments.append(Seg(type='text', data="]")) + segments.append( + Seg( + type="text", + data=f"[回复 {self.reply_message.sender.nickname} 的消息: ", + ) + ) + + segments.append(Seg(type="seglist", data=message_obj.message_segments)) + segments.append(Seg(type="text", data="]")) return segments else: return None @@ -301,12 +295,12 @@ class CQCode: @staticmethod def unescape(text: str) -> str: """反转义CQ码中的特殊字符""" - return text.replace(',', ',') \ - .replace('[', '[') \ - .replace(']', ']') \ - .replace('&', '&') - - + return ( + text.replace(",", ",") + .replace("[", "[") + .replace("]", "]") + .replace("&", "&") + ) class CQCode_tool: @@ -314,29 +308,25 @@ class CQCode_tool: def cq_from_dict_to_class(cq_code: Dict, reply: Optional[Dict] = None) -> CQCode: """ 将CQ码字典转换为CQCode对象 - + Args: cq_code: CQ码字典 reply: 回复消息的字典(可选) - + Returns: CQCode对象 """ # 处理字典形式的CQ码 # 从cq_code字典中获取type字段的值,如果不存在则默认为'text' - cq_type = cq_code.get('type', 'text') + cq_type = cq_code.get("type", "text") params = {} - if cq_type == 'text': - params['text'] = cq_code.get('data', {}).get('text', '') + if cq_type == "text": + params["text"] = cq_code.get("data", {}).get("text", "") else: - params = cq_code.get('data', {}) + params = cq_code.get("data", {}) instance = CQCode( - type=cq_type, - params=params, - group_id=0, - user_id=0, - reply_message=reply + type=cq_type, params=params, group_id=0, user_id=0, reply_message=reply ) # 进行翻译处理 @@ -353,7 +343,7 @@ class CQCode_tool: 回复CQ码字符串 """ return f"[CQ:reply,id={message_id}]" - + @staticmethod def create_emoji_cq(file_path: str) -> str: """ @@ -366,13 +356,15 @@ class CQCode_tool: # 确保使用绝对路径 abs_path = os.path.abspath(file_path) # 转义特殊字符 - escaped_path = abs_path.replace('&', '&') \ - .replace('[', '[') \ - .replace(']', ']') \ - .replace(',', ',') + escaped_path = ( + abs_path.replace("&", "&") + .replace("[", "[") + .replace("]", "]") + .replace(",", ",") + ) # 生成CQ码,设置sub_type=1表示这是表情包 return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" - + @staticmethod def create_emoji_cq_base64(base64_data: str) -> str: """ @@ -383,15 +375,14 @@ class CQCode_tool: 表情包CQ码字符串 """ # 转义base64数据 - escaped_base64 = base64_data.replace('&', '&') \ - .replace('[', '[') \ - .replace(']', ']') \ - .replace(',', ',') + escaped_base64 = ( + base64_data.replace("&", "&") + .replace("[", "[") + .replace("]", "]") + .replace(",", ",") + ) # 生成CQ码,设置sub_type=1表示这是表情包 return f"[CQ:image,file=base64://{escaped_base64},sub_type=1]" - - - cq_code_tool = CQCode_tool() diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 3432f011c..837ee245d 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -1,11 +1,11 @@ import asyncio +import base64 +import hashlib import os import random import time import traceback from typing import Optional, Tuple -import base64 -import hashlib from loguru import logger from nonebot import get_driver @@ -13,9 +13,8 @@ from nonebot import get_driver from ...common.database import Database from ..chat.config import global_config from ..chat.utils import get_embedding -from ..chat.utils_image import image_path_to_base64 +from ..chat.utils_image import ImageManager, image_path_to_base64 from ..models.utils_model import LLM_request -from ..chat.utils_image import ImageManager driver = get_driver() config = driver.config @@ -78,7 +77,6 @@ class EmojiManager: if 'emoji' not in self.db.db.list_collection_names(): self.db.db.create_collection('emoji') self.db.db.emoji.create_index([('embedding', '2dsphere')]) - self.db.db.emoji.create_index([('tags', 1)]) self.db.db.emoji.create_index([('filename', 1)], unique=True) def record_usage(self, emoji_id: str): diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 2803e5a14..dc019038e 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -7,7 +7,7 @@ from nonebot import get_driver from ...common.database import Database from ..models.utils_model import LLM_request from .config import global_config -from .message_cq import Message +from .message import MessageRecv, MessageThinking, MessageSending from .prompt_builder import prompt_builder from .relationship_manager import relationship_manager from .utils import process_llm_response @@ -18,58 +18,88 @@ config = driver.config class ResponseGenerator: def __init__(self): - self.model_r1 = LLM_request(model=global_config.llm_reasoning, temperature=0.7,max_tokens=1000,stream=True) - self.model_v3 = LLM_request(model=global_config.llm_normal, temperature=0.7,max_tokens=1000) - self.model_r1_distill = LLM_request(model=global_config.llm_reasoning_minor, temperature=0.7,max_tokens=1000) - self.model_v25 = LLM_request(model=global_config.llm_normal_minor, temperature=0.7,max_tokens=1000) + self.model_r1 = LLM_request( + model=global_config.llm_reasoning, + temperature=0.7, + max_tokens=1000, + stream=True, + ) + self.model_v3 = LLM_request( + model=global_config.llm_normal, temperature=0.7, max_tokens=1000 + ) + self.model_r1_distill = LLM_request( + model=global_config.llm_reasoning_minor, temperature=0.7, max_tokens=1000 + ) + self.model_v25 = LLM_request( + model=global_config.llm_normal_minor, temperature=0.7, max_tokens=1000 + ) self.db = Database.get_instance() - self.current_model_type = 'r1' # 默认使用 R1 + self.current_model_type = "r1" # 默认使用 R1 - async def generate_response(self, message: Message) -> Optional[Union[str, List[str]]]: + async def generate_response( + self, message: MessageThinking + ) -> Optional[Union[str, List[str]]]: """根据当前模型类型选择对应的生成函数""" # 从global_config中获取模型概率值并选择模型 rand = random.random() if rand < global_config.MODEL_R1_PROBABILITY: - self.current_model_type = 'r1' + self.current_model_type = "r1" current_model = self.model_r1 - elif rand < global_config.MODEL_R1_PROBABILITY + global_config.MODEL_V3_PROBABILITY: - self.current_model_type = 'v3' + elif ( + rand + < global_config.MODEL_R1_PROBABILITY + global_config.MODEL_V3_PROBABILITY + ): + self.current_model_type = "v3" current_model = self.model_v3 else: - self.current_model_type = 'r1_distill' + self.current_model_type = "r1_distill" current_model = self.model_r1_distill - print(f"+++++++++++++++++{global_config.BOT_NICKNAME}{self.current_model_type}思考中+++++++++++++++++") - - model_response = await self._generate_response_with_model(message, current_model) - raw_content=model_response - + print( + f"+++++++++++++++++{global_config.BOT_NICKNAME}{self.current_model_type}思考中+++++++++++++++++" + ) + + model_response = await self._generate_response_with_model( + message, current_model + ) + raw_content = model_response + if model_response: - print(f'{global_config.BOT_NICKNAME}的回复是:{model_response}') + print(f"{global_config.BOT_NICKNAME}的回复是:{model_response}") model_response = await self._process_response(model_response) if model_response: + return model_response, raw_content + return None, raw_content - return model_response ,raw_content - return None,raw_content - - async def _generate_response_with_model(self, message: Message, model: LLM_request) -> Optional[str]: + async def _generate_response_with_model( + self, message: MessageThinking, model: LLM_request + ) -> Optional[str]: """使用指定的模型生成回复""" - sender_name = message.user_nickname or f"用户{message.user_id}" - if message.user_cardname: - sender_name=f"[({message.user_id}){message.user_nickname}]{message.user_cardname}" - + sender_name = ( + message.chat_stream.user_info.user_nickname + or f"用户{message.chat_stream.user_info.user_id}" + ) + if message.chat_stream.user_info.user_cardname: + sender_name = f"[({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}]{message.chat_stream.user_info.user_cardname}" + # 获取关系值 - relationship_value = relationship_manager.get_relationship(message.user_id).relationship_value if relationship_manager.get_relationship(message.user_id) else 0.0 + relationship_value = ( + relationship_manager.get_relationship( + message.chat_stream + ).relationship_value + if relationship_manager.get_relationship(message.chat_stream) + else 0.0 + ) if relationship_value != 0.0: # print(f"\033[1;32m[关系管理]\033[0m 回复中_当前关系值: {relationship_value}") pass - + # 构建prompt prompt, prompt_check = await prompt_builder._build_prompt( message_txt=message.processed_plain_text, sender_name=sender_name, relationship_value=relationship_value, - group_id=message.group_id + stream_id=message.chat_stream.stream_id, ) # 读空气模块 简化逻辑,先停用 @@ -95,7 +125,7 @@ class ResponseGenerator: except Exception as e: print(f"生成回复时出错: {e}") return None - + # 保存到数据库 self._save_to_db( message=message, @@ -107,54 +137,71 @@ class ResponseGenerator: reasoning_content=reasoning_content, # reasoning_content_check=reasoning_content_check if global_config.enable_kuuki_read else "" ) - + return content # def _save_to_db(self, message: Message, sender_name: str, prompt: str, prompt_check: str, # content: str, content_check: str, reasoning_content: str, reasoning_content_check: str): - def _save_to_db(self, message: Message, sender_name: str, prompt: str, prompt_check: str, - content: str, reasoning_content: str,): + def _save_to_db( + self, + message: Message, + sender_name: str, + prompt: str, + prompt_check: str, + content: str, + reasoning_content: str, + ): """保存对话记录到数据库""" - self.db.db.reasoning_logs.insert_one({ - 'time': time.time(), - 'group_id': message.group_id, - 'user': sender_name, - 'message': message.processed_plain_text, - 'model': self.current_model_type, - # 'reasoning_check': reasoning_content_check, - # 'response_check': content_check, - 'reasoning': reasoning_content, - 'response': content, - 'prompt': prompt, - 'prompt_check': prompt_check - }) + self.db.db.reasoning_logs.insert_one( + { + "time": time.time(), + "group_id": message.group_id, + "user": sender_name, + "message": message.processed_plain_text, + "model": self.current_model_type, + # 'reasoning_check': reasoning_content_check, + # 'response_check': content_check, + "reasoning": reasoning_content, + "response": content, + "prompt": prompt, + "prompt_check": prompt_check, + } + ) async def _get_emotion_tags(self, content: str) -> List[str]: """提取情感标签""" try: - prompt = f'''请从以下内容中,从"happy,angry,sad,surprised,disgusted,fearful,neutral"中选出最匹配的1个情感标签并输出 + prompt = f"""请从以下内容中,从"happy,angry,sad,surprised,disgusted,fearful,neutral"中选出最匹配的1个情感标签并输出 只输出标签就好,不要输出其他内容: 内容:{content} 输出: - ''' + """ content, _ = await self.model_v25.generate_response(prompt) - content=content.strip() - if content in ['happy','angry','sad','surprised','disgusted','fearful','neutral']: + content = content.strip() + if content in [ + "happy", + "angry", + "sad", + "surprised", + "disgusted", + "fearful", + "neutral", + ]: return [content] else: return ["neutral"] - + except Exception as e: print(f"获取情感标签时出错: {e}") return ["neutral"] - + async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: """处理响应内容,返回处理后的内容和情感标签""" if not content: return None, [] - + processed_response = process_llm_response(content) - + return processed_response diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 070241ac1..408937fad 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -5,7 +5,6 @@ from typing import Dict, ForwardRef, List, Optional, Union import urllib3 from loguru import logger -from .utils_user import get_groupname, get_user_cardname, get_user_nickname from .utils_image import image_manager from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase from .chat_stream import ChatStream @@ -108,25 +107,32 @@ class MessageRecv(MessageBase): else f"{user_info.user_nickname}(ta的id:{user_info.user_id})" ) return f"[{time_str}] {name}: {self.processed_plain_text}\n" - -@dataclass -class MessageProcessBase(MessageBase): - """消息处理基类,用于处理中和发送中的消息""" +@dataclass +class Message(MessageBase): + chat_stream: ChatStream=None + reply: Optional['Message'] = None + detailed_plain_text: str = "" + processed_plain_text: str = "" + def __init__( self, message_id: str, + time: int, chat_stream: ChatStream, + user_info: UserInfo, message_segment: Optional[Seg] = None, - reply: Optional['MessageRecv'] = None + reply: Optional['MessageRecv'] = None, + detailed_plain_text: str = "", + processed_plain_text: str = "", ): # 构造基础消息信息 message_info = BaseMessageInfo( platform=chat_stream.platform, message_id=message_id, - time=int(time.time()), + time=time, group_info=chat_stream.group_info, - user_info=chat_stream.user_info + user_info=user_info ) # 调用父类初始化 @@ -136,17 +142,41 @@ class MessageProcessBase(MessageBase): raw_message=None ) - # 处理状态相关属性 - self.thinking_start_time = int(time.time()) - self.thinking_time = 0 - + self.chat_stream = chat_stream # 文本处理相关属性 - self.processed_plain_text = "" - self.detailed_plain_text = "" + self.processed_plain_text = detailed_plain_text + self.detailed_plain_text = processed_plain_text # 回复消息 self.reply = reply + +@dataclass +class MessageProcessBase(Message): + """消息处理基类,用于处理中和发送中的消息""" + + def __init__( + self, + message_id: str, + chat_stream: ChatStream, + bot_user_info: UserInfo, + message_segment: Optional[Seg] = None, + reply: Optional['MessageRecv'] = None + ): + # 调用父类初始化 + super().__init__( + message_id=message_id, + time=int(time.time()), + chat_stream=chat_stream, + user_info=bot_user_info, + message_segment=message_segment, + reply=reply + ) + + # 处理状态相关属性 + self.thinking_start_time = int(time.time()) + self.thinking_time = 0 + def update_thinking_time(self) -> float: """更新思考时间""" self.thinking_time = round(time.time() - self.thinking_start_time, 2) @@ -224,12 +254,14 @@ class MessageThinking(MessageProcessBase): self, message_id: str, chat_stream: ChatStream, + bot_user_info: UserInfo, reply: Optional['MessageRecv'] = None ): # 调用父类初始化 super().__init__( message_id=message_id, chat_stream=chat_stream, + bot_user_info=bot_user_info, message_segment=None, # 思考状态不需要消息段 reply=reply ) @@ -237,15 +269,6 @@ class MessageThinking(MessageProcessBase): # 思考状态特有属性 self.interrupt = False - @classmethod - def from_chat_stream(cls, chat_stream: ChatStream, message_id: str, reply: Optional['MessageRecv'] = None) -> 'MessageThinking': - """从聊天流创建思考状态消息""" - return cls( - message_id=message_id, - chat_stream=chat_stream, - reply=reply - ) - @dataclass class MessageSending(MessageProcessBase): """发送状态的消息类""" @@ -254,6 +277,7 @@ class MessageSending(MessageProcessBase): self, message_id: str, chat_stream: ChatStream, + bot_user_info: UserInfo, message_segment: Seg, reply: Optional['MessageRecv'] = None, is_head: bool = False, @@ -263,6 +287,7 @@ class MessageSending(MessageProcessBase): super().__init__( message_id=message_id, chat_stream=chat_stream, + bot_user_info=bot_user_info, message_segment=message_segment, reply=reply ) @@ -296,10 +321,16 @@ class MessageSending(MessageProcessBase): message_id=thinking.message_info.message_id, chat_stream=thinking.chat_stream, message_segment=message_segment, + bot_user_info=thinking.message_info.user_info, reply=thinking.reply, is_head=is_head, is_emoji=is_emoji ) + + def to_dict(self): + ret= super().to_dict() + ret['mesage_info']['user_info']=self.chat_stream.user_info.to_dict() + return ret @dataclass class MessageSet: diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py index 77694ad8c..7b76403de 100644 --- a/src/plugins/chat/message_base.py +++ b/src/plugins/chat/message_base.py @@ -78,6 +78,21 @@ class GroupInfo: def to_dict(self) -> Dict: """转换为字典格式""" return {k: v for k, v in asdict(self).items() if v is not None} + + def from_dict(cls, data: Dict) -> 'GroupInfo': + """从字典创建GroupInfo实例 + + Args: + data: 包含必要字段的字典 + + Returns: + GroupInfo: 新的实例 + """ + return cls( + platform=data.get('platform'), + group_id=data.get('group_id'), + group_name=data.get('group_name',None) + ) @dataclass class UserInfo: @@ -90,6 +105,22 @@ class UserInfo: def to_dict(self) -> Dict: """转换为字典格式""" return {k: v for k, v in asdict(self).items() if v is not None} + + def from_dict(cls, data: Dict) -> 'UserInfo': + """从字典创建UserInfo实例 + + Args: + data: 包含必要字段的字典 + + Returns: + UserInfo: 新的实例 + """ + return cls( + platform=data.get('platform'), + user_id=data.get('user_id'), + user_nickname=data.get('user_nickname',None), + user_cardname=data.get('user_cardname',None) + ) @dataclass class BaseMessageInfo: @@ -147,7 +178,7 @@ class MessageBase: """ message_info = BaseMessageInfo(**data.get('message_info', {})) message_segment = Seg(**data.get('message_segment', {})) - raw_message = data.get('raw_message') + raw_message = data.get('raw_message',None) return cls( message_info=message_info, message_segment=message_segment, diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 7d9c6216d..4d7489bbf 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -139,26 +139,23 @@ class MessageSendCQ(MessageCQ): def __init__( self, - message_id: int, - user_id: int, - message_segment: Seg, - group_id: Optional[int] = None, - reply_to_message_id: Optional[int] = None, - platform: str = "qq" + data: Dict ): # 调用父类初始化 - super().__init__(message_id, user_id, group_id, platform) + message_info = BaseMessageInfo(**data.get('message_info', {})) + message_segment = Seg(**data.get('message_segment', {})) + super().__init__( + message_info.message_id, + message_info.user_info.user_id, + message_info.group_info.group_id if message_info.group_info else None, + message_info.platform) self.message_segment = message_segment - self.raw_message = self._generate_raw_message(reply_to_message_id) + self.raw_message = self._generate_raw_message() - def _generate_raw_message(self, reply_to_message_id: Optional[int] = None) -> str: + def _generate_raw_message(self, ) -> str: """将Seg对象转换为raw_message""" segments = [] - - # 添加回复消息 - if reply_to_message_id: - segments.append(cq_code_tool.create_reply_cq(reply_to_message_id)) # 处理消息段 if self.message_segment.type == 'seglist': diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 9b1ab66be..ed91b614e 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -29,13 +29,12 @@ class Message_Sender: ) -> None: """发送消息""" if isinstance(message, MessageSending): + message_json = message.to_dict() message_send=MessageSendCQ( - message_id=message.message_id, - user_id=message.message_info.user_info.user_id, - message_segment=message.message_segment, - reply=message.reply + data=message_json ) - if message.message_info.group_info: + + if message_send.message_info.group_info: try: await self._current_bot.send_group_msg( group_id=message.message_info.group_info.group_id, diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index e337cef45..46adc343e 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -8,6 +8,7 @@ from ..moods.moods import MoodManager from ..schedule.schedule_generator import bot_schedule from .config import global_config from .utils import get_embedding, get_recent_group_detailed_plain_text +from .chat_stream import ChatStream, chat_manager class PromptBuilder: @@ -22,7 +23,7 @@ class PromptBuilder: message_txt: str, sender_name: str = "某人", relationship_value: float = 0.0, - group_id: Optional[int] = None) -> tuple[str, str]: + stream_id: Optional[int] = None) -> tuple[str, str]: """构建prompt Args: @@ -72,11 +73,17 @@ class PromptBuilder: print(f"\033[1;32m[知识检索]\033[0m 耗时: {(end_time - start_time):.3f}秒") # 获取聊天上下文 + chat_in_group=True chat_talking_prompt = '' - if group_id: - chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, limit=global_config.MAX_CONTEXT_SIZE,combine = True) - - chat_talking_prompt = f"以下是群里正在聊天的内容:\n{chat_talking_prompt}" + if stream_id: + chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, stream_id, limit=global_config.MAX_CONTEXT_SIZE,combine = True) + chat_stream=chat_manager.get_stream(stream_id) + if chat_stream.group_info: + chat_talking_prompt = f"以下是群里正在聊天的内容:\n{chat_talking_prompt}" + else: + chat_in_group=False + chat_talking_prompt = f"以下是你正在和{sender_name}私聊的内容:\n{chat_talking_prompt}" + # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") @@ -112,8 +119,10 @@ class PromptBuilder: #激活prompt构建 activate_prompt = '' - activate_prompt = f"以上是群里正在进行的聊天,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},{mood_prompt},你想要{relation_prompt_2}。" - + if chat_in_group: + activate_prompt = f"以上是群里正在进行的聊天,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和ta{relation_prompt},{mood_prompt},你想要{relation_prompt_2}。" + else: + activate_prompt = f"以上是你正在和{sender_name}私聊的内容,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和ta{relation_prompt},{mood_prompt},你想要{relation_prompt_2}。" #检测机器人相关词汇 bot_keywords = ['人机', 'bot', '机器', '入机', 'robot', '机器人'] is_bot = any(keyword in message_txt.lower() for keyword in bot_keywords) @@ -129,16 +138,20 @@ class PromptBuilder: probability_3 = global_config.PERSONALITY_3 prompt_personality = '' personality_choice = random.random() + if chat_in_group: + prompt_in_group=f"你正在浏览{chat_stream.platform}群" + else: + prompt_in_group=f"你正在{chat_stream.platform}上和{sender_name}私聊" if personality_choice < probability_1: # 第一种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[0]},{prompt_in_group},{promt_info_prompt}, 现在请你给出日常且口语化的回复,平淡一些,尽量简短一些。{is_bot_prompt} 请注意把握群里的聊天内容,不要刻意突出自身学科背景,不要回复的太有条理,可以有个性。''' elif personality_choice < probability_1 + probability_2: # 第二种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[1]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[1]},{prompt_in_group},{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{is_bot_prompt} 请你表达自己的见解和观点。可以有个性。''' else: # 第三种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]},{prompt_in_group},{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{is_bot_prompt} 请你表达自己的见解和观点。可以有个性。''' diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 17fc2ac6a..c08b962ed 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -16,17 +16,16 @@ class Impression: class Relationship: user_id: int = None platform: str = None - platform: str = None gender: str = None age: int = None nickname: str = None relationship_value: float = None saved = False - def __init__(self, chat:ChatStream,data:dict): - self.user_id=chat.user_info.user_id - self.platform=chat.platform - self.nickname=chat.user_info.user_nickname + def __init__(self, chat:ChatStream=None,data:dict=None): + self.user_id=chat.user_info.user_id if chat.user_info else data.get('user_id',0) + self.platform=chat.platform if chat.user_info else data.get('platform','') + self.nickname=chat.user_info.user_nickname if chat.user_info else data.get('nickname','') self.relationship_value=data.get('relationship_value',0) self.age=data.get('age',0) self.gender=data.get('gender','') @@ -35,7 +34,6 @@ class Relationship: class RelationshipManager: def __init__(self): self.relationships: dict[tuple[int, str], Relationship] = {} # 修改为使用(user_id, platform)作为键 - self.relationships: dict[tuple[int, str], Relationship] = {} # 修改为使用(user_id, platform)作为键 async def update_relationship(self, chat_stream:ChatStream, @@ -43,9 +41,7 @@ class RelationshipManager: **kwargs) -> Optional[Relationship]: """更新或创建关系 Args: - user_id: 用户ID(可选,如果提供user_info则不需要) - platform: 平台(可选,如果提供user_info则不需要) - user_info: 用户信息对象(可选) + chat_stream: 聊天流对象 data: 字典格式的数据(可选) **kwargs: 其他参数 Returns: @@ -66,44 +62,18 @@ class RelationshipManager: # 检查是否在内存中已存在 relationship = self.relationships.get(key) - relationship = self.relationships.get(key) if relationship: # 如果存在,更新现有对象 if isinstance(data, dict): for k, value in data.items(): if hasattr(relationship, k) and value is not None: setattr(relationship, k, value) - for k, value in data.items(): - if hasattr(relationship, k) and value is not None: - setattr(relationship, k, value) - else: - for k, value in kwargs.items(): - if hasattr(relationship, k) and value is not None: - setattr(relationship, k, value) - for k, value in kwargs.items(): - if hasattr(relationship, k) and value is not None: - setattr(relationship, k, value) else: # 如果不存在,创建新对象 - if user_info is not None: - relationship = Relationship(user_info=user_info, **kwargs) - elif isinstance(data, dict): - data['platform'] = platform - relationship = Relationship(user_id=user_id, data=data) + if chat_stream.user_info is not None: + relationship = Relationship(chat=chat_stream, **kwargs) else: - kwargs['platform'] = platform - kwargs['user_id'] = user_id - relationship = Relationship(**kwargs) - self.relationships[key] = relationship - if user_info is not None: - relationship = Relationship(user_info=user_info, **kwargs) - elif isinstance(data, dict): - data['platform'] = platform - relationship = Relationship(user_id=user_id, data=data) - else: - kwargs['platform'] = platform - kwargs['user_id'] = user_id - relationship = Relationship(**kwargs) + raise ValueError("必须提供user_id或user_info") self.relationships[key] = relationship # 保存到数据库 @@ -113,36 +83,7 @@ class RelationshipManager: return relationship async def update_relationship_value(self, - user_id: int = None, - platform: str = None, - user_info: UserInfo = None, - **kwargs) -> Optional[Relationship]: - """更新关系值 - Args: - user_id: 用户ID(可选,如果提供user_info则不需要) - platform: 平台(可选,如果提供user_info则不需要) - user_info: 用户信息对象(可选) - **kwargs: 其他参数 - Returns: - Relationship: 关系对象 - """ - # 确定user_id和platform - if user_info is not None: - user_id = user_info.user_id - platform = user_info.platform or 'qq' - else: - platform = platform or 'qq' - - if user_id is None: - raise ValueError("必须提供user_id或user_info") - - # 使用(user_id, platform)作为键 - key = (user_id, platform) - - async def update_relationship_value(self, - user_id: int = None, - platform: str = None, - user_info: UserInfo = None, + chat_stream:ChatStream, **kwargs) -> Optional[Relationship]: """更新关系值 Args: @@ -154,6 +95,7 @@ class RelationshipManager: Relationship: 关系对象 """ # 确定user_id和platform + user_info = chat_stream.user_info if user_info is not None: user_id = user_info.user_id platform = user_info.platform or 'qq' @@ -168,10 +110,7 @@ class RelationshipManager: # 检查是否在内存中已存在 relationship = self.relationships.get(key) - relationship = self.relationships.get(key) if relationship: - for k, value in kwargs.items(): - if k == 'relationship_value': for k, value in kwargs.items(): if k == 'relationship_value': relationship.relationship_value += value @@ -181,43 +120,12 @@ class RelationshipManager: else: # 如果不存在且提供了user_info,则创建新的关系 if user_info is not None: - return await self.update_relationship(user_info=user_info, **kwargs) - print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id}({platform}) 不存在,无法更新") - # 如果不存在且提供了user_info,则创建新的关系 - if user_info is not None: - return await self.update_relationship(user_info=user_info, **kwargs) + return await self.update_relationship(chat_stream=chat_stream, **kwargs) print(f"\033[1;31m[关系管理]\033[0m 用户 {user_id}({platform}) 不存在,无法更新") return None def get_relationship(self, - user_id: int = None, - platform: str = None, - user_info: UserInfo = None) -> Optional[Relationship]: - """获取用户关系对象 - Args: - user_id: 用户ID(可选,如果提供user_info则不需要) - platform: 平台(可选,如果提供user_info则不需要) - user_info: 用户信息对象(可选) - Returns: - Relationship: 关系对象 - """ - # 确定user_id和platform - if user_info is not None: - user_id = user_info.user_id - platform = user_info.platform or 'qq' - else: - platform = platform or 'qq' - - if user_id is None: - raise ValueError("必须提供user_id或user_info") - - key = (user_id, platform) - if key in self.relationships: - return self.relationships[key] - def get_relationship(self, - user_id: int = None, - platform: str = None, - user_info: UserInfo = None) -> Optional[Relationship]: + chat_stream:ChatStream) -> Optional[Relationship]: """获取用户关系对象 Args: user_id: 用户ID(可选,如果提供user_info则不需要) @@ -227,6 +135,8 @@ class RelationshipManager: Relationship: 关系对象 """ # 确定user_id和platform + user_info = chat_stream.user_info + platform = chat_stream.user_info.platform or 'qq' if user_info is not None: user_id = user_info.user_id platform = user_info.platform or 'qq' @@ -248,18 +158,10 @@ class RelationshipManager: if 'platform' not in data: data['platform'] = 'qq' - rela = Relationship(data=data) - """从数据库加载或创建新的关系对象""" - # 确保data中有platform字段,如果没有则默认为'qq' - if 'platform' not in data: - data['platform'] = 'qq' - rela = Relationship(data=data) rela.saved = True key = (rela.user_id, rela.platform) self.relationships[key] = rela - key = (rela.user_id, rela.platform) - self.relationships[key] = rela return rela async def load_all_relationships(self): @@ -277,7 +179,6 @@ class RelationshipManager: # 依次加载每条记录 for data in all_relationships: await self.load_relationship(data) - await self.load_relationship(data) print(f"\033[1;32m[关系管理]\033[0m 已加载 {len(self.relationships)} 条关系记录") while True: @@ -288,19 +189,15 @@ class RelationshipManager: async def _save_all_relationships(self): """将所有关系数据保存到数据库""" # 保存所有关系数据 - for (userid, platform), relationship in self.relationships.items(): for (userid, platform), relationship in self.relationships.items(): if not relationship.saved: relationship.saved = True await self.storage_relationship(relationship) - async def storage_relationship(self, relationship: Relationship): - """将关系记录存储到数据库中""" async def storage_relationship(self, relationship: Relationship): """将关系记录存储到数据库中""" user_id = relationship.user_id platform = relationship.platform - platform = relationship.platform nickname = relationship.nickname relationship_value = relationship.relationship_value gender = relationship.gender @@ -309,10 +206,8 @@ class RelationshipManager: db = Database.get_instance() db.db.relationships.update_one( - {'user_id': user_id, 'platform': platform}, {'user_id': user_id, 'platform': platform}, {'$set': { - 'platform': platform, 'platform': platform, 'nickname': nickname, 'relationship_value': relationship_value, @@ -323,27 +218,6 @@ class RelationshipManager: upsert=True ) - def get_name(self, - user_id: int = None, - platform: str = None, - user_info: UserInfo = None) -> str: - """获取用户昵称 - Args: - user_id: 用户ID(可选,如果提供user_info则不需要) - platform: 平台(可选,如果提供user_info则不需要) - user_info: 用户信息对象(可选) - Returns: - str: 用户昵称 - """ - # 确定user_id和platform - if user_info is not None: - user_id = user_info.user_id - platform = user_info.platform or 'qq' - else: - platform = platform or 'qq' - - if user_id is None: - raise ValueError("必须提供user_id或user_info") def get_name(self, user_id: int = None, @@ -370,11 +244,6 @@ class RelationshipManager: # 确保user_id是整数类型 user_id = int(user_id) key = (user_id, platform) - if key in self.relationships: - return self.relationships[key].nickname - elif user_info is not None: - return user_info.user_nickname or user_info.user_cardname or "某人" - key = (user_id, platform) if key in self.relationships: return self.relationships[key].nickname elif user_info is not None: diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 85abd5150..614246d26 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -18,8 +18,9 @@ class MessageStorage: "time": message.message_info.time, "chat_id":chat_stream.stream_id, "chat_info": chat_stream.to_dict(), - "detailed_plain_text": message.detailed_plain_text, + "user_info": message.message_info.user_info.to_dict(), "processed_plain_text": message.processed_plain_text, + "detailed_plain_text": message.detailed_plain_text, "topic": topic, } self.db.db.messages.insert_one(message_data) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 07c3e2a6f..495d0480d 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -11,7 +11,9 @@ from nonebot import get_driver from ..models.utils_model import LLM_request from ..utils.typo_generator import ChineseTypoGenerator from .config import global_config -from .message_cq import Message +from .message import MessageThinking, MessageRecv,MessageSending,MessageProcessBase,Message +from .message_base import MessageBase,BaseMessageInfo,UserInfo,GroupInfo +from .chat_stream import ChatStream driver = get_driver() config = driver.config @@ -32,7 +34,7 @@ def db_message_to_str(message_dict: Dict) -> str: return result -def is_mentioned_bot_in_message(message: Message) -> bool: +def is_mentioned_bot_in_message(message: MessageRecv) -> bool: """检查消息是否提到了机器人""" keywords = [global_config.BOT_NICKNAME] for keyword in keywords: @@ -41,15 +43,6 @@ def is_mentioned_bot_in_message(message: Message) -> bool: return False -def is_mentioned_bot_in_txt(message: str) -> bool: - """检查消息是否提到了机器人""" - keywords = [global_config.BOT_NICKNAME] - for keyword in keywords: - if keyword in message: - return True - return False - - async def get_embedding(text): """获取文本的embedding向量""" llm = LLM_request(model=global_config.embedding) @@ -84,10 +77,10 @@ def get_cloest_chat_from_db(db, length: int, timestamp: str): if closest_record and closest_record.get('memorized', 0) < 4: closest_time = closest_record['time'] - group_id = closest_record['group_id'] # 获取groupid + chat_id = closest_record['chat_id'] # 获取groupid # 获取该时间戳之后的length条消息,且groupid相同 chat_records = list(db.db.messages.find( - {"time": {"$gt": closest_time}, "group_id": group_id} + {"time": {"$gt": closest_time}, "chat_id": chat_id} ).sort('time', 1).limit(length)) # 更新每条消息的memorized属性 @@ -111,7 +104,7 @@ def get_cloest_chat_from_db(db, length: int, timestamp: str): return '' -async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: +async def get_recent_group_messages(db, chat_id:str, limit: int = 12) -> list: """从数据库获取群组最近的消息记录 Args: @@ -125,35 +118,28 @@ async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: # 从数据库获取最近消息 recent_messages = list(db.db.messages.find( - {"group_id": group_id}, - # { - # "time": 1, - # "user_id": 1, - # "user_nickname": 1, - # "message_id": 1, - # "raw_message": 1, - # "processed_text": 1 - # } + {"chat_id": chat_id}, ).sort("time", -1).limit(limit)) if not recent_messages: return [] # 转换为 Message对象列表 - from .message_cq import Message message_objects = [] for msg_data in recent_messages: try: + chat_info=msg_data.get("chat_info",{}) + chat_stream=ChatStream.from_dict(chat_info) + user_info=msg_data.get("user_info",{}) + user_info=UserInfo.from_dict(user_info) msg = Message( - time=msg_data["time"], - user_id=msg_data["user_id"], - user_nickname=msg_data.get("user_nickname", ""), message_id=msg_data["message_id"], - raw_message=msg_data["raw_message"], + chat_stream=chat_stream, + time=msg_data["time"], + user_info=user_info, processed_plain_text=msg_data.get("processed_text", ""), - group_id=group_id + detailed_plain_text=msg_data.get("detailed_plain_text", "") ) - await msg.initialize() message_objects.append(msg) except KeyError: print("[WARNING] 数据库中存在无效的消息") @@ -164,13 +150,14 @@ async def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: return message_objects -def get_recent_group_detailed_plain_text(db, group_id: int, limit: int = 12, combine=False): +def get_recent_group_detailed_plain_text(db, chat_stream_id: int, limit: int = 12, combine=False): recent_messages = list(db.db.messages.find( - {"group_id": group_id}, + {"chat_id": chat_stream_id}, { "time": 1, # 返回时间字段 - "user_id": 1, # 返回用户ID字段 - "user_nickname": 1, # 返回用户昵称字段 + "chat_id":1, + "chat_info":1, + "user_info": 1, "message_id": 1, # 返回消息ID字段 "detailed_plain_text": 1 # 返回处理后的文本字段 } From 7b35ddd07fc00b1c4f8dc25255b347ae49d95e7c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 01:19:56 +0800 Subject: [PATCH 055/105] =?UTF-8?q?ruff=20=E5=93=A5=E5=8F=88=E6=9C=89?= =?UTF-8?q?=E6=96=B0=E7=82=B9=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/reasoning_gui.py | 4 ++-- src/plugins/chat/bot.py | 2 +- src/plugins/chat/config.py | 6 +++--- src/plugins/chat/cq_code.py | 6 +++--- src/plugins/chat/emoji_manager.py | 12 ++++++------ src/plugins/chat/llm_generator.py | 4 ++-- src/plugins/chat/message_sender.py | 6 +++--- src/plugins/chat/storage.py | 2 +- src/plugins/models/utils_model.py | 12 ++++++------ src/plugins/schedule/schedule_generator.py | 2 +- src/plugins/utils/statistic.py | 2 +- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/gui/reasoning_gui.py b/src/gui/reasoning_gui.py index 514a95dfb..5768ddc09 100644 --- a/src/gui/reasoning_gui.py +++ b/src/gui/reasoning_gui.py @@ -87,7 +87,7 @@ class ReasoningGUI: self.db = Database.get_instance().db logger.success("数据库初始化成功") except Exception: - logger.exception(f"数据库初始化失败") + logger.exception("数据库初始化失败") sys.exit(1) # 存储群组数据 @@ -342,7 +342,7 @@ class ReasoningGUI: 'group_id': self.selected_group_id }) except Exception: - logger.exception(f"自动更新出错") + logger.exception("自动更新出错") # 每5秒更新一次 time.sleep(5) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index c510fe4bf..59d94b5e5 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -129,7 +129,7 @@ class ChatBot: # 如果找不到思考消息,直接返回 if not thinking_message: - logger.warning(f"未找到对应的思考消息,可能已超时被移除") + logger.warning("未找到对应的思考消息,可能已超时被移除") return # 记录开始思考的时间,避免从思考到回复的时间太久 diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index c37d23a46..407624f2a 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -113,7 +113,7 @@ class BotConfig: try: converted = SpecifierSet(value) - except InvalidSpecifier as e: + except InvalidSpecifier: logger.error( f"{value} 分类使用了错误的版本约束表达式\n", "请阅读 https://semver.org/lang/zh-CN/ 修改代码" @@ -135,7 +135,7 @@ class BotConfig: try: config_version: str = toml["inner"]["version"] except KeyError as e: - logger.error(f"配置文件中 inner 段 不存在, 这是错误的配置文件") + logger.error("配置文件中 inner 段 不存在, 这是错误的配置文件") raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") else: toml["inner"] = {"version": "0.0.0"} @@ -143,7 +143,7 @@ class BotConfig: try: ver = version.parse(config_version) - except InvalidVersion as e: + except InvalidVersion: logger.error( "配置文件中 inner段 的 version 键是错误的版本描述\n" "请阅读 https://semver.org/lang/zh-CN/ 修改配置,并参考本项目指定的模板进行修改\n" diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index b13e33e48..21541a78b 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -155,8 +155,8 @@ class CQCode: logger.error(f"最终请求失败: {str(e)}") time.sleep(1.5 ** retry) # 指数退避 - except Exception as e: - logger.exception(f"[未知错误]") + except Exception: + logger.exception("[未知错误]") return None return None @@ -281,7 +281,7 @@ class CQCode: logger.debug(f"合并后的转发消息: {combined_messages}") return f"[转发消息:\n{combined_messages}]" - except Exception as e: + except Exception: logger.exception("处理转发消息失败") return '[转发消息]' diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 740e9a677..5a601d434 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -51,8 +51,8 @@ class EmojiManager: self._initialized = True # 启动时执行一次完整性检查 self.check_emoji_file_integrity() - except Exception as e: - logger.exception(f"初始化表情管理器失败") + except Exception: + logger.exception("初始化表情管理器失败") def _ensure_db(self): """确保数据库已初始化""" @@ -87,8 +87,8 @@ class EmojiManager: {'_id': emoji_id}, {'$inc': {'usage_count': 1}} ) - except Exception as e: - logger.exception(f"记录表情使用失败") + except Exception: + logger.exception("记录表情使用失败") async def get_emoji_for_text(self, text: str) -> Optional[str]: """根据文本内容获取相关表情包 @@ -264,8 +264,8 @@ class EmojiManager: else: logger.warning(f"跳过表情包: {filename}") - except Exception as e: - logger.exception(f"扫描表情包失败") + except Exception: + logger.exception("扫描表情包失败") async def _periodic_scan(self, interval_MINS: int = 10): """定期扫描新表情包""" diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index f40c9a441..4e431d9fd 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -94,7 +94,7 @@ class ResponseGenerator: try: content, reasoning_content = await model.generate_response(prompt) except Exception: - logger.exception(f"生成回复时出错") + logger.exception("生成回复时出错") return None # 保存到数据库 @@ -146,7 +146,7 @@ class ResponseGenerator: return ["neutral"] except Exception: - logger.exception(f"获取情感标签时出错") + logger.exception("获取情感标签时出错") return ["neutral"] async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 4abbd3b3f..0fb40373e 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -61,7 +61,7 @@ class Message_Sender: auto_escape=auto_escape ) logger.debug(f"发送消息{message}成功") - except Exception as e: + except Exception: logger.exception(f"发送消息{message}失败") @@ -120,7 +120,7 @@ class MessageContainer: return True return False except Exception: - logger.exception(f"移除消息时发生错误") + logger.exception("移除消息时发生错误") return False def has_messages(self) -> bool: @@ -214,7 +214,7 @@ class MessageManager: if not container.remove_message(msg): logger.warning("尝试删除不存在的消息") except Exception: - logger.exception(f"处理超时消息时发生错误") + logger.exception("处理超时消息时发生错误") continue async def start_processor(self): diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 1c2d05071..4081f8984 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -45,6 +45,6 @@ class MessageStorage: self.db.db.messages.insert_one(message_data) except Exception: - logger.exception(f"存储消息失败") + logger.exception("存储消息失败") # 如果需要其他存储相关的函数,可以在这里添加 diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index fe8e1a100..ac567600a 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -44,8 +44,8 @@ class LLM_request: self.db.db.llm_usage.create_index([("model_name", 1)]) self.db.db.llm_usage.create_index([("user_id", 1)]) self.db.db.llm_usage.create_index([("request_type", 1)]) - except Exception as e: - logger.error(f"创建数据库索引失败") + except Exception: + logger.error("创建数据库索引失败") def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, user_id: str = "system", request_type: str = "chat", @@ -80,7 +80,7 @@ class LLM_request: f"总计: {total_tokens}" ) except Exception: - logger.error(f"记录token使用情况失败") + logger.error("记录token使用情况失败") def _calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float: """计算API调用成本 @@ -194,7 +194,7 @@ class LLM_request: if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( 'name') == old_model_name: global_config.llm_normal['name'] = self.model_name - logger.warning(f"已将全局配置中的 llm_normal 模型降级") + logger.warning("已将全局配置中的 llm_normal 模型降级") # 更新payload中的模型名 if payload and 'model' in payload: @@ -227,7 +227,7 @@ class LLM_request: delta_content = "" accumulated_content += delta_content except Exception: - logger.exception(f"解析流式输出错") + logger.exception("解析流式输出错") content = accumulated_content reasoning_content = "" think_match = re.search(r'(.*?)', content, re.DOTALL) @@ -355,7 +355,7 @@ class LLM_request: """构建请求头""" if no_key: return { - "Authorization": f"Bearer **********", + "Authorization": "Bearer **********", "Content-Type": "application/json" } else: diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index fc07a152d..b5494f4cf 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -91,7 +91,7 @@ class ScheduleGenerator: try: schedule_dict = json.loads(schedule_text) return schedule_dict - except json.JSONDecodeError as e: + except json.JSONDecodeError: logger.exception("解析日程失败: {}".format(schedule_text)) return False diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index a071355a3..2974389e6 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -155,7 +155,7 @@ class LLMStatistics: all_stats = self._collect_all_statistics() self._save_statistics(all_stats) except Exception: - logger.exception(f"统计数据处理失败") + logger.exception("统计数据处理失败") # 等待1分钟 for _ in range(60): From e4b8865f9978960941f276ffa8dd0fdb3ee94f33 Mon Sep 17 00:00:00 2001 From: Hosigus Date: Mon, 10 Mar 2025 14:33:30 +0800 Subject: [PATCH 056/105] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=AB=E5=90=8D?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E4=BB=A5=E7=94=A8=E4=B8=8D=E5=90=8C=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E5=8F=AC=E5=94=A4=E6=9C=BA=E5=99=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/installation_cute.md | 3 ++- docs/installation_standard.md | 3 +++ src/plugins/chat/config.py | 4 +++- src/plugins/chat/prompt_builder.py | 9 +++++---- src/plugins/chat/utils.py | 16 +++++----------- template/bot_config_template.toml | 3 ++- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/installation_cute.md b/docs/installation_cute.md index 3a63988f1..4465660f9 100644 --- a/docs/installation_cute.md +++ b/docs/installation_cute.md @@ -110,7 +110,8 @@ PLUGINS=["src2.plugins.chat"] # 这里是机器人的插件列表呢 ```toml [bot] qq = "把这里改成你的机器人QQ号喵" # 填写你的机器人QQ号 -nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦 +nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦,建议和机器人QQ名称/群昵称一样哦 +alias_names = ["小麦", "阿麦"] # 也可以用这个招呼机器人,可以不设置呢 [personality] # 这里可以设置机器人的性格呢,让它更有趣一些喵 diff --git a/docs/installation_standard.md b/docs/installation_standard.md index cf8e7eb9b..ec5a05149 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -72,6 +72,9 @@ PLUGINS=["src2.plugins.chat"] [bot] qq = "机器人QQ号" # 必填 nickname = "麦麦" # 机器人昵称 +# alias_names: 配置机器人可使用的别名。当机器人在群聊或对话中被调用时,别名可以作为直接命令或提及机器人的关键字使用。 +# 该配置项为字符串数组。例如: ["小麦", "阿麦"] +alias_names = ["小麦", "阿麦"] # 机器人别名 [personality] prompt_personality = [ diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index c37d23a46..8c45d1664 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -1,6 +1,6 @@ import os from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import Dict, List, Optional import tomli from loguru import logger @@ -16,6 +16,7 @@ class BotConfig: BOT_QQ: Optional[int] = 1 BOT_NICKNAME: Optional[str] = None + BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它 # 消息处理相关配置 MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 @@ -190,6 +191,7 @@ class BotConfig: bot_qq = bot_config.get("qq") config.BOT_QQ = int(bot_qq) config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME) + config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES) def response(parent: dict): response_config = parent["response"] diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 4cf21af19..0805caa5a 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -131,18 +131,19 @@ class PromptBuilder: probability_1 = global_config.PERSONALITY_1 probability_2 = global_config.PERSONALITY_2 probability_3 = global_config.PERSONALITY_3 - prompt_personality = '' + + prompt_personality = f'{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},你还有很多别名:{"/".join(global_config.BOT_ALIAS_NAMES)},' personality_choice = random.random() if personality_choice < probability_1: # 第一种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,平淡一些,尽量简短一些。{keywords_reaction_prompt} 请注意把握群里的聊天内容,不要刻意突出自身学科背景,不要回复的太有条理,可以有个性。''' elif personality_choice < probability_1 + probability_2: # 第二种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[1]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[1]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 请你表达自己的见解和观点。可以有个性。''' else: # 第三种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 请你表达自己的见解和观点。可以有个性。''' diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 054526e94..6619f37af 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -53,19 +53,13 @@ def db_message_to_str(message_dict: Dict) -> str: return result -def is_mentioned_bot_in_message(message: Message) -> bool: - """检查消息是否提到了机器人""" - keywords = [global_config.BOT_NICKNAME] - for keyword in keywords: - if keyword in message.processed_plain_text: - return True - return False - - def is_mentioned_bot_in_txt(message: str) -> bool: """检查消息是否提到了机器人""" - keywords = [global_config.BOT_NICKNAME] - for keyword in keywords: + if global_config.BOT_NICKNAME is None: + return True + if global_config.BOT_NICKNAME in message: + return True + for keyword in global_config.BOT_ALIAS_NAMES: if keyword in message: return True return False diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index bff64d05f..24b2f93c2 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "0.0.4" +version = "0.0.5" #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 @@ -15,6 +15,7 @@ version = "0.0.4" [bot] qq = 123 nickname = "麦麦" +alias_names = ["小麦", "阿麦"] [personality] prompt_personality = [ From 043a7246433c0451c34adc0ee921751afdada59c Mon Sep 17 00:00:00 2001 From: Yan233_ Date: Tue, 11 Mar 2025 01:24:39 +0800 Subject: [PATCH 057/105] =?UTF-8?q?=E4=BF=AE=E4=B8=80=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=B7=B3=E8=BD=AC=EF=BC=8C=E5=B0=8F=E7=BE=8E=E5=8C=96?= =?UTF-8?q?=EF=BC=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/docker_deploy.md | 14 +++++++------- docs/installation_standard.md | 2 +- docs/manual_deploy_linux.md | 15 +++++++-------- docs/manual_deploy_windows.md | 8 ++++---- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 29e25b2b9..3c37acf96 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ### 部署方式 -- 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 +- 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置 - [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md) diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index 3958d2fc4..c00d0cb04 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -9,13 +9,13 @@ wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml ``` -- 若需要启用MongoDB数据库的用户名和密码,可进入docker-compose.yml,取消MongoDB处的注释并修改变量`=`后方的值为你的用户名和密码\ -修改后请注意在之后配置`.env.prod`文件时指定MongoDB数据库的用户名密码 +- 若需要启用MongoDB数据库的用户名和密码,可进入docker-compose.yml,取消MongoDB处的注释并修改变量旁 `=` 后方的值为你的用户名和密码\ +修改后请注意在之后配置 `.env.prod` 文件时指定MongoDB数据库的用户名密码 ### 2. 启动服务: -- **!!! 请在第一次启动前确保当前工作目录下`.env.prod`与`bot_config.toml`文件存在 !!!**\ +- **!!! 请在第一次启动前确保当前工作目录下 `.env.prod` 与 `bot_config.toml` 文件存在 !!!**\ 由于Docker文件映射行为的特殊性,若宿主机的映射路径不存在,可能导致意外的目录创建,而不会创建文件,由于此处需要文件映射到文件,需提前确保文件存在且路径正确,可使用如下命令: ```bash touch .env.prod @@ -32,8 +32,8 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d ### 3. 修改配置并重启Docker: -- 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\ -**需要注意`.env.prod`中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** +- 请前往 [🎀 新手配置指南](./installation_cute.md) 或 [⚙️ 标准配置指南](./installation_standard.md) 完成 `.env.prod` 与 `bot_config.toml` 配置文件的编写\ +**需要注意 `.env.prod` 中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: ```bash @@ -50,10 +50,10 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart ### 4. 登入NapCat管理页添加反向WebSocket -- 在浏览器地址栏输入`http://<宿主机IP>:6099/`进入NapCat的管理Web页,添加一个Websocket客户端 +- 在浏览器地址栏输入 `http://<宿主机IP>:6099/` 进入NapCat的管理Web页,添加一个Websocket客户端 > 网络配置 -> 新建 -> Websocket客户端 -- Websocket客户端的名称自定,URL栏填入`ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ +- Websocket客户端的名称自定,URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ (若修改过容器名称则替换maimbot为你自定的名称) diff --git a/docs/installation_standard.md b/docs/installation_standard.md index cf8e7eb9b..b3a9867f9 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -8,7 +8,7 @@ ## API配置说明 -`.env.prod`和`bot_config.toml`中的API配置关系如下: +`.env.prod` 和 `bot_config.toml` 中的API配置关系如下: ### 在.env.prod中定义API凭证: ```ini diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index d310ffc59..41f0390b8 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -66,7 +66,7 @@ pip install -r requirements.txt ## 数据库配置 ### 3️⃣ **安装并启动MongoDB** -- 安装与启动:Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/),Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) +- 安装与启动: Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/),Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) - 默认连接本地27017端口 --- @@ -76,15 +76,14 @@ pip install -r requirements.txt - 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装 -- 使用QQ小号登录,添加反向WS地址: -`ws://127.0.0.1:8080/onebot/v11/ws` +- 使用QQ小号登录,添加反向WS地址: `ws://127.0.0.1:8080/onebot/v11/ws` --- ## 配置文件设置 ### 5️⃣ **配置文件设置,让麦麦Bot正常工作** -- 修改环境配置文件:`.env.prod` -- 修改机器人配置文件:`bot_config.toml` +- 修改环境配置文件: `.env.prod` +- 修改机器人配置文件: `bot_config.toml` --- @@ -107,9 +106,9 @@ python3 bot.py --- ## 常见问题 -🔧 权限问题:在命令前加`sudo` -🔌 端口占用:使用`sudo lsof -i :8080`查看端口占用 -🛡️ 防火墙:确保8080/27017端口开放 +🔧 权限问题: 在命令前加 `sudo` +🔌 端口占用: 使用 `sudo lsof -i :8080` 查看端口占用 +🛡️ 防火墙: 确保8080/27017端口开放 ```bash sudo ufw allow 8080/tcp sudo ufw allow 27017/tcp diff --git a/docs/manual_deploy_windows.md b/docs/manual_deploy_windows.md index 86238bcd4..eebdc4f41 100644 --- a/docs/manual_deploy_windows.md +++ b/docs/manual_deploy_windows.md @@ -30,7 +30,7 @@ 在创建虚拟环境之前,请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装: -1. 访问Python官网下载页面:https://www.python.org/downloads/release/python-3913/ +1. 访问Python官网下载页面: https://www.python.org/downloads/release/python-3913/ 2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe` 3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项 4. 点击"Install Now"开始安装 @@ -79,11 +79,11 @@ pip install -r requirements.txt ### 3️⃣ **配置NapCat,让麦麦bot与qq取得联系** - 安装并登录NapCat(用你的qq小号) -- 添加反向WS:`ws://127.0.0.1:8080/onebot/v11/ws` +- 添加反向WS: `ws://127.0.0.1:8080/onebot/v11/ws` ### 4️⃣ **配置文件设置,让麦麦Bot正常工作** -- 修改环境配置文件:`.env.prod` -- 修改机器人配置文件:`bot_config.toml` +- 修改环境配置文件: `.env.prod` +- 修改机器人配置文件: `bot_config.toml` ### 5️⃣ **启动麦麦机器人** - 打开命令行,cd到对应路径 From 2e8cd47133dca0f5f9b83231eb93ad255cfe08a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Tue, 11 Mar 2025 02:28:10 +0900 Subject: [PATCH 058/105] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E5=87=BA=E7=8E=B0=E7=9A=84=E6=97=A5=E7=A8=8B=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/schedule/schedule_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index b5494f4cf..e280c6bce 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -68,7 +68,7 @@ class ScheduleGenerator: 1. 早上的学习和工作安排 2. 下午的活动和任务 3. 晚上的计划和休息时间 - 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表,仅返回内容,不要返回注释,时间采用24小时制,格式为{"时间": "活动","时间": "活动",...}。""" + 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表,仅返回内容,不要返回注释,不要添加任何markdown或代码块样式,时间采用24小时制,格式为{"时间": "活动","时间": "活动",...}。""" try: schedule_text, _ = await self.llm_scheduler.generate_response(prompt) From 1294c88b148a65daf42ffbff7eaec2dce6b853a8 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 01:31:12 +0800 Subject: [PATCH 059/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E5=87=86=E5=8C=96=E6=A0=BC=E5=BC=8F=E5=8C=96=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++--- .vscode/settings.json | 3 +++ pyproject.toml | 46 ++++++++++++++++++++++++++++--------------- 3 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 4e1606a54..e51abc5cc 100644 --- a/.gitignore +++ b/.gitignore @@ -193,9 +193,8 @@ cython_debug/ # jieba jieba.cache - -# vscode -/.vscode +# .vscode +!.vscode/settings.json # direnv /.direnv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..23fd35f0e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e54dcdacd..1eff99a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,37 @@ -[project] -name = "Megbot" -version = "0.1.0" -description = "New Bot Project" - -[tool.nonebot] -plugins = ["src.plugins.chat"] -plugin_dirs = ["src/plugins"] - [tool.ruff] -# 设置 Python 版本 -target-version = "py39" +include = ["*.py"] +fixable = ["ALL"] # 启用的规则 select = [ - "E", # pycodestyle 错误 - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear + "E", # pycodestyle 错误 + "F", # pyflakes + "B", # flake8-bugbear ] +ignore = ["E711"] + +# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。 +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + # 行长度设置 -line-length = 88 \ No newline at end of file +line-length = 120 + +[tool.ruff.format] +docstring-code-format = true +indent-style = "space" + +# 使用双引号表示字符串 +quote-style = "double" + +# 尊重魔法尾随逗号 +# 例如: +# items = [ +# "apple", +# "banana", +# "cherry", +# ] +skip-magic-trailing-comma = false + +# 自动检测合适的换行符 +line-ending = "auto" From a63ce969c7e7199da0e73b1c666add2b8544dada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Tue, 11 Mar 2025 02:38:23 +0900 Subject: [PATCH 060/105] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=83=85?= =?UTF-8?q?=E6=84=9F=E5=88=A4=E6=96=AD=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=88=E4=BD=BF=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E9=87=8C?= =?UTF-8?q?=E7=9A=84=20llm=5Femotion=5Fjudge=20=E7=94=9F=E6=95=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/emoji_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 5a601d434..df26daa70 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -33,7 +33,7 @@ class EmojiManager: self.db = Database.get_instance() self._scan_task = None self.vlm = LLM_request(model=global_config.vlm, temperature=0.3, max_tokens=1000) - self.llm_emotion_judge = LLM_request(model=global_config.llm_normal_minor, max_tokens=60, + self.llm_emotion_judge = LLM_request(model=global_config.llm_emotion_judge, max_tokens=60, temperature=0.8) # 更高的温度,更少的token(后续可以根据情绪来调整温度) From efcf00f6af0456a35b938ed9d2ed321849a8dfbf Mon Sep 17 00:00:00 2001 From: Yan233_ Date: Tue, 11 Mar 2025 02:00:00 +0800 Subject: [PATCH 061/105] =?UTF-8?q?Docker=E9=83=A8=E7=BD=B2=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=BF=BD=E5=8A=A0=E6=9B=B4=E6=96=B0=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docker_deploy.md | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index c00d0cb04..b4d0a3c7d 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -1,10 +1,17 @@ # 🐳 Docker 部署指南 -## 部署步骤(推荐,但不一定是最新) +## 部署步骤 (推荐,但不一定是最新) +"更新镜像与容器"部分在Part 6 + +### 0. 前提说明 + +**本文假设读者已具备一定的 Docker 基础知识。若您对 Docker 不熟悉,建议先参考相关教程或文档进行学习,或选择使用 [📦Linux手动部署指南](./manual_deploy_linux.md) 或 [📦Windows手动部署指南](./manual_deploy_windows.md) 。** ### 1. 获取Docker配置文件: +- 建议先单独创建好一个文件夹并进入,作为工作目录 + ```bash wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml ``` @@ -25,14 +32,14 @@ touch bot_config.toml - 启动Docker容器: ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d ``` -- 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 - ### 3. 修改配置并重启Docker: -- 请前往 [🎀 新手配置指南](./installation_cute.md) 或 [⚙️ 标准配置指南](./installation_standard.md) 完成 `.env.prod` 与 `bot_config.toml` 配置文件的编写\ +- 请前往 [🎀新手配置指南](./installation_cute.md) 或 [⚙️标准配置指南](./installation_standard.md) 完成 `.env.prod` 与 `bot_config.toml` 配置文件的编写\ **需要注意 `.env.prod` 中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: @@ -43,10 +50,10 @@ docker restart maimbot # 若修改过容器名称则替换maimbot为你自定 - 下方命令可以但不推荐,只是同时重启NapCat、MongoDB、MaiMBot三个服务 ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose restart ``` -- 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 - ### 4. 登入NapCat管理页添加反向WebSocket @@ -57,7 +64,24 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart (若修改过容器名称则替换maimbot为你自定的名称) -### 5. 愉快地和麦麦对话吧! +### 5. 部署完成,愉快地和麦麦对话吧! + + +### 6. 更新镜像与容器 + +- 以更新MaiMBot为例,其他两个容器可以但没必要 + +- 先拉取最新镜像 +```bash +docker pull sengokucola/maimbot:latest +``` + +- 拉取完最新镜像后回到 `docker-compose.yml` 所在工作目录执行以下命令,该指令会自动重建镜像有更新的容器并启动 +```bash +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d +``` ## ⚠️ 注意事项 From d9a28639ac95eeb555f3c7415991742f4bb577a0 Mon Sep 17 00:00:00 2001 From: Yan233_ Date: Tue, 11 Mar 2025 02:15:46 +0800 Subject: [PATCH 062/105] =?UTF-8?q?=E4=BC=98=E5=8C=96Docker=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0=E5=AE=B9=E5=99=A8?= =?UTF-8?q?=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docker_deploy.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index b4d0a3c7d..a5d648885 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -1,14 +1,15 @@ # 🐳 Docker 部署指南 ## 部署步骤 (推荐,但不一定是最新) -"更新镜像与容器"部分在Part 6 + +**"更新镜像与容器"部分在本文档 [Part 6](#6-更新镜像与容器)** ### 0. 前提说明 **本文假设读者已具备一定的 Docker 基础知识。若您对 Docker 不熟悉,建议先参考相关教程或文档进行学习,或选择使用 [📦Linux手动部署指南](./manual_deploy_linux.md) 或 [📦Windows手动部署指南](./manual_deploy_windows.md) 。** -### 1. 获取Docker配置文件: +### 1. 获取Docker配置文件 - 建议先单独创建好一个文件夹并进入,作为工作目录 @@ -20,16 +21,18 @@ wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.y 修改后请注意在之后配置 `.env.prod` 文件时指定MongoDB数据库的用户名密码 -### 2. 启动服务: +### 2. 启动服务 - **!!! 请在第一次启动前确保当前工作目录下 `.env.prod` 与 `bot_config.toml` 文件存在 !!!**\ 由于Docker文件映射行为的特殊性,若宿主机的映射路径不存在,可能导致意外的目录创建,而不会创建文件,由于此处需要文件映射到文件,需提前确保文件存在且路径正确,可使用如下命令: + ```bash touch .env.prod touch bot_config.toml ``` - 启动Docker容器: + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d # 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 @@ -37,17 +40,19 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d ``` -### 3. 修改配置并重启Docker: +### 3. 修改配置并重启Docker - 请前往 [🎀新手配置指南](./installation_cute.md) 或 [⚙️标准配置指南](./installation_standard.md) 完成 `.env.prod` 与 `bot_config.toml` 配置文件的编写\ **需要注意 `.env.prod` 中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: + ```bash docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名臣 ``` - 下方命令可以但不推荐,只是同时重启NapCat、MongoDB、MaiMBot三个服务 + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart # 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 @@ -58,6 +63,7 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose restart ### 4. 登入NapCat管理页添加反向WebSocket - 在浏览器地址栏输入 `http://<宿主机IP>:6099/` 进入NapCat的管理Web页,添加一个Websocket客户端 + > 网络配置 -> 新建 -> Websocket客户端 - Websocket客户端的名称自定,URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ @@ -69,14 +75,14 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose restart ### 6. 更新镜像与容器 -- 以更新MaiMBot为例,其他两个容器可以但没必要 +- 拉取最新镜像 -- 先拉取最新镜像 ```bash -docker pull sengokucola/maimbot:latest +docker-compose pull ``` -- 拉取完最新镜像后回到 `docker-compose.yml` 所在工作目录执行以下命令,该指令会自动重建镜像有更新的容器并启动 +- 执行启动容器指令,该指令会自动重建镜像有更新的容器并启动 + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d # 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 @@ -88,4 +94,4 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d - 目前部署方案仍在测试中,可能存在未知问题 - 配置文件中的API密钥请妥善保管,不要泄露 -- 建议先在测试环境中运行,确认无误后再部署到生产环境 \ No newline at end of file +- 建议先在测试环境中运行,确认无误后再部署到生产环境 \ No newline at end of file From bf9701365f2d68e8c1fdaf083139e58cc8f27118 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Tue, 11 Mar 2025 02:46:13 +0800 Subject: [PATCH 063/105] =?UTF-8?q?feat:=20=E7=B2=BE=E7=AE=80=E6=97=A5?= =?UTF-8?q?=E5=BF=97=EF=BC=8C=E7=A6=81=E7=94=A8Uvicorn/NoneBot=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=97=A5=E5=BF=97=EF=BC=9B=E5=90=AF=E5=8A=A8=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E6=94=B9=E4=B8=BA=E6=98=BE=E7=A4=BA=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?uvicorn=EF=BC=8C=E4=BB=A5=E4=BE=BF=E4=BC=98=E9=9B=85shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index c2ed3dfdf..471a98eaf 100644 --- a/bot.py +++ b/bot.py @@ -1,9 +1,12 @@ +import asyncio import os import shutil import sys import nonebot import time + +import uvicorn from dotenv import load_dotenv from loguru import logger from nonebot.adapters.onebot.v11 import Adapter @@ -12,6 +15,8 @@ import platform # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} +uvicorn_server = None + def easter_egg(): # 彩蛋 @@ -100,10 +105,12 @@ def load_logger(): "#777777>| {name:.<8}:{function:.<8}:{line: >4} - {message}", colorize=True, - level=os.getenv("LOG_LEVEL", "DEBUG") # 根据环境设置日志级别,默认为INFO + level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO + filter=lambda record: "nonebot" not in record["name"] ) + def scan_provider(env_config: dict): provider = {} @@ -138,7 +145,39 @@ def scan_provider(env_config: dict): raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量") -if __name__ == "__main__": +async def graceful_shutdown(): + try: + global uvicorn_server + if uvicorn_server: + uvicorn_server.force_exit = True # 强制退出 + await uvicorn_server.shutdown() + + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + except Exception as e: + logger.error(f"麦麦关闭失败: {e}") + + +async def uvicorn_main(): + global uvicorn_server + config = uvicorn.Config( + app="__main__:app", + host=os.getenv("HOST", "127.0.0.1"), + port=int(os.getenv("PORT", 8080)), + reload=os.getenv("ENVIRONMENT") == "dev", + timeout_graceful_shutdown=5, + log_config=None, + access_log=False + ) + server = uvicorn.Server(config) + uvicorn_server = server + await server.serve() + + +def raw_main(): # 利用 TZ 环境变量设定程序工作的时区 # 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 if platform.system().lower() != 'windows': @@ -165,10 +204,30 @@ if __name__ == "__main__": nonebot.init(**base_config, **env_config) # 注册适配器 + global driver driver = nonebot.get_driver() driver.register_adapter(Adapter) # 加载插件 nonebot.load_plugins("src/plugins") - nonebot.run() + +if __name__ == "__main__": + + try: + raw_main() + + global app + app = nonebot.get_asgi() + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(uvicorn_main()) + except KeyboardInterrupt: + logger.warning("麦麦会努力做的更好的!正在停止中......") + except Exception as e: + logger.error(f"主程序异常: {e}") + finally: + loop.run_until_complete(graceful_shutdown()) + loop.close() + logger.info("进程终止完毕,麦麦开始休眠......下次再见哦!") From ff65ab8d72f4e3abd8279087bbf799c12a463bc1 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 03:14:22 +0800 Subject: [PATCH 064/105] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=9A=84ruff=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=97=B6=E6=B6=88=E9=99=A4config=E7=9A=84?= =?UTF-8?q?=E6=89=80=E6=9C=89=E4=B8=8D=E7=AC=A6=E5=90=88=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E7=9A=84=E5=9C=B0=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml => ruff.toml | 19 ++-- src/plugins/chat/config.py | 177 ++++++++++++++---------------------- 2 files changed, 78 insertions(+), 118 deletions(-) rename pyproject.toml => ruff.toml (95%) diff --git a/pyproject.toml b/ruff.toml similarity index 95% rename from pyproject.toml rename to ruff.toml index 1eff99a97..54231339f 100644 --- a/pyproject.toml +++ b/ruff.toml @@ -1,6 +1,14 @@ -[tool.ruff] include = ["*.py"] + +# 行长度设置 +line-length = 120 + +[lint] fixable = ["ALL"] +unfixable = [] + +# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。 +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # 启用的规则 select = [ @@ -11,16 +19,11 @@ select = [ ignore = ["E711"] -# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。 -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - -# 行长度设置 -line-length = 120 - -[tool.ruff.format] +[format] docstring-code-format = true indent-style = "space" + # 使用双引号表示字符串 quote-style = "double" diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 407624f2a..3888c0b94 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -12,6 +12,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier @dataclass class BotConfig: """机器人配置类""" + INNER_VERSION: Version = None BOT_QQ: Optional[int] = 1 @@ -81,23 +82,25 @@ class BotConfig: PROMPT_PERSONALITY = [ "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", "是一个女大学生,你有黑色头发,你会刷小红书", - "是一个女大学生,你会刷b站,对ACG文化感兴趣" + "是一个女大学生,你会刷b站,对ACG文化感兴趣", ] - PROMPT_SCHEDULE_GEN="一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" - - PERSONALITY_1: float = 0.6 # 第一种人格概率 - PERSONALITY_2: float = 0.3 # 第二种人格概率 - PERSONALITY_3: float = 0.1 # 第三种人格概率 - - memory_ban_words: list = field(default_factory=lambda: ['表情包', '图片', '回复', '聊天记录']) # 添加新的配置项默认值 - + PROMPT_SCHEDULE_GEN = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" + + PERSONALITY_1: float = 0.6 # 第一种人格概率 + PERSONALITY_2: float = 0.3 # 第二种人格概率 + PERSONALITY_3: float = 0.1 # 第三种人格概率 + + memory_ban_words: list = field( + default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] + ) # 添加新的配置项默认值 + @staticmethod def get_config_dir() -> str: """获取配置文件目录""" current_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) - config_dir = os.path.join(root_dir, 'config') + root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) + config_dir = os.path.join(root_dir, "config") if not os.path.exists(config_dir): os.makedirs(config_dir) return config_dir @@ -108,48 +111,45 @@ class BotConfig: Args: value[str]: 版本表达式(字符串) Returns: - SpecifierSet + SpecifierSet """ try: converted = SpecifierSet(value) except InvalidSpecifier: - logger.error( - f"{value} 分类使用了错误的版本约束表达式\n", - "请阅读 https://semver.org/lang/zh-CN/ 修改代码" - ) + logger.error(f"{value} 分类使用了错误的版本约束表达式\n", "请阅读 https://semver.org/lang/zh-CN/ 修改代码") exit(1) return converted @classmethod def get_config_version(cls, toml: dict) -> Version: - """提取配置文件的 SpecifierSet 版本数据 + """提取配置文件的 SpecifierSet 版本数据 Args: toml[dict]: 输入的配置文件字典 Returns: - Version + Version """ - if 'inner' in toml: + if "inner" in toml: try: config_version: str = toml["inner"]["version"] except KeyError as e: logger.error("配置文件中 inner 段 不存在, 这是错误的配置文件") - raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") + raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") from e else: toml["inner"] = {"version": "0.0.0"} config_version = toml["inner"]["version"] try: ver = version.parse(config_version) - except InvalidVersion: + except InvalidVersion as e: logger.error( "配置文件中 inner段 的 version 键是错误的版本描述\n" "请阅读 https://semver.org/lang/zh-CN/ 修改配置,并参考本项目指定的模板进行修改\n" "本项目在不同的版本下有不同的模板,请注意识别" ) - raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") + raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") from e return ver @@ -159,26 +159,26 @@ class BotConfig: config = cls() def personality(parent: dict): - personality_config = parent['personality'] - personality = personality_config.get('prompt_personality') + personality_config = parent["personality"] + personality = personality_config.get("prompt_personality") if len(personality) >= 2: logger.debug(f"载入自定义人格:{personality}") - config.PROMPT_PERSONALITY = personality_config.get('prompt_personality', config.PROMPT_PERSONALITY) + config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY) logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") - config.PROMPT_SCHEDULE_GEN = personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN) + config.PROMPT_SCHEDULE_GEN = personality_config.get("prompt_schedule", config.PROMPT_SCHEDULE_GEN) if config.INNER_VERSION in SpecifierSet(">=0.0.2"): - config.PERSONALITY_1 = personality_config.get('personality_1_probability', config.PERSONALITY_1) - config.PERSONALITY_2 = personality_config.get('personality_2_probability', config.PERSONALITY_2) - config.PERSONALITY_3 = personality_config.get('personality_3_probability', config.PERSONALITY_3) + config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1) + config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2) + config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3) def emoji(parent: dict): emoji_config = parent["emoji"] config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) - config.EMOJI_CHECK_PROMPT = emoji_config.get('check_prompt', config.EMOJI_CHECK_PROMPT) - config.EMOJI_SAVE = emoji_config.get('auto_save', config.EMOJI_SAVE) - config.EMOJI_CHECK = emoji_config.get('enable_check', config.EMOJI_CHECK) + config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT) + config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE) + config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK) def cq_code(parent: dict): cq_code_config = parent["cq_code"] @@ -195,8 +195,9 @@ class BotConfig: response_config = parent["response"] config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY) config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY) - config.MODEL_R1_DISTILL_PROBABILITY = response_config.get("model_r1_distill_probability", - config.MODEL_R1_DISTILL_PROBABILITY) + config.MODEL_R1_DISTILL_PROBABILITY = response_config.get( + "model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY + ) config.max_response_length = response_config.get("max_response_length", config.max_response_length) def model(parent: dict): @@ -213,7 +214,7 @@ class BotConfig: "llm_emotion_judge", "vlm", "embedding", - "moderation" + "moderation", ] for item in config_list: @@ -222,13 +223,7 @@ class BotConfig: # base_url 的例子: SILICONFLOW_BASE_URL # key 的例子: SILICONFLOW_KEY - cfg_target = { - "name": "", - "base_url": "", - "key": "", - "pri_in": 0, - "pri_out": 0 - } + cfg_target = {"name": "", "base_url": "", "key": "", "pri_in": 0, "pri_out": 0} if config.INNER_VERSION in SpecifierSet("<=0.0.0"): cfg_target = cfg_item @@ -247,7 +242,7 @@ class BotConfig: cfg_target[i] = cfg_item[i] except KeyError as e: logger.error(f"{item} 中的必要字段不存在,请检查") - raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") + raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") from e provider = cfg_item.get("provider") if provider is None: @@ -272,17 +267,19 @@ class BotConfig: if config.INNER_VERSION in SpecifierSet(">=0.0.2"): config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout) - config.response_willing_amplifier = msg_config.get("response_willing_amplifier", - config.response_willing_amplifier) - config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", - config.response_interested_rate_amplifier) + config.response_willing_amplifier = msg_config.get( + "response_willing_amplifier", config.response_willing_amplifier + ) + config.response_interested_rate_amplifier = msg_config.get( + "response_interested_rate_amplifier", config.response_interested_rate_amplifier + ) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) def memory(parent: dict): memory_config = parent["memory"] config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) config.forget_memory_interval = memory_config.get("forget_memory_interval", config.forget_memory_interval) - + # 在版本 >= 0.0.4 时才处理新增的配置项 if config.INNER_VERSION in SpecifierSet(">=0.0.4"): config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) @@ -303,10 +300,12 @@ class BotConfig: config.chinese_typo_enable = chinese_typo_config.get("enable", config.chinese_typo_enable) config.chinese_typo_error_rate = chinese_typo_config.get("error_rate", config.chinese_typo_error_rate) config.chinese_typo_min_freq = chinese_typo_config.get("min_freq", config.chinese_typo_min_freq) - config.chinese_typo_tone_error_rate = chinese_typo_config.get("tone_error_rate", - config.chinese_typo_tone_error_rate) - config.chinese_typo_word_replace_rate = chinese_typo_config.get("word_replace_rate", - config.chinese_typo_word_replace_rate) + config.chinese_typo_tone_error_rate = chinese_typo_config.get( + "tone_error_rate", config.chinese_typo_tone_error_rate + ) + config.chinese_typo_word_replace_rate = chinese_typo_config.get( + "word_replace_rate", config.chinese_typo_word_replace_rate + ) def groups(parent: dict): groups_config = parent["groups"] @@ -325,61 +324,19 @@ class BotConfig: # 例如:"notice": "personality 将在 1.3.2 后被移除",那么在有效版本中的用户就会虽然可以 # 正常执行程序,但是会看到这条自定义提示 include_configs = { - "personality": { - "func": personality, - "support": ">=0.0.0" - }, - "emoji": { - "func": emoji, - "support": ">=0.0.0" - }, - "cq_code": { - "func": cq_code, - "support": ">=0.0.0" - }, - "bot": { - "func": bot, - "support": ">=0.0.0" - }, - "response": { - "func": response, - "support": ">=0.0.0" - }, - "model": { - "func": model, - "support": ">=0.0.0" - }, - "message": { - "func": message, - "support": ">=0.0.0" - }, - "memory": { - "func": memory, - "support": ">=0.0.0", - "necessary": False - }, - "mood": { - "func": mood, - "support": ">=0.0.0" - }, - "keywords_reaction": { - "func": keywords_reaction, - "support": ">=0.0.2", - "necessary": False - }, - "chinese_typo": { - "func": chinese_typo, - "support": ">=0.0.3", - "necessary": False - }, - "groups": { - "func": groups, - "support": ">=0.0.0" - }, - "others": { - "func": others, - "support": ">=0.0.0" - } + "personality": {"func": personality, "support": ">=0.0.0"}, + "emoji": {"func": emoji, "support": ">=0.0.0"}, + "cq_code": {"func": cq_code, "support": ">=0.0.0"}, + "bot": {"func": bot, "support": ">=0.0.0"}, + "response": {"func": response, "support": ">=0.0.0"}, + "model": {"func": model, "support": ">=0.0.0"}, + "message": {"func": message, "support": ">=0.0.0"}, + "memory": {"func": memory, "support": ">=0.0.0", "necessary": False}, + "mood": {"func": mood, "support": ">=0.0.0"}, + "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, + "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, + "groups": {"func": groups, "support": ">=0.0.0"}, + "others": {"func": others, "support": ">=0.0.0"}, } # 原地修改,将 字符串版本表达式 转换成 版本对象 @@ -391,7 +348,7 @@ class BotConfig: with open(config_path, "rb") as f: try: toml_dict = tomli.load(f) - except(tomli.TOMLDecodeError) as e: + except tomli.TOMLDecodeError as e: logger.critical(f"配置文件bot_config.toml填写有误,请检查第{e.lineno}行第{e.colno}处:{e.msg}") exit(1) @@ -406,7 +363,7 @@ class BotConfig: # 检查配置文件版本是否在支持范围内 if config.INNER_VERSION in group_specifierset: # 如果版本在支持范围内,检查是否存在通知 - if 'notice' in include_configs[key]: + if "notice" in include_configs[key]: logger.warning(include_configs[key]["notice"]) include_configs[key]["func"](toml_dict) @@ -420,7 +377,7 @@ class BotConfig: raise InvalidVersion(f"当前程序仅支持以下版本范围: {group_specifierset}") # 如果 necessary 项目存在,而且显式声明是 False,进入特殊处理 - elif "necessary" in include_configs[key] and include_configs[key].get("necessary") == False: + elif "necessary" in include_configs[key] and include_configs[key].get("necessary") is False: # 通过 pass 处理的项虽然直接忽略也是可以的,但是为了不增加理解困难,依然需要在这里显式处理 if key == "keywords_reaction": pass From c789074c5da0bfc121bf402671d73840403b8dfa Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 03:15:01 +0800 Subject: [PATCH 065/105] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0ruff=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | Bin 600 -> 612 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4f969682f9b0ebbbd9aebe4fe94758da052d23b4..0acaade5e6d465465ee87009b5c28551e591953d 100644 GIT binary patch delta 20 acmcb?@`PnW1QQQ00~bRPLn%WV5CQ-^9t5QT delta 7 OcmaFDa)V_<1QP%aWdhFt From 0f99d6a5b2c10c034ca964440d3e089c2d76b149 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 03:34:45 +0800 Subject: [PATCH 066/105] Update docs/docker_deploy.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- docs/docker_deploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index a5d648885..db759dfd0 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -48,7 +48,7 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d - 重启Docker容器: ```bash -docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名臣 +docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名称 ``` - 下方命令可以但不推荐,只是同时重启NapCat、MongoDB、MaiMBot三个服务 From 00e02edc73c46d3bcb250b3bcdc6b063447e0f08 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 04:00:23 +0800 Subject: [PATCH 067/105] =?UTF-8?q?fix:=200.0.5=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E5=A2=9E=E5=8A=A0=E5=88=86=E5=B1=82=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index bc69ffd4e..c088e1a76 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -17,7 +17,7 @@ class BotConfig: BOT_QQ: Optional[int] = 1 BOT_NICKNAME: Optional[str] = None - BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它 + BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它 # 消息处理相关配置 MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 @@ -191,7 +191,9 @@ class BotConfig: bot_qq = bot_config.get("qq") config.BOT_QQ = int(bot_qq) config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME) - config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES) + + if config.INNER_VERSION in SpecifierSet(">=0.0.5"): + config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES) def response(parent: dict): response_config = parent["response"] From eede406e55f1b25130ce65d37d9c08018166fcf8 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 04:00:39 +0800 Subject: [PATCH 068/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dnonebot?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=8A=A0=E8=BD=BD=E9=A1=B9=E7=9B=AE=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruff.toml => pyproject.toml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) rename ruff.toml => pyproject.toml (77%) diff --git a/ruff.toml b/pyproject.toml similarity index 77% rename from ruff.toml rename to pyproject.toml index 54231339f..0a4805744 100644 --- a/ruff.toml +++ b/pyproject.toml @@ -1,9 +1,20 @@ +[project] +name = "MaiMaiBot" +version = "0.1.0" +description = "MaiMaiBot" + +[tool.nonebot] +plugins = ["src.plugins.chat"] +plugin_dirs = ["src/plugins"] + +[tool.ruff] + include = ["*.py"] # 行长度设置 line-length = 120 -[lint] +[tool.ruff.lint] fixable = ["ALL"] unfixable = [] @@ -19,7 +30,7 @@ select = [ ignore = ["E711"] -[format] +[tool.ruff.format] docstring-code-format = true indent-style = "space" From 9e41c4f4c5c7ea90297507d9670c38f0abf04e97 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 04:07:56 +0800 Subject: [PATCH 069/105] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20bot=5Fconf?= =?UTF-8?q?ig=200.0.5=20=E7=89=88=E6=9C=AC=E7=9A=84=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog_config.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog_config.md b/changelog_config.md index 7101fe828..c4c560644 100644 --- a/changelog_config.md +++ b/changelog_config.md @@ -1,6 +1,12 @@ # Changelog +## [0.0.5] - 2025-3-11 +### Added +- 新增了 `alias_names` 配置项,用于指定麦麦的别名。 + ## [0.0.4] - 2025-3-9 ### Added - 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 + + From 8cbf9bb0483630dc73469232637e4dea667cc9e1 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 04:42:24 +0800 Subject: [PATCH 070/105] =?UTF-8?q?feat:=20=E5=8F=B2=E4=B8=8A=E6=9C=80?= =?UTF-8?q?=E5=A5=BD=E7=9A=84=E6=B6=88=E6=81=AF=E6=B5=81=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=92=8C=E5=9B=BE=E7=89=87=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 3 + src/plugins/chat/bot.py | 28 +++++-- src/plugins/chat/chat_stream.py | 30 +++++--- src/plugins/chat/cq_code.py | 44 ++++++++--- src/plugins/chat/emoji_manager.py | 10 +-- src/plugins/chat/llm_generator.py | 6 +- src/plugins/chat/message.py | 31 +++++--- src/plugins/chat/message_base.py | 83 ++++++++++----------- src/plugins/chat/message_cq.py | 54 +++++--------- src/plugins/chat/message_sender.py | 34 ++++----- src/plugins/chat/relationship_manager.py | 12 +-- src/plugins/chat/utils_image.py | 93 +++--------------------- src/plugins/models/utils_model.py | 79 +++++++++++++++++++- 13 files changed, 272 insertions(+), 235 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 0bffaed19..a62343d0c 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -18,6 +18,7 @@ from .config import global_config from .emoji_manager import emoji_manager from .relationship_manager import relationship_manager from .willing_manager import willing_manager +from .chat_stream import chat_manager # 创建LLM统计实例 llm_stats = LLMStatistics("llm_statistics.txt") @@ -101,6 +102,8 @@ async def _(bot: Bot): asyncio.create_task(emoji_manager._periodic_scan(interval_MINS=global_config.EMOJI_REGISTER_INTERVAL)) print("\033[1;38;5;208m-----------开始偷表情包!-----------\033[0m") + asyncio.create_task(chat_manager._initialize()) + asyncio.create_task(chat_manager._auto_save_task()) @group_msg.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a2fdab873..a5f4ac476 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -18,7 +18,7 @@ from .chat_stream import chat_manager from .message_sender import message_manager # 导入新的消息管理器 from .relationship_manager import relationship_manager from .storage import MessageStorage -from .utils import calculate_typing_time, is_mentioned_bot_in_txt +from .utils import calculate_typing_time, is_mentioned_bot_in_message from .utils_image import image_path_to_base64 from .willing_manager import willing_manager # 导入意愿管理器 from .message_base import UserInfo, GroupInfo, Seg @@ -45,8 +45,8 @@ class ChatBot: self.bot = bot # 更新 bot 实例 - group_info = await bot.get_group_info(group_id=event.group_id) - sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) + # group_info = await bot.get_group_info(group_id=event.group_id) + # sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) # 白名单设定由nontbot侧完成 if event.group_id: @@ -54,19 +54,32 @@ class ChatBot: return if event.user_id in global_config.ban_user_id: return + + user_info=UserInfo( + user_id=event.user_id, + user_nickname=event.sender.nickname, + user_cardname=event.sender.card or None, + platform='qq' + ) + + group_info=GroupInfo( + group_id=event.group_id, + group_name=None, + platform='qq' + ) message_cq=MessageRecvCQ( message_id=event.message_id, - user_id=event.user_id, + user_info=user_info, raw_message=str(event.original_message), - group_id=event.group_id, + group_info=group_info, reply_message=event.reply, platform='qq' ) message_json=message_cq.to_dict() # 进入maimbot - message=MessageRecv(**message_json) + message=MessageRecv(message_json) groupinfo=message.message_info.group_info userinfo=message.message_info.user_info @@ -75,6 +88,7 @@ class ChatBot: # 消息过滤,涉及到config有待更新 chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) + message.update_chat_stream(chat) await relationship_manager.update_relationship(chat_stream=chat,) await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value = 0.5) @@ -99,7 +113,7 @@ class ChatBot: await self.storage.store_message(message,chat, topic[0] if topic else None) - is_mentioned = is_mentioned_bot_in_txt(message.processed_plain_text) + is_mentioned = is_mentioned_bot_in_message(message) reply_probability = await willing_manager.change_reply_willing_received( chat_stream=chat, topic=topic[0] if topic else None, diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index 36c97bed0..bee679173 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -1,6 +1,7 @@ import asyncio import hashlib import time +import copy from typing import Dict, Optional from loguru import logger @@ -86,9 +87,9 @@ class ChatManager: self._ensure_collection() self._initialized = True # 在事件循环中启动初始化 - asyncio.create_task(self._initialize()) - # 启动自动保存任务 - asyncio.create_task(self._auto_save_task()) + # asyncio.create_task(self._initialize()) + # # 启动自动保存任务 + # asyncio.create_task(self._auto_save_task()) async def _initialize(self): """异步初始化""" @@ -122,12 +123,18 @@ class ChatManager: self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None ) -> str: """生成聊天流唯一ID""" - # 组合关键信息 - components = [ - platform, - str(user_info.user_id), - str(group_info.group_id) if group_info else "private", - ] + if group_info: + # 组合关键信息 + components = [ + platform, + str(group_info.group_id) + ] + else: + components = [ + platform, + str(user_info.user_id), + "private" + ] # 使用MD5生成唯一ID key = "_".join(components) @@ -153,10 +160,11 @@ class ChatManager: if stream_id in self.streams: stream = self.streams[stream_id] # 更新用户信息和群组信息 + stream.update_active_time() + stream=copy.deepcopy(stream) stream.user_info = user_info if group_info: stream.group_info = group_info - stream.update_active_time() return stream # 检查数据库中是否存在 @@ -180,7 +188,7 @@ class ChatManager: # 保存到内存和数据库 self.streams[stream_id] = stream await self._save_stream(stream) - return stream + return copy.deepcopy(stream) def get_stream(self, stream_id: str) -> Optional[ChatStream]: """通过stream_id获取聊天流""" diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index 7581f8a33..6030b893f 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -59,6 +59,7 @@ class CQCode: params: Dict[str, str] group_id: int user_id: int + user_nickname: str group_name: str = "" user_nickname: str = "" translated_segments: Optional[Union[Seg, List[Seg]]] = None @@ -68,9 +69,7 @@ class CQCode: def __post_init__(self): """初始化LLM实例""" - self._llm = LLM_request( - model=global_config.vlm, temperature=0.4, max_tokens=300 - ) + pass def translate(self): """根据CQ码类型进行相应的翻译处理,转换为Seg对象""" @@ -225,8 +224,7 @@ class CQCode: group_id=msg.get("group_id", 0), ) content_seg = Seg( - type="seglist", data=message_obj.message_segments - ) + type="seglist", data=message_obj.message_segment ) else: content_seg = Seg(type="text", data="[空消息]") else: @@ -241,7 +239,7 @@ class CQCode: group_id=msg.get("group_id", 0), ) content_seg = Seg( - type="seglist", data=message_obj.message_segments + type="seglist", data=message_obj.message_segment ) else: content_seg = Seg(type="text", data="[空消息]") @@ -272,7 +270,7 @@ class CQCode: ) segments = [] - if message_obj.user_id == global_config.BOT_QQ: + if message_obj.message_info.user_info.user_id == global_config.BOT_QQ: segments.append( Seg( type="text", data=f"[回复 {global_config.BOT_NICKNAME} 的消息: " @@ -286,7 +284,7 @@ class CQCode: ) ) - segments.append(Seg(type="seglist", data=message_obj.message_segments)) + segments.append(Seg(type="seglist", data=message_obj.message_segment)) segments.append(Seg(type="text", data="]")) return segments else: @@ -305,12 +303,13 @@ class CQCode: class CQCode_tool: @staticmethod - def cq_from_dict_to_class(cq_code: Dict, reply: Optional[Dict] = None) -> CQCode: + def cq_from_dict_to_class(cq_code: Dict,msg ,reply: Optional[Dict] = None) -> CQCode: """ 将CQ码字典转换为CQCode对象 Args: cq_code: CQ码字典 + msg: MessageCQ对象 reply: 回复消息的字典(可选) Returns: @@ -326,7 +325,13 @@ class CQCode_tool: params = cq_code.get("data", {}) instance = CQCode( - type=cq_type, params=params, group_id=0, user_id=0, reply_message=reply + type=cq_type, + params=params, + group_id=msg.message_info.group_info.group_id, + user_id=msg.message_info.user_info.user_id, + user_nickname=msg.message_info.user_info.user_nickname, + group_name=msg.message_info.group_info.group_name, + reply_message=reply ) # 进行翻译处理 @@ -383,6 +388,25 @@ class CQCode_tool: ) # 生成CQ码,设置sub_type=1表示这是表情包 return f"[CQ:image,file=base64://{escaped_base64},sub_type=1]" + + @staticmethod + def create_image_cq_base64(base64_data: str) -> str: + """ + 创建表情包CQ码 + Args: + base64_data: base64编码的表情包数据 + Returns: + 表情包CQ码字符串 + """ + # 转义base64数据 + escaped_base64 = ( + base64_data.replace("&", "&") + .replace("[", "[") + .replace("]", "]") + .replace(",", ",") + ) + # 生成CQ码,设置sub_type=1表示这是表情包 + return f"[CQ:image,file=base64://{escaped_base64},sub_type=0]" cq_code_tool = CQCode_tool() diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 837ee245d..f3728ce92 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -239,7 +239,7 @@ class EmojiManager: # 即使表情包已存在,也检查是否需要同步到images集合 description = existing_emoji.get('discription') # 检查是否在images集合中存在 - existing_image = await image_manager.db.db.images.find_one({'hash': image_hash}) + existing_image = image_manager.db.db.images.find_one({'hash': image_hash}) if not existing_image: # 同步到images集合 image_doc = { @@ -249,7 +249,7 @@ class EmojiManager: 'description': description, 'timestamp': int(time.time()) } - await image_manager.db.db.images.update_one( + image_manager.db.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True @@ -260,7 +260,7 @@ class EmojiManager: continue # 检查是否在images集合中已有描述 - existing_description = await image_manager._get_description_from_db(image_hash, 'emoji') + existing_description = image_manager._get_description_from_db(image_hash, 'emoji') if existing_description: description = existing_description @@ -302,13 +302,13 @@ class EmojiManager: 'description': description, 'timestamp': int(time.time()) } - await image_manager.db.db.images.update_one( + image_manager.db.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True ) # 保存描述到image_descriptions集合 - await image_manager._save_description_to_db(image_hash, description, 'emoji') + image_manager._save_description_to_db(image_hash, description, 'emoji') logger.success(f"同步保存到images集合: {filename}") else: logger.warning(f"跳过表情包: {filename}") diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index dc019038e..bfd5eec2e 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -7,7 +7,7 @@ from nonebot import get_driver from ...common.database import Database from ..models.utils_model import LLM_request from .config import global_config -from .message import MessageRecv, MessageThinking, MessageSending +from .message import MessageRecv, MessageThinking, MessageSending,Message from .prompt_builder import prompt_builder from .relationship_manager import relationship_manager from .utils import process_llm_response @@ -144,7 +144,7 @@ class ResponseGenerator: # content: str, content_check: str, reasoning_content: str, reasoning_content_check: str): def _save_to_db( self, - message: Message, + message: MessageRecv, sender_name: str, prompt: str, prompt_check: str, @@ -155,7 +155,7 @@ class ResponseGenerator: self.db.db.reasoning_logs.insert_one( { "time": time.time(), - "group_id": message.group_id, + "chat_id": message.chat_stream.stream_id, "user": sender_name, "message": message.processed_plain_text, "model": self.current_model_type, diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 408937fad..5eb93d700 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -7,7 +7,7 @@ from loguru import logger from .utils_image import image_manager from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase -from .chat_stream import ChatStream +from .chat_stream import ChatStream, chat_manager # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -25,8 +25,8 @@ class MessageRecv(MessageBase): Args: message_dict: MessageCQ序列化后的字典 """ - message_info = BaseMessageInfo(**message_dict.get('message_info', {})) - message_segment = Seg(**message_dict.get('message_segment', {})) + message_info = BaseMessageInfo.from_dict(message_dict.get('message_info', {})) + message_segment = Seg.from_dict(message_dict.get('message_segment', {})) raw_message = message_dict.get('raw_message') super().__init__( @@ -39,7 +39,9 @@ class MessageRecv(MessageBase): self.processed_plain_text = "" # 初始化为空字符串 self.detailed_plain_text = "" # 初始化为空字符串 self.is_emoji=False - + def update_chat_stream(self,chat_stream:ChatStream): + self.chat_stream=chat_stream + async def process(self) -> None: """处理消息内容,生成纯文本和详细文本 @@ -83,12 +85,12 @@ class MessageRecv(MessageBase): return seg.data elif seg.type == 'image': # 如果是base64图片数据 - if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) return '[图片]' elif seg.type == 'emoji': self.is_emoji=True - if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) return '[表情]' else: @@ -217,11 +219,11 @@ class MessageProcessBase(Message): return seg.data elif seg.type == 'image': # 如果是base64图片数据 - if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) return '[图片]' elif seg.type == 'emoji': - if isinstance(seg.data, str) and seg.data.startswith(('data:', 'base64:')): + if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) return '[表情]' elif seg.type == 'at': @@ -296,10 +298,15 @@ class MessageSending(MessageProcessBase): self.reply_to_message_id = reply.message_info.message_id if reply else None self.is_head = is_head self.is_emoji = is_emoji - if is_head: + + def set_reply(self, reply: Optional['MessageRecv']) -> None: + """设置回复消息""" + if reply: + self.reply = reply + self.reply_to_message_id = self.reply.message_info.message_id self.message_segment = Seg(type='seglist', data=[ - Seg(type='reply', data=reply.message_info.message_id), - self.message_segment + Seg(type='reply', data=reply.message_info.message_id), + self.message_segment ]) async def process(self) -> None: @@ -329,7 +336,7 @@ class MessageSending(MessageProcessBase): def to_dict(self): ret= super().to_dict() - ret['mesage_info']['user_info']=self.chat_stream.user_info.to_dict() + ret['message_info']['user_info']=self.chat_stream.user_info.to_dict() return ret @dataclass diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py index 7b76403de..d17c2c357 100644 --- a/src/plugins/chat/message_base.py +++ b/src/plugins/chat/message_base.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, asdict from typing import List, Optional, Union, Any, Dict @dataclass -class Seg(dict): +class Seg: """消息片段类,用于表示消息的不同部分 Attributes: @@ -15,47 +15,25 @@ class Seg(dict): """ type: str data: Union[str, List['Seg']] - translated_data: Optional[str] = None + - def __init__(self, type: str, data: Union[str, List['Seg']], translated_data: Optional[str] = None): - """初始化实例,确保字典和属性同步""" - # 先初始化字典 - super().__init__(type=type, data=data) - if translated_data is not None: - self['translated_data'] = translated_data - - # 再初始化属性 - object.__setattr__(self, 'type', type) - object.__setattr__(self, 'data', data) - object.__setattr__(self, 'translated_data', translated_data) + # def __init__(self, type: str, data: Union[str, List['Seg']],): + # """初始化实例,确保字典和属性同步""" + # # 先初始化字典 + # self.type = type + # self.data = data - # 验证数据类型 - self._validate_data() - - def _validate_data(self) -> None: - """验证数据类型的正确性""" - if self.type == 'seglist' and not isinstance(self.data, list): - raise ValueError("seglist类型的data必须是列表") - elif self.type == 'text' and not isinstance(self.data, str): - raise ValueError("text类型的data必须是字符串") - elif self.type == 'image' and not isinstance(self.data, str): - raise ValueError("image类型的data必须是字符串") - - def __setattr__(self, name: str, value: Any) -> None: - """重写属性设置,同时更新字典值""" - # 更新属性 - object.__setattr__(self, name, value) - # 同步更新字典 - if name in ['type', 'data', 'translated_data']: - self[name] = value - - def __setitem__(self, key: str, value: Any) -> None: - """重写字典值设置,同时更新属性""" - # 更新字典 - super().__setitem__(key, value) - # 同步更新属性 - if key in ['type', 'data', 'translated_data']: - object.__setattr__(self, key, value) + @classmethod + def from_dict(cls, data: Dict) -> 'Seg': + """从字典创建Seg实例""" + type=data.get('type') + data=data.get('data') + if type == 'seglist': + data = [Seg.from_dict(seg) for seg in data] + return cls( + type=type, + data=data + ) def to_dict(self) -> Dict: """转换为字典格式""" @@ -64,8 +42,6 @@ class Seg(dict): result['data'] = [seg.to_dict() for seg in self.data] else: result['data'] = self.data - if self.translated_data is not None: - result['translated_data'] = self.translated_data return result @dataclass @@ -79,6 +55,7 @@ class GroupInfo: """转换为字典格式""" return {k: v for k, v in asdict(self).items() if v is not None} + @classmethod def from_dict(cls, data: Dict) -> 'GroupInfo': """从字典创建GroupInfo实例 @@ -106,6 +83,7 @@ class UserInfo: """转换为字典格式""" return {k: v for k, v in asdict(self).items() if v is not None} + @classmethod def from_dict(cls, data: Dict) -> 'UserInfo': """从字典创建UserInfo实例 @@ -126,7 +104,7 @@ class UserInfo: class BaseMessageInfo: """消息信息类""" platform: Optional[str] = None - message_id: Optional[int,str] = None + message_id: Union[str,int,None] = None time: Optional[int] = None group_info: Optional[GroupInfo] = None user_info: Optional[UserInfo] = None @@ -141,6 +119,25 @@ class BaseMessageInfo: else: result[field] = value return result + @classmethod + def from_dict(cls, data: Dict) -> 'BaseMessageInfo': + """从字典创建BaseMessageInfo实例 + + Args: + data: 包含必要字段的字典 + + Returns: + BaseMessageInfo: 新的实例 + """ + group_info = GroupInfo(**data.get('group_info', {})) + user_info = UserInfo(**data.get('user_info', {})) + return cls( + platform=data.get('platform'), + message_id=data.get('message_id'), + time=data.get('time'), + group_info=group_info, + user_info=user_info + ) @dataclass class MessageBase: diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 4d7489bbf..6bfa47c3f 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -27,27 +27,10 @@ class MessageCQ(MessageBase): def __init__( self, message_id: int, - user_id: int, - group_id: Optional[int] = None, + user_info: UserInfo, + group_info: Optional[GroupInfo] = None, platform: str = "qq" ): - # 构造用户信息 - user_info = UserInfo( - platform=platform, - user_id=user_id, - user_nickname=get_user_nickname(user_id), - user_cardname=get_user_cardname(user_id) if group_id else None - ) - - # 构造群组信息(如果有) - group_info = None - if group_id: - group_info = GroupInfo( - platform=platform, - group_id=group_id, - group_name=get_groupname(group_id) - ) - # 构造基础消息信息 message_info = BaseMessageInfo( platform=platform, @@ -56,7 +39,6 @@ class MessageCQ(MessageBase): group_info=group_info, user_info=user_info ) - # 调用父类初始化,message_segment 由子类设置 super().__init__( message_info=message_info, @@ -71,14 +53,17 @@ class MessageRecvCQ(MessageCQ): def __init__( self, message_id: int, - user_id: int, + user_info: UserInfo, raw_message: str, - group_id: Optional[int] = None, + group_info: Optional[GroupInfo] = None, + platform: str = "qq", reply_message: Optional[Dict] = None, - platform: str = "qq" ): # 调用父类初始化 - super().__init__(message_id, user_id, group_id, platform) + super().__init__(message_id, user_info, group_info, platform) + + if group_info.group_name is None: + group_info.group_name = get_groupname(group_info.group_id) # 解析消息段 self.message_segment = self._parse_message(raw_message, reply_message) @@ -117,7 +102,7 @@ class MessageRecvCQ(MessageCQ): # 转换CQ码为Seg对象 for code_item in cq_code_dict_list: - message_obj = cq_code_tool.cq_from_dict_to_class(code_item, reply=reply_message) + message_obj = cq_code_tool.cq_from_dict_to_class(code_item,msg=self,reply=reply_message) if message_obj.translated_segments: segments.append(message_obj.translated_segments) @@ -142,13 +127,14 @@ class MessageSendCQ(MessageCQ): data: Dict ): # 调用父类初始化 - message_info = BaseMessageInfo(**data.get('message_info', {})) - message_segment = Seg(**data.get('message_segment', {})) + message_info = BaseMessageInfo.from_dict(data.get('message_info', {})) + message_segment = Seg.from_dict(data.get('message_segment', {})) super().__init__( message_info.message_id, - message_info.user_info.user_id, - message_info.group_info.group_id if message_info.group_info else None, - message_info.platform) + message_info.user_info, + message_info.group_info if message_info.group_info else None, + message_info.platform + ) self.message_segment = message_segment self.raw_message = self._generate_raw_message() @@ -171,11 +157,9 @@ class MessageSendCQ(MessageCQ): if seg.type == 'text': return str(seg.data) elif seg.type == 'image': - # 如果是base64图片数据 - if seg.data.startswith(('data:', 'base64:')): - return cq_code_tool.create_emoji_cq_base64(seg.data) - # 如果是表情包(本地文件) - return cq_code_tool.create_emoji_cq(seg.data) + return cq_code_tool.create_image_cq_base64(seg.data) + elif seg.type == 'emoji': + return cq_code_tool.create_emoji_cq_base64(seg.data) elif seg.type == 'at': return f"[CQ:at,qq={seg.data}]" elif seg.type == 'reply': diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index ed91b614e..2c3880bb8 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -41,10 +41,10 @@ class Message_Sender: message=message_send.raw_message, auto_escape=False ) - print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + print(f"\033[1;34m[调试]\033[0m 发送消息{message.processed_plain_text}成功") except Exception as e: print(f"发生错误 {e}") - print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + print(f"\033[1;34m[调试]\033[0m 发送消息{message.processed_plain_text}失败") else: try: await self._current_bot.send_private_msg( @@ -52,10 +52,10 @@ class Message_Sender: message=message_send.raw_message, auto_escape=False ) - print(f"\033[1;34m[调试]\033[0m 发送消息{message}成功") + print(f"\033[1;34m[调试]\033[0m 发送消息{message.processed_plain_text}成功") except Exception as e: print(f"发生错误 {e}") - print(f"\033[1;34m[调试]\033[0m 发送消息{message}失败") + print(f"\033[1;34m[调试]\033[0m 发送消息{message.processed_plain_text}失败") class MessageContainer: @@ -137,11 +137,7 @@ class MessageManager: return self.containers[chat_id] def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None: - chat_stream = chat_manager.get_stream_by_info( - platform=message.message_info.platform, - user_info=message.message_info.user_info, - group_info=message.message_info.group_info - ) + chat_stream = message.chat_stream if not chat_stream: raise ValueError("无法找到对应的聊天流") container = self.get_container(chat_stream.stream_id) @@ -165,13 +161,14 @@ class MessageManager: else: print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") if message_earliest.is_head and message_earliest.update_thinking_time() > 30: - await message_sender.send_message(message_earliest) + await message_sender.send_message(message_earliest.set_reply()) else: await message_sender.send_message(message_earliest) - if message_earliest.is_emoji: - message_earliest.processed_plain_text = "[表情包]" - await self.storage.store_message(message_earliest, None) + # if message_earliest.is_emoji: + # message_earliest.processed_plain_text = "[表情包]" + await message_earliest.process() + await self.storage.store_message(message_earliest, message_earliest.chat_stream,None) container.remove_message(message_earliest) @@ -184,13 +181,14 @@ class MessageManager: try: if msg.is_head and msg.update_thinking_time() > 30: - await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False, reply_message_id=msg.reply_message_id) + await message_sender.send_message(msg.set_reply()) else: - await message_sender.send_group_message(chat_id, msg.processed_plain_text, auto_escape=False) + await message_sender.send_message(msg) - if msg.is_emoji: - msg.processed_plain_text = "[表情包]" - await self.storage.store_message(msg, None) + # if msg.is_emoji: + # msg.processed_plain_text = "[表情包]" + await msg.process() + await self.storage.store_message(msg,msg.chat_stream, None) if not container.remove_message(msg): print("\033[1;33m[警告]\033[0m 尝试删除不存在的消息") diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index c08b962ed..5552aee8c 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -23,12 +23,12 @@ class Relationship: saved = False def __init__(self, chat:ChatStream=None,data:dict=None): - self.user_id=chat.user_info.user_id if chat.user_info else data.get('user_id',0) - self.platform=chat.platform if chat.user_info else data.get('platform','') - self.nickname=chat.user_info.user_nickname if chat.user_info else data.get('nickname','') - self.relationship_value=data.get('relationship_value',0) - self.age=data.get('age',0) - self.gender=data.get('gender','') + self.user_id=chat.user_info.user_id if chat else data.get('user_id',0) + self.platform=chat.platform if chat else data.get('platform','') + self.nickname=chat.user_info.user_nickname if chat else data.get('nickname','') + self.relationship_value=data.get('relationship_value',0) if data else 0 + self.age=data.get('age',0) if data else 0 + self.gender=data.get('gender','') if data else '' class RelationshipManager: diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index aba09714c..ac3ff5ac4 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -59,7 +59,7 @@ class ImageManager: self.db.db.image_descriptions.create_index([('hash', 1)], unique=True) self.db.db.image_descriptions.create_index([('type', 1)]) - async def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]: + def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]: """从数据库获取图片描述 Args: @@ -69,13 +69,13 @@ class ImageManager: Returns: Optional[str]: 描述文本,如果不存在则返回None """ - result = await self.db.db.image_descriptions.find_one({ + result= self.db.db.image_descriptions.find_one({ 'hash': image_hash, 'type': description_type }) return result['description'] if result else None - async def _save_description_to_db(self, image_hash: str, description: str, description_type: str) -> None: + def _save_description_to_db(self, image_hash: str, description: str, description_type: str) -> None: """保存图片描述到数据库 Args: @@ -83,7 +83,7 @@ class ImageManager: description: 描述文本 description_type: 描述类型 ('emoji' 或 'image') """ - await self.db.db.image_descriptions.update_one( + self.db.db.image_descriptions.update_one( {'hash': image_hash, 'type': description_type}, { '$set': { @@ -253,8 +253,9 @@ class ImageManager: image_hash = hashlib.md5(image_bytes).hexdigest() # 查询缓存的描述 - cached_description = await self._get_description_from_db(image_hash, 'emoji') + cached_description = self._get_description_from_db(image_hash, 'emoji') if cached_description: + logger.info(f"缓存表情包描述: {cached_description}") return f"[表情包:{cached_description}]" # 调用AI获取描述 @@ -281,7 +282,7 @@ class ImageManager: 'description': description, 'timestamp': timestamp } - await self.db.db.images.update_one( + self.db.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True @@ -291,7 +292,7 @@ class ImageManager: logger.error(f"保存表情包文件失败: {str(e)}") # 保存描述到数据库 - await self._save_description_to_db(image_hash, description, 'emoji') + self._save_description_to_db(image_hash, description, 'emoji') return f"[表情包:{description}]" except Exception as e: @@ -306,7 +307,7 @@ class ImageManager: image_hash = hashlib.md5(image_bytes).hexdigest() # 查询缓存的描述 - cached_description = await self._get_description_from_db(image_hash, 'image') + cached_description = self._get_description_from_db(image_hash, 'image') if cached_description: return f"[图片:{cached_description}]" @@ -334,7 +335,7 @@ class ImageManager: 'description': description, 'timestamp': timestamp } - await self.db.db.images.update_one( + self.db.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True @@ -357,80 +358,6 @@ class ImageManager: image_manager = ImageManager() -def compress_base64_image_by_scale(base64_data: str, target_size: int = 0.8 * 1024 * 1024) -> str: - """压缩base64格式的图片到指定大小 - Args: - base64_data: base64编码的图片数据 - target_size: 目标文件大小(字节),默认0.8MB - Returns: - str: 压缩后的base64图片数据 - """ - try: - # 将base64转换为字节数据 - image_data = base64.b64decode(base64_data) - - # 如果已经小于目标大小,直接返回原图 - if len(image_data) <= 2*1024*1024: - return base64_data - - # 将字节数据转换为图片对象 - img = Image.open(io.BytesIO(image_data)) - - # 获取原始尺寸 - original_width, original_height = img.size - - # 计算缩放比例 - scale = min(1.0, (target_size / len(image_data)) ** 0.5) - - # 计算新的尺寸 - new_width = int(original_width * scale) - new_height = int(original_height * scale) - - # 创建内存缓冲区 - output_buffer = io.BytesIO() - - # 如果是GIF,处理所有帧 - if getattr(img, "is_animated", False): - frames = [] - for frame_idx in range(img.n_frames): - img.seek(frame_idx) - new_frame = img.copy() - new_frame = new_frame.resize((new_width//2, new_height//2), Image.Resampling.LANCZOS) # 动图折上折 - frames.append(new_frame) - - # 保存到缓冲区 - frames[0].save( - output_buffer, - format='GIF', - save_all=True, - append_images=frames[1:], - optimize=True, - duration=img.info.get('duration', 100), - loop=img.info.get('loop', 0) - ) - else: - # 处理静态图片 - resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # 保存到缓冲区,保持原始格式 - if img.format == 'PNG' and img.mode in ('RGBA', 'LA'): - resized_img.save(output_buffer, format='PNG', optimize=True) - else: - resized_img.save(output_buffer, format='JPEG', quality=95, optimize=True) - - # 获取压缩后的数据并转换为base64 - compressed_data = output_buffer.getvalue() - logger.success(f"压缩图片: {original_width}x{original_height} -> {new_width}x{new_height}") - logger.info(f"压缩前大小: {len(image_data)/1024:.1f}KB, 压缩后大小: {len(compressed_data)/1024:.1f}KB") - - return base64.b64encode(compressed_data).decode('utf-8') - - except Exception as e: - logger.error(f"压缩图片失败: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - return base64_data - def image_path_to_base64(image_path: str) -> str: """将图片路径转换为base64编码 Args: diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index c70c26ff9..56ed80693 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -7,10 +7,11 @@ from typing import Tuple, Union import aiohttp from loguru import logger from nonebot import get_driver - +import base64 +from PIL import Image +import io from ...common.database import Database from ..chat.config import global_config -from ..chat.utils_image import compress_base64_image_by_scale driver = get_driver() config = driver.config @@ -405,3 +406,77 @@ class LLM_request: ) return embedding +def compress_base64_image_by_scale(base64_data: str, target_size: int = 0.8 * 1024 * 1024) -> str: + """压缩base64格式的图片到指定大小 + Args: + base64_data: base64编码的图片数据 + target_size: 目标文件大小(字节),默认0.8MB + Returns: + str: 压缩后的base64图片数据 + """ + try: + # 将base64转换为字节数据 + image_data = base64.b64decode(base64_data) + + # 如果已经小于目标大小,直接返回原图 + if len(image_data) <= 2*1024*1024: + return base64_data + + # 将字节数据转换为图片对象 + img = Image.open(io.BytesIO(image_data)) + + # 获取原始尺寸 + original_width, original_height = img.size + + # 计算缩放比例 + scale = min(1.0, (target_size / len(image_data)) ** 0.5) + + # 计算新的尺寸 + new_width = int(original_width * scale) + new_height = int(original_height * scale) + + # 创建内存缓冲区 + output_buffer = io.BytesIO() + + # 如果是GIF,处理所有帧 + if getattr(img, "is_animated", False): + frames = [] + for frame_idx in range(img.n_frames): + img.seek(frame_idx) + new_frame = img.copy() + new_frame = new_frame.resize((new_width//2, new_height//2), Image.Resampling.LANCZOS) # 动图折上折 + frames.append(new_frame) + + # 保存到缓冲区 + frames[0].save( + output_buffer, + format='GIF', + save_all=True, + append_images=frames[1:], + optimize=True, + duration=img.info.get('duration', 100), + loop=img.info.get('loop', 0) + ) + else: + # 处理静态图片 + resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 保存到缓冲区,保持原始格式 + if img.format == 'PNG' and img.mode in ('RGBA', 'LA'): + resized_img.save(output_buffer, format='PNG', optimize=True) + else: + resized_img.save(output_buffer, format='JPEG', quality=95, optimize=True) + + # 获取压缩后的数据并转换为base64 + compressed_data = output_buffer.getvalue() + logger.success(f"压缩图片: {original_width}x{original_height} -> {new_width}x{new_height}") + logger.info(f"压缩前大小: {len(image_data)/1024:.1f}KB, 压缩后大小: {len(compressed_data)/1024:.1f}KB") + + return base64.b64encode(compressed_data).decode('utf-8') + + except Exception as e: + logger.error(f"压缩图片失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return base64_data + From a54ca8ce1819c3136627e0d2f89903f37d034545 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 05:05:07 +0800 Subject: [PATCH 071/105] Merge remote-tracking branch 'upstream/debug' into feat_regix --- .gitignore | 5 +- .vscode/settings.json | 3 + README.md | 29 +- bot.py | 65 +- changelog_config.md | 6 + docker-compose.yml | 4 +- docs/Jonathan R.md | 20 + docs/docker_deploy.md | 66 +- docs/installation_cute.md | 7 +- docs/installation_standard.md | 11 +- docs/manual_deploy_linux.md | 15 +- docs/manual_deploy_windows.md | 8 +- flake.nix | 1 + pyproject.toml | 50 +- requirements.txt | Bin 600 -> 612 bytes run.bat | 2 +- run.py | 2 + src/gui/reasoning_gui.py | 4 +- src/plugins/chat/__init__.py | 6 +- src/plugins/chat/bot.py | 6 +- src/plugins/chat/config.py | 185 ++- src/plugins/chat/cq_code.py | 6 +- src/plugins/chat/emoji_manager.py | 34 +- src/plugins/chat/llm_generator.py | 4 +- src/plugins/chat/message_sender.py | 6 +- src/plugins/chat/prompt_builder.py | 9 +- src/plugins/chat/storage.py | 2 +- src/plugins/chat/utils.py | 16 +- src/plugins/knowledege/knowledge_library.py | 2 +- src/plugins/memory_system/memory.py | 286 +++-- src/plugins/memory_system/memory_test1.py | 1208 +++++++++++++++++++ src/plugins/models/utils_model.py | 12 +- src/plugins/schedule/schedule_generator.py | 4 +- src/plugins/utils/statistic.py | 2 +- template/bot_config_template.toml | 1 + 35 files changed, 1746 insertions(+), 341 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 docs/Jonathan R.md create mode 100644 src/plugins/memory_system/memory_test1.py diff --git a/.gitignore b/.gitignore index 4e1606a54..e51abc5cc 100644 --- a/.gitignore +++ b/.gitignore @@ -193,9 +193,8 @@ cython_debug/ # jieba jieba.cache - -# vscode -/.vscode +# .vscode +!.vscode/settings.json # direnv /.direnv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..23fd35f0e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/README.md b/README.md index 0c02d1cba..f4ebca07d 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,17 @@ -> ⚠️ **注意事项** +> [!WARNING] > - 项目处于活跃开发阶段,代码可能随时更改 > - 文档未完善,有问题可以提交 Issue 或者 Discussion > - QQ机器人存在被限制风险,请自行了解,谨慎使用 > - 由于持续迭代,可能存在一些已知或未知的bug > - 由于开发中,可能消耗较多token -**交流群**: 766798517 一群人较多,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -**交流群**: 571780722 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -**交流群**: 1035228475 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +## 💬交流群 +- [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 ,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 **其他平台版本** @@ -51,18 +52,17 @@ ### 部署方式 -如果你不知道Docker是什么,建议寻找相关教程或使用手动部署(现在不建议使用docker,更新慢,可能不适配) +- 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置 + +- [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md) + +- [📦 Linux 手动部署指南 ](docs/manual_deploy_linux.md) + +如果你不知道Docker是什么,建议寻找相关教程或使用手动部署 **(现在不建议使用docker,更新慢,可能不适配)** - [🐳 Docker部署指南](docs/docker_deploy.md) -- [📦 手动部署指南 Windows](docs/manual_deploy_windows.md) - - -- [📦 手动部署指南 Linux](docs/manual_deploy_linux.md) - -- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置 - ### 配置说明 - [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘 - [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户 @@ -140,9 +140,10 @@ ## 📌 注意事项 -SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 -> ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 +SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 +> [!WARNING] +> 本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 ## 致谢 [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架 diff --git a/bot.py b/bot.py index c2ed3dfdf..471a98eaf 100644 --- a/bot.py +++ b/bot.py @@ -1,9 +1,12 @@ +import asyncio import os import shutil import sys import nonebot import time + +import uvicorn from dotenv import load_dotenv from loguru import logger from nonebot.adapters.onebot.v11 import Adapter @@ -12,6 +15,8 @@ import platform # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} +uvicorn_server = None + def easter_egg(): # 彩蛋 @@ -100,10 +105,12 @@ def load_logger(): "#777777>| {name:.<8}:{function:.<8}:{line: >4} - {message}", colorize=True, - level=os.getenv("LOG_LEVEL", "DEBUG") # 根据环境设置日志级别,默认为INFO + level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO + filter=lambda record: "nonebot" not in record["name"] ) + def scan_provider(env_config: dict): provider = {} @@ -138,7 +145,39 @@ def scan_provider(env_config: dict): raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量") -if __name__ == "__main__": +async def graceful_shutdown(): + try: + global uvicorn_server + if uvicorn_server: + uvicorn_server.force_exit = True # 强制退出 + await uvicorn_server.shutdown() + + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + except Exception as e: + logger.error(f"麦麦关闭失败: {e}") + + +async def uvicorn_main(): + global uvicorn_server + config = uvicorn.Config( + app="__main__:app", + host=os.getenv("HOST", "127.0.0.1"), + port=int(os.getenv("PORT", 8080)), + reload=os.getenv("ENVIRONMENT") == "dev", + timeout_graceful_shutdown=5, + log_config=None, + access_log=False + ) + server = uvicorn.Server(config) + uvicorn_server = server + await server.serve() + + +def raw_main(): # 利用 TZ 环境变量设定程序工作的时区 # 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 if platform.system().lower() != 'windows': @@ -165,10 +204,30 @@ if __name__ == "__main__": nonebot.init(**base_config, **env_config) # 注册适配器 + global driver driver = nonebot.get_driver() driver.register_adapter(Adapter) # 加载插件 nonebot.load_plugins("src/plugins") - nonebot.run() + +if __name__ == "__main__": + + try: + raw_main() + + global app + app = nonebot.get_asgi() + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(uvicorn_main()) + except KeyboardInterrupt: + logger.warning("麦麦会努力做的更好的!正在停止中......") + except Exception as e: + logger.error(f"主程序异常: {e}") + finally: + loop.run_until_complete(graceful_shutdown()) + loop.close() + logger.info("进程终止完毕,麦麦开始休眠......下次再见哦!") diff --git a/changelog_config.md b/changelog_config.md index 7101fe828..c4c560644 100644 --- a/changelog_config.md +++ b/changelog_config.md @@ -1,6 +1,12 @@ # Changelog +## [0.0.5] - 2025-3-11 +### Added +- 新增了 `alias_names` 配置项,用于指定麦麦的别名。 + ## [0.0.4] - 2025-3-9 ### Added - 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 + + diff --git a/docker-compose.yml b/docker-compose.yml index 512558558..227df606b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,6 @@ services: - NAPCAT_UID=${NAPCAT_UID} - NAPCAT_GID=${NAPCAT_GID} # 让 NapCat 获取当前用户 GID,UID,防止权限问题 ports: - - 3000:3000 - - 3001:3001 - 6099:6099 restart: unless-stopped volumes: @@ -19,7 +17,7 @@ services: mongodb: container_name: mongodb environment: - - tz=Asia/Shanghai + - TZ=Asia/Shanghai # - MONGO_INITDB_ROOT_USERNAME=your_username # - MONGO_INITDB_ROOT_PASSWORD=your_password expose: diff --git a/docs/Jonathan R.md b/docs/Jonathan R.md new file mode 100644 index 000000000..660caaeec --- /dev/null +++ b/docs/Jonathan R.md @@ -0,0 +1,20 @@ +Jonathan R. Wolpaw 在 “Memory in neuroscience: rhetoric versus reality.” 一文中提到,从神经科学的感觉运动假设出发,整个神经系统的功能是将经验与适当的行为联系起来,而不是单纯的信息存储。 +Jonathan R,Wolpaw. (2019). Memory in neuroscience: rhetoric versus reality.. Behavioral and cognitive neuroscience reviews(2). + +1. **单一过程理论** + - 单一过程理论认为,识别记忆主要是基于熟悉性这一单一因素的影响。熟悉性是指对刺激的一种自动的、无意识的感知,它可以使我们在没有回忆起具体细节的情况下,判断一个刺激是否曾经出现过。 + - 例如,在一些实验中,研究者发现被试可以在没有回忆起具体学习情境的情况下,对曾经出现过的刺激做出正确的判断,这被认为是熟悉性在起作用1。 +2. **双重过程理论** + - 双重过程理论则认为,识别记忆是基于两个过程:回忆和熟悉性。回忆是指对过去经验的有意识的回忆,它可以使我们回忆起具体的细节和情境;熟悉性则是一种自动的、无意识的感知。 + - 该理论认为,在识别记忆中,回忆和熟悉性共同作用,使我们能够判断一个刺激是否曾经出现过。例如,在 “记得 / 知道” 范式中,被试被要求判断他们对一个刺激的记忆是基于回忆还是熟悉性。研究发现,被试可以区分这两种不同的记忆过程,这为双重过程理论提供了支持1。 + + + +1. **神经元节点与连接**:借鉴神经网络原理,将每个记忆单元视为一个神经元节点。节点之间通过连接相互关联,连接的强度代表记忆之间的关联程度。在形态学联想记忆中,具有相似形态特征的记忆节点连接强度较高。例如,苹果和橘子的记忆节点,由于在形状、都是水果等形态语义特征上相似,它们之间的连接强度大于苹果与汽车记忆节点间的连接强度。 +2. **记忆聚类与层次结构**:依据形态特征的相似性对记忆进行聚类,形成不同的记忆簇。每个记忆簇内部的记忆具有较高的相似性,而不同记忆簇之间的记忆相似性较低。同时,构建记忆的层次结构,高层次的记忆节点代表更抽象、概括的概念,低层次的记忆节点对应具体的实例。比如,“水果” 作为高层次记忆节点,连接着 “苹果”“橘子”“香蕉” 等低层次具体水果的记忆节点。 +3. **网络的动态更新**:随着新记忆的不断加入,记忆网络动态调整。新记忆节点根据其形态特征与现有网络中的节点建立连接,同时影响相关连接的强度。若新记忆与某个记忆簇的特征高度相似,则被纳入该记忆簇;若具有独特特征,则可能引发新的记忆簇的形成。例如,当系统学习到一种新的水果 “番石榴”,它会根据番石榴的形态、语义等特征,在记忆网络中找到与之最相似的区域(如水果记忆簇),并建立相应连接,同时调整周围节点连接强度以适应这一新记忆。 + + + +- **相似性联想**:该理论认为,当两个或多个事物在形态上具有相似性时,它们在记忆中会形成关联。例如,梨和苹果在形状和都是水果这一属性上有相似性,所以当我们看到梨时,很容易通过形态学联想记忆联想到苹果。这种相似性联想有助于我们对新事物进行分类和理解,当遇到一个新的类似水果时,我们可以通过与已有的水果记忆进行相似性匹配,来推测它的一些特征。 +- **时空关联性联想**:除了相似性联想,MAM 还强调时空关联性联想。如果两个事物在时间或空间上经常同时出现,它们也会在记忆中形成关联。比如,每次在公园里看到花的时候,都能听到鸟儿的叫声,那么花和鸟儿叫声的形态特征(花的视觉形态和鸟叫的听觉形态)就会在记忆中形成关联,以后听到鸟叫可能就会联想到公园里的花。 \ No newline at end of file diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index 3958d2fc4..db759dfd0 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -1,67 +1,97 @@ # 🐳 Docker 部署指南 -## 部署步骤(推荐,但不一定是最新) +## 部署步骤 (推荐,但不一定是最新) + +**"更新镜像与容器"部分在本文档 [Part 6](#6-更新镜像与容器)** + +### 0. 前提说明 + +**本文假设读者已具备一定的 Docker 基础知识。若您对 Docker 不熟悉,建议先参考相关教程或文档进行学习,或选择使用 [📦Linux手动部署指南](./manual_deploy_linux.md) 或 [📦Windows手动部署指南](./manual_deploy_windows.md) 。** -### 1. 获取Docker配置文件: +### 1. 获取Docker配置文件 + +- 建议先单独创建好一个文件夹并进入,作为工作目录 ```bash wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml ``` -- 若需要启用MongoDB数据库的用户名和密码,可进入docker-compose.yml,取消MongoDB处的注释并修改变量`=`后方的值为你的用户名和密码\ -修改后请注意在之后配置`.env.prod`文件时指定MongoDB数据库的用户名密码 +- 若需要启用MongoDB数据库的用户名和密码,可进入docker-compose.yml,取消MongoDB处的注释并修改变量旁 `=` 后方的值为你的用户名和密码\ +修改后请注意在之后配置 `.env.prod` 文件时指定MongoDB数据库的用户名密码 -### 2. 启动服务: +### 2. 启动服务 -- **!!! 请在第一次启动前确保当前工作目录下`.env.prod`与`bot_config.toml`文件存在 !!!**\ +- **!!! 请在第一次启动前确保当前工作目录下 `.env.prod` 与 `bot_config.toml` 文件存在 !!!**\ 由于Docker文件映射行为的特殊性,若宿主机的映射路径不存在,可能导致意外的目录创建,而不会创建文件,由于此处需要文件映射到文件,需提前确保文件存在且路径正确,可使用如下命令: + ```bash touch .env.prod touch bot_config.toml ``` - 启动Docker容器: + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d ``` -- 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +### 3. 修改配置并重启Docker -### 3. 修改配置并重启Docker: - -- 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\ -**需要注意`.env.prod`中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** +- 请前往 [🎀新手配置指南](./installation_cute.md) 或 [⚙️标准配置指南](./installation_standard.md) 完成 `.env.prod` 与 `bot_config.toml` 配置文件的编写\ +**需要注意 `.env.prod` 中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: + ```bash -docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名臣 +docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名称 ``` - 下方命令可以但不推荐,只是同时重启NapCat、MongoDB、MaiMBot三个服务 + ```bash NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose restart ``` -- 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 - ### 4. 登入NapCat管理页添加反向WebSocket -- 在浏览器地址栏输入`http://<宿主机IP>:6099/`进入NapCat的管理Web页,添加一个Websocket客户端 +- 在浏览器地址栏输入 `http://<宿主机IP>:6099/` 进入NapCat的管理Web页,添加一个Websocket客户端 + > 网络配置 -> 新建 -> Websocket客户端 -- Websocket客户端的名称自定,URL栏填入`ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ +- Websocket客户端的名称自定,URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ (若修改过容器名称则替换maimbot为你自定的名称) -### 5. 愉快地和麦麦对话吧! +### 5. 部署完成,愉快地和麦麦对话吧! + + +### 6. 更新镜像与容器 + +- 拉取最新镜像 + +```bash +docker-compose pull +``` + +- 执行启动容器指令,该指令会自动重建镜像有更新的容器并启动 + +```bash +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d +# 旧版Docker中可能找不到docker compose,请使用docker-compose工具替代 +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d +``` ## ⚠️ 注意事项 - 目前部署方案仍在测试中,可能存在未知问题 - 配置文件中的API密钥请妥善保管,不要泄露 -- 建议先在测试环境中运行,确认无误后再部署到生产环境 \ No newline at end of file +- 建议先在测试环境中运行,确认无误后再部署到生产环境 \ No newline at end of file diff --git a/docs/installation_cute.md b/docs/installation_cute.md index e7541f7d3..4465660f9 100644 --- a/docs/installation_cute.md +++ b/docs/installation_cute.md @@ -52,12 +52,12 @@ key = "SILICONFLOW_KEY" # 用同一张门票就可以啦 如果你想用DeepSeek官方的服务,就要这样改: ```toml [model.llm_reasoning] -name = "Pro/deepseek-ai/DeepSeek-R1" +name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园 key = "DEEP_SEEK_KEY" # 用DeepSeek的门票 [model.llm_normal] -name = "Pro/deepseek-ai/DeepSeek-V3" +name = "deepseek-chat" # 改成对应的模型名称,这里为DeepseekV3 base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园 key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票 ``` @@ -110,7 +110,8 @@ PLUGINS=["src2.plugins.chat"] # 这里是机器人的插件列表呢 ```toml [bot] qq = "把这里改成你的机器人QQ号喵" # 填写你的机器人QQ号 -nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦 +nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦,建议和机器人QQ名称/群昵称一样哦 +alias_names = ["小麦", "阿麦"] # 也可以用这个招呼机器人,可以不设置呢 [personality] # 这里可以设置机器人的性格呢,让它更有趣一些喵 diff --git a/docs/installation_standard.md b/docs/installation_standard.md index 5f52676d1..03b66dc46 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -8,7 +8,7 @@ ## API配置说明 -`.env.prod`和`bot_config.toml`中的API配置关系如下: +`.env.prod` 和 `bot_config.toml` 中的API配置关系如下: ### 在.env.prod中定义API凭证: ```ini @@ -34,7 +34,7 @@ key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 如需切换到其他API服务,只需修改引用: ```toml [model.llm_reasoning] -name = "Pro/deepseek-ai/DeepSeek-R1" +name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务 key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 ``` @@ -53,11 +53,11 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # 服务配置 HOST=127.0.0.1 # 如果使用Docker部署,需要改成0.0.0.0,否则QQ消息无法传入 -PORT=8080 +PORT=8080 # 与反向端口相同 # 数据库配置 MONGODB_HOST=127.0.0.1 # 如果使用Docker部署,需要改成数据库容器的名字,默认是mongodb -MONGODB_PORT=27017 +MONGODB_PORT=27017 # MongoDB端口 DATABASE_NAME=MegBot MONGODB_USERNAME = "" # 数据库用户名 MONGODB_PASSWORD = "" # 数据库密码 @@ -72,6 +72,9 @@ PLUGINS=["src2.plugins.chat"] [bot] qq = "机器人QQ号" # 必填 nickname = "麦麦" # 机器人昵称 +# alias_names: 配置机器人可使用的别名。当机器人在群聊或对话中被调用时,别名可以作为直接命令或提及机器人的关键字使用。 +# 该配置项为字符串数组。例如: ["小麦", "阿麦"] +alias_names = ["小麦", "阿麦"] # 机器人别名 [personality] prompt_personality = [ diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index d310ffc59..41f0390b8 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -66,7 +66,7 @@ pip install -r requirements.txt ## 数据库配置 ### 3️⃣ **安装并启动MongoDB** -- 安装与启动:Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/),Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) +- 安装与启动: Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/),Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) - 默认连接本地27017端口 --- @@ -76,15 +76,14 @@ pip install -r requirements.txt - 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装 -- 使用QQ小号登录,添加反向WS地址: -`ws://127.0.0.1:8080/onebot/v11/ws` +- 使用QQ小号登录,添加反向WS地址: `ws://127.0.0.1:8080/onebot/v11/ws` --- ## 配置文件设置 ### 5️⃣ **配置文件设置,让麦麦Bot正常工作** -- 修改环境配置文件:`.env.prod` -- 修改机器人配置文件:`bot_config.toml` +- 修改环境配置文件: `.env.prod` +- 修改机器人配置文件: `bot_config.toml` --- @@ -107,9 +106,9 @@ python3 bot.py --- ## 常见问题 -🔧 权限问题:在命令前加`sudo` -🔌 端口占用:使用`sudo lsof -i :8080`查看端口占用 -🛡️ 防火墙:确保8080/27017端口开放 +🔧 权限问题: 在命令前加 `sudo` +🔌 端口占用: 使用 `sudo lsof -i :8080` 查看端口占用 +🛡️ 防火墙: 确保8080/27017端口开放 ```bash sudo ufw allow 8080/tcp sudo ufw allow 27017/tcp diff --git a/docs/manual_deploy_windows.md b/docs/manual_deploy_windows.md index 86238bcd4..eebdc4f41 100644 --- a/docs/manual_deploy_windows.md +++ b/docs/manual_deploy_windows.md @@ -30,7 +30,7 @@ 在创建虚拟环境之前,请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装: -1. 访问Python官网下载页面:https://www.python.org/downloads/release/python-3913/ +1. 访问Python官网下载页面: https://www.python.org/downloads/release/python-3913/ 2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe` 3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项 4. 点击"Install Now"开始安装 @@ -79,11 +79,11 @@ pip install -r requirements.txt ### 3️⃣ **配置NapCat,让麦麦bot与qq取得联系** - 安装并登录NapCat(用你的qq小号) -- 添加反向WS:`ws://127.0.0.1:8080/onebot/v11/ws` +- 添加反向WS: `ws://127.0.0.1:8080/onebot/v11/ws` ### 4️⃣ **配置文件设置,让麦麦Bot正常工作** -- 修改环境配置文件:`.env.prod` -- 修改机器人配置文件:`bot_config.toml` +- 修改环境配置文件: `.env.prod` +- 修改机器人配置文件: `bot_config.toml` ### 5️⃣ **启动麦麦机器人** - 打开命令行,cd到对应路径 diff --git a/flake.nix b/flake.nix index 54737d640..3586857f0 100644 --- a/flake.nix +++ b/flake.nix @@ -22,6 +22,7 @@ pythonEnv = pkgs.python3.withPackages ( ps: with ps; [ + ruff pymongo python-dotenv pydantic diff --git a/pyproject.toml b/pyproject.toml index e54dcdacd..0a4805744 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,51 @@ [project] -name = "Megbot" +name = "MaiMaiBot" version = "0.1.0" -description = "New Bot Project" +description = "MaiMaiBot" [tool.nonebot] plugins = ["src.plugins.chat"] -plugin_dirs = ["src/plugins"] +plugin_dirs = ["src/plugins"] [tool.ruff] -# 设置 Python 版本 -target-version = "py39" + +include = ["*.py"] + +# 行长度设置 +line-length = 120 + +[tool.ruff.lint] +fixable = ["ALL"] +unfixable = [] + +# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。 +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # 启用的规则 select = [ - "E", # pycodestyle 错误 - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear + "E", # pycodestyle 错误 + "F", # pyflakes + "B", # flake8-bugbear ] -# 行长度设置 -line-length = 88 \ No newline at end of file +ignore = ["E711"] + +[tool.ruff.format] +docstring-code-format = true +indent-style = "space" + + +# 使用双引号表示字符串 +quote-style = "double" + +# 尊重魔法尾随逗号 +# 例如: +# items = [ +# "apple", +# "banana", +# "cherry", +# ] +skip-magic-trailing-comma = false + +# 自动检测合适的换行符 +line-ending = "auto" diff --git a/requirements.txt b/requirements.txt index 4f969682f9b0ebbbd9aebe4fe94758da052d23b4..0acaade5e6d465465ee87009b5c28551e591953d 100644 GIT binary patch delta 20 acmcb?@`PnW1QQQ00~bRPLn%WV5CQ-^9t5QT delta 7 OcmaFDa)V_<1QP%aWdhFt diff --git a/run.bat b/run.bat index 659a7545a..91904bc34 100644 --- a/run.bat +++ b/run.bat @@ -3,7 +3,7 @@ chcp 65001 if not exist "venv" ( python -m venv venv call venv\Scripts\activate.bat - pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt + pip install -i https://mirrors.aliyun.com/pypi/simple --upgrade -r requirements.txt ) else ( call venv\Scripts\activate.bat ) diff --git a/run.py b/run.py index baea4d13c..50e312c37 100644 --- a/run.py +++ b/run.py @@ -107,6 +107,8 @@ def install_napcat(): napcat_filename = input( "下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell:" ) + if(napcat_filename[-4:] == ".zip"): + napcat_filename = napcat_filename[:-4] extract_files(napcat_filename + ".zip", "napcat") print("NapCat 安装完成") os.remove(napcat_filename + ".zip") diff --git a/src/gui/reasoning_gui.py b/src/gui/reasoning_gui.py index 514a95dfb..5768ddc09 100644 --- a/src/gui/reasoning_gui.py +++ b/src/gui/reasoning_gui.py @@ -87,7 +87,7 @@ class ReasoningGUI: self.db = Database.get_instance().db logger.success("数据库初始化成功") except Exception: - logger.exception(f"数据库初始化失败") + logger.exception("数据库初始化失败") sys.exit(1) # 存储群组数据 @@ -342,7 +342,7 @@ class ReasoningGUI: 'group_id': self.selected_group_id }) except Exception: - logger.exception(f"自动更新出错") + logger.exception("自动更新出错") # 每5秒更新一次 time.sleep(5) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index bd71be019..c730466b3 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -121,9 +121,9 @@ async def build_memory_task(): @scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") async def forget_memory_task(): """每30秒执行一次记忆构建""" - # print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") - # await hippocampus.operation_forget_topic(percentage=0.1) - # print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") + print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") + await hippocampus.operation_forget_topic(percentage=0.1) + print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval + 10, id="merge_memory") diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 86d0b6944..490b171b5 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -138,7 +138,7 @@ class ChatBot: # 如果找不到思考消息,直接返回 if not thinking_message: - logger.warning(f"未找到对应的思考消息,可能已超时被移除") + logger.warning("未找到对应的思考消息,可能已超时被移除") return # 记录开始思考的时间,避免从思考到回复的时间太久 @@ -187,7 +187,7 @@ class ChatBot: # 检查是否 <没有找到> emoji if emoji_raw != None: - emoji_path, discription = emoji_raw + emoji_path, description = emoji_raw emoji_cq = CQCode.create_emoji_cq(emoji_path) @@ -203,7 +203,7 @@ class ChatBot: raw_message=emoji_cq, plain_text=emoji_cq, processed_plain_text=emoji_cq, - detailed_plain_text=discription, + detailed_plain_text=description, user_nickname=global_config.BOT_NICKNAME, group_name=message.group_name, time=bot_response_time, diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 888e33a7f..547c44a82 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -1,6 +1,6 @@ import os from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import Dict, List, Optional import tomli from loguru import logger @@ -12,10 +12,12 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier @dataclass class BotConfig: """机器人配置类""" + INNER_VERSION: Version = None BOT_QQ: Optional[int] = 1 BOT_NICKNAME: Optional[str] = None + BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它 # 消息处理相关配置 MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 @@ -82,23 +84,25 @@ class BotConfig: PROMPT_PERSONALITY = [ "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", "是一个女大学生,你有黑色头发,你会刷小红书", - "是一个女大学生,你会刷b站,对ACG文化感兴趣" + "是一个女大学生,你会刷b站,对ACG文化感兴趣", ] - PROMPT_SCHEDULE_GEN="一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" - - PERSONALITY_1: float = 0.6 # 第一种人格概率 - PERSONALITY_2: float = 0.3 # 第二种人格概率 - PERSONALITY_3: float = 0.1 # 第三种人格概率 - - memory_ban_words: list = field(default_factory=lambda: ['表情包', '图片', '回复', '聊天记录']) # 添加新的配置项默认值 - + PROMPT_SCHEDULE_GEN = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" + + PERSONALITY_1: float = 0.6 # 第一种人格概率 + PERSONALITY_2: float = 0.3 # 第二种人格概率 + PERSONALITY_3: float = 0.1 # 第三种人格概率 + + memory_ban_words: list = field( + default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] + ) # 添加新的配置项默认值 + @staticmethod def get_config_dir() -> str: """获取配置文件目录""" current_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) - config_dir = os.path.join(root_dir, 'config') + root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) + config_dir = os.path.join(root_dir, "config") if not os.path.exists(config_dir): os.makedirs(config_dir) return config_dir @@ -109,35 +113,32 @@ class BotConfig: Args: value[str]: 版本表达式(字符串) Returns: - SpecifierSet + SpecifierSet """ try: converted = SpecifierSet(value) - except InvalidSpecifier as e: - logger.error( - f"{value} 分类使用了错误的版本约束表达式\n", - "请阅读 https://semver.org/lang/zh-CN/ 修改代码" - ) + except InvalidSpecifier: + logger.error(f"{value} 分类使用了错误的版本约束表达式\n", "请阅读 https://semver.org/lang/zh-CN/ 修改代码") exit(1) return converted @classmethod def get_config_version(cls, toml: dict) -> Version: - """提取配置文件的 SpecifierSet 版本数据 + """提取配置文件的 SpecifierSet 版本数据 Args: toml[dict]: 输入的配置文件字典 Returns: - Version + Version """ - if 'inner' in toml: + if "inner" in toml: try: config_version: str = toml["inner"]["version"] except KeyError as e: - logger.error(f"配置文件中 inner 段 不存在, 这是错误的配置文件") - raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") + logger.error("配置文件中 inner 段 不存在, 这是错误的配置文件") + raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") from e else: toml["inner"] = {"version": "0.0.0"} config_version = toml["inner"]["version"] @@ -150,7 +151,7 @@ class BotConfig: "请阅读 https://semver.org/lang/zh-CN/ 修改配置,并参考本项目指定的模板进行修改\n" "本项目在不同的版本下有不同的模板,请注意识别" ) - raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") + raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") from e return ver @@ -160,26 +161,26 @@ class BotConfig: config = cls() def personality(parent: dict): - personality_config = parent['personality'] - personality = personality_config.get('prompt_personality') + personality_config = parent["personality"] + personality = personality_config.get("prompt_personality") if len(personality) >= 2: logger.debug(f"载入自定义人格:{personality}") - config.PROMPT_PERSONALITY = personality_config.get('prompt_personality', config.PROMPT_PERSONALITY) + config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY) logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") - config.PROMPT_SCHEDULE_GEN = personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN) + config.PROMPT_SCHEDULE_GEN = personality_config.get("prompt_schedule", config.PROMPT_SCHEDULE_GEN) if config.INNER_VERSION in SpecifierSet(">=0.0.2"): - config.PERSONALITY_1 = personality_config.get('personality_1_probability', config.PERSONALITY_1) - config.PERSONALITY_2 = personality_config.get('personality_2_probability', config.PERSONALITY_2) - config.PERSONALITY_3 = personality_config.get('personality_3_probability', config.PERSONALITY_3) + config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1) + config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2) + config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3) def emoji(parent: dict): emoji_config = parent["emoji"] config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) - config.EMOJI_CHECK_PROMPT = emoji_config.get('check_prompt', config.EMOJI_CHECK_PROMPT) - config.EMOJI_SAVE = emoji_config.get('auto_save', config.EMOJI_SAVE) - config.EMOJI_CHECK = emoji_config.get('enable_check', config.EMOJI_CHECK) + config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT) + config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE) + config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK) def cq_code(parent: dict): cq_code_config = parent["cq_code"] @@ -192,12 +193,16 @@ class BotConfig: config.BOT_QQ = int(bot_qq) config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME) + if config.INNER_VERSION in SpecifierSet(">=0.0.5"): + config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES) + def response(parent: dict): response_config = parent["response"] config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY) config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY) - config.MODEL_R1_DISTILL_PROBABILITY = response_config.get("model_r1_distill_probability", - config.MODEL_R1_DISTILL_PROBABILITY) + config.MODEL_R1_DISTILL_PROBABILITY = response_config.get( + "model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY + ) config.max_response_length = response_config.get("max_response_length", config.max_response_length) def model(parent: dict): @@ -214,7 +219,7 @@ class BotConfig: "llm_emotion_judge", "vlm", "embedding", - "moderation" + "moderation", ] for item in config_list: @@ -223,13 +228,7 @@ class BotConfig: # base_url 的例子: SILICONFLOW_BASE_URL # key 的例子: SILICONFLOW_KEY - cfg_target = { - "name": "", - "base_url": "", - "key": "", - "pri_in": 0, - "pri_out": 0 - } + cfg_target = {"name": "", "base_url": "", "key": "", "pri_in": 0, "pri_out": 0} if config.INNER_VERSION in SpecifierSet("<=0.0.0"): cfg_target = cfg_item @@ -248,7 +247,7 @@ class BotConfig: cfg_target[i] = cfg_item[i] except KeyError as e: logger.error(f"{item} 中的必要字段不存在,请检查") - raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") + raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") from e provider = cfg_item.get("provider") if provider is None: @@ -273,10 +272,12 @@ class BotConfig: if config.INNER_VERSION in SpecifierSet(">=0.0.2"): config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout) - config.response_willing_amplifier = msg_config.get("response_willing_amplifier", - config.response_willing_amplifier) - config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", - config.response_interested_rate_amplifier) + config.response_willing_amplifier = msg_config.get( + "response_willing_amplifier", config.response_willing_amplifier + ) + config.response_interested_rate_amplifier = msg_config.get( + "response_interested_rate_amplifier", config.response_interested_rate_amplifier + ) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) if config.INNER_VERSION in SpecifierSet(">=0.0.5"): @@ -286,7 +287,7 @@ class BotConfig: memory_config = parent["memory"] config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) config.forget_memory_interval = memory_config.get("forget_memory_interval", config.forget_memory_interval) - + # 在版本 >= 0.0.4 时才处理新增的配置项 if config.INNER_VERSION in SpecifierSet(">=0.0.4"): config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) @@ -307,10 +308,12 @@ class BotConfig: config.chinese_typo_enable = chinese_typo_config.get("enable", config.chinese_typo_enable) config.chinese_typo_error_rate = chinese_typo_config.get("error_rate", config.chinese_typo_error_rate) config.chinese_typo_min_freq = chinese_typo_config.get("min_freq", config.chinese_typo_min_freq) - config.chinese_typo_tone_error_rate = chinese_typo_config.get("tone_error_rate", - config.chinese_typo_tone_error_rate) - config.chinese_typo_word_replace_rate = chinese_typo_config.get("word_replace_rate", - config.chinese_typo_word_replace_rate) + config.chinese_typo_tone_error_rate = chinese_typo_config.get( + "tone_error_rate", config.chinese_typo_tone_error_rate + ) + config.chinese_typo_word_replace_rate = chinese_typo_config.get( + "word_replace_rate", config.chinese_typo_word_replace_rate + ) def groups(parent: dict): groups_config = parent["groups"] @@ -329,61 +332,19 @@ class BotConfig: # 例如:"notice": "personality 将在 1.3.2 后被移除",那么在有效版本中的用户就会虽然可以 # 正常执行程序,但是会看到这条自定义提示 include_configs = { - "personality": { - "func": personality, - "support": ">=0.0.0" - }, - "emoji": { - "func": emoji, - "support": ">=0.0.0" - }, - "cq_code": { - "func": cq_code, - "support": ">=0.0.0" - }, - "bot": { - "func": bot, - "support": ">=0.0.0" - }, - "response": { - "func": response, - "support": ">=0.0.0" - }, - "model": { - "func": model, - "support": ">=0.0.0" - }, - "message": { - "func": message, - "support": ">=0.0.0" - }, - "memory": { - "func": memory, - "support": ">=0.0.0", - "necessary": False - }, - "mood": { - "func": mood, - "support": ">=0.0.0" - }, - "keywords_reaction": { - "func": keywords_reaction, - "support": ">=0.0.2", - "necessary": False - }, - "chinese_typo": { - "func": chinese_typo, - "support": ">=0.0.3", - "necessary": False - }, - "groups": { - "func": groups, - "support": ">=0.0.0" - }, - "others": { - "func": others, - "support": ">=0.0.0" - } + "personality": {"func": personality, "support": ">=0.0.0"}, + "emoji": {"func": emoji, "support": ">=0.0.0"}, + "cq_code": {"func": cq_code, "support": ">=0.0.0"}, + "bot": {"func": bot, "support": ">=0.0.0"}, + "response": {"func": response, "support": ">=0.0.0"}, + "model": {"func": model, "support": ">=0.0.0"}, + "message": {"func": message, "support": ">=0.0.0"}, + "memory": {"func": memory, "support": ">=0.0.0", "necessary": False}, + "mood": {"func": mood, "support": ">=0.0.0"}, + "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, + "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, + "groups": {"func": groups, "support": ">=0.0.0"}, + "others": {"func": others, "support": ">=0.0.0"}, } # 原地修改,将 字符串版本表达式 转换成 版本对象 @@ -395,7 +356,7 @@ class BotConfig: with open(config_path, "rb") as f: try: toml_dict = tomli.load(f) - except(tomli.TOMLDecodeError) as e: + except tomli.TOMLDecodeError as e: logger.critical(f"配置文件bot_config.toml填写有误,请检查第{e.lineno}行第{e.colno}处:{e.msg}") exit(1) @@ -410,7 +371,7 @@ class BotConfig: # 检查配置文件版本是否在支持范围内 if config.INNER_VERSION in group_specifierset: # 如果版本在支持范围内,检查是否存在通知 - if 'notice' in include_configs[key]: + if "notice" in include_configs[key]: logger.warning(include_configs[key]["notice"]) include_configs[key]["func"](toml_dict) @@ -424,7 +385,7 @@ class BotConfig: raise InvalidVersion(f"当前程序仅支持以下版本范围: {group_specifierset}") # 如果 necessary 项目存在,而且显式声明是 False,进入特殊处理 - elif "necessary" in include_configs[key] and include_configs[key].get("necessary") == False: + elif "necessary" in include_configs[key] and include_configs[key].get("necessary") is False: # 通过 pass 处理的项虽然直接忽略也是可以的,但是为了不增加理解困难,依然需要在这里显式处理 if key == "keywords_reaction": pass diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index b13e33e48..21541a78b 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -155,8 +155,8 @@ class CQCode: logger.error(f"最终请求失败: {str(e)}") time.sleep(1.5 ** retry) # 指数退避 - except Exception as e: - logger.exception(f"[未知错误]") + except Exception: + logger.exception("[未知错误]") return None return None @@ -281,7 +281,7 @@ class CQCode: logger.debug(f"合并后的转发消息: {combined_messages}") return f"[转发消息:\n{combined_messages}]" - except Exception as e: + except Exception: logger.exception("处理转发消息失败") return '[转发消息]' diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 973bcad2d..feba23076 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -51,8 +51,8 @@ class EmojiManager: self._initialized = True # 启动时执行一次完整性检查 self.check_emoji_file_integrity() - except Exception as e: - logger.exception(f"初始化表情管理器失败") + except Exception: + logger.exception("初始化表情管理器失败") def _ensure_db(self): """确保数据库已初始化""" @@ -87,8 +87,8 @@ class EmojiManager: {'_id': emoji_id}, {'$inc': {'usage_count': 1}} ) - except Exception as e: - logger.exception(f"记录表情使用失败") + except Exception: + logger.exception("记录表情使用失败") async def get_emoji_for_text(self, text: str) -> Optional[str]: """根据文本内容获取相关表情包 @@ -117,7 +117,7 @@ class EmojiManager: try: # 获取所有表情包 - all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'discription': 1})) + all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1})) if not all_emojis: logger.warning("数据库中没有任何表情包") @@ -160,9 +160,9 @@ class EmojiManager: {'$inc': {'usage_count': 1}} ) logger.success( - f"找到匹配的表情包: {selected_emoji.get('discription', '无描述')} (相似度: {similarity:.4f})") + f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})") # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 - return selected_emoji['path'], "[ %s ]" % selected_emoji.get('discription', '无描述') + return selected_emoji['path'], "[ %s ]" % selected_emoji.get('description', '无描述') except Exception as search_error: logger.error(f"搜索表情包失败: {str(search_error)}") @@ -174,7 +174,7 @@ class EmojiManager: logger.error(f"获取表情包失败: {str(e)}") return None - async def _get_emoji_discription(self, image_base64: str) -> str: + async def _get_emoji_description(self, image_base64: str) -> str: """获取表情包的标签""" try: prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感' @@ -203,7 +203,7 @@ class EmojiManager: try: prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。' - content, _ = await self.llm_emotion_judge.generate_response_async(prompt) + content, _ = await self.llm_emotion_judge.generate_response_async(prompt,temperature=1.5) logger.info(f"输出描述: {content}") return content @@ -236,36 +236,36 @@ class EmojiManager: continue # 获取表情包的描述 - discription = await self._get_emoji_discription(image_base64) + description = await self._get_emoji_description(image_base64) if global_config.EMOJI_CHECK: check = await self._check_emoji(image_base64) if '是' not in check: os.remove(image_path) - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") logger.info(f"其不满足过滤规则,被剔除 {check}") continue logger.info(f"check通过 {check}") - if discription is not None: - embedding = await get_embedding(discription) + if description is not None: + embedding = await get_embedding(description) # 准备数据库记录 emoji_record = { 'filename': filename, 'path': image_path, 'embedding': embedding, - 'discription': discription, + 'description': description, 'timestamp': int(time.time()) } # 保存到数据库 self.db.db['emoji'].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") - logger.info(f"描述: {discription}") + logger.info(f"描述: {description}") else: logger.warning(f"跳过表情包: {filename}") - except Exception as e: - logger.exception(f"扫描表情包失败") + except Exception: + logger.exception("扫描表情包失败") async def _periodic_scan(self, interval_MINS: int = 10): """定期扫描新表情包""" diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index f40c9a441..4e431d9fd 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -94,7 +94,7 @@ class ResponseGenerator: try: content, reasoning_content = await model.generate_response(prompt) except Exception: - logger.exception(f"生成回复时出错") + logger.exception("生成回复时出错") return None # 保存到数据库 @@ -146,7 +146,7 @@ class ResponseGenerator: return ["neutral"] except Exception: - logger.exception(f"获取情感标签时出错") + logger.exception("获取情感标签时出错") return ["neutral"] async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 4abbd3b3f..0fb40373e 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -61,7 +61,7 @@ class Message_Sender: auto_escape=auto_escape ) logger.debug(f"发送消息{message}成功") - except Exception as e: + except Exception: logger.exception(f"发送消息{message}失败") @@ -120,7 +120,7 @@ class MessageContainer: return True return False except Exception: - logger.exception(f"移除消息时发生错误") + logger.exception("移除消息时发生错误") return False def has_messages(self) -> bool: @@ -214,7 +214,7 @@ class MessageManager: if not container.remove_message(msg): logger.warning("尝试删除不存在的消息") except Exception: - logger.exception(f"处理超时消息时发生错误") + logger.exception("处理超时消息时发生错误") continue async def start_processor(self): diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 4cf21af19..0805caa5a 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -131,18 +131,19 @@ class PromptBuilder: probability_1 = global_config.PERSONALITY_1 probability_2 = global_config.PERSONALITY_2 probability_3 = global_config.PERSONALITY_3 - prompt_personality = '' + + prompt_personality = f'{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},你还有很多别名:{"/".join(global_config.BOT_ALIAS_NAMES)},' personality_choice = random.random() if personality_choice < probability_1: # 第一种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,平淡一些,尽量简短一些。{keywords_reaction_prompt} 请注意把握群里的聊天内容,不要刻意突出自身学科背景,不要回复的太有条理,可以有个性。''' elif personality_choice < probability_1 + probability_2: # 第二种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[1]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[1]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 请你表达自己的见解和观点。可以有个性。''' else: # 第三种人格 - prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, + prompt_personality += f'''{personality[2]}, 你正在浏览qq群,{promt_info_prompt}, 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 请你表达自己的见解和观点。可以有个性。''' diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index 1c2d05071..4081f8984 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -45,6 +45,6 @@ class MessageStorage: self.db.db.messages.insert_one(message_data) except Exception: - logger.exception(f"存储消息失败") + logger.exception("存储消息失败") # 如果需要其他存储相关的函数,可以在这里添加 diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 054526e94..6619f37af 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -53,19 +53,13 @@ def db_message_to_str(message_dict: Dict) -> str: return result -def is_mentioned_bot_in_message(message: Message) -> bool: - """检查消息是否提到了机器人""" - keywords = [global_config.BOT_NICKNAME] - for keyword in keywords: - if keyword in message.processed_plain_text: - return True - return False - - def is_mentioned_bot_in_txt(message: str) -> bool: """检查消息是否提到了机器人""" - keywords = [global_config.BOT_NICKNAME] - for keyword in keywords: + if global_config.BOT_NICKNAME is None: + return True + if global_config.BOT_NICKNAME in message: + return True + for keyword in global_config.BOT_ALIAS_NAMES: if keyword in message: return True return False diff --git a/src/plugins/knowledege/knowledge_library.py b/src/plugins/knowledege/knowledge_library.py index 481076961..4bf6227bb 100644 --- a/src/plugins/knowledege/knowledge_library.py +++ b/src/plugins/knowledege/knowledge_library.py @@ -79,7 +79,7 @@ class KnowledgeLibrary: content = f.read() # 按1024字符分段 - segments = [content[i:i+600] for i in range(0, len(content), 600)] + segments = [content[i:i+600] for i in range(0, len(content), 300)] # 处理每个分段 for segment in segments: diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 9b325b36d..0730f9e57 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -25,26 +25,46 @@ class Memory_graph: self.db = Database.get_instance() def connect_dot(self, concept1, concept2): - # 如果边已存在,增加 strength + # 避免自连接 + if concept1 == concept2: + return + + current_time = datetime.datetime.now().timestamp() + + # 如果边已存在,增加 strength if self.G.has_edge(concept1, concept2): self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1 + # 更新最后修改时间 + self.G[concept1][concept2]['last_modified'] = current_time else: - # 如果是新边,初始化 strength 为 1 - self.G.add_edge(concept1, concept2, strength=1) + # 如果是新边,初始化 strength 为 1 + self.G.add_edge(concept1, concept2, + strength=1, + created_time=current_time, # 添加创建时间 + last_modified=current_time) # 添加最后修改时间 def add_dot(self, concept, memory): + current_time = datetime.datetime.now().timestamp() + if concept in self.G: - # 如果节点已存在,将新记忆添加到现有列表中 if 'memory_items' in self.G.nodes[concept]: if not isinstance(self.G.nodes[concept]['memory_items'], list): - # 如果当前不是列表,将其转换为列表 self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']] self.G.nodes[concept]['memory_items'].append(memory) + # 更新最后修改时间 + self.G.nodes[concept]['last_modified'] = current_time else: self.G.nodes[concept]['memory_items'] = [memory] + # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time + if 'created_time' not in self.G.nodes[concept]: + self.G.nodes[concept]['created_time'] = current_time + self.G.nodes[concept]['last_modified'] = current_time else: - # 如果是新节点,创建新的记忆列表 - self.G.add_node(concept, memory_items=[memory]) + # 如果是新节点,创建新的记忆列表 + self.G.add_node(concept, + memory_items=[memory], + created_time=current_time, # 添加创建时间 + last_modified=current_time) # 添加最后修改时间 def get_dot(self, concept): # 检查节点是否存在于图中 @@ -191,15 +211,11 @@ class Hippocampus: async def memory_compress(self, messages: list, compress_rate=0.1): """压缩消息记录为记忆 - Args: - messages: 消息记录字典列表,每个字典包含text和time字段 - compress_rate: 压缩率 - Returns: - set: (话题, 记忆) 元组集合 + tuple: (压缩记忆集合, 相似主题字典) """ if not messages: - return set() + return set(), {} # 合并消息文本,同时保留时间信息 input_text = "" @@ -246,12 +262,33 @@ class Hippocampus: # 等待所有任务完成 compressed_memory = set() + similar_topics_dict = {} # 存储每个话题的相似主题列表 for topic, task in tasks: response = await task if response: compressed_memory.add((topic, response[0])) + # 为每个话题查找相似的已存在主题 + existing_topics = list(self.memory_graph.G.nodes()) + similar_topics = [] + + for existing_topic in existing_topics: + topic_words = set(jieba.cut(topic)) + existing_words = set(jieba.cut(existing_topic)) + + all_words = topic_words | existing_words + v1 = [1 if word in topic_words else 0 for word in all_words] + v2 = [1 if word in existing_words else 0 for word in all_words] + + similarity = cosine_similarity(v1, v2) + + if similarity >= 0.6: + similar_topics.append((existing_topic, similarity)) + + similar_topics.sort(key=lambda x: x[1], reverse=True) + similar_topics = similar_topics[:5] + similar_topics_dict[topic] = similar_topics - return compressed_memory + return compressed_memory, similar_topics_dict def calculate_topic_num(self, text, compress_rate): """计算文本的话题数量""" @@ -265,33 +302,40 @@ class Hippocampus: return topic_num async def operation_build_memory(self, chat_size=20): - # 最近消息获取频率 - time_frequency = {'near': 2, 'mid': 4, 'far': 2} - memory_sample = self.get_memory_sample(chat_size, time_frequency) - - for i, input_text in enumerate(memory_sample, 1): - # 加载进度可视化 + time_frequency = {'near': 3, 'mid': 8, 'far': 5} + memory_samples = self.get_memory_sample(chat_size, time_frequency) + + for i, messages in enumerate(memory_samples, 1): all_topics = [] - progress = (i / len(memory_sample)) * 100 + # 加载进度可视化 + progress = (i / len(memory_samples)) * 100 bar_length = 30 - filled_length = int(bar_length * i // len(memory_sample)) + filled_length = int(bar_length * i // len(memory_samples)) bar = '█' * filled_length + '-' * (bar_length - filled_length) - logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") + logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - # 生成压缩后记忆 ,表现为 (话题,记忆) 的元组 - compressed_memory = set() compress_rate = 0.1 - compressed_memory = await self.memory_compress(input_text, compress_rate) - logger.info(f"压缩后记忆数量: {len(compressed_memory)}") - - # 将记忆加入到图谱中 + compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) + logger.info(f"压缩后记忆数量: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") + for topic, memory in compressed_memory: logger.info(f"添加节点: {topic}") self.memory_graph.add_dot(topic, memory) - all_topics.append(topic) # 收集所有话题 + all_topics.append(topic) + + # 连接相似的已存在主题 + if topic in similar_topics_dict: + similar_topics = similar_topics_dict[topic] + for similar_topic, similarity in similar_topics: + if topic != similar_topic: + strength = int(similarity * 10) + logger.info(f"连接相似节点: {topic} 和 {similar_topic} (强度: {strength})") + self.memory_graph.G.add_edge(topic, similar_topic, strength=strength) + + # 连接同批次的相关话题 for i in range(len(all_topics)): for j in range(i + 1, len(all_topics)): - logger.info(f"连接节点: {all_topics[i]} 和 {all_topics[j]}") + logger.info(f"连接同批次节点: {all_topics[i]} 和 {all_topics[j]}") self.memory_graph.connect_dot(all_topics[i], all_topics[j]) self.sync_memory_to_db() @@ -302,7 +346,7 @@ class Hippocampus: db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) memory_nodes = list(self.memory_graph.G.nodes(data=True)) - # 转换数据库节点为字典格式,方便查找 + # 转换数据库节点为字典格式,方便查找 db_nodes_dict = {node['concept']: node for node in db_nodes} # 检查并更新节点 @@ -313,13 +357,19 @@ class Hippocampus: # 计算内存中节点的特征值 memory_hash = self.calculate_node_hash(concept, memory_items) + + # 获取时间信息 + created_time = data.get('created_time', datetime.datetime.now().timestamp()) + last_modified = data.get('last_modified', datetime.datetime.now().timestamp()) if concept not in db_nodes_dict: - # 数据库中缺少的节点,添加 + # 数据库中缺少的节点,添加 node_data = { 'concept': concept, 'memory_items': memory_items, - 'hash': memory_hash + 'hash': memory_hash, + 'created_time': created_time, + 'last_modified': last_modified } self.memory_graph.db.db.graph_data.nodes.insert_one(node_data) else: @@ -327,25 +377,21 @@ class Hippocampus: db_node = db_nodes_dict[concept] db_hash = db_node.get('hash', None) - # 如果特征值不同,则更新节点 + # 如果特征值不同,则更新节点 if db_hash != memory_hash: self.memory_graph.db.db.graph_data.nodes.update_one( {'concept': concept}, {'$set': { 'memory_items': memory_items, - 'hash': memory_hash + 'hash': memory_hash, + 'created_time': created_time, + 'last_modified': last_modified }} ) - # 检查并删除数据库中多余的节点 - memory_concepts = set(node[0] for node in memory_nodes) - for db_node in db_nodes: - if db_node['concept'] not in memory_concepts: - self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) - # 处理边的信息 db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) - memory_edges = list(self.memory_graph.G.edges()) + memory_edges = list(self.memory_graph.G.edges(data=True)) # 创建边的哈希值字典 db_edge_dict = {} @@ -357,10 +403,14 @@ class Hippocampus: } # 检查并更新边 - for source, target in memory_edges: + for source, target, data in memory_edges: edge_hash = self.calculate_edge_hash(source, target) edge_key = (source, target) - strength = self.memory_graph.G[source][target].get('strength', 1) + strength = data.get('strength', 1) + + # 获取边的时间信息 + created_time = data.get('created_time', datetime.datetime.now().timestamp()) + last_modified = data.get('last_modified', datetime.datetime.now().timestamp()) if edge_key not in db_edge_dict: # 添加新边 @@ -368,7 +418,9 @@ class Hippocampus: 'source': source, 'target': target, 'strength': strength, - 'hash': edge_hash + 'hash': edge_hash, + 'created_time': created_time, + 'last_modified': last_modified } self.memory_graph.db.db.graph_data.edges.insert_one(edge_data) else: @@ -378,20 +430,12 @@ class Hippocampus: {'source': source, 'target': target}, {'$set': { 'hash': edge_hash, - 'strength': strength + 'strength': strength, + 'created_time': created_time, + 'last_modified': last_modified }} ) - # 删除多余的边 - memory_edge_set = set(memory_edges) - for edge_key in db_edge_dict: - if edge_key not in memory_edge_set: - source, target = edge_key - self.memory_graph.db.db.graph_data.edges.delete_one({ - 'source': source, - 'target': target - }) - def sync_memory_from_db(self): """从数据库同步数据到内存中的图结构""" # 清空当前图 @@ -405,61 +449,107 @@ class Hippocampus: # 确保memory_items是列表 if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] + + # 获取时间信息 + created_time = node.get('created_time', datetime.datetime.now().timestamp()) + last_modified = node.get('last_modified', datetime.datetime.now().timestamp()) + # 添加节点到图中 - self.memory_graph.G.add_node(concept, memory_items=memory_items) + self.memory_graph.G.add_node(concept, + memory_items=memory_items, + created_time=created_time, + last_modified=last_modified) # 从数据库加载所有边 edges = self.memory_graph.db.db.graph_data.edges.find() for edge in edges: source = edge['source'] target = edge['target'] - strength = edge.get('strength', 1) # 获取 strength,默认为 1 + strength = edge.get('strength', 1) # 获取 strength,默认为 1 + + # 获取时间信息 + created_time = edge.get('created_time', datetime.datetime.now().timestamp()) + last_modified = edge.get('last_modified', datetime.datetime.now().timestamp()) + # 只有当源节点和目标节点都存在时才添加边 if source in self.memory_graph.G and target in self.memory_graph.G: - self.memory_graph.G.add_edge(source, target, strength=strength) + self.memory_graph.G.add_edge(source, target, + strength=strength, + created_time=created_time, + last_modified=last_modified) async def operation_forget_topic(self, percentage=0.1): - """随机选择图中一定比例的节点进行检查,根据条件决定是否遗忘""" - # 获取所有节点 + """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘""" all_nodes = list(self.memory_graph.G.nodes()) - # 计算要检查的节点数量 - check_count = max(1, int(len(all_nodes) * percentage)) - # 随机选择节点 - nodes_to_check = random.sample(all_nodes, check_count) - - forgotten_nodes = [] + all_edges = list(self.memory_graph.G.edges()) + + check_nodes_count = max(1, int(len(all_nodes) * percentage)) + check_edges_count = max(1, int(len(all_edges) * percentage)) + + nodes_to_check = random.sample(all_nodes, check_nodes_count) + edges_to_check = random.sample(all_edges, check_edges_count) + + edge_changes = {'weakened': 0, 'removed': 0} + node_changes = {'reduced': 0, 'removed': 0} + + current_time = datetime.datetime.now().timestamp() + + # 检查并遗忘连接 + logger.info("开始检查连接...") + for source, target in edges_to_check: + edge_data = self.memory_graph.G[source][target] + last_modified = edge_data.get('last_modified') + # print(source,target) + # print(f"float(last_modified):{float(last_modified)}" ) + # print(f"current_time:{current_time}") + # print(f"current_time - last_modified:{current_time - last_modified}") + if current_time - last_modified > 3600*24: # test + current_strength = edge_data.get('strength', 1) + new_strength = current_strength - 1 + + if new_strength <= 0: + self.memory_graph.G.remove_edge(source, target) + edge_changes['removed'] += 1 + logger.info(f"\033[1;31m[连接移除]\033[0m {source} - {target}") + else: + edge_data['strength'] = new_strength + edge_data['last_modified'] = current_time + edge_changes['weakened'] += 1 + logger.info(f"\033[1;34m[连接减弱]\033[0m {source} - {target} (强度: {current_strength} -> {new_strength})") + + # 检查并遗忘话题 + logger.info("开始检查节点...") for node in nodes_to_check: - # 获取节点的连接数 - connections = self.memory_graph.G.degree(node) - - # 获取节点的内容条数 - memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - - # 检查连接强度 - weak_connections = True - if connections > 1: # 只有当连接数大于1时才检查强度 - for neighbor in self.memory_graph.G.neighbors(node): - strength = self.memory_graph.G[node][neighbor].get('strength', 1) - if strength > 2: - weak_connections = False - break - - # 如果满足遗忘条件 - if (connections <= 1 and weak_connections) or content_count <= 2: - removed_item = self.memory_graph.forget_topic(node) - if removed_item: - forgotten_nodes.append((node, removed_item)) - logger.debug(f"遗忘节点 {node} 的记忆: {removed_item}") - - # 同步到数据库 - if forgotten_nodes: + node_data = self.memory_graph.G.nodes[node] + last_modified = node_data.get('last_modified', current_time) + + if current_time - last_modified > 3600*24: # test + memory_items = node_data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + current_count = len(memory_items) + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + if memory_items: + self.memory_graph.G.nodes[node]['memory_items'] = memory_items + self.memory_graph.G.nodes[node]['last_modified'] = current_time + node_changes['reduced'] += 1 + logger.info(f"\033[1;33m[记忆减少]\033[0m {node} (记忆数量: {current_count} -> {len(memory_items)})") + else: + self.memory_graph.G.remove_node(node) + node_changes['removed'] += 1 + logger.info(f"\033[1;31m[节点移除]\033[0m {node}") + + if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): self.sync_memory_to_db() - logger.debug(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") + logger.info("\n遗忘操作统计:") + logger.info(f"连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") + logger.info(f"节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") else: - logger.debug("本次检查没有节点满足遗忘条件") + logger.info("\n本次检查没有节点或连接满足遗忘条件") async def merge_memory(self, topic): """ @@ -486,7 +576,7 @@ class Hippocampus: logger.debug(f"选择的记忆:\n{merged_text}") # 使用memory_compress生成新的压缩记忆 - compressed_memories = await self.memory_compress(selected_memories, 0.1) + compressed_memories, _ = await self.memory_compress(selected_memories, 0.1) # 从原记忆列表中移除被选中的记忆 for memory in selected_memories: diff --git a/src/plugins/memory_system/memory_test1.py b/src/plugins/memory_system/memory_test1.py new file mode 100644 index 000000000..bbd734ec2 --- /dev/null +++ b/src/plugins/memory_system/memory_test1.py @@ -0,0 +1,1208 @@ +# -*- coding: utf-8 -*- +import datetime +import math +import os +import random +import sys +import time +from collections import Counter +from pathlib import Path + +import matplotlib.pyplot as plt +import networkx as nx +import pymongo +from dotenv import load_dotenv +from loguru import logger +import jieba + +''' +该理论认为,当两个或多个事物在形态上具有相似性时, +它们在记忆中会形成关联。 +例如,梨和苹果在形状和都是水果这一属性上有相似性, +所以当我们看到梨时,很容易通过形态学联想记忆联想到苹果。 +这种相似性联想有助于我们对新事物进行分类和理解, +当遇到一个新的类似水果时, +我们可以通过与已有的水果记忆进行相似性匹配, +来推测它的一些特征。 + + + +时空关联性联想: +除了相似性联想,MAM 还强调时空关联性联想。 +如果两个事物在时间或空间上经常同时出现,它们也会在记忆中形成关联。 +比如,每次在公园里看到花的时候,都能听到鸟儿的叫声, +那么花和鸟儿叫声的形态特征(花的视觉形态和鸟叫的听觉形态)就会在记忆中形成关联, +以后听到鸟叫可能就会联想到公园里的花。 + +''' + +# from chat.config import global_config +sys.path.append("C:/GitHub/MaiMBot") # 添加项目根目录到 Python 路径 +from src.common.database import Database +from src.plugins.memory_system.offline_llm import LLMModel + +# 获取当前文件的目录 +current_dir = Path(__file__).resolve().parent +# 获取项目根目录(上三层目录) +project_root = current_dir.parent.parent.parent +# env.dev文件路径 +env_path = project_root / ".env.dev" + +# 加载环境变量 +if env_path.exists(): + logger.info(f"从 {env_path} 加载环境变量") + load_dotenv(env_path) +else: + logger.warning(f"未找到环境变量文件: {env_path}") + logger.info("将使用默认配置") + +class Database: + _instance = None + db = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + if not Database.db: + Database.initialize( + host=os.getenv("MONGODB_HOST"), + port=int(os.getenv("MONGODB_PORT")), + db_name=os.getenv("DATABASE_NAME"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE") + ) + + @classmethod + def initialize(cls, host, port, db_name, username=None, password=None, auth_source="admin"): + try: + if username and password: + uri = f"mongodb://{username}:{password}@{host}:{port}/{db_name}?authSource={auth_source}" + else: + uri = f"mongodb://{host}:{port}" + + client = pymongo.MongoClient(uri) + cls.db = client[db_name] + # 测试连接 + client.server_info() + logger.success("MongoDB连接成功!") + + except Exception as e: + logger.error(f"初始化MongoDB失败: {str(e)}") + raise + +def calculate_information_content(text): + """计算文本的信息量(熵)""" + char_count = Counter(text) + total_chars = len(text) + + entropy = 0 + for count in char_count.values(): + probability = count / total_chars + entropy -= probability * math.log2(probability) + + return entropy + +def get_cloest_chat_from_db(db, length: int, timestamp: str): + """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 + + Returns: + list: 消息记录字典列表,每个字典包含消息内容和时间信息 + """ + chat_records = [] + closest_record = db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) + + if closest_record and closest_record.get('memorized', 0) < 4: + closest_time = closest_record['time'] + group_id = closest_record['group_id'] + # 获取该时间戳之后的length条消息,且groupid相同 + records = list(db.db.messages.find( + {"time": {"$gt": closest_time}, "group_id": group_id} + ).sort('time', 1).limit(length)) + + # 更新每条消息的memorized属性 + for record in records: + current_memorized = record.get('memorized', 0) + if current_memorized > 3: + print("消息已读取3次,跳过") + return '' + + # 更新memorized值 + db.db.messages.update_one( + {"_id": record["_id"]}, + {"$set": {"memorized": current_memorized + 1}} + ) + + # 添加到记录列表中 + chat_records.append({ + 'text': record["detailed_plain_text"], + 'time': record["time"], + 'group_id': record["group_id"] + }) + + return chat_records + +class Memory_cortex: + def __init__(self, memory_graph: 'Memory_graph'): + self.memory_graph = memory_graph + + def sync_memory_from_db(self): + """ + 从数据库同步数据到内存中的图结构 + 将清空当前内存中的图,并从数据库重新加载所有节点和边 + """ + # 清空当前图 + self.memory_graph.G.clear() + + # 获取当前时间作为默认时间 + default_time = datetime.datetime.now().timestamp() + + # 从数据库加载所有节点 + nodes = self.memory_graph.db.db.graph_data.nodes.find() + for node in nodes: + concept = node['concept'] + memory_items = node.get('memory_items', []) + # 确保memory_items是列表 + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 获取时间属性,如果不存在则使用默认时间 + created_time = node.get('created_time') + last_modified = node.get('last_modified') + + # 如果时间属性不存在,则更新数据库 + if created_time is None or last_modified is None: + created_time = default_time + last_modified = default_time + # 更新数据库中的节点 + self.memory_graph.db.db.graph_data.nodes.update_one( + {'concept': concept}, + {'$set': { + 'created_time': created_time, + 'last_modified': last_modified + }} + ) + logger.info(f"为节点 {concept} 添加默认时间属性") + + # 添加节点到图中,包含时间属性 + self.memory_graph.G.add_node(concept, + memory_items=memory_items, + created_time=created_time, + last_modified=last_modified) + + # 从数据库加载所有边 + edges = self.memory_graph.db.db.graph_data.edges.find() + for edge in edges: + source = edge['source'] + target = edge['target'] + + # 只有当源节点和目标节点都存在时才添加边 + if source in self.memory_graph.G and target in self.memory_graph.G: + # 获取时间属性,如果不存在则使用默认时间 + created_time = edge.get('created_time') + last_modified = edge.get('last_modified') + + # 如果时间属性不存在,则更新数据库 + if created_time is None or last_modified is None: + created_time = default_time + last_modified = default_time + # 更新数据库中的边 + self.memory_graph.db.db.graph_data.edges.update_one( + {'source': source, 'target': target}, + {'$set': { + 'created_time': created_time, + 'last_modified': last_modified + }} + ) + logger.info(f"为边 {source} - {target} 添加默认时间属性") + + self.memory_graph.G.add_edge(source, target, + strength=edge.get('strength', 1), + created_time=created_time, + last_modified=last_modified) + + logger.success("从数据库同步记忆图谱完成") + + def calculate_node_hash(self, concept, memory_items): + """ + 计算节点的特征值 + """ + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + # 将记忆项排序以确保相同内容生成相同的哈希值 + sorted_items = sorted(memory_items) + # 组合概念和记忆项生成特征值 + content = f"{concept}:{'|'.join(sorted_items)}" + return hash(content) + + def calculate_edge_hash(self, source, target): + """ + 计算边的特征值 + """ + # 对源节点和目标节点排序以确保相同的边生成相同的哈希值 + nodes = sorted([source, target]) + return hash(f"{nodes[0]}:{nodes[1]}") + + def sync_memory_to_db(self): + """ + 检查并同步内存中的图结构与数据库 + 使用特征值(哈希值)快速判断是否需要更新 + """ + current_time = datetime.datetime.now().timestamp() + + # 获取数据库中所有节点和内存中所有节点 + db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + + # 转换数据库节点为字典格式,方便查找 + db_nodes_dict = {node['concept']: node for node in db_nodes} + + # 检查并更新节点 + for concept, data in memory_nodes: + memory_items = data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 计算内存中节点的特征值 + memory_hash = self.calculate_node_hash(concept, memory_items) + + if concept not in db_nodes_dict: + # 数据库中缺少的节点,添加 + node_data = { + 'concept': concept, + 'memory_items': memory_items, + 'hash': memory_hash, + 'created_time': data.get('created_time', current_time), + 'last_modified': data.get('last_modified', current_time) + } + self.memory_graph.db.db.graph_data.nodes.insert_one(node_data) + else: + # 获取数据库中节点的特征值 + db_node = db_nodes_dict[concept] + db_hash = db_node.get('hash', None) + + # 如果特征值不同,则更新节点 + if db_hash != memory_hash: + self.memory_graph.db.db.graph_data.nodes.update_one( + {'concept': concept}, + {'$set': { + 'memory_items': memory_items, + 'hash': memory_hash, + 'last_modified': current_time + }} + ) + + # 检查并删除数据库中多余的节点 + memory_concepts = set(node[0] for node in memory_nodes) + for db_node in db_nodes: + if db_node['concept'] not in memory_concepts: + self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']}) + + # 处理边的信息 + db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 创建边的哈希值字典 + db_edge_dict = {} + for edge in db_edges: + edge_hash = self.calculate_edge_hash(edge['source'], edge['target']) + db_edge_dict[(edge['source'], edge['target'])] = { + 'hash': edge_hash, + 'strength': edge.get('strength', 1) + } + + # 检查并更新边 + for source, target, data in memory_edges: + edge_hash = self.calculate_edge_hash(source, target) + edge_key = (source, target) + strength = data.get('strength', 1) + + if edge_key not in db_edge_dict: + # 添加新边 + edge_data = { + 'source': source, + 'target': target, + 'strength': strength, + 'hash': edge_hash, + 'created_time': data.get('created_time', current_time), + 'last_modified': data.get('last_modified', current_time) + } + self.memory_graph.db.db.graph_data.edges.insert_one(edge_data) + else: + # 检查边的特征值是否变化 + if db_edge_dict[edge_key]['hash'] != edge_hash: + self.memory_graph.db.db.graph_data.edges.update_one( + {'source': source, 'target': target}, + {'$set': { + 'hash': edge_hash, + 'strength': strength, + 'last_modified': current_time + }} + ) + + # 删除多余的边 + memory_edge_set = set((source, target) for source, target, _ in memory_edges) + for edge_key in db_edge_dict: + if edge_key not in memory_edge_set: + source, target = edge_key + self.memory_graph.db.db.graph_data.edges.delete_one({ + 'source': source, + 'target': target + }) + + logger.success("完成记忆图谱与数据库的差异同步") + + def remove_node_from_db(self, topic): + """ + 从数据库中删除指定节点及其相关的边 + + Args: + topic: 要删除的节点概念 + """ + # 删除节点 + self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': topic}) + # 删除所有涉及该节点的边 + self.memory_graph.db.db.graph_data.edges.delete_many({ + '$or': [ + {'source': topic}, + {'target': topic} + ] + }) + +class Memory_graph: + def __init__(self): + self.G = nx.Graph() # 使用 networkx 的图结构 + self.db = Database.get_instance() + + def connect_dot(self, concept1, concept2): + # 避免自连接 + if concept1 == concept2: + return + + current_time = datetime.datetime.now().timestamp() + + # 如果边已存在,增加 strength + if self.G.has_edge(concept1, concept2): + self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1 + # 更新最后修改时间 + self.G[concept1][concept2]['last_modified'] = current_time + else: + # 如果是新边,初始化 strength 为 1 + self.G.add_edge(concept1, concept2, + strength=1, + created_time=current_time, + last_modified=current_time) + + def add_dot(self, concept, memory): + current_time = datetime.datetime.now().timestamp() + + if concept in self.G: + # 如果节点已存在,将新记忆添加到现有列表中 + if 'memory_items' in self.G.nodes[concept]: + if not isinstance(self.G.nodes[concept]['memory_items'], list): + # 如果当前不是列表,将其转换为列表 + self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']] + self.G.nodes[concept]['memory_items'].append(memory) + # 更新最后修改时间 + self.G.nodes[concept]['last_modified'] = current_time + else: + self.G.nodes[concept]['memory_items'] = [memory] + self.G.nodes[concept]['last_modified'] = current_time + else: + # 如果是新节点,创建新的记忆列表 + self.G.add_node(concept, + memory_items=[memory], + created_time=current_time, + last_modified=current_time) + + def get_dot(self, concept): + # 检查节点是否存在于图中 + if concept in self.G: + # 从图中获取节点数据 + node_data = self.G.nodes[concept] + return concept, node_data + return None + + def get_related_item(self, topic, depth=1): + if topic not in self.G: + return [], [] + + first_layer_items = [] + second_layer_items = [] + + # 获取相邻节点 + neighbors = list(self.G.neighbors(topic)) + + # 获取当前节点的记忆项 + node_data = self.get_dot(topic) + if node_data: + concept, data = node_data + if 'memory_items' in data: + memory_items = data['memory_items'] + if isinstance(memory_items, list): + first_layer_items.extend(memory_items) + else: + first_layer_items.append(memory_items) + + # 只在depth=2时获取第二层记忆 + if depth >= 2: + # 获取相邻节点的记忆项 + for neighbor in neighbors: + node_data = self.get_dot(neighbor) + if node_data: + concept, data = node_data + if 'memory_items' in data: + memory_items = data['memory_items'] + if isinstance(memory_items, list): + second_layer_items.extend(memory_items) + else: + second_layer_items.append(memory_items) + + return first_layer_items, second_layer_items + + @property + def dots(self): + # 返回所有节点对应的 Memory_dot 对象 + return [self.get_dot(node) for node in self.G.nodes()] + +# 海马体 +class Hippocampus: + def __init__(self, memory_graph: Memory_graph): + self.memory_graph = memory_graph + self.memory_cortex = Memory_cortex(memory_graph) + self.llm_model = LLMModel() + self.llm_model_small = LLMModel(model_name="deepseek-ai/DeepSeek-V2.5") + self.llm_model_get_topic = LLMModel(model_name="Pro/Qwen/Qwen2.5-7B-Instruct") + self.llm_model_summary = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct") + + def get_memory_sample(self, chat_size=20, time_frequency:dict={'near':2,'mid':4,'far':3}): + """获取记忆样本 + + Returns: + list: 消息记录列表,每个元素是一个消息记录字典列表 + """ + current_timestamp = datetime.datetime.now().timestamp() + chat_samples = [] + + # 短期:1h 中期:4h 长期:24h + for _ in range(time_frequency.get('near')): + random_time = current_timestamp - random.randint(1, 3600*4) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('mid')): + random_time = current_timestamp - random.randint(3600*4, 3600*24) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + for _ in range(time_frequency.get('far')): + random_time = current_timestamp - random.randint(3600*24, 3600*24*7) + messages = get_cloest_chat_from_db(db=self.memory_graph.db, length=chat_size, timestamp=random_time) + if messages: + chat_samples.append(messages) + + return chat_samples + + def calculate_topic_num(self,text, compress_rate): + """计算文本的话题数量""" + information_content = calculate_information_content(text) + topic_by_length = text.count('\n')*compress_rate + topic_by_information_content = max(1, min(5, int((information_content-3) * 2))) + topic_num = int((topic_by_length + topic_by_information_content)/2) + print(f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, topic_num: {topic_num}") + return topic_num + + async def memory_compress(self, messages: list, compress_rate=0.1): + """压缩消息记录为记忆 + + Args: + messages: 消息记录字典列表,每个字典包含text和time字段 + compress_rate: 压缩率 + + Returns: + tuple: (压缩记忆集合, 相似主题字典) + - 压缩记忆集合: set of (话题, 记忆) 元组 + - 相似主题字典: dict of {话题: [(相似主题, 相似度), ...]} + """ + if not messages: + return set(), {} + + # 合并消息文本,同时保留时间信息 + input_text = "" + time_info = "" + # 计算最早和最晚时间 + earliest_time = min(msg['time'] for msg in messages) + latest_time = max(msg['time'] for msg in messages) + + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) + latest_dt = datetime.datetime.fromtimestamp(latest_time) + + # 如果是同一年 + if earliest_dt.year == latest_dt.year: + earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%m-%d %H:%M:%S") + time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" + else: + earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" + + for msg in messages: + input_text += f"{msg['text']}\n" + + print(input_text) + + topic_num = self.calculate_topic_num(input_text, compress_rate) + topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(input_text, topic_num)) + + # 过滤topics + filter_keywords = ['表情包', '图片', '回复', '聊天记录'] + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] + + print(f"过滤后话题: {filtered_topics}") + + # 为每个话题查找相似的已存在主题 + print("\n检查相似主题:") + similar_topics_dict = {} # 存储每个话题的相似主题列表 + + for topic in filtered_topics: + # 获取所有现有节点 + existing_topics = list(self.memory_graph.G.nodes()) + similar_topics = [] + + # 对每个现有节点计算相似度 + for existing_topic in existing_topics: + # 使用jieba分词并计算余弦相似度 + topic_words = set(jieba.cut(topic)) + existing_words = set(jieba.cut(existing_topic)) + + # 计算词向量 + all_words = topic_words | existing_words + v1 = [1 if word in topic_words else 0 for word in all_words] + v2 = [1 if word in existing_words else 0 for word in all_words] + + # 计算余弦相似度 + similarity = cosine_similarity(v1, v2) + + # 如果相似度超过阈值,添加到结果中 + if similarity >= 0.6: # 设置相似度阈值 + similar_topics.append((existing_topic, similarity)) + + # 按相似度降序排序 + similar_topics.sort(key=lambda x: x[1], reverse=True) + # 只保留前5个最相似的主题 + similar_topics = similar_topics[:5] + + # 存储到字典中 + similar_topics_dict[topic] = similar_topics + + # 输出结果 + if similar_topics: + print(f"\n主题「{topic}」的相似主题:") + for similar_topic, score in similar_topics: + print(f"- {similar_topic} (相似度: {score:.3f})") + else: + print(f"\n主题「{topic}」没有找到相似主题") + + # 创建所有话题的请求任务 + tasks = [] + for topic in filtered_topics: + topic_what_prompt = self.topic_what(input_text, topic , time_info) + # 创建异步任务 + task = self.llm_model_small.generate_response_async(topic_what_prompt) + tasks.append((topic.strip(), task)) + + # 等待所有任务完成 + compressed_memory = set() + for topic, task in tasks: + response = await task + if response: + compressed_memory.add((topic, response[0])) + + return compressed_memory, similar_topics_dict + + async def operation_build_memory(self, chat_size=12): + # 最近消息获取频率 + time_frequency = {'near': 3, 'mid': 8, 'far': 5} + memory_samples = self.get_memory_sample(chat_size, time_frequency) + + all_topics = [] # 用于存储所有话题 + + for i, messages in enumerate(memory_samples, 1): + # 加载进度可视化 + all_topics = [] + progress = (i / len(memory_samples)) * 100 + bar_length = 30 + filled_length = int(bar_length * i // len(memory_samples)) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") + + # 生成压缩后记忆 + compress_rate = 0.1 + compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) + print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") + + # 将记忆加入到图谱中 + for topic, memory in compressed_memory: + print(f"\033[1;32m添加节点\033[0m: {topic}") + self.memory_graph.add_dot(topic, memory) + all_topics.append(topic) + + # 连接相似的已存在主题 + if topic in similar_topics_dict: + similar_topics = similar_topics_dict[topic] + for similar_topic, similarity in similar_topics: + # 避免自连接 + if topic != similar_topic: + # 根据相似度设置连接强度 + strength = int(similarity * 10) # 将0.3-1.0的相似度映射到3-10的强度 + print(f"\033[1;36m连接相似节点\033[0m: {topic} 和 {similar_topic} (强度: {strength})") + # 使用相似度作为初始连接强度 + self.memory_graph.G.add_edge(topic, similar_topic, strength=strength) + + # 连接同批次的相关话题 + for i in range(len(all_topics)): + for j in range(i + 1, len(all_topics)): + print(f"\033[1;32m连接同批次节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") + self.memory_graph.connect_dot(all_topics[i], all_topics[j]) + + self.memory_cortex.sync_memory_to_db() + + def forget_connection(self, source, target): + """ + 检查并可能遗忘一个连接 + + Args: + source: 连接的源节点 + target: 连接的目标节点 + + Returns: + tuple: (是否有变化, 变化类型, 变化详情) + 变化类型: 0-无变化, 1-强度减少, 2-连接移除 + """ + current_time = datetime.datetime.now().timestamp() + # 获取边的属性 + edge_data = self.memory_graph.G[source][target] + last_modified = edge_data.get('last_modified', current_time) + + # 如果连接超过7天未更新 + if current_time - last_modified > 6000: # test + # 获取当前强度 + current_strength = edge_data.get('strength', 1) + # 减少连接强度 + new_strength = current_strength - 1 + edge_data['strength'] = new_strength + edge_data['last_modified'] = current_time + + # 如果强度降为0,移除连接 + if new_strength <= 0: + self.memory_graph.G.remove_edge(source, target) + return True, 2, f"移除连接: {source} - {target} (强度降至0)" + else: + return True, 1, f"减弱连接: {source} - {target} (强度: {current_strength} -> {new_strength})" + + return False, 0, "" + + def forget_topic(self, topic): + """ + 检查并可能遗忘一个话题的记忆 + + Args: + topic: 要检查的话题 + + Returns: + tuple: (是否有变化, 变化类型, 变化详情) + 变化类型: 0-无变化, 1-记忆减少, 2-节点移除 + """ + current_time = datetime.datetime.now().timestamp() + # 获取节点的最后修改时间 + node_data = self.memory_graph.G.nodes[topic] + last_modified = node_data.get('last_modified', current_time) + + # 如果话题超过7天未更新 + if current_time - last_modified > 3000: # test + memory_items = node_data.get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + # 获取当前记忆数量 + current_count = len(memory_items) + # 随机选择一条记忆删除 + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + if memory_items: + # 更新节点的记忆项和最后修改时间 + self.memory_graph.G.nodes[topic]['memory_items'] = memory_items + self.memory_graph.G.nodes[topic]['last_modified'] = current_time + return True, 1, f"减少记忆: {topic} (记忆数量: {current_count} -> {len(memory_items)})\n被移除的记忆: {removed_item}" + else: + # 如果没有记忆了,删除节点及其所有连接 + self.memory_graph.G.remove_node(topic) + return True, 2, f"移除节点: {topic} (无剩余记忆)\n最后一条记忆: {removed_item}" + + return False, 0, "" + + async def operation_forget_topic(self, percentage=0.1): + """ + 随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘 + + Args: + percentage: 要检查的节点和边的比例,默认为0.1(10%) + """ + # 获取所有节点和边 + all_nodes = list(self.memory_graph.G.nodes()) + all_edges = list(self.memory_graph.G.edges()) + + # 计算要检查的数量 + check_nodes_count = max(1, int(len(all_nodes) * percentage)) + check_edges_count = max(1, int(len(all_edges) * percentage)) + + # 随机选择要检查的节点和边 + nodes_to_check = random.sample(all_nodes, check_nodes_count) + edges_to_check = random.sample(all_edges, check_edges_count) + + # 用于统计不同类型的变化 + edge_changes = {'weakened': 0, 'removed': 0} + node_changes = {'reduced': 0, 'removed': 0} + + # 检查并遗忘连接 + print("\n开始检查连接...") + for source, target in edges_to_check: + changed, change_type, details = self.forget_connection(source, target) + if changed: + if change_type == 1: + edge_changes['weakened'] += 1 + logger.info(f"\033[1;34m[连接减弱]\033[0m {details}") + elif change_type == 2: + edge_changes['removed'] += 1 + logger.info(f"\033[1;31m[连接移除]\033[0m {details}") + + # 检查并遗忘话题 + print("\n开始检查节点...") + for node in nodes_to_check: + changed, change_type, details = self.forget_topic(node) + if changed: + if change_type == 1: + node_changes['reduced'] += 1 + logger.info(f"\033[1;33m[记忆减少]\033[0m {details}") + elif change_type == 2: + node_changes['removed'] += 1 + logger.info(f"\033[1;31m[节点移除]\033[0m {details}") + + # 同步到数据库 + if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): + self.memory_cortex.sync_memory_to_db() + print("\n遗忘操作统计:") + print(f"连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") + print(f"节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") + else: + print("\n本次检查没有节点或连接满足遗忘条件") + + async def merge_memory(self, topic): + """ + 对指定话题的记忆进行合并压缩 + + Args: + topic: 要合并的话题节点 + """ + # 获取节点的记忆项 + memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 如果记忆项不足,直接返回 + if len(memory_items) < 10: + return + + # 随机选择10条记忆 + selected_memories = random.sample(memory_items, 10) + + # 拼接成文本 + merged_text = "\n".join(selected_memories) + print(f"\n[合并记忆] 话题: {topic}") + print(f"选择的记忆:\n{merged_text}") + + # 使用memory_compress生成新的压缩记忆 + compressed_memories, _ = await self.memory_compress(selected_memories, 0.1) + + # 从原记忆列表中移除被选中的记忆 + for memory in selected_memories: + memory_items.remove(memory) + + # 添加新的压缩记忆 + for _, compressed_memory in compressed_memories: + memory_items.append(compressed_memory) + print(f"添加压缩记忆: {compressed_memory}") + + # 更新节点的记忆项 + self.memory_graph.G.nodes[topic]['memory_items'] = memory_items + print(f"完成记忆合并,当前记忆数量: {len(memory_items)}") + + async def operation_merge_memory(self, percentage=0.1): + """ + 随机检查一定比例的节点,对内容数量超过100的节点进行记忆合并 + + Args: + percentage: 要检查的节点比例,默认为0.1(10%) + """ + # 获取所有节点 + all_nodes = list(self.memory_graph.G.nodes()) + # 计算要检查的节点数量 + check_count = max(1, int(len(all_nodes) * percentage)) + # 随机选择节点 + nodes_to_check = random.sample(all_nodes, check_count) + + merged_nodes = [] + for node in nodes_to_check: + # 获取节点的内容条数 + memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + + # 如果内容数量超过100,进行合并 + if content_count > 100: + print(f"\n检查节点: {node}, 当前记忆数量: {content_count}") + await self.merge_memory(node) + merged_nodes.append(node) + + # 同步到数据库 + if merged_nodes: + self.memory_cortex.sync_memory_to_db() + print(f"\n完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") + else: + print("\n本次检查没有需要合并的节点") + + async def _identify_topics(self, text: str) -> list: + """从文本中识别可能的主题""" + topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(text, 5)) + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + return topics + + def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: + """查找与给定主题相似的记忆主题""" + all_memory_topics = list(self.memory_graph.G.nodes()) + all_similar_topics = [] + + for topic in topics: + if debug_info: + pass + + topic_vector = text_to_vector(topic) + has_similar_topic = False + + for memory_topic in all_memory_topics: + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + similarity = cosine_similarity(v1, v2) + + if similarity >= similarity_threshold: + has_similar_topic = True + all_similar_topics.append((memory_topic, similarity)) + + return all_similar_topics + + def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: + """获取相似度最高的主题""" + seen_topics = set() + top_topics = [] + + for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): + if topic not in seen_topics and len(top_topics) < max_topics: + seen_topics.add(topic) + top_topics.append((topic, score)) + + return top_topics + + async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: + """计算输入文本对记忆的激活程度""" + logger.info(f"[记忆激活]识别主题: {await self._identify_topics(text)}") + + identified_topics = await self._identify_topics(text) + if not identified_topics: + return 0 + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆激活" + ) + + if not all_similar_topics: + return 0 + + top_topics = self._get_top_topics(all_similar_topics, max_topics) + + if len(top_topics) == 1: + topic, score = top_topics[0] + memory_items = self.memory_graph.G.nodes[topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + activation = int(score * 50 * penalty) + print(f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") + return activation + + matched_topics = set() + topic_similarities = {} + + for memory_topic, similarity in top_topics: + memory_items = self.memory_graph.G.nodes[memory_topic].get('memory_items', []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + content_count = len(memory_items) + penalty = 1.0 / (1 + math.log(content_count + 1)) + + for input_topic in identified_topics: + topic_vector = text_to_vector(input_topic) + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + sim = cosine_similarity(v1, v2) + if sim >= similarity_threshold: + matched_topics.add(input_topic) + adjusted_sim = sim * penalty + topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) + print(f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> 「{memory_topic}」(内容数: {content_count}, 相似度: {adjusted_sim:.3f})") + + topic_match = len(matched_topics) / len(identified_topics) + average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 + + activation = int((topic_match + average_similarities) / 2 * 100) + print(f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, 激活值: {activation}") + + return activation + + async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5) -> list: + """根据输入文本获取相关的记忆内容""" + identified_topics = await self._identify_topics(text) + + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆检索" + ) + + relevant_topics = self._get_top_topics(all_similar_topics, max_topics) + + relevant_memories = [] + for topic, score in relevant_topics: + first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) + if first_layer: + if len(first_layer) > max_memory_num/2: + first_layer = random.sample(first_layer, max_memory_num//2) + for memory in first_layer: + relevant_memories.append({ + 'topic': topic, + 'similarity': score, + 'content': memory + }) + + relevant_memories.sort(key=lambda x: x['similarity'], reverse=True) + + if len(relevant_memories) > max_memory_num: + relevant_memories = random.sample(relevant_memories, max_memory_num) + + return relevant_memories + + def find_topic_llm(self,text, topic_num): + prompt = f'这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。' + return prompt + + def topic_what(self,text, topic, time_info): + prompt = f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' + return prompt + +def segment_text(text): + """使用jieba进行文本分词""" + seg_text = list(jieba.cut(text)) + return seg_text + +def text_to_vector(text): + """将文本转换为词频向量""" + words = segment_text(text) + vector = {} + for word in words: + vector[word] = vector.get(word, 0) + 1 + return vector + +def cosine_similarity(v1, v2): + """计算两个向量的余弦相似度""" + dot_product = sum(a * b for a, b in zip(v1, v2)) + norm1 = math.sqrt(sum(a * a for a in v1)) + norm2 = math.sqrt(sum(b * b for b in v2)) + if norm1 == 0 or norm2 == 0: + return 0 + return dot_product / (norm1 * norm2) + +def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): + # 设置中文字体 + plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 + plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 + + G = memory_graph.G + + # 创建一个新图用于可视化 + H = G.copy() + + # 过滤掉内容数量小于2的节点 + nodes_to_remove = [] + for node in H.nodes(): + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + if memory_count < 2: + nodes_to_remove.append(node) + + H.remove_nodes_from(nodes_to_remove) + + # 如果没有符合条件的节点,直接返回 + if len(H.nodes()) == 0: + print("没有找到内容数量大于等于2的节点") + return + + # 计算节点大小和颜色 + node_colors = [] + node_sizes = [] + nodes = list(H.nodes()) + + # 获取最大记忆数用于归一化节点大小 + max_memories = 1 + for node in nodes: + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + max_memories = max(max_memories, memory_count) + + # 计算每个节点的大小和颜色 + for node in nodes: + # 计算节点大小(基于记忆数量) + memory_items = H.nodes[node].get('memory_items', []) + memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) + # 使用指数函数使变化更明显 + ratio = memory_count / max_memories + size = 400 + 2000 * (ratio ** 2) # 增大节点大小 + node_sizes.append(size) + + # 计算节点颜色(基于连接数) + degree = H.degree(node) + if degree >= 30: + node_colors.append((1.0, 0, 0)) # 亮红色 (#FF0000) + else: + # 将1-10映射到0-1的范围 + color_ratio = (degree - 1) / 29.0 if degree > 1 else 0 + # 使用蓝到红的渐变 + red = min(0.9, color_ratio) + blue = max(0.0, 1.0 - color_ratio) + node_colors.append((red, 0, blue)) + + # 绘制图形 + plt.figure(figsize=(16, 12)) # 减小图形尺寸 + pos = nx.spring_layout(H, + k=1, # 调整节点间斥力 + iterations=100, # 增加迭代次数 + scale=1.5, # 减小布局尺寸 + weight='strength') # 使用边的strength属性作为权重 + + nx.draw(H, pos, + with_labels=True, + node_color=node_colors, + node_size=node_sizes, + font_size=12, # 保持增大的字体大小 + font_family='SimHei', + font_weight='bold', + edge_color='gray', + width=1.5) # 统一的边宽度 + + title = '记忆图谱可视化(仅显示内容≥2的节点)\n节点大小表示记忆数量\n节点颜色:蓝(弱连接)到红(强连接)渐变,边的透明度表示连接强度\n连接强度越大的节点距离越近' + plt.title(title, fontsize=16, fontfamily='SimHei') + plt.show() + +async def main(): + # 初始化数据库 + logger.info("正在初始化数据库连接...") + db = Database.get_instance() + start_time = time.time() + + test_pare = {'do_build_memory':True,'do_forget_topic':False,'do_visualize_graph':True,'do_query':False,'do_merge_memory':False} + + # 创建记忆图 + memory_graph = Memory_graph() + + # 创建海马体 + hippocampus = Hippocampus(memory_graph) + + # 从数据库同步数据 + hippocampus.memory_cortex.sync_memory_from_db() + + end_time = time.time() + logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") + + # 构建记忆 + if test_pare['do_build_memory']: + logger.info("开始构建记忆...") + chat_size = 20 + await hippocampus.operation_build_memory(chat_size=chat_size) + + end_time = time.time() + logger.info(f"\033[32m[构建记忆耗时: {end_time - start_time:.2f} 秒,chat_size={chat_size},chat_count = 16]\033[0m") + + if test_pare['do_forget_topic']: + logger.info("开始遗忘记忆...") + await hippocampus.operation_forget_topic(percentage=0.01) + + end_time = time.time() + logger.info(f"\033[32m[遗忘记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") + + if test_pare['do_merge_memory']: + logger.info("开始合并记忆...") + await hippocampus.operation_merge_memory(percentage=0.1) + + end_time = time.time() + logger.info(f"\033[32m[合并记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") + + if test_pare['do_visualize_graph']: + # 展示优化后的图形 + logger.info("生成记忆图谱可视化...") + print("\n生成优化后的记忆图谱:") + visualize_graph_lite(memory_graph) + + if test_pare['do_query']: + # 交互式查询 + while True: + query = input("\n请输入新的查询概念(输入'退出'以结束):") + if query.lower() == '退出': + break + + items_list = memory_graph.get_related_item(query) + if items_list: + first_layer, second_layer = items_list + if first_layer: + print("\n直接相关的记忆:") + for item in first_layer: + print(f"- {item}") + if second_layer: + print("\n间接相关的记忆:") + for item in second_layer: + print(f"- {item}") + else: + print("未找到相关记忆。") + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) + + diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index fe8e1a100..ac567600a 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -44,8 +44,8 @@ class LLM_request: self.db.db.llm_usage.create_index([("model_name", 1)]) self.db.db.llm_usage.create_index([("user_id", 1)]) self.db.db.llm_usage.create_index([("request_type", 1)]) - except Exception as e: - logger.error(f"创建数据库索引失败") + except Exception: + logger.error("创建数据库索引失败") def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, user_id: str = "system", request_type: str = "chat", @@ -80,7 +80,7 @@ class LLM_request: f"总计: {total_tokens}" ) except Exception: - logger.error(f"记录token使用情况失败") + logger.error("记录token使用情况失败") def _calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float: """计算API调用成本 @@ -194,7 +194,7 @@ class LLM_request: if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( 'name') == old_model_name: global_config.llm_normal['name'] = self.model_name - logger.warning(f"已将全局配置中的 llm_normal 模型降级") + logger.warning("已将全局配置中的 llm_normal 模型降级") # 更新payload中的模型名 if payload and 'model' in payload: @@ -227,7 +227,7 @@ class LLM_request: delta_content = "" accumulated_content += delta_content except Exception: - logger.exception(f"解析流式输出错") + logger.exception("解析流式输出错") content = accumulated_content reasoning_content = "" think_match = re.search(r'(.*?)', content, re.DOTALL) @@ -355,7 +355,7 @@ class LLM_request: """构建请求头""" if no_key: return { - "Authorization": f"Bearer **********", + "Authorization": "Bearer **********", "Content-Type": "application/json" } else: diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index fc07a152d..e280c6bce 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -68,7 +68,7 @@ class ScheduleGenerator: 1. 早上的学习和工作安排 2. 下午的活动和任务 3. 晚上的计划和休息时间 - 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表,仅返回内容,不要返回注释,时间采用24小时制,格式为{"时间": "活动","时间": "活动",...}。""" + 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表,仅返回内容,不要返回注释,不要添加任何markdown或代码块样式,时间采用24小时制,格式为{"时间": "活动","时间": "活动",...}。""" try: schedule_text, _ = await self.llm_scheduler.generate_response(prompt) @@ -91,7 +91,7 @@ class ScheduleGenerator: try: schedule_dict = json.loads(schedule_text) return schedule_dict - except json.JSONDecodeError as e: + except json.JSONDecodeError: logger.exception("解析日程失败: {}".format(schedule_text)) return False diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index a071355a3..2974389e6 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -155,7 +155,7 @@ class LLMStatistics: all_stats = self._collect_all_statistics() self._save_statistics(all_stats) except Exception: - logger.exception(f"统计数据处理失败") + logger.exception("统计数据处理失败") # 等待1分钟 for _ in range(60): diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index ff39c9a69..1b33b42cf 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -15,6 +15,7 @@ version = "0.0.5" [bot] qq = 123 nickname = "麦麦" +alias_names = ["小麦", "阿麦"] [personality] prompt_personality = [ From c32c4fb1e2f62f7e73eae4841427a92a6448c02e Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 05:06:58 +0800 Subject: [PATCH 072/105] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E7=9A=84=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/config.py | 2 +- template/bot_config_template.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 547c44a82..7aed9eee8 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -280,7 +280,7 @@ class BotConfig: ) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) - if config.INNER_VERSION in SpecifierSet(">=0.0.5"): + if config.INNER_VERSION in SpecifierSet(">=0.0.6"): config.ban_msgs_regex = msg_config.get("ban_msgs_regex", config.ban_msgs_regex) def memory(parent: dict): diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 1b33b42cf..126fc501d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "0.0.5" +version = "0.0.6" #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 From 67f6d7cd4211df7180118696d3cb4934638dce12 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 06:54:04 +0800 Subject: [PATCH 073/105] =?UTF-8?q?fix:=20=E4=BF=9D=E8=AF=81=E8=83=BD?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=9A=84=E5=B0=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 4 +- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/chat/willing_manager.py | 104 +++++++++++----------------- 3 files changed, 45 insertions(+), 65 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a695cea77..6f18e4da0 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -164,7 +164,6 @@ class ChatBot: response,raw_content = await self.gpt.generate_response(message) if response: - container = message_manager.get_container(chat.stream_id) container = message_manager.get_container(chat.stream_id) thinking_message = None # 找到message,删除 @@ -182,12 +181,12 @@ class ChatBot: # 记录开始思考的时间,避免从思考到回复的时间太久 thinking_start_time = thinking_message.thinking_start_time message_set = MessageSet(chat, think_id) - message_set = MessageSet(chat, think_id) #计算打字时间,1是为了模拟打字,2是避免多条回复乱序 accu_typing_time = 0 mark_head = False for msg in response: + print("test") # print(f"\033[1;32m[回复内容]\033[0m {msg}") # 通过时间改变时间戳 typing_time = calculate_typing_time(msg) @@ -239,6 +238,7 @@ class ChatBot: is_emoji=True ) message_manager.add_message(bot_message) + emotion = await self.gpt._get_emotion_tags(raw_content) logger.debug(f"为 '{response}' 获取到的情感标签为:{emotion}") valuedict = { diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index a26f4dc4b..0056a724a 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -259,7 +259,7 @@ class EmojiManager: upsert=True ) # 保存描述到image_descriptions集合 - await image_manager._save_description_to_db(image_hash, description, 'emoji') + image_manager._save_description_to_db(image_hash, description, 'emoji') logger.success(f"同步已存在的表情包到images集合: {filename}") continue diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 96cf74095..26353e74c 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -16,9 +16,7 @@ class WillingManager: self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿 self._decay_task = None self._started = False - self.min_reply_willing = 0.01 - self.attenuation_coefficient = 0.75 - + async def _decay_reply_willing(self): """定期衰减回复意愿""" while True: @@ -35,9 +33,12 @@ class WillingManager: return self.chat_reply_willing.get(stream.stream_id, 0) return 0 - def set_willing(self, chat_id: int, willing: float): - """设置指定群组的回复意愿""" - self.group_reply_willing[chat_id] = willing + def set_willing(self, chat_id: str, willing: float): + """设置指定聊天流的回复意愿""" + self.chat_reply_willing[chat_id] = willing + def set_willing(self, chat_id: str, willing: float): + """设置指定聊天流的回复意愿""" + self.chat_reply_willing[chat_id] = willing async def change_reply_willing_received(self, chat_stream:ChatStream, @@ -50,67 +51,47 @@ class WillingManager: # 获取或创建聊天流 stream = chat_stream chat_id = stream.stream_id - group_id = stream.group_info.group_id - - # 若非目标回复群组,则直接return - if group_id not in config.talk_allowed_groups: - reply_probability = 0 - return reply_probability - current_willing = self.chat_reply_willing.get(chat_id, 0) - logger.debug(f"[{chat_id}]的初始回复意愿: {current_willing}") - - - # 根据消息类型(被cue/表情包)调控 - if is_mentioned_bot: - current_willing = min( - 3.0, - current_willing + 0.9 - ) - logger.debug(f"被提及, 当前意愿: {current_willing}") - + # print(f"初始意愿: {current_willing}") + if is_mentioned_bot and current_willing < 1.0: + current_willing += 0.9 + print(f"被提及, 当前意愿: {current_willing}") + elif is_mentioned_bot: + current_willing += 0.05 + print(f"被重复提及, 当前意愿: {current_willing}") + if is_emoji: current_willing *= 0.1 - logger.debug(f"表情包, 当前意愿: {current_willing}") - - # 兴趣放大系数,若兴趣 > 0.4则增加回复概率 - interested_rate_amplifier = global_config.response_interested_rate_amplifier - logger.debug(f"放大系数_interested_rate: {interested_rate_amplifier}") - interested_rate *= interested_rate_amplifier - - current_willing += max( - 0.0, - interested_rate - 0.4 - ) - - # 回复意愿系数调控,独立乘区 - willing_amplifier = max( - global_config.response_willing_amplifier, - self.min_reply_willing - ) - current_willing *= willing_amplifier - logger.debug(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") - - # 回复概率迭代,保底0.01回复概率 - reply_probability = max( - (current_willing - 0.45) * 2, - self.min_reply_willing - ) - - # 降低目标低频群组回复概率 - down_frequency_rate = max( - 1.0, - global_config.down_frequency_rate - ) - if group_id in config.talk_frequency_down_groups: - reply_probability = reply_probability / down_frequency_rate + print(f"表情包, 当前意愿: {current_willing}") + + print(f"放大系数_interested_rate: {global_config.response_interested_rate_amplifier}") + interested_rate *= global_config.response_interested_rate_amplifier #放大回复兴趣度 + if interested_rate > 0.4: + # print(f"兴趣度: {interested_rate}, 当前意愿: {current_willing}") + current_willing += interested_rate-0.4 + + current_willing *= global_config.response_willing_amplifier #放大回复意愿 + # print(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}") + + reply_probability = max((current_willing - 0.45) * 2, 0) + + # 检查群组权限(如果是群聊) + if chat_stream.group_info: + if chat_stream.group_info.group_id not in config.talk_allowed_groups: + current_willing = 0 + reply_probability = 0 + + if chat_stream.group_info.group_id in config.talk_frequency_down_groups: + reply_probability = reply_probability / global_config.down_frequency_rate reply_probability = min(reply_probability, 1) - - self.group_reply_willing[group_id] = min(current_willing, 3.0) - logger.debug(f"当前群组{group_id}回复概率:{reply_probability}") + if reply_probability < 0: + reply_probability = 0 + + self.chat_reply_willing[chat_id] = min(current_willing, 3.0) + self.chat_reply_willing[chat_id] = min(current_willing, 3.0) return reply_probability def change_reply_willing_sent(self, chat_stream:ChatStream): @@ -135,6 +116,5 @@ class WillingManager: self._decay_task = asyncio.create_task(self._decay_reply_willing()) self._started = True - # 创建全局实例 -willing_manager = WillingManager() +willing_manager = WillingManager() \ No newline at end of file From 52c93ba080da5e0b2990c4540d7f4d724687d4a3 Mon Sep 17 00:00:00 2001 From: Naptie Date: Tue, 11 Mar 2025 10:16:04 +0800 Subject: [PATCH 074/105] refactor: use Base64 for emoji CQ codes --- src/plugins/chat/cq_code.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index b13e33e48..5ebe4ab3d 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -1,6 +1,5 @@ import base64 import html -import os import time from dataclasses import dataclass from typing import Dict, Optional @@ -17,7 +16,7 @@ from urllib3.util import create_urllib3_context from ..models.utils_model import LLM_request from .config import global_config from .mapper import emojimapper -from .utils_image import storage_emoji, storage_image +from .utils_image import image_path_to_base64, storage_emoji, storage_image from .utils_user import get_user_nickname driver = get_driver() @@ -328,15 +327,10 @@ class CQCode: Returns: 表情包CQ码字符串 """ - # 确保使用绝对路径 - abs_path = os.path.abspath(file_path) - # 转义特殊字符 - escaped_path = abs_path.replace('&', '&') \ - .replace('[', '[') \ - .replace(']', ']') \ - .replace(',', ',') + base64_content = image_path_to_base64(file_path) + # 生成CQ码,设置sub_type=1表示这是表情包 - return f"[CQ:image,file=file:///{escaped_path},sub_type=1]" + return f"[CQ:image,file=base64://{base64_content},sub_type=1]" class CQCode_tool: From 73a3e41e47432aab499cbe97ad9070a07f25dac9 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 11:11:36 +0800 Subject: [PATCH 075/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E6=9B=B4=E6=96=B0bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/plugins/memory_system/memory.py | 57 ++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f4ebca07d..f919e8902 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ ## 📌 注意事项 -SengokuCola纯编程外行,面向cursor编程,很多代码史一样多多包涵 +SengokuCola~~纯编程外行,面向cursor编程,很多代码写得不好多多包涵~~已得到大脑升级 > [!WARNING] > 本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI生成内容不代表本人观点和立场。 diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 0730f9e57..c02b4ba4f 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -438,21 +438,39 @@ class Hippocampus: def sync_memory_from_db(self): """从数据库同步数据到内存中的图结构""" + current_time = datetime.datetime.now().timestamp() + need_update = False + # 清空当前图 self.memory_graph.G.clear() # 从数据库加载所有节点 - nodes = self.memory_graph.db.db.graph_data.nodes.find() + nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) for node in nodes: concept = node['concept'] memory_items = node.get('memory_items', []) - # 确保memory_items是列表 if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] - # 获取时间信息 - created_time = node.get('created_time', datetime.datetime.now().timestamp()) - last_modified = node.get('last_modified', datetime.datetime.now().timestamp()) + # 检查时间字段是否存在 + if 'created_time' not in node or 'last_modified' not in node: + need_update = True + # 更新数据库中的节点 + update_data = {} + if 'created_time' not in node: + update_data['created_time'] = current_time + if 'last_modified' not in node: + update_data['last_modified'] = current_time + + self.memory_graph.db.db.graph_data.nodes.update_one( + {'concept': concept}, + {'$set': update_data} + ) + logger.info(f"为节点 {concept} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = node.get('created_time', current_time) + last_modified = node.get('last_modified', current_time) # 添加节点到图中 self.memory_graph.G.add_node(concept, @@ -461,15 +479,31 @@ class Hippocampus: last_modified=last_modified) # 从数据库加载所有边 - edges = self.memory_graph.db.db.graph_data.edges.find() + edges = list(self.memory_graph.db.db.graph_data.edges.find()) for edge in edges: source = edge['source'] target = edge['target'] - strength = edge.get('strength', 1) # 获取 strength,默认为 1 + strength = edge.get('strength', 1) - # 获取时间信息 - created_time = edge.get('created_time', datetime.datetime.now().timestamp()) - last_modified = edge.get('last_modified', datetime.datetime.now().timestamp()) + # 检查时间字段是否存在 + if 'created_time' not in edge or 'last_modified' not in edge: + need_update = True + # 更新数据库中的边 + update_data = {} + if 'created_time' not in edge: + update_data['created_time'] = current_time + if 'last_modified' not in edge: + update_data['last_modified'] = current_time + + self.memory_graph.db.db.graph_data.edges.update_one( + {'source': source, 'target': target}, + {'$set': update_data} + ) + logger.info(f"为边 {source} - {target} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = edge.get('created_time', current_time) + last_modified = edge.get('last_modified', current_time) # 只有当源节点和目标节点都存在时才添加边 if source in self.memory_graph.G and target in self.memory_graph.G: @@ -477,6 +511,9 @@ class Hippocampus: strength=strength, created_time=created_time, last_modified=last_modified) + + if need_update: + logger.success("已为缺失的时间字段进行补充") async def operation_forget_topic(self, percentage=0.1): """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘""" From 5e069f74e2172d814716e7d121f3d0af475121d0 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 12:06:01 +0800 Subject: [PATCH 076/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=97=B6=E6=97=A0=E6=97=B6=E9=97=B4=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/memory_system/memory.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index c02b4ba4f..e7198b10e 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -318,6 +318,8 @@ class Hippocampus: compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) logger.info(f"压缩后记忆数量: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") + current_time = datetime.datetime.now().timestamp() + for topic, memory in compressed_memory: logger.info(f"添加节点: {topic}") self.memory_graph.add_dot(topic, memory) @@ -330,7 +332,10 @@ class Hippocampus: if topic != similar_topic: strength = int(similarity * 10) logger.info(f"连接相似节点: {topic} 和 {similar_topic} (强度: {strength})") - self.memory_graph.G.add_edge(topic, similar_topic, strength=strength) + self.memory_graph.G.add_edge(topic, similar_topic, + strength=strength, + created_time=current_time, + last_modified=current_time) # 连接同批次的相关话题 for i in range(len(all_topics)): From b6867b904d358bd6e1f285556ac950f4a576df76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Tue, 11 Mar 2025 17:07:37 +0900 Subject: [PATCH 077/105] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8os.getenv=E8=8E=B7=E5=8F=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E4=BF=A1=E6=81=AF=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=BB=8Econfig=E5=AF=B9=E8=B1=A1=E8=8E=B7=E5=8F=96=E4=B8=8D?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84=E5=80=BC=E6=97=B6=E5=87=BA=E7=8E=B0?= =?UTF-8?q?KeyError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 15 ++++++++------- src/plugins/memory_system/memory.py | 15 ++++++++------- src/plugins/memory_system/memory_test1.py | 15 ++++++++------- src/plugins/schedule/schedule_generator.py | 17 +++++++++-------- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 2e01262b1..11c059c1c 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -1,5 +1,6 @@ import asyncio import time +import os from loguru import logger from nonebot import get_driver, on_message, require @@ -31,13 +32,13 @@ driver = get_driver() config = driver.config Database.initialize( - uri=config.MONGODB_URI, - host=config.MONGODB_HOST, - port=int(config.MONGODB_PORT), - db_name=config.DATABASE_NAME, - username=config.MONGODB_USERNAME, - password=config.MONGODB_PASSWORD, - auth_source=config.MONGODB_AUTH_SOURCE + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) logger.success("初始化数据库成功") diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 13a9f8c38..2884b6dae 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -3,6 +3,7 @@ import datetime import math import random import time +import os import jieba import networkx as nx @@ -887,13 +888,13 @@ config = driver.config start_time = time.time() Database.initialize( - uri=config.MONGODB_URI, - host=config.MONGODB_HOST, - port=config.MONGODB_PORT, - db_name=config.DATABASE_NAME, - username=config.MONGODB_USERNAME, - password=config.MONGODB_PASSWORD, - auth_source=config.MONGODB_AUTH_SOURCE + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) # 创建记忆图 memory_graph = Memory_graph() diff --git a/src/plugins/memory_system/memory_test1.py b/src/plugins/memory_system/memory_test1.py index bbd734ec2..72accc2b3 100644 --- a/src/plugins/memory_system/memory_test1.py +++ b/src/plugins/memory_system/memory_test1.py @@ -69,13 +69,14 @@ class Database: def __init__(self): if not Database.db: Database.initialize( - host=os.getenv("MONGODB_HOST"), - port=int(os.getenv("MONGODB_PORT")), - db_name=os.getenv("DATABASE_NAME"), - username=os.getenv("MONGODB_USERNAME"), - password=os.getenv("MONGODB_PASSWORD"), - auth_source=os.getenv("MONGODB_AUTH_SOURCE") - ) + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), + ) @classmethod def initialize(cls, host, port, db_name, username=None, password=None, auth_source="admin"): diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 2fe259c47..12c6ce3b5 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -1,3 +1,4 @@ +import os import datetime import json from typing import Dict, Union @@ -14,13 +15,13 @@ driver = get_driver() config = driver.config Database.initialize( - uri=config.MONGODB_URI, - host=config.MONGODB_HOST, - port=int(config.MONGODB_PORT), - db_name=config.DATABASE_NAME, - username=config.MONGODB_USERNAME, - password=config.MONGODB_PASSWORD, - auth_source=config.MONGODB_AUTH_SOURCE + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) class ScheduleGenerator: @@ -176,6 +177,6 @@ class ScheduleGenerator: # print(scheduler.tomorrow_schedule) # if __name__ == "__main__": -# main() +# main() bot_schedule = ScheduleGenerator() From 1c9b0ccbe33078d97c3620f39ed44a1d435e781c Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 16:16:00 +0800 Subject: [PATCH 078/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86cq=E7=A0=81=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= =?UTF-8?q?=EF=BC=8Cmerge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/cq_code.py | 50 ++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index d0f50c5ae..ea271ac7d 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -18,7 +18,8 @@ from ..models.utils_model import LLM_request from .config import global_config from .mapper import emojimapper from .message_base import Seg -from .utils_user import get_user_nickname +from .utils_user import get_user_nickname,get_groupname +from .message_base import GroupInfo, UserInfo driver = get_driver() config = driver.config @@ -57,11 +58,8 @@ class CQCode: type: str params: Dict[str, str] - group_id: int - user_id: int - user_nickname: str - group_name: str = "" - user_nickname: str = "" + group_info: Optional[GroupInfo] = None + user_info: Optional[UserInfo] = None translated_segments: Optional[Union[Seg, List[Seg]]] = None reply_message: Dict = None # 存储回复消息 image_base64: Optional[str] = None @@ -215,13 +213,23 @@ class CQCode: else: if raw_message: from .message_cq import MessageRecvCQ + user_info=UserInfo( + platform='qq', + user_id=msg.get("user_id", 0), + user_nickname=nickname, + ) + group_info=GroupInfo( + platform='qq', + group_id=msg.get("group_id", 0), + group_name=get_groupname(msg.get("group_id", 0)) + ) message_obj = MessageRecvCQ( - user_id=msg.get("user_id", 0), message_id=msg.get("message_id", 0), + user_info=user_info, raw_message=raw_message, plain_text=raw_message, - group_id=msg.get("group_id", 0), + group_info=group_info, ) content_seg = Seg( type="seglist", data=message_obj.message_segment ) @@ -231,12 +239,22 @@ class CQCode: if raw_message: from .message_cq import MessageRecvCQ - message_obj = MessageRecvCQ( + user_info=UserInfo( + platform='qq', user_id=msg.get("user_id", 0), + user_nickname=nickname, + ) + group_info=GroupInfo( + platform='qq', + group_id=msg.get("group_id", 0), + group_name=get_groupname(msg.get("group_id", 0)) + ) + message_obj = MessageRecvCQ( message_id=msg.get("message_id", 0), + user_info=user_info, raw_message=raw_message, plain_text=raw_message, - group_id=msg.get("group_id", 0), + group_info=group_info, ) content_seg = Seg( type="seglist", data=message_obj.message_segment @@ -262,11 +280,12 @@ class CQCode: return None if self.reply_message.sender.user_id: + message_obj = MessageRecvCQ( - user_id=self.reply_message.sender.user_id, + user_info=UserInfo(user_id=self.reply_message.sender.user_id,user_nickname=self.reply_message.sender.get("nickname",None)), message_id=self.reply_message.message_id, raw_message=str(self.reply_message.message), - group_id=self.group_id, + group_info=GroupInfo(group_id=self.reply_message.group_id), ) segments = [] @@ -300,7 +319,6 @@ class CQCode: .replace("&", "&") ) - class CQCode_tool: @staticmethod def cq_from_dict_to_class(cq_code: Dict,msg ,reply: Optional[Dict] = None) -> CQCode: @@ -327,10 +345,8 @@ class CQCode_tool: instance = CQCode( type=cq_type, params=params, - group_id=msg.message_info.group_info.group_id, - user_id=msg.message_info.user_info.user_id, - user_nickname=msg.message_info.user_info.user_nickname, - group_name=msg.message_info.group_info.group_name, + group_info=msg.message_info.group_info, + user_info=msg.message_info.user_info, reply_message=reply ) From 5760412382bf072a6863393c0b5d5aab7360cf51 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 16:32:35 +0800 Subject: [PATCH 079/105] =?UTF-8?q?fix:=20=E5=B0=8F=E4=BF=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils_image.py | 2 +- src/plugins/chat/willing_manager.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index ac3ff5ac4..24687a8f5 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -345,7 +345,7 @@ class ImageManager: logger.error(f"保存图片文件失败: {str(e)}") # 保存描述到数据库 - await self._save_description_to_db(image_hash, description, 'image') + self._save_description_to_db(image_hash, description, 'image') return f"[图片:{description}]" except Exception as e: diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 26353e74c..39083f0b8 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -78,11 +78,7 @@ class WillingManager: reply_probability = max((current_willing - 0.45) * 2, 0) # 检查群组权限(如果是群聊) - if chat_stream.group_info: - if chat_stream.group_info.group_id not in config.talk_allowed_groups: - current_willing = 0 - reply_probability = 0 - + if chat_stream.group_info: if chat_stream.group_info.group_id in config.talk_frequency_down_groups: reply_probability = reply_probability / global_config.down_frequency_rate @@ -90,7 +86,6 @@ class WillingManager: if reply_probability < 0: reply_probability = 0 - self.chat_reply_willing[chat_id] = min(current_willing, 3.0) self.chat_reply_willing[chat_id] = min(current_willing, 3.0) return reply_probability From e48b32abe033dee55e27de870d8e64b717916998 Mon Sep 17 00:00:00 2001 From: Cookie987 Date: Tue, 11 Mar 2025 16:40:53 +0800 Subject: [PATCH 080/105] =?UTF-8?q?=E5=9C=A8=E6=89=8B=E5=8A=A8=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=95=99=E7=A8=8B=E4=B8=AD=E5=A2=9E=E5=8A=A0=E4=BD=BF?= =?UTF-8?q?=E7=94=A8systemctl=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/manual_deploy_linux.md | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index 4711c81a9..0a68c6da9 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -110,6 +110,53 @@ python3 bot.py --- +### 7️⃣ **使用systemctl管理maimbot** + +使用以下命令添加服务文件: + +```bash +sudo nano /etc/systemd/system/maimbot.service +``` + +输入以下内容: + +```ini +[Unit] +Description=MaiMbot 麦麦 +After=network.target mongod.service + +[Service] +Type=simple +WorkingDirectory=/path/to/your/maimbot/ +ExecStart=/path/to/your/venv/python3 bot.py +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target +``` + +输入以下命令重新加载 systemd: + +```bash +sudo systemctl daemon-reload +``` + +启动并设置开机自启: + +```bash +sudo systemctl start maimbot +sudo systemctl enable maimbot +``` + +输入以下命令查看日志: + +```bash +sudo journalctl -xeu maimbot +``` + +--- + ## **其他组件(可选)** - 直接运行 knowledge.py生成知识库 From 0586700467a5f91106d13bb0ad490b48726cc6d0 Mon Sep 17 00:00:00 2001 From: Cookie987 Date: Tue, 11 Mar 2025 16:49:52 +0800 Subject: [PATCH 081/105] =?UTF-8?q?=E6=8C=89=E7=85=A7Sourcery=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E7=9A=84=E5=BB=BA=E8=AE=AE=E4=BF=AE=E6=94=B9systemctl?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/manual_deploy_linux.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index 0a68c6da9..1dbd74692 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -120,6 +120,9 @@ sudo nano /etc/systemd/system/maimbot.service 输入以下内容: +``:你的maimbot目录 +``:你的venv环境(就是上文创建环境后,执行的代码`source maimbot/bin/activate`中source后面的路径的绝对路径) + ```ini [Unit] Description=MaiMbot 麦麦 @@ -127,8 +130,8 @@ After=network.target mongod.service [Service] Type=simple -WorkingDirectory=/path/to/your/maimbot/ -ExecStart=/path/to/your/venv/python3 bot.py +WorkingDirectory= +ExecStart=/python3 bot.py Restart=always RestartSec=10s @@ -136,7 +139,7 @@ RestartSec=10s WantedBy=multi-user.target ``` -输入以下命令重新加载 systemd: +输入以下命令重新加载systemd: ```bash sudo systemctl daemon-reload From af962c2e84ed68c6cadb54d5d930a3beb1cd644f Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 16:50:40 +0800 Subject: [PATCH 082/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=83=85?= =?UTF-8?q?=E7=BB=AA=E7=AE=A1=E7=90=86=E5=99=A8=E6=B2=A1=E6=9C=89=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=AF=BC=E5=85=A5=E5=AF=BC=E8=87=B4=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=87=BA=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1 --- src/plugins/chat/bot.py | 11 +++++-- src/plugins/chat/llm_generator.py | 5 +++ src/plugins/chat/message_sender.py | 9 +++--- src/plugins/chat/utils.py | 48 ++++++++++++++--------------- src/plugins/chat/utils_image.py | 4 +++ src/plugins/memory_system/memory.py | 2 +- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 6f18e4da0..81361d81b 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -163,12 +163,16 @@ class ChatBot: response,raw_content = await self.gpt.generate_response(message) + # print(f"response: {response}") if response: + # print(f"有response: {response}") container = message_manager.get_container(chat.stream_id) thinking_message = None # 找到message,删除 + # print(f"开始找思考消息") for msg in container.messages: if isinstance(msg, MessageThinking) and msg.message_info.message_id == think_id: + # print(f"找到思考消息: {msg}") thinking_message = msg container.messages.remove(msg) break @@ -186,14 +190,14 @@ class ChatBot: mark_head = False for msg in response: - print("test") # print(f"\033[1;32m[回复内容]\033[0m {msg}") # 通过时间改变时间戳 typing_time = calculate_typing_time(msg) + print(f"typing_time: {typing_time}") accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - message_segment = Seg(type='text', data=msg) + print(f"message_segment: {message_segment}") bot_message = MessageSending( message_id=think_id, chat_stream=chat, @@ -203,12 +207,15 @@ class ChatBot: is_head=not mark_head, is_emoji=False ) + print(f"bot_message: {bot_message}") if not mark_head: mark_head = True + print(f"添加消息到message_set: {bot_message}") message_set.add_message(bot_message) # message_set 可以直接加入 message_manager # print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") + print(f"添加message_set到message_manager") message_manager.add_message(message_set) bot_response_time = tinking_time_point diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 517e8aa7a..af7334afe 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -63,6 +63,9 @@ class ResponseGenerator: ) raw_content = model_response + # print(f"raw_content: {raw_content}") + # print(f"model_response: {model_response}") + if model_response: logger.info(f'{global_config.BOT_NICKNAME}的回复是:{model_response}') model_response = await self._process_response(model_response) @@ -200,6 +203,8 @@ class ResponseGenerator: return None, [] processed_response = process_llm_response(content) + + # print(f"得到了处理后的llm返回{processed_response}") return processed_response diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index d5f710bbf..9db74633f 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -149,6 +149,7 @@ class MessageManager: """处理聊天流消息""" container = self.get_container(chat_id) if container.has_messages(): + # print(f"处理有message的容器chat_id: {chat_id}") message_earliest = container.get_earliest_message() if isinstance(message_earliest, MessageThinking): @@ -161,15 +162,15 @@ class MessageManager: logger.warning(f"消息思考超时({thinking_time}秒),移除该消息") container.remove_message(message_earliest) else: - print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") + if message_earliest.is_head and message_earliest.update_thinking_time() > 30: await message_sender.send_message(message_earliest.set_reply()) else: await message_sender.send_message(message_earliest) - - # if message_earliest.is_emoji: - # message_earliest.processed_plain_text = "[表情包]" await message_earliest.process() + + print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") + await self.storage.store_message(message_earliest, message_earliest.chat_stream,None) container.remove_message(message_earliest) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index a889ef177..55fb9eb43 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -15,6 +15,7 @@ from .config import global_config from .message import MessageThinking, MessageRecv,MessageSending,MessageProcessBase,Message from .message_base import MessageBase,BaseMessageInfo,UserInfo,GroupInfo from .chat_stream import ChatStream +from ..moods.moods import MoodManager driver = get_driver() config = driver.config @@ -72,43 +73,42 @@ def calculate_information_content(text): def get_cloest_chat_from_db(db, length: int, timestamp: str): - """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 + """从数据库中获取最接近指定时间戳的聊天记录 + Args: + db: 数据库实例 + length: 要获取的消息数量 + timestamp: 时间戳 + Returns: - list: 消息记录字典列表,每个字典包含消息内容和时间信息 + list: 消息记录列表,每个记录包含时间和文本信息 """ chat_records = [] closest_record = db.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) - if closest_record and closest_record.get('memorized', 0) < 4: + if closest_record: closest_time = closest_record['time'] - chat_id = closest_record['chat_id'] # 获取groupid - # 获取该时间戳之后的length条消息,且groupid相同 + chat_id = closest_record['chat_id'] # 获取chat_id + # 获取该时间戳之后的length条消息,保持相同的chat_id chat_records = list(db.db.messages.find( - {"time": {"$gt": closest_time}, "chat_id": chat_id} + { + "time": {"$gt": closest_time}, + "chat_id": chat_id # 添加chat_id过滤 + } ).sort('time', 1).limit(length)) - # 更新每条消息的memorized属性 - for record in records: - current_memorized = record.get('memorized', 0) - if current_memorized > 3: - print("消息已读取3次,跳过") - return '' - - # 更新memorized值 - db.db.messages.update_one( - {"_id": record["_id"]}, - {"$set": {"memorized": current_memorized + 1}} - ) - - # 添加到记录列表中 - chat_records.append({ - 'text': record["detailed_plain_text"], + # 转换记录格式 + formatted_records = [] + for record in chat_records: + formatted_records.append({ 'time': record["time"], - 'group_id': record["group_id"] + 'chat_id': record["chat_id"], + 'detailed_plain_text': record.get("detailed_plain_text", "") # 添加文本内容 }) - return chat_records + return formatted_records + + return [] async def get_recent_group_messages(db, chat_id:str, limit: int = 12) -> list: diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index ac3ff5ac4..208cbf15d 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -315,6 +315,10 @@ class ImageManager: prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来。并尝试猜测这个图片的含义。最多200个字。" description, _ = await self._llm.generate_response_for_image(prompt, image_base64) + if description is None: + logger.warning("AI未能生成图片描述") + return "[图片]" + # 根据配置决定是否保存图片 if global_config.EMOJI_SAVE: # 生成文件名和路径 diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 0730f9e57..d122252fb 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -238,7 +238,7 @@ class Hippocampus: time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" for msg in messages: - input_text += f"{msg['text']}\n" + input_text += f"{msg['detailed_plain_text']}\n" logger.debug(input_text) From 20f009d0bb2c9f84d0b575be24ad874607833f90 Mon Sep 17 00:00:00 2001 From: Cookie987 Date: Tue, 11 Mar 2025 16:55:59 +0800 Subject: [PATCH 083/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dsystemctl=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E5=81=9C=E6=AD=A2maimbot=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/manual_deploy_linux.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index 1dbd74692..b19f3d6a7 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -132,6 +132,7 @@ After=network.target mongod.service Type=simple WorkingDirectory= ExecStart=/python3 bot.py +ExecStop=/bin/kill -2 $MAINPID Restart=always RestartSec=10s From 01a6fa80ae34c1f17e367c349b2e9e357890a208 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 16:59:19 +0800 Subject: [PATCH 084/105] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=E7=A5=9E?= =?UTF-8?q?=E7=A7=98test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 6f18e4da0..1ae715a26 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -186,7 +186,6 @@ class ChatBot: mark_head = False for msg in response: - print("test") # print(f"\033[1;32m[回复内容]\033[0m {msg}") # 通过时间改变时间戳 typing_time = calculate_typing_time(msg) From b6edbea96c87413d678b2333f3b52fcd6fd3706e Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 18:17:29 +0800 Subject: [PATCH 085/105] =?UTF-8?q?fix:=20=E5=9B=BE=E7=89=87=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E8=B7=AF=E5=BE=84=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils_image.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index dc14d4bca..25f23359b 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -199,15 +199,6 @@ class ImageManager: logger.error(f"获取base64失败: {str(e)}") return None - async def save_base64_image(self, base64_str: str, description: str = None) -> Optional[str]: - """保存base64图像(带查重) - Args: - base64_str: base64字符串 - description: 图像描述 - Returns: - str: 保存路径,失败返回None - """ - return await self.save_image(base64_str, description=description, is_base64=True) def check_url_exists(self, url: str) -> bool: """检查URL是否已存在 @@ -266,8 +257,8 @@ class ImageManager: if global_config.EMOJI_SAVE: # 生成文件名和路径 timestamp = int(time.time()) - filename = f"emoji_{timestamp}_{image_hash[:8]}.jpg" - file_path = os.path.join(self.IMAGE_DIR, filename) + filename = f"{timestamp}_{image_hash[:8]}.jpg" + file_path = os.path.join(self.IMAGE_DIR, 'emoji',filename) try: # 保存文件 @@ -323,8 +314,8 @@ class ImageManager: if global_config.EMOJI_SAVE: # 生成文件名和路径 timestamp = int(time.time()) - filename = f"image_{timestamp}_{image_hash[:8]}.jpg" - file_path = os.path.join(self.IMAGE_DIR, filename) + filename = f"{timestamp}_{image_hash[:8]}.jpg" + file_path = os.path.join(self.IMAGE_DIR,'image', filename) try: # 保存文件 From dea14c1d8ab525e72427242070d806cafc21b658 Mon Sep 17 00:00:00 2001 From: HYY1116 Date: Tue, 11 Mar 2025 18:21:46 +0800 Subject: [PATCH 086/105] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9E=8B=E9=99=8D?= =?UTF-8?q?=E7=BA=A7=E7=9B=AE=E5=89=8D=E5=8F=AA=E5=AF=B9=E7=A1=85=E5=9F=BA?= =?UTF-8?q?=E6=B5=81=E5=8A=A8=E7=9A=84V3=E5=92=8CR1=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index c6ed6b619..e9d11f339 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -184,9 +184,9 @@ class LLM_request: elif response.status in policy["abort_codes"]: logger.error(f"错误码: {response.status} - {error_code_mapping.get(response.status)}") if response.status == 403: - # 尝试降级Pro模型 - if self.model_name.startswith( - "Pro/") and self.base_url == "https://api.siliconflow.cn/v1/": + #只针对硅基流动的V3和R1进行降级处理 + if self.model_name.startswith( + "Pro/deepseek-ai") and self.base_url == "https://api.siliconflow.cn/v1/": old_model_name = self.model_name self.model_name = self.model_name[4:] # 移除"Pro/"前缀 logger.warning(f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}") @@ -195,7 +195,12 @@ class LLM_request: if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( 'name') == old_model_name: global_config.llm_normal['name'] = self.model_name - logger.warning("已将全局配置中的 llm_normal 模型降级") + logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}") + + if hasattr(global_config, 'llm_reasoning') and global_config.llm_reasoning.get( + 'name') == old_model_name: + global_config.llm_reasoning['name'] = self.model_name + logger.warning(f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}") # 更新payload中的模型名 if payload and 'model' in payload: From 4cc5c8ef2c263981917c7b891779af7a9ee45077 Mon Sep 17 00:00:00 2001 From: Pliosauroidea Date: Tue, 11 Mar 2025 18:37:40 +0800 Subject: [PATCH 087/105] =?UTF-8?q?=E4=BF=AE=E6=AD=A3.env.prod=E5=92=8C.en?= =?UTF-8?q?v.dev=E7=9A=84=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 471a98eaf..9a5d47291 100644 --- a/bot.py +++ b/bot.py @@ -51,15 +51,15 @@ def init_env(): with open(".env", "w") as f: f.write("ENVIRONMENT=prod") - # 检测.env.prod文件是否存在 - if not os.path.exists(".env.prod"): - logger.error("检测到.env.prod文件不存在") - shutil.copy("template.env", "./.env.prod") + # 检测.env.prod文件是否存在 + if not os.path.exists(".env.prod"): + logger.error("检测到.env.prod文件不存在") + shutil.copy("template.env", "./.env.prod") # 检测.env.dev文件是否存在,不存在的话直接复制生产环境配置 if not os.path.exists(".env.dev"): logger.error("检测到.env.dev文件不存在") - shutil.copy(".env.prod", "./.env.dev") + shutil.copy("template.env", "./.env.dev") # 首先加载基础环境变量.env if os.path.exists(".env"): From bfa9a3c02690f4787b8ab074c859b0844db9de43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=88=86=E6=A9=98=E5=AD=90?= Date: Tue, 11 Mar 2025 18:40:03 +0800 Subject: [PATCH 088/105] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E7=BE=A4?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=8E=B7=E5=8F=96=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 140 +++++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 81361d81b..9b39a9be6 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -7,7 +7,7 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent from ..memory_system.memory import hippocampus from ..moods.moods import MoodManager # 导入情绪管理器 from .config import global_config -from .cq_code import CQCode,cq_code_tool # 导入CQCode模块 +from .cq_code import CQCode, cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator from .message import MessageSending, MessageRecv, MessageThinking, MessageSet @@ -24,6 +24,7 @@ from .utils_image import image_path_to_base64 from .willing_manager import willing_manager # 导入意愿管理器 from .message_base import UserInfo, GroupInfo, Seg + class ChatBot: def __init__(self): self.storage = MessageStorage() @@ -46,8 +47,13 @@ class ChatBot: self.bot = bot # 更新 bot 实例 - # group_info = await bot.get_group_info(group_id=event.group_id) - # sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) + try: + group_info_api = await bot.get_group_info(group_id=event.group_id) + logger.info(f"成功获取群信息: {group_info_api}") + group_name = group_info_api["group_name"] + except Exception as e: + logger.error(f"获取群信息失败: {str(e)}") + group_name = None # 白名单设定由nontbot侧完成 if event.group_id: @@ -55,50 +61,53 @@ class ChatBot: return if event.user_id in global_config.ban_user_id: return - - user_info=UserInfo( + + user_info = UserInfo( user_id=event.user_id, user_nickname=event.sender.nickname, user_cardname=event.sender.card or None, - platform='qq' + platform="qq", ) - group_info=GroupInfo( + group_info = GroupInfo( group_id=event.group_id, - group_name=None, - platform='qq' + group_name=group_name, # 使用获取到的群名称或None + platform="qq", ) - message_cq=MessageRecvCQ( + message_cq = MessageRecvCQ( message_id=event.message_id, user_info=user_info, raw_message=str(event.original_message), group_info=group_info, reply_message=event.reply, - platform='qq' + platform="qq", ) - message_json=message_cq.to_dict() + message_json = message_cq.to_dict() # 进入maimbot - message=MessageRecv(message_json) - - groupinfo=message.message_info.group_info - userinfo=message.message_info.user_info - messageinfo=message.message_info + message = MessageRecv(message_json) + + groupinfo = message.message_info.group_info + userinfo = message.message_info.user_info + messageinfo = message.message_info # 消息过滤,涉及到config有待更新 - - chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) + + chat = await chat_manager.get_or_create_stream( + platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo + ) message.update_chat_stream(chat) - await relationship_manager.update_relationship(chat_stream=chat,) - await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value = 0.5) + await relationship_manager.update_relationship( + chat_stream=chat, + ) + await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value=0.5) await message.process() # 过滤词 for word in global_config.ban_words: if word in message.processed_plain_text: - logger.info( - f"[{groupinfo.group_name}]{userinfo.user_nickname}:{message.processed_plain_text}") + logger.info(f"[群{groupinfo.group_id}]{userinfo.user_nickname}:{message.processed_plain_text}") logger.info(f"[过滤词识别]消息中含有{word},filtered") return @@ -106,23 +115,21 @@ class ChatBot: for pattern in global_config.ban_msgs_regex: if re.search(pattern, message.raw_message): logger.info( - f"[{message.group_name}]{message.user_nickname}:{message.raw_message}") + f"[群{message.message_info.group_info.group_id}]{message.user_nickname}:{message.raw_message}" + ) logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return - + current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time)) - - # topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) - topic = '' + topic = "" interested_rate = 0 interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 - logger.debug(f"对{message.processed_plain_text}" - f"的激活度:{interested_rate}") + logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") - - await self.storage.store_message(message,chat, topic[0] if topic else None) + + await self.storage.store_message(message, chat, topic[0] if topic else None) is_mentioned = is_mentioned_bot_in_message(message) reply_probability = await willing_manager.change_reply_willing_received( @@ -131,38 +138,33 @@ class ChatBot: is_mentioned_bot=is_mentioned, config=global_config, is_emoji=message.is_emoji, - interested_rate=interested_rate + interested_rate=interested_rate, ) current_willing = willing_manager.get_willing(chat_stream=chat) - + logger.info( - f"[{current_time}][{chat.group_info.group_name}]{chat.user_info.user_nickname}:" + f"[{current_time}][群{chat.group_info.group_id}]{chat.user_info.user_nickname}:" f"{message.processed_plain_text}[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]" ) response = None - + if random() < reply_probability: - bot_user_info=UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=messageinfo.platform + bot_user_info = UserInfo( + user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, platform=messageinfo.platform ) tinking_time_point = round(time.time(), 2) - think_id = 'mt' + str(tinking_time_point) + think_id = "mt" + str(tinking_time_point) thinking_message = MessageThinking( - message_id=think_id, - chat_stream=chat, - bot_user_info=bot_user_info, - reply=message + message_id=think_id, chat_stream=chat, bot_user_info=bot_user_info, reply=message ) - + message_manager.add_message(thinking_message) willing_manager.change_reply_willing_sent(chat) - - response,raw_content = await self.gpt.generate_response(message) - + + response, raw_content = await self.gpt.generate_response(message) + # print(f"response: {response}") if response: # print(f"有response: {response}") @@ -185,9 +187,9 @@ class ChatBot: # 记录开始思考的时间,避免从思考到回复的时间太久 thinking_start_time = thinking_message.thinking_start_time message_set = MessageSet(chat, think_id) - #计算打字时间,1是为了模拟打字,2是避免多条回复乱序 + # 计算打字时间,1是为了模拟打字,2是避免多条回复乱序 accu_typing_time = 0 - + mark_head = False for msg in response: # print(f"\033[1;32m[回复内容]\033[0m {msg}") @@ -196,7 +198,7 @@ class ChatBot: print(f"typing_time: {typing_time}") accu_typing_time += typing_time timepoint = tinking_time_point + accu_typing_time - message_segment = Seg(type='text', data=msg) + message_segment = Seg(type="text", data=msg) print(f"message_segment: {message_segment}") bot_message = MessageSending( message_id=think_id, @@ -205,7 +207,7 @@ class ChatBot: message_segment=message_segment, reply=message, is_head=not mark_head, - is_emoji=False + is_emoji=False, ) print(f"bot_message: {bot_message}") if not mark_head: @@ -227,14 +229,14 @@ class ChatBot: if emoji_raw != None: emoji_path, description = emoji_raw - emoji_cq = image_path_to_base64(emoji_path) - + emoji_cq = image_path_to_base64(emoji_path) + if random() < 0.5: bot_response_time = tinking_time_point - 1 else: bot_response_time = bot_response_time + 1 - - message_segment = Seg(type='emoji', data=emoji_cq) + + message_segment = Seg(type="emoji", data=emoji_cq) bot_message = MessageSending( message_id=think_id, chat_stream=chat, @@ -242,25 +244,27 @@ class ChatBot: message_segment=message_segment, reply=message, is_head=False, - is_emoji=True + is_emoji=True, ) message_manager.add_message(bot_message) - + emotion = await self.gpt._get_emotion_tags(raw_content) logger.debug(f"为 '{response}' 获取到的情感标签为:{emotion}") valuedict = { - 'happy': 0.5, - 'angry': -1, - 'sad': -0.5, - 'surprised': 0.2, - 'disgusted': -1.5, - 'fearful': -0.7, - 'neutral': 0.1 + "happy": 0.5, + "angry": -1, + "sad": -0.5, + "surprised": 0.2, + "disgusted": -1.5, + "fearful": -0.7, + "neutral": 0.1, } - await relationship_manager.update_relationship_value(chat_stream=chat, relationship_value=valuedict[emotion[0]]) + await relationship_manager.update_relationship_value( + chat_stream=chat, relationship_value=valuedict[emotion[0]] + ) # 使用情绪管理器更新情绪 self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) - + # willing_manager.change_reply_willing_after_sent( # chat_stream=chat # ) From 60a93766c7b99b71e518a5da649ae8327f386183 Mon Sep 17 00:00:00 2001 From: Pliosauroidea Date: Tue, 11 Mar 2025 18:42:35 +0800 Subject: [PATCH 089/105] =?UTF-8?q?=E6=B7=BB=E5=8A=A0logger=E7=9A=84debug?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E5=BC=80=E5=85=B3,=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=BA=E4=B8=8D=E5=BC=80=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/config.py | 9 ++++++++- template/bot_config_template.toml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 7aed9eee8..596d120f9 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -1,4 +1,5 @@ import os +import sys from dataclasses import dataclass, field from typing import Dict, List, Optional @@ -67,6 +68,7 @@ class BotConfig: enable_advance_output: bool = False # 是否启用高级输出 enable_kuuki_read: bool = True # 是否启用读空气功能 + enable_debug_output: bool = False # 是否启用调试输出 mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 mood_decay_rate: float = 0.95 # 情绪衰减率 @@ -325,6 +327,7 @@ class BotConfig: others_config = parent["others"] config.enable_advance_output = others_config.get("enable_advance_output", config.enable_advance_output) config.enable_kuuki_read = others_config.get("enable_kuuki_read", config.enable_kuuki_read) + config.enable_debug_output = others_config.get("enable_debug_output", config.enable_debug_output) # 版本表达式:>=1.0.0,<2.0.0 # 允许字段:func: method, support: str, notice: str, necessary: bool @@ -419,4 +422,8 @@ global_config = BotConfig.load_config(config_path=bot_config_path) if not global_config.enable_advance_output: logger.remove() - pass + +# 调试输出功能 +if global_config.enable_debug_output: + logger.remove() + logger.add(sys.stdout, level="DEBUG") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 126fc501d..bea6ab7b7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -100,6 +100,7 @@ word_replace_rate=0.006 # 整词替换概率 [others] enable_advance_output = true # 是否启用高级输出 enable_kuuki_read = true # 是否启用读空气功能 +enable_debug_output = false # 是否启用调试输出 [groups] talk_allowed = [ From c24bb70291b276d5e471ec554188708c86e7c14c Mon Sep 17 00:00:00 2001 From: pine Date: Tue, 11 Mar 2025 18:51:28 +0800 Subject: [PATCH 090/105] =?UTF-8?q?fix:=20=E6=B5=81=E5=BC=8F=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=A8=A1=E5=BC=8F=E5=A2=9E=E5=8A=A0=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E4=B8=8Etoken=E7=94=A8=E9=87=8F=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e9d11f339..461f542d1 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -216,6 +216,7 @@ class LLM_request: # 将流式输出转化为非流式输出 if stream_mode: + flag_delta_content_finished = False accumulated_content = "" async for line_bytes in response.content: line = line_bytes.decode("utf-8").strip() @@ -227,13 +228,25 @@ class LLM_request: break try: chunk = json.loads(data_str) - delta = chunk["choices"][0]["delta"] - delta_content = delta.get("content") - if delta_content is None: - delta_content = "" - accumulated_content += delta_content + if flag_delta_content_finished: + usage = chunk.get("usage", None) # 获取tokn用量 + else: + delta = chunk["choices"][0]["delta"] + delta_content = delta.get("content") + if delta_content is None: + delta_content = "" + accumulated_content += delta_content + # 检测流式输出文本是否结束 + finish_reason = chunk["choices"][0]["finish_reason"] + if finish_reason == "stop": + usage = chunk.get("usage", None) + if usage: + break + # 部分平台在文本输出结束前不会返回token用量,此时需要再获取一次chunk + flag_delta_content_finished = True + except Exception: - logger.exception("解析流式输出错") + logger.exception("解析流式输出错误") content = accumulated_content reasoning_content = "" think_match = re.search(r'(.*?)', content, re.DOTALL) @@ -242,7 +255,7 @@ class LLM_request: content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip() # 构造一个伪result以便调用自定义响应处理器或默认处理器 result = { - "choices": [{"message": {"content": content, "reasoning_content": reasoning_content}}]} + "choices": [{"message": {"content": content, "reasoning_content": reasoning_content}}], "usage": usage} return response_handler(result) if response_handler else self._default_response_handler( result, user_id, request_type, endpoint) else: From e1019ade3c16d91774ff37665d3595afee35f12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=88=86=E6=A9=98=E5=AD=90?= Date: Tue, 11 Mar 2025 18:52:32 +0800 Subject: [PATCH 091/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 9b39a9be6..a3bab2f53 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -153,8 +153,8 @@ class ChatBot: bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, platform=messageinfo.platform ) - tinking_time_point = round(time.time(), 2) - think_id = "mt" + str(tinking_time_point) + thinking_time_point = round(time.time(), 2) + think_id = "mt" + str(thinking_time_point) thinking_message = MessageThinking( message_id=think_id, chat_stream=chat, bot_user_info=bot_user_info, reply=message ) @@ -197,7 +197,7 @@ class ChatBot: typing_time = calculate_typing_time(msg) print(f"typing_time: {typing_time}") accu_typing_time += typing_time - timepoint = tinking_time_point + accu_typing_time + timepoint = thinking_time_point + accu_typing_time message_segment = Seg(type="text", data=msg) print(f"message_segment: {message_segment}") bot_message = MessageSending( @@ -220,7 +220,7 @@ class ChatBot: print(f"添加message_set到message_manager") message_manager.add_message(message_set) - bot_response_time = tinking_time_point + bot_response_time = thinking_time_point if random() < global_config.emoji_chance: emoji_raw = await emoji_manager.get_emoji_for_text(response) @@ -232,7 +232,7 @@ class ChatBot: emoji_cq = image_path_to_base64(emoji_path) if random() < 0.5: - bot_response_time = tinking_time_point - 1 + bot_response_time = thinking_time_point - 1 else: bot_response_time = bot_response_time + 1 From 7d017be9f7a8f9ea3a54bacceb34474818b37ab4 Mon Sep 17 00:00:00 2001 From: HYY1116 Date: Tue, 11 Mar 2025 19:23:48 +0800 Subject: [PATCH 092/105] =?UTF-8?q?fix:=E6=A8=A1=E5=9E=8B=E9=99=8D?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e9d11f339..6ed0a0b29 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -192,13 +192,11 @@ class LLM_request: logger.warning(f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}") # 对全局配置进行更新 - if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( - 'name') == old_model_name: + if global_config.llm_normal.get('name') == old_model_name: global_config.llm_normal['name'] = self.model_name logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}") - if hasattr(global_config, 'llm_reasoning') and global_config.llm_reasoning.get( - 'name') == old_model_name: + if global_config.llm_reasoning.get('name') == old_model_name: global_config.llm_reasoning['name'] = self.model_name logger.warning(f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}") From ef8691cd9e91696780d816cc144a91c1241d18f4 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 19:30:20 +0800 Subject: [PATCH 093/105] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9message?= =?UTF-8?q?=E7=BB=A7=E6=89=BF=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E6=B6=88=E6=81=AF=E6=97=A0=E6=B3=95=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 4 +- src/plugins/chat/cq_code.py | 10 ++-- src/plugins/chat/message.py | 103 +++++++++++++++++------------------- 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 81361d81b..4af3a6b02 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -50,6 +50,7 @@ class ChatBot: # sender_info = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id, no_cache=True) # 白名单设定由nontbot侧完成 + # 消息过滤,涉及到config有待更新 if event.group_id: if event.group_id not in global_config.talk_allowed_groups: return @@ -68,7 +69,6 @@ class ChatBot: group_name=None, platform='qq' ) - message_cq=MessageRecvCQ( message_id=event.message_id, user_info=user_info, @@ -86,7 +86,7 @@ class ChatBot: userinfo=message.message_info.user_info messageinfo=message.message_info - # 消息过滤,涉及到config有待更新 + chat = await chat_manager.get_or_create_stream(platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo) message.update_chat_stream(chat) diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index 185e98edf..0a8a71df3 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -231,7 +231,8 @@ class CQCode: group_info=group_info, ) content_seg = Seg( - type="seglist", data=message_obj.message_segment ) + type="seglist", data=[message_obj.message_segment] + ) else: content_seg = Seg(type="text", data="[空消息]") else: @@ -256,7 +257,7 @@ class CQCode: group_info=group_info, ) content_seg = Seg( - type="seglist", data=message_obj.message_segment + type="seglist", data=[message_obj.message_segment] ) else: content_seg = Seg(type="text", data="[空消息]") @@ -281,11 +282,12 @@ class CQCode: if self.reply_message.sender.user_id: message_obj = MessageRecvCQ( - user_info=UserInfo(user_id=self.reply_message.sender.user_id,user_nickname=self.reply_message.sender.get("nickname",None)), + user_info=UserInfo(user_id=self.reply_message.sender.user_id,user_nickname=self.reply_message.sender.nickname), message_id=self.reply_message.message_id, raw_message=str(self.reply_message.message), group_info=GroupInfo(group_id=self.reply_message.group_id), ) + segments = [] if message_obj.message_info.user_info.user_id == global_config.BOT_QQ: @@ -302,7 +304,7 @@ class CQCode: ) ) - segments.append(Seg(type="seglist", data=message_obj.message_segment)) + segments.append(Seg(type="seglist", data=[message_obj.message_segment])) segments.append(Seg(type="text", data="]")) return segments else: diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 5eb93d700..d9f54dd47 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -14,9 +14,52 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) #这个类是消息数据类,用于存储和管理消息数据。 #它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 #它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 - + @dataclass -class MessageRecv(MessageBase): +class Message(MessageBase): + chat_stream: ChatStream=None + reply: Optional['Message'] = None + detailed_plain_text: str = "" + processed_plain_text: str = "" + + def __init__( + self, + message_id: str, + time: int, + chat_stream: ChatStream, + user_info: UserInfo, + message_segment: Optional[Seg] = None, + reply: Optional['MessageRecv'] = None, + detailed_plain_text: str = "", + processed_plain_text: str = "", + ): + # 构造基础消息信息 + message_info = BaseMessageInfo( + platform=chat_stream.platform, + message_id=message_id, + time=time, + group_info=chat_stream.group_info, + user_info=user_info + ) + + # 调用父类初始化 + super().__init__( + message_info=message_info, + message_segment=message_segment, + raw_message=None + ) + + self.chat_stream = chat_stream + # 文本处理相关属性 + self.processed_plain_text = detailed_plain_text + self.detailed_plain_text = processed_plain_text + + # 回复消息 + self.reply = reply + + +@dataclass +class MessageRecv(Message): """接收消息类,用于处理从MessageCQ序列化的消息""" def __init__(self, message_dict: Dict): @@ -25,20 +68,16 @@ class MessageRecv(MessageBase): Args: message_dict: MessageCQ序列化后的字典 """ - message_info = BaseMessageInfo.from_dict(message_dict.get('message_info', {})) - message_segment = Seg.from_dict(message_dict.get('message_segment', {})) - raw_message = message_dict.get('raw_message') - - super().__init__( - message_info=message_info, - message_segment=message_segment, - raw_message=raw_message - ) + self.message_info = BaseMessageInfo.from_dict(message_dict.get('message_info', {})) + self.message_segment = Seg.from_dict(message_dict.get('message_segment', {})) + self.raw_message = message_dict.get('raw_message') # 处理消息内容 self.processed_plain_text = "" # 初始化为空字符串 self.detailed_plain_text = "" # 初始化为空字符串 self.is_emoji=False + + def update_chat_stream(self,chat_stream:ChatStream): self.chat_stream=chat_stream @@ -110,48 +149,6 @@ class MessageRecv(MessageBase): ) return f"[{time_str}] {name}: {self.processed_plain_text}\n" -@dataclass -class Message(MessageBase): - chat_stream: ChatStream=None - reply: Optional['Message'] = None - detailed_plain_text: str = "" - processed_plain_text: str = "" - - def __init__( - self, - message_id: str, - time: int, - chat_stream: ChatStream, - user_info: UserInfo, - message_segment: Optional[Seg] = None, - reply: Optional['MessageRecv'] = None, - detailed_plain_text: str = "", - processed_plain_text: str = "", - ): - # 构造基础消息信息 - message_info = BaseMessageInfo( - platform=chat_stream.platform, - message_id=message_id, - time=time, - group_info=chat_stream.group_info, - user_info=user_info - ) - - # 调用父类初始化 - super().__init__( - message_info=message_info, - message_segment=message_segment, - raw_message=None - ) - - self.chat_stream = chat_stream - # 文本处理相关属性 - self.processed_plain_text = detailed_plain_text - self.detailed_plain_text = processed_plain_text - - # 回复消息 - self.reply = reply - @dataclass class MessageProcessBase(Message): From aa41f0d1d81d300709f475c8e6b2398a6f67df4e Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 11 Mar 2025 19:47:09 +0800 Subject: [PATCH 094/105] =?UTF-8?q?fix:=20=E6=94=BE=E5=8F=8D=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index d9f54dd47..c777e7273 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -51,8 +51,8 @@ class Message(MessageBase): self.chat_stream = chat_stream # 文本处理相关属性 - self.processed_plain_text = detailed_plain_text - self.detailed_plain_text = processed_plain_text + self.processed_plain_text = processed_plain_text + self.detailed_plain_text = detailed_plain_text # 回复消息 self.reply = reply From 3bf5cd6131456e630705e4e8e02a25647a48670d Mon Sep 17 00:00:00 2001 From: HYY1116 Date: Tue, 11 Mar 2025 20:11:17 +0800 Subject: [PATCH 095/105] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E9=87=8D=E8=BD=BD=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=9B=E6=96=B0=E5=A2=9E=E6=A0=B9=E6=8D=AE=E4=B8=8D?= =?UTF-8?q?=E5=90=8C=E7=8E=AF=E5=A2=83(dev;prod)=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E7=BA=A7=E5=88=AB=E7=9A=84log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 38 +++++++++++++++++---------- src/plugins/config_reload/__init__.py | 10 +++++++ src/plugins/config_reload/api.py | 17 ++++++++++++ src/plugins/config_reload/test.py | 3 +++ 4 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 src/plugins/config_reload/__init__.py create mode 100644 src/plugins/config_reload/api.py create mode 100644 src/plugins/config_reload/test.py diff --git a/bot.py b/bot.py index 9a5d47291..68068a9f9 100644 --- a/bot.py +++ b/bot.py @@ -51,15 +51,15 @@ def init_env(): with open(".env", "w") as f: f.write("ENVIRONMENT=prod") - # 检测.env.prod文件是否存在 - if not os.path.exists(".env.prod"): - logger.error("检测到.env.prod文件不存在") - shutil.copy("template.env", "./.env.prod") + # 检测.env.prod文件是否存在 + if not os.path.exists(".env.prod"): + logger.error("检测到.env.prod文件不存在") + shutil.copy("template.env", "./.env.prod") # 检测.env.dev文件是否存在,不存在的话直接复制生产环境配置 if not os.path.exists(".env.dev"): logger.error("检测到.env.dev文件不存在") - shutil.copy("template.env", "./.env.dev") + shutil.copy(".env.prod", "./.env.dev") # 首先加载基础环境变量.env if os.path.exists(".env"): @@ -99,15 +99,25 @@ def load_env(): def load_logger(): logger.remove() # 移除默认配置 - logger.add( - sys.stderr, - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", - colorize=True, - level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO - filter=lambda record: "nonebot" not in record["name"] - ) + if os.getenv("ENVIRONMENT") == "dev": + logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", + colorize=True, + level=os.getenv("LOG_LEVEL", "DEBUG"), # 根据环境设置日志级别,默认为DEBUG + ) + else: + logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", + colorize=True, + level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO + filter=lambda record: "nonebot" not in record["name"] + ) diff --git a/src/plugins/config_reload/__init__.py b/src/plugins/config_reload/__init__.py new file mode 100644 index 000000000..ddb7fa754 --- /dev/null +++ b/src/plugins/config_reload/__init__.py @@ -0,0 +1,10 @@ +from nonebot import get_app +from .api import router +from loguru import logger + +# 获取主应用实例并挂载路由 +app = get_app() +app.include_router(router, prefix="/api") + +# 打印日志,方便确认API已注册 +logger.success("配置重载API已注册,可通过 /api/reload-config 访问") \ No newline at end of file diff --git a/src/plugins/config_reload/api.py b/src/plugins/config_reload/api.py new file mode 100644 index 000000000..4202ba9bd --- /dev/null +++ b/src/plugins/config_reload/api.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, HTTPException +from src.plugins.chat.config import BotConfig +import os + +# 创建APIRouter而不是FastAPI实例 +router = APIRouter() + +@router.post("/reload-config") +async def reload_config(): + try: + bot_config_path = os.path.join(BotConfig.get_config_dir(), "bot_config.toml") + global_config = BotConfig.load_config(config_path=bot_config_path) + return {"message": "配置重载成功", "status": "success"} + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"重载配置时发生错误: {str(e)}") \ No newline at end of file diff --git a/src/plugins/config_reload/test.py b/src/plugins/config_reload/test.py new file mode 100644 index 000000000..b3b8a9e92 --- /dev/null +++ b/src/plugins/config_reload/test.py @@ -0,0 +1,3 @@ +import requests +response = requests.post("http://localhost:8080/api/reload-config") +print(response.json()) \ No newline at end of file From b362c355aa98dbd1a355cea1e40fa11d0e90f38e Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 22:17:41 +0800 Subject: [PATCH 096/105] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20flake.nix?= =?UTF-8?q?=20=EF=BC=8C=E9=87=87=E7=94=A8=20venv=20=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E7=94=9F=E6=88=90=E7=8E=AF=E5=A2=83=EF=BC=8Cnixos?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B9=9F=E5=8F=AF=E4=BB=A5=E6=9C=AC=E6=9C=BA?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E9=A1=B9=E7=9B=AE=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake.lock | 56 +++++++++++++++++------------------ flake.nix | 85 +++++++++++++++++++----------------------------------- 2 files changed, 56 insertions(+), 85 deletions(-) diff --git a/flake.lock b/flake.lock index dd215f1c6..894acd486 100644 --- a/flake.lock +++ b/flake.lock @@ -1,43 +1,21 @@ { "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1741196730, - "narHash": "sha256-0Sj6ZKjCpQMfWnN0NURqRCQn2ob7YtXTAOTwCuz7fkA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "48913d8f9127ea6530a2a2f1bd4daa1b8685d8a3", - "type": "github" + "lastModified": 0, + "narHash": "sha256-nJj8f78AYAxl/zqLiFGXn5Im1qjFKU8yBPKoWEeZN5M=", + "path": "/nix/store/f30jn7l0bf7a01qj029fq55i466vmnkh-source", + "type": "path" }, "original": { - "owner": "NixOS", - "ref": "nixos-24.11", - "repo": "nixpkgs", - "type": "github" + "id": "nixpkgs", + "type": "indirect" } }, "root": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "utils": "utils" } }, "systems": { @@ -54,6 +32,24 @@ "repo": "default", "type": "github" } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 3586857f0..7022dd68e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,62 +1,37 @@ { description = "MaiMBot Nix Dev Env"; - # 本配置仅方便用于开发,但是因为 nb-cli 上游打包中并未包含 nonebot2,因此目前本配置并不能用于运行和调试 inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; - flake-utils.url = "github:numtide/flake-utils"; + utils.url = "github:numtide/flake-utils"; }; - outputs = - { - self, - nixpkgs, - flake-utils, - }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = import nixpkgs { - inherit system; - }; + outputs = { + self, + nixpkgs, + utils, + ... + }: + utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs {inherit system;}; + pythonPackages = pkgs.python3Packages; + in { + devShells.default = pkgs.mkShell { + name = "python-venv"; + venvDir = "./.venv"; + buildInputs = [ + pythonPackages.python + pythonPackages.venvShellHook + ]; - pythonEnv = pkgs.python3.withPackages ( - ps: with ps; [ - ruff - pymongo - python-dotenv - pydantic - jieba - openai - aiohttp - requests - urllib3 - numpy - pandas - matplotlib - networkx - python-dateutil - APScheduler - loguru - tomli - customtkinter - colorama - pypinyin - pillow - setuptools - ] - ); - in - { - devShell = pkgs.mkShell { - buildInputs = [ - pythonEnv - pkgs.nb-cli - ]; - - shellHook = '' - ''; - }; - } - ); -} + postVenvCreation = '' + unset SOURCE_DATE_EPOCH + pip install -r requirements.txt + ''; + + postShellHook = '' + # allow pip to install wheels + unset SOURCE_DATE_EPOCH + ''; + }; + }); +} \ No newline at end of file From 2688a96986702d0368f378c230f144360446a9fb Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Tue, 11 Mar 2025 22:47:23 +0800 Subject: [PATCH 097/105] =?UTF-8?q?close=20SengokuCola/MaiMBot#225=20?= =?UTF-8?q?=E8=AE=A9=E9=BA=A6=E9=BA=A6=E5=8F=AF=E4=BB=A5=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=88=86=E4=BA=AB=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index c777e7273..20b9869f6 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,4 +1,7 @@ import time +import html +import re +import json from dataclasses import dataclass from typing import Dict, ForwardRef, List, Optional, Union @@ -69,6 +72,17 @@ class MessageRecv(Message): message_dict: MessageCQ序列化后的字典 """ self.message_info = BaseMessageInfo.from_dict(message_dict.get('message_info', {})) + + message_segment = message_dict.get('message_segment', {}) + + if message_segment.get('data','') == '[json]': + # 提取json消息中的展示信息 + pattern = r'\[CQ:json,data=(?P.+?)\]' + match = re.search(pattern, message_dict.get('raw_message','')) + raw_json = html.unescape(match.group('json_data')) + json_message = json.loads(raw_json) + message_segment['data'] = json_message.get('prompt','') + self.message_segment = Seg.from_dict(message_dict.get('message_segment', {})) self.raw_message = message_dict.get('raw_message') From 8c346377cbb221fab0550a325c3137dae2c37605 Mon Sep 17 00:00:00 2001 From: ChangingSelf Date: Tue, 11 Mar 2025 22:58:07 +0800 Subject: [PATCH 098/105] =?UTF-8?q?=E6=8F=90=E9=AB=98=E5=81=A5=E5=A3=AE?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 20b9869f6..0505c05a6 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -80,7 +80,10 @@ class MessageRecv(Message): pattern = r'\[CQ:json,data=(?P.+?)\]' match = re.search(pattern, message_dict.get('raw_message','')) raw_json = html.unescape(match.group('json_data')) - json_message = json.loads(raw_json) + try: + json_message = json.loads(raw_json) + except json.JSONDecodeError: + json_message = {} message_segment['data'] = json_message.get('prompt','') self.message_segment = Seg.from_dict(message_dict.get('message_segment', {})) From 26782c9181917fb7fb06f30588fc44ff2cd4fb79 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 22:58:40 +0800 Subject: [PATCH 099/105] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ENVIRONMENT?= =?UTF-8?q?=20=E5=8F=98=E9=87=8F=E5=9C=A8=E5=90=8C=E4=B8=80=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E4=B8=8B=E4=B8=8D=E8=83=BD=E8=A2=AB=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 68068a9f9..f6db08dca 100644 --- a/bot.py +++ b/bot.py @@ -63,7 +63,8 @@ def init_env(): # 首先加载基础环境变量.env if os.path.exists(".env"): - load_dotenv(".env") + load_dotenv(".env",override=True) + print(os.getenv("ENVIRONMENT")) logger.success("成功加载基础环境变量配置") From e54038f3d086c254213e9f85dd2a09edc7edf737 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 22:59:56 +0800 Subject: [PATCH 100/105] =?UTF-8?q?fix:=20=E4=BB=8E=20nixpkgs=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20numpy=20=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=87=BA=E7=8E=B0=20libc++.so=20=E6=89=BE?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 7022dd68e..404f7555c 100644 --- a/flake.nix +++ b/flake.nix @@ -21,13 +21,14 @@ buildInputs = [ pythonPackages.python pythonPackages.venvShellHook + pythonPackages.numpy ]; postVenvCreation = '' unset SOURCE_DATE_EPOCH pip install -r requirements.txt ''; - + postShellHook = '' # allow pip to install wheels unset SOURCE_DATE_EPOCH From c681a827f1127cfdd2f4c5773041b7d73e926f00 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 11 Mar 2025 23:21:57 +0800 Subject: [PATCH 101/105] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B0=8F=E5=90=8D?= =?UTF-8?q?=E6=97=A0=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 55fb9eb43..7c658fbf7 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -39,9 +39,13 @@ def db_message_to_str(message_dict: Dict) -> str: def is_mentioned_bot_in_message(message: MessageRecv) -> bool: """检查消息是否提到了机器人""" keywords = [global_config.BOT_NICKNAME] + nicknames = global_config.BOT_ALIAS_NAMES for keyword in keywords: if keyword in message.processed_plain_text: return True + for nickname in nicknames: + if nickname in message.processed_plain_text: + return True return False From 80ed56835cb1ca7b310f3c81bcc276dd9cca1a65 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 11 Mar 2025 23:39:25 +0800 Subject: [PATCH 102/105] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4print=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot.py b/bot.py index f6db08dca..36d621a6e 100644 --- a/bot.py +++ b/bot.py @@ -64,7 +64,6 @@ def init_env(): # 首先加载基础环境变量.env if os.path.exists(".env"): load_dotenv(".env",override=True) - print(os.getenv("ENVIRONMENT")) logger.success("成功加载基础环境变量配置") From ed18f2e96de80562db8e4dcbfa3de52a4d7a7219 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 11 Mar 2025 23:46:49 +0800 Subject: [PATCH 103/105] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E4=B8=80=E9=94=AE=E5=90=AF=E5=8A=A8=E6=BC=82?= =?UTF-8?q?=E4=BA=AE=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + run.py | 12 +- src/common/database.py | 25 +- src/plugins/chat/bot.py | 3 +- src/plugins/chat/llm_generator.py | 2 +- src/plugins/chat/message.py | 6 +- src/plugins/chat/message_base.py | 2 +- src/plugins/chat/message_cq.py | 6 +- src/plugins/chat/message_sender.py | 4 +- src/plugins/chat/prompt_builder.py | 2 +- src/plugins/chat/relationship_manager.py | 3 +- src/plugins/chat/storage.py | 2 - src/plugins/chat/utils.py | 4 +- src/plugins/chat/utils_image.py | 6 +- src/plugins/chat/willing_manager.py | 6 +- src/plugins/knowledege/knowledge_library.py | 199 --------- .../memory_system/memory_manual_build.py | 1 - src/plugins/zhishi/knowledge_library.py | 383 ++++++++++++++++++ 麦麦开始学习.bat | 45 ++ 19 files changed, 454 insertions(+), 258 deletions(-) delete mode 100644 src/plugins/knowledege/knowledge_library.py create mode 100644 src/plugins/zhishi/knowledge_library.py create mode 100644 麦麦开始学习.bat diff --git a/.gitignore b/.gitignore index e51abc5cc..6e1be60b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ data/ +data1/ mongodb/ NapCat.Framework.Windows.Once/ log/ diff --git a/run.py b/run.py index 50e312c37..cfd3a5f14 100644 --- a/run.py +++ b/run.py @@ -128,13 +128,17 @@ if __name__ == "__main__": ) os.system("cls") if choice == "1": - install_napcat() - install_mongodb() + confirm = input("首次安装将下载并配置所需组件\n1.确认\n2.取消\n") + if confirm == "1": + install_napcat() + install_mongodb() + else: + print("已取消安装") elif choice == "2": run_maimbot() - choice = input("是否启动推理可视化?(y/N)").upper() + choice = input("是否启动推理可视化?(未完善)(y/N)").upper() if choice == "Y": run_cmd(r"python src\gui\reasoning_gui.py") - choice = input("是否启动记忆可视化?(y/N)").upper() + choice = input("是否启动记忆可视化?(未完善)(y/N)").upper() if choice == "Y": run_cmd(r"python src/plugins/memory_system/memory_manual_build.py") diff --git a/src/common/database.py b/src/common/database.py index f0954b07c..d592b0f90 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -1,8 +1,6 @@ from typing import Optional - from pymongo import MongoClient - class Database: _instance: Optional["Database"] = None @@ -50,25 +48,4 @@ class Database: def get_instance(cls) -> "Database": if cls._instance is None: raise RuntimeError("Database not initialized") - return cls._instance - - - #测试用 - - def get_random_group_messages(self, group_id: str, limit: int = 5): - # 先随机获取一条消息 - random_message = list(self.db.messages.aggregate([ - {"$match": {"group_id": group_id}}, - {"$sample": {"size": 1}} - ]))[0] - - # 获取该消息之后的消息 - subsequent_messages = list(self.db.messages.find({ - "group_id": group_id, - "time": {"$gt": random_message["time"]} - }).sort("time", 1).limit(limit)) - - # 将随机消息和后续消息合并 - messages = [random_message] + subsequent_messages - - return messages \ No newline at end of file + return cls._instance \ No newline at end of file diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 9b2ac06f1..5bd502a7e 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -7,7 +7,6 @@ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent from ..memory_system.memory import hippocampus from ..moods.moods import MoodManager # 导入情绪管理器 from .config import global_config -from .cq_code import CQCode, cq_code_tool # 导入CQCode模块 from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator from .message import MessageSending, MessageRecv, MessageThinking, MessageSet @@ -218,7 +217,7 @@ class ChatBot: # message_set 可以直接加入 message_manager # print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") - print(f"添加message_set到message_manager") + print("添加message_set到message_manager") message_manager.add_message(message_set) bot_response_time = thinking_time_point diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index af7334afe..46dc34e92 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -8,7 +8,7 @@ from loguru import logger from ...common.database import Database from ..models.utils_model import LLM_request from .config import global_config -from .message import MessageRecv, MessageThinking, MessageSending,Message +from .message import MessageRecv, MessageThinking, Message from .prompt_builder import prompt_builder from .relationship_manager import relationship_manager from .utils import process_llm_response diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 0505c05a6..d848f068f 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -3,14 +3,14 @@ import html import re import json from dataclasses import dataclass -from typing import Dict, ForwardRef, List, Optional, Union +from typing import Dict, List, Optional import urllib3 from loguru import logger from .utils_image import image_manager -from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase -from .chat_stream import ChatStream, chat_manager +from .message_base import Seg, UserInfo, BaseMessageInfo, MessageBase +from .chat_stream import ChatStream # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py index d17c2c357..ae7ec3872 100644 --- a/src/plugins/chat/message_base.py +++ b/src/plugins/chat/message_base.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, asdict -from typing import List, Optional, Union, Any, Dict +from typing import List, Optional, Union, Dict @dataclass class Seg: diff --git a/src/plugins/chat/message_cq.py b/src/plugins/chat/message_cq.py index 6bfa47c3f..cb47ae4b3 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -1,12 +1,12 @@ import time from dataclasses import dataclass -from typing import Dict, ForwardRef, List, Optional, Union +from typing import Dict, Optional import urllib3 -from .cq_code import CQCode, cq_code_tool +from .cq_code import cq_code_tool from .utils_cq import parse_cq_code -from .utils_user import get_groupname, get_user_cardname, get_user_nickname +from .utils_user import get_groupname from .message_base import Seg, GroupInfo, UserInfo, BaseMessageInfo, MessageBase # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 9db74633f..eefa6f4ae 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -5,12 +5,10 @@ from typing import Dict, List, Optional, Union from loguru import logger from nonebot.adapters.onebot.v11 import Bot -from .cq_code import cq_code_tool from .message_cq import MessageSendCQ -from .message import MessageSending, MessageThinking, MessageRecv,MessageSet +from .message import MessageSending, MessageThinking, MessageSet from .storage import MessageStorage from .config import global_config -from .chat_stream import chat_manager class Message_Sender: diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index fec6c7926..b97666763 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -9,7 +9,7 @@ from ..moods.moods import MoodManager from ..schedule.schedule_generator import bot_schedule from .config import global_config from .utils import get_embedding, get_recent_group_detailed_plain_text -from .chat_stream import ChatStream, chat_manager +from .chat_stream import chat_manager class PromptBuilder: diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 9e7cafda0..90e92e7b6 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,6 +1,5 @@ import asyncio -from typing import Optional, Union -from typing import Optional, Union +from typing import Optional from loguru import logger from ...common.database import Database diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index f403b2c8b..c3986a2d0 100644 --- a/src/plugins/chat/storage.py +++ b/src/plugins/chat/storage.py @@ -1,8 +1,6 @@ from typing import Optional, Union -from typing import Optional, Union from ...common.database import Database -from .message_base import MessageBase from .message import MessageSending, MessageRecv from .chat_stream import ChatStream from loguru import logger diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 7c658fbf7..186f2ab79 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -12,8 +12,8 @@ from loguru import logger from ..models.utils_model import LLM_request from ..utils.typo_generator import ChineseTypoGenerator from .config import global_config -from .message import MessageThinking, MessageRecv,MessageSending,MessageProcessBase,Message -from .message_base import MessageBase,BaseMessageInfo,UserInfo,GroupInfo +from .message import MessageRecv,Message +from .message_base import UserInfo from .chat_stream import ChatStream from ..moods.moods import MoodManager diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 25f23359b..42d5f9efc 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -1,16 +1,12 @@ import base64 -import io import os import time -import zlib import aiohttp import hashlib -from typing import Optional, Tuple, Union -from urllib.parse import urlparse +from typing import Optional, Union from loguru import logger from nonebot import get_driver -from PIL import Image from ...common.database import Database from ..chat.config import global_config diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 39083f0b8..f34afb746 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -1,13 +1,9 @@ import asyncio from typing import Dict -from loguru import logger -from typing import Dict -from loguru import logger from .config import global_config -from .message_base import UserInfo, GroupInfo -from .chat_stream import chat_manager,ChatStream +from .chat_stream import ChatStream class WillingManager: diff --git a/src/plugins/knowledege/knowledge_library.py b/src/plugins/knowledege/knowledge_library.py deleted file mode 100644 index e9d7167fd..000000000 --- a/src/plugins/knowledege/knowledge_library.py +++ /dev/null @@ -1,199 +0,0 @@ -import os -import sys -import time - -import requests -from dotenv import load_dotenv - -# 添加项目根目录到 Python 路径 -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -# 加载根目录下的env.edv文件 -env_path = os.path.join(root_path, ".env.dev") -if not os.path.exists(env_path): - raise FileNotFoundError(f"配置文件不存在: {env_path}") -load_dotenv(env_path) - -from src.common.database import Database - -# 从环境变量获取配置 -Database.initialize( - uri=os.getenv("MONGODB_URI"), - host=os.getenv("MONGODB_HOST", "127.0.0.1"), - port=int(os.getenv("MONGODB_PORT", "27017")), - db_name=os.getenv("DATABASE_NAME", "MegBot"), - username=os.getenv("MONGODB_USERNAME"), - password=os.getenv("MONGODB_PASSWORD"), - auth_source=os.getenv("MONGODB_AUTH_SOURCE"), -) - -class KnowledgeLibrary: - def __init__(self): - self.db = Database.get_instance() - self.raw_info_dir = "data/raw_info" - self._ensure_dirs() - self.api_key = os.getenv("SILICONFLOW_KEY") - if not self.api_key: - raise ValueError("SILICONFLOW_API_KEY 环境变量未设置") - - def _ensure_dirs(self): - """确保必要的目录存在""" - os.makedirs(self.raw_info_dir, exist_ok=True) - - def get_embedding(self, text: str) -> list: - """获取文本的embedding向量""" - url = "https://api.siliconflow.cn/v1/embeddings" - payload = { - "model": "BAAI/bge-m3", - "input": text, - "encoding_format": "float" - } - headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - - response = requests.post(url, json=payload, headers=headers) - if response.status_code != 200: - print(f"获取embedding失败: {response.text}") - return None - - return response.json()['data'][0]['embedding'] - - def process_files(self): - """处理raw_info目录下的所有txt文件""" - for filename in os.listdir(self.raw_info_dir): - if filename.endswith('.txt'): - file_path = os.path.join(self.raw_info_dir, filename) - self.process_single_file(file_path) - - def process_single_file(self, file_path: str): - """处理单个文件""" - try: - # 检查文件是否已处理 - if self.db.db.processed_files.find_one({"file_path": file_path}): - print(f"文件已处理过,跳过: {file_path}") - return - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # 按1024字符分段 - segments = [content[i:i+600] for i in range(0, len(content), 300)] - - # 处理每个分段 - for segment in segments: - if not segment.strip(): # 跳过空段 - continue - - # 获取embedding - embedding = self.get_embedding(segment) - if not embedding: - continue - - # 存储到数据库 - doc = { - "content": segment, - "embedding": embedding, - "file_path": file_path, - "segment_length": len(segment) - } - - # 使用文本内容的哈希值作为唯一标识 - content_hash = hash(segment) - - # 更新或插入文档 - self.db.db.knowledges.update_one( - {"content_hash": content_hash}, - {"$set": doc}, - upsert=True - ) - - # 记录文件已处理 - self.db.db.processed_files.insert_one({ - "file_path": file_path, - "processed_time": time.time() - }) - - print(f"成功处理文件: {file_path}") - - except Exception as e: - print(f"处理文件 {file_path} 时出错: {str(e)}") - - def search_similar_segments(self, query: str, limit: int = 5) -> list: - """搜索与查询文本相似的片段""" - query_embedding = self.get_embedding(query) - if not query_embedding: - return [] - - # 使用余弦相似度计算 - pipeline = [ - { - "$addFields": { - "dotProduct": { - "$reduce": { - "input": {"$range": [0, {"$size": "$embedding"}]}, - "initialValue": 0, - "in": { - "$add": [ - "$$value", - {"$multiply": [ - {"$arrayElemAt": ["$embedding", "$$this"]}, - {"$arrayElemAt": [query_embedding, "$$this"]} - ]} - ] - } - } - }, - "magnitude1": { - "$sqrt": { - "$reduce": { - "input": "$embedding", - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]} - } - } - }, - "magnitude2": { - "$sqrt": { - "$reduce": { - "input": query_embedding, - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]} - } - } - } - } - }, - { - "$addFields": { - "similarity": { - "$divide": ["$dotProduct", {"$multiply": ["$magnitude1", "$magnitude2"]}] - } - } - }, - {"$sort": {"similarity": -1}}, - {"$limit": limit}, - {"$project": {"content": 1, "similarity": 1, "file_path": 1}} - ] - - results = list(self.db.db.knowledges.aggregate(pipeline)) - return results - -# 创建单例实例 -knowledge_library = KnowledgeLibrary() - -if __name__ == "__main__": - # 测试知识库功能 - print("开始处理知识库文件...") - knowledge_library.process_files() - - # 测试搜索功能 - test_query = "麦麦评价一下僕と花" - print(f"\n搜索与'{test_query}'相似的内容:") - results = knowledge_library.search_similar_segments(test_query) - for result in results: - print(f"相似度: {result['similarity']:.4f}") - print(f"内容: {result['content'][:100]}...") - print("-" * 50) diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py index 9c1d43ce9..736a50e97 100644 --- a/src/plugins/memory_system/memory_manual_build.py +++ b/src/plugins/memory_system/memory_manual_build.py @@ -10,7 +10,6 @@ from pathlib import Path import matplotlib.pyplot as plt import networkx as nx -import pymongo from dotenv import load_dotenv from loguru import logger import jieba diff --git a/src/plugins/zhishi/knowledge_library.py b/src/plugins/zhishi/knowledge_library.py new file mode 100644 index 000000000..2411e3112 --- /dev/null +++ b/src/plugins/zhishi/knowledge_library.py @@ -0,0 +1,383 @@ +import os +import sys +import time +import requests +from dotenv import load_dotenv +import hashlib +from datetime import datetime +from tqdm import tqdm +from rich.console import Console +from rich.table import Table + +# 添加项目根目录到 Python 路径 +root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +sys.path.append(root_path) + +# 现在可以导入src模块 +from src.common.database import Database + +# 加载根目录下的env.edv文件 +env_path = os.path.join(root_path, ".env.prod") +if not os.path.exists(env_path): + raise FileNotFoundError(f"配置文件不存在: {env_path}") +load_dotenv(env_path) + +class KnowledgeLibrary: + def __init__(self): + # 初始化数据库连接 + if Database._instance is None: + Database.initialize( + uri=os.getenv("MONGODB_URI"), + host=os.getenv("MONGODB_HOST", "127.0.0.1"), + port=int(os.getenv("MONGODB_PORT", "27017")), + db_name=os.getenv("DATABASE_NAME", "MegBot"), + username=os.getenv("MONGODB_USERNAME"), + password=os.getenv("MONGODB_PASSWORD"), + auth_source=os.getenv("MONGODB_AUTH_SOURCE"), + ) + self.db = Database.get_instance() + self.raw_info_dir = "data/raw_info" + self._ensure_dirs() + self.api_key = os.getenv("SILICONFLOW_KEY") + if not self.api_key: + raise ValueError("SILICONFLOW_API_KEY 环境变量未设置") + self.console = Console() + + def _ensure_dirs(self): + """确保必要的目录存在""" + os.makedirs(self.raw_info_dir, exist_ok=True) + + def read_file(self, file_path: str) -> str: + """读取文件内容""" + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + + def split_content(self, content: str, max_length: int = 512) -> list: + """将内容分割成适当大小的块,保持段落完整性 + + Args: + content: 要分割的文本内容 + max_length: 每个块的最大长度 + + Returns: + list: 分割后的文本块列表 + """ + # 首先按段落分割 + paragraphs = [p.strip() for p in content.split('\n\n') if p.strip()] + chunks = [] + current_chunk = [] + current_length = 0 + + for para in paragraphs: + para_length = len(para) + + # 如果单个段落就超过最大长度 + if para_length > max_length: + # 如果当前chunk不为空,先保存 + if current_chunk: + chunks.append('\n'.join(current_chunk)) + current_chunk = [] + current_length = 0 + + # 将长段落按句子分割 + sentences = [s.strip() for s in para.replace('。', '。\n').replace('!', '!\n').replace('?', '?\n').split('\n') if s.strip()] + temp_chunk = [] + temp_length = 0 + + for sentence in sentences: + sentence_length = len(sentence) + if sentence_length > max_length: + # 如果单个句子超长,强制按长度分割 + if temp_chunk: + chunks.append('\n'.join(temp_chunk)) + temp_chunk = [] + temp_length = 0 + for i in range(0, len(sentence), max_length): + chunks.append(sentence[i:i + max_length]) + elif temp_length + sentence_length + 1 <= max_length: + temp_chunk.append(sentence) + temp_length += sentence_length + 1 + else: + chunks.append('\n'.join(temp_chunk)) + temp_chunk = [sentence] + temp_length = sentence_length + + if temp_chunk: + chunks.append('\n'.join(temp_chunk)) + + # 如果当前段落加上现有chunk不超过最大长度 + elif current_length + para_length + 1 <= max_length: + current_chunk.append(para) + current_length += para_length + 1 + else: + # 保存当前chunk并开始新的chunk + chunks.append('\n'.join(current_chunk)) + current_chunk = [para] + current_length = para_length + + # 添加最后一个chunk + if current_chunk: + chunks.append('\n'.join(current_chunk)) + + return chunks + + def get_embedding(self, text: str) -> list: + """获取文本的embedding向量""" + url = "https://api.siliconflow.cn/v1/embeddings" + payload = { + "model": "BAAI/bge-m3", + "input": text, + "encoding_format": "float" + } + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + response = requests.post(url, json=payload, headers=headers) + if response.status_code != 200: + print(f"获取embedding失败: {response.text}") + return None + + return response.json()['data'][0]['embedding'] + + def process_files(self, knowledge_length:int=512): + """处理raw_info目录下的所有txt文件""" + txt_files = [f for f in os.listdir(self.raw_info_dir) if f.endswith('.txt')] + + if not txt_files: + self.console.print("[red]警告:在 {} 目录下没有找到任何txt文件[/red]".format(self.raw_info_dir)) + self.console.print("[yellow]请将需要处理的文本文件放入该目录后再运行程序[/yellow]") + return + + total_stats = { + "processed_files": 0, + "total_chunks": 0, + "failed_files": [], + "skipped_files": [] + } + + self.console.print(f"\n[bold blue]开始处理知识库文件 - 共{len(txt_files)}个文件[/bold blue]") + + for filename in tqdm(txt_files, desc="处理文件进度"): + file_path = os.path.join(self.raw_info_dir, filename) + result = self.process_single_file(file_path, knowledge_length) + self._update_stats(total_stats, result, filename) + + self._display_processing_results(total_stats) + + def process_single_file(self, file_path: str, knowledge_length: int = 512): + """处理单个文件""" + result = { + "status": "success", + "chunks_processed": 0, + "error": None + } + + try: + current_hash = self.calculate_file_hash(file_path) + processed_record = self.db.db.processed_files.find_one({"file_path": file_path}) + + if processed_record: + if processed_record.get("hash") == current_hash: + if knowledge_length in processed_record.get("split_by", []): + result["status"] = "skipped" + return result + + content = self.read_file(file_path) + chunks = self.split_content(content, knowledge_length) + + for chunk in tqdm(chunks, desc=f"处理 {os.path.basename(file_path)} 的文本块", leave=False): + embedding = self.get_embedding(chunk) + if embedding: + knowledge = { + "content": chunk, + "embedding": embedding, + "source_file": file_path, + "split_length": knowledge_length, + "created_at": datetime.now() + } + self.db.db.knowledges.insert_one(knowledge) + result["chunks_processed"] += 1 + + split_by = processed_record.get("split_by", []) if processed_record else [] + if knowledge_length not in split_by: + split_by.append(knowledge_length) + + self.db.db.processed_files.update_one( + {"file_path": file_path}, + { + "$set": { + "hash": current_hash, + "last_processed": datetime.now(), + "split_by": split_by + } + }, + upsert=True + ) + + except Exception as e: + result["status"] = "failed" + result["error"] = str(e) + + return result + + def _update_stats(self, total_stats, result, filename): + """更新总体统计信息""" + if result["status"] == "success": + total_stats["processed_files"] += 1 + total_stats["total_chunks"] += result["chunks_processed"] + elif result["status"] == "failed": + total_stats["failed_files"].append((filename, result["error"])) + elif result["status"] == "skipped": + total_stats["skipped_files"].append(filename) + + def _display_processing_results(self, stats): + """显示处理结果统计""" + self.console.print("\n[bold green]处理完成!统计信息如下:[/bold green]") + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("统计项", style="dim") + table.add_column("数值") + + table.add_row("成功处理文件数", str(stats["processed_files"])) + table.add_row("处理的知识块总数", str(stats["total_chunks"])) + table.add_row("跳过的文件数", str(len(stats["skipped_files"]))) + table.add_row("失败的文件数", str(len(stats["failed_files"]))) + + self.console.print(table) + + if stats["failed_files"]: + self.console.print("\n[bold red]处理失败的文件:[/bold red]") + for filename, error in stats["failed_files"]: + self.console.print(f"[red]- {filename}: {error}[/red]") + + if stats["skipped_files"]: + self.console.print("\n[bold yellow]跳过的文件(已处理):[/bold yellow]") + for filename in stats["skipped_files"]: + self.console.print(f"[yellow]- {filename}[/yellow]") + + def calculate_file_hash(self, file_path): + """计算文件的MD5哈希值""" + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + def search_similar_segments(self, query: str, limit: int = 5) -> list: + """搜索与查询文本相似的片段""" + query_embedding = self.get_embedding(query) + if not query_embedding: + return [] + + # 使用余弦相似度计算 + pipeline = [ + { + "$addFields": { + "dotProduct": { + "$reduce": { + "input": {"$range": [0, {"$size": "$embedding"}]}, + "initialValue": 0, + "in": { + "$add": [ + "$$value", + {"$multiply": [ + {"$arrayElemAt": ["$embedding", "$$this"]}, + {"$arrayElemAt": [query_embedding, "$$this"]} + ]} + ] + } + } + }, + "magnitude1": { + "$sqrt": { + "$reduce": { + "input": "$embedding", + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]} + } + } + }, + "magnitude2": { + "$sqrt": { + "$reduce": { + "input": query_embedding, + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]} + } + } + } + } + }, + { + "$addFields": { + "similarity": { + "$divide": ["$dotProduct", {"$multiply": ["$magnitude1", "$magnitude2"]}] + } + } + }, + {"$sort": {"similarity": -1}}, + {"$limit": limit}, + {"$project": {"content": 1, "similarity": 1, "file_path": 1}} + ] + + results = list(self.db.db.knowledges.aggregate(pipeline)) + return results + +# 创建单例实例 +knowledge_library = KnowledgeLibrary() + +if __name__ == "__main__": + console = Console() + console.print("[bold green]知识库处理工具[/bold green]") + + while True: + console.print("\n请选择要执行的操作:") + console.print("[1] 麦麦开始学习") + console.print("[2] 麦麦全部忘光光(仅知识)") + console.print("[q] 退出程序") + + choice = input("\n请输入选项: ").strip() + + if choice.lower() == 'q': + console.print("[yellow]程序退出[/yellow]") + sys.exit(0) + elif choice == '2': + confirm = input("确定要删除所有知识吗?这个操作不可撤销!(y/n): ").strip().lower() + if confirm == 'y': + knowledge_library.db.db.knowledges.delete_many({}) + console.print("[green]已清空所有知识![/green]") + continue + elif choice == '1': + if not os.path.exists(knowledge_library.raw_info_dir): + console.print(f"[yellow]创建目录:{knowledge_library.raw_info_dir}[/yellow]") + os.makedirs(knowledge_library.raw_info_dir, exist_ok=True) + + # 询问分割长度 + while True: + try: + length_input = input("请输入知识分割长度(默认512,输入q退出,回车使用默认值): ").strip() + if length_input.lower() == 'q': + break + if not length_input: # 如果直接回车,使用默认值 + knowledge_length = 512 + break + knowledge_length = int(length_input) + if knowledge_length <= 0: + print("分割长度必须大于0,请重新输入") + continue + break + except ValueError: + print("请输入有效的数字") + continue + + if length_input.lower() == 'q': + continue + + # 测试知识库功能 + print(f"开始处理知识库文件,使用分割长度: {knowledge_length}...") + knowledge_library.process_files(knowledge_length=knowledge_length) + else: + console.print("[red]无效的选项,请重新选择[/red]") + continue diff --git a/麦麦开始学习.bat b/麦麦开始学习.bat new file mode 100644 index 000000000..f7391150f --- /dev/null +++ b/麦麦开始学习.bat @@ -0,0 +1,45 @@ +@echo off +setlocal enabledelayedexpansion +chcp 65001 +cd /d %~dp0 + +echo ===================================== +echo 选择Python环境: +echo 1 - venv (推荐) +echo 2 - conda +echo ===================================== +choice /c 12 /n /m "输入数字(1或2): " + +if errorlevel 2 ( + echo ===================================== + set "CONDA_ENV=" + set /p CONDA_ENV="请输入要激活的 conda 环境名称: " + + :: 检查输入是否为空 + if "!CONDA_ENV!"=="" ( + echo 错误:环境名称不能为空 + pause + exit /b 1 + ) + + call conda activate !CONDA_ENV! + if errorlevel 1 ( + echo 激活 conda 环境失败 + pause + exit /b 1 + ) + + echo Conda 环境 "!CONDA_ENV!" 激活成功 + python src/plugins/zhishi/knowledge_library.py +) else ( + if exist "venv\Scripts\python.exe" ( + venv\Scripts\python src/plugins/zhishi/knowledge_library.py + ) else ( + echo ===================================== + echo 错误: venv环境不存在,请先创建虚拟环境 + pause + exit /b 1 + ) +) +endlocal +pause From a91ef7bf489e5be7c30850066c4954e904013a36 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 12 Mar 2025 00:05:19 +0800 Subject: [PATCH 104/105] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/auto_update.py | 59 +++++++++++++++++++++++++++++ requirements.txt | Bin 612 -> 630 bytes 如果你的配置文件版本太老就点我.bat | 45 ++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 config/auto_update.py create mode 100644 如果你的配置文件版本太老就点我.bat diff --git a/config/auto_update.py b/config/auto_update.py new file mode 100644 index 000000000..28ab108da --- /dev/null +++ b/config/auto_update.py @@ -0,0 +1,59 @@ +import os +import shutil +import tomlkit +from pathlib import Path + +def update_config(): + # 获取根目录路径 + root_dir = Path(__file__).parent.parent + template_dir = root_dir / "template" + config_dir = root_dir / "config" + + # 定义文件路径 + template_path = template_dir / "bot_config_template.toml" + old_config_path = config_dir / "bot_config.toml" + new_config_path = config_dir / "bot_config.toml" + + # 读取旧配置文件 + old_config = {} + if old_config_path.exists(): + with open(old_config_path, "r", encoding="utf-8") as f: + old_config = tomlkit.load(f) + + # 删除旧的配置文件 + if old_config_path.exists(): + os.remove(old_config_path) + + # 复制模板文件到配置目录 + shutil.copy2(template_path, new_config_path) + + # 读取新配置文件 + with open(new_config_path, "r", encoding="utf-8") as f: + new_config = tomlkit.load(f) + + # 递归更新配置 + def update_dict(target, source): + for key, value in source.items(): + # 跳过version字段的更新 + if key == "version": + continue + if key in target: + if isinstance(value, dict) and isinstance(target[key], (dict, tomlkit.items.Table)): + update_dict(target[key], value) + else: + try: + # 直接使用tomlkit的item方法创建新值 + target[key] = tomlkit.item(value) + except (TypeError, ValueError): + # 如果转换失败,直接赋值 + target[key] = value + + # 将旧配置的值更新到新配置中 + update_dict(new_config, old_config) + + # 保存更新后的配置(保留注释和格式) + with open(new_config_path, "w", encoding="utf-8") as f: + f.write(tomlkit.dumps(new_config)) + +if __name__ == "__main__": + update_config() diff --git a/requirements.txt b/requirements.txt index 0acaade5e6d465465ee87009b5c28551e591953d..8330c8d06fa1c57ae6bce0de82cec7764a199d94 100644 GIT binary patch delta 25 gcmaFD@{MJ~Jx1OVhJ1!xh8%`$hD?T%$u}9h0d$}UdjJ3c delta 11 Scmeyy@`Po>J;uo&7~23MmIZtO diff --git a/如果你的配置文件版本太老就点我.bat b/如果你的配置文件版本太老就点我.bat new file mode 100644 index 000000000..fec1f4cdb --- /dev/null +++ b/如果你的配置文件版本太老就点我.bat @@ -0,0 +1,45 @@ +@echo off +setlocal enabledelayedexpansion +chcp 65001 +cd /d %~dp0 + +echo ===================================== +echo 选择Python环境: +echo 1 - venv (推荐) +echo 2 - conda +echo ===================================== +choice /c 12 /n /m "输入数字(1或2): " + +if errorlevel 2 ( + echo ===================================== + set "CONDA_ENV=" + set /p CONDA_ENV="请输入要激活的 conda 环境名称: " + + :: 检查输入是否为空 + if "!CONDA_ENV!"=="" ( + echo 错误:环境名称不能为空 + pause + exit /b 1 + ) + + call conda activate !CONDA_ENV! + if errorlevel 1 ( + echo 激活 conda 环境失败 + pause + exit /b 1 + ) + + echo Conda 环境 "!CONDA_ENV!" 激活成功 + python config/auto_update.py +) else ( + if exist "venv\Scripts\python.exe" ( + venv\Scripts\python config/auto_update.py + ) else ( + echo ===================================== + echo 错误: venv环境不存在,请先创建虚拟环境 + pause + exit /b 1 + ) +) +endlocal +pause From cbb569e7767ac85d84d60fe47d2d6e09f56de265 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 12 Mar 2025 00:18:28 +0800 Subject: [PATCH 105/105] =?UTF-8?q?Create=20=E5=A6=82=E6=9E=9C=E4=BD=A0?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E7=89=88=E6=9C=AC=EF=BC=8C=E7=82=B9?= =?UTF-8?q?=E6=88=91.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 如果你更新了版本,点我.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 如果你更新了版本,点我.txt diff --git a/如果你更新了版本,点我.txt b/如果你更新了版本,点我.txt new file mode 100644 index 000000000..400e8ae0c --- /dev/null +++ b/如果你更新了版本,点我.txt @@ -0,0 +1,4 @@ +更新版本后,建议删除数据库messages中所有内容,不然会出现报错 +该操作不会影响你的记忆 + +如果显示配置文件版本过低,运行根目录的bat \ No newline at end of file