diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
index 697c4775..9c8cba5d 100644
--- a/.github/workflows/ruff.yml
+++ b/.github/workflows/ruff.yml
@@ -1,9 +1,26 @@
name: Ruff
on: [ push, pull_request ]
+
+permissions:
+ contents: write
+
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ github.head_ref || github.ref_name }}
- uses: astral-sh/ruff-action@v3
+ - run: ruff check --fix
+ - run: ruff format
+ - name: Commit changes
+ if: success()
+ run: |
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
+ git add -A
+ git diff --quiet && git diff --staged --quiet || git commit -m "🤖 自动格式化代码 [skip ci]"
+ git push
diff --git a/.gitignore b/.gitignore
index c2fb389e..3e9b9868 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,8 @@ message_queue_window.bat
message_queue_window.txt
queue_update.txt
memory_graph.gml
+/src/do_tool/tool_can_use/auto_create_tool.py
+/src/do_tool/tool_can_use/execute_python_code_tool.py
.env
.env.*
.cursor
@@ -28,6 +30,9 @@ config/bot_config.toml
config/bot_config.toml.bak
src/plugins/remote/client_uuid.json
run_none.bat
+(测试版)麦麦生成人格.bat
+(临时版)麦麦开始学习.bat
+src/plugins/utils/statistic.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -237,3 +242,4 @@ logs
/config/*
run_none.bat
config/old/bot_config_20250405_212257.toml
+
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..02fe9f82
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,20 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+- **Run Bot**: `python bot.py`
+- **Lint**: `ruff check --fix .` or `ruff format .`
+- **Run Tests**: `python -m unittest discover -v`
+- **Run Single Test**: `python -m unittest src/plugins/message/test.py`
+
+## Code Style
+- **Formatting**: Line length 120 chars, use double quotes for strings
+- **Imports**: Group standard library, external packages, then internal imports
+- **Naming**: snake_case for functions/variables, PascalCase for classes
+- **Error Handling**: Use try/except blocks with specific exceptions
+- **Types**: Use type hints where possible
+- **Docstrings**: Document classes and complex functions
+- **Linting**: Follow ruff rules (E, F, B) with ignores E711, E501
+
+When making changes, run `ruff check --fix .` to ensure code follows style guidelines. The codebase uses Ruff for linting and formatting.
\ No newline at end of file
diff --git a/README.md b/README.md
index fa97fec1..325e3ad2 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,68 @@
# 麦麦!MaiCore-MaiMBot (编辑中)
-
-## 新版0.6.0部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a
-
+
-
-
-
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+
+
+
+
+
+
+
+
+
+ 画师:略nd
+
+
+
MaiBot(麦麦)
+
+ 一款专注于 群组聊天 的赛博网友
+
+ 探索本项目的文档 »
+
+
+
+ 报告Bug
+ ·
+ 提出新特性
+
+
+
+
+## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a
-
## 📝 项目简介
**🍔MaiCore是一个基于大语言模型的可交互智能体**
-- LLM 提供对话能力
-- 动态Prompt构建器
-- 实时的思维系统
-- MongoDB 提供数据持久化支持
-- 可扩展,可支持多种平台和多种功能
-**最新版本: v0.6.0** ([查看更新日志](changelogs/changelog.md))
+- 💭 **智能对话系统**:基于LLM的自然语言交互
+- 🤔 **实时思维系统**:模拟人类思考过程
+- 💝 **情感表达系统**:丰富的表情包和情绪表达
+- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储
+- 🔄 **动态人格系统**:自适应的性格特征
+
+
+
+
+### 📢 版本信息
+
+**最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md))
> [!WARNING]
> 请阅读教程后更新!!!!!!!
> 请阅读教程后更新!!!!!!!
@@ -28,19 +70,12 @@
> 次版本MaiBot将基于MaiCore运行,不再依赖于nonebot相关组件运行。
> MaiBot将通过nonebot的插件与nonebot建立联系,然后nonebot与QQ建立联系,实现MaiBot与QQ的交互
-**分支介绍:**
-- main 稳定版本
-- dev 开发版(不知道什么意思就别下)
-- classical 0.6.0以前的版本
+**分支说明:**
+- `main`: 稳定发布版本
+- `dev`: 开发测试版本(不知道什么意思就别下)
+- `classical`: 0.6.0之前的版本
-
> [!WARNING]
> - 项目处于活跃开发阶段,代码可能随时更改
@@ -49,6 +84,12 @@
> - 由于持续迭代,可能存在一些已知或未知的bug
> - 由于开发中,可能消耗较多token
+### ⚠️ 重要提示
+
+- 升级到v0.6.x版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a)
+- 本版本基于MaiCore重构,通过nonebot插件与QQ平台交互
+- 项目处于活跃开发阶段,功能和API可能随时调整
+
### 💬交流群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码
- [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779
- [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 【已满】
@@ -67,60 +108,41 @@
- [📚 核心Wiki文档](https://docs.mai-mai.org) - 项目最全面的文档中心,你可以了解麦麦有关的一切
### 最新版本部署教程(MaiCore版本)
-- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy.html) - 基于MaiCore的新版本部署方式(与旧版本不兼容)
+- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于MaiCore的新版本部署方式(与旧版本不兼容)
## 🎯 功能介绍
-### 💬 聊天功能
-- 提供思维流(心流)聊天和推理聊天两种对话逻辑
-- 支持关键词检索主动发言:对消息的话题topic进行识别,如果检测到麦麦存储过的话题就会主动进行发言
-- 支持bot名字呼唤发言:检测到"麦麦"会主动发言,可配置
-- 支持多模型,多厂商自定义配置
-- 动态的prompt构建器,更拟人
-- 支持图片,转发消息,回复消息的识别
-- 支持私聊功能,可使用PFC模式的有目的多轮对话(实验性)
+| 模块 | 主要功能 | 特点 |
+|------|---------|------|
+| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 |
+| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 |
+| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 |
+| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 |
+| 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 |
+| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 |
+| 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 |
+| 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 |
+| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 |
-### 🧠 思维流系统
-- 思维流能够在回复前后进行思考,生成实时想法
-- 思维流自动启停机制,提升资源利用效率
-- 思维流与日程系统联动,实现动态日程生成
+## 📐 项目架构
-### 🧠 记忆系统 2.0
-- 优化记忆抽取策略和prompt结构
-- 改进海马体记忆提取机制,提升自然度
-- 对聊天记录进行概括存储,在需要时调用
+```mermaid
+graph TD
+ A[MaiCore] --> B[对话系统]
+ A --> C[心流系统]
+ A --> D[记忆系统]
+ A --> E[情感系统]
+ B --> F[多模型支持]
+ B --> G[动态Prompt]
+ C --> H[实时思考]
+ C --> I[日程联动]
+ D --> J[记忆存储]
+ D --> K[记忆检索]
+ E --> L[表情管理]
+ E --> M[情绪识别]
+```
-### 😊 表情包系统
-- 支持根据发言内容发送对应情绪的表情包
-- 支持识别和处理gif表情包
-- 会自动偷群友的表情包
-- 表情包审查功能
-- 表情包文件完整性自动检查
-- 自动清理缓存图片
-
-### 📅 日程系统
-- 动态更新的日程生成
-- 可自定义想象力程度
-- 与聊天情况交互(思维流模式下)
-
-### 👥 关系系统 2.0
-- 优化关系管理系统,适用于新版本
-- 提供更丰富的关系接口
-- 针对每个用户创建"关系",实现个性化回复
-
-### 📊 统计系统
-- 详细的使用数据统计
-- LLM调用统计
-- 在控制台显示统计信息
-
-### 🔧 系统功能
-- 支持优雅的shutdown机制
-- 自动保存功能,定期保存聊天记录和关系数据
-- 完善的异常处理机制
-- 可自定义时区设置
-- 优化的日志输出格式
-- 配置自动更新功能
## 开发计划TODO:LIST
@@ -157,7 +179,6 @@ MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献,
## 致谢
-- [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架
- [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现
### 贡献者
diff --git a/bot.py b/bot.py
index a0bf3a3c..653efd45 100644
--- a/bot.py
+++ b/bot.py
@@ -7,11 +7,16 @@ from pathlib import Path
import time
import platform
from dotenv import load_dotenv
-from src.common.logger import get_module_logger
+from src.common.logger import get_module_logger, LogConfig, CONFIRM_STYLE_CONFIG
+from src.common.crash_logger import install_crash_handler
from src.main import MainSystem
logger = get_module_logger("main_bot")
-
+confirm_logger_config = LogConfig(
+ console_format=CONFIRM_STYLE_CONFIG["console_format"],
+ file_format=CONFIRM_STYLE_CONFIG["file_format"],
+)
+confirm_logger = get_module_logger("confirm", config=confirm_logger_config)
# 获取没有加载env时的环境变量
env_mask = {key: os.getenv(key) for key in os.environ}
@@ -165,8 +170,8 @@ def check_eula():
# 如果EULA或隐私条款有更新,提示用户重新确认
if eula_updated or privacy_updated:
- print("EULA或隐私条款内容已更新,请在阅读后重新确认,继续运行视为同意更新后的以上两款协议")
- print(
+ confirm_logger.critical("EULA或隐私条款内容已更新,请在阅读后重新确认,继续运行视为同意更新后的以上两款协议")
+ confirm_logger.critical(
f'输入"同意"或"confirmed"或设置环境变量"EULA_AGREE={eula_new_hash}"和"PRIVACY_AGREE={privacy_new_hash}"继续运行'
)
while True:
@@ -175,14 +180,14 @@ def check_eula():
# print("确认成功,继续运行")
# print(f"确认成功,继续运行{eula_updated} {privacy_updated}")
if eula_updated:
- print(f"更新EULA确认文件{eula_new_hash}")
+ logger.info(f"更新EULA确认文件{eula_new_hash}")
eula_confirm_file.write_text(eula_new_hash, encoding="utf-8")
if privacy_updated:
- print(f"更新隐私条款确认文件{privacy_new_hash}")
+ logger.info(f"更新隐私条款确认文件{privacy_new_hash}")
privacy_confirm_file.write_text(privacy_new_hash, encoding="utf-8")
break
else:
- print('请输入"同意"或"confirmed"以继续运行')
+ confirm_logger.critical('请输入"同意"或"confirmed"以继续运行')
return
elif eula_confirmed and privacy_confirmed:
return
@@ -193,6 +198,9 @@ def raw_main():
if platform.system().lower() != "windows":
time.tzset()
+ # 安装崩溃日志处理器
+ install_crash_handler()
+
check_eula()
print("检查EULA和隐私条款完成")
easter_egg()
diff --git a/changelogs/changelog.md b/changelogs/changelog.md
index 6b9898b5..0ddb486b 100644
--- a/changelogs/changelog.md
+++ b/changelogs/changelog.md
@@ -1,5 +1,58 @@
# Changelog
+## [0.6.2] - 2025-4-14
+
+### 摘要
+- MaiBot 0.6.2 版本发布!
+- 优化了心流的观察系统,优化提示词和表现,现在心流表现更好!
+- 新增工具调用能力,可以更好地获取信息
+- 本次更新主要围绕工具系统、心流系统、消息处理和代码优化展开,新增多个工具类,优化了心流系统的逻辑,改进了消息处理流程,并修复了多个问题。
+
+### 🌟 核心功能增强
+#### 工具系统
+- 新增了知识获取工具系统,支持通过心流调用获取多种知识
+- 新增了工具系统使用指南,详细说明工具结构、自动注册机制和添加步骤
+- 新增了多个实用工具类,包括心情调整工具`ChangeMoodTool`、关系查询工具`RelationshipTool`、数值比较工具`CompareNumbersTool`、日程获取工具`GetCurrentTaskTool`、上下文压缩工具`CompressContextTool`和知识获取工具`GetKnowledgeTool`
+- 更新了`ToolUser`类,支持自动获取已注册工具定义并调用`execute`方法
+- 需要配置支持工具调用的模型才能使用完整功能
+
+#### 心流系统
+- 新增了上下文压缩缓存功能,可以有更持久的记忆
+- 新增了心流系统的README.md文件,详细介绍了系统架构、主要功能和工作流程。
+- 优化了心流系统的逻辑,包括子心流自动清理和合理配置更新间隔。
+- 改进了心流观察系统,优化了提示词设计和系统表现,使心流运行更加稳定高效。
+- 更新了`Heartflow`类的方法和属性,支持异步生成提示词并提升生成质量。
+
+#### 消息处理
+- 改进了消息处理流程,包括回复检查、消息生成和发送逻辑。
+- 新增了`ReplyGenerator`类,用于根据观察信息和对话信息生成回复。
+- 优化了消息队列管理系统,支持按时间顺序处理消息。
+
+#### 现在可以启用更好的表情包发送系统
+
+### 💻 系统架构优化
+
+#### 部署支持
+- 更新了Docker部署文档,优化了服务配置和挂载路径。
+- 完善了Linux和Windows脚本支持。
+
+### 🐛 问题修复
+- 修复了消息处理器中的正则表达式匹配问题。
+- 修复了图像处理中的帧大小和拼接问题。
+- 修复了私聊时产生`reply`消息的bug。
+- 修复了配置文件加载时的版本兼容性问题。
+
+### 📚 文档更新
+- 更新了`README.md`文件,包括Python版本要求和协议信息。
+- 新增了工具系统和心流系统的详细文档。
+- 优化了部署相关文档的完整性。
+
+### 🔧 其他改进
+- 新增了崩溃日志记录器,记录崩溃信息到日志文件。
+- 优化了统计信息输出,在控制台显示详细统计信息。
+- 改进了异常处理机制,提升系统稳定性。
+- 现可配置部分模型的temp参数
+
## [0.6.0] - 2025-4-4
### 摘要
diff --git a/changelogs/changelog_config.md b/changelogs/changelog_config.md
index 32912f69..e438ea31 100644
--- a/changelogs/changelog_config.md
+++ b/changelogs/changelog_config.md
@@ -22,7 +22,7 @@
## [0.0.11] - 2025-3-12
### Added
- 新增了 `schedule` 配置项,用于配置日程表生成功能
-- 新增了 `response_spliter` 配置项,用于控制回复分割
+- 新增了 `response_splitter` 配置项,用于控制回复分割
- 新增了 `experimental` 配置项,用于实验性功能开关
- 新增了 `llm_observation` 和 `llm_sub_heartflow` 模型配置
- 新增了 `llm_heartflow` 模型配置
diff --git a/depends-data/maimai.png b/depends-data/maimai.png
new file mode 100644
index 00000000..faccb856
Binary files /dev/null and b/depends-data/maimai.png differ
diff --git a/depends-data/video.png b/depends-data/video.png
new file mode 100644
index 00000000..84176b2d
Binary files /dev/null and b/depends-data/video.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
index bde382e5..000d00c3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,17 +1,14 @@
services:
adapters:
container_name: maim-bot-adapters
- image: maple127667/maimbot-adapter:latest
+ image: unclas/maimbot-adapter:latest
# image: infinitycat/maimbot-adapter:latest
environment:
- TZ=Asia/Shanghai
- ports:
- - "18002:18002"
+# ports:
+# - "8095:8095"
volumes:
- - ./docker-config/adapters/config.py:/adapters/src/plugins/nonebot_plugin_maibot_adapters/config.py # 持久化adapters配置文件
- - ./docker-config/adapters/.env:/adapters/.env # 持久化adapters配置文件
- - ./data/qq:/app/.config/QQ # 持久化QQ本体并同步qq表情和图片到adapters
- - ./data/MaiMBot:/adapters/data
+ - ./docker-config/adapters/config.toml:/adapters/config.toml
restart: always
depends_on:
- mongodb
@@ -25,8 +22,8 @@ services:
- TZ=Asia/Shanghai
# - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA
# - PRIVACY_AGREE=2402af06e133d2d10d9c6c643fdc9333 # 同意EULA
- ports:
- - "8000:8000"
+# ports:
+# - "8000:8000"
volumes:
- ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件
- ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
@@ -42,8 +39,8 @@ services:
- TZ=Asia/Shanghai
# - MONGO_INITDB_ROOT_USERNAME=your_username # 此处配置mongo用户
# - MONGO_INITDB_ROOT_PASSWORD=your_password # 此处配置mongo密码
- ports:
- - "27017:27017"
+# ports:
+# - "27017:27017"
restart: always
volumes:
- mongodb:/data/db # 持久化mongodb数据
@@ -58,11 +55,10 @@ services:
- TZ=Asia/Shanghai
ports:
- "6099:6099"
- - "8095:8095"
volumes:
- ./docker-config/napcat:/app/napcat/config # 持久化napcat配置文件
- ./data/qq:/app/.config/QQ # 持久化QQ本体并同步qq表情和图片到adapters
- - ./data/MaiMBot:/adapters/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题
+ - ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题
container_name: maim-bot-napcat
restart: always
image: mlikiowa/napcat-docker:latest
diff --git a/requirements.txt b/requirements.txt
index ada41d29..0fcb31f8 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/scripts/run.sh b/scripts/run.sh
index 342a23fe..b7ecbc84 100644
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -1,10 +1,10 @@
#!/bin/bash
-# MaiCore & Nonebot adapter一键安装脚本 by Cookie_987
+# MaiCore & NapCat Adapter一键安装脚本 by Cookie_987
# 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9
# 请小心使用任何一键脚本!
-INSTALLER_VERSION="0.0.2-refactor"
+INSTALLER_VERSION="0.0.3-refactor"
LANG=C.UTF-8
# 如无法访问GitHub请修改此处镜像地址
@@ -31,7 +31,7 @@ DEFAULT_INSTALL_DIR="/opt/maicore"
# 服务名称
SERVICE_NAME="maicore"
SERVICE_NAME_WEB="maicore-web"
-SERVICE_NAME_NBADAPTER="maicore-nonebot-adapter"
+SERVICE_NAME_NBADAPTER="maibot-napcat-adapter"
IS_INSTALL_MONGODB=false
IS_INSTALL_NAPCAT=false
@@ -59,9 +59,9 @@ show_menu() {
"1" "启动MaiCore" \
"2" "停止MaiCore" \
"3" "重启MaiCore" \
- "4" "启动Nonebot adapter" \
- "5" "停止Nonebot adapter" \
- "6" "重启Nonebot adapter" \
+ "4" "启动NapCat Adapter" \
+ "5" "停止NapCat Adapter" \
+ "6" "重启NapCat Adapter" \
"7" "拉取最新MaiCore仓库" \
"8" "切换分支" \
"9" "退出" 3>&1 1>&2 2>&3)
@@ -83,15 +83,15 @@ show_menu() {
;;
4)
systemctl start ${SERVICE_NAME_NBADAPTER}
- whiptail --msgbox "✅Nonebot adapter已启动" 10 60
+ whiptail --msgbox "✅NapCat Adapter已启动" 10 60
;;
5)
systemctl stop ${SERVICE_NAME_NBADAPTER}
- whiptail --msgbox "🛑Nonebot adapter已停止" 10 60
+ whiptail --msgbox "🛑NapCat Adapter已停止" 10 60
;;
6)
systemctl restart ${SERVICE_NAME_NBADAPTER}
- whiptail --msgbox "🔄Nonebot adapter已重启" 10 60
+ whiptail --msgbox "🔄NapCat Adapter已重启" 10 60
;;
7)
update_dependencies
@@ -357,8 +357,8 @@ run_installation() {
# Python版本检查
check_python() {
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
- if ! python3 -c "import sys; exit(0) if sys.version_info >= (3,9) else exit(1)"; then
- whiptail --title "⚠️ [4/6] Python 版本过低" --msgbox "检测到 Python 版本为 $PYTHON_VERSION,需要 3.9 或以上!\n请升级 Python 后重新运行本脚本。" 10 60
+ if ! python3 -c "import sys; exit(0) if sys.version_info >= (3,10) else exit(1)"; then
+ whiptail --title "⚠️ [4/6] Python 版本过低" --msgbox "检测到 Python 版本为 $PYTHON_VERSION,需要 3.10 或以上!\n请升级 Python 后重新运行本脚本。" 10 60
exit 1
fi
}
@@ -410,7 +410,7 @@ run_installation() {
# 确认安装
confirm_install() {
local confirm_msg="请确认以下更改:\n\n"
- confirm_msg+="📂 安装MaiCore、Nonebot Adapter到: $INSTALL_DIR\n"
+ confirm_msg+="📂 安装MaiCore、NapCat Adapter到: $INSTALL_DIR\n"
confirm_msg+="🔀 分支: $BRANCH\n"
[[ $IS_INSTALL_DEPENDENCIES == true ]] && confirm_msg+="📦 安装依赖:${missing_packages[@]}\n"
[[ $IS_INSTALL_MONGODB == true || $IS_INSTALL_NAPCAT == true ]] && confirm_msg+="📦 安装额外组件:\n"
@@ -499,50 +499,28 @@ EOF
}
echo -e "${GREEN}克隆 nonebot-plugin-maibot-adapters 仓库...${RESET}"
- git clone $GITHUB_REPO/MaiM-with-u/nonebot-plugin-maibot-adapters.git || {
- echo -e "${RED}克隆 nonebot-plugin-maibot-adapters 仓库失败!${RESET}"
+ git clone $GITHUB_REPO/MaiM-with-u/MaiBot-Napcat-Adapter.git || {
+ echo -e "${RED}克隆 MaiBot-Napcat-Adapter.git 仓库失败!${RESET}"
exit 1
}
echo -e "${GREEN}安装Python依赖...${RESET}"
pip install -r MaiBot/requirements.txt
- pip install nb-cli
- pip install nonebot-adapter-onebot
- pip install 'nonebot2[fastapi]'
+ cd MaiBot
+ pip install uv
+ uv pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt
+ cd ..
echo -e "${GREEN}安装maim_message依赖...${RESET}"
cd maim_message
- pip install -e .
+ uv pip install -i https://mirrors.aliyun.com/pypi/simple -e .
cd ..
- echo -e "${GREEN}部署Nonebot adapter...${RESET}"
- cd MaiBot
- mkdir nonebot-maibot-adapter
- cd nonebot-maibot-adapter
- cat > pyproject.toml <=3.9, <4.0"
-
-[tool.nonebot]
-adapters = [
- { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }
-]
-plugins = []
-plugin_dirs = ["src/plugins"]
-builtin_plugins = []
-EOF
-
- echo "Manually created by run.sh" > README.md
- mkdir src
- cp -r ../../nonebot-plugin-maibot-adapters/nonebot_plugin_maibot_adapters src/plugins/nonebot_plugin_maibot_adapters
+ echo -e "${GREEN}部署MaiBot Napcat Adapter...${RESET}"
+ cd MaiBot-Napcat-Adapter
+ uv pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt
cd ..
- cd ..
-
echo -e "${GREEN}同意协议...${RESET}"
@@ -590,13 +568,13 @@ EOF
cat > /etc/systemd/system/${SERVICE_NAME_NBADAPTER}.service < /etc/maicore_install.conf
diff --git a/src/common/crash_logger.py b/src/common/crash_logger.py
new file mode 100644
index 00000000..d1e4fb51
--- /dev/null
+++ b/src/common/crash_logger.py
@@ -0,0 +1,69 @@
+import sys
+import traceback
+import logging
+from pathlib import Path
+from logging.handlers import RotatingFileHandler
+
+
+def setup_crash_logger():
+ """设置崩溃日志记录器"""
+ # 创建logs/crash目录(如果不存在)
+ crash_log_dir = Path("logs/crash")
+ crash_log_dir.mkdir(parents=True, exist_ok=True)
+
+ # 创建日志记录器
+ crash_logger = logging.getLogger("crash_logger")
+ crash_logger.setLevel(logging.ERROR)
+
+ # 设置日志格式
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s\n异常类型: %(exc_info)s\n详细信息:\n%(message)s\n-------------------\n"
+ )
+
+ # 创建按大小轮转的文件处理器(最大10MB,保留5个备份)
+ log_file = crash_log_dir / "crash.log"
+ file_handler = RotatingFileHandler(
+ log_file,
+ maxBytes=10 * 1024 * 1024, # 10MB
+ backupCount=5,
+ encoding="utf-8",
+ )
+ file_handler.setFormatter(formatter)
+ crash_logger.addHandler(file_handler)
+
+ return crash_logger
+
+
+def log_crash(exc_type, exc_value, exc_traceback):
+ """记录崩溃信息到日志文件"""
+ if exc_type is None:
+ return
+
+ # 获取崩溃日志记录器
+ crash_logger = logging.getLogger("crash_logger")
+
+ # 获取完整的异常堆栈信息
+ stack_trace = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
+
+ # 记录崩溃信息
+ crash_logger.error(stack_trace, exc_info=(exc_type, exc_value, exc_traceback))
+
+
+def install_crash_handler():
+ """安装全局异常处理器"""
+ # 设置崩溃日志记录器
+ setup_crash_logger()
+
+ # 保存原始的异常处理器
+ original_hook = sys.excepthook
+
+ def exception_handler(exc_type, exc_value, exc_traceback):
+ """全局异常处理器"""
+ # 记录崩溃信息
+ log_crash(exc_type, exc_value, exc_traceback)
+
+ # 调用原始的异常处理器
+ original_hook(exc_type, exc_value, exc_traceback)
+
+ # 设置全局异常处理器
+ sys.excepthook = exception_handler
diff --git a/src/common/logger.py b/src/common/logger.py
index 9e118622..7365e34a 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -102,10 +102,28 @@ MOOD_STYLE_CONFIG = {
"file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"),
},
"simple": {
- "console_format": ("{time:MM-DD HH:mm} | 心情 | {message}"),
+ "console_format": ("{time:MM-DD HH:mm} | 心情 | {message}"),
"file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"),
},
}
+# tool use
+TOOL_USE_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "{extra[module]: <12} | "
+ "工具使用 | "
+ "{message}"
+ ),
+ "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 工具使用 | {message}"),
+ },
+ "simple": {
+ "console_format": ("{time:MM-DD HH:mm} | 工具使用 | {message}"),
+ "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 工具使用 | {message}"),
+ },
+}
+
# relationship
RELATION_STYLE_CONFIG = {
@@ -283,13 +301,15 @@ WILLING_STYLE_CONFIG = {
"file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 意愿 | {message}"),
},
"simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 意愿 | {message}"
- ), # noqa: E501
+ "console_format": ("{time:MM-DD HH:mm} | 意愿 | {message}"), # noqa: E501
"file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 意愿 | {message}"),
},
}
+CONFIRM_STYLE_CONFIG = {
+ "console_format": ("{message}"), # noqa: E501
+ "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}"),
+}
# 根据SIMPLE_OUTPUT选择配置
MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY_STYLE_CONFIG["advanced"]
@@ -306,6 +326,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = (
) # noqa: E501
WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"]
CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"]
+TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"]
def is_registered_module(record: dict) -> bool:
diff --git a/src/common/server.py b/src/common/server.py
new file mode 100644
index 00000000..a4998a30
--- /dev/null
+++ b/src/common/server.py
@@ -0,0 +1,73 @@
+from fastapi import FastAPI, APIRouter
+from typing import Optional
+from uvicorn import Config, Server as UvicornServer
+import os
+
+
+class Server:
+ def __init__(self, host: Optional[str] = None, port: Optional[int] = None, app_name: str = "MaiMCore"):
+ self.app = FastAPI(title=app_name)
+ self._host: str = "127.0.0.1"
+ self._port: int = 8080
+ self._server: Optional[UvicornServer] = None
+ self.set_address(host, port)
+
+ def register_router(self, router: APIRouter, prefix: str = ""):
+ """注册路由
+
+ APIRouter 用于对相关的路由端点进行分组和模块化管理:
+ 1. 可以将相关的端点组织在一起,便于管理
+ 2. 支持添加统一的路由前缀
+ 3. 可以为一组路由添加共同的依赖项、标签等
+
+ 示例:
+ router = APIRouter()
+
+ @router.get("/users")
+ def get_users():
+ return {"users": [...]}
+
+ @router.post("/users")
+ def create_user():
+ return {"msg": "user created"}
+
+ # 注册路由,添加前缀 "/api/v1"
+ server.register_router(router, prefix="/api/v1")
+ """
+ self.app.include_router(router, prefix=prefix)
+
+ def set_address(self, host: Optional[str] = None, port: Optional[int] = None):
+ """设置服务器地址和端口"""
+ if host:
+ self._host = host
+ if port:
+ self._port = port
+
+ async def run(self):
+ """启动服务器"""
+ config = Config(app=self.app, host=self._host, port=self._port)
+ self._server = UvicornServer(config=config)
+ try:
+ await self._server.serve()
+ except KeyboardInterrupt:
+ await self.shutdown()
+ raise
+ except Exception as e:
+ await self.shutdown()
+ raise RuntimeError(f"服务器运行错误: {str(e)}") from e
+ finally:
+ await self.shutdown()
+
+ async def shutdown(self):
+ """安全关闭服务器"""
+ if self._server:
+ self._server.should_exit = True
+ await self._server.shutdown()
+ self._server = None
+
+ def get_app(self) -> FastAPI:
+ """获取 FastAPI 实例"""
+ return self.app
+
+
+global_server = Server(host=os.environ["HOST"], port=int(os.environ["PORT"]))
diff --git a/src/do_tool/tool_can_use/README.md b/src/do_tool/tool_can_use/README.md
new file mode 100644
index 00000000..15c77188
--- /dev/null
+++ b/src/do_tool/tool_can_use/README.md
@@ -0,0 +1,102 @@
+# 工具系统使用指南
+
+## 概述
+
+`tool_can_use` 是一个插件式工具系统,允许轻松扩展和注册新工具。每个工具作为独立的文件存在于该目录下,系统会自动发现和注册这些工具。
+
+## 工具结构
+
+每个工具应该继承 `BaseTool` 基类并实现必要的属性和方法:
+
+```python
+from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool
+
+class MyNewTool(BaseTool):
+ # 工具名称,必须唯一
+ name = "my_new_tool"
+
+ # 工具描述,告诉LLM这个工具的用途
+ description = "这是一个新工具,用于..."
+
+ # 工具参数定义,遵循JSONSchema格式
+ parameters = {
+ "type": "object",
+ "properties": {
+ "param1": {
+ "type": "string",
+ "description": "参数1的描述"
+ },
+ "param2": {
+ "type": "integer",
+ "description": "参数2的描述"
+ }
+ },
+ "required": ["param1"] # 必需的参数列表
+ }
+
+ async def execute(self, function_args, message_txt=""):
+ """执行工具逻辑
+
+ Args:
+ function_args: 工具调用参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 包含执行结果的字典,必须包含name和content字段
+ """
+ # 实现工具逻辑
+ result = f"工具执行结果: {function_args.get('param1')}"
+
+ return {
+ "name": self.name,
+ "content": result
+ }
+
+# 注册工具
+register_tool(MyNewTool)
+```
+
+## 自动注册机制
+
+工具系统通过以下步骤自动注册工具:
+
+1. 在`__init__.py`中,`discover_tools()`函数会自动遍历当前目录中的所有Python文件
+2. 对于每个文件,系统会寻找继承自`BaseTool`的类
+3. 这些类会被自动注册到工具注册表中
+
+只要确保在每个工具文件的末尾调用`register_tool(YourToolClass)`,工具就会被自动注册。
+
+## 添加新工具步骤
+
+1. 在`tool_can_use`目录下创建新的Python文件(如`my_new_tool.py`)
+2. 导入`BaseTool`和`register_tool`
+3. 创建继承自`BaseTool`的工具类
+4. 实现必要的属性(`name`, `description`, `parameters`)
+5. 实现`execute`方法
+6. 使用`register_tool`注册工具
+
+## 与ToolUser整合
+
+`ToolUser`类已经更新为使用这个新的工具系统,它会:
+
+1. 自动获取所有已注册工具的定义
+2. 基于工具名称找到对应的工具实例
+3. 调用工具的`execute`方法
+
+## 使用示例
+
+```python
+from src.do_tool.tool_use import ToolUser
+
+# 创建工具用户
+tool_user = ToolUser()
+
+# 使用工具
+result = await tool_user.use_tool(message_txt="查询关于Python的知识", sender_name="用户", chat_stream=chat_stream)
+
+# 处理结果
+if result["used_tools"]:
+ print("工具使用结果:", result["collected_info"])
+else:
+ print("未使用工具")
+```
\ No newline at end of file
diff --git a/src/do_tool/tool_can_use/__init__.py b/src/do_tool/tool_can_use/__init__.py
new file mode 100644
index 00000000..a7ea17ab
--- /dev/null
+++ b/src/do_tool/tool_can_use/__init__.py
@@ -0,0 +1,20 @@
+from src.do_tool.tool_can_use.base_tool import (
+ BaseTool,
+ register_tool,
+ discover_tools,
+ get_all_tool_definitions,
+ get_tool_instance,
+ TOOL_REGISTRY,
+)
+
+__all__ = [
+ "BaseTool",
+ "register_tool",
+ "discover_tools",
+ "get_all_tool_definitions",
+ "get_tool_instance",
+ "TOOL_REGISTRY",
+]
+
+# 自动发现并注册工具
+discover_tools()
diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py
new file mode 100644
index 00000000..b1edf805
--- /dev/null
+++ b/src/do_tool/tool_can_use/base_tool.py
@@ -0,0 +1,113 @@
+from typing import Dict, List, Any, Optional, Type
+import inspect
+import importlib
+import pkgutil
+import os
+from src.common.logger import get_module_logger
+
+logger = get_module_logger("base_tool")
+
+# 工具注册表
+TOOL_REGISTRY = {}
+
+
+class BaseTool:
+ """所有工具的基类"""
+
+ # 工具名称,子类必须重写
+ name = None
+ # 工具描述,子类必须重写
+ description = None
+ # 工具参数定义,子类必须重写
+ parameters = None
+
+ @classmethod
+ def get_tool_definition(cls) -> Dict[str, Any]:
+ """获取工具定义,用于LLM工具调用
+
+ Returns:
+ Dict: 工具定义字典
+ """
+ if not cls.name or not cls.description or not cls.parameters:
+ raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性")
+
+ return {
+ "type": "function",
+ "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters},
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行工具函数
+
+ Args:
+ function_args: 工具调用参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ raise NotImplementedError("子类必须实现execute方法")
+
+
+def register_tool(tool_class: Type[BaseTool]):
+ """注册工具到全局注册表
+
+ Args:
+ tool_class: 工具类
+ """
+ if not issubclass(tool_class, BaseTool):
+ raise TypeError(f"{tool_class.__name__} 不是 BaseTool 的子类")
+
+ tool_name = tool_class.name
+ if not tool_name:
+ raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性")
+
+ TOOL_REGISTRY[tool_name] = tool_class
+ logger.info(f"已注册工具: {tool_name}")
+
+
+def discover_tools():
+ """自动发现并注册tool_can_use目录下的所有工具"""
+ # 获取当前目录路径
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ package_name = os.path.basename(current_dir)
+
+ # 遍历包中的所有模块
+ for _, module_name, _ in pkgutil.iter_modules([current_dir]):
+ # 跳过当前模块和__pycache__
+ if module_name == "base_tool" or module_name.startswith("__"):
+ continue
+
+ # 导入模块
+ module = importlib.import_module(f"src.do_tool.{package_name}.{module_name}")
+
+ # 查找模块中的工具类
+ for _, obj in inspect.getmembers(module):
+ if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool:
+ register_tool(obj)
+
+ logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具")
+
+
+def get_all_tool_definitions() -> List[Dict[str, Any]]:
+ """获取所有已注册工具的定义
+
+ Returns:
+ List[Dict]: 工具定义列表
+ """
+ return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()]
+
+
+def get_tool_instance(tool_name: str) -> Optional[BaseTool]:
+ """获取指定名称的工具实例
+
+ Args:
+ tool_name: 工具名称
+
+ Returns:
+ Optional[BaseTool]: 工具实例,如果找不到则返回None
+ """
+ tool_class = TOOL_REGISTRY.get(tool_name)
+ if not tool_class:
+ return None
+ return tool_class()
diff --git a/src/do_tool/tool_can_use/change_mood.py b/src/do_tool/tool_can_use/change_mood.py
new file mode 100644
index 00000000..53410068
--- /dev/null
+++ b/src/do_tool/tool_can_use/change_mood.py
@@ -0,0 +1,57 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.plugins.config.config import global_config
+from src.common.logger import get_module_logger
+from src.plugins.moods.moods import MoodManager
+from src.plugins.chat_module.think_flow_chat.think_flow_generator import ResponseGenerator
+
+from typing import Dict, Any
+
+logger = get_module_logger("change_mood_tool")
+
+
+class ChangeMoodTool(BaseTool):
+ """改变心情的工具"""
+
+ name = "change_mood"
+ description = "根据收到的内容和自身回复的内容,改变心情,当你回复了别人的消息,你可以使用这个工具"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "引起你改变心情的文本"},
+ "response_set": {"type": "list", "description": "你对文本的回复"},
+ },
+ "required": ["text", "response_set"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str) -> Dict[str, Any]:
+ """执行心情改变
+
+ Args:
+ function_args: 工具参数
+ message_processed_plain_text: 原始消息文本
+ response_set: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ try:
+ response_set = function_args.get("response_set")
+ message_processed_plain_text = function_args.get("text")
+
+ mood_manager = MoodManager.get_instance()
+ gpt = ResponseGenerator()
+
+ if response_set is None:
+ response_set = ["你还没有回复"]
+
+ ori_response = ",".join(response_set)
+ _stance, emotion = await gpt._get_emotion_tags(ori_response, message_processed_plain_text)
+ mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor)
+ return {"name": "change_mood", "content": f"你的心情刚刚变化了,现在的心情是: {emotion}"}
+ except Exception as e:
+ logger.error(f"心情改变工具执行失败: {str(e)}")
+ return {"name": "change_mood", "content": f"心情改变失败: {str(e)}"}
+
+
+# 注册工具
+# register_tool(ChangeMoodTool)
diff --git a/src/do_tool/tool_can_use/change_relationship.py b/src/do_tool/tool_can_use/change_relationship.py
new file mode 100644
index 00000000..9aa08485
--- /dev/null
+++ b/src/do_tool/tool_can_use/change_relationship.py
@@ -0,0 +1,44 @@
+# from src.plugins.person_info.relationship_manager import relationship_manager
+from src.common.logger import get_module_logger
+from src.do_tool.tool_can_use.base_tool import BaseTool
+# from src.plugins.chat_module.think_flow_chat.think_flow_generator import ResponseGenerator
+
+
+logger = get_module_logger("relationship_tool")
+
+
+class RelationshipTool(BaseTool):
+ name = "change_relationship"
+ description = "根据收到的文本和回复内容,修改与特定用户的关系值,当你回复了别人的消息,你可以使用这个工具"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "收到的文本"},
+ "changed_value": {"type": "number", "description": "变更值"},
+ "reason": {"type": "string", "description": "变更原因"},
+ },
+ "required": ["text", "changed_value", "reason"],
+ }
+
+ async def execute(self, args: dict, message_txt: str) -> dict:
+ """执行工具功能
+
+ Args:
+ args: 包含工具参数的字典
+ text: 原始消息文本
+ changed_value: 变更值
+ reason: 变更原因
+
+ Returns:
+ dict: 包含执行结果的字典
+ """
+ try:
+ text = args.get("text")
+ changed_value = args.get("changed_value")
+ reason = args.get("reason")
+
+ return {"content": f"因为你刚刚因为{reason},所以你和发[{text}]这条消息的人的关系值变化为{changed_value}"}
+
+ except Exception as e:
+ logger.error(f"修改关系值时发生错误: {str(e)}")
+ return {"content": f"修改关系值失败: {str(e)}"}
diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py
new file mode 100644
index 00000000..48cee515
--- /dev/null
+++ b/src/do_tool/tool_can_use/compare_numbers_tool.py
@@ -0,0 +1,50 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.common.logger import get_module_logger
+from typing import Dict, Any
+
+logger = get_module_logger("compare_numbers_tool")
+
+
+class CompareNumbersTool(BaseTool):
+ """比较两个数大小的工具"""
+
+ name = "compare_numbers"
+ description = "比较两个数的大小,返回较大的数"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "num1": {"type": "number", "description": "第一个数字"},
+ "num2": {"type": "number", "description": "第二个数字"},
+ },
+ "required": ["num1", "num2"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行比较两个数的大小
+
+ Args:
+ function_args: 工具参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ try:
+ num1 = function_args.get("num1")
+ num2 = function_args.get("num2")
+
+ if num1 > num2:
+ result = f"{num1} 大于 {num2}"
+ elif num1 < num2:
+ result = f"{num1} 小于 {num2}"
+ else:
+ result = f"{num1} 等于 {num2}"
+
+ return {"name": self.name, "content": result}
+ except Exception as e:
+ logger.error(f"比较数字失败: {str(e)}")
+ return {"name": self.name, "content": f"比较数字失败: {str(e)}"}
+
+
+# 注册工具
+# register_tool(CompareNumbersTool)
diff --git a/src/do_tool/tool_can_use/get_current_task.py b/src/do_tool/tool_can_use/get_current_task.py
new file mode 100644
index 00000000..d5660f6a
--- /dev/null
+++ b/src/do_tool/tool_can_use/get_current_task.py
@@ -0,0 +1,59 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.plugins.schedule.schedule_generator import bot_schedule
+from src.common.logger import get_module_logger
+from typing import Dict, Any
+from datetime import datetime
+
+logger = get_module_logger("get_current_task_tool")
+
+
+class GetCurrentTaskTool(BaseTool):
+ """获取当前正在做的事情/最近的任务工具"""
+
+ name = "get_schedule"
+ description = "获取当前正在做的事情,或者某个时间点/时间段的日程信息"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "start_time": {"type": "string", "description": "开始时间,格式为'HH:MM',填写current则获取当前任务"},
+ "end_time": {"type": "string", "description": "结束时间,格式为'HH:MM',填写current则获取当前任务"},
+ },
+ "required": ["start_time", "end_time"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行获取当前任务或指定时间段的日程信息
+
+ Args:
+ function_args: 工具参数
+ message_txt: 原始消息文本,此工具不使用
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ start_time = function_args.get("start_time")
+ end_time = function_args.get("end_time")
+
+ # 如果 start_time 或 end_time 为 "current",则获取当前任务
+ if start_time == "current" or end_time == "current":
+ current_task = bot_schedule.get_current_num_task(num=1, time_info=True)
+ current_time = datetime.now().strftime("%H:%M:%S")
+ current_date = datetime.now().strftime("%Y-%m-%d")
+ if current_task:
+ task_info = f"{current_date} {current_time},你在{current_task}"
+ else:
+ task_info = f"{current_time} {current_date},没在做任何事情"
+ # 如果提供了时间范围,则获取该时间段的日程信息
+ elif start_time and end_time:
+ tasks = await bot_schedule.get_task_from_time_to_time(start_time, end_time)
+ if tasks:
+ task_list = []
+ for task in tasks:
+ task_time = task[0].strftime("%H:%M")
+ task_content = task[1]
+ task_list.append(f"{task_time}时,{task_content}")
+ task_info = "\n".join(task_list)
+ else:
+ task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息"
+
+ return {"name": "get_current_task", "content": f"日程信息: {task_info}"}
diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py
new file mode 100644
index 00000000..b78c0775
--- /dev/null
+++ b/src/do_tool/tool_can_use/get_knowledge.py
@@ -0,0 +1,135 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.plugins.chat.utils import get_embedding
+from src.common.database import db
+from src.common.logger import get_module_logger
+from typing import Dict, Any, Union
+
+logger = get_module_logger("get_knowledge_tool")
+
+
+class SearchKnowledgeTool(BaseTool):
+ """从知识库中搜索相关信息的工具"""
+
+ name = "search_knowledge"
+ description = "从知识库中搜索相关信息"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "搜索查询关键词"},
+ "threshold": {"type": "number", "description": "相似度阈值,0.0到1.0之间"},
+ },
+ "required": ["query"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行知识库搜索
+
+ Args:
+ function_args: 工具参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ try:
+ query = function_args.get("query", message_txt)
+ threshold = function_args.get("threshold", 0.4)
+
+ # 调用知识库搜索
+ embedding = await get_embedding(query, request_type="info_retrieval")
+ if embedding:
+ knowledge_info = self.get_info_from_db(embedding, limit=3, threshold=threshold)
+ if knowledge_info:
+ content = f"你知道这些知识: {knowledge_info}"
+ else:
+ content = f"你不太了解有关{query}的知识"
+ return {"name": "search_knowledge", "content": content}
+ return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"}
+ except Exception as e:
+ logger.error(f"知识库搜索工具执行失败: {str(e)}")
+ return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"}
+
+ def get_info_from_db(
+ self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False
+ ) -> Union[str, list]:
+ """从数据库中获取相关信息
+
+ Args:
+ query_embedding: 查询的嵌入向量
+ limit: 最大返回结果数
+ threshold: 相似度阈值
+ return_raw: 是否返回原始结果
+
+ Returns:
+ Union[str, list]: 格式化的信息字符串或原始结果列表
+ """
+ if not query_embedding:
+ return "" if not return_raw else []
+
+ # 使用余弦相似度计算
+ 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"]}]}}},
+ {
+ "$match": {
+ "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果
+ }
+ },
+ {"$sort": {"similarity": -1}},
+ {"$limit": limit},
+ {"$project": {"content": 1, "similarity": 1}},
+ ]
+
+ results = list(db.knowledges.aggregate(pipeline))
+ logger.debug(f"知识库查询结果数量: {len(results)}")
+
+ if not results:
+ return "" if not return_raw else []
+
+ if return_raw:
+ return results
+ else:
+ # 返回所有找到的内容,用换行分隔
+ return "\n".join(str(result["content"]) for result in results)
+
+
+# 注册工具
+# register_tool(SearchKnowledgeTool)
diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py
new file mode 100644
index 00000000..6a3c1c39
--- /dev/null
+++ b/src/do_tool/tool_can_use/get_memory.py
@@ -0,0 +1,59 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.plugins.memory_system.Hippocampus import HippocampusManager
+from src.common.logger import get_module_logger
+from typing import Dict, Any
+
+logger = get_module_logger("mid_chat_mem_tool")
+
+
+class GetMemoryTool(BaseTool):
+ """从记忆系统中获取相关记忆的工具"""
+
+ name = "mid_chat_mem"
+ description = "从记忆系统中获取相关记忆"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "要查询的相关文本"},
+ "max_memory_num": {"type": "integer", "description": "最大返回记忆数量"},
+ },
+ "required": ["text"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行记忆获取
+
+ Args:
+ function_args: 工具参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ try:
+ text = function_args.get("text", message_txt)
+ max_memory_num = function_args.get("max_memory_num", 2)
+
+ # 调用记忆系统
+ related_memory = await HippocampusManager.get_instance().get_memory_from_text(
+ text=text, max_memory_num=max_memory_num, max_memory_length=2, max_depth=3, fast_retrieval=False
+ )
+
+ memory_info = ""
+ if related_memory:
+ for memory in related_memory:
+ memory_info += memory[1] + "\n"
+
+ if memory_info:
+ content = f"你记得这些事情: {memory_info}"
+ else:
+ content = f"你不太记得有关{text}的记忆,你对此不太了解"
+
+ return {"name": "mid_chat_mem", "content": content}
+ except Exception as e:
+ logger.error(f"记忆获取工具执行失败: {str(e)}")
+ return {"name": "mid_chat_mem", "content": f"记忆获取失败: {str(e)}"}
+
+
+# 注册工具
+# register_tool(GetMemoryTool)
diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py
new file mode 100644
index 00000000..c3c9c837
--- /dev/null
+++ b/src/do_tool/tool_can_use/get_time_date.py
@@ -0,0 +1,38 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.common.logger import get_module_logger
+from typing import Dict, Any
+from datetime import datetime
+
+logger = get_module_logger("get_time_date")
+
+
+class GetCurrentDateTimeTool(BaseTool):
+ """获取当前时间、日期、年份和星期的工具"""
+
+ name = "get_current_date_time"
+ description = "当有人询问或者涉及到具体时间或者日期的时候,必须使用这个工具"
+ parameters = {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行获取当前时间、日期、年份和星期
+
+ Args:
+ function_args: 工具参数(此工具不使用)
+ message_txt: 原始消息文本(此工具不使用)
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ current_time = datetime.now().strftime("%H:%M:%S")
+ current_date = datetime.now().strftime("%Y-%m-%d")
+ current_year = datetime.now().strftime("%Y")
+ current_weekday = datetime.now().strftime("%A")
+
+ return {
+ "name": "get_current_date_time",
+ "content": f"当前时间: {current_time}, 日期: {current_date}, 年份: {current_year}, 星期: {current_weekday}",
+ }
diff --git a/src/do_tool/tool_can_use/mid_chat_mem.py b/src/do_tool/tool_can_use/mid_chat_mem.py
new file mode 100644
index 00000000..26d26704
--- /dev/null
+++ b/src/do_tool/tool_can_use/mid_chat_mem.py
@@ -0,0 +1,40 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.common.logger import get_module_logger
+from typing import Dict, Any
+
+logger = get_module_logger("get_mid_memory_tool")
+
+
+class GetMidMemoryTool(BaseTool):
+ """从记忆系统中获取相关记忆的工具"""
+
+ name = "mid_chat_mem"
+ description = "之前的聊天内容中获取具体信息,当最新消息提到,或者你需要回复的消息中提到,你可以使用这个工具"
+ parameters = {
+ "type": "object",
+ "properties": {
+ "id": {"type": "integer", "description": "要查询的聊天记录id"},
+ },
+ "required": ["id"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ """执行记忆获取
+
+ Args:
+ function_args: 工具参数
+ message_txt: 原始消息文本
+
+ Returns:
+ Dict: 工具执行结果
+ """
+ try:
+ id = function_args.get("id")
+ return {"name": "mid_chat_mem", "content": str(id)}
+ except Exception as e:
+ logger.error(f"聊天记录获取工具执行失败: {str(e)}")
+ return {"name": "mid_chat_mem", "content": f"聊天记录获取失败: {str(e)}"}
+
+
+# 注册工具
+# register_tool(GetMemoryTool)
diff --git a/src/do_tool/tool_can_use/send_emoji.py b/src/do_tool/tool_can_use/send_emoji.py
new file mode 100644
index 00000000..9cd48f0e
--- /dev/null
+++ b/src/do_tool/tool_can_use/send_emoji.py
@@ -0,0 +1,25 @@
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.common.logger import get_module_logger
+
+from typing import Dict, Any
+
+logger = get_module_logger("send_emoji_tool")
+
+
+class SendEmojiTool(BaseTool):
+ """发送表情包的工具"""
+
+ name = "send_emoji"
+ description = "当你觉得需要表达情感,或者帮助表达,可以使用这个工具发送表情包"
+ parameters = {
+ "type": "object",
+ "properties": {"text": {"type": "string", "description": "要发送的表情包描述"}},
+ "required": ["text"],
+ }
+
+ async def execute(self, function_args: Dict[str, Any], message_txt: str) -> Dict[str, Any]:
+ text = function_args.get("text", message_txt)
+ return {
+ "name": "send_emoji",
+ "content": text,
+ }
diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py
new file mode 100644
index 00000000..b14927be
--- /dev/null
+++ b/src/do_tool/tool_use.py
@@ -0,0 +1,193 @@
+from src.plugins.models.utils_model import LLM_request
+from src.plugins.config.config import global_config
+from src.plugins.chat.chat_stream import ChatStream
+from src.common.database import db
+import time
+import json
+from src.common.logger import get_module_logger, TOOL_USE_STYLE_CONFIG, LogConfig
+from src.do_tool.tool_can_use import get_all_tool_definitions, get_tool_instance
+from src.heart_flow.sub_heartflow import SubHeartflow
+
+tool_use_config = LogConfig(
+ # 使用消息发送专用样式
+ console_format=TOOL_USE_STYLE_CONFIG["console_format"],
+ file_format=TOOL_USE_STYLE_CONFIG["file_format"],
+)
+logger = get_module_logger("tool_use", config=tool_use_config)
+
+
+class ToolUser:
+ def __init__(self):
+ self.llm_model_tool = LLM_request(
+ model=global_config.llm_tool_use, temperature=0.2, max_tokens=1000, request_type="tool_use"
+ )
+
+ async def _build_tool_prompt(
+ self, message_txt: str, sender_name: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None
+ ):
+ """构建工具使用的提示词
+
+ Args:
+ message_txt: 用户消息文本
+ sender_name: 发送者名称
+ chat_stream: 聊天流对象
+
+ Returns:
+ str: 构建好的提示词
+ """
+ if subheartflow:
+ mid_memory_info = subheartflow.observations[0].mid_memory_info
+ # print(f"intol111111111111111111111111111111111222222222222mid_memory_info:{mid_memory_info}")
+ else:
+ mid_memory_info = ""
+
+ new_messages = list(
+ db.messages.find({"chat_id": chat_stream.stream_id, "time": {"$gt": time.time()}}).sort("time", 1).limit(15)
+ )
+ new_messages_str = ""
+ for msg in new_messages:
+ if "detailed_plain_text" in msg:
+ new_messages_str += f"{msg['detailed_plain_text']}"
+
+ # 这些信息应该从调用者传入,而不是从self获取
+ bot_name = global_config.BOT_NICKNAME
+ prompt = ""
+ prompt += mid_memory_info
+ prompt += "你正在思考如何回复群里的消息。\n"
+ prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n"
+ prompt += f"注意你就是{bot_name},{bot_name}指的就是你。"
+
+ prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。"
+ return prompt
+
+ def _define_tools(self):
+ """获取所有已注册工具的定义
+
+ Returns:
+ list: 工具定义列表
+ """
+ return get_all_tool_definitions()
+
+ async def _execute_tool_call(self, tool_call, message_txt: str):
+ """执行特定的工具调用
+
+ Args:
+ tool_call: 工具调用对象
+ message_txt: 原始消息文本
+
+ Returns:
+ dict: 工具调用结果
+ """
+ try:
+ function_name = tool_call["function"]["name"]
+ function_args = json.loads(tool_call["function"]["arguments"])
+
+ # 获取对应工具实例
+ tool_instance = get_tool_instance(function_name)
+ if not tool_instance:
+ logger.warning(f"未知工具名称: {function_name}")
+ return None
+
+ # 执行工具
+ result = await tool_instance.execute(function_args, message_txt)
+ if result:
+ # 直接使用 function_name 作为 tool_type
+ tool_type = function_name
+
+ return {
+ "tool_call_id": tool_call["id"],
+ "role": "tool",
+ "name": function_name,
+ "type": tool_type,
+ "content": result["content"],
+ }
+ return None
+ except Exception as e:
+ logger.error(f"执行工具调用时发生错误: {str(e)}")
+ return None
+
+ async def use_tool(
+ self, message_txt: str, sender_name: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None
+ ):
+ """使用工具辅助思考,判断是否需要额外信息
+
+ Args:
+ message_txt: 用户消息文本
+ sender_name: 发送者名称
+ chat_stream: 聊天流对象
+
+ Returns:
+ dict: 工具使用结果,包含结构化的信息
+ """
+ try:
+ # 构建提示词
+ prompt = await self._build_tool_prompt(message_txt, sender_name, chat_stream, subheartflow)
+
+ # 定义可用工具
+ tools = self._define_tools()
+ logger.trace(f"工具定义: {tools}")
+
+ # 使用llm_model_tool发送带工具定义的请求
+ payload = {
+ "model": self.llm_model_tool.model_name,
+ "messages": [{"role": "user", "content": prompt}],
+ "max_tokens": global_config.max_response_length,
+ "tools": tools,
+ "temperature": 0.2,
+ }
+
+ logger.trace(f"发送工具调用请求,模型: {self.llm_model_tool.model_name}")
+ # 发送请求获取模型是否需要调用工具
+ response = await self.llm_model_tool._execute_request(
+ endpoint="/chat/completions", payload=payload, prompt=prompt
+ )
+
+ # 根据返回值数量判断是否有工具调用
+ if len(response) == 3:
+ content, reasoning_content, tool_calls = response
+ # logger.info(f"工具思考: {tool_calls}")
+ # logger.debug(f"工具思考: {content}")
+
+ # 检查响应中工具调用是否有效
+ if not tool_calls:
+ logger.debug("模型返回了空的tool_calls列表")
+ return {"used_tools": False}
+
+ tool_calls_str = ""
+ for tool_call in tool_calls:
+ tool_calls_str += f"{tool_call['function']['name']}\n"
+ logger.info(f"根据:\n{prompt}\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}")
+ tool_results = []
+ structured_info = {} # 动态生成键
+
+ # 执行所有工具调用
+ for tool_call in tool_calls:
+ result = await self._execute_tool_call(tool_call, message_txt)
+ if result:
+ tool_results.append(result)
+ # 使用工具名称作为键
+ tool_name = result["name"]
+ if tool_name not in structured_info:
+ structured_info[tool_name] = []
+ structured_info[tool_name].append({"name": result["name"], "content": result["content"]})
+
+ # 如果有工具结果,返回结构化的信息
+ if structured_info:
+ logger.info(f"工具调用收集到结构化信息: {json.dumps(structured_info, ensure_ascii=False)}")
+ return {"used_tools": True, "structured_info": structured_info}
+ else:
+ # 没有工具调用
+ content, reasoning_content = response
+ logger.debug("模型没有请求调用任何工具")
+
+ # 如果没有工具调用或处理失败,直接返回原始思考
+ return {
+ "used_tools": False,
+ }
+
+ except Exception as e:
+ logger.error(f"工具调用过程中出错: {str(e)}")
+ return {
+ "used_tools": False,
+ "error": str(e),
+ }
diff --git a/src/gui/logger_gui.py b/src/gui/logger_gui.py
index 9488446c..ad6edafb 100644
--- a/src/gui/logger_gui.py
+++ b/src/gui/logger_gui.py
@@ -24,10 +24,10 @@
# # 标记GUI是否运行中
# self.is_running = True
-
+
# # 程序关闭时的清理操作
# self.protocol("WM_DELETE_WINDOW", self._on_closing)
-
+
# # 初始化进程、日志队列、日志数据等变量
# self.process = None
# self.log_queue = queue.Queue()
@@ -236,7 +236,7 @@
# while not self.log_queue.empty():
# line = self.log_queue.get()
# self.process_log_line(line)
-
+
# # 仅在GUI仍在运行时继续处理队列
# if self.is_running:
# self.after(100, self.process_log_queue)
@@ -245,11 +245,11 @@
# """解析单行日志并更新日志数据和筛选器"""
# match = re.match(
# r"""^
-# (?:(?P