diff --git a/.gitattributes b/.gitattributes index cf5cffa22..1c4521779 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.bat text eol=crlf -*.cmd text eol=crlf \ No newline at end of file +*.cmd text eol=crlf +MaiLauncher.bat text eol=crlf working-tree-encoding=GBK \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a4245d0a0..ce2623260 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,6 +12,23 @@ body: - label: "我确认在 Issues 列表中并无其他人已经提出过与此问题相同或相似的问题" required: true - label: "我使用了 Docker" +- type: dropdown + attributes: + label: "使用的分支" + description: "请选择您正在使用的版本分支" + options: + - main + - main-fix + - refactor + validations: + required: true +- type: input + attributes: + label: "具体版本号" + description: "请输入您使用的具体版本号" + placeholder: "例如:0.5.11、0.5.8" + validations: + required: true - type: textarea attributes: label: 遇到的问题 diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5b09b8cda..c06d967ca 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -4,11 +4,10 @@ on: push: branches: - main - - debug # 新增 debug 分支触发 - - stable-dev + - main-fix tags: - - 'v*' - workflow_dispatch: + - 'v*' + workflow_dispatch: jobs: build-and-push: @@ -16,7 +15,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -33,10 +32,8 @@ jobs: echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" == "refs/heads/main" ]; then echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/debug" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:debug" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/stable-dev" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:stable-dev" >> $GITHUB_OUTPUT + elif [ "${{ github.ref }}" == "refs/heads/main-fix" ]; then + echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main-fix" >> $GITHUB_OUTPUT fi - name: Build and Push Docker Image @@ -48,4 +45,4 @@ jobs: tags: ${{ steps.tags.outputs.tags }} push: true cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache - cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max \ No newline at end of file + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 4adeffd74..697c47759 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -6,3 +6,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: astral-sh/ruff-action@v3 + diff --git a/.gitignore b/.gitignore index 6e1be60b4..d17c6adc1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ data1/ mongodb/ NapCat.Framework.Windows.Once/ log/ +logs/ /test /src/test message_queue_content.txt @@ -15,6 +16,8 @@ memory_graph.gml .env.* config/bot_config_dev.toml config/bot_config.toml +config/bot_config.toml.bak +src/plugins/remote/client_uuid.json # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -23,7 +26,7 @@ llm_statistics.txt mongodb napcat run_dev.bat - +elua.confirmed # C extensions *.so @@ -189,7 +192,6 @@ cython_debug/ # PyPI configuration file .pypirc -.env # jieba jieba.cache @@ -198,4 +200,16 @@ jieba.cache !.vscode/settings.json # direnv -/.direnv \ No newline at end of file +/.direnv + +# JetBrains +.idea +*.iml +*.ipr + +# PyEnv +# If using PyEnv and configured to use a specific Python version locally +# a .local-version file will be created in the root of the project to specify the version. +.python-version + +OtherRes.txt \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..8a04e2d84 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.10 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/CLAUDE.md b/CLAUDE.md index d30b0e651..1b61f8ed4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,196 @@ -# MaiMBot 开发指南 +# MaiMBot 开发文档 -## 🛠️ 常用命令 +## 📊 系统架构图 + +```mermaid +graph TD + A[入口点] --> B[核心模块] + A --> C[插件系统] + B --> D[通用功能] + C --> E[聊天系统] + C --> F[记忆系统] + C --> G[情绪系统] + C --> H[意愿系统] + C --> I[其他插件] + + %% 入口点 + A1[bot.py] --> A + A2[run.py] --> A + A3[webui.py] --> A + + %% 核心模块 + B1[src/common/logger.py] --> B + B2[src/common/database.py] --> B + + %% 通用功能 + D1[日志系统] --> D + D2[数据库连接] --> D + D3[配置管理] --> D + + %% 聊天系统 + E1[消息处理] --> E + E2[提示构建] --> E + E3[LLM生成] --> E + E4[关系管理] --> E + + %% 记忆系统 + F1[记忆图] --> F + F2[记忆构建] --> F + F3[记忆检索] --> F + F4[记忆遗忘] --> F + + %% 情绪系统 + G1[情绪状态] --> G + G2[情绪更新] --> G + G3[情绪衰减] --> G + + %% 意愿系统 + H1[回复意愿] --> H + H2[意愿模式] --> H + H3[概率控制] --> H + + %% 其他插件 + I1[远程统计] --> I + I2[配置重载] --> I + I3[日程生成] --> I +``` + +## 📁 核心文件索引 + +| 功能 | 文件路径 | 描述 | +|------|----------|------| +| **入口点** | `/bot.py` | 主入口,初始化环境和启动服务 | +| | `/run.py` | 安装管理脚本,主要用于Windows | +| | `/webui.py` | Gradio基础的配置UI | +| **配置** | `/template.env` | 环境变量模板 | +| | `/template/bot_config_template.toml` | 机器人配置模板 | +| **核心基础** | `/src/common/database.py` | MongoDB连接管理 | +| | `/src/common/logger.py` | 基于loguru的日志系统 | +| **聊天系统** | `/src/plugins/chat/bot.py` | 消息处理核心逻辑 | +| | `/src/plugins/chat/config.py` | 配置管理与验证 | +| | `/src/plugins/chat/llm_generator.py` | LLM响应生成 | +| | `/src/plugins/chat/prompt_builder.py` | LLM提示构建 | +| **记忆系统** | `/src/plugins/memory_system/memory.py` | 图结构记忆实现 | +| | `/src/plugins/memory_system/draw_memory.py` | 记忆可视化 | +| **情绪系统** | `/src/plugins/moods/moods.py` | 情绪状态管理 | +| **意愿系统** | `/src/plugins/willing/willing_manager.py` | 回复意愿管理 | +| | `/src/plugins/willing/mode_classical.py` | 经典意愿模式 | +| | `/src/plugins/willing/mode_dynamic.py` | 动态意愿模式 | +| | `/src/plugins/willing/mode_custom.py` | 自定义意愿模式 | + +## 🔄 模块依赖关系 + +```mermaid +flowchart TD + A[bot.py] --> B[src/common/logger.py] + A --> C[src/plugins/chat/bot.py] + + C --> D[src/plugins/chat/config.py] + C --> E[src/plugins/chat/llm_generator.py] + C --> F[src/plugins/memory_system/memory.py] + C --> G[src/plugins/moods/moods.py] + C --> H[src/plugins/willing/willing_manager.py] + + E --> D + E --> I[src/plugins/chat/prompt_builder.py] + E --> J[src/plugins/models/utils_model.py] + + F --> B + F --> D + F --> J + + G --> D + + H --> B + H --> D + H --> K[src/plugins/willing/mode_classical.py] + H --> L[src/plugins/willing/mode_dynamic.py] + H --> M[src/plugins/willing/mode_custom.py] + + I --> B + I --> F + I --> G + + J --> B +``` + +## 🔄 消息处理流程 + +```mermaid +sequenceDiagram + participant User + participant ChatBot + participant WillingManager + participant Memory + participant PromptBuilder + participant LLMGenerator + participant MoodManager + + User->>ChatBot: 发送消息 + ChatBot->>ChatBot: 消息预处理 + ChatBot->>Memory: 记忆激活 + Memory-->>ChatBot: 激活度 + ChatBot->>WillingManager: 更新回复意愿 + WillingManager-->>ChatBot: 回复决策 + + alt 决定回复 + ChatBot->>PromptBuilder: 构建提示 + PromptBuilder->>Memory: 获取相关记忆 + Memory-->>PromptBuilder: 相关记忆 + PromptBuilder->>MoodManager: 获取情绪状态 + MoodManager-->>PromptBuilder: 情绪状态 + PromptBuilder-->>ChatBot: 完整提示 + ChatBot->>LLMGenerator: 生成回复 + LLMGenerator-->>ChatBot: AI回复 + ChatBot->>MoodManager: 更新情绪 + ChatBot->>User: 发送回复 + else 不回复 + ChatBot->>WillingManager: 更新未回复状态 + end +``` + +## 📋 类和功能清单 + +### 🤖 聊天系统 (`src/plugins/chat/`) + +| 类/功能 | 文件 | 描述 | +|--------|------|------| +| `ChatBot` | `bot.py` | 消息处理主类 | +| `ResponseGenerator` | `llm_generator.py` | 响应生成器 | +| `PromptBuilder` | `prompt_builder.py` | 提示构建器 | +| `Message`系列 | `message.py` | 消息表示类 | +| `RelationshipManager` | `relationship_manager.py` | 用户关系管理 | +| `EmojiManager` | `emoji_manager.py` | 表情符号管理 | + +### 🧠 记忆系统 (`src/plugins/memory_system/`) + +| 类/功能 | 文件 | 描述 | +|--------|------|------| +| `Memory_graph` | `memory.py` | 图结构记忆存储 | +| `Hippocampus` | `memory.py` | 记忆管理主类 | +| `memory_compress()` | `memory.py` | 记忆压缩函数 | +| `get_relevant_memories()` | `memory.py` | 记忆检索函数 | +| `operation_forget_topic()` | `memory.py` | 记忆遗忘函数 | + +### 😊 情绪系统 (`src/plugins/moods/`) + +| 类/功能 | 文件 | 描述 | +|--------|------|------| +| `MoodManager` | `moods.py` | 情绪管理器单例 | +| `MoodState` | `moods.py` | 情绪状态数据类 | +| `update_mood_from_emotion()` | `moods.py` | 情绪更新函数 | +| `_apply_decay()` | `moods.py` | 情绪衰减函数 | + +### 🤔 意愿系统 (`src/plugins/willing/`) + +| 类/功能 | 文件 | 描述 | +|--------|------|------| +| `WillingManager` | `willing_manager.py` | 意愿管理工厂类 | +| `ClassicalWillingManager` | `mode_classical.py` | 经典意愿模式 | +| `DynamicWillingManager` | `mode_dynamic.py` | 动态意愿模式 | +| `CustomWillingManager` | `mode_custom.py` | 自定义意愿模式 | + +## 🔧 常用命令 - **运行机器人**: `python run.py` 或 `python bot.py` - **安装依赖**: `pip install --upgrade -r requirements.txt` @@ -30,19 +220,25 @@ - **错误处理**: 使用带有具体异常的try/except - **文档**: 为类和公共函数编写docstrings -## 🧩 系统架构 +## 📋 常见修改点 -- **框架**: NoneBot2框架与插件架构 -- **数据库**: MongoDB持久化存储 -- **设计模式**: 工厂模式和单例管理器 -- **配置管理**: 使用环境变量和TOML文件 -- **内存系统**: 基于图的记忆结构,支持记忆构建、压缩、检索和遗忘 -- **情绪系统**: 情绪模拟与概率权重 -- **LLM集成**: 支持多个LLM服务提供商(ChatAnywhere, SiliconFlow, DeepSeek) +### 配置修改 +- **机器人配置**: `/template/bot_config_template.toml` +- **环境变量**: `/template.env` -## ⚙️ 环境配置 +### 行为定制 +- **个性调整**: `src/plugins/chat/config.py` 中的 BotConfig 类 +- **回复意愿算法**: `src/plugins/willing/mode_classical.py` +- **情绪反应模式**: `src/plugins/moods/moods.py` -- 使用`template.env`作为环境变量模板 -- 使用`template/bot_config_template.toml`作为机器人配置模板 -- MongoDB配置: 主机、端口、数据库名 -- API密钥配置: 各LLM提供商的API密钥 +### 消息处理 +- **消息管道**: `src/plugins/chat/message.py` +- **话题识别**: `src/plugins/chat/topic_identifier.py` + +### 记忆与学习 +- **记忆算法**: `src/plugins/memory_system/memory.py` +- **手动记忆构建**: `src/plugins/memory_system/memory_manual_build.py` + +### LLM集成 +- **LLM提供商**: `src/plugins/chat/llm_generator.py` +- **模型参数**: `template/bot_config_template.toml` 的 [model] 部分 \ No newline at end of file diff --git a/EULA.md b/EULA.md new file mode 100644 index 000000000..befbd4366 --- /dev/null +++ b/EULA.md @@ -0,0 +1,103 @@ +MaiMBot最终用户许可协议 +版本:V1.0 +更新日期:2025年3月18日 +生效日期:2025年3月18日 +适用的MaiMBot版本号:v0.5.15 + +2025© MaiMBot项目团队 + +● [一、一般条款](#一一般条款) +● [二、许可授权](#二许可授权) +● [源代码许可](#源代码许可) +● [输入输出内容授权](#输入输出内容授权) +● [三、用户行为](#三用户行为) +● [四、免责条款](#四免责条款) +● [五、其他条款](#五其他条款) +● [附录:其他重要须知](#附录其他重要须知) +● [一、风险提示](#一风险提示) +● [二、其他](#二其他) + + + +一、一般条款 + +1.1 MaiMBot项目(包括MaiMBot的源代码、可执行文件、文档,以及其它在本协议中所列出的文件)(以下简称“本项目”)是由开发者及贡献者(以下简称“项目团队”)共同维护,为用户提供自动回复功能的机器人代码项目。以下最终用户许可协议(EULA,以下简称“本协议”)是用户(以下简称“您”)与项目团队之间关于使用本项目所订立的合同条件。 + +1.2 在运行或使用本项目之前,您必须阅读并同意本协议的所有条款。未成年人或其它无/不完全民事行为能力责任人请在监护人的陪同下阅读并同意本协议。如果您不同意,则不得运行或使用本项目。在这种情况下,您应立即从您的设备上卸载或删除本项目及其所有副本。 + + +二、许可授权 + +源代码许可 +2.1 您了解本项目的源代码是基于GPLv3(GNU通用公共许可证第三版)开源协议发布的。您可以自由使用、修改、分发本项目的源代码,但必须遵守GPLv3许可证的要求。详细内容请参阅项目仓库中的LICENSE文件。 + +2.2 您了解本项目的源代码中可能包含第三方开源代码,这些代码的许可证可能与GPLv3许可证不同。您同意在使用这些代码时遵守相应的许可证要求。 + + +输入输出内容授权 +2.3 您了解本项目是使用您的配置信息、提交的指令(以下简称“输入内容”)和生成的内容(以下简称“输出内容”)构建请求发送到第三方API生成回复的机器人项目。 + +2.4 您授权本项目使用您的输入和输出内容按照项目的隐私条款用于以下行为: +● 调用第三方API用于生成回复; +● 调用第三方API用于构建本项目专用的存储于您部署或使用的数据库中的知识库和记忆库; +● 收集并记录本项目专用的存储于您部署或使用的设备中的日志; + +2.5 您了解本项目的源代码中包含第三方API的调用代码,这些API的使用可能受到第三方的服务条款和隐私政策的约束。在使用这些API时,您必须遵守相应的服务条款。 + +2.6 项目团队不对第三方API的服务质量、稳定性、准确性、安全性负责,亦不对第三方API的服务变更、终止、限制等行为负责。 + + +三、用户行为 + +3.1 您了解本项目会将您的配置信息、输入指令和生成内容发送到第三方API,您不应在输入指令和生成内容中包含以下内容: +● 涉及任何国家或地区秘密、商业秘密或其他可能会对国家或地区安全或者公共利益造成不利影响的数据; +● 涉及个人隐私、个人信息或其他敏感信息的数据; +● 侵犯他人合法权益的内容; +● 任何违反您及您部署本项目所用的设备所在的国家或地区的法律法规、政策规定的内容; + +3.2 您不应将本项目用于以下用途: +● 任何违反您及您部署本项目所用的设备所在的国家或地区的法律法规、政策规定的行为; + +3.3 您应当自行确保您被存储在本项目的知识库、记忆库和日志中的输入和输出内容的合法性与合规性以及存储行为的合法性与合规性。由此产生的任何法律责任均由您自行承担。 + + +四、免责条款 + +4.1 本项目的输出内容依赖第三方API,不受项目团队控制,亦不代表项目团队的观点。 + +4.2 除本协议条目2.3提到的之外,项目团队不会对您提供任何形式的担保,亦不对使用本项目的造成的任何后果负责。 + +五、其他条款 + +5.1 项目团队有权随时修改本协议的条款,修改后的协议将在本项目的新版本中生效。您应定期检查本协议的最新版本。 + +5.2 项目团队保有本协议的最终解释权。 + + +附录:其他重要须知 + +一、风险提示 + +1.1 隐私安全风险: 由于: +● 本项目会将您的配置信息、输入指令和生成内容发送到第三方API,而这些API的服务质量、稳定性、准确性、安全性不受项目团队控制。 +● 本项目会收集您的输入和输出内容,用于构建本项目专用的知识库和记忆库,以提高回复的准确性和连贯性。 + + 为了保障您的隐私信息安全,请注意以下事项: +● 避免在涉及个人隐私、个人信息或其他敏感信息的环境中使用本项目; +● 避免在不可信的环境中使用本项目; +● 避免在不可信的网络环境中使用本项目。 + +1.2 精神健康风险: 本项目仅为工具型机器人,不具备情感交互能力。建议用户: +● 避免过度依赖AI回复处理现实问题或情绪困扰; +● 如感到心理不适,请及时寻求专业心理咨询服务。 +● 如遇心理困扰,请寻求专业帮助(全国心理援助热线:12355)。 + +二、过往版本使用条件追溯 +对于本项目此前未配备 EULA 协议的版本,自本协议发布之日起,若用户希望继续使用这些版本,应在本协议生效后的合理时间内,通过升级到最新版本并同意本协议全部条款。若在本协议生效日2025年3月18日之后,用户仍使用此前无 EULA 协议版本且未同意本协议,则用户无权继续使用,项目方有权采取技术手段阻止其使用行为,并保留追究相关法律责任的权利 。 + +三、其他 +2.1 争议解决 +● 本协议适用中国法律,争议提交相关地区法院管辖; +● 若因GPLv3许可产生纠纷,以许可证官方解释为准。 + + diff --git a/MaiLauncher.bat b/MaiLauncher.bat new file mode 100644 index 000000000..7b876bd37 --- /dev/null +++ b/MaiLauncher.bat @@ -0,0 +1,636 @@ +@echo off +@setlocal enabledelayedexpansion +@chcp 936 + +@REM ð汾 +set "VERSION=1.0" + +title Bot̨ v%VERSION% + +@REM PythonGit +set "_root=%~dp0" +set "_root=%_root:~0,-1%" +cd "%_root%" + + +:search_python +cls +if exist "%_root%\python" ( + set "PYTHON_HOME=%_root%\python" +) else if exist "%_root%\venv" ( + call "%_root%\venv\Scripts\activate.bat" + set "PYTHON_HOME=%_root%\venv\Scripts" +) else ( + echo ԶPython... + + where python >nul 2>&1 + if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('where python') do ( + echo %%i | findstr /i /c:"!LocalAppData!\Microsoft\WindowsApps\python.exe" >nul + if errorlevel 1 ( + echo ҵPython%%i + set "py_path=%%i" + goto :validate_python + ) + ) + ) + set "search_paths=%ProgramFiles%\Git*;!LocalAppData!\Programs\Python\Python*" + for /d %%d in (!search_paths!) do ( + if exist "%%d\python.exe" ( + set "py_path=%%d\python.exe" + goto :validate_python + ) + ) + echo ûҵPython,Ҫװ? + set /p pyinstall_confirm="(Y/n): " + if /i "!pyinstall_confirm!"=="Y" ( + cls + echo ڰװPython... + winget install --id Python.Python.3.13 -e --accept-package-agreements --accept-source-agreements + if %errorlevel% neq 0 ( + echo װʧܣֶװPython + start https://www.python.org/downloads/ + exit /b + ) + echo װɣ֤Python... + goto search_python + + ) else ( + echo ȡװPython˳... + pause >nul + exit /b + ) + + echo δҵõPython + exit /b 1 + + :validate_python + "!py_path!" --version >nul 2>&1 + if %errorlevel% neq 0 ( + echo ЧPython%py_path% + exit /b 1 + ) + + :: ȡװĿ¼ + for %%i in ("%py_path%") do set "PYTHON_HOME=%%~dpi" + set "PYTHON_HOME=%PYTHON_HOME:~0,-1%" +) +if not exist "%PYTHON_HOME%\python.exe" ( + echo Python·֤ʧܣ%PYTHON_HOME% + echo Pythonװ·Ƿpython.exeļ + exit /b 1 +) +echo ɹPython·%PYTHON_HOME% + + + +:search_git +cls +if exist "%_root%\tools\git\bin" ( + set "GIT_HOME=%_root%\tools\git\bin" +) else ( + echo ԶGit... + + where git >nul 2>&1 + if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('where git') do ( + set "git_path=%%i" + goto :validate_git + ) + ) + echo ɨ賣װ·... + set "search_paths=!ProgramFiles!\Git\cmd" + for /f "tokens=*" %%d in ("!search_paths!") do ( + if exist "%%d\git.exe" ( + set "git_path=%%d\git.exe" + goto :validate_git + ) + ) + echo ûҵGitҪװ + set /p confirm="(Y/N): " + if /i "!confirm!"=="Y" ( + cls + echo ڰװGit... + set "custom_url=https://ghfast.top/https://github.com/git-for-windows/git/releases/download/v2.48.1.windows.1/Git-2.48.1-64-bit.exe" + + set "download_path=%TEMP%\Git-Installer.exe" + + echo Gitװ... + curl -L -o "!download_path!" "!custom_url!" + + if exist "!download_path!" ( + echo سɹʼװGit... + start /wait "" "!download_path!" /SILENT /NORESTART + ) else ( + echo ʧܣֶװGit + start https://git-scm.com/download/win + exit /b + ) + + del "!download_path!" + echo ʱļ + + echo װɣ֤Git... + where git >nul 2>&1 + if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('where git') do ( + set "git_path=%%i" + goto :validate_git + ) + goto :search_git + + ) else ( + echo װɣδҵGitֶװGit + start https://git-scm.com/download/win + exit /b + ) + + ) else ( + echo ȡװGit˳... + pause >nul + exit /b + ) + + echo δҵõGit + exit /b 1 + + :validate_git + "%git_path%" --version >nul 2>&1 + if %errorlevel% neq 0 ( + echo ЧGit%git_path% + exit /b 1 + ) + + :: ȡװĿ¼ + for %%i in ("%git_path%") do set "GIT_HOME=%%~dpi" + set "GIT_HOME=%GIT_HOME:~0,-1%" +) + +:search_mongodb +cls +sc query | findstr /i "MongoDB" >nul +if !errorlevel! neq 0 ( + echo MongoDBδУǷз + set /p confirm="Ƿ(Y/N): " + if /i "!confirm!"=="Y" ( + echo ڳMongoDB... + powershell -Command "Start-Process -Verb RunAs cmd -ArgumentList '/c net start MongoDB'" + echo ڵȴMongoDB... + echo ȴ... + timeout /t 30 >nul + sc query | findstr /i "MongoDB" >nul + if !errorlevel! neq 0 ( + echo MongoDBʧܣûаװҪװ + set /p install_confirm="װ(Y/N): " + if /i "!install_confirm!"=="Y" ( + echo ڰװMongoDB... + winget install --id MongoDB.Server -e --accept-package-agreements --accept-source-agreements + echo װɣMongoDB... + net start MongoDB + if !errorlevel! neq 0 ( + echo MongoDBʧܣֶ + exit /b + ) else ( + echo MongoDBѳɹ + ) + ) else ( + echo ȡװMongoDB˳... + pause >nul + exit /b + ) + ) + ) else ( + echo "棺MongoDBδУMaiMBot޷ݿ⣡" + ) +) else ( + echo MongoDB +) + +@REM set "GIT_HOME=%_root%\tools\git\bin" +set "PATH=%PYTHON_HOME%;%GIT_HOME%;%PATH%" + +:install_maim +if not exist "!_root!\bot.py" ( + cls + echo ƺûаװBotҪװڵǰĿ¼ + set /p confirm="(Y/N): " + if /i "!confirm!"=="Y" ( + echo ҪʹGit + set /p proxy_confirm="(Y/N): " + if /i "!proxy_confirm!"=="Y" ( + echo ڰװBot... + git clone https://ghfast.top/https://github.com/SengokuCola/MaiMBot + ) else ( + echo ڰװBot... + git clone https://github.com/SengokuCola/MaiMBot + ) + xcopy /E /H /I MaiMBot . >nul 2>&1 + rmdir /s /q MaiMBot + git checkout main-fix + + echo װɣڰװ... + python -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple + python -m pip install virtualenv + python -m virtualenv venv + call venv\Scripts\activate.bat + python -m pip install -r requirements.txt + + echo װɣҪ༭ļ + set /p edit_confirm="(Y/N): " + if /i "!edit_confirm!"=="Y" ( + goto config_menu + ) else ( + echo ȡ༭ļ˵... + ) + ) +) + + +@REM gitȡǰ֧ڱ +for /f "delims=" %%b in ('git symbolic-ref --short HEAD 2^>nul') do ( + set "BRANCH=%%b" +) + +@REM ݲַ֧֧ͬʹòͬɫ +echo ֧: %BRANCH% +if "!BRANCH!"=="main" ( + set "BRANCH_COLOR=" +) else if "!BRANCH!"=="main-fix" ( + set "BRANCH_COLOR=" +@REM ) else if "%BRANCH%"=="stable-dev" ( +@REM set "BRANCH_COLOR=" +) else ( + set "BRANCH_COLOR=" +) + +@REM endlocal & set "BRANCH_COLOR=%BRANCH_COLOR%" + +:check_is_venv +echo ڼ⻷״̬... +if exist "%_root%\config\no_venv" ( + echo ⵽no_venv,⻷ + goto menu +) + +:: +if defined VIRTUAL_ENV ( + goto menu +) + +echo ===================================== +echo ⻷⾯棺 +echo ǰʹϵͳPython·!PYTHON_HOME! +echo δ⵽⻷ + +:env_interaction +echo ===================================== +echo ѡ +echo 1 - Venv⻷ +echo 2 - /Conda⻷ +echo 3 - ʱμ +echo 4 - ⻷ +set /p choice="ѡ(1-4): " + +if "!choice!"=="4" ( + echo Ҫ⻷ + set /p no_venv_confirm="(Y/N): ....." + if /i "!no_venv_confirm!"=="Y" ( + echo 1 > "%_root%\config\no_venv" + echo Ѵno_venvļ + pause >nul + goto menu + ) else ( + echo ȡ⻷飬... + pause >nul + goto env_interaction + ) +) + +if "!choice!"=="3" ( + echo 棺ʹϵͳܵͻ + timeout /t 2 >nul + goto menu +) + +if "!choice!"=="2" goto handle_conda +if "!choice!"=="1" goto handle_venv + +echo Ч룬1-4֮ +timeout /t 2 >nul +goto env_interaction + +:handle_venv +python -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +echo ڳʼVenv... +python -m pip install virtualenv || ( + echo װʧܣ룺!errorlevel! + pause + goto env_interaction +) +echo ⻷venv + python -m virtualenv venv || ( + echo ʧܣ룺!errorlevel! + pause + goto env_interaction +) + +call venv\Scripts\activate.bat +echo ѼVenv +echo Ҫװ +set /p install_confirm="(Y/N): " +if /i "!install_confirm!"=="Y" ( + goto update_dependencies +) +goto menu + +:handle_conda +where conda >nul 2>&1 || ( + echo δ⵽condaԭ + echo 1. δװMiniconda + echo 2. conda쳣 + timeout /t 10 >nul + goto env_interaction +) + +:conda_menu +echo ѡConda +echo 1 - » +echo 2 - л +echo 3 - ϼ˵ +set /p choice="ѡ(1-3): " + +if "!choice!"=="3" goto env_interaction +if "!choice!"=="2" goto activate_conda +if "!choice!"=="1" goto create_conda + +echo Ч룬1-3֮ +timeout /t 2 >nul +goto conda_menu + +:create_conda +set /p "CONDA_ENV=»ƣ" +if "!CONDA_ENV!"=="" ( + echo ƲΪգ + goto create_conda +) +conda create -n !CONDA_ENV! python=3.13 -y || ( + echo ʧܣ룺!errorlevel! + timeout /t 10 >nul + goto conda_menu +) +goto activate_conda + +:activate_conda +set /p "CONDA_ENV=ҪĻƣ" +call conda activate !CONDA_ENV! || ( + echo ʧܣԭ + echo 1. + echo 2. conda쳣 + pause + goto conda_menu +) +echo ɹconda!CONDA_ENV! +echo Ҫװ +set /p install_confirm="(Y/N): " +if /i "!install_confirm!"=="Y" ( + goto update_dependencies +) +:menu +@chcp 936 +cls +echo Bot̨ v%VERSION% ǰ֧: %BRANCH_COLOR%%BRANCH% +echo ǰPython: !PYTHON_HOME! +echo ====================== +echo 1. ²Bot (Ĭ) +echo 2. ֱBot +echo 3. ý +echo 4. 湤 +echo 5. ˳ +echo ====================== + +set /p choice="ѡ (1-5)»سѡ: " + +if "!choice!"=="" set choice=1 + +if "!choice!"=="1" goto update_and_start +if "!choice!"=="2" goto start_bot +if "!choice!"=="3" goto config_menu +if "!choice!"=="4" goto tools_menu +if "!choice!"=="5" exit /b + +echo Ч룬1-5֮ +timeout /t 2 >nul +goto menu + +:config_menu +@chcp 936 +cls +if not exist config/bot_config.toml ( + copy /Y "template\bot_config_template.toml" "config\bot_config.toml" + +) +if not exist .env.prod ( + copy /Y "template\.env.prod" ".env.prod" +) + +start python webui.py + +goto menu + + +:tools_menu +@chcp 936 +cls +echo ʱй ǰ֧: %BRANCH_COLOR%%BRANCH% +echo ====================== +echo 1. +echo 2. л֧ +echo 3. õǰ֧ +echo 4. ļ +echo 5. ѧϰµ֪ʶ +echo 6. ֪ʶļ +echo 7. ˵ +echo ====================== + +set /p choice="ѡ: " +if "!choice!"=="1" goto update_dependencies +if "!choice!"=="2" goto switch_branch +if "!choice!"=="3" goto reset_branch +if "!choice!"=="4" goto update_config +if "!choice!"=="5" goto learn_new_knowledge +if "!choice!"=="6" goto open_knowledge_folder +if "!choice!"=="7" goto menu + +echo Ч룬1-6֮ +timeout /t 2 >nul +goto tools_menu + +:update_dependencies +cls +echo ڸ... +python -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +python.exe -m pip install -r requirements.txt + +echo ɣع˵... +pause +goto tools_menu + +:switch_branch +cls +echo л֧... +echo ǰ֧: %BRANCH% +@REM echo ÷֧: main, debug, stable-dev +echo 1. лmain +echo 2. лmain-fix +echo Ҫлķ֧: +set /p branch_name="֧: " +if "%branch_name%"=="" set branch_name=main +if "%branch_name%"=="main" ( + set "BRANCH_COLOR=" +) else if "%branch_name%"=="main-fix" ( + set "BRANCH_COLOR=" +@REM ) else if "%branch_name%"=="stable-dev" ( +@REM set "BRANCH_COLOR=" +) else if "%branch_name%"=="1" ( + set "BRANCH_COLOR=" + set "branch_name=main" +) else if "%branch_name%"=="2" ( + set "BRANCH_COLOR=" + set "branch_name=main-fix" +) else ( + echo Чķ֧, + timeout /t 2 >nul + goto switch_branch +) + +echo л֧ %branch_name%... +git checkout %branch_name% +echo ֧лɣǰ֧: %BRANCH_COLOR%%branch_name% +set "BRANCH=%branch_name%" +echo ع˵... +pause >nul +goto tools_menu + + +:reset_branch +cls +echo õǰ֧... +echo ǰ֧: !BRANCH! +echo ȷҪõǰ֧ +set /p confirm="(Y/N): " +if /i "!confirm!"=="Y" ( + echo õǰ֧... + git reset --hard !BRANCH! + echo ֧ɣع˵... +) else ( + echo ȡõǰ֧ع˵... +) +pause >nul +goto tools_menu + + +:update_config +cls +echo ڸļ... +echo ȷѱҪݣ޸ĵǰļ +echo 밴Yȡ밴... +set /p confirm="(Y/N): " +if /i "!confirm!"=="Y" ( + echo ڸļ... + python.exe config\auto_update.py + echo ļɣع˵... +) else ( + echo ȡļع˵... +) +pause >nul +goto tools_menu + +:learn_new_knowledge +cls +echo ѧϰµ֪ʶ... +echo ȷѱҪݣ޸ĵǰ֪ʶ⡣ +echo 밴Yȡ밴... +set /p confirm="(Y/N): " +if /i "!confirm!"=="Y" ( + echo ѧϰµ֪ʶ... + python.exe src\plugins\zhishi\knowledge_library.py + echo ѧϰɣع˵... +) else ( + echo ȡѧϰµ֪ʶ⣬ع˵... +) +pause >nul +goto tools_menu + +:open_knowledge_folder +cls +echo ڴ֪ʶļ... +if exist data\raw_info ( + start explorer data\raw_info +) else ( + echo ֪ʶļвڣ + echo ڴļ... + mkdir data\raw_info + timeout /t 2 >nul +) +goto tools_menu + + +:update_and_start +cls +:retry_git_pull +git pull > temp.log 2>&1 +findstr /C:"detected dubious ownership" temp.log >nul +if %errorlevel% equ 0 ( + echo ⵽ֿȨ⣬Զ޸... + git config --global --add safe.directory "%cd%" + echo ⣬git pull... + del temp.log + goto retry_git_pull +) +del temp.log +echo ڸ... +python -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +python -m pip install -r requirements.txt && cls + +echo ǰ: +echo HTTP_PROXY=%HTTP_PROXY% +echo HTTPS_PROXY=%HTTPS_PROXY% + +echo Disable Proxy... +set HTTP_PROXY= +set HTTPS_PROXY= +set no_proxy=0.0.0.0/32 + +REM chcp 65001 +python bot.py +echo. +echo BotֹͣУ˵... +pause >nul +goto menu + +:start_bot +cls +echo ڸ... +python -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple +python -m pip install -r requirements.txt && cls + +echo ǰ: +echo HTTP_PROXY=%HTTP_PROXY% +echo HTTPS_PROXY=%HTTPS_PROXY% + +echo Disable Proxy... +set HTTP_PROXY= +set HTTPS_PROXY= +set no_proxy=0.0.0.0/32 + +REM chcp 65001 +python bot.py +echo. +echo BotֹͣУ˵... +pause >nul +goto menu + + +:open_dir +start explorer "%cd%" +goto menu diff --git a/README.md b/README.md index 2fc8961e5..f1c25ea67 100644 --- a/README.md +++ b/README.md @@ -95,12 +95,10 @@ - MongoDB 提供数据持久化支持 - NapCat 作为QQ协议端支持 -**最新版本: v0.5.13** +**最新版本: v0.5.14** ([查看更新日志](changelog.md)) > [!WARNING] > 注意,3月12日的v0.5.13, 该版本更新较大,建议单独开文件夹部署,然后转移/data文件 和数据库,数据库可能需要删除messages下的内容(不需要删除记忆) - -
麦麦演示视频 @@ -121,24 +119,29 @@ - [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 ,建议加下面的(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 (开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - - +- [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 **📚 有热心网友创作的wiki:** https://maimbot.pages.dev/ +**📚 由SLAPQ制作的B站教程:** https://www.bilibili.com/opus/1041609335464001545 **😊 其他平台版本** - (由 [CabLate](https://github.com/cablate) 贡献) [Telegram 与其他平台(未来可能会有)的版本](https://github.com/cablate/MaiMBot/tree/telegram) - [集中讨论串](https://github.com/SengokuCola/MaiMBot/discussions/149) +## 📝 注意注意注意注意注意注意注意注意注意注意注意注意注意注意注意注意注意 +**如果你有想法想要提交pr** +- 由于本项目在快速迭代和功能调整,并且有重构计划,目前不接受任何未经过核心开发组讨论的pr合并,谢谢!如您仍旧希望提交pr,可以详情请看置顶issue +

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

-### 部署方式 +### 部署方式(忙于开发,部分内容可能过时) - 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置 +- 📦 Linux 自动部署(实验) :请下载并运行项目根目录中的`run.sh`并按照提示安装,部署完成后请参照后续配置指南进行配置 - [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md) @@ -148,13 +151,15 @@ - [🐳 Docker部署指南](docs/docker_deploy.md) - - ### 配置说明 - [🎀 新手配置指南](docs/installation_cute.md) - 通俗易懂的配置教程,适合初次使用的猫娘 - [⚙️ 标准配置指南](docs/installation_standard.md) - 简明专业的配置说明,适合有经验的用户 +### 常见问题 + +- [❓ 快速 Q & A ](docs/fast_q_a.md) - 针对新手的疑难解答,适合完全没接触过编程的新手 +

了解麦麦

diff --git a/bot.py b/bot.py index 36d621a6e..bf853bc0c 100644 --- a/bot.py +++ b/bot.py @@ -2,20 +2,28 @@ import asyncio import os import shutil import sys +from pathlib import Path import nonebot import time import uvicorn from dotenv import load_dotenv -from loguru import logger from nonebot.adapters.onebot.v11 import Adapter import platform +from src.common.logger import get_module_logger + + +# 配置主程序日志格式 +logger = get_module_logger("main_bot") # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} uvicorn_server = None +driver = None +app = None +loop = None def easter_egg(): @@ -63,24 +71,21 @@ def init_env(): # 首先加载基础环境变量.env if os.path.exists(".env"): - load_dotenv(".env",override=True) + load_dotenv(".env", override=True) logger.success("成功加载基础环境变量配置") def load_env(): # 使用闭包实现对加载器的横向扩展,避免大量重复判断 def prod(): - logger.success("加载生产环境变量配置") + logger.success("成功加载生产环境变量配置") load_dotenv(".env.prod", override=True) # override=True 允许覆盖已存在的环境变量 def dev(): - logger.success("加载开发环境变量配置") + logger.success("成功加载开发环境变量配置") load_dotenv(".env.dev", override=True) # override=True 允许覆盖已存在的环境变量 - fn_map = { - "prod": prod, - "dev": dev - } + fn_map = {"prod": prod, "dev": dev} env = os.getenv("ENVIRONMENT") logger.info(f"[load_env] 当前的 ENVIRONMENT 变量值:{env}") @@ -97,29 +102,6 @@ def load_env(): RuntimeError(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在") -def load_logger(): - logger.remove() # 移除默认配置 - if os.getenv("ENVIRONMENT") == "dev": - logger.add( - sys.stderr, - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", - colorize=True, - level=os.getenv("LOG_LEVEL", "DEBUG"), # 根据环境设置日志级别,默认为DEBUG - ) - else: - logger.add( - sys.stderr, - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <7} | {name:.<8}:{function:.<8}:{line: >4} - {message}", - colorize=True, - level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO - filter=lambda record: "nonebot" not in record["name"] - ) - - def scan_provider(env_config: dict): provider = {} @@ -148,10 +130,7 @@ def scan_provider(env_config: dict): # 检查每个 provider 是否同时存在 url 和 key for provider_name, config in provider.items(): if config["url"] is None or config["key"] is None: - logger.error( - f"provider 内容:{config}\n" - f"env_config 内容:{env_config}" - ) + logger.error(f"provider 内容:{config}\nenv_config 内容:{env_config}") raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量") @@ -180,25 +159,47 @@ async def uvicorn_main(): reload=os.getenv("ENVIRONMENT") == "dev", timeout_graceful_shutdown=5, log_config=None, - access_log=False + access_log=False, ) server = uvicorn.Server(config) uvicorn_server = server await server.serve() +def check_eula(): + eula_file = Path("elua.confirmed") + + # 如果已经确认过EULA,直接返回 + if eula_file.exists(): + return + + print("使用MaiMBot前请先阅读ELUA协议,继续运行视为同意协议") + print("协议内容:https://github.com/SengokuCola/MaiMBot/blob/main/EULA.md") + print('输入"同意"或"confirmed"继续运行') + + while True: + user_input = input().strip().lower() # 转换为小写以忽略大小写 + if user_input in ['同意', 'confirmed']: + # 创建确认文件 + eula_file.touch() + break + else: + print('请输入"同意"或"confirmed"以继续运行') + def raw_main(): # 利用 TZ 环境变量设定程序工作的时区 # 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用 - if platform.system().lower() != 'windows': + if platform.system().lower() != "windows": time.tzset() - + + check_eula() + easter_egg() - load_logger() init_config() init_env() load_env() - load_logger() + + # load_logger() env_config = {key: os.getenv(key) for key in os.environ} scan_provider(env_config) @@ -223,21 +224,24 @@ def raw_main(): 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("麦麦会努力做的更好的!正在停止中......") + + try: + loop.run_until_complete(uvicorn_main()) + except KeyboardInterrupt: + logger.warning("收到中断信号,正在优雅关闭...") + loop.run_until_complete(graceful_shutdown()) + finally: + loop.close() + except Exception as e: - logger.error(f"主程序异常: {e}") - finally: - loop.run_until_complete(graceful_shutdown()) - loop.close() - logger.info("进程终止完毕,麦麦开始休眠......下次再见哦!") + logger.error(f"主程序异常: {str(e)}") + if loop and not loop.is_closed(): + loop.run_until_complete(graceful_shutdown()) + loop.close() + sys.exit(1) diff --git a/changelog.md b/changelog.md index b9beed81e..73803d714 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,56 @@ # Changelog +AI总结 + +## [0.5.14] - 2025-3-14 +### 🌟 核心功能增强 +#### 记忆系统优化 +- 修复了构建记忆时重复读取同一段消息导致token消耗暴增的问题 +- 优化了记忆相关的工具模型代码 + +#### 消息处理升级 +- 新增了不回答已撤回消息的功能 +- 新增每小时自动删除存留超过1小时的撤回消息 +- 优化了戳一戳功能的响应机制 +- 修复了回复消息未正常发送的问题 +- 改进了图片发送错误时的处理机制 + +#### 日程系统改进 +- 修复了长时间运行的bot在跨天后无法生成新日程的问题 +- 优化了日程文本解析功能 +- 修复了解析日程时遇到markdown代码块等额外内容的处理问题 + +### 💻 系统架构优化 +#### 日志系统升级 +- 建立了新的日志系统 +- 改进了错误处理机制 +- 优化了代码格式化规范 + +#### 部署支持扩展 +- 改进了NAS部署指南,增加HOST设置说明 +- 优化了部署文档的完整性 + +### 🐛 问题修复 +#### 功能稳定性 +- 修复了utils_model.py中的潜在问题 +- 修复了set_reply相关bug +- 修复了回应所有戳一戳的问题 +- 优化了bot被戳时的判断逻辑 + +### 📚 文档更新 +- 更新了README.md的内容 +- 完善了NAS部署指南 +- 优化了部署相关文档 + +### 主要改进方向 +1. 提升记忆系统的效率和稳定性 +2. 完善消息处理机制 +3. 优化日程系统功能 +4. 改进日志和错误处理 +5. 加强部署文档的完整性 + + ## [0.5.13] - 2025-3-12 -AI总结 ### 🌟 核心功能增强 #### 记忆系统升级 - 新增了记忆系统的时间戳功能,包括创建时间和最后修改时间 @@ -82,3 +131,5 @@ AI总结 4. 提升开发体验和代码质量 5. 加强系统安全性和稳定性 + + diff --git a/config/auto_update.py b/config/auto_update.py index 28ab108da..d87b7c129 100644 --- a/config/auto_update.py +++ b/config/auto_update.py @@ -42,8 +42,16 @@ def update_config(): update_dict(target[key], value) else: try: - # 直接使用tomlkit的item方法创建新值 - target[key] = tomlkit.item(value) + # 对数组类型进行特殊处理 + if isinstance(value, list): + # 如果是空数组,确保它保持为空数组 + if not value: + target[key] = tomlkit.array() + else: + target[key] = tomlkit.array(value) + else: + # 其他类型使用item方法创建新值 + target[key] = tomlkit.item(value) except (TypeError, ValueError): # 如果转换失败,直接赋值 target[key] = value diff --git a/docs/API_KEY.png b/docs/API_KEY.png new file mode 100644 index 000000000..901d1d137 Binary files /dev/null and b/docs/API_KEY.png differ diff --git a/docs/MONGO_DB_0.png b/docs/MONGO_DB_0.png new file mode 100644 index 000000000..8d91d37d8 Binary files /dev/null and b/docs/MONGO_DB_0.png differ diff --git a/docs/MONGO_DB_1.png b/docs/MONGO_DB_1.png new file mode 100644 index 000000000..0ef3b5590 Binary files /dev/null and b/docs/MONGO_DB_1.png differ diff --git a/docs/MONGO_DB_2.png b/docs/MONGO_DB_2.png new file mode 100644 index 000000000..e59cc8793 Binary files /dev/null and b/docs/MONGO_DB_2.png differ diff --git a/docs/avatars/SengokuCola.jpg b/docs/avatars/SengokuCola.jpg new file mode 100644 index 000000000..deebf5ed5 Binary files /dev/null and b/docs/avatars/SengokuCola.jpg differ diff --git a/docs/avatars/default.png b/docs/avatars/default.png new file mode 100644 index 000000000..5b561dac4 Binary files /dev/null and b/docs/avatars/default.png differ diff --git a/docs/avatars/run.bat b/docs/avatars/run.bat new file mode 100644 index 000000000..6b9ca9f2b --- /dev/null +++ b/docs/avatars/run.bat @@ -0,0 +1 @@ +gource gource.log --user-image-dir docs/avatars/ --default-user-image docs/avatars/default.png \ No newline at end of file diff --git a/docs/fast_q_a.md b/docs/fast_q_a.md new file mode 100644 index 000000000..3b995e24a --- /dev/null +++ b/docs/fast_q_a.md @@ -0,0 +1,149 @@ +## 快速更新Q&A❓ + +
+ +- 这个文件用来记录一些常见的新手问题。 + +
+ +### 完整安装教程 + +
+ +[MaiMbot简易配置教程](https://www.bilibili.com/video/BV1zsQ5YCEE6) + +
+ +### Api相关问题 + +
+ +
+ +- 为什么显示:"缺失必要的API KEY" ❓ + +
+ + + + + +--- + +
+ +>
+> +>你需要在 [Silicon Flow Api](https://cloud.siliconflow.cn/account/ak) +>网站上注册一个账号,然后点击这个链接打开API KEY获取页面。 +> +>点击 "新建API密钥" 按钮新建一个给MaiMBot使用的API KEY。不要忘了点击复制。 +> +>之后打开MaiMBot在你电脑上的文件根目录,使用记事本或者其他文本编辑器打开 [.env.prod](../.env.prod) +>这个文件。把你刚才复制的API KEY填入到 "SILICONFLOW_KEY=" 这个等号的右边。 +> +>在默认情况下,MaiMBot使用的默认Api都是硅基流动的。 +> +>
+ +
+ +
+ + +- 我想使用硅基流动之外的Api网站,我应该怎么做 ❓ + +--- + +
+ +>
+> +>你需要使用记事本或者其他文本编辑器打开config目录下的 [bot_config.toml](../config/bot_config.toml) +>然后修改其中的 "provider = " 字段。同时不要忘记模仿 [.env.prod](../.env.prod) +>文件的写法添加 Api Key 和 Base URL。 +> +>举个例子,如果你写了 " provider = \"ABC\" ",那你需要相应的在 [.env.prod](../.env.prod) +>文件里添加形如 " ABC_BASE_URL = https://api.abc.com/v1 " 和 " ABC_KEY = sk-1145141919810 " 的字段。 +> +>**如果你对AI没有较深的了解,修改识图模型和嵌入模型的provider字段可能会产生bug,因为你从Api网站调用了一个并不存在的模型** +> +>这个时候,你需要把字段的值改回 "provider = \"SILICONFLOW\" " 以此解决bug。 +> +>
+ + +
+ +### MongoDB相关问题 + +
+ +- 我应该怎么清空bot内存储的表情包 ❓ + +--- + +
+ +>
+> +>打开你的MongoDB Compass软件,你会在左上角看到这样的一个界面: +> +>
+> +> +> +>
+> +>点击 "CONNECT" 之后,点击展开 MegBot 标签栏 +> +>
+> +> +> +>
+> +>点进 "emoji" 再点击 "DELETE" 删掉所有条目,如图所示 +> +>
+> +> +> +>
+> +>你可以用类似的方式手动清空MaiMBot的所有服务器数据。 +> +>MaiMBot的所有图片均储存在 [data](../data) 文件夹内,按类型分为 [emoji](../data/emoji) 和 [image](../data/image) +> +>在删除服务器数据时不要忘记清空这些图片。 +> +>
+ +
+ +- 为什么我连接不上MongoDB服务器 ❓ + +--- + + +>
+> +>这个问题比较复杂,但是你可以按照下面的步骤检查,看看具体是什么问题 +> +>
+> +> 1. 检查有没有把 mongod.exe 所在的目录添加到 path。 具体可参照 +> +>
+> +>  [CSDN-windows10设置环境变量Path详细步骤](https://blog.csdn.net/flame_007/article/details/106401215) +> +>
+> +>  **需要往path里填入的是 exe 所在的完整目录!不带 exe 本体** +> +>
+> +> 2. 待完成 +> +>
\ No newline at end of file diff --git a/docs/installation_cute.md b/docs/installation_cute.md index e0c03310f..ca97f18e9 100644 --- a/docs/installation_cute.md +++ b/docs/installation_cute.md @@ -43,13 +43,11 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere的地 ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" -base_url = "SILICONFLOW_BASE_URL" # 告诉机器人:去硅基流动游乐园玩 -key = "SILICONFLOW_KEY" # 用硅基流动的门票进去 +provider = "SILICONFLOW" # 告诉机器人:去硅基流动游乐园玩,机器人会自动用硅基流动的门票进去 [model.llm_normal] name = "Pro/deepseek-ai/DeepSeek-V3" -base_url = "SILICONFLOW_BASE_URL" # 还是去硅基流动游乐园 -key = "SILICONFLOW_KEY" # 用同一张门票就可以啦 +provider = "SILICONFLOW" # 还是去硅基流动游乐园 ``` ### 🎪 举个例子喵 @@ -59,13 +57,11 @@ key = "SILICONFLOW_KEY" # 用同一张门票就可以啦 ```toml [model.llm_reasoning] name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 -base_url = "DEEP_SEEK_BASE_URL" # 改成去DeepSeek游乐园 -key = "DEEP_SEEK_KEY" # 用DeepSeek的门票 +provider = "DEEP_SEEK" # 改成去DeepSeek游乐园 [model.llm_normal] name = "deepseek-chat" # 改成对应的模型名称,这里为DeepseekV3 -base_url = "DEEP_SEEK_BASE_URL" # 也去DeepSeek游乐园 -key = "DEEP_SEEK_KEY" # 用同一张DeepSeek门票 +provider = "DEEP_SEEK" # 也去DeepSeek游乐园 ``` ### 🎯 简单来说 @@ -132,28 +128,35 @@ prompt_personality = [ "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", # 贴吧风格的性格 "是一个女大学生,你有黑色头发,你会刷小红书" # 小红书风格的性格 ] -prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" +prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" # 用来提示机器人每天干什么的提示词喵 [message] min_text_length = 2 # 机器人每次至少要说几个字呢 max_context_size = 15 # 机器人能记住多少条消息喵 emoji_chance = 0.2 # 机器人使用表情的概率哦(0.2就是20%的机会呢) -ban_words = ["脏话", "不文明用语"] # 在这里填写不让机器人说的词 +thinking_timeout = 120 # 机器人思考时间,时间越长能思考的时间越多,但是不要太长喵 + +response_willing_amplifier = 1 # 机器人回复意愿放大系数,增大会让他更愿意聊天喵 +response_interested_rate_amplifier = 1 # 机器人回复兴趣度放大系数,听到记忆里的内容时意愿的放大系数喵 +down_frequency_rate = 3.5 # 降低回复频率的群组回复意愿降低系数 +ban_words = ["脏话", "不文明用语"] # 在这里填写不让机器人说的词,要用英文逗号隔开,每个词都要用英文双引号括起来喵 [emoji] auto_save = true # 是否自动保存看到的表情包呢 enable_check = false # 是否要检查表情包是不是合适的喵 check_prompt = "符合公序良俗" # 检查表情包的标准呢 +[others] +enable_advance_output = true # 是否要显示更多的运行信息呢 +enable_kuuki_read = true # 让机器人能够"察言观色"喵 +enable_debug_output = false # 是否启用调试输出喵 +enable_friend_chat = false # 是否启用好友聊天喵 + [groups] talk_allowed = [123456, 789012] # 比如:让机器人在群123456和789012里说话 talk_frequency_down = [345678] # 比如:在群345678里少说点话 ban_user_id = [111222] # 比如:不回复QQ号为111222的人的消息 -[others] -enable_advance_output = true # 是否要显示更多的运行信息呢 -enable_kuuki_read = true # 让机器人能够"察言观色"喵 - # 模型配置部分的详细说明喵~ @@ -162,46 +165,39 @@ enable_kuuki_read = true # 让机器人能够"察言观色"喵 [model.llm_reasoning] #推理模型R1,用来理解和思考的喵 name = "Pro/deepseek-ai/DeepSeek-R1" # 模型名字 # name = "Qwen/QwQ-32B" # 如果想用千问模型,可以把上面那行注释掉,用这个呢 -base_url = "SILICONFLOW_BASE_URL" # 使用在.env.prod里设置的服务地址 -key = "SILICONFLOW_KEY" # 使用在.env.prod里设置的密钥 +provider = "SILICONFLOW" # 使用在.env.prod里设置的宏,也就是去掉"_BASE_URL"留下来的字喵 [model.llm_reasoning_minor] #R1蒸馏模型,是个轻量版的推理模型喵 name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.llm_normal] #V3模型,用来日常聊天的喵 name = "Pro/deepseek-ai/DeepSeek-V3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.llm_normal_minor] #V2.5模型,是V3的前代版本呢 name = "deepseek-ai/DeepSeek-V2.5" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.vlm] #图像识别模型,让机器人能看懂图片喵 name = "deepseek-ai/deepseek-vl2" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.embedding] #嵌入模型,帮助机器人理解文本的相似度呢 name = "BAAI/bge-m3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" # 如果选择了llm方式提取主题,就用这个模型配置喵 [topic.llm_topic] name = "Pro/deepseek-ai/DeepSeek-V3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" ``` ## 💡 模型配置说明喵 1. **关于模型服务**: - 如果你用硅基流动的服务,这些配置都不用改呢 - - 如果用DeepSeek官方API,要把base_url和key改成你在.env.prod里设置的值喵 + - 如果用DeepSeek官方API,要把provider改成你在.env.prod里设置的宏喵 - 如果要用自定义模型,选择一个相似功能的模型配置来改呢 2. **主要模型功能**: diff --git a/docs/installation_standard.md b/docs/installation_standard.md index dfaf0e797..dcbbf0c99 100644 --- a/docs/installation_standard.md +++ b/docs/installation_standard.md @@ -30,8 +30,7 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1 # ChatAnyWhere API地 ```toml [model.llm_reasoning] name = "Pro/deepseek-ai/DeepSeek-R1" -base_url = "SILICONFLOW_BASE_URL" # 引用.env.prod中定义的地址 -key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 +provider = "SILICONFLOW" # 引用.env.prod中定义的宏 ``` 如需切换到其他API服务,只需修改引用: @@ -39,8 +38,7 @@ key = "SILICONFLOW_KEY" # 引用.env.prod中定义的密钥 ```toml [model.llm_reasoning] name = "deepseek-reasoner" # 改成对应的模型名称,这里为DeepseekR1 -base_url = "DEEP_SEEK_BASE_URL" # 切换为DeepSeek服务 -key = "DEEP_SEEK_KEY" # 使用DeepSeek密钥 +provider = "DEEP_SEEK" # 使用DeepSeek密钥 ``` ## 配置文件详解 @@ -82,7 +80,7 @@ PLUGINS=["src2.plugins.chat"] ```toml [bot] -qq = "机器人QQ号" # 必填 +qq = "机器人QQ号" # 机器人的QQ号,必填 nickname = "麦麦" # 机器人昵称 # alias_names: 配置机器人可使用的别名。当机器人在群聊或对话中被调用时,别名可以作为直接命令或提及机器人的关键字使用。 # 该配置项为字符串数组。例如: ["小麦", "阿麦"] @@ -92,13 +90,18 @@ alias_names = ["小麦", "阿麦"] # 机器人别名 prompt_personality = [ "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", "是一个女大学生,你有黑色头发,你会刷小红书" -] -prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" +] # 人格提示词 +prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" # 日程生成提示词 [message] min_text_length = 2 # 最小回复长度 max_context_size = 15 # 上下文记忆条数 emoji_chance = 0.2 # 表情使用概率 +thinking_timeout = 120 # 机器人思考时间,时间越长能思考的时间越多,但是不要太长 + +response_willing_amplifier = 1 # 机器人回复意愿放大系数,增大会更愿意聊天 +response_interested_rate_amplifier = 1 # 机器人回复兴趣度放大系数,听到记忆里的内容时意愿的放大系数 +down_frequency_rate = 3.5 # 降低回复频率的群组回复意愿降低系数 ban_words = [] # 禁用词列表 [emoji] @@ -112,45 +115,40 @@ talk_frequency_down = [] # 降低回复频率的群号 ban_user_id = [] # 禁止回复的用户QQ号 [others] -enable_advance_output = true # 启用详细日志 -enable_kuuki_read = true # 启用场景理解 +enable_advance_output = true # 是否启用高级输出 +enable_kuuki_read = true # 是否启用读空气功能 +enable_debug_output = false # 是否启用调试输出 +enable_friend_chat = false # 是否启用好友聊天 # 模型配置 [model.llm_reasoning] # 推理模型 name = "Pro/deepseek-ai/DeepSeek-R1" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.llm_reasoning_minor] # 轻量推理模型 name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.llm_normal] # 对话模型 name = "Pro/deepseek-ai/DeepSeek-V3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.llm_normal_minor] # 备用对话模型 name = "deepseek-ai/DeepSeek-V2.5" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.vlm] # 图像识别模型 name = "deepseek-ai/deepseek-vl2" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [model.embedding] # 文本向量模型 name = "BAAI/bge-m3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" [topic.llm_topic] name = "Pro/deepseek-ai/DeepSeek-V3" -base_url = "SILICONFLOW_BASE_URL" -key = "SILICONFLOW_KEY" +provider = "SILICONFLOW" ``` ## 注意事项 diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md index b19f3d6a7..a5c91d6e2 100644 --- a/docs/manual_deploy_linux.md +++ b/docs/manual_deploy_linux.md @@ -121,6 +121,7 @@ sudo nano /etc/systemd/system/maimbot.service 输入以下内容: ``:你的maimbot目录 + ``:你的venv环境(就是上文创建环境后,执行的代码`source maimbot/bin/activate`中source后面的路径的绝对路径) ```ini diff --git a/docs/synology_.env.prod.png b/docs/synology_.env.prod.png new file mode 100644 index 000000000..0bdcacdf3 Binary files /dev/null and b/docs/synology_.env.prod.png differ diff --git a/docs/synology_create_project.png b/docs/synology_create_project.png new file mode 100644 index 000000000..f716d4605 Binary files /dev/null and b/docs/synology_create_project.png differ diff --git a/docs/synology_deploy.md b/docs/synology_deploy.md new file mode 100644 index 000000000..a7b3bebda --- /dev/null +++ b/docs/synology_deploy.md @@ -0,0 +1,68 @@ +# 群晖 NAS 部署指南 + +**笔者使用的是 DSM 7.2.2,其他 DSM 版本的操作可能不完全一样** +**需要使用 Container Manager,群晖的部分部分入门级 NAS 可能不支持** + +## 部署步骤 + +### 创建配置文件目录 + +打开 `DSM ➡️ 控制面板 ➡️ 共享文件夹`,点击 `新增` ,创建一个共享文件夹 +只需要设置名称,其他设置均保持默认即可。如果你已经有 docker 专用的共享文件夹了,就跳过这一步 + +打开 `DSM ➡️ FileStation`, 在共享文件夹中创建一个 `MaiMBot` 文件夹 + +### 准备配置文件 + +docker-compose.yml: https://github.com/SengokuCola/MaiMBot/blob/main/docker-compose.yml +下载后打开,将 `services-mongodb-image` 修改为 `mongo:4.4.24`。这是因为最新的 MongoDB 强制要求 AVX 指令集,而群晖似乎不支持这个指令集 +![](https://raw.githubusercontent.com/ProperSAMA/MaiMBot/refs/heads/debug/docs/synology_docker-compose.png) + +bot_config.toml: https://github.com/SengokuCola/MaiMBot/blob/main/template/bot_config_template.toml +下载后,重命名为 `bot_config.toml` +打开它,按自己的需求填写配置文件 + +.env.prod: https://github.com/SengokuCola/MaiMBot/blob/main/template.env +下载后,重命名为 `.env.prod` +将 `HOST` 修改为 `0.0.0.0`,确保 maimbot 能被 napcat 访问 +按下图修改 mongodb 设置,使用 `MONGODB_URI` +![](https://raw.githubusercontent.com/ProperSAMA/MaiMBot/refs/heads/debug/docs/synology_.env.prod.png) + +把 `bot_config.toml` 和 `.env.prod` 放入之前创建的 `MaiMBot`文件夹 + +#### 如何下载? + +点这里!![](https://raw.githubusercontent.com/ProperSAMA/MaiMBot/refs/heads/debug/docs/synology_how_to_download.png) + +### 创建项目 + +打开 `DSM ➡️ ContainerManager ➡️ 项目`,点击 `新增` 创建项目,填写以下内容: + +- 项目名称: `maimbot` +- 路径:之前创建的 `MaiMBot` 文件夹 +- 来源: `上传 docker-compose.yml` +- 文件:之前下载的 `docker-compose.yml` 文件 + +图例: + +![](https://raw.githubusercontent.com/ProperSAMA/MaiMBot/refs/heads/debug/docs/synology_create_project.png) + +一路点下一步,等待项目创建完成 + +### 设置 Napcat + +1. 登陆 napcat + 打开 napcat: `http://<你的nas地址>:6099` ,输入token登陆 + token可以打开 `DSM ➡️ ContainerManager ➡️ 项目 ➡️ MaiMBot ➡️ 容器 ➡️ Napcat ➡️ 日志`,找到类似 `[WebUi] WebUi Local Panel Url: http://127.0.0.1:6099/webui?token=xxxx` 的日志 + 这个 `token=` 后面的就是你的 napcat token + +2. 按提示,登陆你给麦麦准备的QQ小号 + +3. 设置 websocket 客户端 + `网络配置 -> 新建 -> Websocket客户端`,名称自定,URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可。 + 若修改过容器名称,则替换 `maimbot` 为你自定的名称 + +### 部署完成 + +找个群,发送 `麦麦,你在吗` 之类的 +如果一切正常,应该能正常回复了 \ No newline at end of file diff --git a/docs/synology_docker-compose.png b/docs/synology_docker-compose.png new file mode 100644 index 000000000..f70003e29 Binary files /dev/null and b/docs/synology_docker-compose.png differ diff --git a/docs/synology_how_to_download.png b/docs/synology_how_to_download.png new file mode 100644 index 000000000..011f98876 Binary files /dev/null and b/docs/synology_how_to_download.png differ diff --git a/hort --pretty=format-ad -s b/hort --pretty=format-ad -s deleted file mode 100644 index faeacdd5f..000000000 --- a/hort --pretty=format-ad -s +++ /dev/null @@ -1,141 +0,0 @@ -cbb569e - Create 如果你更新了版本,点我.txt -a91ef7b - 自动升级配置文件脚本 -ed18f2e - 新增了知识库一键启动漂亮脚本 -80ed568 - fix: 删除print调试代码 -c681a82 - 修复小名无效问题 -e54038f - fix: 从 nixpkgs 增加 numpy 依赖,以避免出现 libc++.so 找不到的问题 -26782c9 - fix: 修复 ENVIRONMENT 变量在同一终端下不能被覆盖的问题 -8c34637 - 提高健壮性 -2688a96 - close SengokuCola/MaiMBot#225 让麦麦可以正确读取分享卡片 -cd16e68 - 修复表情包发送时的缺失参数 -b362c35 - feat: 更新 flake.nix ,采用 venv 的方式生成环境,nixos用户也可以本机运行项目了 -3c8c897 - 屏蔽一个臃肿的debug信息 -9d0152a - 修复了合并过程中造成的代码重复 -956135c - 添加一些注释 -a412741 - 将print变为logger.debug -3180426 - 修复了没有改掉的typo字段 -aea3bff - 添加私聊过滤开关,更新config,增加约束 -cda6281 - chore: update emoji_manager.py -baed856 - 修正了私聊屏蔽词输出 -66a0f18 - 修复了私聊时产生reply消息的bug -3bf5cd6 - feat: 新增运行时重载配置文件;新增根据不同环境(dev;prod)显示不同级别的log -33cd83b - 添加私聊功能 -aa41f0d - fix: 放反了 -ef8691c - fix: 修改message继承逻辑,修复回复消息无法识别 -7d017be - fix:模型降级 -e1019ad - fix: 修复变量拼写错误并优化代码可读性 -c24bb70 - fix: 流式输出模式增加结束判断与token用量记录 -60a9376 - 添加logger的debug输出开关,默认为不开启 -bfa9a3c - fix: 添加群信息获取的错误处理 (#173) -4cc5c8e - 修正.env.prod和.env.dev的生成 -dea14c1 - fix: 模型降级目前只对硅基流动的V3和R1生效 -b6edbea - fix: 图片保存路径不正确 -01a6fa8 - fix: 删除神秘test -20f009d - 修复systemctl强制停止maimbot的问题 -af962c2 - 修复了情绪管理器没有正确导入导致发布出消息 -0586700 - 按照Sourcery提供的建议修改systemctl管理指南 -e48b32a - 在手动部署教程中增加使用systemctl管理 -5760412 - fix: 小修 -1c9b0cc - fix: 修复部分cq码解析错误,merge -b6867b9 - fix: 统一使用os.getenv获取数据库连接信息,避免从config对象获取不存在的值时出现KeyError -5e069f7 - 修复记忆保存时无时间信息的bug -73a3e41 - 修复记忆更新bug -52c93ba - refactor: use Base64 for emoji CQ codes -67f6d7c - fix: 保证能运行的小修改 -c32c4fb - refactor: 修改配置文件的版本号 -a54ca8c - Merge remote-tracking branch 'upstream/debug' into feat_regix -8cbf9bb - feat: 史上最好的消息流重构和图片管理 -9e41c4f - feat: 修改 bot_config 0.0.5 版本的变更日志 -eede406 - fix: 修复nonebot无法加载项目的问题 -00e02ed - fix: 0.0.5 版本的增加分层控制项 -0f99d6a - Update docs/docker_deploy.md -c789074 - feat: 增加ruff依赖 -ff65ab8 - feat: 修改默认的ruff配置文件,同时消除config的所有不符合规范的地方 -bf97013 - feat: 精简日志,禁用Uvicorn/NoneBot默认日志;启动方式改为显示加载uvicorn,以便优雅shutdown -d9a2863 - 优化Docker部署文档更新容器部分 -efcf00f - Docker部署文档追加更新部分 -a63ce96 - fix: 更新情感判断模型配置(使配置文件里的 llm_emotion_judge 生效) -1294c88 - feat: 增加标准化格式化设置 -2e8cd47 - fix: 避免可能出现的日程解析错误 -043a724 - 修一下文档跳转,小美化( -e4b8865 - 支持别名,可以用不同名称召唤机器人 -7b35ddd - ruff 哥又有新点子 -7899e67 - feat: 重构完成开始测试debug -354d6d0 - 记忆系统优化 -6cef8fd - 修复时区,删去napcat用不到的端口 -cd96644 - 添加使用说明 -84495f8 - fix -204744c - 修改配置名与修改过滤对象为raw_message -a03b490 - Update README.md -2b2b342 - feat: 增加 ruff 依赖 -72a6749 - fix: 修复docker部署时区指定问题 -ee579bc - Update README.md -1b611ec - resolve SengokuCola/MaiMBot#167 根据正则表达式过滤消息 -6e2ea82 - refractor: 几乎写完了,进入测试阶段 -2ffdfef - More -e680405 - fix: typo 'discription' -68b3f57 - Minor Doc Update -312f065 - Create linux_deploy_guide_for_beginners.md -ed505a4 - fix: 使用动态路径替换硬编码的项目路径 -8ff7bb6 - docs: 更新文档,修正格式并添加必要的换行符 -6e36a56 - feat: 增加 MONGODB_URI 的配置项,并将所有env文件的注释单独放在一行(python的dotenv有时无法正确处理行内注释) -4baa6c6 - feat: 实现MongoDB URI方式连接,并统一数据库连接代码。 -8a32d18 - feat: 优化willing_manager逻辑,增加回复保底概率 -c9f1244 - docs: 改进README.md文档格式和排版 -e1b484a - docs: 添加CLAUDE.md开发指南文件(用于Claude Code) -a43f949 - fix: remove duplicate message(CR comments) -fddb641 - fix: 修复错误的空值检测逻辑 -8b7876c - fix: 修复没有上传tag的问题 -6b4130e - feat: 增加stable-dev分支的打包 -052e67b - refactor: 日志打印优化(终于改完了,爽了 -a7f9d05 - 修复记忆整理传入格式问题 -536bb1d - fix: 更新情感判断模型配置 -8d99592 - fix: logger初始化顺序 -052802c - refactor: logger promotion -8661d94 - doc: README.md - telegram version information -5746afa - refactor: logger in src\plugins\chat\bot.py -288dbb6 - refactor: logger in src\plugins\chat\__init__.py -8428a06 - fix: memory logger optimization (CR comment) -665c459 - 改进了可视化脚本 -6c35704 - fix: 调用了错误的函数 -3223153 - feat: 一键脚本新增记忆可视化 -3149dd3 - fix: mongodb.zip 无法解压 fix:更换执行命令的方法 fix:当 db 不存在时自动创建 feat: 一键安装完成后启动麦麦 -089d6a6 - feat: 针对硅基流动的Pro模型添加了自动降级功能 -c4b0917 - 一个记忆可视化小脚本 -6a71ea4 - 修复了记忆时间bug,config添加了记忆屏蔽关键词 -1b5344f - fix: 优化bot初始化的日志&格式 -41aa974 - fix: 优化chat/config.py的日志&格式 -980cde7 - fix: 优化scheduler_generator日志&格式 -31a5514 - fix: 调整全局logger加载顺序 -8baef07 - feat: 添加全局logger初始化设置 -5566f17 - refractor: 几乎写完了,进入测试阶段 -6a66933 - feat: 添加开发环境.env.dev初始化 -411ff1a - feat: 安装 MongoDB Compass -0de9eba - feat: 增加实时更新贡献者列表的功能 -f327f45 - fix: 优化src/plugins/chat/__init__.py的import -826daa5 - fix: 当虚拟环境存在时跳过创建 -f54de42 - fix: time.tzset 仅在类 Unix 系统可用 -47c4990 - fix: 修复docker部署场景下时间错误的问题 -e23a371 - docs: 添加 compose 注释 -1002822 - docs: 标注 Python 最低版本 -564350d - feat: 校验 Python 版本 -4cc4482 - docs: 添加傻瓜式脚本 -757173a - 带麦麦看了心理医生,让她没那么容易陷入负面情绪 -39bb99c - 将错别字生成提取到配置,一句一个错别字太烦了! -fe36847 - feat: 超大型重构 -e304dd7 - Update README.md -b7cfe6d - feat: 发布第 0.0.2 版本配置模板 -ca929d5 - 补充Docker部署文档 -1e97120 - 补充Docker部署文档 -25f7052 - fix: 修复兼容性选项和目前第一个版本之间的版本间隙 0.0.0 版,并将所有的直接退出修改为抛出异常 -c5bdc4f - 防ipv6炸,虽然小概率事件 -d86610d - fix: 修复不能加载环境变量的问题 -2306ebf - feat: 因为判断临界版本范围比较麻烦,增加 notice 字段,删除原本的判断逻辑(存在故障) -dd09576 - fix: 修复 TypeError: BotConfig.convert_to_specifierset() takes 1 positional argument but 2 were given -18f839b - fix: 修复 missing 1 required positional argument: 'INNER_VERSION' -6adb5ed - 调整一些细节,docker部署时可选数据库账密 -07f48e9 - fix: 利用filter来过滤环境变量,避免直接删除key造成的 RuntimeError: dictionary changed size during iteration -5856074 - fix: 修复无法进行基础设置的问题 -32aa032 - feat: 发布 0.0.1 版本的配置文件 -edc07ac - feat: 重构配置加载器,增加配置文件版本控制和程序兼容能力 -0f492ed - fix: 修复 BASE_URL/KEY 组合检查中被 GPG_KEY 干扰的问题 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8330c8d06..1e9e5ff25 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/run-WebUI.bat b/run-WebUI.bat new file mode 100644 index 000000000..8fbbe3dbf --- /dev/null +++ b/run-WebUI.bat @@ -0,0 +1,4 @@ +CHCP 65001 +@echo off +python webui.py +pause \ No newline at end of file diff --git a/run_debian12.sh b/run_debian12.sh new file mode 100644 index 000000000..5a51a1a39 --- /dev/null +++ b/run_debian12.sh @@ -0,0 +1,422 @@ +#!/bin/bash + +# 麦麦Bot一键安装脚本 by Cookie_987 +# 适用于Debian12 +# 请小心使用任何一键脚本! + +LANG=C.UTF-8 + +# 如无法访问GitHub请修改此处镜像地址 +GITHUB_REPO="https://ghfast.top/https://github.com/SengokuCola/MaiMBot.git" + +# 颜色输出 +GREEN="\e[32m" +RED="\e[31m" +RESET="\e[0m" + +# 需要的基本软件包 +REQUIRED_PACKAGES=("git" "sudo" "python3" "python3-venv" "curl" "gnupg" "python3-pip") + +# 默认项目目录 +DEFAULT_INSTALL_DIR="/opt/maimbot" + +# 服务名称 +SERVICE_NAME="maimbot-daemon" +SERVICE_NAME_WEB="maimbot-web" + +IS_INSTALL_MONGODB=false +IS_INSTALL_NAPCAT=false +IS_INSTALL_DEPENDENCIES=false + +INSTALLER_VERSION="0.0.1" + +# 检查是否已安装 +check_installed() { + [[ -f /etc/systemd/system/${SERVICE_NAME}.service ]] +} + +# 加载安装信息 +load_install_info() { + if [[ -f /etc/maimbot_install.conf ]]; then + source /etc/maimbot_install.conf + else + INSTALL_DIR="$DEFAULT_INSTALL_DIR" + BRANCH="main" + fi +} + +# 显示管理菜单 +show_menu() { + while true; do + choice=$(whiptail --title "麦麦Bot管理菜单" --menu "请选择要执行的操作:" 15 60 7 \ + "1" "启动麦麦Bot" \ + "2" "停止麦麦Bot" \ + "3" "重启麦麦Bot" \ + "4" "启动WebUI" \ + "5" "停止WebUI" \ + "6" "重启WebUI" \ + "7" "更新麦麦Bot及其依赖" \ + "8" "切换分支" \ + "9" "更新配置文件" \ + "10" "退出" 3>&1 1>&2 2>&3) + + [[ $? -ne 0 ]] && exit 0 + + case "$choice" in + 1) + systemctl start ${SERVICE_NAME} + whiptail --msgbox "✅麦麦Bot已启动" 10 60 + ;; + 2) + systemctl stop ${SERVICE_NAME} + whiptail --msgbox "🛑麦麦Bot已停止" 10 60 + ;; + 3) + systemctl restart ${SERVICE_NAME} + whiptail --msgbox "🔄麦麦Bot已重启" 10 60 + ;; + 4) + systemctl start ${SERVICE_NAME_WEB} + whiptail --msgbox "✅WebUI已启动" 10 60 + ;; + 5) + systemctl stop ${SERVICE_NAME_WEB} + whiptail --msgbox "🛑WebUI已停止" 10 60 + ;; + 6) + systemctl restart ${SERVICE_NAME_WEB} + whiptail --msgbox "🔄WebUI已重启" 10 60 + ;; + 7) + update_dependencies + ;; + 8) + switch_branch + ;; + 9) + update_config + ;; + 10) + exit 0 + ;; + *) + whiptail --msgbox "无效选项!" 10 60 + ;; + esac + done +} + +# 更新依赖 +update_dependencies() { + cd "${INSTALL_DIR}/repo" || { + whiptail --msgbox "🚫 无法进入安装目录!" 10 60 + return 1 + } + if ! git pull origin "${BRANCH}"; then + whiptail --msgbox "🚫 代码更新失败!" 10 60 + return 1 + fi + source "${INSTALL_DIR}/venv/bin/activate" + if ! pip install -r requirements.txt; then + whiptail --msgbox "🚫 依赖安装失败!" 10 60 + deactivate + return 1 + fi + deactivate + systemctl restart ${SERVICE_NAME} + whiptail --msgbox "✅ 依赖已更新并重启服务!" 10 60 +} + +# 切换分支 +switch_branch() { + new_branch=$(whiptail --inputbox "请输入要切换的分支名称:" 10 60 "${BRANCH}" 3>&1 1>&2 2>&3) + [[ -z "$new_branch" ]] && { + whiptail --msgbox "🚫 分支名称不能为空!" 10 60 + return 1 + } + + cd "${INSTALL_DIR}/repo" || { + whiptail --msgbox "🚫 无法进入安装目录!" 10 60 + return 1 + } + + if ! git ls-remote --exit-code --heads origin "${new_branch}" >/dev/null 2>&1; then + whiptail --msgbox "🚫 分支 ${new_branch} 不存在!" 10 60 + return 1 + fi + + if ! git checkout "${new_branch}"; then + whiptail --msgbox "🚫 分支切换失败!" 10 60 + return 1 + fi + + if ! git pull origin "${new_branch}"; then + whiptail --msgbox "🚫 代码拉取失败!" 10 60 + return 1 + fi + + source "${INSTALL_DIR}/venv/bin/activate" + pip install -r requirements.txt + deactivate + + sed -i "s/^BRANCH=.*/BRANCH=${new_branch}/" /etc/maimbot_install.conf + BRANCH="${new_branch}" + systemctl restart ${SERVICE_NAME} + touch "${INSTALL_DIR}/repo/elua.confirmed" + whiptail --msgbox "✅ 已切换到分支 ${new_branch} 并重启服务!" 10 60 +} + +# 更新配置文件 +update_config() { + cd "${INSTALL_DIR}/repo" || { + whiptail --msgbox "🚫 无法进入安装目录!" 10 60 + return 1 + } + if [[ -f config/bot_config.toml ]]; then + cp config/bot_config.toml config/bot_config.toml.bak + whiptail --msgbox "📁 原配置文件已备份为 bot_config.toml.bak" 10 60 + source "${INSTALL_DIR}/venv/bin/activate" + python3 config/auto_update.py + deactivate + whiptail --msgbox "🆕 已更新配置文件,请重启麦麦Bot!" 10 60 + return 0 + else + whiptail --msgbox "🚫 未找到配置文件 bot_config.toml\n 请先运行一次麦麦Bot" 10 60 + return 1 + fi +} + +# ----------- 主安装流程 ----------- +run_installation() { + # 1/6: 检测是否安装 whiptail + if ! command -v whiptail &>/dev/null; then + echo -e "${RED}[1/6] whiptail 未安装,正在安装...${RESET}" + apt update && apt install -y whiptail + fi + + # 协议确认 + if ! (whiptail --title "ℹ️ [1/6] 使用协议" --yes-button "我同意" --no-button "我拒绝" --yesno "使用麦麦Bot及此脚本前请先阅读ELUA协议\nhttps://github.com/SengokuCola/MaiMBot/blob/main/EULA.md\n\n您是否同意此协议?" 12 70); then + exit 1 + fi + + # 欢迎信息 + whiptail --title "[2/6] 欢迎使用麦麦Bot一键安装脚本 by Cookie987" --msgbox "检测到您未安装麦麦Bot,将自动进入安装流程,安装完成后再次运行此脚本即可进入管理菜单。\n\n项目处于活跃开发阶段,代码可能随时更改\n文档未完善,有问题可以提交 Issue 或者 Discussion\nQQ机器人存在被限制风险,请自行了解,谨慎使用\n由于持续迭代,可能存在一些已知或未知的bug\n由于开发中,可能消耗较多token\n\n本脚本可能更新不及时,如遇到bug请优先尝试手动部署以确定是否为脚本问题" 17 60 + + # 系统检查 + check_system() { + if [[ "$(id -u)" -ne 0 ]]; then + whiptail --title "🚫 权限不足" --msgbox "请使用 root 用户运行此脚本!\n执行方式: sudo bash $0" 10 60 + exit 1 + fi + + if [[ -f /etc/os-release ]]; then + source /etc/os-release + if [[ "$ID" != "debian" || "$VERSION_ID" != "12" ]]; then + whiptail --title "🚫 不支持的系统" --msgbox "此脚本仅支持 Debian 12 (Bookworm)!\n当前系统: $PRETTY_NAME\n安装已终止。" 10 60 + exit 1 + fi + else + whiptail --title "⚠️ 无法检测系统" --msgbox "无法识别系统版本,安装已终止。" 10 60 + exit 1 + fi + } + check_system + + # 检查MongoDB + check_mongodb() { + if command -v mongod &>/dev/null; then + MONGO_INSTALLED=true + else + MONGO_INSTALLED=false + fi + } + check_mongodb + + # 检查NapCat + check_napcat() { + if command -v napcat &>/dev/null; then + NAPCAT_INSTALLED=true + else + NAPCAT_INSTALLED=false + fi + } + check_napcat + + # 安装必要软件包 + install_packages() { + missing_packages=() + for package in "${REQUIRED_PACKAGES[@]}"; do + if ! dpkg -s "$package" &>/dev/null; then + missing_packages+=("$package") + fi + done + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到以下必须的依赖项目缺失:\n${missing_packages[*]}\n\n是否要自动安装?" 12 60 + if [[ $? -eq 0 ]]; then + IS_INSTALL_DEPENDENCIES=true + else + whiptail --title "⚠️ 注意" --yesno "某些必要的依赖项未安装,可能会影响运行!\n是否继续?" 10 60 || exit 1 + fi + fi + } + install_packages + + # 安装MongoDB + install_mongodb() { + [[ $MONGO_INSTALLED == true ]] && return + whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装MongoDB,是否安装?\n如果您想使用远程数据库,请跳过此步。" 10 60 && { + echo -e "${GREEN}安装 MongoDB...${RESET}" + curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + apt update + apt install -y mongodb-org + systemctl enable --now mongod + IS_INSTALL_MONGODB=true + } + } + install_mongodb + + # 安装NapCat + install_napcat() { + [[ $NAPCAT_INSTALLED == true ]] && return + whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装NapCat,是否安装?\n如果您想使用远程NapCat,请跳过此步。" 10 60 && { + echo -e "${GREEN}安装 NapCat...${RESET}" + curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && bash napcat.sh --cli y --docker n + IS_INSTALL_NAPCAT=true + } + } + install_napcat + + # 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 + exit 1 + fi + } + check_python + + # 选择分支 + choose_branch() { + BRANCH=$(whiptail --title "🔀 [5/6] 选择麦麦Bot分支" --menu "请选择要安装的麦麦Bot分支:" 15 60 2 \ + "main" "稳定版本(推荐,供下载使用)" \ + "main-fix" "生产环境紧急修复" 3>&1 1>&2 2>&3) + [[ -z "$BRANCH" ]] && BRANCH="main" + } + choose_branch + + # 选择安装路径 + choose_install_dir() { + INSTALL_DIR=$(whiptail --title "📂 [6/6] 选择安装路径" --inputbox "请输入麦麦Bot的安装目录:" 10 60 "$DEFAULT_INSTALL_DIR" 3>&1 1>&2 2>&3) + [[ -z "$INSTALL_DIR" ]] && { + whiptail --title "⚠️ 取消输入" --yesno "未输入安装路径,是否退出安装?" 10 60 && exit 1 + INSTALL_DIR="$DEFAULT_INSTALL_DIR" + } + } + choose_install_dir + + # 确认安装 + confirm_install() { + local confirm_msg="请确认以下信息:\n\n" + confirm_msg+="📂 安装麦麦Bot到: $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" + + [[ $IS_INSTALL_MONGODB == true ]] && confirm_msg+=" - MongoDB\n" + [[ $IS_INSTALL_NAPCAT == true ]] && confirm_msg+=" - NapCat\n" + confirm_msg+="\n注意:本脚本默认使用ghfast.top为GitHub进行加速,如不想使用请手动修改脚本开头的GITHUB_REPO变量。" + + whiptail --title "🔧 安装确认" --yesno "$confirm_msg" 16 60 || exit 1 + } + confirm_install + + # 开始安装 + echo -e "${GREEN}安装依赖...${RESET}" + [[ $IS_INSTALL_DEPENDENCIES == true ]] && apt update && apt install -y "${missing_packages[@]}" + + echo -e "${GREEN}创建安装目录...${RESET}" + mkdir -p "$INSTALL_DIR" + cd "$INSTALL_DIR" || exit 1 + + echo -e "${GREEN}设置Python虚拟环境...${RESET}" + python3 -m venv venv + source venv/bin/activate + + echo -e "${GREEN}克隆仓库...${RESET}" + git clone -b "$BRANCH" "$GITHUB_REPO" repo || { + echo -e "${RED}克隆仓库失败!${RESET}" + exit 1 + } + + echo -e "${GREEN}安装Python依赖...${RESET}" + pip install -r repo/requirements.txt + + echo -e "${GREEN}同意协议...${RESET}" + touch repo/elua.confirmed + + echo -e "${GREEN}创建系统服务...${RESET}" + cat > /etc/systemd/system/${SERVICE_NAME}.service < /etc/systemd/system/${SERVICE_NAME_WEB}.service < /etc/maimbot_install.conf + echo "INSTALL_DIR=${INSTALL_DIR}" >> /etc/maimbot_install.conf + echo "BRANCH=${BRANCH}" >> /etc/maimbot_install.conf + + whiptail --title "🎉 安装完成" --msgbox "麦麦Bot安装完成!\n已创建系统服务:${SERVICE_NAME},${SERVICE_NAME_WEB}\n\n使用以下命令管理服务:\n启动服务:systemctl start ${SERVICE_NAME}\n查看状态:systemctl status ${SERVICE_NAME}" 14 60 +} + +# ----------- 主执行流程 ----------- +# 检查root权限 +[[ $(id -u) -ne 0 ]] && { + echo -e "${RED}请使用root用户运行此脚本!${RESET}" + exit 1 +} + +# 如果已安装显示菜单 +if check_installed; then + load_install_info + show_menu +else + run_installation + # 安装完成后询问是否启动 + if whiptail --title "安装完成" --yesno "是否立即启动麦麦Bot服务?" 10 60; then + systemctl start ${SERVICE_NAME} + whiptail --msgbox "✅ 服务已启动!\n使用 systemctl status ${SERVICE_NAME} 查看状态" 10 60 + fi +fi diff --git a/src/common/database.py b/src/common/database.py index d592b0f90..cd149e526 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -1,51 +1,51 @@ -from typing import Optional +import os +from typing import cast from pymongo import MongoClient +from pymongo.database import Database -class Database: - _instance: Optional["Database"] = None - - def __init__( - self, - host: str, - port: int, - db_name: str, - username: Optional[str] = None, - password: Optional[str] = None, - auth_source: Optional[str] = None, - uri: Optional[str] = None, - ): - if uri and uri.startswith("mongodb://"): - # 优先使用URI连接 - self.client = MongoClient(uri) - elif username and password: - # 如果有用户名和密码,使用认证连接 - self.client = MongoClient( - host, port, username=username, password=password, authSource=auth_source - ) - else: - # 否则使用无认证连接 - self.client = MongoClient(host, port) - self.db = self.client[db_name] - - @classmethod - def initialize( - cls, - host: str, - port: int, - db_name: str, - username: Optional[str] = None, - password: Optional[str] = None, - auth_source: Optional[str] = None, - uri: Optional[str] = None, - ) -> "Database": - if cls._instance is None: - cls._instance = cls( - host, port, db_name, username, password, auth_source, uri - ) - return cls._instance - - @classmethod - def get_instance(cls) -> "Database": - if cls._instance is None: - raise RuntimeError("Database not initialized") - return cls._instance \ No newline at end of file +_client = None +_db = None + + +def __create_database_instance(): + uri = os.getenv("MONGODB_URI") + host = os.getenv("MONGODB_HOST", "127.0.0.1") + port = int(os.getenv("MONGODB_PORT", "27017")) + db_name = os.getenv("DATABASE_NAME", "MegBot") + username = os.getenv("MONGODB_USERNAME") + password = os.getenv("MONGODB_PASSWORD") + auth_source = os.getenv("MONGODB_AUTH_SOURCE") + + if uri and uri.startswith("mongodb://"): + # 优先使用URI连接 + return MongoClient(uri) + + if username and password: + # 如果有用户名和密码,使用认证连接 + return MongoClient(host, port, username=username, password=password, authSource=auth_source) + + # 否则使用无认证连接 + return MongoClient(host, port) + + +def get_db(): + """获取数据库连接实例,延迟初始化。""" + global _client, _db + if _client is None: + _client = __create_database_instance() + _db = _client[os.getenv("DATABASE_NAME", "MegBot")] + return _db + + +class DBWrapper: + """数据库代理类,保持接口兼容性同时实现懒加载。""" + + def __getattr__(self, name): + return getattr(get_db(), name) + + def __getitem__(self, key): + return get_db()[key] + + +# 全局数据库访问点 +db: Database = DBWrapper() diff --git a/src/common/logger.py b/src/common/logger.py new file mode 100644 index 000000000..c546b700b --- /dev/null +++ b/src/common/logger.py @@ -0,0 +1,198 @@ +from loguru import logger +from typing import Dict, Optional, Union, List +import sys +import os +from types import ModuleType +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +# 保存原生处理器ID +default_handler_id = None +for handler_id in logger._core.handlers: + default_handler_id = handler_id + break + +# 移除默认处理器 +if default_handler_id is not None: + logger.remove(default_handler_id) + +# 类型别名 +LoguruLogger = logger.__class__ + +# 全局注册表:记录模块与处理器ID的映射 +_handler_registry: Dict[str, List[int]] = {} + +# 获取日志存储根地址 +current_file_path = Path(__file__).resolve() +LOG_ROOT = "logs" + +# 默认全局配置 +DEFAULT_CONFIG = { + # 日志级别配置 + "console_level": "INFO", + "file_level": "DEBUG", + + # 格式配置 + "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}" + ), + "log_dir": LOG_ROOT, + "rotation": "00:00", + "retention": "3 days", + "compression": "zip", +} + + +def is_registered_module(record: dict) -> bool: + """检查是否为已注册的模块""" + return record["extra"].get("module") in _handler_registry + + +def is_unregistered_module(record: dict) -> bool: + """检查是否为未注册的模块""" + return not is_registered_module(record) + + +def log_patcher(record: dict) -> None: + """自动填充未设置模块名的日志记录,保留原生模块名称""" + if "module" not in record["extra"]: + # 尝试从name中提取模块名 + module_name = record.get("name", "") + if module_name == "": + module_name = "root" + record["extra"]["module"] = module_name + + +# 应用全局修补器 +logger.configure(patcher=log_patcher) + + +class LogConfig: + """日志配置类""" + + def __init__(self, **kwargs): + self.config = DEFAULT_CONFIG.copy() + self.config.update(kwargs) + + def to_dict(self) -> dict: + return self.config.copy() + + def update(self, **kwargs): + self.config.update(kwargs) + + +def get_module_logger( + module: Union[str, ModuleType], + *, + console_level: Optional[str] = None, + file_level: Optional[str] = None, + extra_handlers: Optional[List[dict]] = None, + config: Optional[LogConfig] = None +) -> LoguruLogger: + module_name = module if isinstance(module, str) else module.__name__ + current_config = config.config if config else DEFAULT_CONFIG + + # 清理旧处理器 + if module_name in _handler_registry: + for handler_id in _handler_registry[module_name]: + logger.remove(handler_id) + del _handler_registry[module_name] + + handler_ids = [] + + # 控制台处理器 + console_id = logger.add( + sink=sys.stderr, + level=os.getenv("CONSOLE_LOG_LEVEL", console_level or current_config["console_level"]), + format=current_config["console_format"], + filter=lambda record: record["extra"].get("module") == module_name, + enqueue=True, + ) + handler_ids.append(console_id) + + # 文件处理器 + log_dir = Path(current_config["log_dir"]) + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / module_name / f"{{time:YYYY-MM-DD}}.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + + file_id = logger.add( + sink=str(log_file), + level=os.getenv("FILE_LOG_LEVEL", file_level or current_config["file_level"]), + format=current_config["file_format"], + rotation=current_config["rotation"], + retention=current_config["retention"], + compression=current_config["compression"], + encoding="utf-8", + filter=lambda record: record["extra"].get("module") == module_name, + enqueue=True, + ) + handler_ids.append(file_id) + + # 额外处理器 + if extra_handlers: + for handler in extra_handlers: + handler_id = logger.add(**handler) + handler_ids.append(handler_id) + + # 更新注册表 + _handler_registry[module_name] = handler_ids + + return logger.bind(module=module_name) + + +def remove_module_logger(module_name: str) -> None: + """清理指定模块的日志处理器""" + if module_name in _handler_registry: + for handler_id in _handler_registry[module_name]: + logger.remove(handler_id) + del _handler_registry[module_name] + + +# 添加全局默认处理器(只处理未注册模块的日志--->控制台) +DEFAULT_GLOBAL_HANDLER = logger.add( + sink=sys.stderr, + level=os.getenv("DEFAULT_CONSOLE_LOG_LEVEL", "SUCCESS"), + format=( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{name: <12} | " + "{message}" + ), + filter=is_unregistered_module, # 只处理未注册模块的日志 + enqueue=True, +) + +# 添加全局默认文件处理器(只处理未注册模块的日志--->logs文件夹) +log_dir = Path(DEFAULT_CONFIG["log_dir"]) +log_dir.mkdir(parents=True, exist_ok=True) +other_log_dir = log_dir / "other" +other_log_dir.mkdir(parents=True, exist_ok=True) + +DEFAULT_FILE_HANDLER = logger.add( + sink=str(other_log_dir / f"{{time:YYYY-MM-DD}}.log"), + level=os.getenv("DEFAULT_FILE_LOG_LEVEL", "DEBUG"), + format=( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{name: <15} | " + "{message}" + ), + rotation=DEFAULT_CONFIG["rotation"], + retention=DEFAULT_CONFIG["retention"], + compression=DEFAULT_CONFIG["compression"], + encoding="utf-8", + filter=is_unregistered_module, # 只处理未注册模块的日志 + enqueue=True, +) diff --git a/src/gui/logger_gui.py b/src/gui/logger_gui.py new file mode 100644 index 000000000..f2dd698cd --- /dev/null +++ b/src/gui/logger_gui.py @@ -0,0 +1,347 @@ +import customtkinter as ctk +import subprocess +import threading +import queue +import re +import os +import signal +from collections import deque + +# 设置应用的外观模式和默认颜色主题 +ctk.set_appearance_mode("dark") +ctk.set_default_color_theme("blue") + + +class LogViewerApp(ctk.CTk): + """日志查看器应用的主类,继承自customtkinter的CTk类""" + + def __init__(self): + """初始化日志查看器应用的界面和状态""" + super().__init__() + self.title("日志查看器") + self.geometry("1200x800") + + # 初始化进程、日志队列、日志数据等变量 + self.process = None + self.log_queue = queue.Queue() + self.log_data = deque(maxlen=10000) # 使用固定长度队列 + self.available_levels = set() + self.available_modules = set() + self.sorted_modules = [] + self.module_checkboxes = {} # 存储模块复选框的字典 + + # 日志颜色配置 + self.color_config = { + "time": "#888888", + "DEBUG": "#2196F3", + "INFO": "#4CAF50", + "WARNING": "#FF9800", + "ERROR": "#F44336", + "module": "#D4D0AB", + "default": "#FFFFFF", + } + + # 列可见性配置 + self.column_visibility = {"show_time": True, "show_level": True, "show_module": True} + + # 选中的日志等级和模块 + self.selected_levels = set() + self.selected_modules = set() + + # 创建界面组件并启动日志队列处理 + self.create_widgets() + self.after(100, self.process_log_queue) + + def create_widgets(self): + """创建应用界面的各个组件""" + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=1) + + # 控制面板 + control_frame = ctk.CTkFrame(self) + control_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5) + + self.start_btn = ctk.CTkButton(control_frame, text="启动", command=self.start_process) + self.start_btn.pack(side="left", padx=5) + + self.stop_btn = ctk.CTkButton(control_frame, text="停止", command=self.stop_process, state="disabled") + self.stop_btn.pack(side="left", padx=5) + + self.clear_btn = ctk.CTkButton(control_frame, text="清屏", command=self.clear_logs) + self.clear_btn.pack(side="left", padx=5) + + column_filter_frame = ctk.CTkFrame(control_frame) + column_filter_frame.pack(side="left", padx=20) + + self.time_check = ctk.CTkCheckBox(column_filter_frame, text="显示时间", command=self.refresh_logs) + self.time_check.pack(side="left", padx=5) + self.time_check.select() + + self.level_check = ctk.CTkCheckBox(column_filter_frame, text="显示等级", command=self.refresh_logs) + self.level_check.pack(side="left", padx=5) + self.level_check.select() + + self.module_check = ctk.CTkCheckBox(column_filter_frame, text="显示模块", command=self.refresh_logs) + self.module_check.pack(side="left", padx=5) + self.module_check.select() + + # 筛选面板 + filter_frame = ctk.CTkFrame(self) + filter_frame.grid(row=0, column=1, rowspan=2, sticky="ns", padx=5) + + ctk.CTkLabel(filter_frame, text="日志等级筛选").pack(pady=5) + self.level_scroll = ctk.CTkScrollableFrame(filter_frame, width=150, height=200) + self.level_scroll.pack(fill="both", expand=True, padx=5) + + ctk.CTkLabel(filter_frame, text="模块筛选").pack(pady=5) + self.module_filter_entry = ctk.CTkEntry(filter_frame, placeholder_text="输入模块过滤词") + self.module_filter_entry.pack(pady=5) + self.module_filter_entry.bind("", self.update_module_filter) + + self.module_scroll = ctk.CTkScrollableFrame(filter_frame, width=300, height=200) + self.module_scroll.pack(fill="both", expand=True, padx=5) + + self.log_text = ctk.CTkTextbox(self, wrap="word") + self.log_text.grid(row=1, column=0, sticky="nsew", padx=10, pady=5) + + self.init_text_tags() + + def update_module_filter(self, event): + """根据模块过滤词更新模块复选框的显示""" + filter_text = self.module_filter_entry.get().strip().lower() + for module, checkbox in self.module_checkboxes.items(): + if filter_text in module.lower(): + checkbox.pack(anchor="w", padx=5, pady=2) + else: + checkbox.pack_forget() + + def update_filters(self, level, module): + """更新日志等级和模块的筛选器""" + if level not in self.available_levels: + self.available_levels.add(level) + self.add_checkbox(self.level_scroll, level, "level") + + module_key = self.get_module_key(module) + if module_key not in self.available_modules: + self.available_modules.add(module_key) + self.sorted_modules = sorted(self.available_modules, key=lambda x: x.lower()) + self.rebuild_module_checkboxes() + + def rebuild_module_checkboxes(self): + """重新构建模块复选框""" + # 清空现有复选框 + for widget in self.module_scroll.winfo_children(): + widget.destroy() + self.module_checkboxes.clear() + + # 重建排序后的复选框 + for module in self.sorted_modules: + self.add_checkbox(self.module_scroll, module, "module") + + def add_checkbox(self, parent, text, type_): + """在指定父组件中添加复选框""" + + def update_filter(): + current = cb.get() + if type_ == "level": + (self.selected_levels.add if current else self.selected_levels.discard)(text) + else: + (self.selected_modules.add if current else self.selected_modules.discard)(text) + self.refresh_logs() + + cb = ctk.CTkCheckBox(parent, text=text, command=update_filter) + cb.select() # 初始选中 + + # 手动同步初始状态到集合(关键修复) + if type_ == "level": + self.selected_levels.add(text) + else: + self.selected_modules.add(text) + + if type_ == "module": + self.module_checkboxes[text] = cb + cb.pack(anchor="w", padx=5, pady=2) + return cb + + def check_filter(self, entry): + """检查日志条目是否符合当前筛选条件""" + level_ok = not self.selected_levels or entry["level"] in self.selected_levels + module_key = self.get_module_key(entry["module"]) + module_ok = not self.selected_modules or module_key in self.selected_modules + return level_ok and module_ok + + def init_text_tags(self): + """初始化日志文本的颜色标签""" + for tag, color in self.color_config.items(): + self.log_text.tag_config(tag, foreground=color) + self.log_text.tag_config("default", foreground=self.color_config["default"]) + + def start_process(self): + """启动日志进程并开始读取输出""" + self.process = subprocess.Popen( + ["nb", "run"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + encoding="utf-8", + errors="ignore", + ) + self.start_btn.configure(state="disabled") + self.stop_btn.configure(state="normal") + threading.Thread(target=self.read_output, daemon=True).start() + + def stop_process(self): + """停止日志进程并清理相关资源""" + if self.process: + try: + if hasattr(self.process, "pid"): + if os.name == "nt": + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], check=True, capture_output=True + ) + else: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + except (subprocess.CalledProcessError, ProcessLookupError, OSError) as e: + print(f"终止进程失败: {e}") + finally: + self.process = None + self.log_queue.queue.clear() + self.start_btn.configure(state="normal") + self.stop_btn.configure(state="disabled") + self.refresh_logs() + + def read_output(self): + """读取日志进程的输出并放入队列""" + try: + while self.process and self.process.poll() is None: + line = self.process.stdout.readline() + if line: + self.log_queue.put(line) + else: + break # 避免空循环 + self.process.stdout.close() # 确保关闭文件描述符 + except ValueError: # 处理可能的I/O操作异常 + pass + + def process_log_queue(self): + """处理日志队列中的日志条目""" + while not self.log_queue.empty(): + line = self.log_queue.get() + self.process_log_line(line) + self.after(100, self.process_log_queue) + + def process_log_line(self, line): + """解析单行日志并更新日志数据和筛选器""" + match = re.match( + r"""^ + (?:(?P