Merge remote-tracking branch 'upstream/debug' into feat_regix

This commit is contained in:
Rikki
2025-03-11 05:05:07 +08:00
parent cd96644a4c
commit a54ca8ce18
35 changed files with 1746 additions and 341 deletions

5
.gitignore vendored
View File

@@ -193,9 +193,8 @@ cython_debug/
# jieba # jieba
jieba.cache jieba.cache
# .vscode
# vscode !.vscode/settings.json
/.vscode
# direnv # direnv
/.direnv /.direnv

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

View File

@@ -29,16 +29,17 @@
</a> </a>
</div> </div>
> ⚠️ **注意事项** > [!WARNING]
> - 项目处于活跃开发阶段,代码可能随时更改 > - 项目处于活跃开发阶段,代码可能随时更改
> - 文档未完善,有问题可以提交 Issue 或者 Discussion > - 文档未完善,有问题可以提交 Issue 或者 Discussion
> - QQ机器人存在被限制风险请自行了解谨慎使用 > - QQ机器人存在被限制风险请自行了解谨慎使用
> - 由于持续迭代可能存在一些已知或未知的bug > - 由于持续迭代可能存在一些已知或未知的bug
> - 由于开发中可能消耗较多token > - 由于开发中可能消耗较多token
**交流群**: 766798517 一群人较多,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 ## 💬交流群
**交流群**: 571780722 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 ,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码
**交流群**: 1035228475 另一个群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码
- [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475开发和建议相关讨论不一定有空回复会优先写文档和代码
**其他平台版本** **其他平台版本**
@@ -51,18 +52,17 @@
### 部署方式 ### 部署方式
如果你不知道Docker是什么建议寻找相关教程或使用手动部署现在不建议使用docker更新慢可能不适配 - 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置
- [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md)
- [📦 Linux 手动部署指南 ](docs/manual_deploy_linux.md)
如果你不知道Docker是什么建议寻找相关教程或使用手动部署 **现在不建议使用docker更新慢可能不适配**
- [🐳 Docker部署指南](docs/docker_deploy.md) - [🐳 Docker部署指南](docs/docker_deploy.md)
- [📦 手动部署指南 Windows](docs/manual_deploy_windows.md)
- [📦 手动部署指南 Linux](docs/manual_deploy_linux.md)
- 📦 Windows 一键傻瓜式部署,请运行项目根目录中的 ```run.bat```,部署完成后请参照后续配置指南进行配置
### 配置说明 ### 配置说明
- [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘 - [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘
- [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户 - [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户
@@ -140,9 +140,10 @@
## 📌 注意事项 ## 📌 注意事项
SengokuCola纯编程外行面向cursor编程很多代码史一样多多包涵
> ⚠️ **警告**:本应用生成内容来自人工智能模型,由 AI 生成请仔细甄别请勿用于违反法律的用途AI生成内容不代表本人观点和立场。 SengokuCola纯编程外行面向cursor编程很多代码史一样多多包涵
> [!WARNING]
> 本应用生成内容来自人工智能模型,由 AI 生成请仔细甄别请勿用于违反法律的用途AI生成内容不代表本人观点和立场。
## 致谢 ## 致谢
[nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架 [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架

65
bot.py
View File

@@ -1,9 +1,12 @@
import asyncio
import os import os
import shutil import shutil
import sys import sys
import nonebot import nonebot
import time import time
import uvicorn
from dotenv import load_dotenv from dotenv import load_dotenv
from loguru import logger from loguru import logger
from nonebot.adapters.onebot.v11 import Adapter from nonebot.adapters.onebot.v11 import Adapter
@@ -12,6 +15,8 @@ import platform
# 获取没有加载env时的环境变量 # 获取没有加载env时的环境变量
env_mask = {key: os.getenv(key) for key in os.environ} env_mask = {key: os.getenv(key) for key in os.environ}
uvicorn_server = None
def easter_egg(): def easter_egg():
# 彩蛋 # 彩蛋
@@ -100,10 +105,12 @@ def load_logger():
"#777777>|</> <cyan>{name:.<8}</cyan>:<cyan>{function:.<8}</cyan>:<cyan>{line: >4}</cyan> <fg " "#777777>|</> <cyan>{name:.<8}</cyan>:<cyan>{function:.<8}</cyan>:<cyan>{line: >4}</cyan> <fg "
"#777777>-</> <level>{message}</level>", "#777777>-</> <level>{message}</level>",
colorize=True, colorize=True,
level=os.getenv("LOG_LEVEL", "DEBUG") # 根据环境设置日志级别默认为INFO level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别默认为INFO
filter=lambda record: "nonebot" not in record["name"]
) )
def scan_provider(env_config: dict): def scan_provider(env_config: dict):
provider = {} provider = {}
@@ -138,7 +145,39 @@ def scan_provider(env_config: dict):
raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量") raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量")
if __name__ == "__main__": async def graceful_shutdown():
try:
global uvicorn_server
if uvicorn_server:
uvicorn_server.force_exit = True # 强制退出
await uvicorn_server.shutdown()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e:
logger.error(f"麦麦关闭失败: {e}")
async def uvicorn_main():
global uvicorn_server
config = uvicorn.Config(
app="__main__:app",
host=os.getenv("HOST", "127.0.0.1"),
port=int(os.getenv("PORT", 8080)),
reload=os.getenv("ENVIRONMENT") == "dev",
timeout_graceful_shutdown=5,
log_config=None,
access_log=False
)
server = uvicorn.Server(config)
uvicorn_server = server
await server.serve()
def raw_main():
# 利用 TZ 环境变量设定程序工作的时区 # 利用 TZ 环境变量设定程序工作的时区
# 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 # 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用
if platform.system().lower() != 'windows': if platform.system().lower() != 'windows':
@@ -165,10 +204,30 @@ if __name__ == "__main__":
nonebot.init(**base_config, **env_config) nonebot.init(**base_config, **env_config)
# 注册适配器 # 注册适配器
global driver
driver = nonebot.get_driver() driver = nonebot.get_driver()
driver.register_adapter(Adapter) driver.register_adapter(Adapter)
# 加载插件 # 加载插件
nonebot.load_plugins("src/plugins") nonebot.load_plugins("src/plugins")
nonebot.run()
if __name__ == "__main__":
try:
raw_main()
global app
app = nonebot.get_asgi()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(uvicorn_main())
except KeyboardInterrupt:
logger.warning("麦麦会努力做的更好的!正在停止中......")
except Exception as e:
logger.error(f"主程序异常: {e}")
finally:
loop.run_until_complete(graceful_shutdown())
loop.close()
logger.info("进程终止完毕,麦麦开始休眠......下次再见哦!")

View File

@@ -1,6 +1,12 @@
# Changelog # Changelog
## [0.0.5] - 2025-3-11
### Added
- 新增了 `alias_names` 配置项,用于指定麦麦的别名。
## [0.0.4] - 2025-3-9 ## [0.0.4] - 2025-3-9
### Added ### Added
- 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 - 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。

View File

@@ -6,8 +6,6 @@ services:
- NAPCAT_UID=${NAPCAT_UID} - NAPCAT_UID=${NAPCAT_UID}
- NAPCAT_GID=${NAPCAT_GID} # 让 NapCat 获取当前用户 GID,UID防止权限问题 - NAPCAT_GID=${NAPCAT_GID} # 让 NapCat 获取当前用户 GID,UID防止权限问题
ports: ports:
- 3000:3000
- 3001:3001
- 6099:6099 - 6099:6099
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -19,7 +17,7 @@ services:
mongodb: mongodb:
container_name: mongodb container_name: mongodb
environment: environment:
- tz=Asia/Shanghai - TZ=Asia/Shanghai
# - MONGO_INITDB_ROOT_USERNAME=your_username # - MONGO_INITDB_ROOT_USERNAME=your_username
# - MONGO_INITDB_ROOT_PASSWORD=your_password # - MONGO_INITDB_ROOT_PASSWORD=your_password
expose: expose:

20
docs/Jonathan R.md Normal file
View File

@@ -0,0 +1,20 @@
Jonathan R. Wolpaw 在 “Memory in neuroscience: rhetoric versus reality.” 一文中提到,从神经科学的感觉运动假设出发,整个神经系统的功能是将经验与适当的行为联系起来,而不是单纯的信息存储。
Jonathan R,Wolpaw. (2019). Memory in neuroscience: rhetoric versus reality.. Behavioral and cognitive neuroscience reviews(2).
1. **单一过程理论**
- 单一过程理论认为,识别记忆主要是基于熟悉性这一单一因素的影响。熟悉性是指对刺激的一种自动的、无意识的感知,它可以使我们在没有回忆起具体细节的情况下,判断一个刺激是否曾经出现过。
- 例如在一些实验中研究者发现被试可以在没有回忆起具体学习情境的情况下对曾经出现过的刺激做出正确的判断这被认为是熟悉性在起作用1。
2. **双重过程理论**
- 双重过程理论则认为,识别记忆是基于两个过程:回忆和熟悉性。回忆是指对过去经验的有意识的回忆,它可以使我们回忆起具体的细节和情境;熟悉性则是一种自动的、无意识的感知。
- 该理论认为,在识别记忆中,回忆和熟悉性共同作用,使我们能够判断一个刺激是否曾经出现过。例如,在 “记得 / 知道” 范式中被试被要求判断他们对一个刺激的记忆是基于回忆还是熟悉性。研究发现被试可以区分这两种不同的记忆过程这为双重过程理论提供了支持1。
1. **神经元节点与连接**:借鉴神经网络原理,将每个记忆单元视为一个神经元节点。节点之间通过连接相互关联,连接的强度代表记忆之间的关联程度。在形态学联想记忆中,具有相似形态特征的记忆节点连接强度较高。例如,苹果和橘子的记忆节点,由于在形状、都是水果等形态语义特征上相似,它们之间的连接强度大于苹果与汽车记忆节点间的连接强度。
2. **记忆聚类与层次结构**:依据形态特征的相似性对记忆进行聚类,形成不同的记忆簇。每个记忆簇内部的记忆具有较高的相似性,而不同记忆簇之间的记忆相似性较低。同时,构建记忆的层次结构,高层次的记忆节点代表更抽象、概括的概念,低层次的记忆节点对应具体的实例。比如,“水果” 作为高层次记忆节点,连接着 “苹果”“橘子”“香蕉” 等低层次具体水果的记忆节点。
3. **网络的动态更新**:随着新记忆的不断加入,记忆网络动态调整。新记忆节点根据其形态特征与现有网络中的节点建立连接,同时影响相关连接的强度。若新记忆与某个记忆簇的特征高度相似,则被纳入该记忆簇;若具有独特特征,则可能引发新的记忆簇的形成。例如,当系统学习到一种新的水果 “番石榴”,它会根据番石榴的形态、语义等特征,在记忆网络中找到与之最相似的区域(如水果记忆簇),并建立相应连接,同时调整周围节点连接强度以适应这一新记忆。
- **相似性联想**:该理论认为,当两个或多个事物在形态上具有相似性时,它们在记忆中会形成关联。例如,梨和苹果在形状和都是水果这一属性上有相似性,所以当我们看到梨时,很容易通过形态学联想记忆联想到苹果。这种相似性联想有助于我们对新事物进行分类和理解,当遇到一个新的类似水果时,我们可以通过与已有的水果记忆进行相似性匹配,来推测它的一些特征。
- **时空关联性联想**除了相似性联想MAM 还强调时空关联性联想。如果两个事物在时间或空间上经常同时出现,它们也会在记忆中形成关联。比如,每次在公园里看到花的时候,都能听到鸟儿的叫声,那么花和鸟儿叫声的形态特征(花的视觉形态和鸟叫的听觉形态)就会在记忆中形成关联,以后听到鸟叫可能就会联想到公园里的花。

View File

@@ -1,67 +1,97 @@
# 🐳 Docker 部署指南 # 🐳 Docker 部署指南
## 部署步骤推荐,但不一定是最新 ## 部署步骤 (推荐,但不一定是最新)
**"更新镜像与容器"部分在本文档 [Part 6](#6-更新镜像与容器)**
### 0. 前提说明
**本文假设读者已具备一定的 Docker 基础知识。若您对 Docker 不熟悉,建议先参考相关教程或文档进行学习,或选择使用 [📦Linux手动部署指南](./manual_deploy_linux.md) 或 [📦Windows手动部署指南](./manual_deploy_windows.md) 。**
### 1. 获取Docker配置文件: ### 1. 获取Docker配置文件
- 建议先单独创建好一个文件夹并进入,作为工作目录
```bash ```bash
wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml wget https://raw.githubusercontent.com/SengokuCola/MaiMBot/main/docker-compose.yml -O docker-compose.yml
``` ```
- 若需要启用MongoDB数据库的用户名和密码可进入docker-compose.yml取消MongoDB处的注释并修改变量`=`后方的值为你的用户名和密码\ - 若需要启用MongoDB数据库的用户名和密码可进入docker-compose.yml取消MongoDB处的注释并修改变量`=` 后方的值为你的用户名和密码\
修改后请注意在之后配置`.env.prod`文件时指定MongoDB数据库的用户名密码 修改后请注意在之后配置 `.env.prod` 文件时指定MongoDB数据库的用户名密码
### 2. 启动服务: ### 2. 启动服务
- **!!! 请在第一次启动前确保当前工作目录下`.env.prod``bot_config.toml`文件存在 !!!**\ - **!!! 请在第一次启动前确保当前工作目录下 `.env.prod``bot_config.toml` 文件存在 !!!**\
由于Docker文件映射行为的特殊性若宿主机的映射路径不存在可能导致意外的目录创建而不会创建文件由于此处需要文件映射到文件需提前确保文件存在且路径正确可使用如下命令: 由于Docker文件映射行为的特殊性若宿主机的映射路径不存在可能导致意外的目录创建而不会创建文件由于此处需要文件映射到文件需提前确保文件存在且路径正确可使用如下命令:
```bash ```bash
touch .env.prod touch .env.prod
touch bot_config.toml touch bot_config.toml
``` ```
- 启动Docker容器: - 启动Docker容器:
```bash ```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d
# 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d
``` ```
- 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
### 3. 修改配置并重启Docker
### 3. 修改配置并重启Docker: - 请前往 [🎀新手配置指南](./installation_cute.md) 或 [⚙️标准配置指南](./installation_standard.md) 完成 `.env.prod``bot_config.toml` 配置文件的编写\
**需要注意 `.env.prod` 中HOST处IP的填写Docker中部署和系统中直接安装的配置会有所不同**
- 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod``bot_config.toml`配置文件的编写\
**需要注意`.env.prod`中HOST处IP的填写Docker中部署和系统中直接安装的配置会有所不同**
- 重启Docker容器: - 重启Docker容器:
```bash ```bash
docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名 docker restart maimbot # 若修改过容器名称则替换maimbot为你自定的名
``` ```
- 下方命令可以但不推荐只是同时重启NapCat、MongoDB、MaiMBot三个服务 - 下方命令可以但不推荐只是同时重启NapCat、MongoDB、MaiMBot三个服务
```bash ```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose restart
# 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose restart
``` ```
- 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
### 4. 登入NapCat管理页添加反向WebSocket ### 4. 登入NapCat管理页添加反向WebSocket
- 在浏览器地址栏输入`http://<宿主机IP>:6099/`进入NapCat的管理Web页添加一个Websocket客户端 - 在浏览器地址栏输入 `http://<宿主机IP>:6099/` 进入NapCat的管理Web页添加一个Websocket客户端
> 网络配置 -> 新建 -> Websocket客户端 > 网络配置 -> 新建 -> Websocket客户端
- Websocket客户端的名称自定URL栏填入`ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\ - Websocket客户端的名称自定URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可\
(若修改过容器名称则替换maimbot为你自定的名称) (若修改过容器名称则替换maimbot为你自定的名称)
### 5. 愉快地和麦麦对话吧! ### 5. 部署完成,愉快地和麦麦对话吧!
### 6. 更新镜像与容器
- 拉取最新镜像
```bash
docker-compose pull
```
- 执行启动容器指令,该指令会自动重建镜像有更新的容器并启动
```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose up -d
# 旧版Docker中可能找不到docker compose请使用docker-compose工具替代
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d
```
## ⚠️ 注意事项 ## ⚠️ 注意事项
- 目前部署方案仍在测试中,可能存在未知问题 - 目前部署方案仍在测试中,可能存在未知问题
- 配置文件中的API密钥请妥善保管不要泄露 - 配置文件中的API密钥请妥善保管不要泄露
- 建议先在测试环境中运行,确认无误后再部署到生产环境 - 建议先在测试环境中运行,确认无误后再部署到生产环境

View File

@@ -52,12 +52,12 @@ key = "SILICONFLOW_KEY" # 用同一张门票就可以啦
如果你想用DeepSeek官方的服务就要这样改 如果你想用DeepSeek官方的服务就要这样改
```toml ```toml
[model.llm_reasoning] [model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1" name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1
base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园 base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园
key = "DEEP_SEEK_KEY" # 用DeepSeek的门票 key = "DEEP_SEEK_KEY" # 用DeepSeek的门票
[model.llm_normal] [model.llm_normal]
name = "Pro/deepseek-ai/DeepSeek-V3" name = "deepseek-chat" # 改成对应的模型名称,这里为DeepseekV3
base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园 base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园
key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票 key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票
``` ```
@@ -110,7 +110,8 @@ PLUGINS=["src2.plugins.chat"] # 这里是机器人的插件列表呢
```toml ```toml
[bot] [bot]
qq = "把这里改成你的机器人QQ号喵" # 填写你的机器人QQ号 qq = "把这里改成你的机器人QQ号喵" # 填写你的机器人QQ号
nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦 nickname = "麦麦" # 机器人的名字,你可以改成你喜欢的任何名字哦建议和机器人QQ名称/群昵称一样哦
alias_names = ["小麦", "阿麦"] # 也可以用这个招呼机器人,可以不设置呢
[personality] [personality]
# 这里可以设置机器人的性格呢,让它更有趣一些喵 # 这里可以设置机器人的性格呢,让它更有趣一些喵

View File

@@ -8,7 +8,7 @@
## API配置说明 ## API配置说明
`.env.prod``bot_config.toml`中的API配置关系如下 `.env.prod``bot_config.toml` 中的API配置关系如下
### 在.env.prod中定义API凭证 ### 在.env.prod中定义API凭证
```ini ```ini
@@ -34,7 +34,7 @@ key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥
如需切换到其他API服务只需修改引用 如需切换到其他API服务只需修改引用
```toml ```toml
[model.llm_reasoning] [model.llm_reasoning]
name = "Pro/deepseek-ai/DeepSeek-R1" name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1
base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务 base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务
key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥
``` ```
@@ -53,11 +53,11 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
# 服务配置 # 服务配置
HOST=127.0.0.1 # 如果使用Docker部署需要改成0.0.0.0否则QQ消息无法传入 HOST=127.0.0.1 # 如果使用Docker部署需要改成0.0.0.0否则QQ消息无法传入
PORT=8080 PORT=8080 # 与反向端口相同
# 数据库配置 # 数据库配置
MONGODB_HOST=127.0.0.1 # 如果使用Docker部署需要改成数据库容器的名字默认是mongodb MONGODB_HOST=127.0.0.1 # 如果使用Docker部署需要改成数据库容器的名字默认是mongodb
MONGODB_PORT=27017 MONGODB_PORT=27017 # MongoDB端口
DATABASE_NAME=MegBot DATABASE_NAME=MegBot
MONGODB_USERNAME = "" # 数据库用户名 MONGODB_USERNAME = "" # 数据库用户名
MONGODB_PASSWORD = "" # 数据库密码 MONGODB_PASSWORD = "" # 数据库密码
@@ -72,6 +72,9 @@ PLUGINS=["src2.plugins.chat"]
[bot] [bot]
qq = "机器人QQ号" # 必填 qq = "机器人QQ号" # 必填
nickname = "麦麦" # 机器人昵称 nickname = "麦麦" # 机器人昵称
# alias_names: 配置机器人可使用的别名。当机器人在群聊或对话中被调用时,别名可以作为直接命令或提及机器人的关键字使用。
# 该配置项为字符串数组。例如: ["小麦", "阿麦"]
alias_names = ["小麦", "阿麦"] # 机器人别名
[personality] [personality]
prompt_personality = [ prompt_personality = [

View File

@@ -66,7 +66,7 @@ pip install -r requirements.txt
## 数据库配置 ## 数据库配置
### 3⃣ **安装并启动MongoDB** ### 3⃣ **安装并启动MongoDB**
- 安装与启动Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/)Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/) - 安装与启动: Debian参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/)Ubuntu参考[官方文档](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/)
- 默认连接本地27017端口 - 默认连接本地27017端口
--- ---
@@ -76,15 +76,14 @@ pip install -r requirements.txt
- 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装 - 参考[NapCat官方文档](https://www.napcat.wiki/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)安装
- 使用QQ小号登录添加反向WS地址 - 使用QQ小号登录添加反向WS地址: `ws://127.0.0.1:8080/onebot/v11/ws`
`ws://127.0.0.1:8080/onebot/v11/ws`
--- ---
## 配置文件设置 ## 配置文件设置
### 5⃣ **配置文件设置让麦麦Bot正常工作** ### 5⃣ **配置文件设置让麦麦Bot正常工作**
- 修改环境配置文件`.env.prod` - 修改环境配置文件: `.env.prod`
- 修改机器人配置文件`bot_config.toml` - 修改机器人配置文件: `bot_config.toml`
--- ---
@@ -107,9 +106,9 @@ python3 bot.py
--- ---
## 常见问题 ## 常见问题
🔧 权限问题在命令前加`sudo` 🔧 权限问题: 在命令前加 `sudo`
🔌 端口占用使用`sudo lsof -i :8080`查看端口占用 🔌 端口占用: 使用 `sudo lsof -i :8080` 查看端口占用
🛡️ 防火墙确保8080/27017端口开放 🛡️ 防火墙: 确保8080/27017端口开放
```bash ```bash
sudo ufw allow 8080/tcp sudo ufw allow 8080/tcp
sudo ufw allow 27017/tcp sudo ufw allow 27017/tcp

View File

@@ -30,7 +30,7 @@
在创建虚拟环境之前请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装: 在创建虚拟环境之前请确保你的电脑上安装了Python 3.9及以上版本。如果没有,可以按以下步骤安装:
1. 访问Python官网下载页面https://www.python.org/downloads/release/python-3913/ 1. 访问Python官网下载页面: https://www.python.org/downloads/release/python-3913/
2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe` 2. 下载Windows安装程序 (64-bit): `python-3.9.13-amd64.exe`
3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项 3. 运行安装程序,并确保勾选"Add Python 3.9 to PATH"选项
4. 点击"Install Now"开始安装 4. 点击"Install Now"开始安装
@@ -79,11 +79,11 @@ pip install -r requirements.txt
### 3⃣ **配置NapCat让麦麦bot与qq取得联系** ### 3⃣ **配置NapCat让麦麦bot与qq取得联系**
- 安装并登录NapCat用你的qq小号 - 安装并登录NapCat用你的qq小号
- 添加反向WS`ws://127.0.0.1:8080/onebot/v11/ws` - 添加反向WS: `ws://127.0.0.1:8080/onebot/v11/ws`
### 4⃣ **配置文件设置让麦麦Bot正常工作** ### 4⃣ **配置文件设置让麦麦Bot正常工作**
- 修改环境配置文件`.env.prod` - 修改环境配置文件: `.env.prod`
- 修改机器人配置文件`bot_config.toml` - 修改机器人配置文件: `bot_config.toml`
### 5⃣ **启动麦麦机器人** ### 5⃣ **启动麦麦机器人**
- 打开命令行cd到对应路径 - 打开命令行cd到对应路径

View File

@@ -22,6 +22,7 @@
pythonEnv = pkgs.python3.withPackages ( pythonEnv = pkgs.python3.withPackages (
ps: with ps; [ ps: with ps; [
ruff
pymongo pymongo
python-dotenv python-dotenv
pydantic pydantic

View File

@@ -1,23 +1,51 @@
[project] [project]
name = "Megbot" name = "MaiMaiBot"
version = "0.1.0" version = "0.1.0"
description = "New Bot Project" description = "MaiMaiBot"
[tool.nonebot] [tool.nonebot]
plugins = ["src.plugins.chat"] plugins = ["src.plugins.chat"]
plugin_dirs = ["src/plugins"] plugin_dirs = ["src/plugins"]
[tool.ruff] [tool.ruff]
# 设置 Python 版本
target-version = "py39" include = ["*.py"]
# 行长度设置
line-length = 120
[tool.ruff.lint]
fixable = ["ALL"]
unfixable = []
# 如果一个变量的名称以下划线开头,即使它未被使用,也不应该被视为错误或警告。
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# 启用的规则 # 启用的规则
select = [ select = [
"E", # pycodestyle 错误 "E", # pycodestyle 错误
"F", # pyflakes "F", # pyflakes
"I", # isort "B", # flake8-bugbear
"B", # flake8-bugbear
] ]
# 行长度设置 ignore = ["E711"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
indent-style = "space"
# 使用双引号表示字符串
quote-style = "double"
# 尊重魔法尾随逗号
# 例如:
# items = [
# "apple",
# "banana",
# "cherry",
# ]
skip-magic-trailing-comma = false
# 自动检测合适的换行符
line-ending = "auto"

Binary file not shown.

View File

@@ -3,7 +3,7 @@ chcp 65001
if not exist "venv" ( if not exist "venv" (
python -m venv venv python -m venv venv
call venv\Scripts\activate.bat call venv\Scripts\activate.bat
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade -r requirements.txt pip install -i https://mirrors.aliyun.com/pypi/simple --upgrade -r requirements.txt
) else ( ) else (
call venv\Scripts\activate.bat call venv\Scripts\activate.bat
) )

2
run.py
View File

@@ -107,6 +107,8 @@ def install_napcat():
napcat_filename = input( napcat_filename = input(
"下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell" "下载完成后请把文件复制到此文件夹,并将**不包含后缀的文件名**输入至此窗口,如 NapCat.32793.Shell"
) )
if(napcat_filename[-4:] == ".zip"):
napcat_filename = napcat_filename[:-4]
extract_files(napcat_filename + ".zip", "napcat") extract_files(napcat_filename + ".zip", "napcat")
print("NapCat 安装完成") print("NapCat 安装完成")
os.remove(napcat_filename + ".zip") os.remove(napcat_filename + ".zip")

View File

@@ -87,7 +87,7 @@ class ReasoningGUI:
self.db = Database.get_instance().db self.db = Database.get_instance().db
logger.success("数据库初始化成功") logger.success("数据库初始化成功")
except Exception: except Exception:
logger.exception(f"数据库初始化失败") logger.exception("数据库初始化失败")
sys.exit(1) sys.exit(1)
# 存储群组数据 # 存储群组数据
@@ -342,7 +342,7 @@ class ReasoningGUI:
'group_id': self.selected_group_id 'group_id': self.selected_group_id
}) })
except Exception: except Exception:
logger.exception(f"自动更新出错") logger.exception("自动更新出错")
# 每5秒更新一次 # 每5秒更新一次
time.sleep(5) time.sleep(5)

View File

@@ -121,9 +121,9 @@ async def build_memory_task():
@scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") @scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory")
async def forget_memory_task(): async def forget_memory_task():
"""每30秒执行一次记忆构建""" """每30秒执行一次记忆构建"""
# print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...")
# await hippocampus.operation_forget_topic(percentage=0.1) await hippocampus.operation_forget_topic(percentage=0.1)
# print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成")
@scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval + 10, id="merge_memory") @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval + 10, id="merge_memory")

View File

@@ -138,7 +138,7 @@ class ChatBot:
# 如果找不到思考消息,直接返回 # 如果找不到思考消息,直接返回
if not thinking_message: if not thinking_message:
logger.warning(f"未找到对应的思考消息,可能已超时被移除") logger.warning("未找到对应的思考消息,可能已超时被移除")
return return
# 记录开始思考的时间,避免从思考到回复的时间太久 # 记录开始思考的时间,避免从思考到回复的时间太久
@@ -187,7 +187,7 @@ class ChatBot:
# 检查是否 <没有找到> emoji # 检查是否 <没有找到> emoji
if emoji_raw != None: if emoji_raw != None:
emoji_path, discription = emoji_raw emoji_path, description = emoji_raw
emoji_cq = CQCode.create_emoji_cq(emoji_path) emoji_cq = CQCode.create_emoji_cq(emoji_path)
@@ -203,7 +203,7 @@ class ChatBot:
raw_message=emoji_cq, raw_message=emoji_cq,
plain_text=emoji_cq, plain_text=emoji_cq,
processed_plain_text=emoji_cq, processed_plain_text=emoji_cq,
detailed_plain_text=discription, detailed_plain_text=description,
user_nickname=global_config.BOT_NICKNAME, user_nickname=global_config.BOT_NICKNAME,
group_name=message.group_name, group_name=message.group_name,
time=bot_response_time, time=bot_response_time,

View File

@@ -1,6 +1,6 @@
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Optional from typing import Dict, List, Optional
import tomli import tomli
from loguru import logger from loguru import logger
@@ -12,10 +12,12 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier
@dataclass @dataclass
class BotConfig: class BotConfig:
"""机器人配置类""" """机器人配置类"""
INNER_VERSION: Version = None INNER_VERSION: Version = None
BOT_QQ: Optional[int] = 1 BOT_QQ: Optional[int] = 1
BOT_NICKNAME: Optional[str] = None BOT_NICKNAME: Optional[str] = None
BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它
# 消息处理相关配置 # 消息处理相关配置
MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度
@@ -82,23 +84,25 @@ class BotConfig:
PROMPT_PERSONALITY = [ PROMPT_PERSONALITY = [
"曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧",
"是一个女大学生,你有黑色头发,你会刷小红书", "是一个女大学生,你有黑色头发,你会刷小红书",
"是一个女大学生你会刷b站对ACG文化感兴趣" "是一个女大学生你会刷b站对ACG文化感兴趣",
] ]
PROMPT_SCHEDULE_GEN="一个曾经学习地质,现在学习心理学和脑科学的女大学生喜欢刷qq贴吧知乎和小红书" PROMPT_SCHEDULE_GEN = "一个曾经学习地质,现在学习心理学和脑科学的女大学生喜欢刷qq贴吧知乎和小红书"
PERSONALITY_1: float = 0.6 # 第一种人格概率 PERSONALITY_1: float = 0.6 # 第一种人格概率
PERSONALITY_2: float = 0.3 # 第二种人格概率 PERSONALITY_2: float = 0.3 # 第二种人格概率
PERSONALITY_3: float = 0.1 # 第三种人格概率 PERSONALITY_3: float = 0.1 # 第三种人格概率
memory_ban_words: list = field(default_factory=lambda: ['表情包', '图片', '回复', '聊天记录']) # 添加新的配置项默认值 memory_ban_words: list = field(
default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"]
) # 添加新的配置项默认值
@staticmethod @staticmethod
def get_config_dir() -> str: def get_config_dir() -> str:
"""获取配置文件目录""" """获取配置文件目录"""
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
config_dir = os.path.join(root_dir, 'config') config_dir = os.path.join(root_dir, "config")
if not os.path.exists(config_dir): if not os.path.exists(config_dir):
os.makedirs(config_dir) os.makedirs(config_dir)
return config_dir return config_dir
@@ -109,35 +113,32 @@ class BotConfig:
Args: Args:
value[str]: 版本表达式(字符串) value[str]: 版本表达式(字符串)
Returns: Returns:
SpecifierSet SpecifierSet
""" """
try: try:
converted = SpecifierSet(value) converted = SpecifierSet(value)
except InvalidSpecifier as e: except InvalidSpecifier:
logger.error( logger.error(f"{value} 分类使用了错误的版本约束表达式\n", "请阅读 https://semver.org/lang/zh-CN/ 修改代码")
f"{value} 分类使用了错误的版本约束表达式\n",
"请阅读 https://semver.org/lang/zh-CN/ 修改代码"
)
exit(1) exit(1)
return converted return converted
@classmethod @classmethod
def get_config_version(cls, toml: dict) -> Version: def get_config_version(cls, toml: dict) -> Version:
"""提取配置文件的 SpecifierSet 版本数据 """提取配置文件的 SpecifierSet 版本数据
Args: Args:
toml[dict]: 输入的配置文件字典 toml[dict]: 输入的配置文件字典
Returns: Returns:
Version Version
""" """
if 'inner' in toml: if "inner" in toml:
try: try:
config_version: str = toml["inner"]["version"] config_version: str = toml["inner"]["version"]
except KeyError as e: except KeyError as e:
logger.error(f"配置文件中 inner 段 不存在, 这是错误的配置文件") logger.error("配置文件中 inner 段 不存在, 这是错误的配置文件")
raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") raise KeyError(f"配置文件中 inner 段 不存在 {e}, 这是错误的配置文件") from e
else: else:
toml["inner"] = {"version": "0.0.0"} toml["inner"] = {"version": "0.0.0"}
config_version = toml["inner"]["version"] config_version = toml["inner"]["version"]
@@ -150,7 +151,7 @@ class BotConfig:
"请阅读 https://semver.org/lang/zh-CN/ 修改配置,并参考本项目指定的模板进行修改\n" "请阅读 https://semver.org/lang/zh-CN/ 修改配置,并参考本项目指定的模板进行修改\n"
"本项目在不同的版本下有不同的模板,请注意识别" "本项目在不同的版本下有不同的模板,请注意识别"
) )
raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") raise InvalidVersion("配置文件中 inner段 的 version 键是错误的版本描述\n") from e
return ver return ver
@@ -160,26 +161,26 @@ class BotConfig:
config = cls() config = cls()
def personality(parent: dict): def personality(parent: dict):
personality_config = parent['personality'] personality_config = parent["personality"]
personality = personality_config.get('prompt_personality') personality = personality_config.get("prompt_personality")
if len(personality) >= 2: if len(personality) >= 2:
logger.debug(f"载入自定义人格:{personality}") logger.debug(f"载入自定义人格:{personality}")
config.PROMPT_PERSONALITY = personality_config.get('prompt_personality', config.PROMPT_PERSONALITY) config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY)
logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}")
config.PROMPT_SCHEDULE_GEN = personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN) config.PROMPT_SCHEDULE_GEN = personality_config.get("prompt_schedule", config.PROMPT_SCHEDULE_GEN)
if config.INNER_VERSION in SpecifierSet(">=0.0.2"): if config.INNER_VERSION in SpecifierSet(">=0.0.2"):
config.PERSONALITY_1 = personality_config.get('personality_1_probability', config.PERSONALITY_1) config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1)
config.PERSONALITY_2 = personality_config.get('personality_2_probability', config.PERSONALITY_2) config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2)
config.PERSONALITY_3 = personality_config.get('personality_3_probability', config.PERSONALITY_3) config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3)
def emoji(parent: dict): def emoji(parent: dict):
emoji_config = parent["emoji"] emoji_config = parent["emoji"]
config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL)
config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL)
config.EMOJI_CHECK_PROMPT = emoji_config.get('check_prompt', config.EMOJI_CHECK_PROMPT) config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT)
config.EMOJI_SAVE = emoji_config.get('auto_save', config.EMOJI_SAVE) config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE)
config.EMOJI_CHECK = emoji_config.get('enable_check', config.EMOJI_CHECK) config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK)
def cq_code(parent: dict): def cq_code(parent: dict):
cq_code_config = parent["cq_code"] cq_code_config = parent["cq_code"]
@@ -192,12 +193,16 @@ class BotConfig:
config.BOT_QQ = int(bot_qq) config.BOT_QQ = int(bot_qq)
config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME) config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME)
if config.INNER_VERSION in SpecifierSet(">=0.0.5"):
config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES)
def response(parent: dict): def response(parent: dict):
response_config = parent["response"] response_config = parent["response"]
config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY) config.MODEL_R1_PROBABILITY = response_config.get("model_r1_probability", config.MODEL_R1_PROBABILITY)
config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY) config.MODEL_V3_PROBABILITY = response_config.get("model_v3_probability", config.MODEL_V3_PROBABILITY)
config.MODEL_R1_DISTILL_PROBABILITY = response_config.get("model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY = response_config.get(
config.MODEL_R1_DISTILL_PROBABILITY) "model_r1_distill_probability", config.MODEL_R1_DISTILL_PROBABILITY
)
config.max_response_length = response_config.get("max_response_length", config.max_response_length) config.max_response_length = response_config.get("max_response_length", config.max_response_length)
def model(parent: dict): def model(parent: dict):
@@ -214,7 +219,7 @@ class BotConfig:
"llm_emotion_judge", "llm_emotion_judge",
"vlm", "vlm",
"embedding", "embedding",
"moderation" "moderation",
] ]
for item in config_list: for item in config_list:
@@ -223,13 +228,7 @@ class BotConfig:
# base_url 的例子: SILICONFLOW_BASE_URL # base_url 的例子: SILICONFLOW_BASE_URL
# key 的例子: SILICONFLOW_KEY # key 的例子: SILICONFLOW_KEY
cfg_target = { cfg_target = {"name": "", "base_url": "", "key": "", "pri_in": 0, "pri_out": 0}
"name": "",
"base_url": "",
"key": "",
"pri_in": 0,
"pri_out": 0
}
if config.INNER_VERSION in SpecifierSet("<=0.0.0"): if config.INNER_VERSION in SpecifierSet("<=0.0.0"):
cfg_target = cfg_item cfg_target = cfg_item
@@ -248,7 +247,7 @@ class BotConfig:
cfg_target[i] = cfg_item[i] cfg_target[i] = cfg_item[i]
except KeyError as e: except KeyError as e:
logger.error(f"{item} 中的必要字段不存在,请检查") logger.error(f"{item} 中的必要字段不存在,请检查")
raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") raise KeyError(f"{item} 中的必要字段 {e} 不存在,请检查") from e
provider = cfg_item.get("provider") provider = cfg_item.get("provider")
if provider is None: if provider is None:
@@ -273,10 +272,12 @@ class BotConfig:
if config.INNER_VERSION in SpecifierSet(">=0.0.2"): if config.INNER_VERSION in SpecifierSet(">=0.0.2"):
config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout) config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout)
config.response_willing_amplifier = msg_config.get("response_willing_amplifier", config.response_willing_amplifier = msg_config.get(
config.response_willing_amplifier) "response_willing_amplifier", config.response_willing_amplifier
config.response_interested_rate_amplifier = msg_config.get("response_interested_rate_amplifier", )
config.response_interested_rate_amplifier) config.response_interested_rate_amplifier = msg_config.get(
"response_interested_rate_amplifier", config.response_interested_rate_amplifier
)
config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate) config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate)
if config.INNER_VERSION in SpecifierSet(">=0.0.5"): if config.INNER_VERSION in SpecifierSet(">=0.0.5"):
@@ -286,7 +287,7 @@ class BotConfig:
memory_config = parent["memory"] memory_config = parent["memory"]
config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval)
config.forget_memory_interval = memory_config.get("forget_memory_interval", config.forget_memory_interval) config.forget_memory_interval = memory_config.get("forget_memory_interval", config.forget_memory_interval)
# 在版本 >= 0.0.4 时才处理新增的配置项 # 在版本 >= 0.0.4 时才处理新增的配置项
if config.INNER_VERSION in SpecifierSet(">=0.0.4"): if config.INNER_VERSION in SpecifierSet(">=0.0.4"):
config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) config.memory_ban_words = set(memory_config.get("memory_ban_words", []))
@@ -307,10 +308,12 @@ class BotConfig:
config.chinese_typo_enable = chinese_typo_config.get("enable", config.chinese_typo_enable) config.chinese_typo_enable = chinese_typo_config.get("enable", config.chinese_typo_enable)
config.chinese_typo_error_rate = chinese_typo_config.get("error_rate", config.chinese_typo_error_rate) config.chinese_typo_error_rate = chinese_typo_config.get("error_rate", config.chinese_typo_error_rate)
config.chinese_typo_min_freq = chinese_typo_config.get("min_freq", config.chinese_typo_min_freq) config.chinese_typo_min_freq = chinese_typo_config.get("min_freq", config.chinese_typo_min_freq)
config.chinese_typo_tone_error_rate = chinese_typo_config.get("tone_error_rate", config.chinese_typo_tone_error_rate = chinese_typo_config.get(
config.chinese_typo_tone_error_rate) "tone_error_rate", config.chinese_typo_tone_error_rate
config.chinese_typo_word_replace_rate = chinese_typo_config.get("word_replace_rate", )
config.chinese_typo_word_replace_rate) config.chinese_typo_word_replace_rate = chinese_typo_config.get(
"word_replace_rate", config.chinese_typo_word_replace_rate
)
def groups(parent: dict): def groups(parent: dict):
groups_config = parent["groups"] groups_config = parent["groups"]
@@ -329,61 +332,19 @@ class BotConfig:
# 例如:"notice": "personality 将在 1.3.2 后被移除",那么在有效版本中的用户就会虽然可以 # 例如:"notice": "personality 将在 1.3.2 后被移除",那么在有效版本中的用户就会虽然可以
# 正常执行程序,但是会看到这条自定义提示 # 正常执行程序,但是会看到这条自定义提示
include_configs = { include_configs = {
"personality": { "personality": {"func": personality, "support": ">=0.0.0"},
"func": personality, "emoji": {"func": emoji, "support": ">=0.0.0"},
"support": ">=0.0.0" "cq_code": {"func": cq_code, "support": ">=0.0.0"},
}, "bot": {"func": bot, "support": ">=0.0.0"},
"emoji": { "response": {"func": response, "support": ">=0.0.0"},
"func": emoji, "model": {"func": model, "support": ">=0.0.0"},
"support": ">=0.0.0" "message": {"func": message, "support": ">=0.0.0"},
}, "memory": {"func": memory, "support": ">=0.0.0", "necessary": False},
"cq_code": { "mood": {"func": mood, "support": ">=0.0.0"},
"func": cq_code, "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False},
"support": ">=0.0.0" "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False},
}, "groups": {"func": groups, "support": ">=0.0.0"},
"bot": { "others": {"func": others, "support": ">=0.0.0"},
"func": bot,
"support": ">=0.0.0"
},
"response": {
"func": response,
"support": ">=0.0.0"
},
"model": {
"func": model,
"support": ">=0.0.0"
},
"message": {
"func": message,
"support": ">=0.0.0"
},
"memory": {
"func": memory,
"support": ">=0.0.0",
"necessary": False
},
"mood": {
"func": mood,
"support": ">=0.0.0"
},
"keywords_reaction": {
"func": keywords_reaction,
"support": ">=0.0.2",
"necessary": False
},
"chinese_typo": {
"func": chinese_typo,
"support": ">=0.0.3",
"necessary": False
},
"groups": {
"func": groups,
"support": ">=0.0.0"
},
"others": {
"func": others,
"support": ">=0.0.0"
}
} }
# 原地修改,将 字符串版本表达式 转换成 版本对象 # 原地修改,将 字符串版本表达式 转换成 版本对象
@@ -395,7 +356,7 @@ class BotConfig:
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
try: try:
toml_dict = tomli.load(f) toml_dict = tomli.load(f)
except(tomli.TOMLDecodeError) as e: except tomli.TOMLDecodeError as e:
logger.critical(f"配置文件bot_config.toml填写有误请检查第{e.lineno}行第{e.colno}处:{e.msg}") logger.critical(f"配置文件bot_config.toml填写有误请检查第{e.lineno}行第{e.colno}处:{e.msg}")
exit(1) exit(1)
@@ -410,7 +371,7 @@ class BotConfig:
# 检查配置文件版本是否在支持范围内 # 检查配置文件版本是否在支持范围内
if config.INNER_VERSION in group_specifierset: if config.INNER_VERSION in group_specifierset:
# 如果版本在支持范围内,检查是否存在通知 # 如果版本在支持范围内,检查是否存在通知
if 'notice' in include_configs[key]: if "notice" in include_configs[key]:
logger.warning(include_configs[key]["notice"]) logger.warning(include_configs[key]["notice"])
include_configs[key]["func"](toml_dict) include_configs[key]["func"](toml_dict)
@@ -424,7 +385,7 @@ class BotConfig:
raise InvalidVersion(f"当前程序仅支持以下版本范围: {group_specifierset}") raise InvalidVersion(f"当前程序仅支持以下版本范围: {group_specifierset}")
# 如果 necessary 项目存在,而且显式声明是 False进入特殊处理 # 如果 necessary 项目存在,而且显式声明是 False进入特殊处理
elif "necessary" in include_configs[key] and include_configs[key].get("necessary") == False: elif "necessary" in include_configs[key] and include_configs[key].get("necessary") is False:
# 通过 pass 处理的项虽然直接忽略也是可以的,但是为了不增加理解困难,依然需要在这里显式处理 # 通过 pass 处理的项虽然直接忽略也是可以的,但是为了不增加理解困难,依然需要在这里显式处理
if key == "keywords_reaction": if key == "keywords_reaction":
pass pass

View File

@@ -155,8 +155,8 @@ class CQCode:
logger.error(f"最终请求失败: {str(e)}") logger.error(f"最终请求失败: {str(e)}")
time.sleep(1.5 ** retry) # 指数退避 time.sleep(1.5 ** retry) # 指数退避
except Exception as e: except Exception:
logger.exception(f"[未知错误]") logger.exception("[未知错误]")
return None return None
return None return None
@@ -281,7 +281,7 @@ class CQCode:
logger.debug(f"合并后的转发消息: {combined_messages}") logger.debug(f"合并后的转发消息: {combined_messages}")
return f"[转发消息:\n{combined_messages}]" return f"[转发消息:\n{combined_messages}]"
except Exception as e: except Exception:
logger.exception("处理转发消息失败") logger.exception("处理转发消息失败")
return '[转发消息]' return '[转发消息]'

View File

@@ -51,8 +51,8 @@ class EmojiManager:
self._initialized = True self._initialized = True
# 启动时执行一次完整性检查 # 启动时执行一次完整性检查
self.check_emoji_file_integrity() self.check_emoji_file_integrity()
except Exception as e: except Exception:
logger.exception(f"初始化表情管理器失败") logger.exception("初始化表情管理器失败")
def _ensure_db(self): def _ensure_db(self):
"""确保数据库已初始化""" """确保数据库已初始化"""
@@ -87,8 +87,8 @@ class EmojiManager:
{'_id': emoji_id}, {'_id': emoji_id},
{'$inc': {'usage_count': 1}} {'$inc': {'usage_count': 1}}
) )
except Exception as e: except Exception:
logger.exception(f"记录表情使用失败") logger.exception("记录表情使用失败")
async def get_emoji_for_text(self, text: str) -> Optional[str]: async def get_emoji_for_text(self, text: str) -> Optional[str]:
"""根据文本内容获取相关表情包 """根据文本内容获取相关表情包
@@ -117,7 +117,7 @@ class EmojiManager:
try: try:
# 获取所有表情包 # 获取所有表情包
all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'discription': 1})) all_emojis = list(self.db.db.emoji.find({}, {'_id': 1, 'path': 1, 'embedding': 1, 'description': 1}))
if not all_emojis: if not all_emojis:
logger.warning("数据库中没有任何表情包") logger.warning("数据库中没有任何表情包")
@@ -160,9 +160,9 @@ class EmojiManager:
{'$inc': {'usage_count': 1}} {'$inc': {'usage_count': 1}}
) )
logger.success( logger.success(
f"找到匹配的表情包: {selected_emoji.get('discription', '无描述')} (相似度: {similarity:.4f})") f"找到匹配的表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})")
# 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了
return selected_emoji['path'], "[ %s ]" % selected_emoji.get('discription', '无描述') return selected_emoji['path'], "[ %s ]" % selected_emoji.get('description', '无描述')
except Exception as search_error: except Exception as search_error:
logger.error(f"搜索表情包失败: {str(search_error)}") logger.error(f"搜索表情包失败: {str(search_error)}")
@@ -174,7 +174,7 @@ class EmojiManager:
logger.error(f"获取表情包失败: {str(e)}") logger.error(f"获取表情包失败: {str(e)}")
return None return None
async def _get_emoji_discription(self, image_base64: str) -> str: async def _get_emoji_description(self, image_base64: str) -> str:
"""获取表情包的标签""" """获取表情包的标签"""
try: try:
prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感' prompt = '这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感'
@@ -203,7 +203,7 @@ class EmojiManager:
try: try:
prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。' prompt = f'这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,注意不要输出任何对消息内容的分析内容,只输出\"一种什么样的感觉\"中间的形容词部分。'
content, _ = await self.llm_emotion_judge.generate_response_async(prompt) content, _ = await self.llm_emotion_judge.generate_response_async(prompt,temperature=1.5)
logger.info(f"输出描述: {content}") logger.info(f"输出描述: {content}")
return content return content
@@ -236,36 +236,36 @@ class EmojiManager:
continue continue
# 获取表情包的描述 # 获取表情包的描述
discription = await self._get_emoji_discription(image_base64) description = await self._get_emoji_description(image_base64)
if global_config.EMOJI_CHECK: if global_config.EMOJI_CHECK:
check = await self._check_emoji(image_base64) check = await self._check_emoji(image_base64)
if '' not in check: if '' not in check:
os.remove(image_path) os.remove(image_path)
logger.info(f"描述: {discription}") logger.info(f"描述: {description}")
logger.info(f"其不满足过滤规则,被剔除 {check}") logger.info(f"其不满足过滤规则,被剔除 {check}")
continue continue
logger.info(f"check通过 {check}") logger.info(f"check通过 {check}")
if discription is not None: if description is not None:
embedding = await get_embedding(discription) embedding = await get_embedding(description)
# 准备数据库记录 # 准备数据库记录
emoji_record = { emoji_record = {
'filename': filename, 'filename': filename,
'path': image_path, 'path': image_path,
'embedding': embedding, 'embedding': embedding,
'discription': discription, 'description': description,
'timestamp': int(time.time()) 'timestamp': int(time.time())
} }
# 保存到数据库 # 保存到数据库
self.db.db['emoji'].insert_one(emoji_record) self.db.db['emoji'].insert_one(emoji_record)
logger.success(f"注册新表情包: {filename}") logger.success(f"注册新表情包: {filename}")
logger.info(f"描述: {discription}") logger.info(f"描述: {description}")
else: else:
logger.warning(f"跳过表情包: {filename}") logger.warning(f"跳过表情包: {filename}")
except Exception as e: except Exception:
logger.exception(f"扫描表情包失败") logger.exception("扫描表情包失败")
async def _periodic_scan(self, interval_MINS: int = 10): async def _periodic_scan(self, interval_MINS: int = 10):
"""定期扫描新表情包""" """定期扫描新表情包"""

View File

@@ -94,7 +94,7 @@ class ResponseGenerator:
try: try:
content, reasoning_content = await model.generate_response(prompt) content, reasoning_content = await model.generate_response(prompt)
except Exception: except Exception:
logger.exception(f"生成回复时出错") logger.exception("生成回复时出错")
return None return None
# 保存到数据库 # 保存到数据库
@@ -146,7 +146,7 @@ class ResponseGenerator:
return ["neutral"] return ["neutral"]
except Exception: except Exception:
logger.exception(f"获取情感标签时出错") logger.exception("获取情感标签时出错")
return ["neutral"] return ["neutral"]
async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: async def _process_response(self, content: str) -> Tuple[List[str], List[str]]:

View File

@@ -61,7 +61,7 @@ class Message_Sender:
auto_escape=auto_escape auto_escape=auto_escape
) )
logger.debug(f"发送消息{message}成功") logger.debug(f"发送消息{message}成功")
except Exception as e: except Exception:
logger.exception(f"发送消息{message}失败") logger.exception(f"发送消息{message}失败")
@@ -120,7 +120,7 @@ class MessageContainer:
return True return True
return False return False
except Exception: except Exception:
logger.exception(f"移除消息时发生错误") logger.exception("移除消息时发生错误")
return False return False
def has_messages(self) -> bool: def has_messages(self) -> bool:
@@ -214,7 +214,7 @@ class MessageManager:
if not container.remove_message(msg): if not container.remove_message(msg):
logger.warning("尝试删除不存在的消息") logger.warning("尝试删除不存在的消息")
except Exception: except Exception:
logger.exception(f"处理超时消息时发生错误") logger.exception("处理超时消息时发生错误")
continue continue
async def start_processor(self): async def start_processor(self):

View File

@@ -131,18 +131,19 @@ class PromptBuilder:
probability_1 = global_config.PERSONALITY_1 probability_1 = global_config.PERSONALITY_1
probability_2 = global_config.PERSONALITY_2 probability_2 = global_config.PERSONALITY_2
probability_3 = global_config.PERSONALITY_3 probability_3 = global_config.PERSONALITY_3
prompt_personality = ''
prompt_personality = f'{activate_prompt}你的网名叫{global_config.BOT_NICKNAME},你还有很多别名:{"/".join(global_config.BOT_ALIAS_NAMES)}'
personality_choice = random.random() personality_choice = random.random()
if personality_choice < probability_1: # 第一种人格 if personality_choice < probability_1: # 第一种人格
prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[0]}, 你正在浏览qq群,{promt_info_prompt}, prompt_personality += f'''{personality[0]}, 你正在浏览qq群,{promt_info_prompt},
现在请你给出日常且口语化的回复,平淡一些,尽量简短一些。{keywords_reaction_prompt} 现在请你给出日常且口语化的回复,平淡一些,尽量简短一些。{keywords_reaction_prompt}
请注意把握群里的聊天内容,不要刻意突出自身学科背景,不要回复的太有条理,可以有个性。''' 请注意把握群里的聊天内容,不要刻意突出自身学科背景,不要回复的太有条理,可以有个性。'''
elif personality_choice < probability_1 + probability_2: # 第二种人格 elif personality_choice < probability_1 + probability_2: # 第二种人格
prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[1]}, 你正在浏览qq群{promt_info_prompt}, prompt_personality += f'''{personality[1]}, 你正在浏览qq群{promt_info_prompt},
现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt}
请你表达自己的见解和观点。可以有个性。''' 请你表达自己的见解和观点。可以有个性。'''
else: # 第三种人格 else: # 第三种人格
prompt_personality = f'''{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[2]}, 你正在浏览qq群{promt_info_prompt}, prompt_personality += f'''{personality[2]}, 你正在浏览qq群{promt_info_prompt},
现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt} 现在请你给出日常且口语化的回复,请表现你自己的见解,不要一昧迎合,尽量简短一些。{keywords_reaction_prompt}
请你表达自己的见解和观点。可以有个性。''' 请你表达自己的见解和观点。可以有个性。'''

View File

@@ -45,6 +45,6 @@ class MessageStorage:
self.db.db.messages.insert_one(message_data) self.db.db.messages.insert_one(message_data)
except Exception: except Exception:
logger.exception(f"存储消息失败") logger.exception("存储消息失败")
# 如果需要其他存储相关的函数,可以在这里添加 # 如果需要其他存储相关的函数,可以在这里添加

View File

@@ -53,19 +53,13 @@ def db_message_to_str(message_dict: Dict) -> str:
return result return result
def is_mentioned_bot_in_message(message: Message) -> bool:
"""检查消息是否提到了机器人"""
keywords = [global_config.BOT_NICKNAME]
for keyword in keywords:
if keyword in message.processed_plain_text:
return True
return False
def is_mentioned_bot_in_txt(message: str) -> bool: def is_mentioned_bot_in_txt(message: str) -> bool:
"""检查消息是否提到了机器人""" """检查消息是否提到了机器人"""
keywords = [global_config.BOT_NICKNAME] if global_config.BOT_NICKNAME is None:
for keyword in keywords: return True
if global_config.BOT_NICKNAME in message:
return True
for keyword in global_config.BOT_ALIAS_NAMES:
if keyword in message: if keyword in message:
return True return True
return False return False

View File

@@ -79,7 +79,7 @@ class KnowledgeLibrary:
content = f.read() content = f.read()
# 按1024字符分段 # 按1024字符分段
segments = [content[i:i+600] for i in range(0, len(content), 600)] segments = [content[i:i+600] for i in range(0, len(content), 300)]
# 处理每个分段 # 处理每个分段
for segment in segments: for segment in segments:

View File

@@ -25,26 +25,46 @@ class Memory_graph:
self.db = Database.get_instance() self.db = Database.get_instance()
def connect_dot(self, concept1, concept2): def connect_dot(self, concept1, concept2):
# 如果边已存在,增加 strength # 避免自连接
if concept1 == concept2:
return
current_time = datetime.datetime.now().timestamp()
# 如果边已存在,增加 strength
if self.G.has_edge(concept1, concept2): if self.G.has_edge(concept1, concept2):
self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1 self.G[concept1][concept2]['strength'] = self.G[concept1][concept2].get('strength', 1) + 1
# 更新最后修改时间
self.G[concept1][concept2]['last_modified'] = current_time
else: else:
# 如果是新边初始化 strength 为 1 # 如果是新边,初始化 strength 为 1
self.G.add_edge(concept1, concept2, strength=1) self.G.add_edge(concept1, concept2,
strength=1,
created_time=current_time, # 添加创建时间
last_modified=current_time) # 添加最后修改时间
def add_dot(self, concept, memory): def add_dot(self, concept, memory):
current_time = datetime.datetime.now().timestamp()
if concept in self.G: if concept in self.G:
# 如果节点已存在,将新记忆添加到现有列表中
if 'memory_items' in self.G.nodes[concept]: if 'memory_items' in self.G.nodes[concept]:
if not isinstance(self.G.nodes[concept]['memory_items'], list): if not isinstance(self.G.nodes[concept]['memory_items'], list):
# 如果当前不是列表,将其转换为列表
self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']] self.G.nodes[concept]['memory_items'] = [self.G.nodes[concept]['memory_items']]
self.G.nodes[concept]['memory_items'].append(memory) self.G.nodes[concept]['memory_items'].append(memory)
# 更新最后修改时间
self.G.nodes[concept]['last_modified'] = current_time
else: else:
self.G.nodes[concept]['memory_items'] = [memory] self.G.nodes[concept]['memory_items'] = [memory]
# 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time
if 'created_time' not in self.G.nodes[concept]:
self.G.nodes[concept]['created_time'] = current_time
self.G.nodes[concept]['last_modified'] = current_time
else: else:
# 如果是新节点创建新的记忆列表 # 如果是新节点,创建新的记忆列表
self.G.add_node(concept, memory_items=[memory]) self.G.add_node(concept,
memory_items=[memory],
created_time=current_time, # 添加创建时间
last_modified=current_time) # 添加最后修改时间
def get_dot(self, concept): def get_dot(self, concept):
# 检查节点是否存在于图中 # 检查节点是否存在于图中
@@ -191,15 +211,11 @@ class Hippocampus:
async def memory_compress(self, messages: list, compress_rate=0.1): async def memory_compress(self, messages: list, compress_rate=0.1):
"""压缩消息记录为记忆 """压缩消息记录为记忆
Args:
messages: 消息记录字典列表每个字典包含text和time字段
compress_rate: 压缩率
Returns: Returns:
set: (话题, 记忆) 元组集合 tuple: (压缩记忆集合, 相似主题字典)
""" """
if not messages: if not messages:
return set() return set(), {}
# 合并消息文本,同时保留时间信息 # 合并消息文本,同时保留时间信息
input_text = "" input_text = ""
@@ -246,12 +262,33 @@ class Hippocampus:
# 等待所有任务完成 # 等待所有任务完成
compressed_memory = set() compressed_memory = set()
similar_topics_dict = {} # 存储每个话题的相似主题列表
for topic, task in tasks: for topic, task in tasks:
response = await task response = await task
if response: if response:
compressed_memory.add((topic, response[0])) compressed_memory.add((topic, response[0]))
# 为每个话题查找相似的已存在主题
existing_topics = list(self.memory_graph.G.nodes())
similar_topics = []
for existing_topic in existing_topics:
topic_words = set(jieba.cut(topic))
existing_words = set(jieba.cut(existing_topic))
all_words = topic_words | existing_words
v1 = [1 if word in topic_words else 0 for word in all_words]
v2 = [1 if word in existing_words else 0 for word in all_words]
similarity = cosine_similarity(v1, v2)
if similarity >= 0.6:
similar_topics.append((existing_topic, similarity))
similar_topics.sort(key=lambda x: x[1], reverse=True)
similar_topics = similar_topics[:5]
similar_topics_dict[topic] = similar_topics
return compressed_memory return compressed_memory, similar_topics_dict
def calculate_topic_num(self, text, compress_rate): def calculate_topic_num(self, text, compress_rate):
"""计算文本的话题数量""" """计算文本的话题数量"""
@@ -265,33 +302,40 @@ class Hippocampus:
return topic_num return topic_num
async def operation_build_memory(self, chat_size=20): async def operation_build_memory(self, chat_size=20):
# 最近消息获取频率 time_frequency = {'near': 3, 'mid': 8, 'far': 5}
time_frequency = {'near': 2, 'mid': 4, 'far': 2} memory_samples = self.get_memory_sample(chat_size, time_frequency)
memory_sample = self.get_memory_sample(chat_size, time_frequency)
for i, messages in enumerate(memory_samples, 1):
for i, input_text in enumerate(memory_sample, 1):
# 加载进度可视化
all_topics = [] all_topics = []
progress = (i / len(memory_sample)) * 100 # 加载进度可视化
progress = (i / len(memory_samples)) * 100
bar_length = 30 bar_length = 30
filled_length = int(bar_length * i // len(memory_sample)) filled_length = int(bar_length * i // len(memory_samples))
bar = '' * filled_length + '-' * (bar_length - filled_length) bar = '' * filled_length + '-' * (bar_length - filled_length)
logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_sample)})") logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})")
# 生成压缩后记忆 ,表现为 (话题,记忆) 的元组
compressed_memory = set()
compress_rate = 0.1 compress_rate = 0.1
compressed_memory = await self.memory_compress(input_text, compress_rate) compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate)
logger.info(f"压缩后记忆数量: {len(compressed_memory)}") logger.info(f"压缩后记忆数量: {len(compressed_memory)},似曾相识的话题: {len(similar_topics_dict)}")
# 将记忆加入到图谱中
for topic, memory in compressed_memory: for topic, memory in compressed_memory:
logger.info(f"添加节点: {topic}") logger.info(f"添加节点: {topic}")
self.memory_graph.add_dot(topic, memory) self.memory_graph.add_dot(topic, memory)
all_topics.append(topic) # 收集所有话题 all_topics.append(topic)
# 连接相似的已存在主题
if topic in similar_topics_dict:
similar_topics = similar_topics_dict[topic]
for similar_topic, similarity in similar_topics:
if topic != similar_topic:
strength = int(similarity * 10)
logger.info(f"连接相似节点: {topic}{similar_topic} (强度: {strength})")
self.memory_graph.G.add_edge(topic, similar_topic, strength=strength)
# 连接同批次的相关话题
for i in range(len(all_topics)): for i in range(len(all_topics)):
for j in range(i + 1, len(all_topics)): for j in range(i + 1, len(all_topics)):
logger.info(f"连接节点: {all_topics[i]}{all_topics[j]}") logger.info(f"连接同批次节点: {all_topics[i]}{all_topics[j]}")
self.memory_graph.connect_dot(all_topics[i], all_topics[j]) self.memory_graph.connect_dot(all_topics[i], all_topics[j])
self.sync_memory_to_db() self.sync_memory_to_db()
@@ -302,7 +346,7 @@ class Hippocampus:
db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find()) db_nodes = list(self.memory_graph.db.db.graph_data.nodes.find())
memory_nodes = list(self.memory_graph.G.nodes(data=True)) memory_nodes = list(self.memory_graph.G.nodes(data=True))
# 转换数据库节点为字典格式方便查找 # 转换数据库节点为字典格式,方便查找
db_nodes_dict = {node['concept']: node for node in db_nodes} db_nodes_dict = {node['concept']: node for node in db_nodes}
# 检查并更新节点 # 检查并更新节点
@@ -313,13 +357,19 @@ class Hippocampus:
# 计算内存中节点的特征值 # 计算内存中节点的特征值
memory_hash = self.calculate_node_hash(concept, memory_items) memory_hash = self.calculate_node_hash(concept, memory_items)
# 获取时间信息
created_time = data.get('created_time', datetime.datetime.now().timestamp())
last_modified = data.get('last_modified', datetime.datetime.now().timestamp())
if concept not in db_nodes_dict: if concept not in db_nodes_dict:
# 数据库中缺少的节点添加 # 数据库中缺少的节点,添加
node_data = { node_data = {
'concept': concept, 'concept': concept,
'memory_items': memory_items, 'memory_items': memory_items,
'hash': memory_hash 'hash': memory_hash,
'created_time': created_time,
'last_modified': last_modified
} }
self.memory_graph.db.db.graph_data.nodes.insert_one(node_data) self.memory_graph.db.db.graph_data.nodes.insert_one(node_data)
else: else:
@@ -327,25 +377,21 @@ class Hippocampus:
db_node = db_nodes_dict[concept] db_node = db_nodes_dict[concept]
db_hash = db_node.get('hash', None) db_hash = db_node.get('hash', None)
# 如果特征值不同则更新节点 # 如果特征值不同,则更新节点
if db_hash != memory_hash: if db_hash != memory_hash:
self.memory_graph.db.db.graph_data.nodes.update_one( self.memory_graph.db.db.graph_data.nodes.update_one(
{'concept': concept}, {'concept': concept},
{'$set': { {'$set': {
'memory_items': memory_items, 'memory_items': memory_items,
'hash': memory_hash 'hash': memory_hash,
'created_time': created_time,
'last_modified': last_modified
}} }}
) )
# 检查并删除数据库中多余的节点
memory_concepts = set(node[0] for node in memory_nodes)
for db_node in db_nodes:
if db_node['concept'] not in memory_concepts:
self.memory_graph.db.db.graph_data.nodes.delete_one({'concept': db_node['concept']})
# 处理边的信息 # 处理边的信息
db_edges = list(self.memory_graph.db.db.graph_data.edges.find()) db_edges = list(self.memory_graph.db.db.graph_data.edges.find())
memory_edges = list(self.memory_graph.G.edges()) memory_edges = list(self.memory_graph.G.edges(data=True))
# 创建边的哈希值字典 # 创建边的哈希值字典
db_edge_dict = {} db_edge_dict = {}
@@ -357,10 +403,14 @@ class Hippocampus:
} }
# 检查并更新边 # 检查并更新边
for source, target in memory_edges: for source, target, data in memory_edges:
edge_hash = self.calculate_edge_hash(source, target) edge_hash = self.calculate_edge_hash(source, target)
edge_key = (source, target) edge_key = (source, target)
strength = self.memory_graph.G[source][target].get('strength', 1) strength = data.get('strength', 1)
# 获取边的时间信息
created_time = data.get('created_time', datetime.datetime.now().timestamp())
last_modified = data.get('last_modified', datetime.datetime.now().timestamp())
if edge_key not in db_edge_dict: if edge_key not in db_edge_dict:
# 添加新边 # 添加新边
@@ -368,7 +418,9 @@ class Hippocampus:
'source': source, 'source': source,
'target': target, 'target': target,
'strength': strength, 'strength': strength,
'hash': edge_hash 'hash': edge_hash,
'created_time': created_time,
'last_modified': last_modified
} }
self.memory_graph.db.db.graph_data.edges.insert_one(edge_data) self.memory_graph.db.db.graph_data.edges.insert_one(edge_data)
else: else:
@@ -378,20 +430,12 @@ class Hippocampus:
{'source': source, 'target': target}, {'source': source, 'target': target},
{'$set': { {'$set': {
'hash': edge_hash, 'hash': edge_hash,
'strength': strength 'strength': strength,
'created_time': created_time,
'last_modified': last_modified
}} }}
) )
# 删除多余的边
memory_edge_set = set(memory_edges)
for edge_key in db_edge_dict:
if edge_key not in memory_edge_set:
source, target = edge_key
self.memory_graph.db.db.graph_data.edges.delete_one({
'source': source,
'target': target
})
def sync_memory_from_db(self): def sync_memory_from_db(self):
"""从数据库同步数据到内存中的图结构""" """从数据库同步数据到内存中的图结构"""
# 清空当前图 # 清空当前图
@@ -405,61 +449,107 @@ class Hippocampus:
# 确保memory_items是列表 # 确保memory_items是列表
if not isinstance(memory_items, list): if not isinstance(memory_items, list):
memory_items = [memory_items] if memory_items else [] memory_items = [memory_items] if memory_items else []
# 获取时间信息
created_time = node.get('created_time', datetime.datetime.now().timestamp())
last_modified = node.get('last_modified', datetime.datetime.now().timestamp())
# 添加节点到图中 # 添加节点到图中
self.memory_graph.G.add_node(concept, memory_items=memory_items) self.memory_graph.G.add_node(concept,
memory_items=memory_items,
created_time=created_time,
last_modified=last_modified)
# 从数据库加载所有边 # 从数据库加载所有边
edges = self.memory_graph.db.db.graph_data.edges.find() edges = self.memory_graph.db.db.graph_data.edges.find()
for edge in edges: for edge in edges:
source = edge['source'] source = edge['source']
target = edge['target'] target = edge['target']
strength = edge.get('strength', 1) # 获取 strength默认为 1 strength = edge.get('strength', 1) # 获取 strength,默认为 1
# 获取时间信息
created_time = edge.get('created_time', datetime.datetime.now().timestamp())
last_modified = edge.get('last_modified', datetime.datetime.now().timestamp())
# 只有当源节点和目标节点都存在时才添加边 # 只有当源节点和目标节点都存在时才添加边
if source in self.memory_graph.G and target in self.memory_graph.G: if source in self.memory_graph.G and target in self.memory_graph.G:
self.memory_graph.G.add_edge(source, target, strength=strength) self.memory_graph.G.add_edge(source, target,
strength=strength,
created_time=created_time,
last_modified=last_modified)
async def operation_forget_topic(self, percentage=0.1): async def operation_forget_topic(self, percentage=0.1):
"""随机选择图中一定比例的节点进行检查根据条件决定是否遗忘""" """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘"""
# 获取所有节点
all_nodes = list(self.memory_graph.G.nodes()) all_nodes = list(self.memory_graph.G.nodes())
# 计算要检查的节点数量 all_edges = list(self.memory_graph.G.edges())
check_count = max(1, int(len(all_nodes) * percentage))
# 随机选择节点 check_nodes_count = max(1, int(len(all_nodes) * percentage))
nodes_to_check = random.sample(all_nodes, check_count) check_edges_count = max(1, int(len(all_edges) * percentage))
forgotten_nodes = [] nodes_to_check = random.sample(all_nodes, check_nodes_count)
edges_to_check = random.sample(all_edges, check_edges_count)
edge_changes = {'weakened': 0, 'removed': 0}
node_changes = {'reduced': 0, 'removed': 0}
current_time = datetime.datetime.now().timestamp()
# 检查并遗忘连接
logger.info("开始检查连接...")
for source, target in edges_to_check:
edge_data = self.memory_graph.G[source][target]
last_modified = edge_data.get('last_modified')
# print(source,target)
# print(f"float(last_modified):{float(last_modified)}" )
# print(f"current_time:{current_time}")
# print(f"current_time - last_modified:{current_time - last_modified}")
if current_time - last_modified > 3600*24: # test
current_strength = edge_data.get('strength', 1)
new_strength = current_strength - 1
if new_strength <= 0:
self.memory_graph.G.remove_edge(source, target)
edge_changes['removed'] += 1
logger.info(f"\033[1;31m[连接移除]\033[0m {source} - {target}")
else:
edge_data['strength'] = new_strength
edge_data['last_modified'] = current_time
edge_changes['weakened'] += 1
logger.info(f"\033[1;34m[连接减弱]\033[0m {source} - {target} (强度: {current_strength} -> {new_strength})")
# 检查并遗忘话题
logger.info("开始检查节点...")
for node in nodes_to_check: for node in nodes_to_check:
# 获取节点的连接数 node_data = self.memory_graph.G.nodes[node]
connections = self.memory_graph.G.degree(node) last_modified = node_data.get('last_modified', current_time)
# 获取节点的内容条数 if current_time - last_modified > 3600*24: # test
memory_items = self.memory_graph.G.nodes[node].get('memory_items', []) memory_items = node_data.get('memory_items', [])
if not isinstance(memory_items, list): if not isinstance(memory_items, list):
memory_items = [memory_items] if memory_items else [] memory_items = [memory_items] if memory_items else []
content_count = len(memory_items)
if memory_items:
# 检查连接强度 current_count = len(memory_items)
weak_connections = True removed_item = random.choice(memory_items)
if connections > 1: # 只有当连接数大于1时才检查强度 memory_items.remove(removed_item)
for neighbor in self.memory_graph.G.neighbors(node):
strength = self.memory_graph.G[node][neighbor].get('strength', 1) if memory_items:
if strength > 2: self.memory_graph.G.nodes[node]['memory_items'] = memory_items
weak_connections = False self.memory_graph.G.nodes[node]['last_modified'] = current_time
break node_changes['reduced'] += 1
logger.info(f"\033[1;33m[记忆减少]\033[0m {node} (记忆数量: {current_count} -> {len(memory_items)})")
# 如果满足遗忘条件 else:
if (connections <= 1 and weak_connections) or content_count <= 2: self.memory_graph.G.remove_node(node)
removed_item = self.memory_graph.forget_topic(node) node_changes['removed'] += 1
if removed_item: logger.info(f"\033[1;31m[节点移除]\033[0m {node}")
forgotten_nodes.append((node, removed_item))
logger.debug(f"遗忘节点 {node} 的记忆: {removed_item}") if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()):
# 同步到数据库
if forgotten_nodes:
self.sync_memory_to_db() self.sync_memory_to_db()
logger.debug(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") logger.info("\n遗忘操作统计:")
logger.info(f"连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除")
logger.info(f"节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除")
else: else:
logger.debug("本次检查没有节点满足遗忘条件") logger.info("\n本次检查没有节点或连接满足遗忘条件")
async def merge_memory(self, topic): async def merge_memory(self, topic):
""" """
@@ -486,7 +576,7 @@ class Hippocampus:
logger.debug(f"选择的记忆:\n{merged_text}") logger.debug(f"选择的记忆:\n{merged_text}")
# 使用memory_compress生成新的压缩记忆 # 使用memory_compress生成新的压缩记忆
compressed_memories = await self.memory_compress(selected_memories, 0.1) compressed_memories, _ = await self.memory_compress(selected_memories, 0.1)
# 从原记忆列表中移除被选中的记忆 # 从原记忆列表中移除被选中的记忆
for memory in selected_memories: for memory in selected_memories:

File diff suppressed because it is too large Load Diff

View File

@@ -44,8 +44,8 @@ class LLM_request:
self.db.db.llm_usage.create_index([("model_name", 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([("user_id", 1)])
self.db.db.llm_usage.create_index([("request_type", 1)]) self.db.db.llm_usage.create_index([("request_type", 1)])
except Exception as e: except Exception:
logger.error(f"创建数据库索引失败") logger.error("创建数据库索引失败")
def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, def _record_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int,
user_id: str = "system", request_type: str = "chat", user_id: str = "system", request_type: str = "chat",
@@ -80,7 +80,7 @@ class LLM_request:
f"总计: {total_tokens}" f"总计: {total_tokens}"
) )
except Exception: except Exception:
logger.error(f"记录token使用情况失败") logger.error("记录token使用情况失败")
def _calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float: def _calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
"""计算API调用成本 """计算API调用成本
@@ -194,7 +194,7 @@ class LLM_request:
if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get( if hasattr(global_config, 'llm_normal') and global_config.llm_normal.get(
'name') == old_model_name: 'name') == old_model_name:
global_config.llm_normal['name'] = self.model_name global_config.llm_normal['name'] = self.model_name
logger.warning(f"已将全局配置中的 llm_normal 模型降级") logger.warning("已将全局配置中的 llm_normal 模型降级")
# 更新payload中的模型名 # 更新payload中的模型名
if payload and 'model' in payload: if payload and 'model' in payload:
@@ -227,7 +227,7 @@ class LLM_request:
delta_content = "" delta_content = ""
accumulated_content += delta_content accumulated_content += delta_content
except Exception: except Exception:
logger.exception(f"解析流式输出错") logger.exception("解析流式输出错")
content = accumulated_content content = accumulated_content
reasoning_content = "" reasoning_content = ""
think_match = re.search(r'<think>(.*?)</think>', content, re.DOTALL) think_match = re.search(r'<think>(.*?)</think>', content, re.DOTALL)
@@ -355,7 +355,7 @@ class LLM_request:
"""构建请求头""" """构建请求头"""
if no_key: if no_key:
return { return {
"Authorization": f"Bearer **********", "Authorization": "Bearer **********",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
else: else:

View File

@@ -68,7 +68,7 @@ class ScheduleGenerator:
1. 早上的学习和工作安排 1. 早上的学习和工作安排
2. 下午的活动和任务 2. 下午的活动和任务
3. 晚上的计划和休息时间 3. 晚上的计划和休息时间
请按照时间顺序列出具体时间点和对应的活动用一个时间点而不是时间段来表示时间用JSON格式返回日程表仅返回内容不要返回注释时间采用24小时制格式为{"时间": "活动","时间": "活动",...}。""" 请按照时间顺序列出具体时间点和对应的活动用一个时间点而不是时间段来表示时间用JSON格式返回日程表仅返回内容不要返回注释不要添加任何markdown或代码块样式时间采用24小时制格式为{"时间": "活动","时间": "活动",...}。"""
try: try:
schedule_text, _ = await self.llm_scheduler.generate_response(prompt) schedule_text, _ = await self.llm_scheduler.generate_response(prompt)
@@ -91,7 +91,7 @@ class ScheduleGenerator:
try: try:
schedule_dict = json.loads(schedule_text) schedule_dict = json.loads(schedule_text)
return schedule_dict return schedule_dict
except json.JSONDecodeError as e: except json.JSONDecodeError:
logger.exception("解析日程失败: {}".format(schedule_text)) logger.exception("解析日程失败: {}".format(schedule_text))
return False return False

View File

@@ -155,7 +155,7 @@ class LLMStatistics:
all_stats = self._collect_all_statistics() all_stats = self._collect_all_statistics()
self._save_statistics(all_stats) self._save_statistics(all_stats)
except Exception: except Exception:
logger.exception(f"统计数据处理失败") logger.exception("统计数据处理失败")
# 等待1分钟 # 等待1分钟
for _ in range(60): for _ in range(60):

View File

@@ -15,6 +15,7 @@ version = "0.0.5"
[bot] [bot]
qq = 123 qq = 123
nickname = "麦麦" nickname = "麦麦"
alias_names = ["小麦", "阿麦"]
[personality] [personality]
prompt_personality = [ prompt_personality = [