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/README.md b/README.md index 5fddcb320..ad318aecc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ - MongoDB 提供数据持久化支持 - NapCat 作为QQ协议端支持 -**最新版本: v0.5.*** +**最新版本: v0.5.13** +> [!WARNING] +> 注意,3月12日的v0.5.13, 该版本更新较大,建议单独开文件夹部署,然后转移/data文件 和数据库,数据库可能需要删除messages下的内容(不需要删除记忆) + +
@@ -40,7 +44,12 @@ - [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -**其他平台版本** + + +**📚 有热心网友创作的wiki:** https://maimbot.pages.dev/ + + +**😊 其他平台版本** - (由 [CabLate](https://github.com/cablate) 贡献) [Telegram 与其他平台(未来可能会有)的版本](https://github.com/cablate/MaiMBot/tree/telegram) - [集中讨论串](https://github.com/SengokuCola/MaiMBot/discussions/149) diff --git a/bot.py b/bot.py index 9a5d47291..8d51cee3c 100644 --- a/bot.py +++ b/bot.py @@ -12,6 +12,8 @@ from loguru import logger from nonebot.adapters.onebot.v11 import Adapter import platform +from src.common.database import Database + # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} @@ -51,19 +53,19 @@ 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"): - load_dotenv(".env") + load_dotenv(".env",override=True) logger.success("成功加载基础环境变量配置") @@ -96,18 +98,39 @@ def load_env(): logger.error(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在") RuntimeError(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在") +def init_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"), + ) + 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"] + ) @@ -188,6 +211,7 @@ def raw_main(): init_config() init_env() load_env() + init_database() # 加载完成环境后初始化database load_logger() env_config = {key: os.getenv(key) for key in os.environ} @@ -213,7 +237,6 @@ def raw_main(): if __name__ == "__main__": - try: raw_main() diff --git a/changelog.md b/changelog.md index c68a16ad9..b9beed81e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,84 @@ # Changelog -## [0.5.12] - 2025-3-9 -### Added -- 新增了 我是测试 +## [0.5.13] - 2025-3-12 +AI总结 +### 🌟 核心功能增强 +#### 记忆系统升级 +- 新增了记忆系统的时间戳功能,包括创建时间和最后修改时间 +- 新增了记忆图节点和边的时间追踪功能 +- 新增了自动补充缺失时间字段的功能 +- 新增了记忆遗忘机制,基于时间条件自动遗忘旧记忆 +- 优化了记忆系统的数据同步机制 +- 优化了记忆系统的数据结构,确保所有数据类型的一致性 + +#### 私聊功能完善 +- 新增了完整的私聊功能支持,包括消息处理和回复 +- 新增了聊天流管理器,支持群聊和私聊的上下文管理 +- 新增了私聊过滤开关功能 +- 优化了关系管理系统,支持跨平台用户关系 + +#### 消息处理升级 +- 新增了消息队列管理系统,支持按时间顺序处理消息 +- 新增了消息发送控制器,实现人性化的发送速度和间隔 +- 新增了JSON格式分享卡片读取支持 +- 新增了Base64格式表情包CQ码支持 +- 改进了消息处理流程,支持多种消息类型 + +### 💻 系统架构优化 +#### 配置系统改进 +- 新增了配置文件自动更新和版本检测功能 +- 新增了配置文件热重载API接口 +- 新增了配置文件版本兼容性检查 +- 新增了根据不同环境(dev/prod)显示不同级别的日志功能 +- 优化了配置文件格式和结构 + +#### 部署支持扩展 +- 新增了Linux系统部署指南 +- 新增了Docker部署支持的详细文档 +- 新增了NixOS环境支持(使用venv方式) +- 新增了优雅的shutdown机制 +- 优化了Docker部署文档 + +### 🛠️ 开发体验提升 +#### 工具链升级 +- 新增了ruff代码格式化和检查工具 +- 新增了知识库一键启动脚本 +- 新增了自动保存脚本,定期保存聊天记录和关系数据 +- 新增了表情包自动获取脚本 +- 优化了日志记录(使用logger.debug替代print) +- 精简了日志输出,禁用了Uvicorn/NoneBot默认日志 + +#### 安全性强化 +- 新增了API密钥安全管理机制 +- 新增了数据库完整性检查功能 +- 新增了表情包文件完整性自动检查 +- 新增了异常处理和自动恢复机制 +- 优化了安全性检查机制 + +### 🐛 关键问题修复 +#### 系统稳定性 +- 修复了systemctl强制停止的问题 +- 修复了ENVIRONMENT变量在同一终端下不能被覆盖的问题 +- 修复了libc++.so依赖问题 +- 修复了数据库索引创建失败的问题 +- 修复了MongoDB连接配置相关问题 +- 修复了消息队列溢出问题 +- 修复了配置文件加载时的版本兼容性问题 + +#### 功能完善性 +- 修复了私聊时产生reply消息的bug +- 修复了回复消息无法识别的问题 +- 修复了CQ码解析错误 +- 修复了情绪管理器导入问题 +- 修复了小名无效的问题 +- 修复了表情包发送时的参数缺失问题 +- 修复了表情包重复注册问题 +- 修复了变量拼写错误问题 + +### 主要改进方向 +1. 提升记忆系统的智能性和可靠性 +2. 完善私聊功能的完整生态 +3. 优化系统架构和部署便利性 +4. 提升开发体验和代码质量 +5. 加强系统安全性和稳定性 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/docs/linux_deploy_guide_for_beginners.md b/docs/linux_deploy_guide_for_beginners.md new file mode 100644 index 000000000..04601923f --- /dev/null +++ b/docs/linux_deploy_guide_for_beginners.md @@ -0,0 +1,444 @@ +# 面向纯新手的Linux服务器麦麦部署指南 + +## 你得先有一个服务器 + +为了能使麦麦在你的电脑关机之后还能运行,你需要一台不间断开机的主机,也就是我们常说的服务器。 + +华为云、阿里云、腾讯云等等都是在国内可以选择的选择。 + +你可以去租一台最低配置的就足敷需要了,按月租大概十几块钱就能租到了。 + +我们假设你已经租好了一台Linux架构的云服务器。我用的是阿里云ubuntu24.04,其他的原理相似。 + +## 0.我们就从零开始吧 + +### 网络问题 + +为访问github相关界面,推荐去下一款加速器,新手可以试试watttoolkit。 + +### 安装包下载 + +#### MongoDB + +对于ubuntu24.04 x86来说是这个: + +https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/multiverse/binary-amd64/mongodb-org-server_8.0.5_amd64.deb + +如果不是就在这里自行选择对应版本 + +https://www.mongodb.com/try/download/community-kubernetes-operator + +#### Napcat + +在这里选择对应版本。 + +https://github.com/NapNeko/NapCatQQ/releases/tag/v4.6.7 + +对于ubuntu24.04 x86来说是这个: + +https://dldir1.qq.com/qqfile/qq/QQNT/ee4bd910/linuxqq_3.2.16-32793_amd64.deb + +#### 麦麦 + +https://github.com/SengokuCola/MaiMBot/archive/refs/tags/0.5.8-alpha.zip + +下载这个官方压缩包。 + +### 路径 + +我把麦麦相关文件放在了/moi/mai里面,你可以凭喜好更改,记得适当调整下面涉及到的部分即可。 + +文件结构: + +``` +moi +└─ mai + ├─ linuxqq_3.2.16-32793_amd64.deb + ├─ mongodb-org-server_8.0.5_amd64.deb + └─ bot + └─ MaiMBot-0.5.8-alpha.zip +``` + +### 网络 + +你可以在你的服务器控制台网页更改防火墙规则,允许6099,8080,27017这几个端口的出入。 + +## 1.正式开始! + +远程连接你的服务器,你会看到一个黑框框闪着白方格,这就是我们要进行设置的场所——终端了。以下的bash命令都是在这里输入。 + +## 2. Python的安装 + +- 导入 Python 的稳定版 PPA: + +```bash +sudo add-apt-repository ppa:deadsnakes/ppa +``` + +- 导入 PPA 后,更新 APT 缓存: + +```bash +sudo apt update +``` + +- 在「终端」中执行以下命令来安装 Python 3.12: + +```bash +sudo apt install python3.12 +``` + +- 验证安装是否成功: + +```bash +python3.12 --version +``` + +- 在「终端」中,执行以下命令安装 pip: + +```bash +sudo apt install python3-pip +``` + +- 检查Pip是否安装成功: + +```bash +pip --version +``` + +- 安装必要组件 + +``` bash +sudo apt install python-is-python3 +``` + +## 3.MongoDB的安装 + +``` bash +cd /moi/mai +``` + +``` bash +dpkg -i mongodb-org-server_8.0.5_amd64.deb +``` + +``` bash +mkdir -p /root/data/mongodb/{data,log} +``` + +## 4.MongoDB的运行 + +```bash +service mongod start +``` + +```bash +systemctl status mongod #通过这条指令检查运行状态 +``` + +有需要的话可以把这个服务注册成开机自启 + +```bash +sudo systemctl enable mongod +``` + +## 5.napcat的安装 + +``` bash +curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh +``` + +上面的不行试试下面的 + +``` bash +dpkg -i linuxqq_3.2.16-32793_amd64.deb +apt-get install -f +dpkg -i linuxqq_3.2.16-32793_amd64.deb +``` + +成功的标志是输入``` napcat ```出来炫酷的彩虹色界面 + +## 6.napcat的运行 + +此时你就可以根据提示在```napcat```里面登录你的QQ号了。 + +```bash +napcat start <你的QQ号> +napcat status #检查运行状态 +``` + +然后你就可以登录napcat的webui进行设置了: + +```http://<你服务器的公网IP>:6099/webui?token=napcat``` + +第一次是这个,后续改了密码之后token就会对应修改。你也可以使用```napcat log <你的QQ号>```来查看webui地址。把里面的```127.0.0.1```改成<你服务器的公网IP>即可。 + +登录上之后在网络配置界面添加websocket客户端,名称随便输一个,url改成`ws://127.0.0.1:8080/onebot/v11/ws`保存之后点启用,就大功告成了。 + +## 7.麦麦的安装 + +### step 1 安装解压软件 + +``` +sudo apt-get install unzip +``` + +### step 2 解压文件 + +```bash +cd /moi/mai/bot # 注意:要切换到压缩包的目录中去 +unzip MaiMBot-0.5.8-alpha.zip +``` + +### step 3 进入虚拟环境安装库 + +```bash +cd /moi/mai/bot +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### step 4 试运行 + +```bash +cd /moi/mai/bot +python -m venv venv +source venv/bin/activate +python bot.py +``` + +肯定运行不成功,不过你会发现结束之后多了一些文件 + +``` +bot +├─ .env.prod +└─ config + └─ bot_config.toml +``` + +你要会vim直接在终端里修改也行,不过也可以把它们下到本地改好再传上去: + +### step 5 文件配置 + +本项目需要配置两个主要文件: + +1. `.env.prod` - 配置API服务和系统环境 +2. `bot_config.toml` - 配置机器人行为和模型 + +#### API + +你可以注册一个硅基流动的账号,通过邀请码注册有14块钱的免费额度:https://cloud.siliconflow.cn/i/7Yld7cfg。 + +#### 在.env.prod中定义API凭证: + +``` +# API凭证配置 +SILICONFLOW_KEY=your_key # 硅基流动API密钥 +SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/ # 硅基流动API地址 + +DEEP_SEEK_KEY=your_key # DeepSeek API密钥 +DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 # DeepSeek API地址 + +CHAT_ANY_WHERE_KEY=your_key # ChatAnyWhere API密钥 +CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere API地址 +``` + +#### 在bot_config.toml中引用API凭证: + +``` +[model.llm_reasoning] +name = "Pro/deepseek-ai/DeepSeek-R1" +base_url = "SILICONFLOW_BASE_URL" # 引用.env.prod中定义的地址 +key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 +``` + +如需切换到其他API服务,只需修改引用: + +``` +[model.llm_reasoning] +name = "Pro/deepseek-ai/DeepSeek-R1" +base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务 +key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 +``` + +#### 配置文件详解 + +##### 环境配置文件 (.env.prod) + +``` +# API配置 +SILICONFLOW_KEY=your_key +SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/ +DEEP_SEEK_KEY=your_key +DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1 +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消息无法传入 +PORT=8080 + +# 数据库配置 +MONGODB_HOST=127.0.0.1 # 如果使用Docker部署,需要改成数据库容器的名字,默认是mongodb +MONGODB_PORT=27017 +DATABASE_NAME=MegBot +MONGODB_USERNAME = "" # 数据库用户名 +MONGODB_PASSWORD = "" # 数据库密码 +MONGODB_AUTH_SOURCE = "" # 认证数据库 + +# 插件配置 +PLUGINS=["src2.plugins.chat"] +``` + +##### 机器人配置文件 (bot_config.toml) + +``` +[bot] +qq = "机器人QQ号" # 必填 +nickname = "麦麦" # 机器人昵称(你希望机器人怎么称呼它自己) + +[personality] +prompt_personality = [ + "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", + "是一个女大学生,你有黑色头发,你会刷小红书" +] +prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" + +[message] +min_text_length = 2 # 最小回复长度 +max_context_size = 15 # 上下文记忆条数 +emoji_chance = 0.2 # 表情使用概率 +ban_words = [] # 禁用词列表 + +[emoji] +auto_save = true # 自动保存表情 +enable_check = false # 启用表情审核 +check_prompt = "符合公序良俗" + +[groups] +talk_allowed = [] # 允许对话的群号 +talk_frequency_down = [] # 降低回复频率的群号 +ban_user_id = [] # 禁止回复的用户QQ号 + +[others] +enable_advance_output = true # 启用详细日志 +enable_kuuki_read = true # 启用场景理解 + +# 模型配置 +[model.llm_reasoning] # 推理模型 +name = "Pro/deepseek-ai/DeepSeek-R1" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + +[model.llm_reasoning_minor] # 轻量推理模型 +name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + +[model.llm_normal] # 对话模型 +name = "Pro/deepseek-ai/DeepSeek-V3" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + +[model.llm_normal_minor] # 备用对话模型 +name = "deepseek-ai/DeepSeek-V2.5" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + +[model.vlm] # 图像识别模型 +name = "deepseek-ai/deepseek-vl2" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + +[model.embedding] # 文本向量模型 +name = "BAAI/bge-m3" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" + + +[topic.llm_topic] +name = "Pro/deepseek-ai/DeepSeek-V3" +base_url = "SILICONFLOW_BASE_URL" +key = "SILICONFLOW_KEY" +``` + +**step # 6** 运行 + +现在再运行 + +```bash +cd /moi/mai/bot +python -m venv venv +source venv/bin/activate +python bot.py +``` + +应该就能运行成功了。 + +## 8.事后配置 + +可是现在还有个问题:只要你一关闭终端,bot.py就会停止运行。那该怎么办呢?我们可以把bot.py注册成服务。 + +重启服务器,打开MongoDB和napcat服务。 + +新建一个文件,名为`bot.service`,内容如下 + +``` +[Unit] +Description=maimai bot + +[Service] +WorkingDirectory=/moi/mai/bot +ExecStart=/moi/mai/bot/venv/bin/python /moi/mai/bot/bot.py +Restart=on-failure +User=root + +[Install] +WantedBy=multi-user.target +``` + +里面的路径视自己的情况更改。 + +把它放到`/etc/systemd/system`里面。 + +重新加载 `systemd` 配置: + +```bash +sudo systemctl daemon-reload +``` + +启动服务: + +```bash +sudo systemctl start bot.service # 启动服务 +sudo systemctl restart bot.service # 或者重启服务 +``` + +检查服务状态: + +```bash +sudo systemctl status bot.service +``` + +现在再关闭终端,检查麦麦能不能正常回复QQ信息。如果可以的话就大功告成了! + +## 9.命令速查 + +```bash +service mongod start # 启动mongod服务 +napcat start <你的QQ号> # 登录napcat +cd /moi/mai/bot # 切换路径 +python -m venv venv # 创建虚拟环境 +source venv/bin/activate # 激活虚拟环境 + +sudo systemctl daemon-reload # 重新加载systemd配置 +sudo systemctl start bot.service # 启动bot服务 +sudo systemctl enable bot.service # 启动bot服务 + +sudo systemctl status bot.service # 检查bot服务状态 +``` + +``` +python bot.py +``` + diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index 4711c81a9..b19f3d6a7 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -110,6 +110,57 @@ python3 bot.py --- +### 7️⃣ **使用systemctl管理maimbot** + +使用以下命令添加服务文件: + +```bash +sudo nano /etc/systemd/system/maimbot.service +``` + +输入以下内容: + +``:你的maimbot目录 +``:你的venv环境(就是上文创建环境后,执行的代码`source maimbot/bin/activate`中source后面的路径的绝对路径) + +```ini +[Unit] +Description=MaiMbot 麦麦 +After=network.target mongod.service + +[Service] +Type=simple +WorkingDirectory= +ExecStart=/python3 bot.py +ExecStop=/bin/kill -2 $MAINPID +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生成知识库 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..404f7555c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,62 +1,38 @@ { 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 + pythonPackages.numpy + ]; - 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 - ]; + postVenvCreation = '' + unset SOURCE_DATE_EPOCH + pip install -r requirements.txt + ''; - shellHook = '' - ''; - }; - } - ); -} + postShellHook = '' + # allow pip to install wheels + unset SOURCE_DATE_EPOCH + ''; + }; + }); +} \ No newline at end of file diff --git a/hort --pretty=format-ad -s b/hort --pretty=format-ad -s new file mode 100644 index 000000000..faeacdd5f --- /dev/null +++ b/hort --pretty=format-ad -s @@ -0,0 +1,141 @@ +cbb569e - Create 如果你更新了版本,点我.txt +a91ef7b - 自动升级配置文件脚本 +ed18f2e - 新增了知识库一键启动漂亮脚本 +80ed568 - fix: 删除print调试代码 +c681a82 - 修复小名无效问题 +e54038f - fix: 从 nixpkgs 增加 numpy 依赖,以避免出现 libc++.so 找不到的问题 +26782c9 - fix: 修复 ENVIRONMENT 变量在同一终端下不能被覆盖的问题 +8c34637 - 提高健壮性 +2688a96 - close SengokuCola/MaiMBot#225 让麦麦可以正确读取分享卡片 +cd16e68 - 修复表情包发送时的缺失参数 +b362c35 - feat: 更新 flake.nix ,采用 venv 的方式生成环境,nixos用户也可以本机运行项目了 +3c8c897 - 屏蔽一个臃肿的debug信息 +9d0152a - 修复了合并过程中造成的代码重复 +956135c - 添加一些注释 +a412741 - 将print变为logger.debug +3180426 - 修复了没有改掉的typo字段 +aea3bff - 添加私聊过滤开关,更新config,增加约束 +cda6281 - chore: update emoji_manager.py +baed856 - 修正了私聊屏蔽词输出 +66a0f18 - 修复了私聊时产生reply消息的bug +3bf5cd6 - feat: 新增运行时重载配置文件;新增根据不同环境(dev;prod)显示不同级别的log +33cd83b - 添加私聊功能 +aa41f0d - fix: 放反了 +ef8691c - fix: 修改message继承逻辑,修复回复消息无法识别 +7d017be - fix:模型降级 +e1019ad - fix: 修复变量拼写错误并优化代码可读性 +c24bb70 - fix: 流式输出模式增加结束判断与token用量记录 +60a9376 - 添加logger的debug输出开关,默认为不开启 +bfa9a3c - fix: 添加群信息获取的错误处理 (#173) +4cc5c8e - 修正.env.prod和.env.dev的生成 +dea14c1 - fix: 模型降级目前只对硅基流动的V3和R1生效 +b6edbea - fix: 图片保存路径不正确 +01a6fa8 - fix: 删除神秘test +20f009d - 修复systemctl强制停止maimbot的问题 +af962c2 - 修复了情绪管理器没有正确导入导致发布出消息 +0586700 - 按照Sourcery提供的建议修改systemctl管理指南 +e48b32a - 在手动部署教程中增加使用systemctl管理 +5760412 - fix: 小修 +1c9b0cc - fix: 修复部分cq码解析错误,merge +b6867b9 - fix: 统一使用os.getenv获取数据库连接信息,避免从config对象获取不存在的值时出现KeyError +5e069f7 - 修复记忆保存时无时间信息的bug +73a3e41 - 修复记忆更新bug +52c93ba - refactor: use Base64 for emoji CQ codes +67f6d7c - fix: 保证能运行的小修改 +c32c4fb - refactor: 修改配置文件的版本号 +a54ca8c - Merge remote-tracking branch 'upstream/debug' into feat_regix +8cbf9bb - feat: 史上最好的消息流重构和图片管理 +9e41c4f - feat: 修改 bot_config 0.0.5 版本的变更日志 +eede406 - fix: 修复nonebot无法加载项目的问题 +00e02ed - fix: 0.0.5 版本的增加分层控制项 +0f99d6a - Update docs/docker_deploy.md +c789074 - feat: 增加ruff依赖 +ff65ab8 - feat: 修改默认的ruff配置文件,同时消除config的所有不符合规范的地方 +bf97013 - feat: 精简日志,禁用Uvicorn/NoneBot默认日志;启动方式改为显示加载uvicorn,以便优雅shutdown +d9a2863 - 优化Docker部署文档更新容器部分 +efcf00f - Docker部署文档追加更新部分 +a63ce96 - fix: 更新情感判断模型配置(使配置文件里的 llm_emotion_judge 生效) +1294c88 - feat: 增加标准化格式化设置 +2e8cd47 - fix: 避免可能出现的日程解析错误 +043a724 - 修一下文档跳转,小美化( +e4b8865 - 支持别名,可以用不同名称召唤机器人 +7b35ddd - ruff 哥又有新点子 +7899e67 - feat: 重构完成开始测试debug +354d6d0 - 记忆系统优化 +6cef8fd - 修复时区,删去napcat用不到的端口 +cd96644 - 添加使用说明 +84495f8 - fix +204744c - 修改配置名与修改过滤对象为raw_message +a03b490 - Update README.md +2b2b342 - feat: 增加 ruff 依赖 +72a6749 - fix: 修复docker部署时区指定问题 +ee579bc - Update README.md +1b611ec - resolve SengokuCola/MaiMBot#167 根据正则表达式过滤消息 +6e2ea82 - refractor: 几乎写完了,进入测试阶段 +2ffdfef - More +e680405 - fix: typo 'discription' +68b3f57 - Minor Doc Update +312f065 - Create linux_deploy_guide_for_beginners.md +ed505a4 - fix: 使用动态路径替换硬编码的项目路径 +8ff7bb6 - docs: 更新文档,修正格式并添加必要的换行符 +6e36a56 - feat: 增加 MONGODB_URI 的配置项,并将所有env文件的注释单独放在一行(python的dotenv有时无法正确处理行内注释) +4baa6c6 - feat: 实现MongoDB URI方式连接,并统一数据库连接代码。 +8a32d18 - feat: 优化willing_manager逻辑,增加回复保底概率 +c9f1244 - docs: 改进README.md文档格式和排版 +e1b484a - docs: 添加CLAUDE.md开发指南文件(用于Claude Code) +a43f949 - fix: remove duplicate message(CR comments) +fddb641 - fix: 修复错误的空值检测逻辑 +8b7876c - fix: 修复没有上传tag的问题 +6b4130e - feat: 增加stable-dev分支的打包 +052e67b - refactor: 日志打印优化(终于改完了,爽了 +a7f9d05 - 修复记忆整理传入格式问题 +536bb1d - fix: 更新情感判断模型配置 +8d99592 - fix: logger初始化顺序 +052802c - refactor: logger promotion +8661d94 - doc: README.md - telegram version information +5746afa - refactor: logger in src\plugins\chat\bot.py +288dbb6 - refactor: logger in src\plugins\chat\__init__.py +8428a06 - fix: memory logger optimization (CR comment) +665c459 - 改进了可视化脚本 +6c35704 - fix: 调用了错误的函数 +3223153 - feat: 一键脚本新增记忆可视化 +3149dd3 - fix: mongodb.zip 无法解压 fix:更换执行命令的方法 fix:当 db 不存在时自动创建 feat: 一键安装完成后启动麦麦 +089d6a6 - feat: 针对硅基流动的Pro模型添加了自动降级功能 +c4b0917 - 一个记忆可视化小脚本 +6a71ea4 - 修复了记忆时间bug,config添加了记忆屏蔽关键词 +1b5344f - fix: 优化bot初始化的日志&格式 +41aa974 - fix: 优化chat/config.py的日志&格式 +980cde7 - fix: 优化scheduler_generator日志&格式 +31a5514 - fix: 调整全局logger加载顺序 +8baef07 - feat: 添加全局logger初始化设置 +5566f17 - refractor: 几乎写完了,进入测试阶段 +6a66933 - feat: 添加开发环境.env.dev初始化 +411ff1a - feat: 安装 MongoDB Compass +0de9eba - feat: 增加实时更新贡献者列表的功能 +f327f45 - fix: 优化src/plugins/chat/__init__.py的import +826daa5 - fix: 当虚拟环境存在时跳过创建 +f54de42 - fix: time.tzset 仅在类 Unix 系统可用 +47c4990 - fix: 修复docker部署场景下时间错误的问题 +e23a371 - docs: 添加 compose 注释 +1002822 - docs: 标注 Python 最低版本 +564350d - feat: 校验 Python 版本 +4cc4482 - docs: 添加傻瓜式脚本 +757173a - 带麦麦看了心理医生,让她没那么容易陷入负面情绪 +39bb99c - 将错别字生成提取到配置,一句一个错别字太烦了! +fe36847 - feat: 超大型重构 +e304dd7 - Update README.md +b7cfe6d - feat: 发布第 0.0.2 版本配置模板 +ca929d5 - 补充Docker部署文档 +1e97120 - 补充Docker部署文档 +25f7052 - fix: 修复兼容性选项和目前第一个版本之间的版本间隙 0.0.0 版,并将所有的直接退出修改为抛出异常 +c5bdc4f - 防ipv6炸,虽然小概率事件 +d86610d - fix: 修复不能加载环境变量的问题 +2306ebf - feat: 因为判断临界版本范围比较麻烦,增加 notice 字段,删除原本的判断逻辑(存在故障) +dd09576 - fix: 修复 TypeError: BotConfig.convert_to_specifierset() takes 1 positional argument but 2 were given +18f839b - fix: 修复 missing 1 required positional argument: 'INNER_VERSION' +6adb5ed - 调整一些细节,docker部署时可选数据库账密 +07f48e9 - fix: 利用filter来过滤环境变量,避免直接删除key造成的 RuntimeError: dictionary changed size during iteration +5856074 - fix: 修复无法进行基础设置的问题 +32aa032 - feat: 发布 0.0.1 版本的配置文件 +edc07ac - feat: 重构配置加载器,增加配置文件版本控制和程序兼容能力 +0f492ed - fix: 修复 BASE_URL/KEY 组合检查中被 GPG_KEY 干扰的问题 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0acaade5e..8330c8d06 100644 Binary files a/requirements.txt and b/requirements.txt differ 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..c6cead225 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -1,7 +1,6 @@ from typing import Optional - from pymongo import MongoClient - +from pymongo.database import Database as MongoDatabase class Database: _instance: Optional["Database"] = None @@ -27,7 +26,7 @@ class Database: else: # 否则使用无认证连接 self.client = MongoClient(host, port) - self.db = self.client[db_name] + self.db: MongoDatabase = self.client[db_name] @classmethod def initialize( @@ -39,18 +38,18 @@ class Database: password: Optional[str] = None, auth_source: Optional[str] = None, uri: Optional[str] = None, - ) -> "Database": + ) -> MongoDatabase: if cls._instance is None: cls._instance = cls( host, port, db_name, username, password, auth_source, uri ) - return cls._instance + return cls._instance.db @classmethod - def get_instance(cls) -> "Database": + def get_instance(cls) -> MongoDatabase: if cls._instance is None: raise RuntimeError("Database not initialized") - return cls._instance + return cls._instance.db #测试用 diff --git a/src/gui/reasoning_gui.py b/src/gui/reasoning_gui.py index e131658b8..84b95adaf 100644 --- a/src/gui/reasoning_gui.py +++ b/src/gui/reasoning_gui.py @@ -46,7 +46,7 @@ class ReasoningGUI: # 初始化数据库连接 try: - self.db = Database.get_instance().db + self.db = Database.get_instance() logger.success("数据库连接成功") except RuntimeError: logger.warning("数据库未初始化,正在尝试初始化...") @@ -60,7 +60,7 @@ class ReasoningGUI: password=os.getenv("MONGODB_PASSWORD"), auth_source=os.getenv("MONGODB_AUTH_SOURCE"), ) - self.db = Database.get_instance().db + self.db = Database.get_instance() logger.success("数据库初始化成功") except Exception: logger.exception("数据库初始化失败") diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index ec3d4f01d..1c6bf3f35 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -4,7 +4,7 @@ import os from loguru import logger from nonebot import get_driver, on_message, require -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageSegment +from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageSegment,MessageEvent from nonebot.typing import T_State from ...common.database import Database @@ -32,26 +32,14 @@ _message_manager_started = False driver = get_driver() config = driver.config -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"), -) -logger.success("初始化数据库成功") - - # 初始化表情管理器 emoji_manager.initialize() logger.debug(f"正在唤醒{global_config.BOT_NICKNAME}......") # 创建机器人实例 chat_bot = ChatBot() -# 注册群消息处理器 -group_msg = on_message(priority=5) +# 注册消息处理器 +msg_in = on_message(priority=5) # 创建定时任务 scheduler = require("nonebot_plugin_apscheduler").scheduler @@ -103,8 +91,8 @@ async def _(bot: Bot): asyncio.create_task(chat_manager._auto_save_task()) -@group_msg.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): +@msg_in.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): await chat_bot.handle_message(event, bot) @@ -127,7 +115,7 @@ async def build_memory_task(): async def forget_memory_task(): """每30秒执行一次记忆构建""" print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") - await hippocampus.operation_forget_topic(percentage=0.1) + await hippocampus.operation_forget_topic(percentage=global_config.memory_forget_percentage) print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 9b2ac06f1..f335a2ba3 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -2,12 +2,16 @@ import re import time from random import random from loguru import logger -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent +from nonebot.adapters.onebot.v11 import ( + Bot, + GroupMessageEvent, + MessageEvent, + PrivateMessageEvent, +) 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 @@ -42,39 +46,53 @@ class ChatBot: if not self._started: self._started = True - async def handle_message(self, event: GroupMessageEvent, bot: Bot) -> None: - """处理收到的群消息""" + async def handle_message(self, event: MessageEvent, bot: Bot) -> None: + """处理收到的消息""" self.bot = bot # 更新 bot 实例 - 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侧完成 - # 消息过滤,涉及到config有待更新 - 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 - user_info = UserInfo( - user_id=event.user_id, - user_nickname=event.sender.nickname, - user_cardname=event.sender.card or None, - platform="qq", - ) + # 处理私聊消息 + if isinstance(event, PrivateMessageEvent): + if not global_config.enable_friend_chat: # 私聊过滤 + return + else: + try: + user_info = UserInfo( + user_id=event.user_id, + user_nickname=(await bot.get_stranger_info(user_id=event.user_id, no_cache=True))["nickname"], + user_cardname=None, + platform="qq", + ) + except Exception as e: + logger.error(f"获取陌生人信息失败: {e}") + return + logger.debug(user_info) - group_info = GroupInfo( - group_id=event.group_id, - group_name=group_name, # 使用获取到的群名称或None - platform="qq", - ) + # group_info = GroupInfo(group_id=0, group_name="私聊", platform="qq") + group_info = None + + # 处理群聊消息 + else: + # 白名单设定由nontbot侧完成 + if event.group_id: + if event.group_id not in global_config.talk_allowed_groups: + 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") + + # 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) message_cq = MessageRecvCQ( message_id=event.message_id, @@ -88,7 +106,6 @@ class ChatBot: # 进入maimbot message = MessageRecv(message_json) - groupinfo = message.message_info.group_info userinfo = message.message_info.user_info messageinfo = message.message_info @@ -108,7 +125,9 @@ class ChatBot: # 过滤词 for word in global_config.ban_words: if word in message.processed_plain_text: - logger.info(f"[群{groupinfo.group_id}]{userinfo.user_nickname}:{message.processed_plain_text}") + logger.info( + f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{userinfo.user_nickname}:{message.processed_plain_text}" + ) logger.info(f"[过滤词识别]消息中含有{word},filtered") return @@ -116,7 +135,7 @@ class ChatBot: for pattern in global_config.ban_msgs_regex: if re.search(pattern, message.raw_message): logger.info( - f"[群{message.message_info.group_info.group_id}]{message.user_nickname}:{message.raw_message}" + f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{message.user_nickname}:{message.raw_message}" ) logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return @@ -124,8 +143,8 @@ class ChatBot: 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 = "" - interested_rate = 0 interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") @@ -144,7 +163,7 @@ class ChatBot: current_willing = willing_manager.get_willing(chat_stream=chat) logger.info( - f"[{current_time}][群{chat.group_info.group_id}]{chat.user_info.user_nickname}:" + f"[{current_time}][{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{chat.user_info.user_nickname}:" f"{message.processed_plain_text}[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]" ) @@ -152,12 +171,17 @@ class ChatBot: if random() < reply_probability: bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, platform=messageinfo.platform + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform, ) 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 + message_id=think_id, + chat_stream=chat, + bot_user_info=bot_user_info, + reply=message, ) message_manager.add_message(thinking_message) @@ -196,15 +220,16 @@ class ChatBot: # print(f"\033[1;32m[回复内容]\033[0m {msg}") # 通过时间改变时间戳 typing_time = calculate_typing_time(msg) - print(f"typing_time: {typing_time}") + logger.debug(f"typing_time: {typing_time}") accu_typing_time += typing_time timepoint = thinking_time_point + accu_typing_time message_segment = Seg(type="text", data=msg) - print(f"message_segment: {message_segment}") + # logger.debug(f"message_segment: {message_segment}") bot_message = MessageSending( message_id=think_id, chat_stream=chat, bot_user_info=bot_user_info, + sender_info=userinfo, message_segment=message_segment, reply=message, is_head=not mark_head, @@ -218,7 +243,9 @@ class ChatBot: # message_set 可以直接加入 message_manager # print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") - print(f"添加message_set到message_manager") + + logger.debug("添加message_set到message_manager") + message_manager.add_message(message_set) bot_response_time = thinking_time_point @@ -242,6 +269,7 @@ class ChatBot: message_id=think_id, chat_stream=chat, bot_user_info=bot_user_info, + sender_info=userinfo, message_segment=message_segment, reply=message, is_head=False, diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index bee679173..3ccd03f81 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -111,11 +111,11 @@ class ChatManager: 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.list_collection_names(): + self.db.create_collection("chat_streams") # 创建索引 - self.db.db.chat_streams.create_index([("stream_id", 1)], unique=True) - self.db.db.chat_streams.create_index( + self.db.chat_streams.create_index([("stream_id", 1)], unique=True) + self.db.chat_streams.create_index( [("platform", 1), ("user_info.user_id", 1), ("group_info.group_id", 1)] ) @@ -168,7 +168,7 @@ class ChatManager: return stream # 检查数据库中是否存在 - data = self.db.db.chat_streams.find_one({"stream_id": stream_id}) + data = self.db.chat_streams.find_one({"stream_id": stream_id}) if data: stream = ChatStream.from_dict(data) # 更新用户信息和群组信息 @@ -204,7 +204,7 @@ class ChatManager: async def _save_stream(self, stream: ChatStream): """保存聊天流到数据库""" if not stream.saved: - self.db.db.chat_streams.update_one( + self.db.chat_streams.update_one( {"stream_id": stream.stream_id}, {"$set": stream.to_dict()}, upsert=True ) stream.saved = True @@ -216,7 +216,7 @@ class ChatManager: async def load_all_streams(self): """从数据库加载所有聊天流""" - all_streams = self.db.db.chat_streams.find({}) + all_streams = self.db.chat_streams.find({}) for data in all_streams: stream = ChatStream.from_dict(data) self.streams[stream.stream_id] = stream diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 596d120f9..88cb31ed5 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -37,8 +37,7 @@ class BotConfig: 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 # 偷表情包 @@ -69,6 +68,7 @@ class BotConfig: enable_advance_output: bool = False # 是否启用高级输出 enable_kuuki_read: bool = True # 是否启用读空气功能 enable_debug_output: bool = False # 是否启用调试输出 + enable_friend_chat: bool = False # 是否启用好友聊天 mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 mood_decay_rate: float = 0.95 # 情绪衰减率 @@ -94,7 +94,13 @@ class BotConfig: PERSONALITY_1: float = 0.6 # 第一种人格概率 PERSONALITY_2: float = 0.3 # 第二种人格概率 PERSONALITY_3: float = 0.1 # 第三种人格概率 - + + build_memory_interval: int = 600 # 记忆构建间隔(秒) + + forget_memory_interval: int = 600 # 记忆遗忘间隔(秒) + memory_forget_time: int = 24 # 记忆遗忘时间(小时) + memory_forget_percentage: float = 0.01 # 记忆遗忘比例 + memory_compress_rate: float = 0.1 # 记忆压缩率 memory_ban_words: list = field( default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] ) # 添加新的配置项默认值 @@ -293,6 +299,11 @@ class BotConfig: # 在版本 >= 0.0.4 时才处理新增的配置项 if config.INNER_VERSION in SpecifierSet(">=0.0.4"): config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) + + if config.INNER_VERSION in SpecifierSet(">=0.0.7"): + config.memory_forget_time = memory_config.get("memory_forget_time", config.memory_forget_time) + config.memory_forget_percentage = memory_config.get("memory_forget_percentage", config.memory_forget_percentage) + config.memory_compress_rate = memory_config.get("memory_compress_rate", config.memory_compress_rate) def mood(parent: dict): mood_config = parent["mood"] @@ -327,7 +338,9 @@ 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) + if config.INNER_VERSION in SpecifierSet(">=0.0.7"): + config.enable_debug_output = others_config.get("enable_debug_output", config.enable_debug_output) + config.enable_friend_chat = others_config.get("enable_friend_chat", config.enable_friend_chat) # 版本表达式:>=1.0.0,<2.0.0 # 允许字段:func: method, support: str, notice: str, necessary: bool diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 3adb952d3..f15251077 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -76,16 +76,16 @@ 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([('filename', 1)], unique=True) + if 'emoji' not in self.db.list_collection_names(): + self.db.create_collection('emoji') + self.db.emoji.create_index([('embedding', '2dsphere')]) + self.db.emoji.create_index([('filename', 1)], unique=True) def record_usage(self, emoji_id: str): """记录表情使用次数""" try: self._ensure_db() - self.db.db.emoji.update_one( + self.db.emoji.update_one( {'_id': emoji_id}, {'$inc': {'usage_count': 1}} ) @@ -119,7 +119,7 @@ class EmojiManager: try: # 获取所有表情包 - all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1})) + all_emojis = list(self.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1})) if not all_emojis: logger.warning("数据库中没有任何表情包") @@ -157,10 +157,11 @@ class EmojiManager: if selected_emoji and 'path' in selected_emoji: # 更新使用次数 - self.db.db.emoji.update_one( + self.db.emoji.update_one( {'_id': selected_emoji['_id']}, {'$inc': {'usage_count': 1}} ) + logger.success( f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})") # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 @@ -176,8 +177,10 @@ class EmojiManager: logger.error(f"获取表情包失败: {str(e)}") return None + async def _get_emoji_discription(self, image_base64: str) -> str: """获取表情包的标签,使用image_manager的描述生成功能""" + try: # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀 description = await image_manager.get_emoji_description(image_base64) @@ -236,7 +239,7 @@ class EmojiManager: image_hash = hashlib.md5(image_bytes).hexdigest() # 检查是否已经注册过 - existing_emoji = self.db.db['emoji'].find_one({'filename': filename}) + existing_emoji = self.db['emoji'].find_one({'filename': filename}) description = None if existing_emoji: @@ -272,11 +275,14 @@ class EmojiManager: # 获取表情包的描述 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"描述: {description}") + logger.info(f"描述: {description}") logger.info(f"其不满足过滤规则,被剔除 {check}") continue @@ -287,6 +293,7 @@ class EmojiManager: if description is not None: embedding = await get_embedding(description) + # 准备数据库记录 emoji_record = { 'filename': filename, @@ -298,9 +305,10 @@ class EmojiManager: } # 保存到emoji数据库 - self.db.db['emoji'].insert_one(emoji_record) + self.db['emoji'].insert_one(emoji_record) logger.success(f"注册新表情包: {filename}") logger.info(f"描述: {description}") + # 保存到images数据库 image_doc = { @@ -338,7 +346,7 @@ class EmojiManager: try: self._ensure_db() # 获取所有表情包记录 - all_emojis = list(self.db.db.emoji.find()) + all_emojis = list(self.db.emoji.find()) removed_count = 0 total_count = len(all_emojis) @@ -346,13 +354,13 @@ class EmojiManager: try: if 'path' not in emoji: logger.warning(f"发现无效记录(缺少path字段),ID: {emoji.get('_id', 'unknown')}") - self.db.db.emoji.delete_one({'_id': emoji['_id']}) + self.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']}) + self.db.emoji.delete_one({'_id': emoji['_id']}) removed_count += 1 continue @@ -360,7 +368,7 @@ class EmojiManager: if not os.path.exists(emoji['path']): logger.warning(f"表情包文件已被删除: {emoji['path']}") # 从数据库中删除记录 - result = self.db.db.emoji.delete_one({'_id': emoji['_id']}) + result = self.db.emoji.delete_one({'_id': emoji['_id']}) if result.deleted_count > 0: logger.debug(f"成功删除数据库记录: {emoji['_id']}") removed_count += 1 @@ -371,7 +379,7 @@ class EmojiManager: continue # 验证清理结果 - remaining_count = self.db.db.emoji.count_documents({}) + remaining_count = self.db.emoji.count_documents({}) if removed_count > 0: logger.success(f"已清理 {removed_count} 个失效的表情包记录") logger.info(f"清理前总数: {total_count} | 清理后总数: {remaining_count}") @@ -389,5 +397,7 @@ class EmojiManager: # 创建全局单例 + emoji_manager = EmojiManager() + diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index af7334afe..84e1937b0 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 @@ -154,7 +154,7 @@ class ResponseGenerator: reasoning_content: str, ): """保存对话记录到数据库""" - self.db.db.reasoning_logs.insert_one( + self.db.reasoning_logs.insert_one( { "time": time.time(), "chat_id": message.chat_stream.stream_id, diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index c777e7273..626e7cf4e 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,19 +1,25 @@ import time +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 + # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -#这个类是消息数据类,用于存储和管理消息数据。 -#它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 -#它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 +# 这个类是消息数据类,用于存储和管理消息数据。 +# 它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 +# 它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 + @dataclass class Message(MessageBase): @@ -61,14 +67,28 @@ class Message(MessageBase): @dataclass class MessageRecv(Message): """接收消息类,用于处理从MessageCQ序列化的消息""" - + def __init__(self, message_dict: Dict): """从MessageCQ的字典初始化 - + Args: 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')) + 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', {})) self.raw_message = message_dict.get('raw_message') @@ -83,68 +103,74 @@ class MessageRecv(Message): async def process(self) -> None: """处理消息内容,生成纯文本和详细文本 - + 这个方法必须在创建实例后显式调用,因为它包含异步操作。 """ - self.processed_plain_text = await self._process_message_segments(self.message_segment) + 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': + 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) + 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': + if seg.type == "text": return seg.data - elif seg.type == 'image': + elif seg.type == "image": # 如果是base64图片数据 if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) - return '[图片]' - elif seg.type == 'emoji': - self.is_emoji=True + return "[图片]" + elif seg.type == "emoji": + self.is_emoji = True if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) - return '[表情]' + return "[表情]" else: return f"[{seg.type}:{str(seg.data)}]" except Exception as e: - logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") + 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)) + 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!='' + 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" @@ -153,14 +179,14 @@ class MessageRecv(Message): @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 + reply: Optional["MessageRecv"] = None, ): # 调用父类初始化 super().__init__( @@ -169,7 +195,7 @@ class MessageProcessBase(Message): chat_stream=chat_stream, user_info=bot_user_info, message_segment=message_segment, - reply=reply + reply=reply, ) # 处理状态相关属性 @@ -183,78 +209,83 @@ class MessageProcessBase(Message): async def _process_message_segments(self, segment: Seg) -> str: """递归处理消息段,转换为文字描述 - + Args: segment: 要处理的消息段 - + Returns: str: 处理后的文本 """ - if segment.type == 'seglist': + 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) + 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': + if seg.type == "text": return seg.data - elif seg.type == 'image': + elif seg.type == "image": # 如果是base64图片数据 if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) - return '[图片]' - elif seg.type == 'emoji': + return "[图片]" + elif seg.type == "emoji": if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) - return '[表情]' - elif seg.type == 'at': + return "[表情]" + elif seg.type == "at": return f"[@{seg.data}]" - elif seg.type == 'reply': - if self.reply and hasattr(self.reply, 'processed_plain_text'): + 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}") + 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)) + 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 != '' + 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, chat_stream: ChatStream, bot_user_info: UserInfo, - reply: Optional['MessageRecv'] = None + reply: Optional["MessageRecv"] = None, ): # 调用父类初始化 super().__init__( @@ -262,25 +293,27 @@ class MessageThinking(MessageProcessBase): chat_stream=chat_stream, bot_user_info=bot_user_info, message_segment=None, # 思考状态不需要消息段 - reply=reply + reply=reply, ) - + # 思考状态特有属性 self.interrupt = False + @dataclass class MessageSending(MessageProcessBase): """发送状态的消息类""" - + def __init__( self, message_id: str, chat_stream: ChatStream, bot_user_info: UserInfo, + sender_info: UserInfo, # 用来记录发送者信息,用于私聊回复 message_segment: Seg, - reply: Optional['MessageRecv'] = None, + reply: Optional["MessageRecv"] = None, is_head: bool = False, - is_emoji: bool = False + is_emoji: bool = False, ): # 调用父类初始化 super().__init__( @@ -288,28 +321,34 @@ class MessageSending(MessageProcessBase): chat_stream=chat_stream, bot_user_info=bot_user_info, message_segment=message_segment, - reply=reply + reply=reply, ) - + # 发送状态特有属性 + self.sender_info = sender_info self.reply_to_message_id = reply.message_info.message_id if reply else None self.is_head = is_head self.is_emoji = is_emoji - - def set_reply(self, reply: Optional['MessageRecv']) -> None: + + 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 - ]) + self.message_segment = Seg( + type="seglist", + data=[ + Seg(type="reply", data=reply.message_info.message_id), + self.message_segment, + ], + ) async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" if self.message_segment: - self.processed_plain_text = await self._process_message_segments(self.message_segment) + self.processed_plain_text = await self._process_message_segments( + self.message_segment + ) self.detailed_plain_text = self._generate_detailed_text() @classmethod @@ -318,8 +357,8 @@ class MessageSending(MessageProcessBase): thinking: MessageThinking, message_segment: Seg, is_head: bool = False, - is_emoji: bool = False - ) -> 'MessageSending': + is_emoji: bool = False, + ) -> "MessageSending": """从思考状态消息创建发送状态消息""" return cls( message_id=thinking.message_info.message_id, @@ -328,41 +367,50 @@ class MessageSending(MessageProcessBase): bot_user_info=thinking.message_info.user_info, reply=thinking.reply, is_head=is_head, - is_emoji=is_emoji + is_emoji=is_emoji, ) - + def to_dict(self): - ret= super().to_dict() - ret['message_info']['user_info']=self.chat_stream.user_info.to_dict() + ret = super().to_dict() + ret["message_info"]["user_info"] = self.chat_stream.user_info.to_dict() return ret + def is_private_message(self) -> bool: + """判断是否为私聊消息""" + return ( + self.message_info.group_info is None + or self.message_info.group_info.group_id is None + ) + + @dataclass class MessageSet: """消息集合类,可以存储多个发送消息""" + 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) - + 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.message_info.time) - + 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[MessageSending]: """获取最接近指定时间的消息""" if not self.messages: return None - + left, right = 0, len(self.messages) - 1 while left < right: mid = (left + right) // 2 @@ -370,25 +418,22 @@ class MessageSet: left = mid + 1 else: right = mid - + return self.messages[left] - + def clear_messages(self) -> None: """清空所有消息""" self.messages.clear() - + def remove_message(self, message: MessageSending) -> bool: """移除指定消息""" if message in self.messages: self.messages.remove(message) return True return False - + def __str__(self) -> str: return f"MessageSet(id={self.message_id}, count={len(self.messages)})" - + def __len__(self) -> int: return len(self.messages) - - - diff --git a/src/plugins/chat/message_base.py b/src/plugins/chat/message_base.py index 334859e5b..80b8b6618 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 aa82d8822..4c46d3bf2 100644 --- a/src/plugins/chat/message_cq.py +++ b/src/plugins/chat/message_cq.py @@ -1,55 +1,47 @@ 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) -#这个类是消息数据类,用于存储和管理消息数据。 -#它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。 -#它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。 +# 这个类是消息数据类,用于存储和管理消息数据。 +# 它定义了消息的属性,包括群组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_info: UserInfo, - group_info: Optional[GroupInfo] = None, - platform: str = "qq" + self, message_id: int, user_info: UserInfo, group_info: Optional[GroupInfo] = None, platform: str = "qq" ): # 构造基础消息信息 message_info = BaseMessageInfo( - platform=platform, - message_id=message_id, - time=int(time.time()), - group_info=group_info, - user_info=user_info + 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 - ) + 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, @@ -62,9 +54,13 @@ class MessageRecvCQ(MessageCQ): # 调用父类初始化 super().__init__(message_id, user_info, group_info, platform) - if group_info and group_info.group_name is None: + # 私聊消息不携带group_info + if group_info is None: + pass + + elif 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) self.raw_message = raw_message @@ -73,10 +69,10 @@ class MessageRecvCQ(MessageCQ): """解析消息内容为Seg对象""" cq_code_dict_list = [] segments = [] - + start = 0 while True: - cq_start = message.find('[CQ:', start) + cq_start = message.find("[CQ:", start) if cq_start == -1: if start < len(message): text = message[start:].strip() @@ -89,81 +85,80 @@ class MessageRecvCQ(MessageCQ): if text: cq_code_dict_list.append(parse_cq_code(text)) - cq_end = message.find(']', cq_start) + 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 = 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,msg=self,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) # 如果只有一个segment,直接返回 if len(segments) == 1: return segments[0] - + # 否则返回seglist类型的Seg - return Seg(type='seglist', data=segments) + 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, - data: Dict - ): + + def __init__(self, data: Dict): # 调用父类初始化 - message_info = BaseMessageInfo.from_dict(data.get('message_info', {})) - message_segment = Seg.from_dict(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, - message_info.group_info if message_info.group_info else None, - message_info.platform - ) - + message_info.message_id, + 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() - def _generate_raw_message(self, ) -> str: + def _generate_raw_message( + self, + ) -> str: """将Seg对象转换为raw_message""" segments = [] # 处理消息段 - if self.message_segment.type == 'seglist': + 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) + return "".join(segments) def _seg_to_cq_code(self, seg: Seg) -> str: """将单个Seg对象转换为CQ码字符串""" - if seg.type == 'text': + if seg.type == "text": return str(seg.data) - elif seg.type == 'image': + elif seg.type == "image": return cq_code_tool.create_image_cq_base64(seg.data) - elif seg.type == 'emoji': + elif seg.type == "emoji": return cq_code_tool.create_emoji_cq_base64(seg.data) - elif seg.type == 'at': + elif seg.type == "at": return f"[CQ:at,qq={seg.data}]" - elif seg.type == 'reply': + 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 9db74633f..5b580f244 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -5,12 +5,12 @@ 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, MessageRecv, MessageSet + from .storage import MessageStorage from .config import global_config -from .chat_stream import chat_manager +from .utils import truncate_message class Message_Sender: @@ -26,49 +26,54 @@ class Message_Sender: self._current_bot = bot async def send_message( - self, - message: MessageSending, + self, + message: MessageSending, ) -> None: """发送消息""" + if isinstance(message, MessageSending): message_json = message.to_dict() - message_send=MessageSendCQ( - data=message_json - ) - - if message_send.message_info.group_info: + message_send = MessageSendCQ(data=message_json) + # logger.debug(message_send.message_info,message_send.raw_message) + message_preview = truncate_message(message.processed_plain_text) + if ( + message_send.message_info.group_info + and message_send.message_info.group_info.group_id + ): 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 + auto_escape=False, ) - logger.success(f"[调试] 发送消息{message.processed_plain_text}成功") + logger.success(f"[调试] 发送消息“{message_preview}”成功") except Exception as e: logger.error(f"[调试] 发生错误 {e}") - logger.error(f"[调试] 发送消息{message.processed_plain_text}失败") + logger.error(f"[调试] 发送消息“{message_preview}”失败") else: try: + logger.debug(message.message_info.user_info) await self._current_bot.send_private_msg( - user_id=message.message_info.user_info.user_id, + user_id=message.sender_info.user_id, message=message_send.raw_message, - auto_escape=False + auto_escape=False, ) - logger.success(f"[调试] 发送消息{message.processed_plain_text}成功") + logger.success(f"[调试] 发送消息“{message_preview}”成功") except Exception as e: - logger.error(f"发生错误 {e}") - logger.error(f"[调试] 发送消息{message.processed_plain_text}失败") + logger.error(f"[调试] 发生错误 {e}") + logger.error(f"[调试] 发送消息“{message_preview}”失败") class MessageContainer: """单个聊天流的发送/思考消息容器""" + 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[MessageSending]: """获取所有超时的Message_Sending对象(思考时间超过30秒),按thinking_start_time排序""" current_time = time.time() @@ -83,12 +88,12 @@ class MessageContainer: timeout_messages.sort(key=lambda x: x.thinking_start_time) return timeout_messages - + def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: """获取thinking_start_time最早的消息对象""" if not self.messages: return None - earliest_time = float('inf') + earliest_time = float("inf") earliest_message = None for msg in self.messages: msg_time = msg.thinking_start_time @@ -96,7 +101,7 @@ class MessageContainer: earliest_time = msg_time earliest_message = msg return earliest_message - + def add_message(self, message: Union[MessageThinking, MessageSending]) -> None: """添加消息到队列""" if isinstance(message, MessageSet): @@ -104,7 +109,7 @@ class MessageContainer: self.messages.append(single_message) else: self.messages.append(message) - + def remove_message(self, message: Union[MessageThinking, MessageSending]) -> bool: """移除消息,如果消息存在则返回True,否则返回False""" try: @@ -119,7 +124,7 @@ class MessageContainer: def has_messages(self) -> bool: """检查是否有待发送的消息""" return bool(self.messages) - + def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]: """获取所有消息""" return list(self.messages) @@ -127,72 +132,91 @@ class MessageContainer: class MessageManager: """管理所有聊天流的消息容器""" + def __init__(self): self.containers: Dict[str, MessageContainer] = {} # chat_id -> MessageContainer self.storage = MessageStorage() self._running = True - + 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[MessageThinking, MessageSending, MessageSet]) -> None: + + def add_message( + self, message: Union[MessageThinking, MessageSending, MessageSet] + ) -> None: chat_stream = message.chat_stream if not chat_stream: raise ValueError("无法找到对应的聊天流") container = self.get_container(chat_stream.stream_id) container.add_message(message) - + async def process_chat_messages(self, chat_id: str): """处理聊天流消息""" 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): message_earliest.update_thinking_time() thinking_time = message_earliest.thinking_time - print(f"消息正在思考中,已思考{int(thinking_time)}秒\r", end='', flush=True) + print( + f"消息正在思考中,已思考{int(thinking_time)}秒\r", + end="", + flush=True, + ) # 检查是否超时 if thinking_time > global_config.thinking_timeout: logger.warning(f"消息思考超时({thinking_time}秒),移除该消息") container.remove_message(message_earliest) else: - - if message_earliest.is_head and message_earliest.update_thinking_time() > 30: + + if ( + message_earliest.is_head + and message_earliest.update_thinking_time() > 30 + and not message_earliest.is_private_message() # 避免在私聊时插入reply + ): await message_sender.send_message(message_earliest.set_reply()) else: await message_sender.send_message(message_earliest) 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) - + + print( + f"\033[1;34m[调试]\033[0m 消息“{truncate_message(message_earliest.processed_plain_text)}”正在发送中" + ) + + await self.storage.store_message( + message_earliest, message_earliest.chat_stream, None + ) + container.remove_message(message_earliest) - + message_timeout = container.get_timeout_messages() if 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: + if ( + msg.is_head + and msg.update_thinking_time() > 30 + and not message_earliest.is_private_message() # 避免在私聊时插入reply + ): await message_sender.send_message(msg.set_reply()) else: await message_sender.send_message(msg) - + # if msg.is_emoji: # msg.processed_plain_text = "[表情包]" await msg.process() - await self.storage.store_message(msg,msg.chat_stream, None) - + await self.storage.store_message(msg, msg.chat_stream, None) + if not container.remove_message(msg): logger.warning("尝试删除不存在的消息") except Exception: @@ -206,7 +230,7 @@ class MessageManager: tasks = [] 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/prompt_builder.py b/src/plugins/chat/prompt_builder.py index fec6c7926..c89bf3e07 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: @@ -311,7 +311,7 @@ class PromptBuilder: {"$project": {"content": 1, "similarity": 1}} ] - results = list(self.db.db.knowledges.aggregate(pipeline)) + results = list(self.db.knowledges.aggregate(pipeline)) # print(f"\033[1;34m[调试]\033[0m获取知识库内容结果: {results}") if not results: diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 9e7cafda0..fbd8cec59 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 @@ -169,7 +168,7 @@ class RelationshipManager: async def load_all_relationships(self): """加载所有关系对象""" db = Database.get_instance() - all_relationships = db.db.relationships.find({}) + all_relationships = db.relationships.find({}) for data in all_relationships: await self.load_relationship(data) @@ -177,7 +176,7 @@ class RelationshipManager: """每5分钟自动保存一次关系数据""" db = Database.get_instance() # 获取所有关系记录 - all_relationships = db.db.relationships.find({}) + all_relationships = db.relationships.find({}) # 依次加载每条记录 for data in all_relationships: await self.load_relationship(data) @@ -207,7 +206,7 @@ class RelationshipManager: saved = relationship.saved db = Database.get_instance() - db.db.relationships.update_one( + db.relationships.update_one( {'user_id': user_id, 'platform': platform}, {'$set': { 'platform': platform, diff --git a/src/plugins/chat/storage.py b/src/plugins/chat/storage.py index f403b2c8b..ec155bbe9 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 @@ -25,7 +23,7 @@ class MessageStorage: "detailed_plain_text": message.detailed_plain_text, "topic": topic, } - self.db.db.messages.insert_one(message_data) + self.db.messages.insert_one(message_data) except Exception: logger.exception("存储消息失败") diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 55fb9eb43..cf3e59f73 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 @@ -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 @@ -402,3 +406,10 @@ def find_similar_topics_simple(text: str, topics: list, top_k: int = 5) -> list: # 按相似度降序排序并返回前k个 return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_k] + + +def truncate_message(message: str, max_length=20) -> str: + """截断消息,使其不超过指定长度""" + if len(message) > max_length: + return message[:max_length] + "..." + return message diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 25f23359b..8f09a21a0 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 @@ -44,20 +40,20 @@ class ImageManager: def _ensure_image_collection(self): """确保images集合存在并创建索引""" - if 'images' not in self.db.db.list_collection_names(): - self.db.db.create_collection('images') + if 'images' not in self.db.list_collection_names(): + self.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)]) + self.db.images.create_index([('hash', 1)], unique=True) + self.db.images.create_index([('url', 1)]) + self.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') + if 'image_descriptions' not in self.db.list_collection_names(): + self.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)]) + self.db.image_descriptions.create_index([('hash', 1)], unique=True) + self.db.image_descriptions.create_index([('type', 1)]) def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]: """从数据库获取图片描述 @@ -69,7 +65,7 @@ class ImageManager: Returns: Optional[str]: 描述文本,如果不存在则返回None """ - result= self.db.db.image_descriptions.find_one({ + result= self.db.image_descriptions.find_one({ 'hash': image_hash, 'type': description_type }) @@ -83,7 +79,7 @@ class ImageManager: description: 描述文本 description_type: 描述类型 ('emoji' 或 'image') """ - self.db.db.image_descriptions.update_one( + self.db.image_descriptions.update_one( {'hash': image_hash, 'type': description_type}, { '$set': { @@ -125,7 +121,7 @@ class ImageManager: image_hash = hashlib.md5(image_bytes).hexdigest() # 查重 - existing = self.db.db.images.find_one({'hash': image_hash}) + existing = self.db.images.find_one({'hash': image_hash}) if existing: return existing['path'] @@ -146,7 +142,7 @@ class ImageManager: 'description': description, 'timestamp': timestamp } - self.db.db.images.insert_one(image_doc) + self.db.images.insert_one(image_doc) return file_path @@ -163,7 +159,7 @@ class ImageManager: """ try: # 先查找是否已存在 - existing = self.db.db.images.find_one({'url': url}) + existing = self.db.images.find_one({'url': url}) if existing: return existing['path'] @@ -207,7 +203,7 @@ class ImageManager: Returns: bool: 是否存在 """ - return self.db.db.images.find_one({'url': url}) is not None + return self.db.images.find_one({'url': url}) is not None def check_hash_exists(self, image_data: Union[str, bytes], is_base64: bool = False) -> bool: """检查图像是否已存在 @@ -230,7 +226,7 @@ class ImageManager: return False image_hash = hashlib.md5(image_bytes).hexdigest() - return self.db.db.images.find_one({'hash': image_hash}) is not None + return self.db.images.find_one({'hash': image_hash}) is not None except Exception as e: logger.error(f"检查哈希失败: {str(e)}") @@ -273,7 +269,7 @@ class ImageManager: 'description': description, 'timestamp': timestamp } - self.db.db.images.update_one( + self.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True @@ -330,7 +326,7 @@ class ImageManager: 'description': description, 'timestamp': timestamp } - self.db.db.images.update_one( + self.db.images.update_one( {'hash': image_hash}, {'$set': image_doc}, upsert=True 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/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 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/draw_memory.py b/src/plugins/memory_system/draw_memory.py index 9f15164f1..d6ba8f3b2 100644 --- a/src/plugins/memory_system/draw_memory.py +++ b/src/plugins/memory_system/draw_memory.py @@ -96,7 +96,7 @@ class Memory_graph: dot_data = { "concept": node } - self.db.db.store_memory_dots.insert_one(dot_data) + self.db.store_memory_dots.insert_one(dot_data) @property def dots(self): @@ -106,7 +106,7 @@ class Memory_graph: 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)]) # 调试输出 + closest_record = self.db.messages.find_one({"time": {"$lte": timestamp}}, sort=[('time', -1)]) # 调试输出 logger.info( f"距离time最近的消息时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(closest_record['time'])))}") @@ -115,7 +115,7 @@ class Memory_graph: 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( + self.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']))) @@ -130,34 +130,34 @@ class Memory_graph: def save_graph_to_db(self): # 清空现有的图数据 - self.db.db.graph_data.delete_many({}) + self.db.graph_data.delete_many({}) # 保存节点 for node in self.G.nodes(data=True): node_data = { 'concept': node[0], 'memory_items': node[1].get('memory_items', []) # 默认为空列表 } - self.db.db.graph_data.nodes.insert_one(node_data) + self.db.graph_data.nodes.insert_one(node_data) # 保存边 for edge in self.G.edges(): edge_data = { 'source': edge[0], 'target': edge[1] } - self.db.db.graph_data.edges.insert_one(edge_data) + self.db.graph_data.edges.insert_one(edge_data) def load_graph_from_db(self): # 清空当前图 self.G.clear() # 加载节点 - nodes = self.db.db.graph_data.nodes.find() + nodes = self.db.graph_data.nodes.find() for node in nodes: memory_items = node.get('memory_items', []) if not isinstance(memory_items, list): memory_items = [memory_items] if memory_items else [] self.G.add_node(node['concept'], memory_items=memory_items) # 加载边 - edges = self.db.db.graph_data.edges.find() + edges = self.db.graph_data.edges.find() for edge in edges: self.G.add_edge(edge['source'], edge['target']) diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index c0b551b58..48fc19261 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -303,7 +303,7 @@ class Hippocampus: return topic_num async def operation_build_memory(self, chat_size=20): - time_frequency = {'near': 3, 'mid': 8, 'far': 5} + time_frequency = {'near': 1, 'mid': 4, 'far': 4} memory_samples = self.get_memory_sample(chat_size, time_frequency) for i, messages in enumerate(memory_samples, 1): @@ -315,7 +315,7 @@ class Hippocampus: bar = '█' * filled_length + '-' * (bar_length - filled_length) logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - compress_rate = 0.1 + compress_rate = global_config.memory_compress_rate compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) logger.info(f"压缩后记忆数量: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}") @@ -523,9 +523,14 @@ class Hippocampus: async def operation_forget_topic(self, percentage=0.1): """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘""" + # 检查数据库是否为空 all_nodes = list(self.memory_graph.G.nodes()) all_edges = list(self.memory_graph.G.edges()) + if not all_nodes and not all_edges: + logger.info("记忆图为空,无需进行遗忘操作") + return + check_nodes_count = max(1, int(len(all_nodes) * percentage)) check_edges_count = max(1, int(len(all_edges) * percentage)) @@ -546,7 +551,7 @@ class Hippocampus: # 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 + if current_time - last_modified > 3600*global_config.memory_forget_time: # test current_strength = edge_data.get('strength', 1) new_strength = current_strength - 1 @@ -887,15 +892,6 @@ config = driver.config start_time = time.time() -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"), -) # 创建记忆图 memory_graph = Memory_graph() # 创建海马体 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/models/utils_model.py b/src/plugins/models/utils_model.py index 3424d662c..75b46f611 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -41,10 +41,10 @@ class LLM_request: """初始化数据库集合""" try: # 创建llm_usage集合的索引 - self.db.db.llm_usage.create_index([("timestamp", 1)]) - 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)]) + self.db.llm_usage.create_index([("timestamp", 1)]) + self.db.llm_usage.create_index([("model_name", 1)]) + self.db.llm_usage.create_index([("user_id", 1)]) + self.db.llm_usage.create_index([("request_type", 1)]) except Exception: logger.error("创建数据库索引失败") @@ -73,7 +73,7 @@ class LLM_request: "status": "success", "timestamp": datetime.now() } - self.db.db.llm_usage.insert_one(usage_data) + self.db.llm_usage.insert_one(usage_data) logger.info( f"Token使用情况 - 模型: {self.model_name}, " f"用户: {user_id}, 类型: {request_type}, " diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 12c6ce3b5..bde593890 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -14,16 +14,6 @@ from ..models.utils_model import LLM_request driver = get_driver() config = driver.config -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 ScheduleGenerator: def __init__(self): # 根据global_config.llm_normal这一字典配置指定模型 @@ -56,7 +46,7 @@ class ScheduleGenerator: schedule_text = str - existing_schedule = self.db.db.schedule.find_one({"date": date_str}) + existing_schedule = self.db.schedule.find_one({"date": date_str}) if existing_schedule: logger.debug(f"{date_str}的日程已存在:") schedule_text = existing_schedule["schedule"] @@ -73,7 +63,7 @@ class ScheduleGenerator: try: schedule_text, _ = await self.llm_scheduler.generate_response(prompt) - self.db.db.schedule.insert_one({"date": date_str, "schedule": schedule_text}) + self.db.schedule.insert_one({"date": date_str, "schedule": schedule_text}) except Exception as e: logger.error(f"生成日程失败: {str(e)}") schedule_text = "生成日程时出错了" @@ -153,7 +143,7 @@ class ScheduleGenerator: """打印完整的日程安排""" if not self._parse_schedule(self.today_schedule_text): logger.warning("今日日程有误,将在下次运行时重新生成") - self.db.db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) + self.db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) else: logger.info("=== 今日日程安排 ===") for time_str, activity in self.today_schedule.items(): diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index 2974389e6..4629f0e0b 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -53,7 +53,7 @@ class LLMStatistics: "costs_by_model": defaultdict(float) } - cursor = self.db.db.llm_usage.find({ + cursor = self.db.llm_usage.find({ "timestamp": {"$gte": start_time} }) 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/template/bot_config_template.toml b/template/bot_config_template.toml index bea6ab7b7..089be69b0 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "0.0.6" +version = "0.0.8" #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 @@ -65,8 +65,13 @@ model_r1_distill_probability = 0.1 # 麦麦回答时选择次要回复模型3 max_response_length = 1024 # 麦麦回答的最大token数 [memory] -build_memory_interval = 300 # 记忆构建间隔 单位秒 -forget_memory_interval = 300 # 记忆遗忘间隔 单位秒 +build_memory_interval = 600 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 +memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 + +forget_memory_interval = 600 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 +memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 +memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 + memory_ban_words = [ #不希望记忆的词 # "403","张三" @@ -101,6 +106,7 @@ word_replace_rate=0.006 # 整词替换概率 enable_advance_output = true # 是否启用高级输出 enable_kuuki_read = true # 是否启用读空气功能 enable_debug_output = false # 是否启用调试输出 +enable_friend_chat = false # 是否启用好友聊天 [groups] talk_allowed = [ 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 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 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